Skip to content

feat(cli,input): MAN-121 — let listen --source replay gen's own IQ output, paced in realtime - #135

Open
catalyst-cloud-connector[bot] wants to merge 6 commits into
mainfrom
MAN-121
Open

feat(cli,input): MAN-121 — let listen --source replay gen's own IQ output, paced in realtime#135
catalyst-cloud-connector[bot] wants to merge 6 commits into
mainfrom
MAN-121

Conversation

@catalyst-cloud-connector

@catalyst-cloud-connector catalyst-cloud-connector Bot commented Sep 7, 2026

Copy link
Copy Markdown

Summary

Two independent defects made the README's own quickstart a dead end: manta gen
writes 96 kHz stereo IQ, but manta listen --source only accepted 48 kHz mono
audio (AudioIqSource requires 48000 Hz, got 96000); and unpaced file replay
drained 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 --loop for leaving a demo running, and adds
the 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 new
    dispatching WAV opener: 2-channel files are read as IQ via the existing
    WavIqSource (any sample rate, sidecar-aware) unless they're exactly 48 kHz
    and lack a parseable <stem>.json sidecar, in which case they fall through
    to the existing AudioIqSource rig-audio path unchanged. listen --source
    and soak --source both switch to this opener in place of the old
    AudioIqSource-only call, so manta gen v1 --out /tmp/v1 output now decodes
    through listen with no format error and no resampling — Channelizer::new
    already accepts any fs where fs / 93.75 is 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 make
    the --dial-freq-hz gate in manta-cli format-aware: a sidecar-backed IQ WAV
    already reports a real RF frequency, so the gate no longer demands
    --dial-freq-hz for it. The probe swallows every error, including a
    nonexistent path, which preserves an existing regression guard
    (server_config_without_dial_freq_for_audio_source_is_a_clean_error) that
    depends on the flag check running strictly before any file I/O.
  • manta_input::pace::PacedSource (new) — an opt-in wrapper enabled by a
    new listen --realtime flag. It sleeps based on cumulative delivered samples
    against a single start Instant, so it never compounds drift and degrades to
    fully unpaced if the consumer falls behind — it cannot stall the pipeline.
    It sits outside the decode path, so --realtime output is byte-identical to
    unpaced output (asserted by test); the default (unpaced) path is unchanged,
    keeping the existing test suite and soak fast.
  • manta_input::replay::LoopingWavSource (new) — backs a new --loop
    flag 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/--loop both requires = "source" (clap-enforced usage
    error), 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 ... --realtime
    with no SDR and no --dial-freq-hz, connects a stock TCP client to the
    telnet 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.
  • Docs: README quickstart gained the hardware-free three-command demo
    (including a sh/dx hint, since the live spot broadcast has no history for
    late subscribers); README's Inputs/Outputs tables and the --source help
    text (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.md
    records the four design choices below; wiki/pages/replay-input-dispatch.md
    • wiki/INDEX.md added.

Design choices (see the linked decision doc for full rationale)

  1. Dispatch on WAV shape, not a new flag or a resampler. No resampling and
    no new gen output variant — gen's existing 96 kHz IQ output becomes a
    first-class listen input by choosing the right reader.
  2. The 2-channel dispatch rule has a 48 kHz tie-break, stricter than the
    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.
  3. --realtime is opt-in, not the default — required by the broad
    review'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 --source by ~30x.
  4. --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 (33
suites, 0 failed), including the two new telnet_e2e acceptance tests (12.62s
total), 20/20 in cli.rs (7 new MAN-121 cases plus the two named regression
guards for fail-fast flag ordering and JSON determinism), and all manta-input
unit tests for the new dispatch/pacing/loop code. cargo fmt --all -- --check
and 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) and soak_ci are
unaffected 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/v1demomanta listen --source /tmp/v1demo/v1.wav --server-config /tmp/server.toml --realtime, no --dial-freq-hz, no SDR): a stock TCP client
received DX de N0CALL-#: 14012.4 W1AW CW 12 dB 18 WPM CQ at 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_e2e for flake, and
a run on macOS — both deferred to CI, which runs on ubuntu-latest and
macos-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:

  1. The --dial-freq-hz gate's error text says a sidecar is "missing" when it
    may instead be present-but-malformed (e.g. a JSON typo) — misleading, easy
    fix by distinguishing "absent" from "invalid" in the probe.
  2. The same ambiguity means a 48 kHz stereo IQ file with a broken sidecar is
    silently downmixed as rig audio instead of erroring — pre-existing
    behavior for this input shape, not a regression, but worth closing.
  3. Two new PacedSource unit tests assert wall-clock upper bounds, which the
    plan's own "never assert an upper bound" rule warns against as flake-prone
    under CI load.
  4. The telnet acceptance test matches frequency by rounded-integer substring
    instead of numeric tolerance (the companion JSON test already does this
    correctly).
  5. wiki/pages/replay-input-dispatch.md documents the plan's original
    channel-count-only dispatch rule rather than the shipped 48 kHz tie-break
    correction (the decision doc has the current rule).
  6. The README's new demo server.toml omits bind_addr, so it binds
    0.0.0.0 by default while telling the reader to connect via localhost
    (pre-existing default, not introduced here).

Compatibility

Fully backward compatible: listen --source on a mono 48 kHz WAV behaves
exactly as before, including its existing error message for a wrong-rate mono
file; --realtime/--loop are new and default off; the --dial-freq-hz gate
only ever relaxes (for a sidecar-backed IQ WAV) and anyone already passing the
flag is unaffected; soak --source gains IQ WAV support additively.

8e38717 feat: MAN-121 — A visitor should be able to see a real spot on the telnet port without any SDR hardware
a467969 feat: MAN-121 — A visitor should be able to see a real spot on the telnet port without any SDR hardware

Catalyst-Replay-Squash: 49f05a4
@catalyst-cloud-connector catalyst-cloud-connector Bot changed the title feat: MAN-121 — A visitor should be able to see a real spot on the telnet port without any SDR hardware feat(cli,input): MAN-121 — let listen --source replay gen's own IQ output, paced in realtime Sep 7, 2026
@catalyst-cloud-connector
catalyst-cloud-connector Bot marked this pull request as ready for review September 7, 2026 17:31
@thagale

thagale commented Sep 8, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-08T03:40:41.884141Z 079d876 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread crates/manta-input/src/pace.rs Outdated
Comment thread crates/manta-input/src/lib.rs Outdated
Comment thread wiki/pages/replay-input-dispatch.md Outdated
Comment thread README.md
Comment thread crates/manta-cli/src/main.rs Outdated
@thagale

thagale commented Sep 8, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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)?))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread crates/manta-cli/src/main.rs
Comment thread crates/manta-cli/tests/telnet_e2e.rs Outdated
Comment thread crates/manta-cli/src/main.rs
@thagale

thagale commented Sep 8, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread wiki/pages/replay-input-dispatch.md Outdated
Comment thread crates/manta-cli/src/main.rs
@thagale

thagale commented Sep 8, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@thagale

thagale commented Sep 8, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

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