[test](trx-backend-soapysdr): add multi-tone wfm stereo separation test
Test L-only and R-only signals with tones at 400, 2000, 8000 and 14000 Hz to catch frequency-dependent group delay and phase trim issues that the single 1 kHz test misses. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -1427,6 +1427,99 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Multi-tone stereo separation test.
|
||||||
|
///
|
||||||
|
/// Generates a composite FM stereo signal with tones at 400, 2000, 8000
|
||||||
|
/// and 14000 Hz on a single channel (L-only then R-only), demodulates,
|
||||||
|
/// and verifies that the silent channel stays quiet across the full
|
||||||
|
/// audio band. This catches group-delay and phase-trim problems that
|
||||||
|
/// a single 1 kHz tone would miss.
|
||||||
|
#[test]
|
||||||
|
fn test_wfm_stereo_separation_multitone() {
|
||||||
|
use std::f32::consts::TAU;
|
||||||
|
|
||||||
|
let composite_rate: u32 = 240_000;
|
||||||
|
let audio_rate: u32 = 48_000;
|
||||||
|
let fs = composite_rate as f32;
|
||||||
|
let duration_secs = 0.8_f32;
|
||||||
|
let num_samples = (fs * duration_secs) as usize;
|
||||||
|
let freqs = [400.0_f32, 2_000.0, 8_000.0, 14_000.0];
|
||||||
|
let pilot_freq = 19_000.0_f32;
|
||||||
|
let carrier_freq = 38_000.0_f32;
|
||||||
|
|
||||||
|
// Test both L-only (diff = +audio) and R-only (diff = -audio).
|
||||||
|
for (label, diff_sign) in [("L-only", 1.0_f32), ("R-only", -1.0_f32)] {
|
||||||
|
let mut composite = vec![0.0_f32; num_samples];
|
||||||
|
for n in 0..num_samples {
|
||||||
|
let t = n as f32 / fs;
|
||||||
|
let audio: f32 = freqs.iter().map(|&f| (TAU * f * t).sin()).sum::<f32>()
|
||||||
|
/ freqs.len() as f32;
|
||||||
|
let sum = audio; // L + R (same for both cases)
|
||||||
|
let diff = audio * diff_sign; // L - R
|
||||||
|
let pilot = 0.1 * (TAU * pilot_freq * t).cos();
|
||||||
|
let carrier = (TAU * carrier_freq * t).cos();
|
||||||
|
composite[n] = sum + pilot + diff * carrier;
|
||||||
|
}
|
||||||
|
|
||||||
|
let peak_composite = 2.1_f32;
|
||||||
|
let deviation_hz = 75_000.0_f32;
|
||||||
|
let mod_index = TAU * deviation_hz / (peak_composite * fs);
|
||||||
|
let mut phase: f32 = 0.0;
|
||||||
|
let mut iq = Vec::with_capacity(num_samples);
|
||||||
|
for &c in &composite {
|
||||||
|
phase += mod_index * c;
|
||||||
|
iq.push(Complex::from_polar(1.0, phase));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut decoder = WfmStereoDecoder::new(
|
||||||
|
composite_rate,
|
||||||
|
audio_rate,
|
||||||
|
2,
|
||||||
|
true,
|
||||||
|
50,
|
||||||
|
);
|
||||||
|
let output = decoder.process_iq(&iq);
|
||||||
|
|
||||||
|
let skip_samples = (0.3 * audio_rate as f32) as usize;
|
||||||
|
let stereo_pairs = output.len() / 2;
|
||||||
|
assert!(stereo_pairs > skip_samples + 100,
|
||||||
|
"{label}: not enough output samples");
|
||||||
|
|
||||||
|
let mut active_energy = 0.0_f64;
|
||||||
|
let mut silent_energy = 0.0_f64;
|
||||||
|
let mut count = 0_u64;
|
||||||
|
for i in skip_samples..stereo_pairs {
|
||||||
|
let l = output[2 * i] as f64;
|
||||||
|
let r = output[2 * i + 1] as f64;
|
||||||
|
if diff_sign > 0.0 {
|
||||||
|
// L-only: L is active, R is silent
|
||||||
|
active_energy += l * l;
|
||||||
|
silent_energy += r * r;
|
||||||
|
} else {
|
||||||
|
// R-only: R is active, L is silent
|
||||||
|
active_energy += r * r;
|
||||||
|
silent_energy += l * l;
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
let active_rms = (active_energy / count as f64).sqrt();
|
||||||
|
let silent_rms = (silent_energy / count as f64).sqrt();
|
||||||
|
|
||||||
|
let separation_db = if silent_rms > 1e-10 {
|
||||||
|
20.0 * (active_rms / silent_rms).log10()
|
||||||
|
} else {
|
||||||
|
f64::INFINITY
|
||||||
|
};
|
||||||
|
|
||||||
|
eprintln!("{label}: active RMS = {active_rms:.6}, silent RMS = {silent_rms:.6}, separation = {separation_db:.1} dB");
|
||||||
|
|
||||||
|
assert!(active_rms > 0.01,
|
||||||
|
"{label}: active channel has no energy: {active_rms:.6}");
|
||||||
|
assert!(separation_db > 15.0,
|
||||||
|
"{label}: multitone stereo separation too low: {separation_db:.1} dB");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_wfm_no_pilot_stays_mono_detect() {
|
fn test_wfm_no_pilot_stays_mono_detect() {
|
||||||
use std::f32::consts::TAU;
|
use std::f32::consts::TAU;
|
||||||
|
|||||||
Reference in New Issue
Block a user