Scaper Web Audio Particle Engine

by soulwire

HTML

<div id="ui">
  <label><input type="file" id="fileInput" multiple /> Load Audio</label>
  <label
    >Mode
    <select id="mode">
      <option value="ambient">Ambient</option>
      <option value="drone">Drone</option>
      <option value="pulse">Pulse</option>
      <option value="cloud">Cloud</option>
    </select>
  </label>
  <label
    >Preset
    <select id="presetSelect">
      <option value="default">Default</option>
      <option value="spaceEcho">Space Echo</option>
      <option value="whispers">Whispers</option>
      <option value="glimmer">Glimmer</option>
    </select>
  </label>
  <label
    >Pitch Shift
    <input type="range" id="pitchShift" min="0.5" max="2" step="0.01" value="1"
  /></label>
  <label>Enable LFO <input type="checkbox" id="enableLfo" /></label>
  <label
    >Grain Size
    <input
      type="range"
      id="grainSizeSlider"
      min="0.05"
      max="1"
      step="0.01"
      value="0.2"
  /></label>
  <label
    >Variation
    <input
      type="range"
      id="variationSlider"
      min="0"
      max="1"
      step="0.01"
      value="0.3"
  /></label>
  <label
    >Density
    <input
      type="range"
      id="densitySlider"
      min="1"
      max="100"
      step="1"
      value="40"
  /></label>
  <label
    >Reverb Mix
    <input
      type="range"
      id="reverbMixSlider"
      min="0"
      max="1"
      step="0.01"
      value="0.5"
  /></label>
  <label
    >Delay Feedback
    <input
      type="range"
      id="delayFeedbackSlider"
      min="0"
      max="1"
      step="0.01"
      value="0.4"
  /></label>
  <button id="startExport">Export WAV</button>
  <a id="downloadLink" style="display: none" download="scaper.wav"
    >Download Recording</a
  >
</div>
<canvas id="canvas"></canvas>

CSS

body {
  margin: 0;
  font-family: sans-serif;
  background: #000;
  color: white;
  overflow: hidden;
}
#ui {
  position: absolute;
  top: 10px;
  left: 10px;
  background: rgba(0, 0, 0, 0.6);
  padding: 10px;
  border-radius: 8px;
  z-index: 10;
}
label {
  display: block;
  margin: 4px 0;
  font-size: 12px;
}
input,
select {
  width: 100%;
}
canvas {
  display: block;
}

JavaScript

const canvas = document.getElementById("canvas");
const ctx2d = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const masterGain = audioCtx.createGain();
const convolver = audioCtx.createConvolver();
const globalDelay = audioCtx.createDelay();
const feedback = audioCtx.createGain();
const compressor = audioCtx.createDynamicsCompressor();
const analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.9;

globalDelay.delayTime.value = 0.4;
feedback.gain.value = 0.4;
globalDelay.connect(feedback).connect(globalDelay);

convolver.connect(masterGain);
globalDelay.connect(masterGain);

masterGain.connect(compressor).connect(audioCtx.destination);

masterGain.connect(analyser);
analyser.connect(compressor);

let sources = [];

const particleSystem = [];
const colors = ["#77f", "#f77", "#7f7", "#ff7", "#7ff"];

const fileInput = document.getElementById("fileInput");
const pitchSlider = document.getElementById("pitchShift");
const enableLfo = document.getElementById("enableLfo");
const presetSelect = document.getElementById("presetSelect");
const modeSelect = document.getElementById("mode");
const grainSizeSlider = document.getElementById("grainSizeSlider");
const variationSlider = document.getElementById("variationSlider");
const densitySlider = document.getElementById("densitySlider");
const reverbMixSlider = document.getElementById("reverbMixSlider");
const delayFeedbackSlider = document.getElementById("delayFeedbackSlider");
const startExportBtn = document.getElementById("startExport");
const downloadLink = document.getElementById("downloadLink");

fileInput.addEventListener("change", async (e) => {
  for (const file of e.target.files) {
    const buffer = await file.arrayBuffer();
    const audioBuffer = await audioCtx.decodeAudioData(buffer);
    const...