feat(icom): transmit-audio taps that bracket the RS-BA1 seam, and the fixes they needed (#5011) - #5058
feat(icom): transmit-audio taps that bracket the RS-BA1 seam, and the fixes they needed (#5011)#5058nigelfenton wants to merge 14 commits into
Conversation
Update — three more commits, and the headline finding has changedSince this was opened I put the taps on a real IC-9700 and they disagreed with each other. Chasing that turned up three separate defects in the instrumentation itself, and the conclusion the taps were pointing at turned out not to be supported.
|
| run | first frame differing from the post tap | wire length vs post |
|---|---|---|
| 1 | frame 8 (160 ms) | −828 samples |
| 2 | frame 24 (480 ms) | +3264 samples |
— while the post-resample capture was 86016 samples both times. A capture whose length varies for a transmission of constant length is an instrument fault. Now latched from key-down until the packetiser has drained.
a9f59663 — the tap was recording retransmits
sendRaw() is also the replay path: a retransmit is served by sendRaw(*it) on the retained copy, and sendRawTwice() writes the same bytes twice. So every packet the radio asked for again was recorded again, out of order, interleaved with the live stream.
isAudioData() correctly rejects keepalives, pings and retransmit requests — but the replay of a genuine audio payload is audio and passes. Moved into sendTrackedImpl() after the first sendRaw(), gated on isPayload. Byte-identical run grew 8 → 29 frames; inverted frames 4 → 0; length error +11244 → −2496.
3035877f — the packetiser test was measuring its own shortcut
icom_tx_resample_ax25_test handed the whole 64960-byte burst over in one submit(), so the 24000-byte cap discarded the front immediately and the test reported 40960 bytes lost — 63% of the burst. That number is an artefact. Fed at the real cadence (30 chunks × 20 ms against the 10 ms pump):
bulk submit-then-drain 23040 bytes out, 40960 dropped (63%)
paced 20 ms in / 10 ms out 63360 bytes out, 1600 residue, none dropped
The 250 ms cap never bites on a real transmission. The residue is the tail of a burst that is not a whole number of frames (64960 / 1920 = 33.83), and refusing to emit a short frame is deliberate.
This commit also adds live-path counters — txSubmitCalls, txSubmitSamples, txFramesSent, txPumpTicks, txPumpEmpty — so feed-vs-drain can be measured on a real burst instead of inferred.
What the counters then showed
submitted 26 call(s)/34432 samples (717.3 ms), frames sent 35 (700 ms), pending=0
97.6% of submitted audio left as whole frames, nothing dropped, nothing pending. The 832-sample shortfall is exactly the partial tail frame the offline test predicts.
So: the modem hands over the whole burst and the packetiser sends all of it. If 35 whole frames reached sendTracked(), the datagrams carried the whole transmission — which means the wire capture's "preamble repeated, data frames missing" is not what went on the air.
Correcting the PR body above
The original body says the taps bracket a real fault. On the evidence now, they do not — every "the wire is corrupt" reading has dissolved under inspection, and AE's client TX chain measures clean by two independent methods:
- resample ratio 1.0000
- LPCM worst error 0.000026 (quantum 0.000031)
- packetiser loses nothing when paced
- feed and drain agree on live hardware
- the post-resample capture decodes in Direwolf every single run (
G0JKN>K3CAL-1, level 70, 19–20/20)
I am not claiming AE corrupts transmit audio, and I would not want this merged as if it demonstrated that. What it demonstrates is the opposite, which is still worth having: the chain is exonerated, and the remaining suspects for #5011 are downstream — the radio's own handling of the RS-BA1 LAN audio, or the deviation path, which is where the issue's original observation (deviation pinned, LAN MOD Level has no effect) already pointed.
⚠ Still Windows/MSVC only, still one radio. And the wire tap, while much better, has now been wrong three ways — I would treat its output as indicative rather than authoritative until someone reproduces it on other hardware.
|
Filed #5060 for the part of this that is not fixed here: after both tap fixes in this PR, |
|
Thanks for this — and especially for the follow-up comment correcting your own headline finding. Retracting a conclusion because your instruments disagreed with each other is the hard part, and you did it with the measurements attached. That's worth more to the project than the fix would have been. On the CI failure at What CI actually shows
On macOS, I could not retrieve the raw compiler output — my tooling only exposes step-level status for this run, not the log text — so I'm not going to invent an error message for you. What I can do is narrow it, and I've spent the effort to rule things out rather than speculate. What I ruled out (so you don't have to)I merged your branch into current
So the break isn't any of the obvious candidates. My local Qt is 6.11.1; CI pins 6.8.3 on macOS and Windows ( The thing I'd fix first: your base is 23 commits staleYou verified against v26.8.3 (
Git merges all of that without a textual conflict, which is precisely how a semantic break gets in unnoticed. Rebase onto A real defect regardless:
|
66901a7 to
8b8310e
Compare
|
Rebased onto The first compiler error, and the root causeYou were right that the version gap mattered, but the mechanism turned out to be an include, not semantic drift: Both headers declared a The rebase
The split you suggestedDone: the One heads-up on framing: per the PTT-timing analysis in #5011, the audio fault these taps were built to find likely does not exist — the failure is in when audio starts relative to the radio keying, not in the samples. I'll re-scope this PR's description around timing instrumentation once that discussion settles, rather than let it keep implying an audio-seam cause. |
8b8310e to
4d4ea4b
Compare
`modem txprobe [text]` builds what WOULD be transmitted and reports each seam
of the chain separately — build, frame, modulate, txgate, route — without
keying the radio.
The failure it exists for: AE keys the transmitter, real RF goes out, and the
receiving modem sees no AX.25 frame structure at all (rejectBadFcs stays 0
against thousands of HDLC candidates) while AE's own txBytes and txAttempts
report 0. Those three symptoms are equally consistent with "the modulator emits
nothing coherent" and with "no bytes ever reached the modulator". A measurement
of the whole chain cannot tell those apart; a measurement of each seam can.
It reports peak/RMS dBFS of the modulated PCM, so digital silence is named
rather than left to be inferred from a sample count; the queue gate's inputs
individually, so a deferred transmit says WHICH condition held it; and the
audio route to the radio (hostModulates / takesTxAudioOverSeam / bypassesDax /
txStreamId), which is what differs between backends.
Deliberately does not transmit and is not TX-gated: a diagnostic should not
need the permission whose absence it may be diagnosing, and it stays usable
with no radio connected and off the air.
Proven on hardware 2026-08-13. It killed three standing hypotheses about an
open AX.25 transmit bug:
- "the HL2 waits for a DAX stream that never arrives" — route reports
bypassesDax=true; it never waits.
- "the TX FIFO is not fed" — underflow is true only at idle.
- "the modulator produces silence" — modulate reports peak -9.1 dBFS, and
the HL2's own txMicPeakDbfs reads -9.119 under key, i.e. the audio
arriving at the radio is bit-for-bit the modulator's output.
Run against an IC-9700 (seam route) and an HL2 (direct route) it reports the
two different transmit routes explicitly, which is how the remaining fault was
narrowed to shared code above the backend.
`txaudio` keeps a running peak/RMS and deliberately discards the samples. That answered "did audio reach the backend" but could not answer the two questions that actually mattered while chasing an AX.25 transmission that modulated correctly yet decoded nowhere: - is the waveform clipped? - does the burst ever change from preamble flags into data? Both are shapes. A level is an average, and it hid the difference: the tap read a healthy -9.1 dBFS for a keyed burst AND for a stream of digital silence. Same number, entirely different content. txwave arm | save <path> | off records the int16 samples handed to RadioModel::submitTxAudio and writes a stereo WAV. Capacity is reserved when armed and capture stops at the cap, so the audio path never allocates. It records the FIRST samples of a burst rather than the last, because the preamble-to-data transition is at the start and that transition is the thing under test. Like txaudio and txmonitor it is an observer: it keys nothing, so it is not TX-gated. Gating an observer behind the permission whose effects it observes would make the failure it exists for unreachable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
txAudioBypassesDax() is an OR of two different reasons — the HOST runs the modulator (HL2: hostModulates=true), or the audio crosses the SEAM to a radio that modulates it (Icom: hostModulates=false, takesTxAudioOverSeam= true). The operator-facing log line reported both as "host-modulated (no DAX stream)", which describes the HL2's case on a radio where the flag is false. Not cosmetic in practice. Chasing an AX.25 transmit fault on an IC-9700, this line said "host-modulated (no DAX stream)" while `modem txprobe` reported hostModulates:false for the same transmission. Read together they look like a contradiction in the transmit path, and it cost a diversion looking for a missing DAX stream on a radio that has never had one. The internal qCInfo on the next line was already correct (route=seam/host); only the line the operator reads was wrong. The HL2 keeps its existing text because hostModulates is tested first. Routing behaviour is unchanged — this touches the label only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Direwolf's atest decodes a `txwave` recording of AE's transmit audio perfectly — 3 of 3 AX.25 frames, ideal level, mark/space 20/20 — while the same transmission off the air decodes nowhere: audible on a Kenwood, and a known-good decoder on an independent radio and interface gets nothing. So the fault lives between RadioModel::submitTxAudio and the antenna. `txwave arm post` captures at the far end of the client's half of that stretch: after IcomCivBackend's 24k->48k resample, on the line before IcomSession::sendAudio, which is the last point AE sees its own audio. Everything past it is the LPCM codec, UDP and the radio. Both taps feed the same recorder, so the run is unchanged — `txwave arm post` -> transmit -> `txwave save <path>` -> `atest -B 1200` — and the two captures answer one question between them: decodable after the resampler clears the client entirely, undecodable indicts the resampler directly above the tap. Icom-only, because it is the one backend that resamples on the way out. A no-op on every other family rather than an error: asking a Flex for its post-resample audio is a question with no referent. Off unless armed, read with a relaxed atomic, and it keys nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dio silently TxPacketizer::submit() discards the OLDEST bytes past its 250 ms cap. For voice that is correct and the comment at the drop site argues it well: latency must not grow, and the freshest audio is what matters. For a digital burst it is destructive in a way nothing reports — the oldest bytes of an AX.25 transmission are the preamble and the opening flag, so what survives keys the radio, sounds exactly like packet on a receiver, and syncs on nothing. Nothing surfaced that. No error, no gap, no counter: a transmission that lost 63 % of itself looked identical to one that did not. submit() now counts dropped bytes and overflow events; IcomSession::Stats carries them; onLinkTick reports new drops at WARNING with the running total and the pending/cap ratio, on the edge rather than per drop (one oversized submit sheds thousands of bytes in a single loop). Counting rather than logging inside IcomAudio.cpp keeps that file pure protocol — no QObject, no Qt logging — which is also what lets the unit test link it standalone. The test asserts the counter against the arithmetic (droppedBytes == the bytes actually missing, 40960 of 64960 submitted) rather than trusting it, because an instrument that under-reports here would turn a lost preamble back into silence. This is diagnosis, not a fix: the drop rule is unchanged and a burst that overflows still loses its front. Whether production reaches it depends on pacing — onTxPaceTick ships a larger chunk when a tick lands late — and this is what will answer that question from a real transmission. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`room` is a raw capacity difference and can be odd. An odd `take` writes take/2 pairs into take slots — one sample left uninitialised, and the next block starting at an odd offset, which swaps left and right for the rest of the capture and makes the WAV undecodable however good the audio was. Not observed: the capacity and the arriving block sizes are both even, so `room` stayed even and both existing captures have L == R on every frame. Fixed anyway because this is a diagnostic whose entire value is that its output can be trusted — verified separately by pushing known-good AFSK through the tap's exact conversion (float -> clamp -> lround(v*32767) -> duplicate) and confirming atest still decodes 3/3 at level 70, 20/20. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tried 2026-08-15 and it is wrong: the modem's own AFSK reaches txFinalMonitorPcmReady too, via sendModemTxAudio() -> feedDaxTxAudioInternal(), so the gate silences the MODEM rather than the microphone. Measured on air as "RF power but no modulation". The comment stays so the next reader does not repeat it. If the two sources ever do need separating, it has to happen upstream of this shared tap, where they are still distinguishable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dd a wire tap `txwave` exists so that "is the transmitted audio right?" can be answered with samples rather than levels. Three defects meant its own captures could not be trusted, and all three are the same shape: several taps at DIFFERENT SAMPLE RATES sharing one recording buffer and one rate stamp. 1. The post-resample tap and the pre-resample path both wrote `m_txAudioRecordBuffer` and `m_txAudioTapSampleRate`. `txwave arm post` also arms the level tap, which runs the pre-resample path, so a "post" capture interleaved 24 kHz and 48 kHz samples and was stamped with whichever wrote last. The resulting WAV decodes as nothing no matter how good the audio was. 2. `setTxPostResampleTapEnabled()` and `setTxWireTapEnabled()` both wrote one boolean, and AutomationServer calls both on every arm — so `arm post` set the claim and the very next line cleared it. Symptom: a `post` capture came back at 24000 Hz / 16 s instead of 48000 Hz / ~1.8 s. Replaced with a three-state owner (`PreResample` / `PostResample` / `Wire`): one variable, one owner, each setter releasing only its own claim, and each recording lambda checking ownership rather than assuming its connection implies it. 3. The new wire tap (below) emitted from the socket write path ungated, at ~100 packets/s, allocating per datagram. Now gated on `m_keyed || m_tuning`, exactly as `submitTxAudio` is. Also adds two taps that the AX.25 investigation needed and could not get: * `txwave arm wire` — the payload of each outbound UDP datagram, captured after the socket write, so it reports what was handed to the socket rather than what was about to be. Reuses `isAudioData()`/`audioPayload()` (structural, not a size whitelist, so keepalives and the serial stream fail it) and decodes with the same `decodeAudio()` the receive path uses — any difference from the post-resample tap is then a difference in the BYTES, not in how they were interpreted. * `txwave arm dual` — post-resample AND wire from ONE transmission, into separate buffers (`save` writes post, `save2` writes wire). Comparing two keyings cannot distinguish "the wire alters the audio" from "those were different bursts"; this can. `save`/`save2` share one WAV writer deliberately: a second copy of the header code is how the two files would drift into being not-comparable, which is the whole point of capturing them together. `save` does not disarm during a dual capture, or the first save would discard the wire buffer before `save2` could read it. Verified on hardware (IC-9700 over LAN, 145.070 DFM): `arm dual` on a single transmission gives post-resample and wire captures that are byte-identical — 4096/4096 samples exact, max diff 0 — which is the result the collisions had been hiding. Before the ownership fix the same command returned a 24 kHz 16 s capture; after it, 48 kHz 1.9 s as intended. The one-line comment on `kTxAfskAmplitude` records a measurement made with these taps and changes no behaviour: raising it 0.35 -> 0.90 moves the modulator by the expected +8.2 dB and does not change the transmitted deviation at all on that path. See aethersdr#5011. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wire tap gated on the INSTANTANEOUS key state, which truncated the capture at a point that moved from run to run — so the instrument, not the transmission, was what changed. Unkey races the packetiser drain: setKeying(false) clears m_keyed and only then calls flushTxAudio(), while onTxPump() fires every 10 ms in between, so a variable number of frames escape after the tap has already stopped recording. Measured on an IC-9700, two identical AX.25 bursts at 145.070 DFM: run first frame differing from the post-resample tap wire length 1 frame 8 (160 ms) -828 samples 2 frame 24 (480 ms) +3264 samples while the post-resample capture was 86016 samples both times. A capture whose LENGTH varies for a transmission of constant length is an instrument fault — and it reads exactly like "the wire is corrupt", which is the conclusion it nearly produced. Fix: latch on at key-down and hold until the packetiser has actually drained (txPendingBytes == 0). The burst is captured whole, and the inter-burst stream still never reaches the recording — which is what the original gate existed to prevent, and why an ungated tap is not the answer: measured 96.7 % non-zero over a 2.1 s capture, which atest cannot sync on. icom_backend_test, icom_civ_test, icom_civ_scheduler_test and icom_tx_resample_ax25_test all pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ere double-recorded
sendRaw() is also the REPLAY path. A retransmit request is served by
sendRaw(*it) on the retained copy, and sendRawTwice() deliberately writes
the same bytes twice. The wire tap sat in sendRaw(), so every packet the
radio asked for again was recorded again — out of order and interleaved
with the live stream.
In a capture that is indistinguishable from corrupted audio, and it is
not what went on the air once.
Measured on an IC-9700 over LAN, one AX.25 connect attempt (3 retries):
post-resample capture 185152 samples Direwolf decodes 4 of 5 bursts
wire capture 196396 samples Direwolf decodes 0 of 3 bursts
+11244 (+234 ms)
with duplicated and reordered 20 ms frames, and 4 frames that were the
exact sample-wise NEGATION of the post capture. The burst splitter used
to measure that was validated on the post capture first, where it
recovers 4 decodable bursts, so the wire failures were not an artefact
of how the audio was cut.
Moved into sendTrackedImpl() after the first sendRaw(), gated on
isPayload, so each payload is recorded exactly once in send order.
Retransmits, keepalives, pings and the serial stream still never reach
it — isAudioData() rejected those already, but the replay of a genuine
audio payload passed that test, which is precisely the case this misses
when the tap sits lower down.
icom_backend_test, icom_civ_test, icom_protocol_test, icom_session_test,
icom_civ_scheduler_test and icom_tx_resample_ax25_test all pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…isible
The packetiser can be starved as well as overrun, and starvation is the
quieter failure: onTxPump() simply sends nothing on a tick where
takeFrame() has less than a whole 20 ms frame, so the radio's jitter
buffer sees a gap rather than an error. On an AX.25 burst that gap lands
in the middle of a frame whose preamble has already gone out.
Nothing reported it, because an empty takeFrame() is indistinguishable
from "nothing to send". These counters make the two separable:
txSubmitCalls / txSubmitSamples what the modem HANDED US
txFramesSent whole 20 ms frames that reached the wire
txPumpTicks / txPumpEmpty pump ticks while keyed, and how many
found less than a frame ready
A burst whose audio is 597 ms should yield ~30 frames; markedly fewer,
with txPumpEmpty climbing, is starvation rather than loss.
resetTxFlowCounters() zeroes them so one transmission can be measured
alone.
Counting only — no behaviour change on the transmit path.
…plicates A count of blocks cannot detect the same block arriving twice, and that is the failure under investigation. Captured off the wire with tshark on 2026-08-17, one AX.25 burst left the machine as 35 frames of which only ELEVEN were distinct: the two preamble frames repeated 24 times in place of the information field. Every level, tone and block-count measurement called that healthy, because the blocks really were submitted — they were duplicates. Two hash points, deliberately bracketing the resampler: RadioModel::noteTxAudioSubmission() what the backend was GIVEN IcomCivBackend before sendAudio() what it HANDS ON, after 24k->48k Each reports total blocks, DISTINCT blocks, and consecutive repeats. Consecutive repeats are the signature of a stalled producer re-offering its last buffer; a low distinct count is the signature of a burst whose payload never arrived at all. Different faults, so both are counted. Comparing the two distinct-counts localises a duplication to one side of the resampler with no radio involved. It also settles a contradiction: the post-resample tap shows all 89 frames the modulator built while the wire shows 11, and the packetiser between them measures clean offline — so one of those observations is not what it appears. FNV-1a over the PCM: cheap, allocation-free, adequate for "is this byte-for-byte the block I just saw". This runs on the audio path, so it does not allocate — the set insert is the one exception and only grows to the number of distinct blocks in a burst. Surfaced as distinctBlocks/repeatedBlocks in txAudioTapSnapshot (so `txaudio status` reports them) and appended to the Icom TX flow log line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both headers declare a std::set member with no <set> include: IcomCivBackend.h std::set<std::uint64_t> m_txOutHashes RadioModel.h std::set<std::uint64_t> m_txAudioTapHashes They compile locally and fail on CI, which is the whole tell. Local Qt here is 6.10.3 and CI pins 6.8.3 (ci.yml); 6.10.3's headers happen to drag <set> in transitively and 6.8.3's do not, so the declaration was resting on a Qt implementation detail rather than on an include. Verified rather than assumed: stripping the include again on this box still compiles clean, so the local build genuinely cannot reproduce the failure. What does reproduce it is a standalone TU declaring the same member without <set> -- MSVC emits exactly the four errors CI reported, in order: error C2039: 'set': is not a member of 'std' error C7568: argument list missing after assumed function template 'set' error C2062: type '<error>' unexpected error C2238: unexpected token(s) preceding ';' That is the reported break in full, so this is the cause and not merely a plausible candidate. Both headers now also compile standalone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4d4ea4b to
97436d3
Compare
|
Rebased onto current Both conflicts were in m_counters.txBytes += static_cast<quint64>(n);
++m_counters.txPackets; // #5274
m_lastTxAtMs = m_activityClock.elapsed(); // #5274This branch has two commits that move through the same lines in sequence — Verified rather than assumed: the branch's diff against I left a comment at the site explaining why the two are treated differently, since the distinction is easy to lose next time someone edits this function:
A question for @jensenpat, not a finding. That distinction is exactly why the tap moved. Build and tests on the rebased branch: |
|
Bump for review — and an honest note on why this one has sat for eleven days with no human look. It is big: 1,438 additions across 17 files, 14 commits. That is a lot to ask of a reviewer, so here is the shape of it, what is safe to skim, and where I actually want eyes. What this isTransmit-audio taps that bracket the RS-BA1 seam (#5011) — instrumentation to find where AX.25 TX audio was being corrupted between AetherSDR and an IC-9700. The headline finding was retracted, by me, in this thread. The taps disagreed with each other on real hardware; chasing that turned up three defects in the instrumentation itself, and the conclusion they had been pointing at was not supported. The client TX path measured clean. What is left in this branch is the tap fixes and the taps themselves — not a claim that AetherSDR corrupts the wire. The part that is still not fixed is filed separately as #5060 ( Where to actually look
It can be shrunk
If a reviewer would rather I split this further before spending time on it, say so and I will. It is instrumentation, so it divides fairly cleanly. State
Not verifiedNo IC-9700 on the bench for this push, so the taps are not re-measured on hardware since the rebase; the measurements in this thread stand as originally reported. The @jeremymturner @jensenpat @ten9876 — whenever there is a moment. Happy to split it if that makes it reviewable. |
|
Superceded by #5311 |
Closes nothing — this is the instrumentation that made #5011 measurable, plus the fixes those instruments needed in order not to lie. The taps have done their job on my bench; what I am asking for is a maintainer's read on whether this shape belongs in the tree, and a direction on the one architecture question below (
submitTxAudio()as a shared funnel) that I have deliberately not answered myself.Why taps and fixes are one PR
They cannot be split. Three commits repair the taps corrupting each other's captures, and the code they repair is not on
main— cherry-picking the fixes alone fails to compile (m_txPostResampleTapEnabled: undeclared identifier).What this is for
#5011: AX.25 transmit over RS-BA1 LAN audio is loud, on frequency, with correct tones, and undecodable by any TNC. Nothing in the tree could say why, because every existing instrument reports levels, and a level cannot distinguish a correct burst from silence — peak and RMS read −9.1 dBFS for both. These commits add the ability to record samples, at two points that bracket the seam.
What the instruments found
1. AE's client TX path is clean, end to end.
Direwolf's
atestdecodes AE's own transmit audio 3/3 frames when captured at the tap. This is settled and should not be re-tested.2. The fault is radio-side. The 08-20 off-air bench (RTL-SDR V4 as an independent receiver, IC-9700 into a dummy load) found three front-panel faults invisible to AE — DATA MOD input on
ACC(radio radiated a dead carrier: RF present, 0 Hz content), TX bandwidth onDX+, and drive level wrong in both directions. AE's own post-resample tap decoded 3/3 in that same session. Note AE deliberately never writesmod.input.data(radio-authoritative per its owncontrols map), so AE cannot detect or fix that class of fault.3. The remaining fault is a deviation shortfall, and it is not PTT timing. After ten runs: tones correct (1175/2224 Hz), balance flat, four clean bursts per run, but deviation stuck at ~370 Hz RMS / 650 Hz p99.5 where 1200-baud packet wants ~2500–3000 Hz. Separately, the space tone reads 2224 Hz at the client tap and 1775 Hz off-air — a spectral discrepancy the taps are what surfaced.
Pat's PTT/CI-V timing defect is real and worth landing on its own merits, but it is excluded as the cause here: re-tested live 08-24 with the fix in place, decode still fails.
4.
submitTxAudio()is a shared funnel. InMainWindow.cpp, the mic-driventxFinalMonitorPcmReadypath and AetherModem's AX.25 audio both reach the radio's single modulator input and sum there. A capture at the far end contains two interleaved sources in ~0.9 s blocks — the modem's AFSK (peak ~11476, energy 1180–1330 Hz) and the mic path (peak ~16000, broadband, dominant frequency wandering 770–1200 Hz every 100 ms).atestdecodes the modem's audio alone perfectly and nothing from the mixture.The comment there also records a wrong fix that was tried and measured: gating the feed on
isDaxTxMode()silences the modem, not the microphone, because the modem's AFSK reaches the same signal viasendModemTxAudio() → feedDaxTxAudioInternal() → txFinalMonitorPcmReady. On air that presents as "RF power but no modulation". I have deliberately not applied a separation fix — it has to happen upstream of the shared tap where the two sources are still distinguishable, and that is the architecture decision I would rather a maintainer direct than guess at.Using these taps: they no-op silently on a non-native backend
The taps arm only behind a
qobject_cast<IcomCivBackend*>. Connecting via Aether-gate — which presents as a FLEX-6700 — makes them silently no-op: three real-RF trigger attempts recorded zero samples with no error of any kind. Confirmget radioreports the nativeicom:<ip>before trusting a "nothing recorded" result. This has cost time twice.Commits
feat(modem)— a non-keying transmit-chain probe for AetherModemfeat(modem)— record TX-audio samples, not just their levelfix(automation)— register the TX final monitor sotxmonitorhas a sourcefix(ax25)— name the transmit route that actually firedfeat(icom)— tap transmit audio after the resampler, to bracket the seamfeat(icom)— count and report TX packetiser drops instead of losing audio silentlyfix(icom)— round the post-resample tap's copy to a whole stereo pairdocs(ax25)— warn against gating the TX seam feed onisDaxTxMode(code comments only — nodocs/file)fix(icom)— stop txwave's taps corrupting each other's captures, and add a wire tapThe packetiser drop counter is the one behaviour change beyond instrumentation:
submit()drops the oldest bytes past its 250 ms cap — right for voice, destructive for a digital burst where the oldest bytes are the preamble and opening flag. Previously silent: no error, no gap in the audio. It now warns on the edge with a running total.That counter work is split out as #5162 and is awaiting review; this branch keeps the production counters it needs and no longer carries their tests.
Test suite split out too. The four
test(icom)commits that were here are now a standalone branch (test/icom-tx-resample-ax25), opening as its own PR once #5162 lands — the test file includes onlyResampler.handIcomAudio.h, references no tap symbol, and depends on #5162'sdroppedBytes()/dropEvents(). That takes this PR from 1938 lines to 1431, leaving the taps and the fixes they needed — the part that actually needs the architecture call.Verification
Rebased onto current
main(56edc21b) and built clean on Windows/MSVC —AetherSDR.exelinks, exit 0.Icom and AX.25 suites on this branch, run rather than assumed — 12/12 pass:
The end-to-end TX-chain measurements this PR's instruments produced (resample ratio 1.0000, LPCM worst error 0.000026 against a 0.000031 quantum, AFSK tones preserved, drop counters 40960 bytes / 1 event) now live in the split-out test branch, not here — see the split note above.
⚠
icom_backend_testandicom_session_testare declared intests/tests.cmakebut do not reach the generated build on this configuration, so they were not exercised. That is a pre-existing property of the tree on Windows, not something this branch changes — flagging it rather than listing them as passing.What is not done
mainas of this edit; will rebase before marking ready.