[fix](trx-backend-soapysdr): drop wfm limiter and widen cutoffs
Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -11,14 +11,13 @@ const RDS_BPF_Q: f32 = 10.0;
|
||||
/// Pilot tone frequency (Hz).
|
||||
const PILOT_HZ: f32 = 19_000.0;
|
||||
/// Audio bandwidth for WFM (Hz).
|
||||
/// 15.8 kHz leaves more guard band below the 19 kHz pilot and reduces
|
||||
/// top-end artifacts on strong signals while still preserving the useful
|
||||
/// broadcast audio range.
|
||||
const AUDIO_BW_HZ: f32 = 15_800.0;
|
||||
/// 17 kHz preserves more top-end while still leaving guard band below the
|
||||
/// 19 kHz pilot.
|
||||
const AUDIO_BW_HZ: f32 = 17_000.0;
|
||||
/// Stereo L-R subchannel bandwidth for WFM (Hz).
|
||||
/// Keep this a bit lower than the mono path because the recovered difference
|
||||
/// signal is noisier and more prone to high-frequency artifacts.
|
||||
const STEREO_DIFF_BW_HZ: f32 = 14_500.0;
|
||||
const STEREO_DIFF_BW_HZ: f32 = 15_800.0;
|
||||
/// Q values for a proper 4th-order Butterworth cascade (two 2nd-order stages).
|
||||
/// Stage 1: Q = 1 / (2 cos(π/8))
|
||||
const BW4_Q1: f32 = 0.5412;
|
||||
@@ -100,7 +99,6 @@ fn polyphase_resample(
|
||||
.sum()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn smoothstep01(x: f32) -> f32 {
|
||||
let x = x.clamp(0.0, 1.0);
|
||||
x * x * (3.0 - 2.0 * x)
|
||||
@@ -222,24 +220,6 @@ impl SoftAgc {
|
||||
(x * gain).clamp(-1.0, 1.0)
|
||||
}
|
||||
|
||||
pub(crate) fn process_with_level(&mut self, x: f32, level: f32) -> f32 {
|
||||
let gain = self.update_gain(level.abs());
|
||||
(x * gain).clamp(-1.0, 1.0)
|
||||
}
|
||||
|
||||
pub(crate) fn process_pair_with_level(
|
||||
&mut self,
|
||||
left: f32,
|
||||
right: f32,
|
||||
level: f32,
|
||||
) -> (f32, f32) {
|
||||
let gain = self.update_gain(level.abs());
|
||||
(
|
||||
(left * gain).clamp(-1.0, 1.0),
|
||||
(right * gain).clamp(-1.0, 1.0),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn process_complex(&mut self, x: Complex<f32>) -> Complex<f32> {
|
||||
let gain = self.update_gain((x.re * x.re + x.im * x.im).sqrt());
|
||||
let mut y = x * gain;
|
||||
|
||||
@@ -267,15 +267,12 @@ impl BlockFirFilter {
|
||||
/// 500 ms / 5 s only reacts to slow carrier-amplitude fading, not audio.
|
||||
///
|
||||
/// CW uses a fast attack/release to follow individual dots and dashes.
|
||||
/// WFM uses a linked downward-only peak limiter (max gain = 1.0) so louder
|
||||
/// stations are tamed without boosting quieter ones.
|
||||
/// All other modes use 5 ms / 500 ms, suitable for SSB voice and FM.
|
||||
fn agc_for_mode(mode: &RigMode, audio_sample_rate: u32) -> SoftAgc {
|
||||
let sr = audio_sample_rate.max(1) as f32;
|
||||
match mode {
|
||||
RigMode::CW | RigMode::CWR => SoftAgc::new(sr, 1.0, 50.0, 0.5, 30.0),
|
||||
RigMode::AM => SoftAgc::new(sr, 500.0, 5_000.0, 0.5, 30.0),
|
||||
RigMode::WFM => SoftAgc::new(sr, 0.2, 80.0, 0.92, 0.0),
|
||||
_ => SoftAgc::new(sr, 5.0, 500.0, 0.5, 30.0),
|
||||
}
|
||||
}
|
||||
@@ -305,10 +302,6 @@ fn dc_for_mode(mode: &RigMode) -> Option<DcBlocker> {
|
||||
}
|
||||
}
|
||||
|
||||
fn wfm_limiter_sidechain() -> Option<(DcBlocker, DcBlocker)> {
|
||||
Some((DcBlocker::new(0.985), DcBlocker::new(0.985)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Channel DSP context
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -366,9 +359,6 @@ pub struct ChannelDsp {
|
||||
iq_agc: Option<SoftAgc>,
|
||||
/// Soft AGC applied to all demodulated audio for consistent cross-mode levels.
|
||||
audio_agc: SoftAgc,
|
||||
/// Optional high-passed detector path for the WFM limiter so bass does not
|
||||
/// dominate gain reduction and smear treble.
|
||||
limiter_sidechain: Option<(DcBlocker, DcBlocker)>,
|
||||
/// DC blocker for modes whose demodulator output can carry a DC offset
|
||||
/// (USB/LSB/AM/FM/DIG). None for CW and WFM.
|
||||
audio_dc: Option<DcBlocker>,
|
||||
@@ -450,11 +440,6 @@ impl ChannelDsp {
|
||||
}
|
||||
self.iq_agc = iq_agc_for_mode(&self.mode, channel_sample_rate);
|
||||
self.audio_agc = agc_for_mode(&self.mode, self.audio_sample_rate);
|
||||
self.limiter_sidechain = if self.mode == RigMode::WFM {
|
||||
wfm_limiter_sidechain()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.audio_dc = dc_for_mode(&self.mode);
|
||||
self.frame_buf.clear();
|
||||
}
|
||||
@@ -542,11 +527,6 @@ impl ChannelDsp {
|
||||
},
|
||||
iq_agc: iq_agc_for_mode(mode, channel_sample_rate),
|
||||
audio_agc: agc_for_mode(mode, audio_sample_rate),
|
||||
limiter_sidechain: if *mode == RigMode::WFM {
|
||||
wfm_limiter_sidechain()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
audio_dc: dc_for_mode(mode),
|
||||
}
|
||||
}
|
||||
@@ -701,44 +681,10 @@ impl ChannelDsp {
|
||||
// --- 4. Demodulate + post-process -----------------------------------
|
||||
// WFM: full composite decoder (handles its own DC blocks + deemphasis).
|
||||
// All other modes: stateless demodulator → DC blocker (where enabled) → AGC.
|
||||
// WFM uses linked audio AGC after stereo decode; all other modes use
|
||||
// the normal post-demod AGC path.
|
||||
// WFM bypasses post-audio AGC so the deemphasized stereo path is
|
||||
// heard directly; all other modes use the normal post-demod AGC path.
|
||||
let audio = if let Some(decoder) = self.wfm_decoder.as_mut() {
|
||||
let mut out = decoder.process_iq(&decimated);
|
||||
if !self.wfm_stereo && self.output_channels >= 2 {
|
||||
for pair in out.chunks_exact_mut(2) {
|
||||
let detect = if let Some((left_sc, _)) = &mut self.limiter_sidechain {
|
||||
left_sc.process(pair[0]).abs()
|
||||
} else {
|
||||
pair[0].abs()
|
||||
};
|
||||
let mono = self.audio_agc.process_with_level(pair[0], detect);
|
||||
pair[0] = mono;
|
||||
pair[1] = mono;
|
||||
}
|
||||
} else if self.wfm_stereo && self.output_channels >= 2 {
|
||||
for pair in out.chunks_exact_mut(2) {
|
||||
let detect = if let Some((left_sc, right_sc)) = &mut self.limiter_sidechain {
|
||||
left_sc.process(pair[0]).abs().max(right_sc.process(pair[1]).abs())
|
||||
} else {
|
||||
pair[0].abs().max(pair[1].abs())
|
||||
};
|
||||
let (left, right) =
|
||||
self.audio_agc.process_pair_with_level(pair[0], pair[1], detect);
|
||||
pair[0] = left;
|
||||
pair[1] = right;
|
||||
}
|
||||
} else {
|
||||
for s in &mut out {
|
||||
let detect = if let Some((left_sc, _)) = &mut self.limiter_sidechain {
|
||||
left_sc.process(*s).abs()
|
||||
} else {
|
||||
s.abs()
|
||||
};
|
||||
*s = self.audio_agc.process_with_level(*s, detect);
|
||||
}
|
||||
}
|
||||
out
|
||||
decoder.process_iq(&decimated)
|
||||
} else {
|
||||
let mut raw = self.demodulator.demodulate(&decimated);
|
||||
for s in &mut raw {
|
||||
|
||||
Reference in New Issue
Block a user