Skip to content

fix(voice): scope forwardAudio playback-started listener to its own segment - #1760

Merged
chenghao-mou merged 1 commit into
1.5.0from
fix/forward-audio-playback-started-scope
Jun 11, 2026
Merged

fix(voice): scope forwardAudio playback-started listener to its own segment#1760
chenghao-mou merged 1 commit into
1.5.0from
fix/forward-audio-playback-started-scope

Conversation

@chenghao-mou

Copy link
Copy Markdown
Member

Summary

Fixes an interruption-time crash where the interrupted speech and the new response speech got cross-wired through the shared audio output:

  • RtcError: InvalidState - sample_rate and num_channels don't match thrown from AudioSource.captureFrame during forwardAudio.
  • Accompanied by playback finished before speech started and playback_finished called more times than playback segments were captured.

Root cause

When a speech is interrupted, the scheduling loop returns from waitIfNotInterrupted immediately and authorizes the next speech while the interrupted segment's pipelineReply is still tearing down (cancelAndWaitclearBufferwaitForPlayout) on the shared audioOutput.

The new segment's forwardAudio registers its playback_started listener on that shared output before reading its first frame. A stray PLAYBACK_STARTED emitted by the interrupted segment's teardown then resolves the new segment's firstFrameFut prematurely. Because resampler creation is gated on !firstFrameFut.done, the resampler is skipped and an unresampled 16 kHz TTS frame is pushed to the 24 kHz AudioSourceRtcError.

Fix

Scope the listener to the segment: forwardAudio only resolves firstFrameFut after it has captured its own first frame (hasCapturedOwnFrame). Stray events from an interrupted overlapping segment are ignored, so segments no longer cross-wire their first-frame / playback bookkeeping.

Note: this cross-wiring exists in upstream Python livekit-agents too (perform_audio_forwarding registers playback_started on the shared output the same way). Worth porting back.

Test plan

  • pnpm build:agents
  • pnpm test -- --run agents/src/voice/generation_tts_timeout.test.ts (added 2 regression tests: a stray PLAYBACK_STARTED before the first capture must not resolve firstFrameFut / skip resampling)
  • pnpm test -- --run agents/src/voice/generation_tools.test.ts agents/src/voice/agent_activity.test.ts
  • Manually verified the interrupt repro no longer throws the RtcError

Made with Cursor

…egment

When a speech is interrupted, the scheduling loop immediately authorizes the
next speech, so the new segment's forwardAudio registers its playback_started
listener on the shared audio output while the interrupted segment is still
emitting events during teardown. The stray event resolved the new segment's
firstFrameFut before its first frame was captured, skipping resampler creation
and pushing an unresampled frame to the AudioSource
(RtcError: sample_rate and num_channels don't match) and corrupting playback
bookkeeping. Only resolve firstFrameFut after the segment captures its own
first frame.

Co-authored-by: Cursor <cursoragent@cursor.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@chenghao-mou
chenghao-mou merged commit ec765ea into 1.5.0 Jun 11, 2026
2 checks passed
@chenghao-mou
chenghao-mou deleted the fix/forward-audio-playback-started-scope branch June 11, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants