[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).
|
/// Pilot tone frequency (Hz).
|
||||||
const PILOT_HZ: f32 = 19_000.0;
|
const PILOT_HZ: f32 = 19_000.0;
|
||||||
/// Audio bandwidth for WFM (Hz).
|
/// Audio bandwidth for WFM (Hz).
|
||||||
/// 15.8 kHz leaves more guard band below the 19 kHz pilot and reduces
|
/// 17 kHz preserves more top-end while still leaving guard band below the
|
||||||
/// top-end artifacts on strong signals while still preserving the useful
|
/// 19 kHz pilot.
|
||||||
/// broadcast audio range.
|
const AUDIO_BW_HZ: f32 = 17_000.0;
|
||||||
const AUDIO_BW_HZ: f32 = 15_800.0;
|
|
||||||
/// Stereo L-R subchannel bandwidth for WFM (Hz).
|
/// Stereo L-R subchannel bandwidth for WFM (Hz).
|
||||||
/// Keep this a bit lower than the mono path because the recovered difference
|
/// Keep this a bit lower than the mono path because the recovered difference
|
||||||
/// signal is noisier and more prone to high-frequency artifacts.
|
/// 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).
|
/// Q values for a proper 4th-order Butterworth cascade (two 2nd-order stages).
|
||||||
/// Stage 1: Q = 1 / (2 cos(π/8))
|
/// Stage 1: Q = 1 / (2 cos(π/8))
|
||||||
const BW4_Q1: f32 = 0.5412;
|
const BW4_Q1: f32 = 0.5412;
|
||||||
@@ -100,7 +99,6 @@ fn polyphase_resample(
|
|||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn smoothstep01(x: f32) -> f32 {
|
fn smoothstep01(x: f32) -> f32 {
|
||||||
let x = x.clamp(0.0, 1.0);
|
let x = x.clamp(0.0, 1.0);
|
||||||
x * x * (3.0 - 2.0 * x)
|
x * x * (3.0 - 2.0 * x)
|
||||||
@@ -222,24 +220,6 @@ impl SoftAgc {
|
|||||||
(x * gain).clamp(-1.0, 1.0)
|
(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> {
|
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 gain = self.update_gain((x.re * x.re + x.im * x.im).sqrt());
|
||||||
let mut y = x * gain;
|
let mut y = x * gain;
|
||||||
|
|||||||
@@ -267,15 +267,12 @@ impl BlockFirFilter {
|
|||||||
/// 500 ms / 5 s only reacts to slow carrier-amplitude fading, not audio.
|
/// 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.
|
/// 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.
|
/// 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 {
|
fn agc_for_mode(mode: &RigMode, audio_sample_rate: u32) -> SoftAgc {
|
||||||
let sr = audio_sample_rate.max(1) as f32;
|
let sr = audio_sample_rate.max(1) as f32;
|
||||||
match mode {
|
match mode {
|
||||||
RigMode::CW | RigMode::CWR => SoftAgc::new(sr, 1.0, 50.0, 0.5, 30.0),
|
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::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),
|
_ => 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
|
// Channel DSP context
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -366,9 +359,6 @@ pub struct ChannelDsp {
|
|||||||
iq_agc: Option<SoftAgc>,
|
iq_agc: Option<SoftAgc>,
|
||||||
/// Soft AGC applied to all demodulated audio for consistent cross-mode levels.
|
/// Soft AGC applied to all demodulated audio for consistent cross-mode levels.
|
||||||
audio_agc: SoftAgc,
|
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
|
/// DC blocker for modes whose demodulator output can carry a DC offset
|
||||||
/// (USB/LSB/AM/FM/DIG). None for CW and WFM.
|
/// (USB/LSB/AM/FM/DIG). None for CW and WFM.
|
||||||
audio_dc: Option<DcBlocker>,
|
audio_dc: Option<DcBlocker>,
|
||||||
@@ -450,11 +440,6 @@ impl ChannelDsp {
|
|||||||
}
|
}
|
||||||
self.iq_agc = iq_agc_for_mode(&self.mode, channel_sample_rate);
|
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.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.audio_dc = dc_for_mode(&self.mode);
|
||||||
self.frame_buf.clear();
|
self.frame_buf.clear();
|
||||||
}
|
}
|
||||||
@@ -542,11 +527,6 @@ impl ChannelDsp {
|
|||||||
},
|
},
|
||||||
iq_agc: iq_agc_for_mode(mode, channel_sample_rate),
|
iq_agc: iq_agc_for_mode(mode, channel_sample_rate),
|
||||||
audio_agc: agc_for_mode(mode, audio_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),
|
audio_dc: dc_for_mode(mode),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -701,44 +681,10 @@ impl ChannelDsp {
|
|||||||
// --- 4. Demodulate + post-process -----------------------------------
|
// --- 4. Demodulate + post-process -----------------------------------
|
||||||
// WFM: full composite decoder (handles its own DC blocks + deemphasis).
|
// WFM: full composite decoder (handles its own DC blocks + deemphasis).
|
||||||
// All other modes: stateless demodulator → DC blocker (where enabled) → AGC.
|
// All other modes: stateless demodulator → DC blocker (where enabled) → AGC.
|
||||||
// WFM uses linked audio AGC after stereo decode; all other modes use
|
// WFM bypasses post-audio AGC so the deemphasized stereo path is
|
||||||
// the normal post-demod AGC path.
|
// heard directly; all other modes use the normal post-demod AGC path.
|
||||||
let audio = if let Some(decoder) = self.wfm_decoder.as_mut() {
|
let audio = if let Some(decoder) = self.wfm_decoder.as_mut() {
|
||||||
let mut out = decoder.process_iq(&decimated);
|
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
|
|
||||||
} else {
|
} else {
|
||||||
let mut raw = self.demodulator.demodulate(&decimated);
|
let mut raw = self.demodulator.demodulate(&decimated);
|
||||||
for s in &mut raw {
|
for s in &mut raw {
|
||||||
|
|||||||
Reference in New Issue
Block a user