[fix](trx-backend-soapysdr): preserve RDS subcarrier with narrow WFM bandwidth

The IQ prefilter cutoff was audio_bandwidth_hz/2, so any setting below
~120 kHz would cut off the 57 kHz RDS subcarrier before FM demod.

- Clamp IQ prefilter cutoff to >= 60 kHz for WFM in both new() and
  rebuild_filters() — audio quality is unaffected since WfmStereoDecoder
  applies its own 18 kHz lowpass internally
- Ensure pipeline target rate >= 120 kHz for WFM so the decimated IQ
  sample rate can represent the 60 kHz cutoff

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-26 23:31:54 +01:00
parent 1994534b4d
commit 32e090f927
@@ -328,7 +328,11 @@ impl ChannelDsp {
} }
let target_rate = match mode { let target_rate = match mode {
RigMode::WFM => audio_bandwidth_hz.max(audio_sample_rate.saturating_mul(4)), // Ensure composite rate is at least 120 kHz so the IQ filter can
// pass the 57 kHz RDS subcarrier regardless of the user's audio BW.
RigMode::WFM => audio_bandwidth_hz
.max(audio_sample_rate.saturating_mul(4))
.max(120_000),
RigMode::VDES => audio_sample_rate.max(96_000), RigMode::VDES => audio_sample_rate.max(96_000),
_ => audio_sample_rate.max(1), _ => audio_sample_rate.max(1),
}; };
@@ -346,10 +350,20 @@ impl ChannelDsp {
self.audio_sample_rate, self.audio_sample_rate,
self.audio_bandwidth_hz, self.audio_bandwidth_hz,
); );
let cutoff_hz = self let cutoff_hz = {
.audio_bandwidth_hz let raw = self
.min(channel_sample_rate.saturating_sub(1)) as f32 .audio_bandwidth_hz
/ 2.0; .min(channel_sample_rate.saturating_sub(1)) as f32
/ 2.0;
// For WFM, always pass at least the 57 kHz RDS subcarrier.
// Audio bandwidth is handled inside WfmStereoDecoder, so widening
// the IQ prefilter here does not affect output audio quality.
if self.mode == RigMode::WFM {
raw.max(60_000.0)
} else {
raw
}
};
let cutoff_norm = if self.sdr_sample_rate == 0 { let cutoff_norm = if self.sdr_sample_rate == 0 {
0.1 0.1
} else { } else {
@@ -424,7 +438,14 @@ impl ChannelDsp {
let (decim_factor, channel_sample_rate) = let (decim_factor, channel_sample_rate) =
Self::pipeline_rates(mode, sdr_sample_rate, audio_sample_rate, audio_bandwidth_hz); Self::pipeline_rates(mode, sdr_sample_rate, audio_sample_rate, audio_bandwidth_hz);
let cutoff_hz = audio_bandwidth_hz.min(channel_sample_rate.saturating_sub(1)) as f32 / 2.0; let cutoff_hz = {
let raw = audio_bandwidth_hz.min(channel_sample_rate.saturating_sub(1)) as f32 / 2.0;
if *mode == RigMode::WFM {
raw.max(60_000.0)
} else {
raw
}
};
let cutoff_norm = if sdr_sample_rate == 0 { let cutoff_norm = if sdr_sample_rate == 0 {
0.1 0.1
} else { } else {