/** * pcm-capture-processor.js — AudioWorklet processor pour T2 Live (Sprint 6b). * * Capture du micro à `sampleRate` natif du navigateur (typiquement 48 kHz), * rééchantillonnage vers 16 kHz si nécessaire, conversion Float32 → Int16 * little-endian, envoi par chunks de ~4096 samples (≈ 256 ms à 16 kHz). * * Format de sortie attendu par Gemini Live API : * PCM brut, 16 kHz, 16 bits, little-endian, mono. * * Le rééchantillonnage utilise une interpolation linéaire — équivalent * à `resample16kTo24k` côté audio-utils.ts mais en sens inverse. * * Vanille JS (pas TS) : les AudioWorklet processors s'exécutent dans un * scope global isolé qui ne peut pas importer depuis le bundle TS. */ const TARGET_SAMPLE_RATE = 16000 const CHUNK_SIZE_16K = 4096 // ≈ 256 ms à 16 kHz class PcmCaptureProcessor extends AudioWorkletProcessor { constructor() { super() this.buffer16k = new Float32Array(0) } /** * Rééchantillonne un Float32 du sample rate source vers 16 kHz par * interpolation linéaire. Si srcRate === 16000, no-op. */ resampleTo16k(input, srcRate) { if (srcRate === TARGET_SAMPLE_RATE) return input const ratio = TARGET_SAMPLE_RATE / srcRate const outLength = Math.floor(input.length * ratio) const out = new Float32Array(outLength) for (let i = 0; i < outLength; i++) { const srcIndex = i / ratio const srcFloor = Math.floor(srcIndex) const srcCeil = Math.min(srcFloor + 1, input.length - 1) const frac = srcIndex - srcFloor out[i] = input[srcFloor] * (1 - frac) + input[srcCeil] * frac } return out } process(inputs) { const input = inputs[0] if (!input || !input[0]) return true const channelData = input[0] // mono // Rééchantillonner d'abord vers 16 kHz puis accumuler. // `sampleRate` est une variable globale du scope AudioWorklet (Web Audio spec). const resampled = this.resampleTo16k(channelData, sampleRate) const newBuffer = new Float32Array(this.buffer16k.length + resampled.length) newBuffer.set(this.buffer16k) newBuffer.set(resampled, this.buffer16k.length) this.buffer16k = newBuffer while (this.buffer16k.length >= CHUNK_SIZE_16K) { const chunk = this.buffer16k.slice(0, CHUNK_SIZE_16K) this.buffer16k = this.buffer16k.slice(CHUNK_SIZE_16K) // Float32 [-1, 1] → Int16 PCM little-endian const pcm = new ArrayBuffer(chunk.length * 2) const view = new DataView(pcm) for (let i = 0; i < chunk.length; i++) { const s = Math.max(-1, Math.min(1, chunk[i])) view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7fff, true) } this.port.postMessage(pcm, [pcm]) } return true } } registerProcessor('pcm-capture-processor', PcmCaptureProcessor)