[fix](trx-backend-soapysdr): compute resampler cutoff from actual rate ratio

The fixed WFM_RESAMP_CUTOFF of 0.94 passed frequencies up to 94 kHz at
200 kHz composite rate, while the output Nyquist is only ~24 kHz. The
38 kHz demod products in the stereo diff path were only ~31 dB attenuated
by the Butterworth and aliased back into 10-20 kHz audio, causing treble
corruption in stereo mode. Now the cutoff is computed as
audio_rate / composite_rate, properly anti-aliasing the polyphase
resampler output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-01 09:07:07 +01:00
parent a5e66ed287
commit 4994a869f9
@@ -42,10 +42,7 @@ const STEREO_DIFF_DC_R: f32 = 0.9995;
const WFM_RESAMP_TAPS: usize = 16; const WFM_RESAMP_TAPS: usize = 16;
/// Polyphase slots for the WFM fractional FIR resampler. /// Polyphase slots for the WFM fractional FIR resampler.
const WFM_RESAMP_PHASES: usize = 32; const WFM_RESAMP_PHASES: usize = 32;
/// Slightly sub-Nyquist sinc cutoff to tame top-end imaging. fn build_wfm_resample_bank(cutoff: f32) -> [[f32; WFM_RESAMP_TAPS]; WFM_RESAMP_PHASES] {
const WFM_RESAMP_CUTOFF: f32 = 0.94;
fn build_wfm_resample_bank() -> [[f32; WFM_RESAMP_TAPS]; WFM_RESAMP_PHASES] {
let mut bank = [[0.0; WFM_RESAMP_TAPS]; WFM_RESAMP_PHASES]; let mut bank = [[0.0; WFM_RESAMP_TAPS]; WFM_RESAMP_PHASES];
let anchor = (WFM_RESAMP_TAPS / 2 - 1) as f32; let anchor = (WFM_RESAMP_TAPS / 2 - 1) as f32;
for (phase_idx, phase) in bank.iter_mut().enumerate() { for (phase_idx, phase) in bank.iter_mut().enumerate() {
@@ -55,9 +52,9 @@ fn build_wfm_resample_bank() -> [[f32; WFM_RESAMP_TAPS]; WFM_RESAMP_PHASES] {
for (tap_idx, coeff) in phase.iter_mut().enumerate() { for (tap_idx, coeff) in phase.iter_mut().enumerate() {
let x = tap_idx as f32 - center; let x = tap_idx as f32 - center;
let sinc = if x.abs() < 1e-6 { let sinc = if x.abs() < 1e-6 {
WFM_RESAMP_CUTOFF cutoff
} else { } else {
let arg = std::f32::consts::PI * x * WFM_RESAMP_CUTOFF; let arg = std::f32::consts::PI * x * cutoff;
arg.sin() / (std::f32::consts::PI * x) arg.sin() / (std::f32::consts::PI * x)
}; };
let window = if WFM_RESAMP_TAPS == 1 { let window = if WFM_RESAMP_TAPS == 1 {
@@ -703,7 +700,7 @@ impl WfmStereoDecoder {
stereo_detect_level: 0.0, stereo_detect_level: 0.0,
stereo_detected: false, stereo_detected: false,
fm_gain: composite_rate_f / (2.0 * 75_000.0), fm_gain: composite_rate_f / (2.0 * 75_000.0),
resample_bank: build_wfm_resample_bank(), resample_bank: build_wfm_resample_bank(audio_rate as f32 / composite_rate_f),
sum_hist: [0.0; WFM_RESAMP_TAPS], sum_hist: [0.0; WFM_RESAMP_TAPS],
diff_hist: [0.0; WFM_RESAMP_TAPS], diff_hist: [0.0; WFM_RESAMP_TAPS],
diff_q_hist: [0.0; WFM_RESAMP_TAPS], diff_q_hist: [0.0; WFM_RESAMP_TAPS],