[fix](trx-frontend-http,trx-backend-soapysdr): fix audio rate and stereo copy
JS: fix stereo AudioData channel copy — frame.copyTo with planeIndex:0 only fills the left plane; reading it as interleaved data caused every other sample to be zero, making WFM stereo play at half speed. Now calls copyTo per channel with the correct planeIndex. Rust WFM: replace integer output_decim/output_counter in WfmStereoDecoder with a fractional phase accumulator (output_phase_inc = audio_rate / composite_rate). Integer division caused the effective output rate to drift from audio_sample_rate when the SDR rate is not an exact multiple (e.g. 2 MHz SDR → 250 kHz composite → ~50 kHz output instead of 48 kHz, making audio play 4% slow). Rust non-WFM: add resample_phase/resample_phase_inc to ChannelDsp and use a fractional-phase resampler in process_block for non-WFM paths, ensuring exactly audio_sample_rate samples/sec regardless of SDR rate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -62,8 +62,13 @@ pub struct WfmStereoDecoder {
|
||||
deemph_m: Deemphasis,
|
||||
deemph_l: Deemphasis,
|
||||
deemph_r: Deemphasis,
|
||||
output_decim: usize,
|
||||
output_counter: usize,
|
||||
/// Fractional phase increment per composite sample = audio_rate / composite_rate.
|
||||
/// Avoids integer-division rate error when composite_rate is not an exact
|
||||
/// multiple of audio_rate (e.g. 250 kHz composite → 48 kHz audio).
|
||||
output_phase_inc: f64,
|
||||
/// Fractional phase accumulator (0 .. 1). Emits an output sample whenever
|
||||
/// it crosses 1.0, ensuring the long-term rate is exactly audio_rate.
|
||||
output_phase: f64,
|
||||
}
|
||||
|
||||
impl WfmStereoDecoder {
|
||||
@@ -74,7 +79,7 @@ impl WfmStereoDecoder {
|
||||
deemphasis_us: u32,
|
||||
) -> Self {
|
||||
let composite_rate_f = composite_rate.max(1) as f32;
|
||||
let output_decim = (composite_rate / audio_rate.max(1)).max(1) as usize;
|
||||
let output_phase_inc = audio_rate.max(1) as f64 / composite_rate.max(1) as f64;
|
||||
let deemphasis_us = deemphasis_us as f32;
|
||||
Self {
|
||||
output_channels: output_channels.max(1),
|
||||
@@ -89,8 +94,8 @@ impl WfmStereoDecoder {
|
||||
deemph_m: Deemphasis::new(audio_rate.max(1) as f32, deemphasis_us),
|
||||
deemph_l: Deemphasis::new(audio_rate.max(1) as f32, deemphasis_us),
|
||||
deemph_r: Deemphasis::new(audio_rate.max(1) as f32, deemphasis_us),
|
||||
output_decim,
|
||||
output_counter: 0,
|
||||
output_phase_inc,
|
||||
output_phase: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +107,8 @@ impl WfmStereoDecoder {
|
||||
let _ = self.rds_decoder.process_samples(&composite);
|
||||
|
||||
let mut output = Vec::with_capacity(
|
||||
(composite.len() / self.output_decim.max(1)) * self.output_channels.max(1),
|
||||
((composite.len() as f64 * self.output_phase_inc).ceil() as usize + 1)
|
||||
* self.output_channels.max(1),
|
||||
);
|
||||
|
||||
for x in composite {
|
||||
@@ -121,11 +127,11 @@ impl WfmStereoDecoder {
|
||||
let stereo_carrier = (2.0 * self.pilot_phase).cos() * 2.0;
|
||||
let diff = self.diff_lp.process(x * stereo_carrier) * stereo_blend;
|
||||
|
||||
self.output_counter += 1;
|
||||
if self.output_counter < self.output_decim {
|
||||
self.output_phase += self.output_phase_inc;
|
||||
if self.output_phase < 1.0 {
|
||||
continue;
|
||||
}
|
||||
self.output_counter = 0;
|
||||
self.output_phase -= 1.0;
|
||||
|
||||
if self.output_channels >= 2 {
|
||||
let left = self.deemph_l.process((sum + diff) * 0.5).clamp(-1.0, 1.0);
|
||||
|
||||
@@ -282,8 +282,15 @@ pub struct ChannelDsp {
|
||||
pub mixer_phase: f64,
|
||||
/// Phase increment per IQ sample.
|
||||
pub mixer_phase_inc: f64,
|
||||
/// Decimation counter.
|
||||
/// Decimation counter (used only for the WFM path).
|
||||
decim_counter: usize,
|
||||
/// Fractional-phase resampler for non-WFM paths.
|
||||
/// Advances by `resample_phase_inc` per IQ sample and emits an output
|
||||
/// sample whenever it crosses 1.0, giving a long-term output rate of
|
||||
/// exactly `audio_sample_rate` regardless of SDR rate rounding.
|
||||
resample_phase: f64,
|
||||
/// `audio_sample_rate / sdr_sample_rate` (updated in `rebuild_filters`).
|
||||
resample_phase_inc: f64,
|
||||
/// Dedicated WFM decoder that preserves the FM composite baseband.
|
||||
wfm_decoder: Option<WfmStereoDecoder>,
|
||||
}
|
||||
@@ -335,6 +342,12 @@ impl ChannelDsp {
|
||||
)
|
||||
.0;
|
||||
self.decim_counter = 0;
|
||||
self.resample_phase = 0.0;
|
||||
self.resample_phase_inc = if self.sdr_sample_rate == 0 {
|
||||
1.0
|
||||
} else {
|
||||
self.audio_sample_rate as f64 / self.sdr_sample_rate as f64
|
||||
};
|
||||
self.wfm_decoder = if self.mode == RigMode::WFM {
|
||||
Some(WfmStereoDecoder::new(
|
||||
channel_sample_rate,
|
||||
@@ -403,6 +416,12 @@ impl ChannelDsp {
|
||||
mixer_phase: 0.0,
|
||||
mixer_phase_inc,
|
||||
decim_counter: 0,
|
||||
resample_phase: 0.0,
|
||||
resample_phase_inc: if sdr_sample_rate == 0 {
|
||||
1.0
|
||||
} else {
|
||||
audio_sample_rate as f64 / sdr_sample_rate as f64
|
||||
},
|
||||
wfm_decoder: if *mode == RigMode::WFM {
|
||||
Some(WfmStereoDecoder::new(
|
||||
channel_sample_rate,
|
||||
@@ -496,16 +515,33 @@ impl ChannelDsp {
|
||||
let filtered_i = self.lpf_i.filter_block(&mixed_i);
|
||||
let filtered_q = self.lpf_q.filter_block(&mixed_q);
|
||||
|
||||
// --- 3. Decimate ----------------------------------------------------
|
||||
// --- 3. Decimate / resample -----------------------------------------
|
||||
let capacity = n / self.decim_factor + 1;
|
||||
let mut decimated: Vec<Complex<f32>> = Vec::with_capacity(capacity);
|
||||
for i in 0..n {
|
||||
self.decim_counter += 1;
|
||||
if self.decim_counter >= self.decim_factor {
|
||||
self.decim_counter = 0;
|
||||
let fi = filtered_i.get(i).copied().unwrap_or(0.0);
|
||||
let fq = filtered_q.get(i).copied().unwrap_or(0.0);
|
||||
decimated.push(Complex::new(fi, fq));
|
||||
if self.wfm_decoder.is_some() {
|
||||
// WFM: integer decimation preserves the FM composite signal at the
|
||||
// rate expected by WfmStereoDecoder. Final rate correction is done
|
||||
// inside WfmStereoDecoder via its own fractional-phase accumulator.
|
||||
for i in 0..n {
|
||||
self.decim_counter += 1;
|
||||
if self.decim_counter >= self.decim_factor {
|
||||
self.decim_counter = 0;
|
||||
let fi = filtered_i.get(i).copied().unwrap_or(0.0);
|
||||
let fq = filtered_q.get(i).copied().unwrap_or(0.0);
|
||||
decimated.push(Complex::new(fi, fq));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Non-WFM: fractional-phase resampler so the long-term output rate
|
||||
// equals exactly `audio_sample_rate` regardless of SDR rate rounding.
|
||||
for i in 0..n {
|
||||
self.resample_phase += self.resample_phase_inc;
|
||||
if self.resample_phase >= 1.0 {
|
||||
self.resample_phase -= 1.0;
|
||||
let fi = filtered_i.get(i).copied().unwrap_or(0.0);
|
||||
let fq = filtered_q.get(i).copied().unwrap_or(0.0);
|
||||
decimated.push(Complex::new(fi, fq));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user