aethersdr / aethersdr/AetherSDR
[RFC] Isolate TCI RX/TX audio from UI scheduling across Flex, HL2 and Icom
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 221
- Forks
- 117
- Avg merge
- 2d 9h
- Merged PRs (30d)
- 302
Description
Problem and tracking purpose
TCI digital-mode RX/TX must retain continuous audio while the desktop UI is busy. Today UI/event backlog delays audio delivery and TX_CHRONO requests, which can starve or bunch audio and disrupt data communications. This RFC tracks a bounded solution across FlexRadio, Hermes-Lite 2 and Icom, including implementation milestones and separate platform/radio validation.
Related user report: #5133; related multi-receiver load case: #5340. This is an architecture tracker, not a claim that every reported RF spur has the same cause or that a fix has already been demonstrated.
Design status: proposed, for maintainer review under the RFC process. No implementation PR has been opened for this plan.
Research baseline: c2a13228c0236a9065f487fd3cc09f7a6f1ccd67, refreshed origin/main, September 13, 2026.
Research branch: research/tci-thread-isolation
Status: research and design only. No application source changes, build, tests, simulator run, or live-radio operation. Findings below distinguish current source from proposed behavior. Historical measurements have not been reused as current validation.
Recommendation
Give TCI's network and streaming work one dedicated event-loop thread, retain a small controller beside RadioModel, and connect streaming to backend-owned audio endpoints that do not relay samples through the UI. Preserve each family's existing DSP, wire formats and radio authority.
Moving the current TciServer object wholesale is unsafe: its roughly 4,400 lines combine WebSockets, stream conversion, model reads, routing mutations, PTT ownership, settings, and lifecycle. Moving only its timer is insufficient: both the socket and downstream audio can still wait for the UI.
The smallest complete scope is continuous TCI RX/TX audio through Flex, HL2 and Icom during UI stalls. It requires one TCI worker per session and, for Icom, a private audio worker. Flex and HL2 reuse their existing workers. This is not a whole-engine migration or a new general media framework.
What the research baseline establishes
| Path | Current execution | Consequence |
|---|---|---|
| TCI network and TX_CHRONO | Constructed in MainWindow; server, sockets and timers inherit UI affinity | A UI stall prevents receiving TX_AUDIO and sending RX audio or new audio requests |
| TCI request pacing | 5 ms timer, elapsed-time accumulator, unbounded catch-up loop; nominal block is 1,024 stereo frames at 48 kHz, about 21.333 ms | After a stall the application can issue many requests together; average sample rate alone cannot establish continuity |
| Flex RX | PanadapterStream network worker → TciServer on UI | Independent acquisition exists, but TCI conversion and WebSocket delivery stall |
| Flex TX | TciServer on UI → AudioEngine worker → PanadapterStream network worker | After TCI, steady audio has a worker-to-worker path; AudioEngine still packetizes all available blocks in a callback |
| HL2 RX | Metis/RX DSP on hl2-io → Hl2Backend on UI → RadioModel → MainWindow → TCI |
Samples are already available off-thread, but the per-slice publication deliberately returns through UI |
| HL2 TX | TCI → AudioEngine → MainWindow on UI → RadioModel → Hl2Backend → TX DSP/Metis on hl2-io |
Independent DSP/EP2 pacing exists; input can still starve at the UI hop |
| Icom RX | IcomSession/IcomStream socket → assembler → backend resampler → model/GUI relay → TCI, all on UI before AudioEngine | Removing only the last UI hop leaves socket reception and conversion blocked |
| Icom TX | AudioEngine → MainWindow/model/backend on UI → resampler/packetizer → IcomSession pump on UI | Both audio submission and the actual packet clock depend on UI scheduling |
Icom's current pump is already elapsed-time based, nominally one 20 ms frame, with catch-up capped at three frames per tick. The earlier description of draining everything in one callback is obsolete. Preserve this improvement while relocating ownership; evaluate any additional pacing change separately.
The current IRadioBackend contract explicitly requires all existing seam signals, including PCM, to emit on the backend owner's thread. Simply changing those connections to DirectConnection would violate the contract and expose model state across threads. A narrow, explicitly documented data endpoint is necessary.
PcmFrame already carries owned immutable samples, rate/layout, producer/session identity, receiver instance, sample position and a revocable epoch. Reuse it. Currently TCI converts it to an untagged QByteArray at ingress; a threaded design should retain identity until consumption and retain session identity through stateful conversion buffers.
These findings establish a structural scheduling dependency. They do not prove that every reported RF spur has this cause.
Proposed ownership
TciServer controller: existing model thread
Keep the public session/UI facade and model-facing behavior here: receiver mapping, TciRoutingState, model-backed TciProtocol commands, DAX/IQ acquisition and release, radio-confirmed notifications, PTT preflight, settings persistence and client/status snapshots.
Keep the existing routing machinery substantially intact. Replace socket identity in routing requests and continuations with opaque monotonically assigned client IDs plus server/session generations. A socket pointer must never become a cross-thread control handle.
UI calls such as start/stop, port changes and client-list queries need explicit lifecycle semantics. Publish starting/running/stopping/failed and the confirmed bound port; do not report listening merely because a start request was queued. Routine UI queries read controller-owned snapshots. Avoid blocking worker queries.
TciIoWorker: dedicated TCI thread
Own the listener, accepted sockets, WebSocket reads/writes, streaming subscriptions and negotiated formats, RX/TX resamplers and accumulators, TX_CHRONO scheduling, bounded stream queues and counters. Create and close event-driven objects on this thread. All socket writes happen here.
The worker receives copied routing/configuration snapshots and accepted TX-session information. It never dereferences RadioModel, SliceModel, a concrete backend, widgets or AppSettings. Publish meters and monitor traffic in bounded/coalesced batches so streaming does not create a new UI event backlog.
Keep radio commands serialized through the controller. Preserve arrival order with sequence numbers and generation-tagged completions; a stream command requiring DAX setup or a routing change cannot acknowledge ahead of its prerequisites. Commands received while initialization is incomplete must have a bounded waiting state. Preserve one TCI command per outbound text message and existing sender/observer confirmations.
Socket disconnect immediately retires that client's local streaming state. Controller callbacks arriving afterwards cannot restart it. Do not run model-backed TciProtocol from the worker merely because some existing setters happen to use invokeMethod: its reads are synchronous too.
A bounded audio endpoint, installed by the backend
Expose a small backend-neutral data handle established on the model thread. It owns only queues, immutable metadata and revocation state; it exposes no backend QObject, wire object or thread. Backend-private producers/consumers attach internally. Existing backend virtual methods and seam signals retain their affinity contract.
RX is one bounded FIFO per consumer and receiver. TX is an ordered bounded FIFO with explicit source identity and an accepted TX-session token. A producer publishes samples without waiting for the UI or acquiring a UI-held mutex. Use a single-producer/single-consumer queue only where that topology is actually enforced; multiple clients must be serialized before that queue.
Retain PcmFrame through RX handoff, validate its epoch at drain, and reset accumulation/resampler history when identity changes. Do not copy a PcmProducer into multiple threads: one execution context owns production. If a new fast route and an existing model route share a producer, publish the same immutable frame to both, with only one selected input reaching TCI.
TX needs source/session revocation in addition to RX's PcmFrame semantics. Align with the existing TX coordinator and the work in PR #5659; do not create a second authority system. Revalidate at the terminal audio consumer, including queued work and retries. Retiring a client/session must invalidate queued PCM and converted packet/DSP residue, not just stop new enqueues. Preserve clientLeveled so external digital tones do not acquire voice processing or unintended HL2 ALC makeup.
Backend changes
| Family | Proposed bounded change | Preserve |
|---|---|---|
| Flex | Attach the TCI RX endpoint inside the backend at existing DAX production. Deliver TCI TX into AudioEngine without UI mediation; retain its network-worker output. Remove the old TCI feed in the same change | DAX rather than speaker audio; per-channel routing; PC Audio/mute independence; DAX holds and IQ ownership; both existing TX routes |
| HL2 | Publish per-receiver PCM from its private DSP/I/O context into the endpoint. Consume authorized TX input in the existing TX DSP/I/O context | Stable receiver identity across DDC renumbering; pre-mute/pre-gain RX; exactly one TCI producer; keyed/automation guards; 24 kHz conversion; client-level flag; existing EP2 clock |
| Icom | Extract an audio worker behind IcomCivBackend/IcomSession owning the audio IcomStream, RX assembler, persistent RX/TX conversion, TX packetizer and 20 ms pump | Current 48 kHz mono radio transport and 24 kHz compatibility boundary; LPCM framing; loss concealment; sequence/retry behavior; identity/PTT/TUNE gating; partial-frame completion and drain |
For Icom, retain authentication/session orchestration and CI-V model work on their current owner initially. Binding the audio socket must return its actual local port before the session sends its stream request; stream start/stop and failures need generation-tagged messages. Audio transport statistics become published snapshots, and pad/drain completion becomes an asynchronous barrier. No synchronous getter should reach into the worker's packetizer.
An audio-only extraction minimizes changes to the many synchronous IcomSession control callers. If implementation proves that splitting stream negotiation/lease ownership is more complex than moving the complete RS-BA1 transport behind an owner-thread facade, resolve that in the Icom design increment before coding the move. Moving IcomCivBackend itself is not the alternative: it must remain on the model thread.
Do not change ANAN, RTL, Sim, speaker mixing or IQ capability as part of these family adaptations. They need regression coverage for shared lifecycle changes. Keep the existing 24 kHz TCI behavior; the separate #5468 A4 rate-aware TCI work must be coordinated, not silently folded in.
Pacing, overload and failure semantics
Thread isolation removes UI scheduling as a continuous-media dependency. It does not make a desktop OS, TCP connection or remote client hard real time. Qt also documents that precise timers can run late. Qt timer behavior
Maintain two distinct concepts: client audio requests and backend playout. Request accounting uses actual sample frames/duration and the active owner's replies, not just messages received or a global block counter. Consider queued audio and outstanding requests together. Bound startup silence, missing replies and post-stall recovery. Preserve sample rate over long intervals rather than dropping ordinary timer lateness.
Measure final packet spacing before changing a family's existing packet clock. Flex may need a bounded digital-audio pacer on AudioEngine; HL2 and Icom already have terminal clocks. Do not stack an additional universal packet timer over their clocks. A per-wake cap alone does not bound cumulative catch-up across successive wakes.
Queues must have both byte/sample limits and duration limits. PCM is an ordered stream: latest-frame-wins is suitable for waterfall/meter snapshots, not healthy audio. Preserve all samples inside the normal operating envelope. Beyond it, explicitly count discontinuity/underrun/overflow; do not silently replay seconds of stale data. Sustained overload should terminate the affected stream/client through the existing cancellation path. Any silence/drop recovery policy needs protocol and hardware validation before becoming shipping behavior.
WebSocket receive-size and client caps already exist. Add outbound backlog accounting using bytesToWrite() and bounded application queues. Schedule control and chrono ahead of optional spectrum work before it enters Qt's socket buffer; bytes already queued on the same TCP stream cannot be overtaken. A stalled client must not block other clients. Qt WebSocket API
Two nearby behaviors need explicit treatment in the pacing increment: current audio_stream_samples and tx_stream_audio_buffering handlers echo requests without implementing them, while chrono remains fixed; current binary TX ingress does not itself check the sender against the active owner. Do not base new accounting on those echoes or let another client's audio replenish the owner's outstanding budget. Preserve known WSJT-X/JTDX compatibility while adding focused ownership and negotiation tests.
Control timing is a separate limit
The controller still runs beside RadioModel. Continuous audio can therefore survive a UI stall while new frequency/PTT requests or radio-confirmed notifications wait. A new key request must be checked for cancellation, age, route and authority when it is actually processed; it must not turn into a surprise late transmission.
On owner loss/unkey, the worker can immediately revoke local audio and request the existing unkey path. That does not prove immediate physical RF unkey while the UI thread is blocked. Removing that remaining dependency requires separately scoped TX-coordinator/control execution work; a socket worker must not bypass RadioModel and write vendor PTT commands directly. Measure both request-to-radio-key and stop-to-radio-unkey latency. Do not market this media change as solving all TCI command timing.
Lifecycle contract
Use an explicit stopped → starting → running → stopping lifecycle, with fresh listener/client/session generations. Test enable toggles, failed bind, port change, rapid reconnect, receiver removal/recreation, backend family switch, and quit during active input.
Shutdown order must be updated: current MainWindow stops/deletes AudioEngine before deleting TciServer. With an independently running producer, first stop admission and revoke media/TX sessions, perform controller-side unkey/resource cleanup while models remain alive, quiesce and close TCI in its owning thread, then stop audio/backend consumers in dependency order. Account for PanadapterStream independently of AudioEngine: stopping AudioEngine does not stop that producer.
No worker may wait synchronously for the controller while the controller joins it. Use completion-driven teardown and a bounded shutdown policy; never delete a live worker or use thread termination as normal cleanup. Disconnect alone cannot revoke events already queued to a surviving receiver. Retain generation checks through worker, conversion and terminal dispatch. Qt thread/ownership rules
Implementation sequence
- Freeze the design and measurement contract. Use this proposal for the threading RFC. Explicitly document the audio endpoint alongside the existing IRadioBackend affinity contract, and align with #5554 §2.6, #5262, #5468 A4 and #5659. Define steady-stream and control-latency claims separately.
- Create a reproducer and timing baseline. Add bounded timestamp/sample counters at TCI ingress/egress, AudioEngine and backend terminal submission. Exercise the production server with an independent client and deliberately stalled controller. Record unchanged-source behavior first. Keep this separate from hardware proof.
- Extract TCI transport and media processing. Retain the controller/routing facade; move sockets, conversion, streaming state and request timing into one worker. Implement lifecycle, command ordering and bounded queues. This increment alone is not an all-radio fix.
- Wire the neutral audio endpoint, one family at a time. Flex first, then HL2, then Icom's private audio worker. Remove each replaced TCI feed atomically with its replacement. One logical change and its tests per PR; do not widen family behavior opportunistically.
- Complete pacing and qualification. Use terminal measurements to decide the needed Flex pacer, validate active-owner request accounting and overload recovery, run platform/client/hardware gates, and update existing architecture documentation. Ship the full three-family claim only when each family passes its own evidence gates.
Validation and acceptance
| Layer | Concrete proof |
|---|---|
| Socket-free deterministic tests | Inject PCM/control and a fake clock into actual processing seams: rate/sample accounting, bounded queues, startup/no-reply recovery, long-stall cumulative behavior, non-owner input, resampler reset, cancellation, stale session/receiver rejection and no audio after TX authority is retired |
| Production TCI server integration | Our real server on an ephemeral port, client and audio source/sink on independent execution contexts. Deliberately block the model/UI thread for 50, 100, 250, 600 and 1,000 ms after stream establishment. Compare steady RX sample continuity, TX request cadence and terminal audio output with stationary runs. Existing own-server socket-test rules apply |
| Lifecycle/sanitizers | Exercise queued delivery across disconnect, source replacement, rapid start/stop, port changes, owner loss and shutdown. ASan for lifetime; appropriately instrumented TSan for races. Preserve existing affinity tests and add endpoint-specific ones instead of weakening the old contract |
| Platform/build | Headless selected tests on the current changes, macOS/Windows/Linux application builds, correct Qt floor, strict boundary/touchpoint/test-registration checks. New tests enter tests.cmake; do not expand the frozen per-PR test allow-list |
| Application/client behavior | Automation bridge for positive model/UI convergence; real WSJT-X/JTDX/WSJT-Z compatibility; one and multiple receiver/client cases; muted speaker and PC Audio off must preserve TCI decoding; IQ/spectrum load must not starve audio |
| Live radio | Separate Flex, HL2 and Icom RX observations, then explicitly authorized dummy-load TX with fresh in-use/lock/safety checks. Capture radio-facing timing and independent RF evidence during idle versus resize/panadapter load. Measure decode results, output discontinuities and key/unkey timing separately |
Report p50/p95/p99/max timing gaps, cumulative sample counts, sample-position discontinuities, queue high-water duration, underrun/overflow, outstanding request duration, socket backlog and shutdown time. Make deterministic invariants exact. Establish numeric real-time tolerances from baseline/client/backend buffer budgets before gating; do not invent a universal microsecond target or use a clean-looking waterfall as the sole acceptance test.
Relevant existing tests to extend or retain include tci_server_review_test, tci_automation_test, tci_trxmap_test, hl2_tci_signaling_test, pcm_frame_test, backend_seam_affinity_test, backend_family_switch_test, icom_audio_test, icom_tci_unkey_settle_test, hl2_tx_gate_test and hl2_txdsp_test. Their presence was inspected; none were run in this research task. A future test selection must verify what actually registers and runs. On this Mac, compile with exactly cmake --build <build-dir> -j22; Qt tests run with QT_QPA_PLATFORM=offscreen and eligible CTest selections with -j22 --no-tests=error.
Cross-platform impact
Use portable Qt/C++ ownership and bounded data handoffs on Linux, macOS and Windows. No new external dependency or platform-native scheduling API is proposed.
- Linux: preserve the supported Qt floor and existing backend/audio routes; build the application and exercise selected headless tests, endpoint/lifecycle coverage and client compatibility. Sanitizer results must identify instrumentation coverage.
- macOS: validate the worker path under live window resizing, high-DPI panadapter/waterfall load and multiple clients; preserve audio-device and application shutdown behavior. The Mac Studio build preference below applies only to that development host.
- Windows: validate the same load and lifecycle scenarios, including active microphone capture, TCI source suppression and orderly multimedia/network teardown. macOS results do not substitute for Windows evidence.
Default ports, gains, sample-format compatibility and radio control behavior remain unchanged unless a separately identified protocol correction is reviewed. Thread priority or larger buffers are not prerequisites for correctness.
Alternatives considered
| Alternative | Assessment |
|---|---|
| Move the existing TciServer wholesale | Too much model/socket state crosses the affinity boundary; makes routing and lifetime harder to review |
| Move only TX_CHRONO | Socket I/O, RX conversion and host-family transport still depend on UI |
| Put TCI on AudioEngine's thread | Couples network clients, IQ/spectrum conversion and socket backlog to microphone/speaker DSP and shutdown |
| Raise thread priority or enlarge buffers | May change symptoms; does not remove UI dependencies and can increase stale audio latency |
| Move RadioModel and every backend off UI | Could address control timing too, but is a much larger engine migration than this media fix |
| Per-client TCI threads | Adds ownership/coordination cost before evidence shows one bounded worker is insufficient |
Tracking checklist
- Maintainer reviews the worker/controller boundary, data endpoint and explicit control-latency limit.
- Define queue budgets, overload recovery and measurable steady-stream/control-latency acceptance.
- Refresh and coordinate overlapping TX-ownership, TCI routing and PCM-rate work.
- Record baseline with independent TCI client/source/sink and deliberately stalled controller.
- Extract the TCI worker with ordered command completions, bounded queues and explicit lifecycle.
- Add the backend-neutral audio endpoint while preserving existing seam affinity guarantees.
- Integrate Flex RX/TX without duplicate feeds or DAX/IQ ownership regressions.
- Integrate HL2 RX/TX using existing private I/O/DSP workers and stable receiver identities.
- Finalize and integrate Icom audio-worker ownership, negotiation, conversion, packet pacing and drain barriers.
- Complete active-owner request accounting, negotiation coverage and measured terminal pacing.
- Pass deterministic, production-server and sanitizer/lifecycle gates against the implemented changes.
- Validate macOS, Windows and Linux independently.
- Validate actual TCI clients, multiple receivers, PC Audio off/mute and IQ/spectrum load.
- Record separate live Flex, HL2 and Icom RX evidence and explicitly authorized dummy-load TX/RF evidence.
- Update existing architecture/transport documentation and publish exact evidence and remaining limits.
Track implementation as one logical change and its tests per PR. Closing this tracker requires the complete three-family media claim to be supported; a timer-only move or one-radio success is insufficient. Physical key/unkey responsiveness during a blocked model thread remains an explicit separately scoped limitation unless its control/coordinator work is also completed and demonstrated.
Source and coordination index
All source locations below refer to the baseline commit, not mutable main. Paths are repository-relative. Links are pinned to the research baseline.
- src/gui/MainWindow_Session.cpp:2181,2266,2402,2414: TCI construction and family-specific audio wiring.
- src/core/TciServer.cpp:539,554,799,2596,2772,2779,3554,3887: timer/catch-up, client setup, binary TX, AudioEngine handoff, model-coupled RX, chrono lifecycle and IQ fan-out.
- src/core/TciProtocol.cpp:329 and its model-backed command handlers: initialization and synchronous state access.
- src/gui/MainWindow.cpp:1739,2796,2837: TX PCM UI relay and shutdown ordering.
- src/core/AudioEngine.cpp:9659,9745,9771,9846: digital TX bypass, host PCM and Flex packetization.
- src/core/backends/IRadioBackend.h:83: explicit six-rule threading/lifetime contract.
- src/models/RadioModel.cpp:3151,3175,9136: guarded PCM relays, existing UI-bound demod bus and TX forwarding.
- src/core/PcmFrame.h:31,46,103: stream identity, atomic revocation and single-context producer contract.
- src/core/backends/flex/FlexBackend.cpp:45: existing network/connection workers.
- src/core/backends/hl2/Hl2Backend.cpp:397,875,3817;
MetisClient.cpp:320: I/O thread, UI audio relay, TX submission and EP2 pacing. - src/core/backends/icom/IcomCivBackend.cpp:916,3209,3249;
IcomSession.cpp:100,721,742: UI-owned session, conversion, TX gates, audio socket and bounded pump. - src/models/RadioSession.cpp:33: TCI facade ownership.
- Existing docs to update during implementation:
docs/architecture/pipelines.md,audio-pipeline.md,tx-audio-signal-path.md,tci-routing-ordering.md,tci-receivers.md, anddocs/audio-engine-rate-domains.mdwhere boundaries change. - Issue #5133: open FT8/TCI report. PR #5403: checked live; closed, unmerged, last head
38e901e4. Its earlier experiments are context, not current proof. - Backend architecture #5554, migration #5262, rate-aware PCM work #5468.
- Open overlapping work at research time: TX ownership #5659, TCI VFO-B ownership #5681, HL2 source-level semantics #5647. Refresh their heads before implementation.
- Official TCI repository, local Thetis oracle §TX ownership/chrono, and local Icom/HL2 reference indexes were consulted. Protocol encoding or buffer-policy changes still require checking the relevant primary implementation/reference, not copying historical constants blindly.
The research supports this bounded architecture. Queue budgets, complete Icom worker API, control-latency acceptance and independent radio evidence remain implementation/design gates, not established results.
Generated with OpenAI Codex (GPT-6 Astra)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the existing TciServer, IRadioBackend, Flex, HL2, and IcomSession/IcomCivBackend paths described in the RFC, then compare them with the proposed TciIoWorker and bounded audio endpoint ownership. The design is done when maintainer-approved milestones, thread-affinity rules, lifecycle semantics, pacing behavior, and regression coverage are specified before implementation begins.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- audio-video-rtc, backend
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 28/100