[fix](trx-backend-soapysdr): fix AM demodulation quality

Three bugs made the AM path sound wrong:

1. AGC attack too fast (5 ms).  The slowest audio a broadcast AM station
   can transmit is ~50 Hz (20 ms period).  A 5 ms attack lets the AGC track
   individual audio cycles, which causes severe pumping and amplitude
   distortion.  Change to 500 ms attack / 5 s release so the AGC only
   responds to slow carrier-amplitude fading, not the audio modulation itself.

2. Bandwidth too narrow.  The IQ filter cutoff is audio_bandwidth_hz / 2,
   so the previous 6 000 Hz setting gave only 3 kHz audio bandwidth.
   AM broadcast sidebands extend to ±4.5–5 kHz; raise the default to
   12 000 (cutoff 6 kHz) to cover the full audio band.

3. DC blocker rate inconsistent.  For AM the demodulated magnitude is
   always ≥ 0 and the DC component equals the carrier amplitude; only true
   DC needs removing.  Unify all non-WFM modes to r = 0.9999 (corner
   ≈ 0.76 Hz @ 48 kHz), which strips carrier DC without touching any
   audible bass content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-02-28 17:00:32 +01:00
parent 1f3bdb988a
commit 332ad4448b
@@ -255,28 +255,30 @@ impl BlockFirFilter {
/// Build the AGC for a given mode.
///
/// CW uses fast attack/release to follow individual dots and dashes.
/// AM uses a moderate release so the AGC tracks fading carriers.
/// All other modes use a longer release suitable for voice and data signals.
/// AM AGC must be far slower than audio modulation. With a 50 Hz bass
/// component the modulation period is 20 ms; an attack faster than that
/// causes the AGC to follow the audio envelope and distort (pumping).
/// 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.
/// 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, 5.0, 200.0, 0.5, 30.0),
RigMode::AM => SoftAgc::new(sr, 500.0, 5_000.0, 0.5, 30.0),
_ => SoftAgc::new(sr, 5.0, 500.0, 0.5, 30.0),
}
}
/// Build the DC blocker for a given mode, or `None` if not applicable.
///
/// WFM is excluded because it has its own internal DC blockers on each output
/// channel. AM uses a slightly faster blocker (r = 0.999, corner ≈ 7.6 Hz
/// @ 48 kHz) so it can track slow carrier-amplitude fading. All other modes
/// (including CW, which now demodulates as USB) use r = 0.9999 (≈ 0.76 Hz).
/// WFM is excluded because it has its own internal DC blockers per channel.
/// All other modes use r = 0.9999 (corner ≈ 0.76 Hz @ 48 kHz), which strips
/// only true carrier DC without affecting any audible bass content.
fn dc_for_mode(mode: &RigMode) -> Option<DcBlocker> {
match mode {
RigMode::WFM => None,
RigMode::AM => Some(DcBlocker::new(0.999)),
_ => Some(DcBlocker::new(0.9999)),
}
}
@@ -513,7 +515,7 @@ fn default_bandwidth_for_mode(mode: &RigMode) -> u32 {
match mode {
RigMode::LSB | RigMode::USB | RigMode::PKT | RigMode::DIG => 3_000,
RigMode::CW | RigMode::CWR => 500,
RigMode::AM => 6_000,
RigMode::AM => 12_000,
RigMode::FM => 12_500,
RigMode::WFM => 180_000,
RigMode::Other(_) => 3_000,