1// Audio worklet: downmix to mono, linearly resample to 16 kHz, quantize to PCM16 LE, 2// and ship frames via `port.postMessage(ArrayBuffer, [ArrayBuffer])`. 3const TARGET_SAMPLE_RATE = 16000 4 5// Every frame is one WebSocket message, which is one Lambda invocation and one 6// S3 PutObject, so small frames are expensive for nothing: transcription happens 7// once, on 'completed', and frame size has no bearing on what the user waits for. 8// Past ~500 ms the trade-off reverses â a dropped socket costs more speech, and 9// the frame grows against API Gateway's 128 KB cap (20.8 KB here). Matches 10// matcha-jps. 11const FRAME_MS = 500 12const FRAME_SIZE = (TARGET_SAMPLE_RATE * FRAME_MS) / 1000 // 8000 samples 13 14class Pcm16Downsampler extends AudioWorkletProcessor { 15 constructor() { 16 super() 17 // A recording almost never ends on a frame boundary. Without a flush the 18 // final partial frame is discarded on teardown â up to half a second here, 19 // a whole word rather than a tick of silence. 20 this.port.onmessage = (event) => { 21 if (event.data?.type === 'flush') this._flush() 22 } 23 this._inputSampleRate = sampleRate 24 this._ratio = this._inputSampleRate / TARGET_SAMPLE_RATE 25 this._readIndex = 0 26 this._buffer = [] 27 this._frame = new Int16Array(FRAME_SIZE) 28 this._frameOffset = 0 29 } 30 31 process(inputs) { 32 const input = inputs[0] 33 if (!input || input.length === 0) return true 34 const channelCount = input.length 35 const length = input[0].length 36 if (length === 0) return true 37 38 const mono = new Float32Array(length) 39 if (channelCount === 1) { 40 mono.set(input[0]) 41 } 42 else { 43 for (let i = 0; i < length; i++) { 44 let sum = 0 45 for (let c = 0; c < channelCount; c++) sum += input[c][i] 46 mono[i] = sum / channelCount 47 } 48 } 49 50 // Buffered across callbacks so resampling can span chunk boundaries. 51 this._buffer.push(mono) 52 // Combine if we have more than one chunk (keeps resampling simple). 53 if (this._buffer.length > 1) { 54 let total = 0 55 for (const chunk of this._buffer) total += chunk.length 56 const combined = new Float32Array(total) 57 let offset = 0 58 for (const chunk of this._buffer) { 59 combined.set(chunk, offset) 60 offset += chunk.length 61 } 62 this._buffer = [combined] 63 } 64 const source = this._buffer[0] 65 66 // Linear-resample from source at readIndex, emitting PCM16 into the current frame. 67 while (true) { 68 const baseIdx = Math.floor(this._readIndex) 69 const nextIdx = baseIdx + 1 70 if (nextIdx >= source.length) break 71 72 const t = this._readIndex - baseIdx 73 const sample = source[baseIdx] * (1 - t) + source[nextIdx] * t 74 const clamped = Math.max(-1, Math.min(1, sample)) 75 this._frame[this._frameOffset++] = (clamped * 0x7FFF) | 0 76 77 if (this._frameOffset === FRAME_SIZE) this._flush() 78 79 this._readIndex += this._ratio 80 } 81 82 // Drop consumed samples to keep memory bounded. 83 const consumed = Math.floor(this._readIndex) 84 if (consumed > 0 && consumed < source.length) { 85 const remaining = source.subarray(consumed) 86 this._buffer = [new Float32Array(remaining)] 87 this._readIndex -= consumed 88 } 89 else if (consumed >= source.length) { 90 this._buffer = [] 91 this._readIndex = 0 92 } 93 94 return true 95 } 96 97 /** Emit whatever is buffered, whether or not it fills a frame. */ 98 _flush() { 99 if (this._frameOffset === 0) return 100 const out = new ArrayBuffer(this._frameOffset * 2) 101 new Int16Array(out).set(this._frame.subarray(0, this._frameOffset)) 102 this.port.postMessage(out, [out]) 103 this._frameOffset = 0 104 } 105} 106 107registerProcessor('pcm16-downsampler', Pcm16Downsampler)
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.