Scaper Particle Engine

by soulwire

HTML

<h1>Scaper Particle Engine</h1>
<div id="controls">
  <input type="file" id="fileInput" accept="audio/*" multiple />
  <button id="startBtn">Start Atmosphere</button>
  <button id="recordBtn">Start Recording</button>
  <button id="downloadBtn" disabled>Download WAV</button>
  <label
    >Reverb Mix
    <input type="range" id="reverbMix" min="0" max="1" step="0.01" value="0.5"
  /></label>
  <label
    >Delay Feedback
    <input
      type="range"
      id="delayFeedback"
      min="0"
      max="0.95"
      step="0.01"
      value="0.6"
  /></label>
  <label
    >Mode
    <select id="modeSelect">
      <option value="ambient">Ambient</option>
      <option value="drone">Drone</option>
      <option value="pulse">Rhythmic Pulse</option>
      <option value="cloud">Grain Cloud</option>
    </select>
  </label>
</div>
<canvas id="canvas" width="800" height="600"></canvas>

CSS

body {
  font-family: sans-serif;
  background: #111;
  color: #eee;
  text-align: center;
  padding: 2rem;
}
input,
button,
label,
select {
  margin: 0.5rem;
  font-size: 1.2rem;
}
canvas {
  display: block;
  margin: 2rem auto;
  background: #000;
  border: 1px solid #333;
}
#controls {
  margin-bottom: 1rem;
}

JavaScript

const ctx = new (window.AudioContext || window.webkitAudioContext)();
const canvas = document.getElementById("canvas");
const canvasCtx = canvas.getContext("2d");
const reverbMixSlider = document.getElementById("reverbMix");
const delayFeedbackSlider = document.getElementById("delayFeedback");
const modeSelect = document.getElementById("modeSelect");

let sources = [];
const particles = [];
let isPlaying = false;

const convolver = ctx.createConvolver();
const globalDelay = ctx.createDelay();
globalDelay.delayTime.value = 0.7;
const feedback = ctx.createGain();
feedback.gain.value = parseFloat(delayFeedbackSlider.value);
globalDelay.connect(feedback).connect(globalDelay);

const masterGain = ctx.createGain();
masterGain.connect(ctx.destination);
convolver.connect(masterGain);
globalDelay.connect(masterGain);

fetch(
  "https://cdn.jsdelivr.net/gh/mdn/webaudio-examples/audio-buffer/concert-crowd.ogg",
)
  .then((res) => res.arrayBuffer())
  .then((buf) => ctx.decodeAudioData(buf))
  .then((decoded) => (convolver.buffer = decoded));

document.getElementById("fileInput").addEventListener("change", async (e) => {
  const files = Array.from(e.target.files);
  for (const file of files) {
    const arrayBuffer = await file.arrayBuffer();
    const buffer = await ctx.decodeAudioData(arrayBuffer);
    const reversed = reverseBuffer(buffer);
    const color = `hsl(${Math.floor(Math.random() * 360)}, 80%, 60%)`;
    sources.push({ buffer, reversedBuffer: reversed, color });
  }
});

document.getElementById("startBtn").addEventListener("click", () => {
  if (!sources.length || isPlaying) return;
  isPlaying = true;
  animate();
  emitLoop();
});

let recorder,
  recordedChunks = [];
const dest = ctx.createMediaStreamDestination();
masterGain.connect(dest);

document.getElementById("recordBtn").onclick = () => {
  if (!recorder || recorder.state === "inactive") {
    recordedChunks = [];
    recorder = new...