// audio-worklet.js — streaming PCM player for the kitsunechat TTS bridge. // // Input: chunks of Float32 mono PCM at the AudioContext's native rate // (already resampled by tts-bridge.js). We queue them and write them to // the output buffer in order, sample-by-sample. // // NO fade-in/out. Pocket TTS emits many small chunks (one per diffusion // generation_step). Applying a per-chunk fade creates audible dips to // silence at every chunk boundary — the "air gaps every 1 second" bug. // Match the tts-site worklet: straight FIFO copy, gapless by construction. // // 'clear' message: drop the queue + reset state (used when active message // changes; stops any stale OLD-gen audio from playing). // 'finish' message: set `finishing = true`; once queue drains, post 'ended'. class StreamingAudioProcessor extends AudioWorkletProcessor { constructor() { super(); this.queue = []; // Float32Array chunks (FIFO, already at native rate) this.offset = 0; // read position in head chunk this.finishing = false; this.framesProcessed = 0; this.lastRmsReport = 0; this.port.onmessage = (e) => { const msg = e.data; if (msg.type === 'chunk') { this.queue.push(msg.samples); } else if (msg.type === 'finish') { this.finishing = true; } else if (msg.type === 'clear') { this.queue = []; this.offset = 0; this.finishing = false; } }; } process(inputs, outputs) { const out = outputs[0][0]; if (!out || out.length === 0) return true; let written = 0; while (written < out.length && this.queue.length > 0) { const chunk = this.queue[0]; const available = chunk.length - this.offset; const needed = out.length - written; const n = Math.min(available, needed); // Straight copy — no envelope. Adjacent chunks play continuously. out.set(chunk.subarray(this.offset, this.offset + n), written); written += n; this.offset += n; if (this.offset >= chunk.length) { this.queue.shift(); this.offset = 0; } } for (let i = written; i < out.length; i++) out[i] = 0; // Diagnostics: post RMS level of the output buffer every ~250ms so the // main thread can confirm audio is being produced (and not silent). this.framesProcessed++; if (this.framesProcessed - this.lastRmsReport >= 12) { this.lastRmsReport = this.framesProcessed; let sumSq = 0; for (let i = 0; i < out.length; i++) sumSq += out[i] * out[i]; const rms = Math.sqrt(sumSq / out.length); const peak = 0; this.port.postMessage({ type: 'level', rms, peak, frame: this.framesProcessed }); } if (this.finishing && this.queue.length === 0) { this.port.postMessage({ type: 'ended' }); this.finishing = false; } return true; } } registerProcessor('streaming-audio', StreamingAudioProcessor);