[fix](trx-backend-soapysdr): demodulate CW as USB (real part, not envelope)

CW signals in SDR are centred at an audio offset (e.g. 700 Hz) by the
upstream FIR filter, so demodulating as USB (taking the real part) produces
the correct side-tone.  The previous magnitude/envelope approach produced a
DC pulse per key press with no audible tone.

Re-enable the DC blocker for CW/CWR (r = 0.9999): the output is now audio
that can carry a DC offset from BFO frequency error, identical to USB.

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 16:25:53 +01:00
parent eed5673f95
commit 1f9c09668d
2 changed files with 17 additions and 22 deletions
@@ -577,17 +577,14 @@ fn demod_fm(samples: &[Complex<f32>]) -> Vec<f32> {
// CW // CW
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// CW envelope detector: magnitude of IQ. /// CW demodulator: take the real part of each baseband IQ sample.
/// ///
/// Returns the raw envelope amplitude. Level normalisation is handled /// The upstream FIR filter centres the CW carrier at the configured audio
/// downstream by the per-channel AGC. No DC blocker is applied to CW because /// offset (e.g. 700 Hz), so demodulating identically to USB produces the
/// the envelope is always non-negative; high-pass filtering would create /// characteristic CW side-tone. Level normalisation is handled downstream
/// negative-going artifacts on each key release. /// by the per-channel AGC.
fn demod_cw(samples: &[Complex<f32>]) -> Vec<f32> { fn demod_cw(samples: &[Complex<f32>]) -> Vec<f32> {
samples samples.iter().map(|s| s.re).collect()
.iter()
.map(|s| (s.re * s.re + s.im * s.im).sqrt())
.collect()
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -705,18 +702,17 @@ mod tests {
} }
} }
// Test 7: CW envelope detector returns raw magnitudes. // Test 7: CW demodulator returns the real part (same as USB).
// Normalisation is handled downstream by the AGC.
#[test] #[test]
fn test_cw_raw_magnitude() { fn test_cw_takes_real_part() {
let input = vec![ let input = vec![
Complex::new(3.0_f32, 4.0), // magnitude 5.0 Complex::new(3.0_f32, 4.0),
Complex::new(0.0, 0.0), // magnitude 0.0 Complex::new(0.0, 0.0),
Complex::new(1.0, 0.0), // magnitude 1.0 Complex::new(1.0, 0.0),
]; ];
let out = Demodulator::Cw.demodulate(&input); let out = Demodulator::Cw.demodulate(&input);
assert_eq!(out.len(), 3); assert_eq!(out.len(), 3);
assert_approx_eq(out[0], 5.0, 1e-6, "CW sample 0"); assert_approx_eq(out[0], 3.0, 1e-6, "CW sample 0");
assert_approx_eq(out[1], 0.0, 1e-6, "CW sample 1"); assert_approx_eq(out[1], 0.0, 1e-6, "CW sample 1");
assert_approx_eq(out[2], 1.0, 1e-6, "CW sample 2"); assert_approx_eq(out[2], 1.0, 1e-6, "CW sample 2");
} }
@@ -269,14 +269,13 @@ fn agc_for_mode(mode: &RigMode, audio_sample_rate: u32) -> SoftAgc {
/// Build the DC blocker for a given mode, or `None` if not applicable. /// Build the DC blocker for a given mode, or `None` if not applicable.
/// ///
/// CW and WFM are excluded: CW envelope is always non-negative (DC blocking /// WFM is excluded because it has its own internal DC blockers on each output
/// would create negative artifacts on key releases), and WFM has its own /// channel. AM uses a slightly faster blocker (r = 0.999, corner ≈ 7.6 Hz
/// internal DC blockers on each output channel. /// @ 48 kHz) so it can track slow carrier-amplitude fading. All other modes
/// AM uses a slightly faster blocker (r = 0.999, corner ≈ 7.6 Hz @ 48 kHz) /// (including CW, which now demodulates as USB) use r = 0.9999 (≈ 0.76 Hz).
/// so it can track slow carrier-amplitude fading.
fn dc_for_mode(mode: &RigMode) -> Option<DcBlocker> { fn dc_for_mode(mode: &RigMode) -> Option<DcBlocker> {
match mode { match mode {
RigMode::CW | RigMode::CWR | RigMode::WFM => None, RigMode::WFM => None,
RigMode::AM => Some(DcBlocker::new(0.999)), RigMode::AM => Some(DcBlocker::new(0.999)),
_ => Some(DcBlocker::new(0.9999)), _ => Some(DcBlocker::new(0.9999)),
} }