feat: add StreamingResampler#25
Merged
Merged
Conversation
AudioFrame::resample builds a fresh stateless rubato per call. For real-time consumers that feed it short frames in sequence (20 ms G.711 packets off an RTP socket are the motivating case), each call re-pays the sinc reconstruction's edge cost: it sees an abrupt step from zero to the chunk's first sample, smears the result, and produces audible boundary artifacts at the frame rate (50 Hz for 20 ms packets). Real playback of resampled telephony audio sounds like continuous noise/buzz over the voice. StreamingResampler builds rubato once with `new(source, target, chunk_size)` and reuses its internal filter state for every `process` call. State carries across calls, so the only edge cost is paid once at the start of the stream. The sinc parameters and rubato construction are factored into a `build_sinc_resampler` helper shared with `AudioFrame::resample`, so the two paths can't drift on quality settings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merged
wavekat-eason
pushed a commit
that referenced
this pull request
May 14, 2026
## 🤖 New release * `wavekat-core`: 0.0.10 -> 0.0.11 (✓ API compatible changes) <details><summary><i><b>Changelog</b></i></summary><p> <blockquote> ## [0.0.11](v0.0.10...v0.0.11) - 2026-05-14 ### Added - add StreamingResampler ([#25](#25)) </blockquote> </p></details> --- This PR was generated with [release-plz](https://github.com/release-plz/release-plz/). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
StreamingResampler— a stateful resampler for real-time pipelines that feed short frames in sequence.AudioFrame::resampleis fine for one-shot conversions, but it constructs a fresh rubato per call. A consumer that calls it on every 20 ms G.711 packet pays the sinc reconstruction's edge cost on every chunk — the resampler sees an abrupt step from zero to the chunk's first sample and produces audible boundary artifacts at the frame rate (50 Hz for 20 ms packets). On a real playback path the user hears continuous noise/buzz over the voice. This bitwavekat-voice's M1 media work after the short-input fix from #23 maderesampleitself succeed.StreamingResampler::new(source_rate, target_rate, chunk_size)builds rubato once.processaccumulates input until it haschunk_size, runs one rubato step, and emits output. Internal filter state is preserved across calls.Also factors the sinc parameters + builder out into a private
build_sinc_resamplerhelper shared withAudioFrame::resample, so the two paths can't drift on quality settings.API
Pass-through fast path when
source_rate == target_rate: no resampler is built,processis a copy.Test plan
cargo test --features resample— 53 unit tests + 5 doctests pass.StreamingResamplertests:same_rate_is_passthrough— proves the no-op fast path doesn't go through rubato.short_input_chunked_calls— 10× 160-sample inputs at 8 kHz → 44.1 kHz produce roughly 10 × 882 output samples (the exact RTP receive shape).buffers_across_partial_calls— splitting an input across twoprocesscalls produces sample-for-sample-identical output to one big call.avoids_per_frame_edge_artifacts— the regression guard: a smooth 600 Hz sine, resampled chunk-by-chunk withStreamingResamplervsAudioFrame::resample. The streaming output's max consecutive-sample delta stays inside the expected smooth bound (under 10 spikes in steady state); the stateless per-chunk output has 5×+ more spikes because of the chunk-boundary transients.rejects_zero_rate/rejects_zero_chunk_size— input validation.cargo clippy --workspace --all-features -- -D warningsclean.cargo fmt --all --checkclean.Downstream
wavekat-voice'sLocalDeviceSinkwill switch from per-frameAudioFrame::resampletoStreamingResampleronce this lands and bumps to 0.0.11.🤖 Generated with Claude Code