[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:
@@ -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)),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user