Scaper Granular Synth

by soulwire

HTML

<canvas id="canvas"></canvas>
<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="pauseBtn">Pause Audio</button>
  <button id="startExport">Export WAV</button>
  <a id="downloadLink" style="display: none" download="scaper.wav"
    >Download Recording</a
  >
</div>
<script src="scaper.js"></script>

CSS

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

JavaScript

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

// Audio context setup
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const masterGain = audioCtx.createGain();
const compressor = audioCtx.createDynamicsCompressor();
const analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.85;

const convolver = audioCtx.createConvolver();
const globalDelay = audioCtx.createDelay();
const feedback = audioCtx.createGain();
globalDelay.delayTime.value = 0.25;
feedback.gain.value = 0.4;
globalDelay.connect(feedback).connect(globalDelay);

masterGain.connect(analyser);
analyser.connect(convolver);
convolver.connect(globalDelay);
globalDelay.connect(compressor);
compressor.connect(audioCtx.destination);

// Globals
let sources = [];
let particleSystem = [];
let isDragging = false;
let mouse = { x: 0, y: 0 };

// DOM elements
const fileInput = document.getElementById("fileInput");
const pitchSlider = document.getElementById("pitchShift");
const enableLfo = document.getElementById("enableLfo");
const presetSelect = document.getElementById("presetSelect");
const pauseBtn = document.getElementById("pauseBtn");
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 startExport = document.getElementById("startExport");
const downloadLink = document.getElementById("downloadLink");

// File loading
fileInput.addEventListener("change", async (e) => {
  const files = Array.from(e.target.files);
  for (const file of files) {
    const arrayBuffer = await...