[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:
2026-02-28 07:57:42 +01:00
parent 7fa4f5d133
commit 6a47fb00ad
3 changed files with 69 additions and 29 deletions
@@ -2504,8 +2504,6 @@ function startRxAudio() {
const sampleRate = (streamInfo && streamInfo.sample_rate) || 48000;
opusDecoder = new AudioDecoder({
output: (frame) => {
const buf = new Float32Array(frame.numberOfFrames * frame.numberOfChannels);
frame.copyTo(buf, { planeIndex: 0 });
const forceMono = frame.numberOfChannels >= 2
&& wfmAudioModeEl
&& wfmAudioModeEl.value === "mono"
@@ -2514,21 +2512,21 @@ function startRxAudio() {
const outChannels = forceMono ? 1 : frame.numberOfChannels;
const ab = audioCtx.createBuffer(outChannels, frame.numberOfFrames, frame.sampleRate);
if (forceMono) {
// Mix all planes down to mono
const monoData = new Float32Array(frame.numberOfFrames);
for (let i = 0; i < frame.numberOfFrames; i++) {
let sum = 0;
for (let ch = 0; ch < frame.numberOfChannels; ch++) {
sum += buf[i * frame.numberOfChannels + ch];
}
monoData[i] = sum / frame.numberOfChannels;
for (let ch = 0; ch < frame.numberOfChannels; ch++) {
const plane = new Float32Array(frame.numberOfFrames);
frame.copyTo(plane, { planeIndex: ch });
for (let i = 0; i < frame.numberOfFrames; i++) monoData[i] += plane[i];
}
const inv = 1 / frame.numberOfChannels;
for (let i = 0; i < frame.numberOfFrames; i++) monoData[i] *= inv;
ab.copyToChannel(monoData, 0);
} else {
// Copy each plane directly — AudioData uses planar layout (f32-planar)
for (let ch = 0; ch < frame.numberOfChannels; ch++) {
const chData = new Float32Array(frame.numberOfFrames);
for (let i = 0; i < frame.numberOfFrames; i++) {
chData[i] = buf[i * frame.numberOfChannels + ch];
}
frame.copyTo(chData, { planeIndex: ch });
ab.copyToChannel(chData, ch);
}
}