Skip to content

fix(voice): place recorded audio where it actually played - #2327

Open
longcw wants to merge 2 commits into
mainfrom
longc/port-recorder-playback-progress
Open

fix(voice): place recorded audio where it actually played#2327
longcw wants to merge 2 commits into
mainfrom
longc/port-recorder-playback-progress

Conversation

@longcw

@longcw longcw commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Ports livekit/agents#6921, merged upstream as d3621006.

Problem

RecorderIO rebuilds the agent channel by inference.
It collects the frames it forwarded, places them from the moment playbackFinished arrives, and pads the difference with silence.
Nothing measures where the audio really went.

Four conditions break that inference: a sink that runs dry mid-segment, a late flush, audio discarded on pause(), and a microphone that stops.
Each one moves the audio, and none of them records the gap.
A slow TTS or a muted participant can hit it.

Fix

A sink that owns its playback device now reports where its audio went.
AudioOutput gains playbackProgressed next to playbackStarted and playbackFinished, and ParticipantAudioOutput reports one run at a time.
Each report is past tense, so it is never revised, and a jump in its offset describes audio the sink discarded rather than played.

RecorderIO places both channels on one absolute timeline and leaves unwritten time silent.
When a sink reports nothing, the recorder uses its segment endpoints instead, which is all a remote avatar worker can know.
That removes the pause reconstruction, the end-anchored padding, and the playbackPosition truncation.

Two behaviour changes follow from the absolute timeline: close() now always writes the settled tail, so a silent session still produces a file, and the recording only extends as far as the wall clock does.

The checked-in agents.api.md is stale on main, so this change adds only the lines for the new event.

RecorderIO rebuilt the agent channel by inference: it collected the
frames it forwarded, placed them from the moment playbackFinished
arrived, and padded the difference with silence. A sink that ran dry
inside a segment, a late flush, audio discarded on pause(), or a
microphone that stopped delivering each moved the audio, and none of
them recorded the gap.

AudioOutput now carries playbackProgressed next to playbackStarted and
playbackFinished, and ParticipantAudioOutput reports one run at a time.
RecorderIO places both channels on one absolute timeline and leaves
unwritten time silent, falling back to the segment endpoints for a sink
that reports nothing. That removes the pause reconstruction, the
end-anchored padding, and the playbackPosition truncation.

Ports livekit/agents#6921.
@longcw
longcw requested a review from a team as a code owner August 24, 2026 08:04
@changeset-bot

changeset-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 831e6b9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 39 packages
Name Type
@livekit/agents Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-anthropic Patch
@livekit/agents-plugin-assemblyai Patch
@livekit/agents-plugin-azure Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-cerebras Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-did Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-fishaudio Patch
@livekit/agents-plugin-google Patch
@livekit/agents-plugin-hedra Patch
@livekit/agents-plugin-hume Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-krisp Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-liveavatar Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-minimax Patch
@livekit/agents-plugin-mistral Patch
@livekit/agents-plugin-mistralai Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-perplexity Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-protoface Patch
@livekit/agents-plugin-resemble Patch
@livekit/agents-plugin-rime Patch
@livekit/agents-plugin-runway Patch
@livekit/agents-plugin-sarvam Patch
@livekit/agents-plugin-silero Patch
@livekit/agents-plugin-soniox Patch
@livekit/agents-plugin-tavus Patch
@livekit/agents-plugins-test Patch
@livekit/agents-plugin-trugen Patch
@livekit/agents-plugin-xai Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@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 potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

…amps

Every existing case calls onPlaybackProgressed directly on the object
under test, so the listener AudioOutput registers on nextInChain had no
coverage. A broken registration there leaves the whole suite green and
puts every recorder silently on the segment-endpoint fallback. The new
case sends a report from a leaf two levels below the recorder and
asserts the audio lands on the sink's own timestamp.

The write task had no coverage either, so neither the stall timeout for
a source that went quiet nor the pendingSince hold for a segment in
flight was tested. WRITE_INTERVAL_MS and INPUT_STALL_TIMEOUT_MS are
exported so the test reads the same constants the loop does; neither
reaches the public entry point.
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