[feat](trx-rs): add WFM RDS and playback controls
Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -889,6 +889,9 @@ function render(update) {
|
||||
if (update.filter && typeof update.filter.bandwidth_hz === "number") {
|
||||
currentBandwidthHz = update.filter.bandwidth_hz;
|
||||
syncBandwidthInput(currentBandwidthHz);
|
||||
if (wfmDeemphasisEl && typeof update.filter.wfm_deemphasis_us === "number") {
|
||||
wfmDeemphasisEl.value = String(update.filter.wfm_deemphasis_us);
|
||||
}
|
||||
}
|
||||
if (update.status && update.status.freq && typeof update.status.freq.hz === "number") {
|
||||
lastFreqHz = update.status.freq.hz;
|
||||
@@ -904,7 +907,7 @@ function render(update) {
|
||||
if (update.status && update.status.mode) {
|
||||
const mode = normalizeMode(update.status.mode);
|
||||
modeEl.value = mode ? mode.toUpperCase() : "";
|
||||
updateWfmAudioModeControl();
|
||||
updateWfmControls();
|
||||
// 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.
|
||||
@@ -1475,6 +1478,7 @@ async function applyModeFromPicker() {
|
||||
showHint("Mode missing", 1500);
|
||||
return;
|
||||
}
|
||||
updateWfmControls();
|
||||
modeEl.disabled = true;
|
||||
showHint("Setting mode…");
|
||||
try {
|
||||
@@ -2052,7 +2056,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 wfmControlsCol = document.getElementById("wfm-controls-col");
|
||||
const wfmDeemphasisEl = document.getElementById("wfm-deemphasis");
|
||||
const wfmAudioModeEl = document.getElementById("wfm-audio-mode");
|
||||
|
||||
// Hide audio row if audio is not configured on the server
|
||||
@@ -2080,6 +2085,8 @@ let txTimeoutTimer = null;
|
||||
let txTimeoutRemaining = 0;
|
||||
let txTimeoutInterval = null;
|
||||
const hasWebCodecs = typeof AudioDecoder !== "undefined" && typeof AudioEncoder !== "undefined";
|
||||
const MAX_RX_BUFFER_SECS = 0.25;
|
||||
const TARGET_RX_BUFFER_SECS = 0.04;
|
||||
|
||||
if (wfmAudioModeEl) {
|
||||
wfmAudioModeEl.value = loadSetting("wfmAudioMode", "stereo");
|
||||
@@ -2087,12 +2094,16 @@ if (wfmAudioModeEl) {
|
||||
saveSetting("wfmAudioMode", wfmAudioModeEl.value);
|
||||
});
|
||||
}
|
||||
if (wfmDeemphasisEl) {
|
||||
wfmDeemphasisEl.addEventListener("change", () => {
|
||||
postPath(`/set_wfm_deemphasis?us=${encodeURIComponent(wfmDeemphasisEl.value)}`).catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
function updateWfmAudioModeControl() {
|
||||
if (!wfmAudioModeWrap) return;
|
||||
function updateWfmControls() {
|
||||
if (!wfmControlsCol) return;
|
||||
const mode = (modeEl && modeEl.value ? modeEl.value : "").toUpperCase();
|
||||
const channels = (streamInfo && streamInfo.channels) || 1;
|
||||
wfmAudioModeWrap.style.display = mode === "WFM" && channels >= 2 ? "" : "none";
|
||||
wfmControlsCol.style.display = mode === "WFM" ? "" : "none";
|
||||
}
|
||||
|
||||
// Show compatibility warning for non-Chromium browsers
|
||||
@@ -2148,8 +2159,9 @@ function startRxAudio() {
|
||||
// Stream info JSON
|
||||
try {
|
||||
streamInfo = JSON.parse(evt.data);
|
||||
updateWfmAudioModeControl();
|
||||
updateWfmControls();
|
||||
audioCtx = new AudioContext({ sampleRate: streamInfo.sample_rate || 48000 });
|
||||
audioCtx.resume().catch(() => {});
|
||||
rxGainNode = audioCtx.createGain();
|
||||
rxGainNode.gain.value = rxVolSlider.value / 100;
|
||||
rxGainNode.connect(audioCtx.destination);
|
||||
@@ -2214,6 +2226,9 @@ function startRxAudio() {
|
||||
src.buffer = ab;
|
||||
src.connect(rxGainNode);
|
||||
const now = audioCtx.currentTime;
|
||||
if (nextPlayTime && nextPlayTime - now > MAX_RX_BUFFER_SECS) {
|
||||
nextPlayTime = now + TARGET_RX_BUFFER_SECS;
|
||||
}
|
||||
const schedTime = Math.max(now, (nextPlayTime || now));
|
||||
src.start(schedTime);
|
||||
nextPlayTime = schedTime + ab.duration;
|
||||
@@ -2249,7 +2264,7 @@ function startRxAudio() {
|
||||
if (txActive) { stopTxAudio(); }
|
||||
rxActive = false;
|
||||
streamInfo = null;
|
||||
updateWfmAudioModeControl();
|
||||
updateWfmControls();
|
||||
rxAudioBtn.style.borderColor = "";
|
||||
rxAudioBtn.style.color = "";
|
||||
audioStatus.textContent = "Off";
|
||||
@@ -2272,7 +2287,7 @@ function stopRxAudio() {
|
||||
streamInfo = null;
|
||||
if (audioWs) { audioWs.close(); audioWs = null; }
|
||||
if (audioCtx) { audioCtx.close(); audioCtx = null; }
|
||||
updateWfmAudioModeControl();
|
||||
updateWfmControls();
|
||||
rxGainNode = null;
|
||||
if (opusDecoder) {
|
||||
try { opusDecoder.close(); } catch(e) {}
|
||||
|
||||
@@ -117,6 +117,23 @@
|
||||
<button id="jog-up" type="button" class="jog-btn">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls-col controls-col-wfm label-below-col" id="wfm-controls-col" style="display:none;">
|
||||
<div class="inline wfm-controls-inline">
|
||||
<label class="wfm-control">Deemphasis
|
||||
<select id="wfm-deemphasis" class="status-input">
|
||||
<option value="50">50 uS</option>
|
||||
<option value="75">75 uS</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="wfm-control">Audio
|
||||
<select id="wfm-audio-mode" class="status-input">
|
||||
<option value="stereo">Stereo</option>
|
||||
<option value="mono">Mono</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="label"><span>WFM</span></div>
|
||||
</div>
|
||||
<div class="controls-col controls-col-power label-below-col" id="tx-power-col">
|
||||
<div class="label"><span>Transmit / Power</span></div>
|
||||
<div class="btn-grid">
|
||||
@@ -168,7 +185,6 @@
|
||||
<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>
|
||||
|
||||
@@ -79,7 +79,7 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
|
||||
#freq { font-family: 'DSEG14 Classic', monospace; font-size: 2rem; padding: 0.5rem 0.6rem; letter-spacing: 0.05em; text-align: center; }
|
||||
.controls-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
@@ -114,6 +114,26 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
|
||||
width: auto;
|
||||
align-items: center;
|
||||
}
|
||||
.controls-col-wfm.label-below-col .label {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.wfm-controls-inline {
|
||||
gap: 0.6rem;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.wfm-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.wfm-control .status-input {
|
||||
min-width: 4.6rem;
|
||||
width: auto;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.controls-col-center::after {
|
||||
content: "";
|
||||
display: block;
|
||||
@@ -583,9 +603,11 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
.header-rig-switch { width: 100%; justify-content: flex-end; }
|
||||
.header-rig-switch select { min-width: 6.5rem; }
|
||||
.controls-row { grid-template-columns: 1fr auto; }
|
||||
.controls-col-wfm { grid-column: 1 / -1; }
|
||||
.controls-col-power { grid-column: 1 / -1; }
|
||||
.controls-col.label-below-col .inline,
|
||||
.controls-col.label-below-col .btn-grid { margin-top: 0; }
|
||||
.wfm-controls-inline { flex-wrap: wrap; }
|
||||
.ft8-controls { flex-wrap: wrap; }
|
||||
#ft8-decode-toggle-btn, #wspr-decode-toggle-btn { white-space: nowrap; }
|
||||
.jog-container { flex-wrap: wrap; }
|
||||
|
||||
@@ -468,6 +468,19 @@ pub async fn set_fir_taps(
|
||||
send_command(&rig_tx, RigCommand::SetFirTaps(query.taps)).await
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct WfmDeemphasisQuery {
|
||||
pub us: u32,
|
||||
}
|
||||
|
||||
#[post("/set_wfm_deemphasis")]
|
||||
pub async fn set_wfm_deemphasis(
|
||||
query: web::Query<WfmDeemphasisQuery>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
send_command(&rig_tx, RigCommand::SetWfmDeemphasis(query.us)).await
|
||||
}
|
||||
|
||||
#[post("/toggle_aprs_decode")]
|
||||
pub async fn toggle_aprs_decode(
|
||||
state: web::Data<watch::Receiver<RigState>>,
|
||||
@@ -679,6 +692,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(set_tx_limit)
|
||||
.service(set_bandwidth)
|
||||
.service(set_fir_taps)
|
||||
.service(set_wfm_deemphasis)
|
||||
.service(toggle_aprs_decode)
|
||||
.service(toggle_cw_decode)
|
||||
.service(set_cw_auto)
|
||||
|
||||
Reference in New Issue
Block a user