From f06dbc921a2edcc18b244c00e2e5a19fa5e3faa9 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 28 Feb 2026 22:23:03 +0100 Subject: [PATCH] [fix](trx-frontend-http): reconfigure audio stream on rig change Co-authored-by: OpenAI Codex Signed-off-by: Stan Grams --- .../trx-frontend-http/assets/web/app.js | 46 ++++++++++--- .../trx-frontend-http/src/audio.rs | 68 ++++++++++++------- 2 files changed, 80 insertions(+), 34 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 026e451..100dc18 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -2642,6 +2642,40 @@ function clearTxTimeout() { txTimeoutRemaining = 0; } +function resetRxDecoder() { + if (opusDecoder) { + try { opusDecoder.close(); } catch (e) {} + opusDecoder = null; + } + nextPlayTime = 0; +} + +function configureRxStream(nextInfo) { + const nextSampleRate = (nextInfo && nextInfo.sample_rate) || 48000; + const sampleRateChanged = !audioCtx || audioCtx.sampleRate !== nextSampleRate; + streamInfo = nextInfo; + updateWfmControls(); + resetRxDecoder(); + if (sampleRateChanged && audioCtx) { + audioCtx.close().catch(() => {}); + audioCtx = null; + rxGainNode = null; + } + if (!audioCtx) { + audioCtx = new AudioContext({ sampleRate: nextSampleRate }); + audioCtx.resume().catch(() => {}); + } + if (!rxGainNode) { + rxGainNode = audioCtx.createGain(); + rxGainNode.connect(audioCtx.destination); + } + rxGainNode.gain.value = rxVolSlider.value / 100; + rxActive = true; + rxAudioBtn.style.borderColor = "#00d17f"; + rxAudioBtn.style.color = "#00d17f"; + audioStatus.textContent = "RX"; +} + function startRxAudio() { if (rxActive) { stopRxAudio(); return; } if (!hasWebCodecs) { @@ -2661,17 +2695,7 @@ function startRxAudio() { if (typeof evt.data === "string") { // Stream info JSON try { - streamInfo = JSON.parse(evt.data); - 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); - rxActive = true; - rxAudioBtn.style.borderColor = "#00d17f"; - rxAudioBtn.style.color = "#00d17f"; - audioStatus.textContent = "RX"; + configureRxStream(JSON.parse(evt.data)); } catch (e) { console.error("Audio stream info parse error", e); } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs index bcaa753..93e490d 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs @@ -226,7 +226,7 @@ pub async fn audio_ws( let (response, mut session, mut msg_stream) = actix_ws::handle(&req, body)?; actix_web::rt::spawn(async move { - let info = loop { + let mut current_info = loop { if let Some(info) = info_rx.borrow().clone() { break info; } @@ -236,7 +236,7 @@ pub async fn audio_ws( } }; - let info_json = match serde_json::to_string(&info) { + let info_json = match serde_json::to_string(¤t_info) { Ok(j) => j, Err(_) => { let _ = session.close(None).await; @@ -247,34 +247,56 @@ pub async fn audio_ws( return; } - let mut rx_session = session.clone(); - let rx_handle = actix_web::rt::spawn(async move { - loop { - match rx_sub.recv().await { - Ok(packet) => { - if rx_session.binary(packet).await.is_err() { - break; + loop { + tokio::select! { + changed = info_rx.changed() => { + match changed { + Ok(()) => { + let Some(next_info) = info_rx.borrow().clone() else { + continue; + }; + let changed = next_info.sample_rate != current_info.sample_rate + || next_info.channels != current_info.channels + || next_info.frame_duration_ms != current_info.frame_duration_ms; + if changed { + current_info = next_info; + let info_json = match serde_json::to_string(¤t_info) { + Ok(j) => j, + Err(_) => break, + }; + if session.text(info_json).await.is_err() { + break; + } + } } + Err(_) => break, } - Err(broadcast::error::RecvError::Lagged(n)) => { - warn!("Audio WS: dropped {} RX frames", n); + } + packet = rx_sub.recv() => { + match packet { + Ok(packet) => { + if session.binary(packet).await.is_err() { + break; + } + } + Err(broadcast::error::RecvError::Lagged(n)) => { + warn!("Audio WS: dropped {} RX frames", n); + } + Err(broadcast::error::RecvError::Closed) => break, } - Err(broadcast::error::RecvError::Closed) => break, } - } - }); - - while let Some(Ok(msg)) = msg_stream.recv().await { - match msg { - Message::Binary(data) => { - let _ = tx_sender.send(Bytes::from(data.to_vec())).await; + msg = msg_stream.recv() => { + match msg { + Some(Ok(Message::Binary(data))) => { + let _ = tx_sender.send(Bytes::from(data.to_vec())).await; + } + Some(Ok(Message::Close(_))) => break, + Some(Ok(_)) => {} + Some(Err(_)) | None => break, + } } - Message::Close(_) => break, - _ => {} } } - - rx_handle.abort(); let _ = session.close(None).await; });