[feat](trx-frontend-http): add WFM mono and stereo playback switch

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-02-27 23:32:09 +01:00
parent 14cefd1198
commit b8c154af60
2 changed files with 46 additions and 5 deletions
@@ -905,6 +905,7 @@ function render(update) {
if (update.status && update.status.mode) {
const mode = normalizeMode(update.status.mode);
modeEl.value = mode ? mode.toUpperCase() : "";
updateWfmAudioModeControl();
// When filter panel is active (SDR backend), update the BW slider range
// to match the new mode — but only if the server hasn't already sent a
// filter state that overrides it.
@@ -2007,6 +2008,8 @@ const txAudioBtn = document.getElementById("tx-audio-btn");
const audioStatus = document.getElementById("audio-status");
const audioLevelFill = document.getElementById("audio-level-fill");
const audioRow = document.getElementById("audio-row");
const wfmAudioModeWrap = document.getElementById("wfm-audio-mode-wrap");
const wfmAudioModeEl = document.getElementById("wfm-audio-mode");
// Hide audio row if audio is not configured on the server
fetch("/audio", { method: "GET" }).then((r) => {
@@ -2034,6 +2037,20 @@ let txTimeoutRemaining = 0;
let txTimeoutInterval = null;
const hasWebCodecs = typeof AudioDecoder !== "undefined" && typeof AudioEncoder !== "undefined";
if (wfmAudioModeEl) {
wfmAudioModeEl.value = loadSetting("wfmAudioMode", "stereo");
wfmAudioModeEl.addEventListener("change", () => {
saveSetting("wfmAudioMode", wfmAudioModeEl.value);
});
}
function updateWfmAudioModeControl() {
if (!wfmAudioModeWrap) return;
const mode = (modeEl && modeEl.value ? modeEl.value : "").toUpperCase();
const channels = (streamInfo && streamInfo.channels) || 1;
wfmAudioModeWrap.style.display = mode === "WFM" && channels >= 2 ? "" : "none";
}
// Show compatibility warning for non-Chromium browsers
if (!hasWebCodecs) {
rxAudioBtn.disabled = true;
@@ -2087,6 +2104,7 @@ function startRxAudio() {
// Stream info JSON
try {
streamInfo = JSON.parse(evt.data);
updateWfmAudioModeControl();
audioCtx = new AudioContext({ sampleRate: streamInfo.sample_rate || 48000 });
rxGainNode = audioCtx.createGain();
rxGainNode.gain.value = rxVolSlider.value / 100;
@@ -2122,13 +2140,31 @@ function startRxAudio() {
output: (frame) => {
const buf = new Float32Array(frame.numberOfFrames * frame.numberOfChannels);
frame.copyTo(buf, { planeIndex: 0 });
const ab = audioCtx.createBuffer(frame.numberOfChannels, frame.numberOfFrames, frame.sampleRate);
for (let ch = 0; ch < frame.numberOfChannels; ch++) {
const chData = new Float32Array(frame.numberOfFrames);
const forceMono = frame.numberOfChannels >= 2
&& wfmAudioModeEl
&& wfmAudioModeEl.value === "mono"
&& modeEl
&& (modeEl.value || "").toUpperCase() === "WFM";
const outChannels = forceMono ? 1 : frame.numberOfChannels;
const ab = audioCtx.createBuffer(outChannels, frame.numberOfFrames, frame.sampleRate);
if (forceMono) {
const monoData = new Float32Array(frame.numberOfFrames);
for (let i = 0; i < frame.numberOfFrames; i++) {
chData[i] = buf[i * frame.numberOfChannels + ch];
let sum = 0;
for (let ch = 0; ch < frame.numberOfChannels; ch++) {
sum += buf[i * frame.numberOfChannels + ch];
}
monoData[i] = sum / frame.numberOfChannels;
}
ab.copyToChannel(monoData, 0);
} else {
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];
}
ab.copyToChannel(chData, ch);
}
ab.copyToChannel(chData, ch);
}
const src = audioCtx.createBufferSource();
src.buffer = ab;
@@ -2168,6 +2204,8 @@ function startRxAudio() {
// If TX was active when WS closed, release PTT
if (txActive) { stopTxAudio(); }
rxActive = false;
streamInfo = null;
updateWfmAudioModeControl();
rxAudioBtn.style.borderColor = "";
rxAudioBtn.style.color = "";
audioStatus.textContent = "Off";
@@ -2187,8 +2225,10 @@ function startRxAudio() {
function stopRxAudio() {
rxActive = false;
streamInfo = null;
if (audioWs) { audioWs.close(); audioWs = null; }
if (audioCtx) { audioCtx.close(); audioCtx = null; }
updateWfmAudioModeControl();
rxGainNode = null;
if (opusDecoder) {
try { opusDecoder.close(); } catch(e) {}
@@ -165,6 +165,7 @@
<button id="tx-audio-btn" type="button">TX Audio</button>
<label class="vol-label">RX<input type="range" id="rx-vol" min="0" max="100" value="80" class="vol-slider" /><small class="vol-pct" id="rx-vol-pct">80%</small></label>
<label class="vol-label">TX<input type="range" id="tx-vol" min="0" max="100" value="80" class="vol-slider" /><small class="vol-pct" id="tx-vol-pct">80%</small></label>
<label class="vol-label" id="wfm-audio-mode-wrap" style="display:none;">WFM<select id="wfm-audio-mode" class="status-input"><option value="stereo">Stereo</option><option value="mono">Mono</option></select></label>
<div id="audio-level">
<div id="audio-level-fill"></div>
</div>