fix(audio): dax_rx no longer overwrites the speaker stream's id (#34) - #43
Conversation
Starting WSJT-X collapsed AetherSDR's own reception while WSJT-X stayed
healthy. The issue hypothesised contention -- two consumers draining one
`_audio_q`. It was not that. There was only ever one consumer.
Both `stream create` branches assigned the same field:
remote_audio_rx -> self.audio_stream_id = sid (engine.py:1484)
dax_rx -> self.audio_stream_id = sid (engine.py:1506)
and the audio thread addressed every frame to that single value (:2295). So
arming dax_rx did not ADD a consumer -- it OVERWROTE the speaker's id. From
that moment every frame went to the DAX stream and remote_audio_rx received
nothing. AE went deaf; WSJT-X was not winning a race, it was the only
addressee left. `stream remove` had the mirror defect: removing DAX matched
the one id and stopped the audio thread outright.
Fix: keep a stream id PER TYPE (`audio_streams`), and emit each generated
frame to every registered stream from ONE get_audio() call. Calling
get_audio() per stream would pop the single _audio_q twice and starve the
demod -- i.e. it would make the issue's original hypothesis come true. Each
stream carries its own VITA sequence counter; a shared one makes AE see
1-in-N gaps on every stream as soon as a second is armed. `stream remove`
now drops only the id named and stops the thread only when nothing is left.
`audio_stream_id` is kept as the most-recently-registered stream for the
single-stream path and for logging, and falls back to a surviving stream
rather than to None.
Test proven by mutation, not by a green run: reverting _Streams to the old
single-id behaviour fails 4 checks and exits 1 ("the speaker is STILL
registered", "speaker is a target"). Registered in the hand-maintained CI
list -- otherwise it is silently skipped. Full stdlib suite: 24/24.
NOT verified on hardware. The flattened AE noise floor reported in #34 is a
SEPARATE symptom and is not addressed here: the panadapter FFT reads
`_latest` (adapter.py:326) while audio pops `_audio_q` (:513), so audio
routing cannot flatten the FFT. That half needs its own investigation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verified on hardware — the "not verified" caveat in the PR body is now retiredRun on the Pi5 gate ( Rather than drive AE and WSJT-X by hand, the probe speaks the Flex protocol directly and counts VITA audio packets per stream id, which measures the thing that actually matters: which stream is being addressed. Before the fix — gate at
|
Fixes #34.
What was actually wrong
The issue hypothesised contention — two consumers draining one
_audio_q. That is not what happens; there is only ever one consumer. Bothstream createbranches assigned the same field:and the audio thread addressed every frame to that single value (
:2295).So arming
dax_rxdid not add a consumer — it overwrote the speaker's stream id. From that moment every frame went to DAX andremote_audio_rxreceived nothing. AE was not losing a race; WSJT-X was simply the only addressee left. That is exactly the reported asymmetry: WSJT-X healthy, AE starved.stream removehad the mirror defect — it compared against the one id, so removing DAX stopped the audio thread outright.The fix
audio_streams: {type -> sid}).get_audio()call. Calling it per stream would pop the single_audio_qtwice and starve the demod — i.e. it would make the issue's original hypothesis come true.stream removedrops only the id named, clearsdax_channelonly for DAX, and stops the audio thread only when nothing is left.audio_stream_idis kept as the most-recently-registered stream (single-stream path, logging) and now falls back to a survivor rather than toNone.How it is proven
By mutation, not by a green run. Reverting
_Streamsto the old single-id behaviour makes the new suite fail 4 checks and exit 1 — on precisely the right ones:The suite covers: DAX not evicting the speaker; one frame reaching both; per-stream counters not skipping; removing DAX leaving the speaker registered and running; removing the last stream doing stop it; remove-then-rearm on a different channel; and an unknown id being ignored.
Registered in the hand-maintained CI list in
tests.yml— a new stdlib suite is silently skipped otherwise. Full stdlib suite: 24/24.What this does NOT do
⚠ Not verified on hardware. The static defect is certain, but the live repro (Pi5 gate + Radioberry + WSJT-X on 14.074) has not been re-run. Worth doing before merge — the gate is available.
⚠ The flattened noise floor is a separate symptom and is not fixed here.
_latest(panadapter FFT,adapters/hpsdr/adapter.py:326) and_audio_q(demod,:327) are independent;get_audio()pops_audio_q(:513-514) and never touches_latest, so audio routing cannot flatten the FFT. That half needs its own investigation, and I would rather leave #34's second symptom open than let this PR imply it is solved.⚠ Also unverified: whether #34 still reproduced at all before this change.
c8e79b0("bound the demodulator's IQ backlog in time, not blocks") landed in this path after the issue was filed, among ~78 commits between v0.3.0 and v0.5.1.🤖 Generated with Claude Code