Skip to content

Update AudioStreamTrack to use stateful av.AudioResampler for better quality#272

Open
dangusev wants to merge 6 commits into
mainfrom
feat/audiostreamtrack-stateful-resampler
Open

Update AudioStreamTrack to use stateful av.AudioResampler for better quality#272
dangusev wants to merge 6 commits into
mainfrom
feat/audiostreamtrack-stateful-resampler

Conversation

@dangusev

@dangusev dangusev commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Use the new FrameResampler (a thin wrapper around av.AudioResampler) inside AudioStreamTrack to improve the output audio quality.

Previous version defaulted to PcmData.resample(), which uses a stateless algorithm under the hood for chunks <500ms.
On such short chunks, the stateless transform introduces artifacts at the chunk boundaries.
av.AudioResampler is stateful, and it outputs much smoother audio.

Performance-wise, both versions are very similar, with FrameResampler having slightly higher overhead when the AudioStreamTrack.write(..., final=True) is called because it re-inits the av.AudioResampler under the hood.

What's changed

  • Added new FrameResampler that accepts PcmData and outputs a list of av.AudioFrame objects.
  • The AudioStreamTrack is updated to use the FrameResampler for incoming PCM chunks and buffer the av.AudioFrames instead of raw bytes under the hood.
    • AudioStreamTrack.write() gained final:bool parameter to signal the end-of-utterance events when the internal resampler state needs to be flushed and added to the track buffer for output.
  • Tests are updated to verify the new behavior and test the outputs instead of internals (many tests assert on AudioStreamTrack._buffer which is discouraged).

What has not changed

  • PcmData.resample() remains as-is. In fact, the stateless algorithm may be handy in some scenarios where quality is less of a concern (e.g., sending input to STT)

Summary by CodeRabbit

Summary

  • Improvements

    • Audio is emitted in consistent 20 ms chunks with real-time pacing and smoother timeline progression.
    • Higher-quality sample-rate conversion, including handling of changing input audio characteristics.
    • When audio is temporarily unavailable, silence is generated without stalling timestamps.
  • Bug Fixes / Behavior Changes

    • Buffer overflow now drops the oldest audio to preserve the newest.
    • Partial writes are delivered only after finalization, with appropriate silence padding.
    • Flushing fully resets pending audio so the next output starts cleanly.
  • API Updates

    • write() now supports final to control draining; output is restricted to S16 format.
  • Testing

    • Expanded coverage for resampling quality and timing/PTS behavior.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 33e4f55f-18be-4988-a026-84823ad73510

📥 Commits

Reviewing files that changed from the base of the PR and between 6f62904 and 7be9b75.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • getstream/video/rtc/audio_track.py
  • getstream/video/rtc/track_util.py
  • tests/rtc/test_track_util.py
  • tests/test_audio_stream_track.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • getstream/video/rtc/track_util.py
  • tests/rtc/test_track_util.py
  • getstream/video/rtc/audio_track.py
  • tests/test_audio_stream_track.py

📝 Walkthrough

Walkthrough

AudioStreamTrack now uses a stateful resampler to produce fixed 20ms frames, buffers them with drop-oldest overflow handling, and emits paced audio or silence with continuous timestamps. Tests cover resampling, flushing, buffering, format conversion, FIFO delivery, concurrency, and timing.

Changes

Audio frame pipeline

Layer / File(s) Summary
Stateful frame resampling
getstream/video/rtc/track_util.py, tests/rtc/test_track_util.py
Adds FrameResampler with signature-aware rebuilding, fixed-size output, tail flushing, reset behavior, contiguous ndarray handling, and end-to-end validation.
Track buffering and writes
getstream/video/rtc/audio_track.py, tests/test_audio_stream_track.py
Rebuilds initialization, PCM writes, final flushing, format validation, frame buffering, and drop-oldest overflow handling.
Paced frame delivery
getstream/video/rtc/audio_track.py, tests/test_audio_stream_track.py
Adds real-time pacing, monotonic PTS generation, FIFO delivery, silence synthesis, short-frame padding, and concurrency coverage.
Audio track behavior validation
tests/test_audio_stream_track.py
Updates tests for channel conversion, resampling quality, flushing, overflow, FIFO delivery, timing, and starvation behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AudioStreamTrack
  participant FrameResampler
  participant FrameDeque
  participant FramePacer
  AudioStreamTrack->>FrameResampler: Resample PCM input
  FrameResampler-->>AudioStreamTrack: Return fixed-size frames
  AudioStreamTrack->>FrameDeque: Buffer frames and drop oldest on overflow
  AudioStreamTrack->>FramePacer: Request next PTS
  FramePacer-->>AudioStreamTrack: Return paced PTS
  AudioStreamTrack->>FrameDeque: Dequeue frame or generate silence
  AudioStreamTrack-->>AudioStreamTrack: Pad and timestamp output frame
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: AudioStreamTrack now uses a stateful resampler to improve audio quality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/audiostreamtrack-stateful-resampler

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
tests/test_audio_stream_track.py (1)

3-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate _sine_chunks/SINE_FREQ helper.

This helper and constant are byte-for-byte identical to _sine_chunks in tests/rtc/test_track_util.py:14-34 (same signature, same body). Consider extracting to a shared test utility module (e.g. tests/audio_test_utils.py or a conftest.py fixture) so the two test suites don't drift independently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_audio_stream_track.py` around lines 3 - 34, Extract the identical
SINE_FREQ constant and _sine_chunks helper into a shared test utility module,
then update tests/test_audio_stream_track.py and tests/rtc/test_track_util.py to
import and reuse them. Remove the duplicated local definitions while preserving
the existing helper signature and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@getstream/video/rtc/audio_track.py`:
- Around line 158-178: Update _FramePacer.next_pts to use time.monotonic() for
both the initial _start anchor and the current-time calculation used to derive
wait, while preserving the existing timestamp progression and sleep behavior.
- Around line 76-109: Move the _resampler.resample call in write() inside the
_frame_lock critical section, keeping frame enqueueing and buffer-cap
enforcement in that same section. This must serialize resampling with flush() so
an in-flight write cannot enqueue frames computed before the resampler reset;
preserve the existing empty-frame and final-flush behavior.

In `@getstream/video/rtc/track_util.py`:
- Around line 2631-2680: The _ensure_av_resampler method currently replaces the
existing av.AudioResampler on input signature changes without draining its
buffered samples. Before creating and assigning a new resampler, flush the old
instance and preserve those returned frames in the resample flow; ensure the new
resampler is then used for the current input. Add a regression test covering a
partial frame followed by a rate or format change and verify the buffered tail
is emitted.

In `@tests/test_audio_stream_track.py`:
- Around line 302-312: Loosen the real-time elapsed_ms upper bounds in
test_first_frame_is_immediate_and_pts_zero and
test_slow_consumer_does_not_accumulate_drift to tolerate CI scheduling and
runtime jitter while still detecting unintended pacing waits. Keep the
assertions for immediate first-frame behavior, pts=0, and drift semantics
unchanged.

---

Nitpick comments:
In `@tests/test_audio_stream_track.py`:
- Around line 3-34: Extract the identical SINE_FREQ constant and _sine_chunks
helper into a shared test utility module, then update
tests/test_audio_stream_track.py and tests/rtc/test_track_util.py to import and
reuse them. Remove the duplicated local definitions while preserving the
existing helper signature and behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c4ce0ad2-b72d-492e-b323-9812a906d7fd

📥 Commits

Reviewing files that changed from the base of the PR and between f552d7e and e54ce2d.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • getstream/video/rtc/audio_track.py
  • getstream/video/rtc/track_util.py
  • tests/rtc/test_track_util.py
  • tests/test_audio_stream_track.py

Comment thread getstream/video/rtc/audio_track.py
Comment thread getstream/video/rtc/audio_track.py
Comment thread getstream/video/rtc/track_util.py Outdated
Comment thread tests/test_audio_stream_track.py Outdated
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.

1 participant