PageSourceSearch

https://app.wundermatch.ai/audio-worklet/pcm16-downsampler.js

js wundermatch.ai collected 2026-10-05 13:23:52 UTC 3,734 bytes, 107 lines download raw bytes

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.