// 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);