Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions scripts/mlow-vectors/mlow_frames.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Encode a raw PCM file as MLow/smpl packets and decode each one back through the same
// reference, so a single run produces both halves of a decoder cross-check vector.
//
// Output, one line per packet, on stdout:
//
// <hex payload> <hex s16le pcm>
//
// Diagnostics (packet index, byte count, TOC, sample count) go to stderr, so stdout can be
// redirected straight into a fixture builder.
//
// Two things are easy to get wrong and cost an afternoon each:
//
// * smpl_CreateCodec() must run before the first encode. Without it every encode fails with
// SMPL_ENC_NO_GLOBAL_DATA (-112), which opus_encode flattens into a bare OPUS_INTERNAL_ERROR
// (-3) with no hint that global tables were the problem.
// * The output buffer handed to opus_encode is a hard bound, not a capacity. Under CBR the
// encoder pads to exactly that size, so an oversized buffer fails the pad. Keep it near the
// real packet size.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "opus.h"

// Declared here rather than pulled from a private header: the reference exposes it as a plain
// symbol and the harness only needs the one entry point.
extern int smpl_CreateCodec(void);

#define FS 16000
#define MAX_PACKET 400

static void usage(const char *argv0) {
fprintf(stderr,
"usage: %s <input.raw> <frame_ms> [packets] [bitrate]\n"
" input.raw s16le mono @ 16 kHz\n"
" frame_ms 10, 20, 60 or 120\n"
" packets how many to emit (default: until EOF)\n"
" bitrate encoder bitrate in bps (default 20000)\n",
argv0);
}

int main(int argc, char **argv) {
if (argc < 3) {
usage(argv[0]);
return 2;
}
const char *path = argv[1];
const int frame_ms = atoi(argv[2]);
const int want = argc > 3 ? atoi(argv[3]) : -1;
const int bitrate = argc > 4 ? atoi(argv[4]) : 20000;

if (frame_ms != 10 && frame_ms != 20 && frame_ms != 60 && frame_ms != 120) {
fprintf(stderr, "error: frame_ms must be 10, 20, 60 or 120 (got %d)\n", frame_ms);
return 2;
}
const int samps = FS / 1000 * frame_ms;

if (smpl_CreateCodec() != 0) {
fprintf(stderr, "error: smpl_CreateCodec failed\n");
return 1;
}

FILE *f = fopen(path, "rb");
if (!f) {
perror("open input");
return 2;
}

int err = 0;
OpusEncoder *enc = opus_encoder_create(FS, 1, OPUS_APPLICATION_VOIP, &err);
if (err != OPUS_OK || !enc) {
fprintf(stderr, "error: encoder create: %s\n", opus_strerror(err));
return 1;
}
opus_encoder_ctl(enc, OPUS_SET_USING_SMPL(1));
opus_encoder_ctl(enc, OPUS_SET_BITRATE(bitrate));
opus_encoder_ctl(enc, OPUS_SET_FORCE_CHANNELS(1));

OpusDecoder *dec = opus_decoder_create(FS, 1, &err);
if (err != OPUS_OK || !dec) {
fprintf(stderr, "error: decoder create: %s\n", opus_strerror(err));
return 1;
}
opus_decoder_ctl(dec, OPUS_SET_USING_SMPL(1));

opus_int16 *pcm = malloc((size_t)samps * sizeof(opus_int16));
opus_int16 *out = malloc((size_t)samps * sizeof(opus_int16));
unsigned char packet[MAX_PACKET];
if (!pcm || !out) {
fprintf(stderr, "error: out of memory\n");
return 1;
}

int emitted = 0;
int short_read = 0;
for (int n = 0; want < 0 || n < want; n++) {
if (fread(pcm, sizeof(opus_int16), (size_t)samps, f) != (size_t)samps) {

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 Decode the little-endian PCM before encoding

On a big-endian host, this fread places the little-endian bytes from synth_mic.raw directly into native opus_int16 elements, so the encoder sees every sample byte-swapped even though the harness advertises an s16le input; the output path, by contrast, explicitly serializes little-endian samples. The same pinned oracle can therefore regenerate different frames and reference PCM solely because of host endianness, so convert each input sample from little endian or explicitly reject non-little-endian hosts.

Useful? React with 👍 / 👎.

// A partial frame at EOF, or a read error. Either way this run cannot produce the
// vector that was asked for, and callers overwrite committed fixtures with whatever
// comes out of it -- so remember it and fail below rather than emit a short vector.
short_read = 1;
if (ferror(f)) {
perror("read input");
}
break;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Comment on lines +98 to +106

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 Reject short reads before replacing the fixtures

When EOF or an I/O error occurs after at least one complete frame, this break still lets the harness exit successfully because the final status only checks emitted > 0. The regeneration script explicitly requests eight packets with emit 120 8, so it will then overwrite both fixtures with a shorter vector, and the new tripwire accepts any nonempty matching frame count; an intermittent read failure can therefore silently reduce regression coverage. When want >= 0, return an error unless exactly want packets were emitted, and also distinguish ferror(f) from normal EOF.

Useful? React with 👍 / 👎.

}
opus_int32 len = opus_encode(enc, pcm, samps, packet, sizeof(packet));
if (len <= 0) {
fprintf(stderr, "error: encode packet %d: %s\n", n, opus_strerror(len));
return 1;
}
int ns = opus_decode(dec, packet, len, out, samps, 0);
if (ns <= 0) {
fprintf(stderr, "error: decode packet %d: %s\n", n, opus_strerror(ns));
return 1;
}
for (opus_int32 i = 0; i < len; i++) {
printf("%02x", packet[i]);
}
printf(" ");
for (int i = 0; i < ns; i++) {
unsigned short v = (unsigned short)out[i];
printf("%02x%02x", v & 0xff, (v >> 8) & 0xff);
}
printf("\n");
fprintf(stderr, "packet %d: %d bytes, TOC 0x%02x, %d samples\n", n, len, packet[0], ns);
emitted++;
}

fprintf(stderr, "emitted %d packet(s) of %d ms\n", emitted, frame_ms);

// Exiting 0 with fewer packets than requested would let a regeneration quietly replace a
// fixture with a smaller one: the relative length checks downstream stay satisfied, so the
// coverage loss is invisible. A short count is a failure, not a partial success.
int ok = 1;
if (emitted == 0) {
fprintf(stderr, "error: no packets emitted\n");
ok = 0;
} else if (want >= 0 && emitted != want) {
fprintf(stderr,
"error: asked for %d packet(s) but the input only yielded %d%s\n",
want, emitted, short_read ? " (short read)" : "");
ok = 0;
}

free(pcm);
free(out);
opus_encoder_destroy(enc);
opus_decoder_destroy(dec);
fclose(f);
return ok ? 0 : 1;
}
88 changes: 88 additions & 0 deletions scripts/regenerate-mlow-vectors.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
# Regenerate the MLow decoder cross-check fixtures from the `smpl` C reference.
#
# Consumers never run this — the fixtures are committed and the tests read them directly. It
# exists so the vectors stop being a documented recipe with a missing binary: everything needed
# to reproduce them, including the two non-obvious setup steps, lives in
# `scripts/mlow-vectors/mlow_frames.c`.
#
# The committed fixtures were produced with this exact oracle:
#
# https://github.com/edgardmessias/opus_mlow at 84b076e0809412df22e8a0d26f944610c4a3e40f
#
# Byte-for-byte reproduction is only claimed against that revision. A different checkout can
# change the output because the ORACLE changed, which is not the same thing as a decoder change,
# so the script prints the revision it actually built against and warns when it differs.
#
# Requires a built `smpl` reference (the WhatsApp MLow fork of libopus):
#
# cd "$MLOW_REFERENCE" && ./autogen.sh && ./configure --disable-shared --disable-doc \
# --disable-extra-programs && make -j"$(nproc)"
#
# Then:
#
# MLOW_REFERENCE=/path/to/opus_mlow scripts/regenerate-mlow-vectors.sh
#
# Regenerating changes committed fixtures. Re-run the decoder suite afterwards and treat any
# correlation change as a finding, not as a number to paper over:
#
# cargo test -p wacore --features voip-mlow --lib

set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
testdata="$repo_root/wacore/src/voip/mlow/testdata"
harness_src="$repo_root/scripts/mlow-vectors/mlow_frames.c"

ref="${MLOW_REFERENCE:-}"
if [[ -z "$ref" ]]; then
echo "error: set MLOW_REFERENCE to the built smpl/opus reference checkout" >&2
exit 1
fi
if [[ ! -f "$ref/.libs/libopus.a" ]]; then
echo "error: $ref/.libs/libopus.a not found; build the reference first (see the header of this script)" >&2
exit 1
fi

# Recorded so a regeneration says which oracle produced it; see the header.
expected_rev="84b076e0809412df22e8a0d26f944610c4a3e40f"
actual_rev="$(git -C "$ref" rev-parse HEAD 2>/dev/null || echo unknown)"

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 Verify the linked archive matches the pinned revision

When a developer has checked out expected_rev but .libs/libopus.a is stale from another commit—or was built from dirty sources—rev-parse HEAD reports the expected oracle even though the compile on line 64 links different code, and the script proceeds to overwrite both reference fixtures. Fresh evidence after the earlier revision comment is that this new check validates only the checkout's HEAD, not the pre-existing archive it actually consumes; require a clean checkout plus rebuild, or validate an archive checksum, before claiming byte-for-byte regeneration.

Useful? React with 👍 / 👎.

if [[ "$actual_rev" == "unknown" ]]; then
echo "warning: $ref is not a git checkout; cannot confirm the oracle revision" >&2
elif [[ "$actual_rev" != "$expected_rev" ]]; then
echo "warning: oracle is $actual_rev, fixtures were generated with $expected_rev" >&2
echo " differences below may come from the reference, not from this repository" >&2
fi
echo "==> oracle: $ref @ $actual_rev"

work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT

echo "==> building the harness against $ref"
cc -O2 \
-I "$ref/include" -I "$ref/src" -I "$ref/celt" -I "$ref" \
-o "$work/mlow_frames" "$harness_src" "$ref/.libs/libopus.a" -lm

# One packet per line: "<hex payload> <hex s16le pcm>". Split into the frames fixture and the
# reference PCM the decoder test compares against.
emit() {
local frame_ms="$1" packets="$2" frames_json="$3" pcm_raw="$4"
echo "==> ${frame_ms}ms x ${packets} -> $(basename "$frames_json"), $(basename "$pcm_raw")"
"$work/mlow_frames" "$testdata/synth_mic.raw" "$frame_ms" "$packets" > "$work/vectors.txt"
python3 - "$work/vectors.txt" "$frames_json" "$pcm_raw" <<'PY'
import json, sys
src, frames_path, pcm_path = sys.argv[1:4]
frames, pcm = [], bytearray()
for line in open(src):
payload, samples = line.split()
frames.append(payload)
pcm += bytes.fromhex(samples)
json.dump(frames, open(frames_path, "w"), indent=1)
open(pcm_path, "wb").write(pcm)
print(f" {len(frames)} frames, {len(pcm)//2} samples, TOCs: {sorted({f[:2] for f in frames})}")
PY
}

emit 120 8 "$testdata/mlow_120ms_frames.json" "$testdata/ref_120ms_expected.raw"

echo "==> done; re-run: cargo test -p wacore --features voip-mlow --lib"
34 changes: 34 additions & 0 deletions wacore/src/voip/mlow/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,40 @@ mod tests {
assert!(!dec.had_error(), "the drop must not open the range decoder");
}

/// The two halves of a cross-check vector come out of one harness run and mean nothing apart:
/// refreshing only one leaves the comparison reading mismatched data, which surfaces as a
/// correlation number that moved rather than as an obvious error. Pin what ties them, and pin
/// that the fixture still exercises the multi-frame path it was added for.
#[test]
fn multi_frame_fixture_halves_stay_in_step() {
let frames: Vec<String> =
serde_json::from_str(include_str!("testdata/mlow_120ms_frames.json"))
.expect("mlow_120ms_frames.json");
let pcm_bytes = include_bytes!("testdata/ref_120ms_expected.raw").len();

// An absolute count, not just "non-empty": a regeneration that ends early produces a
// shorter vector whose PCM length still matches its own frame count, so a relative check
// would accept it and the coverage loss would be invisible.
assert_eq!(
frames.len(),
8,
"fixture no longer holds the 8 packets it was generated with; a short regeneration silently reduces coverage"
);
for (i, f) in frames.iter().enumerate() {
let toc = hex::decode(f).expect("hex frame")[0];
assert_eq!(
toc, 0x58,
"frame {i} is TOC {toc:#04x}, not the 120 ms packet this fixture exists to cover"
);
}
assert_eq!(
pcm_bytes,
frames.len() * 6 * SMPL_INTF_LEN * 2,
"reference PCM does not match the frame count; regenerate both halves together with \
scripts/regenerate-mlow-vectors.sh"
);
}

/// The content check: decode a stream of real 120 ms packets and compare against the reference
/// decoder's own output for the same bytes. Geometry alone is not enough, since running the loop
/// the wrong number of times would still produce plausibly-shaped audio while consuming the
Expand Down
42 changes: 31 additions & 11 deletions wacore/src/voip/mlow/testdata/PROVENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,37 @@ exact wire bytes (config-1 `0x10` and config-2 `0x12` frames included). The trip
committed stream still carries `0x10`, `0x12`, and `0x50` TOCs so the per-config decode branches stay
covered; regenerating it requires the external encoder above on `synth_mic.raw`.

## Multi-frame (120 ms) packets
## Multi-frame (120 ms) packets — regenerable in one command

| fixture | consumer / test | oracle recipe |
| --- | --- | --- |
| `mlow_120ms_frames.json` | `decoder.rs::multi_frame_decode_matches_the_reference` | `smpl` C reference encoding `synth_mic.raw` at 120 ms, hex frames |
| `ref_120ms_expected.raw` | same test | the same C reference decoding those frames; s16le @ 16 kHz |

Also not Rust-reproducible: this crate's encoder only emits 60 ms packets. Both files come from one
run of a harness linked against the `smpl` C reference, which encodes `synth_mic.raw` in 1920-sample
(120 ms) frames and decodes each packet back, emitting `<hex payload> <hex s16le pcm>` per line. The
encoder needs `smpl_CreateCodec()` before the first `opus_encode` (it fails
`SMPL_ENC_NO_GLOBAL_DATA` otherwise), `OPUS_SET_USING_SMPL(1)`, and a `max_data_bytes` the CBR pad
can satisfy. Every frame is TOC `0x58`, which the test asserts so the fixture cannot silently drift
off the multi-frame path.
| `mlow_120ms_frames.json` | `decoder.rs::multi_frame_decode_matches_the_reference` | `scripts/regenerate-mlow-vectors.sh` |
| `ref_120ms_expected.raw` | same test | same run of the same script |

```sh
MLOW_REFERENCE=/path/to/opus_mlow scripts/regenerate-mlow-vectors.sh
```

The harness it builds lives in `scripts/mlow-vectors/mlow_frames.c`: it encodes `synth_mic.raw` at
the requested duration through the `smpl` C reference and decodes each packet back, emitting both
halves of the vector in one pass.

The committed bytes were produced against one specific oracle —
`github.com/edgardmessias/opus_mlow` at `84b076e0809412df22e8a0d26f944610c4a3e40f`. Reproduction is
byte for byte **against that revision**, which is what makes a changed fixture a real change rather
than tool drift; against a different checkout the reference itself may have moved, so the script
prints the revision it built with and warns when it does not match.

Both halves must be regenerated together — `decoder.rs::multi_frame_fixture_halves_stay_in_step`
fails if the PCM length stops matching the frame count, and asserts every frame is still TOC `0x58`
so the fixture cannot quietly drift off the multi-frame path.

## What is still not reproducible, and why

The fixtures above the multi-frame section predate the harness and were produced by tools that no
longer exist here. The harness reproduces their SHAPE — run at 60 ms with DTX off it emits the same
TOC mix as `inbound_capture_frames.json` (13x `0x10`, 2x `0x12`, 95x `0x50`) — but not their exact
bytes: packet sizes bracket the committed ones without landing on them, so the original run used an
encoder configuration (or reference build) that was not recorded. Regenerating them would therefore
REPLACE those vectors rather than reproduce them, which is a deliberate decision to make with the
correlation thresholds in hand, not a mechanical refresh.