Captures requirements REQ-REC-001–006, REQ-PLAY-001–002, and REQ-SYNC-001 into a structured design document covering the new trx-recorder crate, session layout (FLAC/Opus audio + JSONL data track + seek index), command API, playback engine, and phased implementation plan. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
11 KiB
Recorder Feature Plan
Overview
This document describes the design and implementation plan for the recorder feature in trx-rs. The recorder captures the demodulated audio stream alongside associated metadata (FFT data, decoded signals, rig state) into a structured session on disk, with full playback and seeking support from within the application.
Requirements
| ID | Description |
|---|---|
| REQ-REC-001 | When the user starts recording, the system shall record the currently demodulated audio stream. |
| REQ-REC-002 | When recording audio, the system shall store the recording in either FLAC or OPUS format. |
| REQ-REC-003 | While recording audio, the system shall automatically detect whether the recording should be stored in mono or stereo and select the appropriate format. |
| REQ-REC-004 | While recording is active, the system shall simultaneously record FFT data and all currently visible decoded elements, including APRS and FT8. |
| REQ-REC-005 | While recording metadata, the system shall store FFT data and decoded signal data in a structured data file format. |
| REQ-PLAY-001 | Where recorded sessions exist, the system shall allow playback of recordings from within the same application. |
| REQ-PLAY-002 | During playback, the system shall allow the user to seek to any position in the recording. |
| REQ-SYNC-001 | The system shall maintain time synchronization between the audio recording and the associated data file with at least one-second resolution. |
| REQ-REC-006 | While recording is active, the system shall allow the current cursor position to be stored. |
Architecture
New Crate: trx-recorder
A new crate src/trx-server/trx-recorder/ handles all record and playback logic. It is a library crate consumed by trx-server.
src/trx-server/
trx-recorder/
src/
lib.rs # Public API: RecorderHandle, start_recorder_task()
session.rs # RecordingSession: file management, open/close/finalise
writer.rs # AudioWriter: PCM → FLAC or Opus encoder
data_file.rs # DataFileWriter: structured JSON Lines data track
index.rs # SeekIndex: time → byte-offset table for audio seeking
playback.rs # PlaybackEngine: file → PCM broadcast for clients
config.rs # RecorderConfig (serde, derives Default)
Integration Points in trx-server
| Source | What is tapped | How |
|---|---|---|
audio.rs pcm_tx |
Raw demodulated PCM frames | New broadcast::Receiver<Vec<f32>> subscriber |
audio.rs spectrum broadcast |
FFT/spectrum frames per RigState.spectrum |
New subscriber on the spectrum watch channel |
audio.rs decoded-message broadcast |
FT8, WSPR, CW, APRS, FT4, FT2, APRS-HF frames | New broadcast::Receiver<DecodedMessage> subscriber |
rig_task.rs state watch |
Frequency/mode/PTT changes | watch::Receiver<RigState> clone |
New RecorderCommand enum |
Start, Stop, MarkCursor | Injected into the existing command pipeline |
No existing code paths are modified beyond:
- Passing a
RecorderHandle(cheapArcwrapper) into the audio and rig tasks. - Adding
RecorderCommandvariants to the command enum (alongside existingSetFreq,SetMode, etc.). - Adding a
[recorder]section toServerConfig.
Session Layout on Disk
Each recording is a session directory named by UTC start time and opening rig state:
<output_dir>/
20260317T142301Z_14074000_USB/
audio.flac # or audio.opus
data.jsonl # structured event log (see below)
index.bin # seek index: sorted table of (offset_ms u64, audio_byte u64)
output_dir defaults to ~/.local/share/trx-rs/recordings.
Audio File (REQ-REC-001, REQ-REC-002, REQ-REC-003)
- Format: FLAC (lossless, seekable) or Opus (compressed, needs external seek index). Configured via
recorder.format = "flac" | "opus". - Channel count: determined at session open from
AudioConfig.channels. Ifchannels == 1→ mono; ifchannels == 2→ stereo. Written into the file header and recorded in the session's first data event. - Sample rate: preserved from
AudioConfig.sample_rate(default 48 000 Hz). - FLAC: uses the
claxoncrate for writing, or a thin wrapper aroundlibflacviaflac-sys. Native seekable by frame. - Opus: uses the
opuscrate (already a workspace dependency viatrx-ft8). Seek index (index.bin) provides byte → time mapping.
Data File (REQ-REC-004, REQ-REC-005)
data.jsonl — one JSON object per line, each with a required offset_ms field giving the millisecond offset from session start (satisfies REQ-SYNC-001 at ≥1 s resolution):
{"offset_ms":0,"type":"session_start","freq_hz":14074000,"mode":"USB","channels":1,"sample_rate":48000,"format":"flac"}
{"offset_ms":1000,"type":"rig_state","freq_hz":14074000,"mode":"USB","ptt":false}
{"offset_ms":2000,"type":"fft","bins_db":[-90.1,-88.4,...]}
{"offset_ms":3412,"type":"ft8","snr_db":-12,"dt_s":0.3,"freq_hz":14074350,"message":"CQ W5XYZ EN34"}
{"offset_ms":4100,"type":"aprs","from":"W5XYZ-9","to":"APRS","path":"WIDE1-1","info":"!3351.00N/09722.00W-"}
{"offset_ms":5000,"type":"cursor","label":"interesting QSO"}
{"offset_ms":61000,"type":"session_end"}
Supported type values:
| Type | Source | Cadence |
|---|---|---|
session_start |
recorder | once, at open |
session_end |
recorder | once, at close |
rig_state |
watch::Receiver<RigState> change |
on change |
fft |
spectrum data from RigState.spectrum |
≤1 Hz (configurable, default 1 s) |
ft8 / ft4 / ft2 / wspr |
DecodedMessage broadcast |
on decode event |
aprs / aprs_hf |
DecodedMessage broadcast |
on decode event |
cw |
DecodedMessage broadcast |
on decode event |
cursor |
RecorderCommand::MarkCursor { label } |
on user request |
Seek Index (REQ-PLAY-002)
index.bin is a flat binary table of 16-byte records written every index_interval_ms (default 1 000 ms):
[offset_ms: u64 LE][audio_byte_offset: u64 LE] ...
At playback seek time, binary search on offset_ms locates the nearest audio frame boundary, enabling random-access playback without full file scan.
RecorderConfig
Added to ServerConfig under [recorder]:
[recorder]
enabled = false
output_dir = "~/.local/share/trx-rs/recordings"
format = "flac" # "flac" | "opus"
opus_bitrate_bps = 32000
fft_record_interval_ms = 1000
index_interval_ms = 1000
max_session_duration_s = 3600 # auto-split at 1 h; 0 = unlimited
Command API
New variants added to the existing command enum (handled in rig_task.rs):
StartRecording,
StopRecording,
MarkCursor { label: String },
These are exposed via:
- HTTP frontend:
POST /api/recorder/start,POST /api/recorder/stop,POST /api/recorder/cursor - http-json frontend: same commands as JSON messages
Playback Engine (REQ-PLAY-001, REQ-PLAY-002)
PlaybackEngine opens a session directory and:
- Reads
audio.flacoraudio.opusand decodes PCM frames in real time. - Publishes decoded PCM frames onto a
broadcast::Sender<Vec<f32>>— the same channel type as the livepcm_tx, so existing decoder tasks and audio-streaming clients receive playback data transparently. - Replays
data.jsonlevents on their originaloffset_mstimestamps, injecting them into theDecodedMessagebroadcast so the HTTP frontend displays historic decodes during playback. - For seek: binary-searches
index.binto find the audio byte offset, then replays data events from the same point.
The playback state machine has two modes, switched by a new RigState.playback field:
pub enum PlaybackState {
Live,
Playing { session: String, offset_ms: u64 },
Paused { session: String, offset_ms: u64 },
}
While PlaybackState is not Live, the server suppresses live hardware polling and PCM capture to avoid mixing live and playback audio.
Time Synchronisation (REQ-SYNC-001)
All timestamps use a single session_epoch: std::time::Instant captured at StartRecording. Every PCM frame, every data event, and every seek-index entry is stamped as (Instant::now() - session_epoch).as_millis() as u64. This gives sub-millisecond internal precision; the requirement of ≥1 s resolution is met by orders of magnitude.
Wall-clock UTC is embedded only in session_start (wall_clock_utc) and in the session directory name, providing absolute time anchoring without depending on system clock monotonicity for sync.
Implementation Phases
Phase 1 — Audio recording (REQ-REC-001, REQ-REC-002, REQ-REC-003)
- Add
trx-recordercrate skeleton;RecorderConfig;RecorderHandle. - Implement
AudioWriterwith FLAC output (lossless path first). - Subscribe
AudioWritertopcm_txinaudio.rs; open session onStartRecordingcommand. - Auto-detect channel count from
AudioConfig.channels. - Write Opus variant behind a feature flag; test both.
Phase 2 — Metadata recording (REQ-REC-004, REQ-REC-005, REQ-SYNC-001)
- Implement
DataFileWriter; define full event schema. - Subscribe to
DecodedMessagebroadcast; fan-in all decoder types. - Subscribe to state watch; emit
rig_stateevents on freq/mode change. - Emit
fftevents at configured interval from spectrum data. - Write
SeekIndexin parallel with audio.
Phase 3 — Cursor (REQ-REC-006)
- Add
MarkCursorcommand + HTTP endpoint. - Write
cursorevent todata.jsonlwith currentoffset_ms.
Phase 4 — Playback (REQ-PLAY-001, REQ-PLAY-002)
- Implement
PlaybackEngine; FLAC decode + PCM broadcast. - Add
PlaybackStatetoRigState; suppress live capture during playback. - Implement seek via
index.binbinary search. - Replay
data.jsonlevents; feed intoDecodedMessagebroadcast. - Expose start/stop/seek endpoints in
trx-frontend-http.
Dependencies to Add
| Crate | Use | Already present? |
|---|---|---|
claxon or flac |
FLAC encode/decode | No |
opus |
Opus encode/decode | Yes (via trx-backend-soapysdr) |
serde_json |
data.jsonl serialisation | Yes |
tokio::fs |
async file I/O | Yes |
Open Questions
- FLAC encoder choice:
claxonis a pure-Rust decoder but has no encoder;libflac-syshas encode+decode but requires a C toolchain. May need to add a pure-Rust FLAC encoder or use an intermediate WAV stage with post-encode. - Playback isolation: Should playback be exclusive (block all CAT commands) or concurrent? Initial design blocks CAT polling; revisit if users need to change frequency during playback.
- Session listing API: The HTTP frontend needs an endpoint to enumerate sessions (
GET /api/recorder/sessions). Schema TBD in Phase 4. - Storage limits:
max_session_duration_sauto-splits sessions; amax_total_size_gbhousekeeping option may be needed but is out of scope for initial phases.