musicSlip / static /scanner-worklet.js
MattSymetry's picture
Upload 11 files
552b5b3 verified
Raw History Blame Contribute Delete
2.69 kB
// PCM sample player for the waveform decoders. The main thread reads a row of
// the slip image back as raw audio samples and posts them here; this just
// buffers them in a ring and plays them at the audio rate. No STFT, no phase
// reconstruction -- the waveform is the signal, so there's nothing to invent.
// Pitch is correct when samples are fed at the AudioContext's sample rate.
// Hann low-pass FIR, used to reconstruct the thermal (1-bit dither) stream.
// Applied over the CONTINUOUS sample ring (no per-row seams), so there is no
// row-boundary buzz. No-op for the gray / digital modes.
const LP_L = 21, LP_HALF = LP_L >> 1;
const LP_W = (() => {
const w = new Float32Array(LP_L); let s = 0;
for (let i = 0; i < LP_L; i++) { w[i] = 0.5 - 0.5 * Math.cos((2 * Math.PI * i) / (LP_L - 1)); s += w[i]; }
for (let i = 0; i < LP_L; i++) w[i] /= s;
return w;
})();
class PCMPlayer extends AudioWorkletProcessor {
constructor() {
super();
this.ring = new Float32Array(1 << 19); // ~0.5 M samples of slack
this.mask = this.ring.length - 1;
this.w = 0; this.r = 0; this.gain = 1.0;
this.lp = false; // thermal reconstruction low-pass
this.port.onmessage = (e) => {
const d = e.data;
if (d.type === "samples") {
const s = d.data, ring = this.ring, mask = this.mask;
for (let i = 0; i < s.length; i++) { ring[this.w & mask] = s[i]; this.w++; }
} else if (d.type === "gain") {
this.gain = d.v;
} else if (d.type === "lowpass") {
this.lp = !!d.on;
} else if (d.type === "reset") {
this.r = this.w; // drop anything buffered
}
};
}
sampleAt(r) {
const ring = this.ring, mask = this.mask;
if (!this.lp) return ring[r & mask];
let acc = 0; // Hann low-pass with look-ahead
for (let t = -LP_HALF; t <= LP_HALF; t++) {
const idx = r + t;
acc += LP_W[t + LP_HALF] * (idx >= 0 && idx < this.w ? ring[idx & mask] : 0);
}
return acc;
}
process(inputs, outputs) {
const ch = outputs[0], out = ch[0], n = out.length, g = this.gain;
// Keep LP_HALF samples of look-ahead available for the low-pass.
const avail = () => this.w - this.r > (this.lp ? LP_HALF : 0);
for (let k = 0; k < n; k++) {
out[k] = avail() ? Math.tanh(this.sampleAt(this.r) * g) : 0;
if (this.r < this.w) this.r++;
}
const cap = (sampleRate * 0.5) | 0; // bound write-ahead latency
if (this.w - this.r > cap) this.r = this.w - cap;
for (let c = 1; c < ch.length; c++) ch[c].set(out);
return true;
}
}
registerProcessor("pcm", PCMPlayer);