Skip to content

feat(otasim): real-HF noise-bed channel model (task #71)#35

Merged
secup merged 1 commit into
mainfrom
feat/otasim-real-hf-noise-bed
May 18, 2026
Merged

feat(otasim): real-HF noise-bed channel model (task #71)#35
secup merged 1 commit into
mainfrom
feat/otasim-real-hf-noise-bed

Conversation

@secup
Copy link
Copy Markdown
Owner

@secup secup commented May 18, 2026

Summary

Adds `RealHfLoopChannelModel` — uses a recorded HF WAV (the 20m noise
bed committed in #34) as the noise source, with the same SNR scaling
formula as AWGN. `otasim_ctl set-channel --model real_hf_loop --snr 12`
gives per-sample noise RMS identical to AWGN at SNR=12 in level,
but with real-HF spectral character (atmospherics, NCDXF beacon
rotation, band noise) preserved.

Default unchanged: lobby still starts on AWGN. Real-HF noise is
opt-in via the server flag + admin SetChannel switch. Tests untouched.

How to use

```bash
./build/ota_simulator serve \
--bind 127.0.0.1:50051 --udp-bind 127.0.0.1:50052 \
--tokens /tmp/ota_tokens.conf \
--noise-bed-wav recordings/ota_noise_bed_2026-05-18_20m_14100/noise_bed.wav

./build/otasim_ctl --token admin_tok set-channel --model real_hf_loop --snr 12
./build/otasim_ctl --token alpha_tok get-channel

→ session=lobby model=real_hf_loop snr_db=12.00

```

If `--noise-bed-wav` isn't provided and someone tries to set the model,
the server returns INVALID_ARGUMENT with a clear message.

Implementation

  • `src/ota_channel_core/models.{cpp,hpp}` — new `ChannelType::REAL_HF_LOOP`,
    WAV loader (16-bit PCM mono 48 kHz, normalized to unit RMS),
    `RealHfLoopChannelModel` class mirroring AWGN structure.
  • `src/ota_channel_core/session_*` + service layer — carry the cached
    noise vector into per-session model construction.
  • `tools/ota_simulator.cpp` — `serve --noise-bed-wav ` flag,
    startup load.
  • `tools/otasim_ctl.cpp` — usage string lists real_hf_loop (no other
    changes; --model already passes through as a free string).
  • `tests/test_real_hf_loop_channel.cpp` — scaling, position wrap,
    setSNR, empty-loop passthrough, parse/name round-trip.
  • `tests/test_grpc_service_smoke.cpp` — asserts real_hf_loop is
    rejected when --noise-bed-wav was not provided.

Test plan

  • `cmake --build build -j4` clean
  • New `RealHfLoopChannel` unit test → PASSED
  • `OTASimulatorFileTransferSNR15` (300s slow test) → PASSED on
    unrestricted Mac (Codex sandbox timed out, which is environment
    not regression)
  • Live two-step smoke: server with --noise-bed-wav accepts the asset;
    set-channel real_hf_loop SNR=12 switches lobby; get-channel
    reflects; switch back to awgn works.
  • CI matrix (Linux + macOS + Windows)

Worked example at SNR=12

```
target noise RMS = kModemReferenceRms × 10^(-12/20)
= 0.3180724 × 0.25118864
= 0.07989617
```

AWGN at SNR=12 produces noise stddev = 0.07989617. The real_hf_loop
model produces noise RMS = 0.07989617 (because the loop is
pre-normalized to unit RMS, and the scaling factor is identical).
Per-sample level matches; spectral character differs.

Follow-ups (queued)

  • Auto-discover the asset and switch default to real_hf_loop
    (separate PR; will need test fixups, intentionally not in this one).
  • Multi-bed support (different bands, day vs night, contest vs quiet).
  • Combined AWGN + real_hf_loop overlay (model thermal noise PLUS QRM).

🤖 Generated with Claude Code

Adds RealHfLoopChannelModel — a drop-in alongside AWGN that uses
a recorded HF WAV as the noise source instead of generated Gaussian
noise. Same kModemReferenceRms * 10^(-snr/20) scaling formula, so
"set-channel --model real_hf_loop --snr 12" delivers per-sample
noise RMS = 0.0799, identical to AWGN at SNR=12 in level — but with
real-HF spectral character (atmospherics, NCDXF beacon rotation,
band noise) preserved from the recording.

Default behavior unchanged: lobby still starts with AWGN; real-HF
noise is opt-in via the server flag + SetChannel switch.

Server interface:
  ./build/ota_simulator serve --bind 127.0.0.1:50051 --udp-bind ... \
      --tokens /tmp/ota_tokens.conf \
      --noise-bed-wav recordings/ota_noise_bed_2026-05-18_20m_14100/noise_bed.wav
  ./build/otasim_ctl --token admin_tok set-channel --model real_hf_loop --snr 12

If --noise-bed-wav is not provided, SetChannel for real_hf_loop
returns INVALID_ARGUMENT with a clear message. WAV format must be
16-bit PCM mono 48 kHz; loaded once at startup, normalized to unit
RMS, shared across sessions, per-session position counter wraps
modulo the loop length.

Implementation:
- src/ota_channel_core/models.{cpp,hpp}: new ChannelType::REAL_HF_LOOP,
  WAV loader + normalizer, RealHfLoopChannelModel class mirroring
  AWGNChannelModel structure, parseChannelType / channelTypeName
  string round-trip for "real_hf_loop".
- src/ota_channel_core/{session_config.hpp,session_context.cpp}:
  carry the shared normalized noise vector into per-session channel
  construction.
- src/ota_simulator_service/ota_simulator_service.{hpp,cpp}: cache
  the noise bed at service start, reject "real_hf_loop" SetChannel
  requests when the cache is empty.
- tools/ota_simulator.cpp: --noise-bed-wav serve flag, startup load,
  cached handoff to the service.
- tools/otasim_ctl.cpp: usage string lists real_hf_loop.
- tools/decode_bench.cpp: usage string update.

Tests:
- tests/test_real_hf_loop_channel.cpp: scaling at fixed SNR, position
  wrap, setSNR while in flight, empty-loop passthrough, parse/name
  round-trip.
- tests/test_grpc_service_smoke.cpp: asserts SetChannel real_hf_loop
  is rejected when --noise-bed-wav was not provided.

Test gate (user's unrestricted Mac):
  cmake --build build -j4                                  -> clean
  ctest --test-dir build -R "RealHfLoopChannel|OTASimulatorFileTransferSNR15"
    --output-on-failure -j1
  -> 2/2 PASS (incl. the 300s file-transfer regression that Codex's
     sandbox couldn't sit through).

End-to-end smoke verified live: ota_simulator serve --noise-bed-wav
accepts the asset, otasim_ctl set-channel --model real_hf_loop --snr 12
switches the lobby cleanly, get-channel reflects the change,
switching back to awgn works.

Out of scope (queued for follow-ups):
- Auto-discover the asset and default to real_hf_loop (separate PR,
  needs test fixups; the explicit opt-in design here is intentional
  for now).
- Multi-bed support (one bed per server start; multi-band selection
  by name later).
- Combined AWGN + loop overlay (single model per session today).
- Loop-seam zero-crossing alignment (10-min loop has no audible
  seam; optimize only if anyone notices).

3-perspective check:
- PHY: nominal SNR uses the same modem reference RMS formula as AWGN
  (matched per-sample noise RMS). Real-HF noise is colored (300-3000
  Hz SSB-passband shape), so OFDM per-carrier SNR differs from AWGN
  at the same nominal SNR — this is intentional realism.
- DSP: WAV strictly 16-bit PCM mono 48 kHz; normalized to unit RMS
  once at load; shared shared_ptr across sessions; per-instance
  position counter wraps modulo loop length.
- Operator: explicit server flag means there's no surprise — the
  channel only becomes real-HF when an admin explicitly switches it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@secup secup merged commit abfdd5c into main May 18, 2026
@secup secup deleted the feat/otasim-real-hf-noise-bed branch May 18, 2026 16:44
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