feat(cli,input): MAN-121 — let listen --source replay gen's own IQ output, paced in realtime - #135
feat(cli,input): MAN-121 — let listen --source replay gen's own IQ output, paced in realtime#135catalyst-cloud-connector[bot] wants to merge 6 commits into
Conversation
a467969 to
233e07e
Compare
…lnet port without any SDR hardware
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b4ed6853bb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…lnet port without any SDR hardware
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1be137e4a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let is_iq = spec.channels == 2 | ||
| && (spec.sample_rate != TARGET_RATE_HZ || replay_wav_has_iq_sidecar(path)); | ||
| if is_iq { | ||
| Ok(Box::new(WavIqSource::open(path)?)) |
There was a problem hiding this comment.
Validate replay rates before allocating calibration buffers
When listen --source or soak --source receives a two-channel WAV with an unsupported, very large sample-rate header (for example, near u32::MAX), this new branch accepts it as a WavIqSource; manta_engine::listen subsequently allocates fs * 2 complex calibration samples before calling Channelizer::new to validate the rate. Thus, even a tiny malformed WAV can request tens of GiB and abort the process instead of returning the documented unsupported-rate error. Validate and bound the channelizer rate before exposing the source to the engine's startup allocation.
Useful? React with 👍 / 👎.
…lnet port without any SDR hardware
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3783c119b1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…lnet port without any SDR hardware
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 079d876e9f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // `AudioIqSource` needs no equivalent guard -- it already accepts | ||
| // exactly `TARGET_RATE_HZ` and nothing else. | ||
| let fs = spec.sample_rate as f64; | ||
| if !manta_dsp::channelizer::Channelizer::supports_rate(fs) { |
There was a problem hiding this comment.
Cap replay sample rates before allocation
Fresh evidence after the earlier malformed-rate finding: a minimal stereo Int16 WAV declaring 786,432,000 Hz passes this predicate because fs / 93.75 == 2^23. listen then constructs an enormous channelizer and allocates a two-second Complex32 calibration buffer of about 12.6 GB, despite the architecture's 768 kS/s supported ceiling, so a tiny local file can still abort the process instead of returning a rate error. Enforce the documented ceiling here, not merely the power-of-two relationship.
AGENTS.md reference: AGENTS.md:L25-L34
Useful? React with 👍 / 👎.
…lnet port without any SDR hardware
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
Summary
Two independent defects made the README's own quickstart a dead end:
manta genwrites 96 kHz stereo IQ, but
manta listen --sourceonly accepted 48 kHz monoaudio (
AudioIqSource requires 48000 Hz, got 96000); and unpaced file replaydrained a 60–120 s recording in 1.5–3.5 s wall-clock, so the telnet/JSON
servers exited before any client could connect and observe a spot. Together
these blocked ROADMAP.md's M3 acceptance gate ("a stock DX cluster client
connects and receives well-formed spots") from being verifiable by anyone
without SDR hardware.
This PR fixes both, adds opt-in
--loopfor leaving a demo running, and addsthe end-to-end test that makes the M3 gate self-verifying in CI without any
hardware.
What changed
manta_input::open_replay_wav(crates/manta-input/src/lib.rs) — a newdispatching WAV opener: 2-channel files are read as IQ via the existing
WavIqSource(any sample rate, sidecar-aware) unless they're exactly 48 kHzand lack a parseable
<stem>.jsonsidecar, in which case they fall throughto the existing
AudioIqSourcerig-audio path unchanged.listen --sourceand
soak --sourceboth switch to this opener in place of the oldAudioIqSource-only call, somanta gen v1 --out /tmp/v1output now decodesthrough
listenwith no format error and no resampling —Channelizer::newalready accepts any
fswherefs / 93.75is a power of two (96000 does),so this is a reader choice, not a new DSP stage.
manta_input::replay_wav_center_freq_hz— a companion probe used to makethe
--dial-freq-hzgate inmanta-cliformat-aware: a sidecar-backed IQ WAValready reports a real RF frequency, so the gate no longer demands
--dial-freq-hzfor it. The probe swallows every error, including anonexistent path, which preserves an existing regression guard
(
server_config_without_dial_freq_for_audio_source_is_a_clean_error) thatdepends on the flag check running strictly before any file I/O.
manta_input::pace::PacedSource(new) — an opt-in wrapper enabled by anew
listen --realtimeflag. It sleeps based on cumulative delivered samplesagainst a single start
Instant, so it never compounds drift and degrades tofully unpaced if the consumer falls behind — it cannot stall the pipeline.
It sits outside the decode path, so
--realtimeoutput is byte-identical tounpaced output (asserted by test); the default (unpaced) path is unchanged,
keeping the existing test suite and
soakfast.manta_input::replay::LoopingWavSource(new) — backs a new--loopflag that reopens the file at EOF instead of exiting, for a demo left
running. Documented (help text + decision doc) that the spot dedupe window
(600 s of simulated time per callsign/frequency-bucket) means a looped short
file yields roughly one spot per pass window, not one per loop — this is
correct skimmer behavior and intentionally not special-cased.
--realtime/--loopbothrequires = "source"(clap-enforced usageerror), since both are meaningless for a live device or network SDR source.
crates/manta-cli/tests/telnet_e2e.rs(new) — the M3 acceptance test:spawns
manta listen --source <gen v1 fixture> --server-config ... --realtimewith no SDR and no
--dial-freq-hz, connects a stock TCP client to thetelnet port and a JSON client to the JSON-Lines port, and asserts each
receives a well-formed spot for W1AW at the vector's own expected frequency.
(including a
sh/dxhint, since the live spot broadcast has no history forlate subscribers); README's Inputs/Outputs tables and the
--sourcehelptext (previously claiming replay was "paced by its own sample rate", which
was false) were corrected; ROADMAP M3 now cites the new test as covering its
"stock client receives spots" bullet;
docs/DECISIONS/2026-09-07-man121-hardware-free-replay.mdrecords the four design choices below;
wiki/pages/replay-input-dispatch.mdwiki/INDEX.mdadded.Design choices (see the linked decision doc for full rationale)
no new
genoutput variant —gen's existing 96 kHz IQ output becomes afirst-class
listeninput by choosing the right reader.original plan: a 2-channel 48 kHz file without a parseable sidecar is still
read as rig audio (the old behavior), because a stereo soundcard capture at
48 kHz is otherwise indistinguishable from IQ by channel count alone. Two
dedicated tests cover both sides of the tie-break.
--realtimeis opt-in, not the default — required by the broadreview's own protect-list ("pacing must be opt-in and never touch the
decode path"), and defaulting would slow the existing test suite and
soak --sourceby ~30x.--loop's interaction with the 600 s dedupe window is documented, not"fixed" — suppressing it would misrepresent real skimmer behavior.
Testing
Full workspace suite green:
cargo test --workspace --lib --bins --tests(33suites, 0 failed), including the two new
telnet_e2eacceptance tests (12.62stotal), 20/20 in
cli.rs(7 new MAN-121 cases plus the two named regressionguards for fail-fast flag ordering and JSON determinism), and all
manta-inputunit tests for the new dispatch/pacing/loop code.
cargo fmt --all -- --checkand
cargo clippy --workspace --all-targets -- -D warnings(plus--features hpsdr) are clean. The golden-vector determinism suites (golden_v1,golden_v2_v3,golden_v7_v9_v10,golden_v8_v8w) andsoak_ciareunaffected and stayed green.
Both ticket scenarios were also verified by hand end-to-end against the built
binary, running the README's own commands verbatim (
manta gen v1 --out /tmp/v1demo→manta listen --source /tmp/v1demo/v1.wav --server-config /tmp/server.toml --realtime, no--dial-freq-hz, no SDR): a stock TCP clientreceived
DX de N0CALL-#: 14012.4 W1AW CW 12 dB 18 WPM CQat t=6.2s,naming the vector's own expected frequency rather than an audio-tone offset.
Not verified in this branch's environment (resource-constrained container: 2
CPUs, 11 GB disk): three consecutive repeat runs of
telnet_e2efor flake, anda run on macOS — both deferred to CI, which runs on
ubuntu-latestandmacos-latest.Known non-blocking follow-ups
A high-effort code review during validation found six findings, none
correctness or security, so they don't block this PR per this repo's
round-1-unrestricted / round-2-onward-ticketed review convergence policy.
Worth a look during review, in priority order:
--dial-freq-hzgate's error text says a sidecar is "missing" when itmay instead be present-but-malformed (e.g. a JSON typo) — misleading, easy
fix by distinguishing "absent" from "invalid" in the probe.
silently downmixed as rig audio instead of erroring — pre-existing
behavior for this input shape, not a regression, but worth closing.
PacedSourceunit tests assert wall-clock upper bounds, which theplan's own "never assert an upper bound" rule warns against as flake-prone
under CI load.
instead of numeric tolerance (the companion JSON test already does this
correctly).
wiki/pages/replay-input-dispatch.mddocuments the plan's originalchannel-count-only dispatch rule rather than the shipped 48 kHz tie-break
correction (the decision doc has the current rule).
server.tomlomitsbind_addr, so it binds0.0.0.0by default while telling the reader to connect vialocalhost(pre-existing default, not introduced here).
Compatibility
Fully backward compatible:
listen --sourceon a mono 48 kHz WAV behavesexactly as before, including its existing error message for a wrong-rate mono
file;
--realtime/--loopare new and default off; the--dial-freq-hzgateonly ever relaxes (for a sidecar-backed IQ WAV) and anyone already passing the
flag is unaffected;
soak --sourcegains IQ WAV support additively.