audioio: fix per-period click in TX upsampler (issue #81) - #84
Merged
Conversation
The 8 kHz -> 48 kHz playback (TX) upsampler interpolated each read- period in isolation: at the last sample of every period it set next=current, flattening the tail, then the next period jumped to its first sample. The signal transition across the boundary was never interpolated, producing a step discontinuity — an audible click once per audio period. This matches every report in issue #81: a pop at the start of each period, its rate scaling with the period count, and bigger buffers only lowering the pop *rate* (PR #83 masked it, did not fix it). Fix: make the interpolator stateful — bridge the PREVIOUS input sample to the current one and carry it across periods (resamp_prev), so the output is continuous over period boundaries. Silence now also ramps cleanly in/out. Also widens the interpolation product to 64 bit (cur-prev spans 2^32 at full scale). Offline test (tests/audioio/test_resampler.c, no sound card needed): the old algorithm's worst sample-to-sample jump lands exactly on a period boundary at 6.0x the smooth signal slope (~71% of full scale); the new one is 0.41x — continuous — and chunked-vs-whole output is bit-identical, proving boundaries are invisible. Does NOT address the separate image-rejection weakness of linear interpolation (the residual aliasing/'wavetable' quality also noted in #81) — that needs a polyphase anti-imaging filter and follows next. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Finishes the resampler overhaul behind the per-period click fix (#81): - TX upsample (8k->48k) used linear interpolation, a weak anti-imaging filter: a 2400 Hz tone left an image only ~14 dB down at 5600 Hz. - RX downsample (48k->8k) was bare 1-in-6 decimation with NO anti-alias filter at all, folding everything above 4 kHz straight into the modem band (a 5600 Hz input aliases to 2400 Hz). Both are replaced by a shared stateful polyphase FIR (audioio/ resampler.c): a 180-tap windowed-sinc low-pass, fc=3400 Hz at 48 kHz (flat past the widest modem waveform ~2.5 kHz, stopband by the 8 kHz Nyquist), decomposed polyphase (~1.4M mults/s each way — trivial even on a Pi Zero) and carried statefully across read periods so boundaries stay continuous. Measured (offline Goertzel test, tests/audioio/test_resampler.c): - upsample image rejection: -66 dB (was -14) - downsample alias rejection: -66 dB (was ~0) - passband flat to 0.00 dB at the 2700 Hz band edge - whole-vs-period-chunked output bit-identical (boundaries invisible) The soundcard path is exercised by the unit tests; the FIFO/null backends run at 8 kHz and bypass resampling, so the ARQ E2E (still green) confirms no modem/logic regression. Real-hardware audio quality is the field-test gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.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.
The 8 kHz -> 48 kHz playback (TX) upsampler interpolated each read- period in isolation: at the last sample of every period it set next=current, flattening the tail, then the next period jumped to its first sample. The signal transition across the boundary was never interpolated, producing a step discontinuity — an audible click once per audio period. This matches every report in issue #81: a pop at the start of each period, its rate scaling with the period count, and bigger buffers only lowering the pop rate (PR #83 masked it, did not fix it).
Fix: make the interpolator stateful — bridge the PREVIOUS input sample to the current one and carry it across periods (resamp_prev), so the output is continuous over period boundaries. Silence now also ramps cleanly in/out. Also widens the interpolation product to 64 bit (cur-prev spans 2^32 at full scale).
Offline test (tests/audioio/test_resampler.c, no sound card needed): the old algorithm's worst sample-to-sample jump lands exactly on a period boundary at 6.0x the smooth signal slope (~71% of full scale); the new one is 0.41x — continuous — and chunked-vs-whole output is bit-identical, proving boundaries are invisible.
Does NOT address the separate image-rejection weakness of linear interpolation (the residual aliasing/'wavetable' quality also noted in #81) — that needs a polyphase anti-imaging filter and follows next.