Scaper-like Atmosphere Generator

by soulwire

HTML

<h1>Scaper-like Atmosphere Generator</h1>
  <input type="file" id="fileInput" accept="audio/*">
  <button id="startBtn">Start Atmosphere</button>
  <label>Grain Size (ms): <input type="range" id="grainSize" min="20" max="500" value="200"></label>
  <label>Density (grains/sec): <input type="range" id="density" min="1" max="30" value="10"></label>
  <label>Filter Cutoff (Hz): <input type="range" id="cutoff" min="200" max="12000" value="1000"></label>
  <canvas id="visualizer" width="800" height="200"></canvas>

CSS

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

JavaScript

const ctx = new (window.AudioContext || window.webkitAudioContext)();
    let buffer = null;
    let isPlaying = false;
    let intervalId = null;

    const grainSizeSlider = document.getElementById('grainSize');
    const densitySlider = document.getElementById('density');
    const cutoffSlider = document.getElementById('cutoff');

    const analyser = ctx.createAnalyser();
    const canvas = document.getElementById('visualizer');
    const canvasCtx = canvas.getContext('2d');

    const reverb = ctx.createConvolver();
    const delay = ctx.createDelay();
    delay.delayTime.value = 0.4;
    const feedback = ctx.createGain();
    feedback.gain.value = 0.5;

    delay.connect(feedback).connect(delay);

    // Load simple impulse response for reverb
    fetch('https://cdn.jsdelivr.net/gh/mdn/webaudio-examples/audio-buffer/concert-crowd.ogg')
      .then(response => response.arrayBuffer())
      .then(data => ctx.decodeAudioData(data))
      .then(decoded => reverb.buffer = decoded);

    document.getElementById('fileInput').addEventListener('change', async (e) => {
      const file = e.target.files[0];
      const arrayBuffer = await file.arrayBuffer();
      buffer = await ctx.decodeAudioData(arrayBuffer);
    });

    document.getElementById('startBtn').addEventListener('click', () => {
      if (!buffer || isPlaying) return;
      isPlaying = true;
      const interval = 1000 / parseInt(densitySlider.value);
      intervalId = setInterval(() => playGrain(buffer), interval);
      visualize();
    });

    function playGrain(buffer) {
      const grainSize = parseInt(grainSizeSlider.value) / 1000;
      const offset = Math.random() * (buffer.duration - grainSize);

      const source = ctx.createBufferSource();
      source.buffer = buffer;

      const gain = ctx.createGain();
      gain.gain.setValueAtTime(0, ctx.currentTime);
      gain.gain.linearRampToValueAtTime(1, ctx.currentTime + 0.01);
     ...