[fix](trx-frontend-http): handle stereo audio frame layouts

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-02-28 22:26:56 +01:00
parent f06dbc921a
commit 85ad204974
@@ -2676,6 +2676,37 @@ function configureRxStream(nextInfo) {
audioStatus.textContent = "RX";
}
function extractAudioFrameChannels(frame) {
const channels = Math.max(1, frame.numberOfChannels || 1);
const frames = Math.max(0, frame.numberOfFrames || 0);
const format = String(frame.format || "").toLowerCase();
const isPlanar = format.includes("planar");
if (!isPlanar) {
const interleaved = new Float32Array(frames * channels);
frame.copyTo(interleaved);
const out = Array.from({ length: channels }, () => new Float32Array(frames));
for (let i = 0; i < frames; i++) {
for (let ch = 0; ch < channels; ch++) {
out[ch][i] = interleaved[i * channels + ch];
}
}
return out;
}
const out = [];
for (let ch = 0; ch < channels; ch++) {
let len = frames;
try {
len = Math.max(frames, Math.floor(frame.allocationSize({ planeIndex: ch }) / 4));
} catch (e) {}
const plane = new Float32Array(len);
frame.copyTo(plane, { planeIndex: ch });
out.push(plane.length === frames ? plane : plane.subarray(0, frames));
}
return out;
}
function startRxAudio() {
if (rxActive) { stopRxAudio(); return; }
if (!hasWebCodecs) {
@@ -2721,30 +2752,26 @@ function startRxAudio() {
const sampleRate = (streamInfo && streamInfo.sample_rate) || 48000;
opusDecoder = new AudioDecoder({
output: (frame) => {
const frameChannels = extractAudioFrameChannels(frame);
const forceMono = frame.numberOfChannels >= 2
&& wfmAudioModeEl
&& wfmAudioModeEl.value === "mono"
&& modeEl
&& (modeEl.value || "").toUpperCase() === "WFM";
const outChannels = forceMono ? 1 : frame.numberOfChannels;
const outChannels = forceMono ? 1 : frameChannels.length;
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 ch = 0; ch < frame.numberOfChannels; ch++) {
const plane = new Float32Array(frame.numberOfFrames);
frame.copyTo(plane, { planeIndex: ch });
for (let ch = 0; ch < frameChannels.length; ch++) {
const plane = frameChannels[ch];
for (let i = 0; i < frame.numberOfFrames; i++) monoData[i] += plane[i];
}
const inv = 1 / frame.numberOfChannels;
const inv = 1 / Math.max(1, frameChannels.length);
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);
frame.copyTo(chData, { planeIndex: ch });
ab.copyToChannel(chData, ch);
for (let ch = 0; ch < frameChannels.length; ch++) {
ab.copyToChannel(frameChannels[ch], ch);
}
}
const src = audioCtx.createBufferSource();