test(lxst): bidirectional audio content-fidelity validation

Extends the LXST harness from frame-flow only (#62) and decoder QoS
only (#63) to a full content-fidelity test: a known 1kHz sine wave
flows in BOTH directions through the Codec2-3200 round-trip, and
the harness asserts the decoded RMS at each end matches expected
energy within tolerance (after Codec2's lossy speech-codec behavior).

Firmware additions (under PYXIS_TEST_HOOKS):

I2SCapture
  setInjectSine(enabled, freq=1000, amp=0.5)
    Replaces mic input with a phase-continuous synthesized sine.
    Bypasses ES7210 capture and the voice filter chain so the
    encoder sees pure samples — peer's decoded RMS validates that
    pyxis's TX path delivers content.

I2SPlayback
  pcmSampleCount(), pcmSumSquares()
    Decoded-PCM energy accumulators, fed from each successful
    Codec2 decode. uint64 sumsq holds ~2³⁴ frames before overflow,
    far longer than any test call.

LXSTAudio + UIManager (test-only)
  captureSetInjectSine, playbackPcmSampleCount, playbackPcmSumSquares
  test_call_set_inject_sine, test_call_pcm_sample_count,
  test_call_pcm_sum_squares

Serial T: hooks (main.cpp)
  T:CALL_INJECT <on|off> [freq] [amp_pct]
    Drive the capture-side injection from the harness.
  T:CALL_QOS now also returns pcm_n + pcm_ss
    Harness divides + sqrts to RMS for content validation.

Validated with /tmp/lxst_call_harness.py + /tmp/lxst_call_bot.py
(scripts vault-local per the no-PII rule):

  pyxis_rms = 6363, bot_rms = 1930, decode_fail = 0
  PASS: bidirectional audio + content-fidelity validated

The empirical RMS floor is 800 (pycodec2 self-test on 1kHz amp 0.5
yields ~1400; pyxis decoder hits ~6300; bot decoder hits ~1900 —
all far above the ~5-50 silence floor). Codec2 is a speech codec
so pure-tone round-trip is naturally lossy; the test gates on
"audio bytes carry actual content energy", not lossless round-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
torlando-tech
2026-05-08 03:11:09 -04:00
co-authored by Claude Opus 4.7
parent 8b608ec258
commit 12fd28b67c
9 changed files with 171 additions and 6 deletions
+40 -4
View File
@@ -1845,18 +1845,54 @@ static void handle_test_hook_command(const String& line) {
// T:CALL_QOS — wire-level audio fidelity counters from the
// playback decode path. decode_ok = frames Codec2 successfully
// decoded into PCM; decode_fail = frames it rejected (bad mode
// header, corrupt subframe, internal codec error). With a
// well-behaved peer all received frames decode_ok and
// decode_fail stays 0 — a non-zero fail count points at
// the peer's encoder OR network corruption.
// header, corrupt subframe, internal codec error). pcm_n /
// pcm_ss = sample count + cumulative sum-of-squares the harness
// divides into RMS for content-level validation. With a peer
// injecting a 1kHz sine at peak P the expected RMS = P/√2.
if (!ui_manager) { Serial.println("T:ERR no ui_manager"); return; }
Serial.print("T:OK decode_ok=");
Serial.print((unsigned long)ui_manager->test_call_decode_ok());
Serial.print(" decode_fail=");
Serial.print((unsigned long)ui_manager->test_call_decode_fail());
Serial.print(" pcm_n=");
Serial.print((unsigned long)ui_manager->test_call_pcm_sample_count());
Serial.print(" pcm_ss=");
Serial.print((unsigned long long)ui_manager->test_call_pcm_sum_squares());
Serial.print(" state=");
Serial.println(ui_manager->test_call_state_name());
}
else if (cmd == "T:CALL_INJECT") {
// T:CALL_INJECT <on|off> [freq_hz] [amp_pct]
// Replace mic capture with a synthesized sine wave for the
// active call (bypasses ES7210 + voice filters). The bot
// decodes pyxis's audio packets and computes RMS over the
// decoded PCM — should match the expected sine energy.
if (!ui_manager) { Serial.println("T:ERR no ui_manager"); return; }
int sp = args.indexOf(' ');
String on_off = (sp < 0) ? args : args.substring(0, sp);
bool enabled = (on_off == "on" || on_off == "1" || on_off == "true");
int freq = 1000;
float amp = 0.5f;
if (sp >= 0) {
String rest = args.substring(sp + 1);
int sp2 = rest.indexOf(' ');
if (sp2 < 0) {
freq = rest.toInt();
} else {
freq = rest.substring(0, sp2).toInt();
amp = rest.substring(sp2 + 1).toFloat();
}
if (freq <= 0) freq = 1000;
if (amp <= 0.f || amp > 1.f) amp = 0.5f;
}
ui_manager->test_call_set_inject_sine(enabled, freq, amp);
Serial.print("T:OK inject=");
Serial.print(enabled ? "on" : "off");
Serial.print(" freq=");
Serial.print(freq);
Serial.print(" amp=");
Serial.println(amp, 3);
}
else {
Serial.print("T:ERR unknown cmd ");
Serial.println(cmd);