-
-
Notifications
You must be signed in to change notification settings - Fork 122
chore(voip/mlow): make the decoder cross-check vectors regenerable #1112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) { | ||
| // 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; | ||
|
greptile-apps[bot] marked this conversation as resolved.
Comment on lines
+98
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When EOF or an I/O error occurs after at least one complete frame, this 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; | ||
| } | ||
| 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)" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a developer has checked out 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" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On a big-endian host, this
freadplaces the little-endian bytes fromsynth_mic.rawdirectly into nativeopus_int16elements, 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 👍 / 👎.