Skip to content

Composite stereo TX recording — true on-air timeline (WAV + API/UI) - #13

Merged
M0LTE merged 1 commit into
mainfrom
composite-tx-recording
Jun 4, 2026
Merged

Composite stereo TX recording — true on-air timeline (WAV + API/UI)#13
M0LTE merged 1 commit into
mainfrom
composite-tx-recording

Conversation

@M0LTE

@M0LTE M0LTE commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

What

Adds a composite recording: capture a chosen set of transmitters into a single, sample-aligned, multi-channel WAV — one transmitter per channel, paced through one real-time clock. For the canonical two-station setup that's a stereo file with one station's transmitted audio in each ear.

This is the feature requested: a WAV that is an accurate timeline of what actually happened on the channel — overlapping transmissions overlap in time, inter-burst gaps are real silence, and both channels share one start time. Drop it into Audacity and you can see at a glance who was keying when (ideal for hidden-node collisions — put the two hidden stations in left/right).

Why it's new, not the existing recorder

The per-port *.tx.wav files are written straight from each TNC as it bursts TX audio (samoyed can dump a 500 ms frame onto its UDP socket in a few wall-clock ms), so they carry the waveform faithfully but are not a real-time timeline. The composite recorder drains each transmitter's blocks through a single ticker at the block rate — the same mechanism rxFeeder already uses to pace audio into the receivers — writing one interleaved frame per tick. Every channel is advanced together by one goroutine, so they stay sample-aligned and share one start time; idle channels emit silence so gaps are preserved. The captured audio is the clean transmitter output (full-scale, before per-link loss / noise / the capture-effect mixer) — what each station put on the air, not what any one receiver heard.

Changes

  • internal/audio — generalise the WAV header writer to N channels; add MultiWAVWriter and an Interleave helper.
  • internal/routercompositeRecorder with per-channel queues fed from the txReader hot path (atomic pointer, mirroring the existing recorder) and a real-time pacer goroutine; Router.StartCompositeRecording / StopCompositeRecording / CompositeStatus; graceful drain + WAV-header patch on stop and on shutdown.
  • cmd/sim-webPOST/GET /api/record/composite/{start,stop,status,download} for external software; a composite block in /api/status; a Left/Right-ear picker with Start/Stop/Download on the control page. Map UI untouched. Gated on -record DIR.
  • cmd/sim-router-composite a.vhf,b.vhf (requires -record) to capture on startup.
  • README — documents the feature, CLI, and HTTP API.

API (for external software)

POST /api/record/composite/start with optional {"ports":["a.vhf","c.vhf"]} (omit → first two ports) → transmit over KISS → POST .../stopGET .../download.

Testing

  • Unit: multichannel WAV header + interleave round-trip; recorder start/stop/validation/defaults/real-time pacing; sim-web handlers via httptest.
  • A concurrent-feed test passes under -race.
  • End-to-end in the Docker image with live samoyed children: started a composite recording via the API, and a 0.7 s wall-clock capture produced exactly 0.70 s of valid stereo 44.1 kHz audio — confirming real-time pacing through the whole pipeline. (The PR's docker workflow rebuilds the image to validate it; merge to main publishes :main.)

Add a "composite recording": capture a chosen set of transmitters into a single, sample-aligned, multi-channel WAV — one transmitter per channel — paced through one real-time clock. For the canonical two-station setup that's a stereo file with one station's transmitted audio in each ear.

Why this is new rather than reusing the per-port `*.tx.wav` files: those are written straight from each TNC as it *bursts* TX audio (samoyed can emit a 500 ms frame onto its UDP socket in a few wall-clock ms), so they carry the waveform faithfully but are not a real-time timeline — you can't line two of them up and trust the gaps. The composite recorder instead drains each transmitter's blocks through a single ticker at the block rate (the same mechanism rxFeeder uses to pace audio into the receivers), writing one interleaved frame per tick. Because every channel is advanced together by one goroutine they share one start time and stay sample-aligned, and idle channels emit silence so gaps are preserved. The result is an accurate timeline of what was on the air: overlapping transmissions overlap in time. The audio is the clean transmitter output (full-scale, before per-link loss / noise / the capture mixer).

Implementation:
- internal/audio: generalise the WAV header writer to N channels, add MultiWAVWriter and an Interleave helper.
- internal/router: compositeRecorder with per-channel queues fed from the txReader hot path (atomic pointer, like the existing recorder) and a real-time pacer goroutine; Router.StartCompositeRecording / StopCompositeRecording / CompositeStatus; graceful drain + WAV-header patch on stop and on shutdown.
- cmd/sim-web: /api/record/composite/{start,stop,status,download} for external software, a composite block in /api/status, and a Left/Right-ear picker + Start/Stop/Download panel on the control page (map UI untouched). The -record DIR flag enables it.
- cmd/sim-router: -composite a.vhf,b.vhf flag (requires -record) to capture on startup.

Tests: multichannel WAV header + interleave round-trip; recorder start/stop/validation/defaults/real-time pacing; a concurrent-feed test that passes under -race; sim-web handler tests via httptest. Verified end-to-end inside the Docker image with live samoyed children — a 0.7 s wall-clock recording produced 0.70 s of stereo audio.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@M0LTE
M0LTE merged commit e8ffcba into main Jun 4, 2026
1 check passed
@M0LTE
M0LTE deleted the composite-tx-recording branch June 4, 2026 03:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant