fc24dc37ed
Move Scheduler under a new Settings tab in the HTTP frontend. Add the virtual-channel audio implementation plan document. Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
7.3 KiB
7.3 KiB
Virtual-Channel Audio — Implementation Plan
Goal
Each virtual channel (SDR DSP slice) has its own Opus audio stream. When the
browser switches to a non-primary virtual channel the /audio WebSocket should
deliver audio demodulated at that channel's frequency and mode, not the primary
channel's audio.
Current Architecture (baseline)
SoapySDR HW
└─ SdrPipeline (slot 0: primary, slot 1..N: virtual)
pcm_tx[0] pcm_tx[1] ... pcm_tx[N] (broadcast::Sender<Vec<f32>>)
trx-server/src/main.rs
subscribe_pcm(slot 0) → Opus encode → rx_audio_tx (broadcast::Sender<Bytes>)
trx-server/src/audio.rs handle_audio_client()
writes [0x00] StreamInfo
[0x0a] history blob
loop: [0x01] RX frame ← only primary channel
trx-client/src/audio_client.rs
reads all frames → rx_audio_tx.send(bytes) (single broadcast)
FrontendRuntimeContext.audio_rx (single broadcast::Sender<Bytes>)
audio.rs / audio_ws()
subscribes to audio_rx → WebSocket to browser
Only slot 0 (primary) is ever encoded/transmitted. All sessions hear the same audio.
Planned Architecture
SdrPipeline pcm_tx[0..N]
│
trx-server/src/audio.rs (extended handle_audio_client)
┌── per-rig VChanAudioMixer ──────────────────────────────────┐
│ tracks (server_uuid → OpusEncoder + broadcast::Sender<Bytes>) │
│ listens for VCHAN_SUB/VCHAN_UNSUB from client │
│ Opus-encodes each channel's PCM independently │
└─────────────────────────────────────────────────────────────┘
│ wire frames:
│ [0x01] RX_FRAME (primary channel, unchanged)
│ [0x0b] RX_FRAME_CH [16 B UUID][N B Opus] ← NEW
│ [0x0c] VCHAN_ALLOCATED [16 B UUID] ← NEW
│ client→server:
│ [0x0d] VCHAN_SUB [16 B UUID] subscribe to channel
│ [0x0e] VCHAN_UNSUB [16 B UUID] unsubscribe
trx-client/src/audio_client.rs
demux 0x0b frames by UUID → per-channel broadcast::Sender<Bytes>
on 0x0c (allocated): publish UUID to per-channel map
FrontendRuntimeContext
audio_rx: Option<broadcast::Sender<Bytes>> (primary, unchanged)
vchan_audio: Arc<RwLock<HashMap<Uuid, broadcast::Sender<Bytes>>>> ← NEW
ClientChannelManager (trx-frontend-http/src/vchan.rs)
allocate(): after creating local entry, sends VCHAN_SUB via new
vchan_audio_tx: mpsc::Sender<VChanAudioCmd> ← NEW
delete_channel(): sends VCHAN_UNSUB
expose: subscribe_audio(channel_id) → Option<broadcast::Receiver<Bytes>>
audio_ws() (trx-frontend-http/src/audio.rs)
accepts ?channel_id=<uuid> query param
if present → lookup context.vchan_audio[uuid] → subscribe
else → context.audio_rx (primary, current behaviour)
Wire Protocol Additions (trx-core/src/audio.rs)
AUDIO_MSG_RX_FRAME_CH = 0x0b
AUDIO_MSG_VCHAN_ALLOCATED = 0x0c
AUDIO_MSG_VCHAN_SUB = 0x0d
AUDIO_MSG_VCHAN_UNSUB = 0x0e
Frame layout for RX_FRAME_CH:
[0x0b] [4 B BE length = 16 + opus_len] [16 B UUID bytes] [opus_len B Opus]
Frame layout for VCHAN_ALLOCATED, VCHAN_SUB, VCHAN_UNSUB:
[type] [4 B BE length = 16] [16 B UUID bytes]
Layer-by-Layer Changes
1. trx-core/src/audio.rs
- Add four new
AUDIO_MSG_*constants. - Add helper
read_vchan_frame(reader) -> (Uuid, Bytes)andwrite_vchan_frame(writer, msg_type, uuid, payload).
2. trx-server/src/audio.rs (handle_audio_client)
- Accept
vchan_manager: Option<SharedVChanManager>fromRigHandle. - Spawn a
VChanAudioMixertask:- Holds
HashMap<Uuid, (JoinHandle, broadcast::Sender<Bytes>)>. - On
VCHAN_SUB { uuid }: callvchan_manager.subscribe_pcm(uuid), spawn Opus-encode task, writeVCHAN_ALLOCATED { uuid }to client. - On
VCHAN_UNSUB { uuid }: abort encode task, remove from map. - On PCM ready: Opus-encode, write
RX_FRAME_CH { uuid, opus }.
- Holds
- Add the
vchan_managerparameter torun_audio_listener()and pass it through frommain.rs.
3. trx-server/src/main.rs
- Pass
rig_handle.vchan_manager.clone()torun_audio_listener().
4. trx-client/src/audio_client.rs
- Add
vchan_audio_tx: mpsc::Sender<VChanAudioEvent>parameter (whereVChanAudioEvent = Allocated(Uuid, broadcast::Sender<Bytes>) | Frame(Uuid, Bytes)). - On
RX_FRAME_CH { uuid, opus }: forward to per-channel sender (create if first frame for that uuid). - On
VCHAN_ALLOCATED { uuid }: signal that the channel is ready.
5. trx-client/src/main.rs
- Create
vchan_audio: Arc<RwLock<HashMap<Uuid, broadcast::Sender<Bytes>>>>shared between audio_client task and FrontendRuntimeContext. - Add an
mpsc::Sender<VChanAudioCmd>that lets the HTTP frontend request SUB/UNSUB over the audio TCP; pass it intorun_audio_client().
6. trx-client/trx-frontend/src/lib.rs (FrontendRuntimeContext)
- Add:
pub vchan_audio: Arc<RwLock<HashMap<Uuid, broadcast::Sender<Bytes>>>>, pub vchan_audio_cmd: Option<mpsc::Sender<VChanAudioCmd>>, - Initialise both to empty/None in
new().
7. trx-client/trx-frontend/trx-frontend-http/src/vchan.rs (ClientChannelManager)
allocate(): after inserting the local record, ifvchan_audio_cmdis available, sendVChanAudioCmd::Subscribe(uuid).delete_channel(): sendVChanAudioCmd::Unsubscribe(uuid).subscribe_audio(channel_id, context) -> Option<broadcast::Receiver<Bytes>>: look upcontext.vchan_audio.read()[channel_id].subscribe().
8. trx-client/trx-frontend/trx-frontend-http/src/audio.rs (audio_ws)
- Parse optional
channel_id: Option<Uuid>from query string. - If
Some(uuid):- Look up
context.vchan_audio.read()[uuid]→broadcast::Sender<Bytes>. - Subscribe, forward Opus frames exactly as today but from that sender.
- Look up
- Else: current primary-channel path unchanged.
9. assets/web/plugins/vchan.js
vchanSubscribe()andvchanAllocate()callvchanReconnectAudio().vchanReconnectAudio():- If on virtual channel:
reconnectAudioWs(vchanActiveId)(pass channel UUID). - If on primary:
reconnectAudioWs(null).
- If on virtual channel:
reconnectAudioWs(channelId)(new inapp.jsorvchan.js):- Close existing
audioWs. - Reopen
new WebSocket('/audio' + (channelId ? '?channel_id=' + channelId : '')).
- Close existing
Out of Scope (non-SDR rigs)
Non-SDR rigs (vchan_manager === None) are unaffected. The new message types
are only exchanged when the server-side vchan manager is present. Primary-
channel audio behaviour is 100% backwards-compatible.
Implementation Order
trx-core/src/audio.rs— add constants and frame helpers (no breakage)trx-server/src/audio.rs—VChanAudioMixer+ new frame handlingtrx-server/src/main.rs— plumb vchan_manager throughtrx-client/src/audio_client.rs— demux RX_FRAME_CHtrx-client/src/main.rs— shared vchan_audio map + cmd channeltrx-frontend/src/lib.rs— new FrontendRuntimeContext fieldstrx-frontend-http/src/vchan.rs— SUB/UNSUB on allocate/deletetrx-frontend-http/src/audio.rs— channel_id query param routingvchan.js— reconnect WebSocket on channel switch