[feat](trx-rs): per-virtual-channel audio streaming
Add end-to-end audio routing for virtual DSP channels:
Server (trx-server):
- New wire protocol: AUDIO_MSG_RX_FRAME_CH (0x0b), VCHAN_ALLOCATED (0x0c),
VCHAN_SUB (0x0d), VCHAN_UNSUB (0x0e), VCHAN_FREQ (0x0f), VCHAN_MODE (0x10),
VCHAN_REMOVE (0x11) frame types in trx-core audio.rs
- Add frame helpers: write_vchan_uuid_msg, write_vchan_audio_frame,
parse_vchan_audio_frame, parse_vchan_uuid_msg
- Add ensure_channel_pcm() to VirtualChannelManager trait; implement in
SdrVirtualChannelManager with create-or-subscribe semantics using client UUID
- Extend audio.rs handle_audio_client: VChanCmd dispatcher, per-channel Opus
encoder tasks, VCHAN_SUB/UNSUB/FREQ/MODE/REMOVE reader loop handlers
- Thread vchan_manager through run_audio_listener / spawn_rig_audio_stack
Client (trx-client):
- Add VChanAudioCmd enum to trx-frontend; add vchan_audio and vchan_audio_cmd
fields to FrontendRuntimeContext
- Extend audio_client: demux AUDIO_MSG_RX_FRAME_CH to per-channel broadcasters,
handle VCHAN_ALLOCATED; forward VChanAudioCmd over TCP write loop
- Wire vchan_cmd_tx/rx channel in main.rs; pass vchan_audio map to audio_client
- ClientChannelManager.set_audio_cmd() / send_audio_cmd(): dispatch
Subscribe/Remove/SetFreq/SetMode on allocate/delete/freq/mode operations
- Wire audio_cmd sender in server.rs serve() after creating vchan_mgr
HTTP frontend:
- /audio?channel_id=<uuid>: route WebSocket to per-channel Opus broadcaster
- vchan.js: vchanReconnectAudio() stops/restarts RX audio on channel switch;
_audioChannelOverride in app.js selects primary vs virtual WS endpoint
- app.js: _audioChannelOverride variable; startRxAudio appends channel param
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>