[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:
@@ -905,6 +905,7 @@ function render(update) {
|
|||||||
if (update.status && update.status.mode) {
|
if (update.status && update.status.mode) {
|
||||||
const mode = normalizeMode(update.status.mode);
|
const mode = normalizeMode(update.status.mode);
|
||||||
modeEl.value = mode ? mode.toUpperCase() : "";
|
modeEl.value = mode ? mode.toUpperCase() : "";
|
||||||
|
updateWfmAudioModeControl();
|
||||||
// When filter panel is active (SDR backend), update the BW slider range
|
// 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
|
// to match the new mode — but only if the server hasn't already sent a
|
||||||
// filter state that overrides it.
|
// filter state that overrides it.
|
||||||
@@ -2007,6 +2008,8 @@ const txAudioBtn = document.getElementById("tx-audio-btn");
|
|||||||
const audioStatus = document.getElementById("audio-status");
|
const audioStatus = document.getElementById("audio-status");
|
||||||
const audioLevelFill = document.getElementById("audio-level-fill");
|
const audioLevelFill = document.getElementById("audio-level-fill");
|
||||||
const audioRow = document.getElementById("audio-row");
|
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
|
// Hide audio row if audio is not configured on the server
|
||||||
fetch("/audio", { method: "GET" }).then((r) => {
|
fetch("/audio", { method: "GET" }).then((r) => {
|
||||||
@@ -2034,6 +2037,20 @@ let txTimeoutRemaining = 0;
|
|||||||
let txTimeoutInterval = null;
|
let txTimeoutInterval = null;
|
||||||
const hasWebCodecs = typeof AudioDecoder !== "undefined" && typeof AudioEncoder !== "undefined";
|
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
|
// Show compatibility warning for non-Chromium browsers
|
||||||
if (!hasWebCodecs) {
|
if (!hasWebCodecs) {
|
||||||
rxAudioBtn.disabled = true;
|
rxAudioBtn.disabled = true;
|
||||||
@@ -2087,6 +2104,7 @@ function startRxAudio() {
|
|||||||
// Stream info JSON
|
// Stream info JSON
|
||||||
try {
|
try {
|
||||||
streamInfo = JSON.parse(evt.data);
|
streamInfo = JSON.parse(evt.data);
|
||||||
|
updateWfmAudioModeControl();
|
||||||
audioCtx = new AudioContext({ sampleRate: streamInfo.sample_rate || 48000 });
|
audioCtx = new AudioContext({ sampleRate: streamInfo.sample_rate || 48000 });
|
||||||
rxGainNode = audioCtx.createGain();
|
rxGainNode = audioCtx.createGain();
|
||||||
rxGainNode.gain.value = rxVolSlider.value / 100;
|
rxGainNode.gain.value = rxVolSlider.value / 100;
|
||||||
@@ -2122,13 +2140,31 @@ function startRxAudio() {
|
|||||||
output: (frame) => {
|
output: (frame) => {
|
||||||
const buf = new Float32Array(frame.numberOfFrames * frame.numberOfChannels);
|
const buf = new Float32Array(frame.numberOfFrames * frame.numberOfChannels);
|
||||||
frame.copyTo(buf, { planeIndex: 0 });
|
frame.copyTo(buf, { planeIndex: 0 });
|
||||||
const ab = audioCtx.createBuffer(frame.numberOfChannels, frame.numberOfFrames, frame.sampleRate);
|
const forceMono = frame.numberOfChannels >= 2
|
||||||
for (let ch = 0; ch < frame.numberOfChannels; ch++) {
|
&& wfmAudioModeEl
|
||||||
const chData = new Float32Array(frame.numberOfFrames);
|
&& 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++) {
|
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();
|
const src = audioCtx.createBufferSource();
|
||||||
src.buffer = ab;
|
src.buffer = ab;
|
||||||
@@ -2168,6 +2204,8 @@ function startRxAudio() {
|
|||||||
// If TX was active when WS closed, release PTT
|
// If TX was active when WS closed, release PTT
|
||||||
if (txActive) { stopTxAudio(); }
|
if (txActive) { stopTxAudio(); }
|
||||||
rxActive = false;
|
rxActive = false;
|
||||||
|
streamInfo = null;
|
||||||
|
updateWfmAudioModeControl();
|
||||||
rxAudioBtn.style.borderColor = "";
|
rxAudioBtn.style.borderColor = "";
|
||||||
rxAudioBtn.style.color = "";
|
rxAudioBtn.style.color = "";
|
||||||
audioStatus.textContent = "Off";
|
audioStatus.textContent = "Off";
|
||||||
@@ -2187,8 +2225,10 @@ function startRxAudio() {
|
|||||||
|
|
||||||
function stopRxAudio() {
|
function stopRxAudio() {
|
||||||
rxActive = false;
|
rxActive = false;
|
||||||
|
streamInfo = null;
|
||||||
if (audioWs) { audioWs.close(); audioWs = null; }
|
if (audioWs) { audioWs.close(); audioWs = null; }
|
||||||
if (audioCtx) { audioCtx.close(); audioCtx = null; }
|
if (audioCtx) { audioCtx.close(); audioCtx = null; }
|
||||||
|
updateWfmAudioModeControl();
|
||||||
rxGainNode = null;
|
rxGainNode = null;
|
||||||
if (opusDecoder) {
|
if (opusDecoder) {
|
||||||
try { opusDecoder.close(); } catch(e) {}
|
try { opusDecoder.close(); } catch(e) {}
|
||||||
|
|||||||
@@ -165,6 +165,7 @@
|
|||||||
<button id="tx-audio-btn" type="button">TX Audio</button>
|
<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">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">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">
|
||||||
<div id="audio-level-fill"></div>
|
<div id="audio-level-fill"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user