Architecture

Audio capture pipeline

How raw microphone audio becomes a 16 kHz PCM16 WAV file ready for transcription — inside an AudioWorklet, streamed over IPC, and buffered in the main process.

Target rate

16 kHz mono

Chunk size

1600 samples · 100 ms

Sample format

Int16 (PCM16)

Capture in an AudioWorklet

The overlay opens the microphone with getUserMedia (mono, with echo cancellation, noise suppression, and auto-gain), then routes it through a Web Audio graph into a custom AudioWorkletProcessor that runs on the audio thread.

getUserMedia~48 kHz mono
GainNodeunity gain
AudioWorkletresample + quantize
postMessageInt16 chunks
muted sinkkeeps graph alive
The Web Audio graph. A muted sink keeps the graph pulling audio without any audible loopback.

Why the worklet code is inlined

The processor is registered from an inline blob URL rather than a separate file. Loading a worklet module by file path breaks inside an asar-packaged Electron build, so the code ships as a string and is turned into a Blob at runtime.

Resampling & quantization

The hardware captures at roughly 48 kHz, but Whisper wants 16 kHz. The worklet linearly interpolates samples down to the target rate, clamps each to [-1, 1], and quantizes to signed 16-bit. Samples accumulate into a 1600-sample buffer — 100 ms of audio — which is posted to the main thread whenever it fills.

ParameterValueNotes
TARGET_SAMPLE_RATE16000Whisper's expected input rate.
CHUNK_SIZE1600Samples per chunk — exactly 100 ms at 16 kHz.
ratiosampleRate / 16000Decimation ratio; the read position advances by this each output sample.
encodingInt16LENegative samples scaled by 0x8000, positive by 0x7FFF.

Streaming & buffering

Each 100 ms chunk is sent to the main process over the realtime:audio-chunkchannel. Despite the “realtime” naming, the main process simply buffers these chunks in an array — nothing is sent to Groq until you stop.

Naming vs behavior

Several modules and IPC channels are named realtime for historical reasons (they once wrapped a streaming websocket). The current implementation buffers locally and issues a single transcription request on stop. This documentation describes the buffering behavior, which is what actually runs.

Building the WAV on stop

When recording stops, all buffered chunks are concatenated and a 44-byte WAV header is prepended to produce a standard mono PCM16 WAV in memory. Recording duration is derived directly from the byte length:

duration from PCM byte length
const durationSec = mergedPcm.length / 2 / 16000;
// 2 bytes per Int16 sample, 16000 samples per second

That WAV is what gets uploaded to Groq. Continue to Transcription for what happens next.