- a resampler, because the rates differ
- a frame aligner, because a resampled buffer does not divide evenly into the wire’s 640-byte frame, and dropping the remainder clips the end of every turn
CallServer. A plugin that has to reimplement them is a plugin the SDK failed.
Both helpers are dependency-free on purpose. Speech at these rates does not need a windowed-sinc filter, and the alternative is putting NumPy or SciPy on the critical path of every audio frame of every call.
Constants
frame_duration_ms
pcm16_rms
DEFAULT_SPEECH_RMS (0.02) and ECHO_BARGE_IN_RMS (0.04) are both numbers on this scale, and a threshold you tune in one place reads the same in the other.
Dependency-free for the same reason the resampler is: this runs on every inbound frame of every call, and NumPy on that path buys microseconds at the cost of a wheel on every deployment. An odd trailing byte is dropped rather than raising.
resample_pcm16
The remainder problem
This is the part that is easy to miss, and it is audible. At 24 kHz to 16 kHz the ratio is 2:3, so a chunk only lands on a 640-byte boundary by luck. Take a realistic provider chunk:
That one divides cleanly. Now the chunk after it, arriving at 7000 bytes of 24 kHz audio, resamples to 4666 bytes: seven whole frames and a 186-byte remainder. Send the seven and drop the remainder and you have lost about 5.8 ms. Do that at every chunk seam for a whole turn and the word endings go clipped and clicky.
FrameAligner exists so the remainder is carried instead of dropped.
FrameAligner
flush() pads rather than drops, because the tail of the last word matters more than a few milliseconds of silence.
FrameAligner(frame_bytes=...) takes a different frame size if you need one, but the wire’s frame is 640 bytes and there is rarely a reason to change it.
reset on barge-in
The full barge-in sequence, in order:cancel_playback() goes first because it is the part the caller can hear. See Barge-in.
A complete outbound path
reset() is what clears it.
Agent audio can be dropped
Everything above gets whole frames onto the wire. The wire can still refuse them. When the socket has more than 1 MB (MAX_AUDIO_BUFFER_BYTES) unflushed, send_audio advances the timeline and returns without sending, and logs at most once every five seconds. The caller hears a gap rather than the call wedging, and the timeline still advances because a dropped frame is a gap in what they hear, not a rewind. Control frames are never shed: a call that cannot be ended is the failure this exists to prevent.
Watch session.buffered_bytes before pushing a continuous stream, and treat a zero as no evidence of backpressure rather than proof of an idle socket, because it reads zero when the transport cannot report it. See Backpressure.
Inbound audio
on_caller_audio receives the caller’s PCM already decoded and already validated: base64 that is not canonical is rejected, and so is a payload that is not a whole number of PCM16 samples. A malformed frame is dropped by the server with a log line and never reaches your handler, so no aligner is needed on the way in.
Resample if your provider wants a different rate, and hand it over without blocking.
Next
Call handler
Where these helpers sit in a real handler.
CallServer
The sequence number and timeline the SDK owns for you.