TAI BUI
Back to curriculum
BuildPython

Real-Time Audio Processing

Batch pipelines process a file. Real-time pipelines have to finish the current 20 milliseconds before the next 20 arrive. Every conversational AI, broadcast studio, and telephony bot lives or dies by that budget.

TIME: ~75 minutesPREREQUISITES: Phase 6 · 02 (Spectrograms), Phase 6 · 04 (ASR), Phase 6 · 07 (TTS)

Learning Objectives

  • Explain the end-to-end latency budget for a hear-to-speak voice pipeline, and name the three techniques that took cascaded systems from ~2500 ms down to today's 200-400 ms.
  • Build a ring buffer and an energy-based VAD gate, and know when to swap the heuristic for a real model like Silero VAD.
  • Implement a barge-in handler that cancels in-flight TTS generation the moment the user starts talking over it.
  • Pick a complete 2026 real-time voice stack (transport, VAD, streaming ASR, LLM, TTS) for a target latency, and avoid the buffering, threading, and echo pitfalls that blow the budget.

The Problem

You want a voice assistant that feels alive, not like a phone tree. Human turn-taking latency, the silence-to-response gap in an ordinary conversation, runs about 230 ms. Cross 500 ms and the assistant starts sounding robotic; cross 1500 ms and it sounds broken. That's the bar. Here's the budget for a full hear → understand → respond → speak loop in 2026:

StageBudget
Mic → buffer20 ms
VAD10 ms
ASR (streaming)150 ms
LLM (first token)100 ms
TTS (first chunk)100 ms
Render → speaker20 ms
Total~400 ms

Moshi (Kyutai, 2024) hit 200 ms full duplex. GPT-4o-realtime (2024) runs around 320 ms. Cascaded pipelines in 2022 shipped at 2500 ms, roughly 10× slower. Three techniques closed that gap: streaming everywhere, asynchronous pipelining with partial results, and interruptible generation.

Warm-up0/3
1. A mono audio signal is sampled at 16 kHz. How many samples does it contain per second?Warm-up
A one-second mono waveform with dots marking individual samples along it, a time axis from 0 s to 1 s, and the sample rate 16 kHz labelled.
Schematic. Only 41 dots are drawn so they stay visible; the dot count is not the real count.
2. A speech recognizer transcribes a finished recording. What can it use that a recognizer working on a live microphone cannot?Warm-up
Two inputs feed the same speech recognizer: a finished recording saved on disk and a live microphone with someone speaking now. A question mark sits between them and the recognizer.
3. In a first-in, first-out (FIFO) queue, which item does a read return first?Warm-up
Four numbers, 5, 8, 2 and 6, are added to a first-in, first-out queue one at a time in that order. A single read() call returns a value marked with a question mark.
Illustrative values.
Check: The Problem0/2
1. Which three techniques closed the gap between 2022's 2500 ms cascaded pipelines and today's 200-400 ms systems?Define
Two bars on one scale: 2022 cascaded pipelines at 2500 ms, and today's systems at 200 to 400 ms, with a question mark below asking what made the difference.
Figures from the lesson's Problem section.
2. A team's stage budgets are mic to buffer 20 ms, VAD 10 ms, streaming ASR 150 ms, LLM first token 100 ms, TTS first chunk 100 ms, render to speaker 20 ms. What total latency do they add up to?Apply
A table of six stage budgets: mic to buffer 20 ms, VAD 10 ms, streaming ASR 150 ms, LLM first token 100 ms, TTS first chunk 100 ms, render to speaker 20 ms. The total row shows a question mark.
Budgets from the question.

The Concept

Streaming audio pipeline with ring buffer, VAD gate, interruption

The pipeline is seven pieces. In normal operation, a frame lands in the ring buffer, VAD checks it, and streaming ASR transcribes it; interruption can cut in at any point, and transport plus the jitter buffer handle getting the audio there in the first place.

Frame / chunk / window. Real-time audio flows as fixed-size blocks, not a continuous stream. The common choice is 20 ms, 320 samples at 16 kHz, and everything downstream has to keep pace with that cadence.

Ring buffer. A fixed-size circular buffer: a producer thread writes new frames, a consumer thread reads them, and nothing gets allocated in the hot path. Size it as maximum latency times sample rate. A 2-second ring at 16 kHz holds 32,000 samples.

VAD (Voice Activity Detection). A classifier, model or simple energy heuristic, that labels each frame speech or non-speech. It gates downstream work whenever nobody's speaking. Silero VAD 4.0 (2024) runs under 1 ms per 30 ms frame on CPU; webrtcvad is the older alternative.

Streaming ASR. A model that emits partial transcripts as audio arrives, rather than waiting for the whole utterance. Parakeet-CTC-0.6B in streaming mode (NeMo, 2024) hits 2–5% WER at 320 ms latency. Whisper-Streaming (Macháček et al., 2023) chunks Whisper into something close to streaming, at roughly 2 s of latency.

Interruption. When the user speaks while the assistant is talking, you must detect the barge-in, stop the TTS, and discard the remaining LLM output, all within 100 ms, or the user perceives a deaf assistant.

WebRTC Opus transport. 20 ms frames, 48 kHz, adaptive bitrate from 8 to 128 kbps. This is the standard for browser and mobile; LiveKit, Daily.co, and Pion are the 2026 stacks for building on top of it.

Jitter buffer. A small queue that reorders and smooths network packets that arrive out of order or late: too small and you get audible gaps, too large and you add latency. 60–80 ms is typical.

Common gotchas

  • Thread contention. Python's GIL plus a heavy model can starve the audio thread. Use a C-callback audio library (sounddevice, PortAudio) and keep Python off the hot path.
  • Sample-rate conversion latency. Resampling inside the pipeline adds 5–20 ms. Resample upfront, or use a zero-latency resampler (PolyPhase, soxr_hq).
  • TTS priming. Even fast TTS like Kokoro has a 100–200 ms warm-up on the first request. Cache the model and warm it with a dummy run before the first real turn.
  • Echo cancellation. Without AEC, TTS output re-enters the mic and triggers ASR on the bot's own voice. WebRTC AEC3 is the open-source default.
Check: The Concept0/18
1. What is a ring buffer in a real-time audio pipeline?Define
A circle of eight slots. Five slots hold frames f1 to f5, three are hollow and free. One arrow marks the slot for the next write and another marks the slot for the next read.
Schematic. Eight slots are drawn for readability; a real buffer holds thousands of samples.
2. What does a voice activity detector (VAD) do?Define
Twelve audio frames drawn as bars of different heights, standing on a baseline. Under each frame is a question mark for its VAD label, and an arrow leads from a VAD box to a box called next stage.
Typed schematic; amplitudes are illustrative.
3. What is streaming ASR?Define
A waveform split into six chunks by dashed lines, with a time arrow under it. An ASR model box in streaming mode points to a box reading text output so far with a question mark.
Typed schematic.
4. What is a jitter buffer?Define
Five numbered packets leave a sender at even spacing and arrive at a receiver at uneven times. Below, a chain reads network, jitter buffer, decoder, speaker, with the jitter buffer highlighted and the label jitter buffer does with a question mark.
Typed schematic; arrival times are illustrative.
5. What does AEC (acoustic echo cancellation) do?Define
Block diagram: TTS plays through a speaker, and a dashed line labelled sound in the room runs from the speaker to the microphone. The user also reaches the microphone. The microphone signal goes through a highlighted AEC box marked does what with a question mark, then on to ASR.
Typed schematic.
6. What is barge-in?Define
A timeline with two lanes. The bot audio lane has a long bar for a reply playing. The user mic lane has a shorter bar starting partway through the bot's bar, with a dashed vertical line at its start and the label this moment is called with a question mark.
Typed schematic.
7. What does full duplex mean for a voice system?Define
A caller box and a voice bot box joined by two arrows, one carrying the user's voice in and the other carrying the bot's voice out. A label reads this mode is called with a question mark.
8. What latency and word error rate does the lesson report for Parakeet-CTC-0.6B in streaming mode?Define
An excerpt of the lesson's NeMo code loading nvidia/parakeet-ctc-0.6b and calling transcribe_streaming on each audio chunk. Below are two empty readouts, word error rate and latency, each marked with a question mark.
Excerpt of the lesson's Step 3 code; the comment lines are omitted.
9. What jitter buffer size does the lesson call typical?Define
Six packets drawn as boxes at uneven spacing along an arrival-time axis, with a bracket spanning all of them labelled jitter buffer size with a question mark in milliseconds.
Typed schematic; spacing is illustrative.
10. What does the lesson say about WebRTC Opus transport for voice apps?Define
A browser voice app joined to a voice server by an arrow labelled WebRTC Opus transport. A small table lists frame length, sample rate and bitrate, each with a question mark as its value.
11. What do the lesson's gotchas say about sample-rate conversion latency inside the pipeline?Define
A chain from WebRTC audio at 48 kHz, through a highlighted resample step inside the pipeline, to VAD and ASR at 16 kHz, with a label added delay with a question mark in milliseconds.
Rates from the lesson's Concept section.
12. What does the capacity of a ring buffer set for the pipeline?Explain
A chain microphone, ring buffer, VAD, with the ring buffer labelled capacity equals C. Below is the lesson's RingBuffer class showing the __init__ signature with its body elided, and a level method.
Class copied from the lesson's Build It section; __init__ body elided.
13. Why does the pipeline run VAD before streaming ASR instead of sending every frame to ASR?Explain
A waveform with three bursts of sound separated by quiet stretches, cut into frames by small tick marks. Below, a chain reads audio frames, VAD, streaming ASR with the VAD box highlighted.
Typed schematic; the waveform is illustrative.
14. Why does the lesson recommend a C-callback audio library like sounddevice or PortAudio instead of keeping heavy models on the audio thread?Explain
Two lanes in one Python process. The audio callback lane shows twelve dashed boxes, one due every 20 ms. The heavy model call lane shows a long bar of model inference overlapping the middle of that time. A label asks what happens to the due blocks.
Typed schematic; durations are illustrative.
15. Why must a barge-in also discard the remaining LLM output, not just stop the speaker?Explain
A pipeline LLM, TTS, speaker. The LLM is annotated still generating text, TTS is annotated synthesizing, and the speaker has a red cross and the note playback stopped. A dashed arrow from a box reading user starts speaking points at the speaker.
Typed schematic.
16. What happens without echo cancellation, per the lesson's gotchas section?Explain
Two rows of boxes. One has TTS feeding a speaker. The other has a microphone, a dashed box reading no AEC, VAD and streaming ASR, with an arrow to a label transcript contains with a question mark.
Typed schematic.
17. Silero VAD works on 30 ms frames at 16 kHz. How many samples is one such frame, given that a 20 ms frame is 320 samples?Apply
Two blocks drawn in proportion to duration. A 20 ms frame is labelled 320 samples, and a longer 30 ms frame used by Silero VAD is labelled with a question mark for its sample count. The sample rate is 16 kHz.
20 ms = 320 samples is given in the question.
18. Your voice bot's very first reply after a cold start has an extra 150 ms of dead air compared to every reply after that. Per the lesson's gotchas, what's going on?Analyze
Three bars of dead air before replies. Reply 1, after a cold start, has a usual-delay bar plus an extra block of 150 ms. Replies 2 and 3 have only the usual-delay bar.
Typed schematic; only the 150 ms figure comes from the question.

Build It

Step 1: ring buffer

python
import collections
 
class RingBuffer:
    def __init__(self, capacity):
        self.buf = collections.deque(maxlen=capacity)
    def write(self, frame):
        self.buf.extend(frame)
    def read(self, n):
        return [self.buf.popleft() for _ in range(min(n, len(self.buf)))]
    def level(self):
        return len(self.buf)

Capacity sets your maximum buffering latency: 32,000 samples at 16 kHz buys you 2 seconds.

Step 2: VAD gate

python
def simple_energy_vad(frame, threshold=0.01):
    return sum(x * x for x in frame) / len(frame) > threshold ** 2

In production, swap this for Silero VAD:

python
import torch
vad, _ = torch.hub.load("snakers4/silero-vad", "silero_vad")
is_speech = vad(torch.tensor(frame), 16000).item() > 0.5

Step 3: streaming ASR

Same idea as the VAD swap: start from an interface, then point it at a real streaming model. Parakeet emits partial text on every chunk instead of waiting for silence, which is what lets the pipeline show words on screen while the user is still talking.

python
# Parakeet-CTC-0.6B streaming via NeMo
from nemo.collections.asr.models import EncDecCTCModelBPE
asr = EncDecCTCModelBPE.from_pretrained("nvidia/parakeet-ctc-0.6b")
# chunk_ms=320 ms, look_ahead_ms=80 ms
for chunk in audio_stream():
    partial_text = asr.transcribe_streaming(chunk)
    print(partial_text, end="\r")

Step 4: interruption handler

python
class Dialog:
    def __init__(self):
        self.tts_task = None
 
    def on_user_speech(self, frame):
        if self.tts_task and not self.tts_task.done():
            self.tts_task.cancel()   # barge-in
        # then feed to streaming ASR
 
    def on_final_user_utterance(self, text):
        self.tts_task = asyncio.create_task(self.reply(text))
 
    async def reply(self, text):
        async for tts_chunk in llm_then_tts(text):
            speaker.write(tts_chunk)

This hinges on async I/O and cancellable TTS streaming; calling peerconnection.stop() on the WebRTC audio track is the canonical way to do it.

Check: Build It0/6
1. How large does the lesson size a ring buffer to hold 2 seconds of audio at 16 kHz?Apply
A wide bar labelled 2 seconds of audio at 16 kHz with a bracket under it, and a label capacity with a question mark in samples.
2. A RingBuffer(capacity=4) gets write([1, 2, 3]) then write([4, 5, 6]). What does read(2) return?Apply
Code showing a RingBuffer of capacity 4 followed by write of 1, 2, 3, write of 4, 5, 6 and read of 2, plus the lesson's write and read methods, and a label read(2) returns with a question mark.
Methods from the lesson; __init__ omitted.
3. What does simple_energy_vad actually check to decide a frame is speech?Apply
A frame of samples enters simple_energy_vad with threshold 0.01 and comes out as speech true or false. Below is only the function signature with its body elided, and a label what is checked to decide with a question mark.
Signature copied from the lesson; body elided.
4. simple_energy_vad is called with the default threshold on a frame of four samples that are each 0.005. What does it return?Apply
Code that builds a frame of four samples, each 0.005, and calls simple_energy_vad with the default threshold. The signature shows threshold equal to 0.01, and a label returns with a question mark.
Call written for the question; signature from the lesson.
5. In the Dialog class's on_user_speech method, what happens if a TTS task is still running when the user starts speaking?Apply
The lesson's Dialog class with the body of on_user_speech elided, and below it two boxes: tts_task still running and a user frame with speech detected, then a label what happens with a question mark.
Class skeleton copied from the lesson; method body elided.
6. You must make a TTS-speaking bot stop within 100 ms when the user talks over it. Which design fits?Design
A timeline with a bot audio bar and a user mic bar that starts partway through it. Dashed lines mark t equals 0 at the start of user speech and t equals 100 ms later, with a label asking for the design that stops playback by then.
Typed schematic. 100 ms is the requirement from the question.

Use It

Here's the 2026 stack, layer by layer:

LayerPick
TransportLiveKit (WebRTC) or Pion (Go)
VADSilero VAD 4.0
Streaming ASRParakeet-CTC-0.6B or Whisper-Streaming
LLM first-tokenGroq, Cerebras, vLLM-streaming
Streaming TTSKokoro or ElevenLabs Turbo v2.5
Echo cancelWebRTC AEC3
End-to-end nativeOpenAI Realtime API or Moshi
Check: Use It0/3
1. Per the lesson's Use It table, what's picked for the LLM first-token stage and for streaming TTS?Define
Five layer boxes in a row: transport, VAD, streaming ASR, LLM first-token and streaming TTS. The last two are highlighted, each with a label pick with a question mark beneath it.
Layer names from the lesson's Use It table.
2. Per the lesson's Use It table, what's the pick for an end-to-end native solution, as opposed to a cascaded pipeline?Define
Two designs. A cascaded row of five boxes: transport, VAD, ASR, LLM, TTS. Below it, a single box reading audio in, one model, audio out, with a label pick with a question mark.
Typed schematic.
3. You must ship a browser voice agent that answers in about 400 ms. Which stack choice fits that target?Design
A browser box followed by five dashed stage boxes, transport, VAD, ASR, LLM and TTS, each with a question mark beneath, and a bracket spanning the stages labelled whole loop about 400 ms.
Typed schematic.

Pitfalls

  • Buffering 500 ms to be safe. The buffer is your latency floor. Shrink it.
  • Not pinning threads. Give the audio callback a lower priority than the UI thread and you get glitches under load.
  • TTS chunks too small. Sub-200 ms chunks make vocoder artifacts audible. 320 ms chunks are the sweet spot.
  • No jitter buffer. Real networks are jittery. Skip the smoothing and you get pops.
  • Single-shot error handling. Audio pipelines must be crash-proof. One exception kills the session.
Check: Pitfalls0/6
1. A single unhandled exception fires somewhere in your audio pipeline mid-call. Per the lesson's pitfalls section, what happens next?Define
A call timeline. A green bar for the call running normally ends at a red vertical line marking one unhandled exception. After it is an empty dashed region with a question mark.
Typed schematic.
2. Why does the lesson treat buffering an extra 500 ms 'to be safe' as a mistake rather than a safety margin?Explain
A row of six pipeline stages with the lesson's budgets: buffer 20 ms, VAD 10 ms, ASR 150 ms, LLM 100 ms, TTS 100 ms, speaker 20 ms. The first, buffer, is dashed and highlighted, with a note that the team adds 500 ms there to be safe.
Stage budgets from the lesson's Problem section.
3. What does the pitfalls section warn happens if your audio callback runs at a lower thread priority than the UI?Explain
Two boxes: a UI thread with higher priority and an audio callback with lower priority, an arrow between them, notes that the UI thread runs UI work and a block is due every 20 ms, and a label under load the callback with a question mark.
Typed schematic.
4. Your streaming TTS emits 100 ms chunks to shave a little latency, and users start hearing warbly, robotic artifacts in the voice. What does the pitfalls section say is going on?Analyze
A waveform with ten chunks of 100 ms each marked by dashed lines and numbered 1 to 10, covering one second, and a note that users report a warbly, robotic voice.
Typed schematic; the waveform is illustrative.
5. Your voice agent works fine over ethernet in testing, but mobile users report occasional pops and clicks in the assistant's speech. What does the pitfalls section say is the likely cause?Analyze
Two rows, each with an agent sending audio to a client. In the ethernet row the playback waveform is smooth and labelled speech sounds fine. In the mobile row three sharp spikes interrupt the waveform, labelled occasional pops and clicks.
Typed schematic.
6. A demo glitches, so the team raises the buffer from 40 ms to 500 ms. The glitch vanishes but conversations now feel sluggish. What went wrong?Analyze
Two bars: the buffer before at 40 ms, with a demo that glitches, and the buffer after at 500 ms, with the glitch gone but conversations feeling sluggish.
Values from the question.

Ship It

Save this as outputs/skill-realtime-pipeline.md: a real-time audio pipeline design with a concrete latency budget for every stage.

Author's Note

The thing that makes real-time audio different from every other latency problem in this curriculum: you don't measure the bug, you feel it. A 300 ms delay in a batch job is a number in a log. A 300 ms delay before your voice assistant answers is a silence that feels wrong before you've opened a profiler. That's why teams reach for a bigger buffer the first time something glitches. It works for exactly one release, because the extra buffer was masking some other bug, and now your latency floor is permanently worse. Build the budget table from the Problem section before you write a line of pipeline code. Know which 20 ms you're spending on what, or you'll find out the hard way, mid-demo.

Exercises

  1. Easy. Run code/main.py. Simulates a ring buffer + energy VAD; prints stage latencies for a fake 10-second stream.
  2. Medium. Using sounddevice, build a passthrough loop that processes your mic in 20 ms frames and prints VAD state at each frame.
  3. Hard. Build a full duplex echo test with aiortc: browser → WebRTC → Python → WebRTC → browser. Measure glass-to-glass latency with a 1 kHz pulse.

Key Terms

TermWhat people sayWhat it actually means
Ring bufferThe circular queueFixed-size, lock-free (or SPSC-locked) FIFO for audio frames.
VADSilence gateModel or heuristic marking speech vs non-speech.
Streaming ASRReal-time STTEmits partial text as audio arrives; bounded lookahead.
Jitter bufferNetwork smootherQueue reordering out-of-order packets; 60–80 ms typical.
AECEcho cancellationSubtracts speaker-to-mic feedback path.
Barge-inUser interruptSystem detects user speech mid-TTS; must cancel playback.
Full duplexSimultaneous both waysUser and bot can talk at the same time; Moshi is full duplex.

Further Reading

Code

Ships With