Skip to content

Instantly share code, notes, and snippets.

@Nepi24
Created December 6, 2025 01:51
Show Gist options
  • Select an option

  • Save Nepi24/1476b663361bafbf82a5b9768be57ca0 to your computer and use it in GitHub Desktop.

Select an option

Save Nepi24/1476b663361bafbf82a5b9768be57ca0 to your computer and use it in GitHub Desktop.
susurrate
<div id="wave"></div>
<div id="hint">Click on the waveform</div>
const looperProcessorModule = `
class LooperProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{
name: "playbackRate",
defaultValue: 1,
minValue: 0
},
{
name: "loopStart",
defaultValue: 0,
minValue: 0
}
];
}
constructor({processorOptions}) {
super();
this.bufferData = null;
this.loopLength = processorOptions.loopLength;
this.crossfadeLength = processorOptions.crossfadeLength;
this.position = 0;
this.currentLoopStart = -1;
this.nextLoopStart = -1;
this.port.onmessage = this.onmessage.bind(this);
}
onmessage(msg) {
switch (msg.data.msg) {
case 'setBuffer':
this.bufferData = msg.data.data;
break;
}
}
process(inputs, outputs, params) {
if (!this.bufferData) return true;
const playbackRate = params.playbackRate;
const playbackRateARate = playbackRate.length > 1;
const loopStart = params.loopStart;
const loopStartARate = loopStart.length > 1;
const quantumSize = outputs[0][0].length;
for (let s=0 ; s<quantumSize ; s++) {
for (let o=0 ; o<outputs.length ; o++) {
for (let oCh=0 ; oCh<outputs[o].length ; oCh++) {
outputs[o][oCh][s] = 0;
}
}
if (this.currentLoopStart === -1 || this.position > this.currentLoopStart + this.loopLength) {
if (this.nextLoopStart >= 0) {
this.currentLoopStart = this.nextLoopStart;
this.nextLoopStart = -1;
} else {
this.currentLoopStart = loopStartARate ? loopStart[s] : loopStart[0];
}
this.position = this.currentLoopStart;
}
if (this.position > this.currentLoopStart + this.loopLength - this.crossfadeLength) {
let i = this.currentLoopStart + this.loopLength - this.position;
let relI = i / this.crossfadeLength;
if (this.nextLoopStart === -1) {
this.nextLoopStart = loopStartARate ? loopStart[s] : loopStart[0];
}
this.outputInterpolatedSampleValue(this.position, s, Math.sqrt(relI), outputs);
this.outputInterpolatedSampleValue(this.nextLoopStart - i, s, Math.sqrt(1 - relI), outputs);
} else {
this.outputInterpolatedSampleValue(this.position, s, 1, outputs);
}
this.position += playbackRateARate ? playbackRate[s] : playbackRate[0];
}
return true;
}
outputInterpolatedSampleValue(bufferPosition, outputPosition, weight, outputs) {
let minPos = this.loopStart - this.crossfadeLength;
let maxPos = this.loopEnd - 3;
let iPos = Math.floor(bufferPosition);
let frac = 0;
if (iPos < minPos) {
iPos = minPos;
frac = 0;
} else if (iPos > maxPos) {
iPos = maxPos;
frac = 1;
} else {
frac = bufferPosition - iPos;
}
for (let ch=0 ; ch<this.bufferData.length ; ch++) {
const buf = this.bufferData[ch];
const a = buf[iPos - 1];
const b = buf[iPos];
const c = buf[iPos + 1];
const d = buf[iPos + 2];
const cMinusB = c - b;
const sample =
b +
frac * (cMinusB - 0.1666667 * (1.0 - frac) * ((d - a - 3.0 * cMinusB) * frac + (d + 2.0 * a - 3.0 * b)));
for (let o=0 ; o<outputs.length ; o++) {
for (let oCh=ch ; ch<outputs[o].length ; ch += this.bufferData.length) {
outputs[o][oCh][outputPosition] += sample * weight;
}
}
}
}
}
registerProcessor('looper-processor', LooperProcessor);
`;
class LooperNode extends AudioWorkletNode {
static register(audioCtx) {
return audioCtx.audioWorklet.addModule(
`data:text/javascript,${encodeURIComponent(looperProcessorModule)}`
);
}
constructor(loopLength, crossfadeLength, audioCtx) {
super(audioCtx, "looper-processor", {
numberOfInputs: 0,
processorOptions: { loopLength, crossfadeLength },
});
this._buffer = null;
}
get playbackRate() {
return this.parameters.get("playbackRate");
}
get loopStart() {
return this.parameters.get("loopStart");
}
get buffer() {
return this._buffer;
}
set buffer(buffer) {
this._buffer = buffer;
let data = [];
for (let i = 0; i < buffer.numberOfChannels; i++) {
data.push(buffer.getChannelData(0));
}
this.port.postMessage({ msg: "setBuffer", data });
}
}
let audioCtx = new AudioContext(),
comp = audioCtx.createDynamicsCompressor(),
masterGain = audioCtx.createGain(),
ws;
let samplePromise = fetch("https://teropa.info/ext-assets/39914__digifishmusic__katy-sings-laaoooaaa.mp3")
.then((res) => res.arrayBuffer())
.then((buf) => audioCtx.decodeAudioData(buf));
async function init(buffer) {
if (ws) return;
await LooperNode.register(audioCtx);
comp.attack.value = 0.05;
comp.release.value = 0.5;
masterGain.gain.value = 0.75;
ws = WaveSurfer.create({
container: "#wave",
height: window.innerHeight,
barHeight: 0.75,
waveColor: "#99a",
cursorWidth: 2,
cursorColor: "rgba(255, 255, 255, 0)",
progressColor: "rgba(255, 255, 255, 0)",
plugins: [WaveSurfer.regions.create({})],
});
ws.loadDecodedBuffer(buffer);
ws.on("seek", (pos) => {
audioCtx.resume();
comp.connect(masterGain);
masterGain.connect(audioCtx.destination);
startNote(buffer, Math.round(pos * buffer.length));
document.getElementById("hint").classList.add("gone");
});
}
function startNote(sample, offset) {
let playbackRate = 1.0;
let rnd = Math.random();
if (rnd < 0.15) {
playbackRate = 0.25;
} else if (rnd < 0.3) {
playbackRate = 0.5;
} else {
playbackRate += (Math.random() - 0.5) * 0.05;
}
let offsetLfoAmount = 7000;
let offsetLfoFreq = Math.random() / 10;
let loopLength = 1500 + Math.round(Math.random() * 1000);
let crossfadeLength = Math.round(loopLength / 3);
let attackDur = 3.0;
let sustainDur = 10;
let releaseDur = 15;
let envelopeGain = audioCtx.createGain();
envelopeGain.gain.setValueAtTime(0, audioCtx.currentTime);
envelopeGain.gain.linearRampToValueAtTime(
1,
audioCtx.currentTime + attackDur
);
envelopeGain.gain.setValueAtTime(
1,
audioCtx.currentTime + attackDur + sustainDur
);
envelopeGain.gain.exponentialRampToValueAtTime(
0.0001,
audioCtx.currentTime + attackDur + sustainDur + releaseDur
);
envelopeGain.connect(comp);
let looper = new LooperNode(loopLength, crossfadeLength, audioCtx);
looper.buffer = sample;
looper.loopStart.value = offset;
looper.playbackRate.value = playbackRate;
looper.connect(envelopeGain);
let offsetLfo = audioCtx.createOscillator();
offsetLfo.frequency.value = offsetLfoFreq;
let offsetLfoGain = audioCtx.createGain();
offsetLfoGain.gain.value = offsetLfoAmount;
offsetLfo.connect(offsetLfoGain);
offsetLfoGain.connect(looper.loopStart);
offsetLfo.start();
let region = ws.addRegion({
start: (offset / sample.length) * sample.duration,
end: (offset / sample.length) * sample.duration + 0.07,
drag: false,
resize: false,
color: "white",
});
let running = true,
startedAt = audioCtx.currentTime;
let frame = () => {
if (!running) return;
let age =
(audioCtx.currentTime - startedAt) /
((attackDur + sustainDur + releaseDur) * 0.8);
region.update({
color: `rgba(255, 255, 255, ${Math.min(1, Math.max(0, 1 - age))})`,
});
setTimeout(frame);
};
frame();
setTimeout(() => {
envelopeGain.disconnect();
region.remove();
running = false;
}, (attackDur + sustainDur + releaseDur) * 1000);
}
samplePromise.then(init);
<script src="https://unpkg.com/[email protected]/dist/wavesurfer.js"></script>
<script src="https://unpkg.com/[email protected]/dist/plugin/wavesurfer.regions.js"></script>
html,
body {
padding: 0;
margin: 0;
background-color: #111;
}
#hint {
color: white;
position: fixed;
top: 30vh;
left: 0;
width: 100%;
text-align: center;
font-family: sans-serif;
transition: opacity 1s;
opacity: 1;
}
#hint.gone {
opacity: 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment