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...