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