Skip to content

audioio: fix per-period click in TX upsampler (issue #81) - #84

Merged
rafael2k merged 2 commits into
mercuryv2from
fix/audio-resampler-crackle
Jun 14, 2026
Merged

audioio: fix per-period click in TX upsampler (issue #81)#84
rafael2k merged 2 commits into
mercuryv2from
fix/audio-resampler-crackle

Conversation

@rafael2k

Copy link
Copy Markdown
Contributor

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.

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>
@rafael2k
rafael2k merged commit 27ebe9a into mercuryv2 Jun 14, 2026
6 checks passed
@pedromessetti
pedromessetti deleted the fix/audio-resampler-crackle branch June 25, 2026 15:53
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