feat: AudioBroadcaster TX on the neutral surface (MOR-544, AudioTransport 11/12)#1748
Conversation
Step 11/12 of the MOR-532 AudioTransport epic. The web audio handler's
TX path now prefers the codec/transport-neutral surface:
- audio_start/audio_stop tx use radio.start_tx()/stop_tx() when present,
with a single explicit legacy per-codec fallback helper
(_legacy_tx_lifecycle) for non-AudioTransport radios.
- All radio pushes in _handle_tx_audio route through radio.push_tx(...)
(legacy push_audio_tx_pcm/opus fallback); browser-codec branching and
transcode decisions still key off _tx_codec().
- _tx_codec() preference reordered: first-class audio_tx_codec ->
contract tx_codec -> audio_codec.
- _tx_sample_rate() unified with the half-duplicated fallback chain in
_ensure_tx_transcoder (contract -> radio.audio_sample_rate -> 48000).
- Double-start tolerance broadened (MOR-541 review note): besides the
LAN stream's "Already transmitting", the USB driver's
AudioDriverLifecycleError("TX stream already started.") is now treated
as the benign poller-first reuse case instead of re-raising and
closing the audio WebSocket.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Agent Review: PASS Independent review of 1. Live-TX-path parity (per backend, vs main)
2.
|
Summary
Step 11/12 of the MOR-532 AudioTransport epic (Linear MOR-544). Moves the web audio handler's TX path onto the codec/transport-neutral
AudioTransportsurface, with explicit legacy fallback for non-AudioTransport radios. RX side was already neutral;AudioFftScopeuntouched.Changes (
src/rigplane/web/handlers/audio.py)audio_start/audio_stop direction=txnow callradio.start_tx()/radio.stop_tx()when the radio exposes the neutral surface (detected viagetattr, same pattern as the MOR-543 poller migration). One explicit legacy helper_legacy_tx_lifecycle("start"|"stop")preserves the previous per-codecstart_audio_tx_pcm(sample_rate=...)/start_audio_tx_opus()branching._handle_tx_audioroute throughradio.push_tx(...)via_push_tx(data, legacy_method=...); browser-codec branching (PCM vs Opus from the browser) and transcode decisions still key off_tx_codec()._tx_codec()ordering: first-classaudio_tx_codecproperty →audio_stream_contract.tx_codec→audio_codec. (For FTX-1 this keeps selecting PCM either way; for Icom LAN the property reads the same contract.)_tx_sample_rate()unified with the half-duplicated fallback in_ensure_tx_transcoder: one helper,contract.tx_sample_rate_hz→radio.audio_sample_rate→ 48000."Already transmitting"(LAN stream). The USB driver raisesAudioDriverLifecycleError("TX stream already started.")— a poller-first double start on USB radios re-raised and closed the audio WS._is_benign_tx_restart()now accepts "already transmitting" / "already started" case-insensitively.Tests (
tests/test_handlers_coverage.py, +7, TDD red→green)audio_start tx→start_tx()(no per-codec methods touched); stop →stop_tx()._tx_codec()ordering: first-class wins over contract; contract-only;audio_codecfallback.push_txreceives bytes identical to whatpush_audio_tx_pcmreceived on the legacy path.AudioDriverLifecycleError("TX stream already started.")on start → treated as already-transmitting,_tx_active=True, no raise (WS stays open).4 of the new tests failed before the implementation (red), all pass after; existing handler tests pass unmodified.
Gate results (worktree)
uv run pytest tests/ -q --tb=short --ignore=tests/integration— 7352 passed, 9 skipped, 3 deselected, 11 xfailed, 1 xpassed, 0 failuresuv run ruff check src/ tests/— clean;uv run ruff format --check— 551 files already formatteduv run mypy src/— 21 errors in 8 files, identical to baseline (zero inaudio.py; zero new)uv run lint-imports— 4 kept, 0 broken🤖 Generated with Claude Code