diff --git a/scripts/mlow-vectors/mlow_frames.c b/scripts/mlow-vectors/mlow_frames.c new file mode 100644 index 000000000..0eb9cc08c --- /dev/null +++ b/scripts/mlow-vectors/mlow_frames.c @@ -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: +// +// +// +// 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 +#include +#include + +#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 [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; + } + 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; +} diff --git a/scripts/regenerate-mlow-vectors.sh b/scripts/regenerate-mlow-vectors.sh new file mode 100755 index 000000000..56d1d1890 --- /dev/null +++ b/scripts/regenerate-mlow-vectors.sh @@ -0,0 +1,126 @@ +#!/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 +# +# The script refuses to run against a reference worktree with uncommitted changes, or against a +# library older than the checkout, because neither can be attributed to a recorded revision. Set +# MLOW_ALLOW_DIRTY_REFERENCE=1 when generating from a modified oracle is the actual intent. +# +# 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)" +if [[ "$actual_rev" == "unknown" ]]; then + echo "warning: $ref is not a git checkout; cannot confirm the oracle revision" >&2 +else + # The revision alone does not identify the build: uncommitted changes still compile into the + # static library while rev-parse keeps reporting the pinned commit, so a modified oracle would + # silently rewrite the fixtures and look like a legitimate update. Refuse unless told otherwise. + if [[ -n "$(git -C "$ref" status --porcelain 2>/dev/null)" ]]; then + if [[ "${MLOW_ALLOW_DIRTY_REFERENCE:-}" == "1" ]]; then + echo "warning: oracle worktree is DIRTY; output does not correspond to $actual_rev" >&2 + else + echo "error: oracle worktree has uncommitted changes, so the fixtures it produces would not" >&2 + echo " correspond to any recorded revision. Commit or stash them, or re-run with" >&2 + echo " MLOW_ALLOW_DIRTY_REFERENCE=1 if you mean to generate from a modified oracle." >&2 + exit 1 + fi + fi + if [[ "$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 +fi +# A clean worktree at the right commit still proves nothing about the ARCHIVE: switching revisions +# without rebuilding leaves a stale .a that links fine and attributes its output to a commit it was +# never built from. Compare against the last thing that changed the checkout. +lib="$ref/.libs/libopus.a" +# Compare against the SOURCES rather than any git metadata file: `git reset --hard` on the same +# branch moves the branch ref while leaving .git/HEAD untouched, so a HEAD mtime check would accept +# an archive built from a different revision. A source newer than the archive is the direct +# statement that the archive does not correspond to the tree it would be attributed to. +if newer="$(find "$ref/smpl" "$ref/src" "$ref/celt" \ + \( -name '*.c' -o -name '*.h' \) -newer "$lib" -print -quit 2>/dev/null)" \ + && [[ -n "$newer" ]]; then + echo "error: $lib is older than $newer, so it was not built from the current tree." >&2 + echo " Rebuild it (make -j\"\$(nproc)\" in \$MLOW_REFERENCE) and re-run." >&2 + exit 1 +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: " ". 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" +# 60 ms with DTX off: the encoder emits VoA=00 frames (TOC 0x10) for the silent stretches of +# synth_mic.raw, which is the only in-repo coverage of the DTX-off decode path. +emit 60 110 "$testdata/mlow_dtx_off_frames.json" "$testdata/ref_dtx_off_expected.raw" + +echo "==> done; re-run: cargo test -p wacore --features voip-mlow --lib" diff --git a/wacore/src/voip/engine.rs b/wacore/src/voip/engine.rs index c8aa5e4eb..58d66ee3a 100644 --- a/wacore/src/voip/engine.rs +++ b/wacore/src/voip/engine.rs @@ -71,7 +71,12 @@ const MAX_PENDING_REACTIONS: usize = 64; /// 20ms @ 16kHz: samples drained to the speaker per playout tick. #[cfg(feature = "voip-mlow")] const PLAYOUT_DRAIN: usize = 320; -/// ~150ms latency ceiling; a burst past this resyncs (drops oldest) instead of lagging. +/// 60ms @ 16kHz: the peer packet size the playout constants were written for, and the assumption +/// used until the first decode reports what the peer actually sends. +#[cfg(feature = "voip-mlow")] +const OPUS_FRAME_SAMPS_60MS: usize = 960; +/// ~150ms latency ceiling for a 60ms peer frame; a burst past this resyncs (drops oldest) instead +/// of lagging. The floor for [`playout_bounds`], which scales it to the peer's packet. #[cfg(feature = "voip-mlow")] const PLAYOUT_CAP: usize = 2400; /// Prebuffer target: prime playout until the jitter buffer holds two 60ms peer frames, so the @@ -81,6 +86,35 @@ const PLAYOUT_CAP: usize = 2400; /// arrival underruns. The cushion has to be one frame above what the per-cycle drain consumes. #[cfg(feature = "voip-mlow")] const PLAYOUT_TARGET: usize = 1920; + +/// Prime target and latency ceiling for a peer sending `packet_samps`-sample packets. +/// +/// The constants above assume a 60ms peer frame, which held while the decoder only produced those. +/// A 120ms packet is a full [`PLAYOUT_TARGET`] on its own, so priming would end on the first one +/// with no cushion at all, and two in flight would exceed [`PLAYOUT_CAP`] and be trimmed on arrival. +/// Keep the same shape instead: prime to two packets so the steady-state buffer never drains below +/// one, and let the ceiling hold that cushion plus a drain slice. +#[cfg(feature = "voip-mlow")] +fn playout_bounds(packet_samps: usize) -> (usize, usize) { + let target = PLAYOUT_TARGET.max(packet_samps.saturating_mul(2)); + (target, PLAYOUT_CAP.max(target + PLAYOUT_DRAIN)) +} + +/// The ceiling to enforce now, given the one in force, the peer's current packet and what is queued. +/// +/// It rises with the packet immediately, but only falls once the backlog fits underneath: a stream +/// dropping to a shorter packet (a genuine switch, or the SID that DTX canonicalizes to) would +/// otherwise trim audio that was legally queued under the previous bound and has not been played. +/// The ceiling exists to bound latency under a burst, not to punish a change of packet size. +#[cfg(feature = "voip-mlow")] +fn effective_playout_cap(current: usize, packet_samps: usize, queued: usize) -> usize { + let want = playout_bounds(packet_samps).1; + if want >= current || queued <= want { + want + } else { + current + } +} /// Bound on how long playout primes before flushing a partial buffer: if the peer sends one frame /// then goes DTX the jitter buffer never reaches `PLAYOUT_TARGET`, so after this many 20ms ticks /// (~200ms) drain whatever is queued instead of holding it (silent) forever. Comfortably above the @@ -641,6 +675,12 @@ struct PcmAudioState { /// Consecutive playout ticks spent priming; bounds the wait so a partial buffer (the peer sent one /// frame then went DTX) is flushed after `MAX_PRIME_TICKS` instead of being held silent forever. priming_ticks: u32, + /// Samples in the peer's most recent packet, the input to [`playout_bounds`]. Starts at the + /// 60ms default until the first decode reports otherwise. + packet_samps: usize, + /// Latency ceiling in force, tracked rather than recomputed so it can lag a shrinking packet + /// until the backlog drains; see [`effective_playout_cap`]. + playout_cap: usize, } /// The video half of the media plane. No jitter buffer or playout tick: an AU is handed to the @@ -897,6 +937,8 @@ impl CallEngine { jitter: VecDeque::new(), priming: true, priming_ticks: 0, + packet_samps: OPUS_FRAME_SAMPS_60MS, + playout_cap: playout_bounds(OPUS_FRAME_SAMPS_60MS).1, }), playout_deadline: NEVER, }) @@ -1753,7 +1795,15 @@ impl CallEngine { let frame = if let Some(frame) = group_frame { frame } else if let Some(pcm) = m.pcm.as_mut() { - drain_playout(&mut pcm.jitter, &mut pcm.priming, &mut pcm.priming_ticks) + pcm.playout_cap = + effective_playout_cap(pcm.playout_cap, pcm.packet_samps, pcm.jitter.len()); + drain_playout( + &mut pcm.jitter, + &mut pcm.priming, + &mut pcm.priming_ticks, + pcm.packet_samps, + pcm.playout_cap, + ) } else { Vec::new() }; @@ -2153,17 +2203,26 @@ impl CallEngine { pcm.decoder .set_redundancy(i32::from(header.payload_type == RTP_PAYLOAD_TYPE_MLOW_RED)); #[cfg(feature = "voip-mlow")] - for s in pcm.decoder.decode(&encoded) { - pcm.jitter - .push_back((s * 32767.0).clamp(-32768.0, 32767.0) as i16); + { + let decoded = pcm.decoder.decode(&encoded); + // Declared, not decoded; see `MlowDecoder::last_packet_samps`. + pcm.packet_samps = pcm.decoder.last_packet_samps(); + for s in decoded { + pcm.jitter + .push_back((s * 32767.0).clamp(-32768.0, 32767.0) as i16); + } } // Bound the buffer on the feed side too: a burst of inbound packets arriving between two 20ms // playout ticks must not grow `jitter` without limit (drain_playout's cap only runs on a // tick). Drop oldest past the same ceiling the drain path uses. #[cfg(feature = "voip-mlow")] - if pcm.jitter.len() > PLAYOUT_CAP { - let drop_n = pcm.jitter.len() - PLAYOUT_CAP; - pcm.jitter.drain(..drop_n); + { + pcm.playout_cap = + effective_playout_cap(pcm.playout_cap, pcm.packet_samps, pcm.jitter.len()); + if pcm.jitter.len() > pcm.playout_cap { + let drop_n = pcm.jitter.len() - pcm.playout_cap; + pcm.jitter.drain(..drop_n); + } } } @@ -2583,13 +2642,16 @@ fn drain_playout( jitter: &mut VecDeque, priming: &mut bool, priming_ticks: &mut u32, + packet_samps: usize, + cap: usize, ) -> Vec { - if jitter.len() > PLAYOUT_CAP { - let drop_n = jitter.len() - PLAYOUT_CAP; + let target = playout_bounds(packet_samps).0; + if jitter.len() > cap { + let drop_n = jitter.len() - cap; jitter.drain(..drop_n); } if *priming { - let reached_target = jitter.len() >= PLAYOUT_TARGET; + let reached_target = jitter.len() >= target; // Bounded wait: a partial buffer that never reaches the target (peer DTX after one frame) is // flushed rather than held silent forever / replayed stale when a much later packet arrives. let timed_out = *priming_ticks >= MAX_PRIME_TICKS && !jitter.is_empty(); @@ -4682,22 +4744,102 @@ mod tests { // One frame is below PLAYOUT_TARGET (two frames): playout holds silence without draining. feed_frame(&mut buf); assert!( - drain_playout(&mut buf, &mut priming, &mut priming_ticks) - .iter() - .all(|&s| s == 0), + drain_playout( + &mut buf, + &mut priming, + &mut priming_ticks, + OPUS_FRAME_SAMPS_60MS, + playout_bounds(OPUS_FRAME_SAMPS_60MS).1 + ) + .iter() + .all(|&s| s == 0), "below the prebuffer target playout primes with silence" ); assert_eq!(buf.len(), 960, "priming must not consume the buffer"); // The second frame reaches the target; playout now produces real audio. feed_frame(&mut buf); assert!( - drain_playout(&mut buf, &mut priming, &mut priming_ticks) - .iter() - .any(|&s| s != 0), + drain_playout( + &mut buf, + &mut priming, + &mut priming_ticks, + OPUS_FRAME_SAMPS_60MS, + playout_bounds(OPUS_FRAME_SAMPS_60MS).1 + ) + .iter() + .any(|&s| s != 0), "at the prebuffer target playout starts real audio" ); } + /// Shrinking the ceiling must never discard audio that is already queued. A 120 ms stream that + /// drops to a shorter packet -- a genuine switch, or the `0x90` SID that `packetize_opus_for_mlow` + /// canonicalizes DTX to, which declares 60 ms -- would otherwise trim the backlog built under the + /// larger bound, clipping the tail of the utterance that is still playing out. + #[test] + fn a_smaller_packet_does_not_trim_the_existing_backlog() { + const BIG: usize = 1920; // 120 ms + const SMALL: usize = 960; // 60 ms, e.g. the canonical SID + let (big_cap, small_cap) = (playout_bounds(BIG).1, playout_bounds(SMALL).1); + assert!( + small_cap < big_cap, + "the premise: the ceiling really does shrink" + ); + + // A primed 120 ms stream carrying more than the smaller ceiling would allow. + let mut cap = big_cap; + let queued = small_cap + 480; + assert!( + queued <= big_cap, + "the premise: legal under the bound it was built with" + ); + + // The shorter packet arrives: the ceiling must not drop below what is already queued. + cap = effective_playout_cap(cap, SMALL, queued); + assert!( + cap >= queued, + "shrinking to {cap} would discard {} queued samples", + queued - cap + ); + + // Once the backlog has drained under the smaller bound, the ceiling follows it down. + cap = effective_playout_cap(cap, SMALL, small_cap - 320); + assert_eq!(cap, small_cap, "the ceiling must not stay large forever"); + } + + /// A peer sending 120 ms packets (WhatsApp Desktop) delivers 1920 samples at a time. The + /// prebuffer and the latency ceiling were both sized around a 60 ms peer frame, so with the + /// larger packet the cushion collapses to zero (one packet already meets the target) and, worse, + /// two in flight exceed the cap and get trimmed — dropping audio on every arrival. Both have to + /// scale with the packet the peer actually sends. + #[test] + fn playout_scales_its_cushion_to_the_peer_packet() { + const P: usize = 1920; // 120 ms @ 16 kHz + let feed_120 = + |b: &mut VecDeque| b.extend((0..P as i32).map(|i| (i % 200) as i16 - 99)); + + // One packet must NOT end priming: draining it takes its own 120 ms, so the buffer would be + // empty again exactly when the next one is due, leaving nothing for a late arrival. + let (mut buf, mut priming, mut ticks) = (VecDeque::new(), true, 0u32); + feed_120(&mut buf); + let first = drain_playout(&mut buf, &mut priming, &mut ticks, P, playout_bounds(P).1); + assert!( + first.iter().all(|&s| s == 0), + "a single 120ms packet is a zero cushion; playout must keep priming" + ); + + // Two in flight must survive: the ceiling has to hold the cushion it just asked for. + feed_120(&mut buf); + let before = buf.len(); + let _ = drain_playout(&mut buf, &mut priming, &mut ticks, P, playout_bounds(P).1); + assert!( + buf.len() + PLAYOUT_DRAIN >= before, + "the latency ceiling trimmed the 120ms cushion: {before} -> {} samples", + buf.len() + ); + assert!(!priming, "two packets is the cushion; playout must start"); + } + #[test] fn playout_prebuffer_absorbs_inter_arrival_jitter() { // Packets (one 60ms peer frame) arrive at a jittered cadence around every 3rd 20ms tick, with @@ -4745,9 +4887,15 @@ mod tests { let real: Vec = (0..ticks) .map(|t| { feed(&mut buf, t); - drain_playout(&mut buf, &mut priming, &mut priming_ticks) - .iter() - .any(|&s| s != 0) + drain_playout( + &mut buf, + &mut priming, + &mut priming_ticks, + OPUS_FRAME_SAMPS_60MS, + playout_bounds(OPUS_FRAME_SAMPS_60MS).1, + ) + .iter() + .any(|&s| s != 0) }) .collect(); assert_eq!( @@ -5942,7 +6090,13 @@ mod tests { if arrivals.contains(&t) { feed_frame(&mut buf); } - let _ = drain_playout(&mut buf, &mut priming, &mut priming_ticks); + let _ = drain_playout( + &mut buf, + &mut priming, + &mut priming_ticks, + OPUS_FRAME_SAMPS_60MS, + playout_bounds(OPUS_FRAME_SAMPS_60MS).1, + ); max_occupancy = max_occupancy.max(buf.len()); } assert!( @@ -5955,9 +6109,15 @@ mod tests { if t % 3 == 0 { feed_frame(&mut buf); } - if drain_playout(&mut buf, &mut priming, &mut priming_ticks) - .iter() - .any(|&s| s != 0) + if drain_playout( + &mut buf, + &mut priming, + &mut priming_ticks, + OPUS_FRAME_SAMPS_60MS, + playout_bounds(OPUS_FRAME_SAMPS_60MS).1, + ) + .iter() + .any(|&s| s != 0) { recovered = true; } @@ -5979,7 +6139,13 @@ mod tests { feed_frame(&mut buf); // one 60ms frame (960) < PLAYOUT_TARGET (1920), then nothing (DTX) // Up to MAX_PRIME_TICKS the partial buffer is held: silence, no drain. for _ in 0..MAX_PRIME_TICKS { - let f = drain_playout(&mut buf, &mut priming, &mut priming_ticks); + let f = drain_playout( + &mut buf, + &mut priming, + &mut priming_ticks, + OPUS_FRAME_SAMPS_60MS, + playout_bounds(OPUS_FRAME_SAMPS_60MS).1, + ); assert!(f.iter().all(|&s| s == 0), "still priming -> silence"); assert_eq!( buf.len(), @@ -5988,7 +6154,13 @@ mod tests { ); } // The next tick hits the bound and flushes the held frame as real audio. - let flushed = drain_playout(&mut buf, &mut priming, &mut priming_ticks); + let flushed = drain_playout( + &mut buf, + &mut priming, + &mut priming_ticks, + OPUS_FRAME_SAMPS_60MS, + playout_bounds(OPUS_FRAME_SAMPS_60MS).1, + ); assert!( flushed.iter().any(|&s| s != 0), "the partial buffer must flush to real audio after the bounded wait" @@ -6008,12 +6180,24 @@ mod tests { let mut priming = true; let mut priming_ticks = 0u32; for _ in 0..(MAX_PRIME_TICKS * 2) { - let f = drain_playout(&mut buf, &mut priming, &mut priming_ticks); + let f = drain_playout( + &mut buf, + &mut priming, + &mut priming_ticks, + OPUS_FRAME_SAMPS_60MS, + playout_bounds(OPUS_FRAME_SAMPS_60MS).1, + ); assert!(f.iter().all(|&s| s == 0), "empty buffer -> silence"); } // First frame arrives: must NOT flush instantly -- the counter didn't age while empty. feed_frame(&mut buf); - let f = drain_playout(&mut buf, &mut priming, &mut priming_ticks); + let f = drain_playout( + &mut buf, + &mut priming, + &mut priming_ticks, + OPUS_FRAME_SAMPS_60MS, + playout_bounds(OPUS_FRAME_SAMPS_60MS).1, + ); assert!( f.iter().all(|&s| s == 0), "one frame is below the target -> still priming, no instant flush" @@ -6021,7 +6205,13 @@ mod tests { assert_eq!(buf.len(), 960, "the first frame is held for the cushion"); // The second frame reaches the target -> real audio drains. feed_frame(&mut buf); - let f = drain_playout(&mut buf, &mut priming, &mut priming_ticks); + let f = drain_playout( + &mut buf, + &mut priming, + &mut priming_ticks, + OPUS_FRAME_SAMPS_60MS, + playout_bounds(OPUS_FRAME_SAMPS_60MS).1, + ); assert!( f.iter().any(|&s| s != 0), "at the target playout starts real audio" diff --git a/wacore/src/voip/mlow/decoder.rs b/wacore/src/voip/mlow/decoder.rs index 289068cc9..fcfcf4658 100644 --- a/wacore/src/voip/mlow/decoder.rs +++ b/wacore/src/voip/mlow/decoder.rs @@ -1,5 +1,5 @@ -//! MLow top-level decoder: RED strip -> TOC routing -> active-frame decode (3 chained 20 ms internal -//! frames: LSF -> pulses -> pitch/gains -> CELP synthesis) -> 60 ms PCM. The synthesis +//! MLow top-level decoder: RED strip -> TOC routing -> active-frame decode (chained 20 ms internal +//! frames: LSF -> pulses -> pitch/gains -> CELP synthesis) -> PCM. The synthesis //! (`smpl_celpdec`) runs the excitation in the codec's float domain (gen_noise + LPC synthesis). The //! cross-frame predictor and synthesis history persist across calls because the stream is //! continuous. @@ -20,8 +20,33 @@ use super::toc::parse_mlow_toc; const OPUS_FRAME_SAMPS: usize = 960; // 60 ms @ 16 kHz +/// Internal 20 ms frames chained inside one packet, or `None` for a duration this decoder cannot +/// run. A packet is not a single unit of decode: the reference derives the loop count from the +/// declared duration while the geometry inside each iteration stays fixed, so 20/60/120 ms differ +/// only in how many times the same decode repeats. +/// +/// 10 ms is the exception and stays unsupported: it halves the internal frame length and the +/// subframe count, which the synthesis does not implement, and decoding it under the wrong geometry +/// would consume the payload with the wrong symbol count and desync the range coder. +fn internal_frames(frame_ms: i32) -> Option { + (frame_ms > 10).then(|| ((frame_ms + 10) / 20) as usize) +} + +/// Does a decode that ended after `consumed` bytes of a `storage`-byte body land where a valid +/// stream can end? Under-running is always malformed; the upper slack absorbs the range coder's +/// final carry bytes, which the encoder does not emit. +/// +/// The bound is four, taken from the shipped decoder rather than from the C fork: `WhatsAppNative.dll` +/// @ `0x180337e30` does `add r8d, 0x4` before its second compare, while the fork's +/// `smpl_check_end_result` allows only `+2`. Two is the stricter of the pair, so it would conceal +/// frames the official client plays — a false report of corruption, silencing audio that is fine. +fn endpoint_is_valid(storage: u32, consumed: u32) -> bool { + storage <= consumed && consumed <= storage + 4 +} + /// Stateful pure-Rust MLow decoder. Decodes one RTP payload (a bare MLow frame, or a SplitRed -/// packet when redundancy was negotiated) into a 60 ms / 960-sample PCM frame at 16 kHz. +/// packet when redundancy was negotiated) into a PCM frame at 16 kHz, one 20 ms internal frame +/// per chained frame in the packet. pub struct MlowDecoder { state: SmplDecoderState, redundancy: i32, @@ -31,11 +56,18 @@ pub struct MlowDecoder { /// never gates output. had_error: bool, /// Count of inbound frames dropped because they fall outside this decoder's single operating point - /// (16kHz wideband, low_rate=0, 60ms). Such a frame would desync the range coder if decoded, so it - /// is dropped (treated as a lost frame). The count drives a once + every-100th `warn` (which names - /// the offending dimension) so a live capture reveals whether real peers emit these (decides the - /// follow-ups). + /// (16kHz wideband, low_rate=0, and a duration whose internal geometry it implements). Such a + /// frame would desync the range coder if decoded, so it is dropped (treated as a lost frame). The + /// count drives a once + every-100th `warn` naming the offending dimension. dropped_unsupported: u32, + /// Frames concealed because the decode did not end where the body said it should. Drives a + /// once + every-100th `warn`, so a peer sending a stream this decoder cannot read is visible in + /// a log rather than silently quiet. + malformed: u32, + /// Samples the last packet DECLARED, from its TOC, which is not always what `decode` returned: + /// a SID, a drop or a standard-Opus escape emits a fixed slot regardless of duration. Consumers + /// sizing a jitter cushion need the declared value, since that is what sets arrival cadence. + last_packet_samps: usize, } impl Default for MlowDecoder { @@ -51,6 +83,8 @@ impl MlowDecoder { redundancy: 0, had_error: false, dropped_unsupported: 0, + malformed: 0, + last_packet_samps: OPUS_FRAME_SAMPS, } } @@ -62,6 +96,12 @@ impl MlowDecoder { self.had_error } + /// Samples in the most recent packet as its TOC declared them, independent of what `decode` + /// emitted for it. Starts at a 60 ms packet. + pub fn last_packet_samps(&self) -> usize { + self.last_packet_samps + } + /// Set the negotiated RED redundancy level (0 = bare frames, the common case). pub fn set_redundancy(&mut self, n: i32) { self.redundancy = n; @@ -73,7 +113,8 @@ impl MlowDecoder { self.had_error = false; } - /// Decode one RTP MLow payload into a 60 ms (960-sample) PCM frame, float in [-1, 1]. + /// Decode one RTP MLow payload into a PCM frame, float in [-1, 1]. The sample count follows + /// the packet's declared duration; a dropped or silenced frame yields a 60 ms slot. pub fn decode(&mut self, payload: &[u8]) -> Vec { if payload.is_empty() { return vec![0.0; OPUS_FRAME_SAMPS]; @@ -100,6 +141,9 @@ impl MlowDecoder { return vec![0.0; OPUS_FRAME_SAMPS]; } let toc = parse_mlow_toc(frame[0]); + if toc.frame_ms > 0 && toc.sample_rate > 0 { + self.last_packet_samps = (toc.sample_rate / 1000 * toc.frame_ms) as usize; + } if toc.std_opus { let out_len = (16000 / 1000 * toc.frame_ms) as usize; log::debug!( @@ -108,27 +152,29 @@ impl MlowDecoder { ); return vec![0.0; out_len]; } - // Inactive / SID (DTX/CNG) frames carry no decodable voice and are silenced without opening the - // range coder, so their geometry can never desync. Handle them before the operating-point guard: - // otherwise an inactive off-point frame (e.g. the 10ms startup silence a real peer emits before - // speech) would trip the loud "dropped" canary instead of being the benign silence it is. A full - // 60ms slot keeps the playout cadence regardless of the frame's nominal duration. - if toc.sid || !toc.active { - log::debug!("mlow: DTX/SID TOC 0x{:02x} -> 60ms silence", frame[0]); + // A SID (DTX/CNG) frame carries comfort noise rather than coded voice and is silenced without + // opening the range coder, so its geometry can never desync. Handle it before the + // operating-point guard so an off-point SID is the benign silence it is rather than tripping + // the "dropped" canary. A full 60ms slot keeps the playout cadence regardless of the frame's + // nominal duration. + // + // A frame that is merely coded inactive is NOT silence: with DTX off the encoder keeps sending + // background noise this way, and the reference decodes it. It goes through the normal path. + if toc.sid { + log::debug!("mlow: SID TOC 0x{:02x} -> 60ms silence", frame[0]); return vec![0.0; OPUS_FRAME_SAMPS]; } - // Operating-point guard for active frames: an active frame at a different internal rate, the - // low_rate=1 2x160 geometry, or a non-60ms duration would desync the range coder, since - // decode_active_frame always runs the 3x20ms / 60ms geometry and would consume the payload with - // the wrong symbol count (garbage plus a poisoned cross-frame predictor that propagates to later - // packets). Drop it as a lost frame so the predictor holds its last good values. flag2 is the - // smpl TOC's low_rate bit; the warn names the offending dimension so a live capture shows whether - // real peers ever emit active out-of-point frames in 1:1 calls. + // Operating-point guard for active frames: a different internal rate, the low_rate=1 2x160 + // geometry, or a duration whose internal geometry differs would consume the payload with the + // wrong symbol count and desync the range coder (garbage plus a poisoned cross-frame predictor + // that propagates to later packets). Drop those as lost frames so the predictor holds its last + // good values. flag2 is the smpl TOC's low_rate bit. + let frames = internal_frames(toc.frame_ms); let off_point = if toc.sample_rate != 16000 { Some(("rate", i64::from(toc.sample_rate / 1000))) } else if toc.flag2 { Some(("low_rate", 1)) - } else if toc.frame_ms != 60 { + } else if frames.is_none() { Some(("frame_ms", i64::from(toc.frame_ms))) } else { None @@ -138,17 +184,26 @@ impl MlowDecoder { if self.dropped_unsupported == 1 || self.dropped_unsupported.is_multiple_of(100) { log::warn!( "mlow: dropping out-of-operating-point frame #{} ({dim}={val}, TOC 0x{:02x}); \ - the 1:1 decoder is 16kHz / low_rate=0 / 60ms only", + the decoder runs 16kHz / low_rate=0 / 20-120ms", self.dropped_unsupported, frame[0] ); } return vec![0.0; OPUS_FRAME_SAMPS]; } - self.decode_active_frame(frame, OPUS_FRAME_SAMPS) + let frames = frames.expect("the guard above rejected every unsupported duration"); + self.decode_active_frame(frame, frames * SMPL_INTF_LEN, frames, toc.active) } - fn decode_active_frame(&mut self, frame: &[u8], out_len: usize) -> Vec { + /// `coded_as_active_voice` gates two symbols that a frame coded inactive never puts on the wire; + /// reading them would consume symbols that were never written and desync everything after. + fn decode_active_frame( + &mut self, + frame: &[u8], + out_len: usize, + frames: usize, + coded_as_active_voice: bool, + ) -> Vec { let config = (frame[0] >> 2) as usize & 1; let tbl = load_smpl_tables(); let synth_t = load_smpl_synth_tables(); @@ -159,19 +214,34 @@ impl MlowDecoder { // The low_rate bit of the smpl TOC (this capture is low_rate==0; the synth gates on it). let low_rate = (frame[0] >> 2) & 1 != 0; - let mut out: Vec = Vec::with_capacity(3 * SMPL_INTF_LEN); - // Collect the per-40-block lags (8 per frame, 24 per packet) and the average normalized - // bitrate for the per-packet harmonic postfilter. - let mut packet_lags: Vec = Vec::with_capacity(3 * 8); + // The overrun that invalidates a body is only detectable after the last internal frame, by + // which point the loop has already advanced the LSF predictor, the CELP history and + // `prev_nlsf`. Keep a copy so concealment can undo them: parameters invented past the end of + // a bad body must not seed the next packet. The reference leaves them advanced, but it never + // meets a stream it cannot read; this decoder does, and the leak is audible in the frame + // after. The copy is a few KB once per packet, against 20-120 ms of audio. + let state_before = self.state.clone(); + + let mut out: Vec = Vec::with_capacity(frames * SMPL_INTF_LEN); + // Collect the per-40-block lags (8 per internal frame) and the average normalized bitrate + // for the per-packet harmonic postfilter. + let mut packet_lags: Vec = Vec::with_capacity(frames * 8); let mut avg_norm_br = 0.0f32; - for f in 0..3 { - let lsf = decode_smpl_lsf(&mut dec, tbl, &mut self.state.lstate, config, f); + for f in 0..frames { + let lsf = decode_smpl_lsf( + &mut dec, + tbl, + &mut self.state.lstate, + config, + f, + coded_as_active_voice, + ); let pulses = decode_smpl_pulses( &mut dec, cc, SMPL_INTF_LEN as i32, 4, - 1, + i32::from(coded_as_active_voice), config as i32, lsf.stage1, ); @@ -241,6 +311,36 @@ impl MlowDecoder { out.extend_from_slice(&sig); } + // Endpoint check, before the postfilter as in the reference: the range decoder returns zero + // past either end of its storage WITHOUT flagging it, so an impossible stream decodes into + // plausible-looking symbols and the synthesis can diverge to full scale. Comparing where the + // decode ended against what the body actually held is the only way to see it. Anything + // outside the accepted window is a malformed frame, concealed as a lost one rather than + // synthesized. State already advanced is left as-is, matching the reference, which also + // returns without rolling back. + let consumed_bytes = (dec.tell().max(0) as u32).div_ceil(8); + let body = dec.storage(); + if !endpoint_is_valid(body, consumed_bytes) || dec.err != 0 { + // Sticky flag first: this branch swallows the range-decoder failure it is reporting, and + // `had_error` is what the suites read to see it. + if dec.err != 0 { + self.had_error = true; + } + self.state = state_before; + self.malformed += 1; + if self.malformed == 1 || self.malformed.is_multiple_of(100) { + log::warn!( + "mlow: concealing malformed frame #{} (TOC 0x{:02x}: decode ended at {} bytes \ + of a {}-byte body)", + self.malformed, + frame[0], + consumed_bytes, + body + ); + } + return vec![0.0; out_len]; + } + // Per-packet harmonic postfilter (the codec's final pitch comb + 48-sample group delay), run // once over the whole packet with the 24 per-40-block lags and the average normalized bitrate. let plen = out.len(); @@ -250,7 +350,7 @@ impl MlowDecoder { plen, &packet_lags, packet_lags.len(), - avg_norm_br / 3.0, + avg_norm_br / frames as f32, ); // The C-domain synthesis output is already float in [-1, 1]; clamp in place. @@ -309,7 +409,7 @@ pub(crate) fn diag_decode_params() -> Vec { let config = (frame[0] >> 2) as usize & 1; let mut dec = RangeDecoder::new(&frame[1..]); for f in 0..3 { - let lsf = decode_smpl_lsf(&mut dec, tbl, &mut lstate, config, f); + let lsf = decode_smpl_lsf(&mut dec, tbl, &mut lstate, config, f, true); let pulses = decode_smpl_pulses( &mut dec, cc, @@ -362,6 +462,318 @@ pub(crate) fn diag_decode_params() -> Vec { mod tests { use super::*; + /// The loop count per packet duration, against the reference decoder's + /// `num_frames = (packet_len_ms + 10) / 20`. 10 ms is excluded because it also changes the + /// internal frame length and subframe count, which the synthesis does not implement. + #[test] + fn internal_frame_count_matches_the_reference_geometry() { + assert_eq!(internal_frames(20), Some(1)); + assert_eq!(internal_frames(60), Some(3)); + assert_eq!(internal_frames(120), Some(6)); + assert_eq!(internal_frames(10), None); + } + + /// WhatsApp Desktop sends 120 ms packets (TOC 0x58) on ordinary 1:1 calls. They must decode, + /// not be discarded: dropping them silences the whole stream while the peer is speaking. + #[test] + fn multi_frame_packet_decodes_to_its_full_duration() { + let toc = parse_mlow_toc(0x58); + assert_eq!(toc.frame_ms, 120, "0x58 declares a 120 ms packet"); + assert!(toc.active && !toc.sid && !toc.std_opus); + assert_eq!(toc.sample_rate, 16000); + assert!(!toc.flag2, "0x58 is the supported rate mode"); + + let mut dec = MlowDecoder::new(); + let out = dec.decode(&[0x58, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22]); + assert_eq!( + out.len(), + 6 * SMPL_INTF_LEN, + "a 120 ms packet must yield 120 ms of PCM" + ); + } + + /// A 20 ms packet shares the same internal geometry and must decode to exactly one frame. + #[test] + fn single_frame_packet_decodes_to_one_internal_frame() { + let mut dec = MlowDecoder::new(); + let out = dec.decode(&[0x48, 0xAA, 0xBB, 0xCC]); + assert_eq!( + out.len(), + SMPL_INTF_LEN, + "a 20 ms packet is one 20 ms frame" + ); + } + + /// The failure case the geometry guard exists for: 10 ms halves the internal frame length and + /// the subframe count, so it must still be dropped rather than decoded under the wrong geometry, + /// into the same 60 ms silence slot the other drops use. + #[test] + fn ten_ms_active_packet_is_still_dropped() { + let toc = parse_mlow_toc(0x40); + assert_eq!(toc.frame_ms, 10); + assert!(toc.active); + + let mut dec = MlowDecoder::new(); + let out = dec.decode(&[0x40, 0xAA, 0xBB, 0xCC]); + assert_eq!(out.len(), OPUS_FRAME_SAMPS); + assert!( + out.iter().all(|&s| s == 0.0), + "10 ms runs a geometry the synthesis does not implement" + ); + assert!(!dec.had_error(), "the drop must not open the range decoder"); + } + + /// The playout cushion is sized from the peer's packet duration, and a SID must not shrink it: + /// a DTX transition returns a fixed 60 ms silence slot regardless of the duration the packet + /// declares, so reading the cushion off the OUTPUT length would drop buffered speech that had + /// not been played yet. The declared duration is the one that governs arrival cadence. + #[test] + fn declared_duration_survives_a_dtx_transition() { + let mut dec = MlowDecoder::new(); + let _ = dec.decode(&[0x58, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22]); + assert_eq!(dec.last_packet_samps(), 6 * SMPL_INTF_LEN); + + // A SID that still declares 120 ms (0x98 = SID | 120 ms) emits a 60 ms silence slot. + let sid = dec.decode(&[0x98, 0xAA, 0xBB, 0xCC]); + assert_eq!(sid.len(), OPUS_FRAME_SAMPS, "SID is a fixed silence slot"); + assert_eq!( + dec.last_packet_samps(), + 6 * SMPL_INTF_LEN, + "the peer is still on 120 ms packets; the cushion must not shrink" + ); + + // A peer that genuinely moves to 60 ms is learned. + let _ = dec.decode(&[0x50, 0xAA, 0xBB, 0xCC]); + assert_eq!(dec.last_packet_samps(), 3 * SMPL_INTF_LEN); + } + + /// A `VoA=00` packet is a normal frame carrying background noise, not a SID: the reference + /// decodes it, and with DTX off a peer sends nothing else during a pause. Silencing it drops + /// ~12% of a real stream on the floor while the call merely sounds quiet. + #[test] + fn dtx_off_frames_decode_to_audio() { + let frames: Vec = + serde_json::from_str(include_str!("testdata/mlow_dtx_off_frames.json")) + .expect("mlow_dtx_off_frames.json"); + let refp: Vec = include_bytes!("testdata/ref_dtx_off_expected.raw") + .chunks_exact(2) + .map(|b| i16::from_le_bytes([b[0], b[1]]) as f32 / 32768.0) + .collect(); + + let mut dec = MlowDecoder::new(); + let mut out: Vec = Vec::new(); + let mut spans = Vec::new(); + for hex_frame in &frames { + let frame = hex::decode(hex_frame).unwrap(); + let start = out.len(); + out.extend_from_slice(&dec.decode(&frame)); + // The frames this covers: not SID, VoA=0, hang-over clear, i.e. coded_as_active_voice + // == 0. `0x12` shares VoA=0 but sets the hang-over bit, which makes it active and + // already decoded, so it must not be counted here. + if frame[0] & 0xC2 == 0 { + spans.push((start, out.len())); + } + } + assert!( + !spans.is_empty(), + "fixture lost its DTX-off frames: this path is no longer covered" + ); + assert_eq!(out.len(), refp.len()); + + let energy = |v: &[f32]| v.iter().map(|&x| (x as f64).powi(2)).sum::(); + let (mut e_ref, mut e_ours, mut n) = (0.0, 0.0, 0usize); + for (s, e) in &spans { + e_ref += energy(&refp[*s..*e]); + e_ours += energy(&out[*s..*e]); + n += e - s; + } + let (rms_ref, rms_ours) = ((e_ref / n as f64).sqrt(), (e_ours / n as f64).sqrt()); + assert!( + rms_ref > 0.01, + "the reference itself has no audio here; the fixture is wrong" + ); + assert!( + rms_ours > rms_ref * 0.5, + "DTX-off frames decoded to {rms_ours:.5} rms against the reference's {rms_ref:.5}" + ); + } + + /// A stream that ends where it claims to must be accepted. This is the guard against the + /// endpoint check being too strict: the synthetic 120 ms vector is a well-formed six-frame + /// packet, and rejecting it would silence audio that decodes correctly. + #[test] + fn endpoint_check_accepts_a_well_formed_packet() { + let frames: Vec = + serde_json::from_str(include_str!("testdata/mlow_120ms_frames.json")).unwrap(); + let mut dec = MlowDecoder::new(); + for hex_frame in &frames { + let out = dec.decode(&hex::decode(hex_frame).unwrap()); + assert!( + out.iter().any(|&s| s != 0.0), + "a valid packet must not be rejected as malformed" + ); + } + } + + /// The accepted window is the shipped decoder's, not the C fork's. The fork stops at `+2`, so a + /// stream ending three or four bytes long would be concealed here and played by the official + /// client. Pinned as arithmetic because no synthetic body lands on `+3` on demand. + #[test] + fn endpoint_window_matches_the_shipped_decoder() { + assert!(endpoint_is_valid(40, 40), "exact end is valid"); + assert!(endpoint_is_valid(40, 42), "the fork's +2 stays valid"); + assert!( + endpoint_is_valid(40, 44), + "+4 is valid: WhatsAppNative.dll @ 0x180337e30 adds 4 before its second compare, and \ + rejecting here would silence audio the official client plays" + ); + assert!(!endpoint_is_valid(40, 45), "+5 over-runs"); + assert!( + !endpoint_is_valid(40, 39), + "under-running is always malformed" + ); + } + + /// A body whose decode consumes far more bits than it holds is malformed: the range decoder + /// returns zero past the end without flagging it, so the synthesis runs on invented symbols and + /// can diverge to full scale. Conceal it as a lost frame rather than emitting that. + #[test] + fn endpoint_check_rejects_an_overrunning_body() { + // A 120 ms TOC with a body far too short for six internal frames. + let mut dec = MlowDecoder::new(); + let out = dec.decode(&[0x58, 0x03, 0x1a, 0xfb, 0x0a]); + assert_eq!(out.len(), 6 * SMPL_INTF_LEN, "still a full 120 ms slot"); + assert!( + out.iter().all(|&s| s == 0.0), + "an over-running body must be concealed, not synthesized" + ); + } + + /// Real 120 ms packets captured from a live WhatsApp Desktop peer must decode to speech, not to + /// full-scale noise. Reported on #1105 after the multi-frame admission landed: frames are now + /// accepted, but the decode diverges partway through the packet and saturates, which is audibly + /// worse than the silence it replaced. + /// + /// The assertions are deliberately coarse — this pins "the output is not garbage", which is what + /// regressed, without pretending to a bit-exact target the fixture cannot supply. + /// + /// Concealing a malformed frame must leave no trace: the decode loop mutates the LSF predictor, + /// the CELP history and `prev_nlsf` before the overrun is detectable, so without a rollback the + /// NEXT packet is synthesized partly from parameters invented past the end of the bad body. + /// The reference does not roll back, but it also never meets these packets; we do, repeatedly. + #[test] + fn a_concealed_frame_does_not_contaminate_the_next() { + let frames: Vec = + serde_json::from_str(include_str!("testdata/inbound_capture_frames.json")).unwrap(); + let real = hex::decode(&frames[0]).unwrap(); + + let mut fresh = MlowDecoder::new(); + let want = fresh.decode(&real); + + let mut contaminated = MlowDecoder::new(); + let bad = contaminated.decode(&[0x58, 0x03, 0x1a, 0xfb, 0x0a]); + assert!(bad.iter().all(|&s| s == 0.0), "the bad frame is concealed"); + let got = contaminated.decode(&real); + + assert_eq!(got.len(), want.len()); + assert!( + got == want, + "a real frame after a concealed one must decode identically to one decoded on a fresh \ + decoder; state from the malformed body leaked into it" + ); + } + + /// 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 = + 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 + /// payload at the wrong symbol count. See testdata/PROVENANCE.md for the oracle. + #[test] + fn multi_frame_decode_matches_the_reference() { + let frames: Vec = + serde_json::from_str(include_str!("testdata/mlow_120ms_frames.json")) + .expect("mlow_120ms_frames.json"); + let refp: Vec = include_bytes!("testdata/ref_120ms_expected.raw") + .chunks_exact(2) + .map(|b| i16::from_le_bytes([b[0], b[1]]) as f32 / 32768.0) + .collect(); + + let mut dec = MlowDecoder::new(); + let mut out: Vec = Vec::new(); + for hex_frame in &frames { + let frame = hex::decode(hex_frame).unwrap(); + assert_eq!(frame[0], 0x58, "the fixture must stay 120 ms packets"); + out.extend_from_slice(&dec.decode(&frame)); + } + assert_eq!(out.len(), refp.len(), "decode length vs reference"); + + let n = refp.len(); + let (mr, mo) = ( + refp.iter().map(|&v| v as f64).sum::() / n as f64, + out.iter().map(|&v| v as f64).sum::() / n as f64, + ); + let (mut sxy, mut sxx, mut syy) = (0f64, 0f64, 0f64); + for i in 0..n { + let (dr, dz) = (refp[i] as f64 - mr, out[i] as f64 - mo); + sxy += dr * dz; + sxx += dr * dr; + syy += dz * dz; + } + let corr = sxy / (sxx * syy).sqrt(); + assert!(corr > 0.999, "lag-0 corr {corr:.6} vs reference"); + } + + /// Decoding a multi-frame packet must leave the cross-frame predictor usable: a real 60 ms frame + /// after it still has to produce audio. + #[test] + fn multi_frame_packet_does_not_poison_later_frames() { + let frames: Vec = + serde_json::from_str(include_str!("testdata/inbound_capture_frames.json")).unwrap(); + let real = hex::decode(&frames[0]).unwrap(); + + let mut dec = MlowDecoder::new(); + let _ = dec.decode(&[0x58, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22]); + let after = dec.decode(&real); + assert_eq!(after.len(), 960); + assert!( + after.iter().any(|&s| s != 0.0), + "a real 60 ms frame after a 120 ms packet must still decode" + ); + } + // End-to-end: decode the whole capture and compare against the reference output // (`ref_usesmpl_expected.raw`; see testdata/PROVENANCE.md). // @@ -498,13 +910,14 @@ mod tests { "low_rate=1 frame must drop to silence" ); - // A non-60ms ACTIVE 16kHz/low_rate=0 TOC (20ms, e.g. 0x48) must also drop: decode_active_frame - // hardcodes the 3x20ms / 60ms geometry, so a 20ms frame would otherwise desync the range coder. - let out_20ms = dec.decode(&[0x48, 0xAA, 0xBB, 0xCC]); - assert_eq!(out_20ms.len(), 960); + // A 10ms ACTIVE 16kHz/low_rate=0 TOC (0x40) must also drop: it is the one duration whose + // internal frame length and subframe count differ, so decoding it under the implemented + // geometry would desync the range coder. 20/60/120ms all decode (see the geometry tests). + let out_10ms = dec.decode(&[0x40, 0xAA, 0xBB, 0xCC]); + assert_eq!(out_10ms.len(), 960); assert!( - out_20ms.iter().all(|&s| s == 0.0), - "a non-60ms active frame must drop to silence" + out_10ms.iter().all(|&s| s == 0.0), + "a 10ms active frame must drop to silence" ); // The drops never opened the range decoder, so the predictor is intact: the real frame still @@ -520,13 +933,12 @@ mod tests { ); } - // An inactive / DTX frame (TOC 0x00: vad=false so active=false, 16kHz, low_rate=0, 10ms) is the - // benign startup/comfort silence a real peer emits, not a desync hazard: inactive frames are - // silenced without opening the range coder regardless of geometry. It must take the quiet DTX/SID - // path (a full 60ms silence slot, range coder untouched) and must NOT count as an out-of-operating - // -point drop, which is reserved for active frames that would have lost decodable audio. + // A SID frame (TOC 0x80, the DTX/CNG marker) is comfort noise, not a desync hazard: it is + // silenced without opening the range coder regardless of geometry. It must take the quiet path (a + // full 60ms silence slot, range coder untouched) and must NOT count as an out-of-operating-point + // drop, which is reserved for frames that would have lost decodable audio. #[test] - fn inactive_off_point_frame_is_silenced_not_dropped() { + fn sid_frame_is_silenced_not_dropped() { let frames: Vec = serde_json::from_str(include_str!("testdata/inbound_capture_frames.json")).unwrap(); let real = hex::decode(&frames[0]).unwrap(); @@ -537,8 +949,8 @@ mod tests { "a real low_rate=0 frame must decode to audio" ); - // TOC 0x00 -> inactive: silenced via the DTX/SID path, not the operating-point drop. - let inactive = dec.decode(&[0x00, 0xAA, 0xBB, 0xCC]); + // TOC 0x80 -> SID: silenced via the comfort-noise path, not the operating-point drop. + let inactive = dec.decode(&[0x80, 0xAA, 0xBB, 0xCC]); assert_eq!( inactive.len(), 960, @@ -550,16 +962,16 @@ mod tests { ); assert_eq!( dec.dropped_unsupported, 0, - "an inactive frame is the DTX/silence path, not an operating-point drop" + "a SID frame is the comfort-noise path, not an operating-point drop" ); assert!( !dec.had_error(), - "the inactive path must not open the range decoder" + "the SID path must not open the range decoder" ); - // Contrast: an active off-point frame (0x48 = vad=true, 20ms) IS counted, proving the drop + // Contrast: an active off-point frame (0x60 = vad=true, 32 kHz) IS counted, proving the drop // counter discriminates real audio loss from benign inactive silence. - let _ = dec.decode(&[0x48, 0xAA, 0xBB, 0xCC]); + let _ = dec.decode(&[0x60, 0xAA, 0xBB, 0xCC]); assert_eq!( dec.dropped_unsupported, 1, "an active off-point frame must count as a drop" diff --git a/wacore/src/voip/mlow/encode.rs b/wacore/src/voip/mlow/encode.rs index 350e6f418..167007987 100644 --- a/wacore/src/voip/mlow/encode.rs +++ b/wacore/src/voip/mlow/encode.rs @@ -660,6 +660,7 @@ mod tests { &mut lstate, config, f, + true, ); let pulses = super::super::smpl_pulse::decode_smpl_pulses( &mut dec, diff --git a/wacore/src/voip/mlow/quality_tests.rs b/wacore/src/voip/mlow/quality_tests.rs index 0ca2c93a8..2757e4a40 100644 --- a/wacore/src/voip/mlow/quality_tests.rs +++ b/wacore/src/voip/mlow/quality_tests.rs @@ -660,8 +660,13 @@ fn decoder_silence_frames_produce_zero() { let ref_r = rms(&want); let got = dec.decode(&frame); let rust_r = rms(&got); - // A frame the reference decodes to near-silence must also be near-silence in Rust. - if ref_r < 0.001 { + // Only a SID frame is genuinely silence. This fixture also holds coded-inactive frames + // (VoA=0, hang-over clear) whose PCM was ZEROED when it was generated, to match a decoder + // that routed them to silence. The reference decodes those to background noise, so they are + // not an oracle for this property and asserting over them would re-pin the old behavior; + // `decoder.rs::dtx_off_frames_decode_to_audio` covers them against an unmodified vector. + let is_sid = frame[0] & 0x80 != 0; + if ref_r < 0.001 && is_sid { assert!( rust_r < 0.001, "frame {i}: reference is silence (RMS={ref_r:.6}) but Rust produced RMS={rust_r:.6}" diff --git a/wacore/src/voip/mlow/rangecoder.rs b/wacore/src/voip/mlow/rangecoder.rs index c6ab76e10..8e1b7d3a2 100644 --- a/wacore/src/voip/mlow/rangecoder.rs +++ b/wacore/src/voip/mlow/rangecoder.rs @@ -337,6 +337,13 @@ impl<'a> RangeDecoder<'a> { pub(crate) fn tell(&self) -> i32 { self.nbits_total - ilog(self.rng) } + + /// Bytes the payload actually holds. Paired with [`tell`] this is how a caller checks that a + /// stream ended where it claimed to: reads past either end silently return zero here, so an + /// impossible length is only visible by comparing the two. + pub(crate) fn storage(&self) -> u32 { + self.storage + } } /// Opus/CELT range ENCODER (`ec_enc`), the exact inverse of `RangeDecoder`, used by the mlow diff --git a/wacore/src/voip/mlow/smpl_celpdec.rs b/wacore/src/voip/mlow/smpl_celpdec.rs index 1e55e1599..9d438beba 100644 --- a/wacore/src/voip/mlow/smpl_celpdec.rs +++ b/wacore/src/voip/mlow/smpl_celpdec.rs @@ -344,6 +344,26 @@ pub(crate) struct CelpDecState { pub(crate) dbg_exc_pre: Vec, } +// Hand-written so the test-only excitation trace is NOT carried along. It accumulates every +// synthesized subframe for the whole stream and is never cleared, so copying it in the per-packet +// rollback snapshot would make decoding quadratic in stream length. It is a diagnostic capture, not +// codec state that concealment has to restore. +impl Clone for CelpDecState { + fn clone(&self) -> Self { + Self { + noise: self.noise.clone(), + acb_state: self.acb_state.clone(), + acb_state_len: self.acb_state_len, + lpc_synth_mem: self.lpc_synth_mem, + lsf_prev: self.lsf_prev, + prev_nrgres: self.prev_nrgres, + hp: self.hp.clone(), + #[cfg(test)] + dbg_exc_pre: Vec::new(), + } + } +} + impl Default for CelpDecState { fn default() -> Self { let acb_state_len = SMPL_SUBFR_LEN + 2 * SMPL_MAX_PITCH_LAG + SMPL_LTP_INTERPOL_DELAY; @@ -541,7 +561,7 @@ mod tests { let low_rate = (frame[0] >> 2) & 1 != 0; let mut dec = crate::voip::mlow::rangecoder::RangeDecoder::new(&frame[1..]); for f in 0..3 { - let lsf = decode_smpl_lsf(&mut dec, tbl, &mut lstate, config, f); + let lsf = decode_smpl_lsf(&mut dec, tbl, &mut lstate, config, f, true); let pulses = decode_smpl_pulses(&mut dec, cc, 320, 4, 1, config as i32, lsf.stage1); let voiced = lsf.stage1 == 1; let mut params = CelpDecParams { diff --git a/wacore/src/voip/mlow/smpl_decode.rs b/wacore/src/voip/mlow/smpl_decode.rs index db8c4bce4..3a9e48516 100644 --- a/wacore/src/voip/mlow/smpl_decode.rs +++ b/wacore/src/voip/mlow/smpl_decode.rs @@ -87,6 +87,7 @@ pub(crate) fn decode_smpl_lsf( st: &mut SmplLsfState, config: usize, intf: usize, + coded_as_active_voice: bool, ) -> SmplLsfIndices { let mut idx = SmplLsfIndices { stage1: 0, @@ -95,8 +96,13 @@ pub(crate) fn decode_smpl_lsf( extra: 0, }; - // Read 1: stage-1 selector. The first internal frame uses the dedicated row 0; later frames - // pick row 1/2 by the previous frame's stage-1 result. + // Read 1: the voicing symbol, whose value doubles as the stage-1 selector. The first internal + // frame uses the dedicated row 0; later frames pick row 1/2 by the previous frame's result. + // + // It is only on the wire when the frame was coded as active voice. A frame coded inactive + // (DTX off, so the encoder still sends background noise) carries no voicing symbol at all and + // is unvoiced by definition; reading one would consume a symbol that was never written and + // desync every field after it. let sel = if intf == 0 { 0 } else if st.prev_stage1 != 0 { @@ -104,7 +110,11 @@ pub(crate) fn decode_smpl_lsf( } else { 1 }; - let stage1 = dec.decode_cdf(&t.lsf_sel[sel]); + let stage1 = if coded_as_active_voice { + dec.decode_cdf(&t.lsf_sel[sel]) + } else { + 0 + }; idx.stage1 = stage1; // match := enter_match && stage1 == prev_stage1. enter_match is false for the first internal @@ -143,8 +153,13 @@ pub(crate) fn decode_smpl_lsf( idx.stage2[k] = dec.decode_cdf(c); } - // "Extra" LSF read: a 3-symbol static CDF, always fires for our path (p4=1, num_subfr>=2). - idx.extra = dec.decode_cdf(&t.lsf_extra); + // The LSF interpolation index, on the wire only for an active-voice frame with more than one + // subframe (the reference gates it on the same flag as the voicing symbol). + idx.extra = if coded_as_active_voice { + dec.decode_cdf(&t.lsf_extra) + } else { + 0 + }; log::trace!( "mlow LSF intf={intf} sel={sel} m={m}: stage1={stage1} grid={grid} extra={} stage2={:?}", @@ -172,7 +187,7 @@ mod tests { let frame = hex::decode(rec["frame"].as_str().unwrap()).unwrap(); let mut st = SmplLsfState::default(); let mut dec = RangeDecoder::new(&frame[1..]); - let idx = decode_smpl_lsf(&mut dec, t, &mut st, 0, 0); + let idx = decode_smpl_lsf(&mut dec, t, &mut st, 0, 0, true); assert_eq!(idx.stage1, rec["stage1"].as_i64().unwrap() as i32, "stage1"); assert_eq!(idx.grid, rec["grid"].as_i64().unwrap() as i32, "grid"); assert_eq!(idx.extra, rec["extra"].as_i64().unwrap() as i32, "extra"); diff --git a/wacore/src/voip/mlow/smpl_gains.rs b/wacore/src/voip/mlow/smpl_gains.rs index bf9316aa5..ac99fbe61 100644 --- a/wacore/src/voip/mlow/smpl_gains.rs +++ b/wacore/src/voip/mlow/smpl_gains.rs @@ -94,7 +94,7 @@ mod tests { let frame = hex::decode(rec["frame"].as_str().unwrap()).unwrap(); let mut st = SmplLsfState::default(); let mut dec = RangeDecoder::new(&frame[1..]); - let lsf = decode_smpl_lsf(&mut dec, tbl, &mut st, 0, 0); + let lsf = decode_smpl_lsf(&mut dec, tbl, &mut st, 0, 0, true); let pulses = decode_smpl_pulses(&mut dec, cc, 320, 4, 1, 0, lsf.stage1); let g = decode_smpl_gains(&mut dec, cc, 4, pulses.subfr); assert_eq!(g.gain_q.to_vec(), as_i32(&rec["gain_q"]), "gain_q"); diff --git a/wacore/src/voip/mlow/smpl_pitch.rs b/wacore/src/voip/mlow/smpl_pitch.rs index 0f65a3df6..06a56731c 100644 --- a/wacore/src/voip/mlow/smpl_pitch.rs +++ b/wacore/src/voip/mlow/smpl_pitch.rs @@ -248,7 +248,7 @@ mod tests { let frame = hex::decode(rec["frame"].as_str().unwrap()).unwrap(); let mut st = SmplLsfState::default(); let mut dec = RangeDecoder::new(&frame[1..]); - let lsf = decode_smpl_lsf(&mut dec, tbl, &mut st, 0, 0); + let lsf = decode_smpl_lsf(&mut dec, tbl, &mut st, 0, 0, true); let pulses = decode_smpl_pulses(&mut dec, cc, 320, 4, 1, 0, lsf.stage1); let pr = decode_smpl_pitch(&mut dec, mem, cc, &mut st, 320, 4, 0, pulses.subfr); diff --git a/wacore/src/voip/mlow/smpl_pulse.rs b/wacore/src/voip/mlow/smpl_pulse.rs index b73e8a720..bed9c8b1b 100644 --- a/wacore/src/voip/mlow/smpl_pulse.rs +++ b/wacore/src/voip/mlow/smpl_pulse.rs @@ -230,7 +230,7 @@ mod tests { let frame = hex::decode(rec["frame"].as_str().unwrap()).unwrap(); let mut st = SmplLsfState::default(); let mut dec = RangeDecoder::new(&frame[1..]); - let lsf = decode_smpl_lsf(&mut dec, tbl, &mut st, 0, 0); + let lsf = decode_smpl_lsf(&mut dec, tbl, &mut st, 0, 0, true); let pr = decode_smpl_pulses(&mut dec, cc, 320, 4, 1, 0, lsf.stage1); let want_subfr: Vec = rec["subfr"] diff --git a/wacore/src/voip/mlow/smpl_synth.rs b/wacore/src/voip/mlow/smpl_synth.rs index 68c790d7a..eae682b4a 100644 --- a/wacore/src/voip/mlow/smpl_synth.rs +++ b/wacore/src/voip/mlow/smpl_synth.rs @@ -539,7 +539,7 @@ pub(crate) fn synth_internal_frame( } /// Cross-frame decoder state (the persistent LSF/pitch predictor, prev NLSF, CELP synthesis). -#[derive(Default)] +#[derive(Default, Clone)] pub(crate) struct SmplDecoderState { pub(crate) lstate: super::smpl_decode::SmplLsfState, pub(crate) prev_nlsf: Vec, diff --git a/wacore/src/voip/mlow/testdata/PROVENANCE.md b/wacore/src/voip/mlow/testdata/PROVENANCE.md index abab1d64b..61f11cbdd 100644 --- a/wacore/src/voip/mlow/testdata/PROVENANCE.md +++ b/wacore/src/voip/mlow/testdata/PROVENANCE.md @@ -78,3 +78,43 @@ the reference decoder, one record per frame, compared byte-for-byte by the Rust exact wire bytes (config-1 `0x10` and config-2 `0x12` frames included). The tripwire test asserts the 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 — regenerable in one command + +| fixture | consumer / test | oracle recipe | +| --- | --- | --- | +| `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. + +The committed vector is an intentional 8-packet prefix, not the whole input: `synth_mic.raw` chunked +into 120 ms frames would yield ~55 packets, which is far more than the decode path needs and 7x the +bytes. A regeneration that produces more is the script defaulting to the whole file — pass the +packet count, as the script does. + +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. diff --git a/wacore/src/voip/mlow/testdata/mlow_120ms_frames.json b/wacore/src/voip/mlow/testdata/mlow_120ms_frames.json new file mode 100644 index 000000000..0440a4eaf --- /dev/null +++ b/wacore/src/voip/mlow/testdata/mlow_120ms_frames.json @@ -0,0 +1,10 @@ +[ + "58e5638cd7b84c934ad6200696fdd57ad59328d16487059c4ceba9a663aee2f2a8539717c9ad61003c481cd88b7d53671dd9d922348facd652c3412a29fe0b104781cdae50ffe628069d82b85803f440b7dfa3ba14723358977cdddd4ad455f18ca2f590f8ccbc3fc731094450bf79114129608b7ea4550f2adb8dfc6db9679f1d4374493bc49b13461de9d2e62c54fd8a204664292fd00f32d0645d62c1924df289ccb0871fa135700e2a12298b131b234cab25c595b4a7757377e0008c2810a99933060235181d44fb009fd4f95f382a5b009699b9ac8e8837df93f0b024112fee742f231607e222052834037b48e0b78ec5d33515adba720eabd68650c1f8003b1b738fa9f9eac138f5a663928a3a7204e3b69dbaff50bdcf247562b92658519f199866eba75a8043b0f187ca3a636703f99358e34d930e1a43f745428b61d6bafc3c4d0dfb3ce1570339530f2024458dbc", + "58e9e778031e4920b57f3e4ac91b28104458c23717a2ef45ec4ac8ed28db2d34a6b47eefc5d172817094ff5d778ad2bdfbafec70e255d97eb4456d454975b0edf24dd4b002e8e0728bd4dbab127c7f6cd22daef1628283c3c25d2a43eced73b9a2671ec0a4de3df7e22cabf11a631238a1b2862b7a1185538e8d50e5b1cdb58f16d50ab32230d4b5037474232b6fd472dc30d0ce219600d540e52596d0b55ca5cfe8db7788efdf79d011bd960a1c58400945681558a95a8ead4a4858aaf986f0b36629e1466bbc740d489d31f6649abe645e6f825e243c67ed0dec3cd23780a4cdbc9fc0e2f68fece39283edc7db440e2f99d87616005afaa574d962d4ea10c2d2c2bd58f96f2ca5d33beb6873aab03eaf1df5a3f7a53cbdd31bade5346d2d42da47535f54fd7ae87a7d5e0a4c5b0a937e0125bca0f9c434ae381b8ad497d80d31a22e3327285d5c32385fefebbdc6c954d3b3c8cd067db648e25ae33156b0", + "58e5638c50b876732ce74b53483134f0dc3201d9f32cd6083a3fcd5286aa31e8c67ccb0f6c49c4b373e1f6bbb176e8df81d415ee9bfa6d67cf3c45ae4518c17f4acb9a28d0dfdce23b6cf7506c83fa9a6db45c3a8ed2a6000772c2b4b6ab5940e88e91d1b672f9e30dfa4c0763f790c8b35ec71a0543d204662984151a3c2dbf19258913b1dbb7e275da6801646cdccd2bbc3f11d1a26ee0f33cc406c49fd89dc37e74d8dcaf3c38113fca17a956c4dc9033acdb8318992039c5e49809a4c3cc10073a678031f48318511e36ff545293fa4c230b0ababbe7582da6c7865711eef2123814af0143875246cc9d25a7bc8a0606c6294bf414af39633525cf036ee81ba9bfa3ffc029be007d18a69add72e93e8292a54b1237565654f36f0c2208e409394f8bd7caa9f24d0b6804b99529a4e65ea1dd8a748d7d7ccbdd0641e4f4ee3a81f416f0af6f7a5eedd63ea0f9df87c422ee9280", + "58e5ea1b94e67249c76a198f60f02bd0a603907fb0304bc529aa190f405157a84efadb7056df8b333542428c3a438bef0eca01720d99d0dc6767171713431498b88c5046ad57afb4f56bf34a1bac4e41ca70438e182cfb1d52e8d2644e904ea2b503fee76f89fc5f2f89c568f5ca7e94e0a2fccb0ae6b1d72668ff97c2572df5e907b4118f8f7159f5269e82661bf25974e642554818952a0f57a58e6f85a744ed5ff24a9d8d609227318fd5c66cb3263d7aebf12e0a2b095b3017db1d7e5733de7c03ca5db83d65a26d405bdcf93cc9e948191ebda1c8870c7086df294fe15dcaf621c8ca74327fcc7a1110af09f88233813df513c3becc94751cb0b482fd809747469e69a665311d664d1414ad965a5e1001b1467a1becb7e0272a78c11508c5eaff91d188445d0c84e55fe2d1f1f2f68d83cf932192c2ff06e2c5da3f5306c5bbab10bf030084e295d9a5", + "58f366c7f2ba215ad687e0b1658d46dd883f3df0e3f83bcd5764558b9dcf1befb26eb09d6d814a61885d7f9f557e995fa7b3fbe7066ecd3548ae1ad77aecedccb3e1be1e21dfdd982996ed5616c9bd4c7f0b4b72b6d0adfdbca9d0759c963464c343fface01b22adfbc7e1e3daf12e1ec014b15d935e070c76f5731b531fd6ffa4459ad4b8ab682a8610c3b97b5967b21574fc4df83497dfaf642b8bd65b6ccc908147d23492b14b7ef37f4e9a1ab96f37ce95abe3913051bea4dfac7cb7dc1ff92317e87ca8fda20eb834b360a52b869710713a52386446c62de4b86ce1920503b2e87ef9d149286eeecc63d04873d72a060269609d3289dc118e1dcca2d409f9ec2f54d4d9e9b654c52aec121f80ef078b9bb0dfddc7cb995fffff31de789d3ab5f129f1be09e58685a51bc827df7da8918f66dfbfc95b80", + "58e7c05da3f3be6edd6a0638812f9c07ebdbc0d01803ff38e14e33ad2c4866b123962f1c503e87b7f10c2013ea856bb05bf807a6f7ac0173f35af1012a481dc09c6efb3fe7a3ed04767bcc6fd3c776add1149c6e7327edafaa7b0d712dd901365560b46412eab157ec1a56270da774b8e90b7c71149b0948e5bedce296f35b7c3d7c2a2a80d184f247bdd5c9caa4ddfaba5172e3558920e7a511cbb5e75eafa2e1f11ed3d948a50a4506be8ccaa4494c598cef0ff4fad6d9ba7d9ad73adef931709e74c80c20e8c92adf37b7f847954482231351f5192ddb0ba8934ec92b09b45614dd0a10cbba61a0926697236a9f2de9a6d1b56ea8008a3956ffe82158ab584ada221a44ef80a2f4525683886ed63aaed48f", + "58ea36b1092866af49f88133faa16deebef1290ca3c54f70d35db431431a0b3cc9000a1cb9ecaf4df25e7ec8499afed9f57bd2d0e450c6bd2fb7baa569ac5c554a1e1a6d2d7557e4595d0857880af6fc8c193925ecf54f71977642fe82ceb17fcb467579df29426d380ac9d0d60621f7584c2126472ceaa853df196e85a51e851c188c6411dcd5a7e43cad20a4ce6d7636f32b7601fa2457844a0e3727a38f512d36b2b0198d8699b8c32a200c332fcd32781aa65632bd838d51d889d70e8b3b31034b72c5755210960657edaa8fe2ba82cf0f7a631befa598c96d3112857cabc46b73fac922927c8062431cf6847454861907577c03be9f1feffcf9cc45258702deb7eaf36e72f480", + "58f443673a7fb2b56193aab900f2e9cbc4d9131d193b1f1c0adc94fe1ac0c2c31168652218d73ddb6a066979d4d29acaf6e34b62efa24ef3c4adc69d8952daad209d22e8a9c0b95e281fe9f6cf06f1e11a4724446176e2e6b38152e72ab9de190d779b56b3abf5117eea6863ffea06a03397c3499b472104e3914c93bdc071f9eeea86120f9af23ff6e071358d223b59d1648a1470fa8bdf4e009bc1f3d601fdf85460c7d4f2c882f59c1902efe20fd9f2ccac88994f6cfebbbca4a958c6cadc967590fb6d20117265b929972c11fb9657a928896772200d8df0e727021cd9aed06baa060ce52f8d9f688c4b82226e685db9a5d10dbb0a8a2c7578432e393cf36dce1fe3ff572dc822efe0" +] \ No newline at end of file diff --git a/wacore/src/voip/mlow/testdata/mlow_dtx_off_frames.json b/wacore/src/voip/mlow/testdata/mlow_dtx_off_frames.json new file mode 100644 index 000000000..ebf24d3b1 --- /dev/null +++ b/wacore/src/voip/mlow/testdata/mlow_dtx_off_frames.json @@ -0,0 +1,112 @@ +[ + "50e5638cd7b84c934ad6200696fdd57ad59328d16487059c4ceba9a663aee2f2a8539717c9ad61003c481cd88b7d53671dd9d922348facd652c3412a29fe0b104781cdae50ffe628069d82b85803f440b7dfa3ba14723358977cdddd4ad455f18ca2f590f8ccbc3fc731094450bf791141188de1b468dba40ccbbc237691b0e9ba28c7f8b8a7bb33123f170de08ca27cdef0bdab26e36a432646ab8bcdedf0e368cd658b7c89229b", + "50e5ea1b95063cba6dc37c99cf062a4de197e4104843dfebaefb87c9d086dd120fd455bdcaec543576130ff01f81e469313f922c24f1bb41a2b5b746c715412113a0d05ea1c96c4f88ae2a690b0c441f0519c07eacb8a7ffa5e95df245cbcea5b98cc6ab0cee4e37abe45dfec39d6c2b4a726468a878995d7d6a6fd16a206891aebc8c9c8be9105d980ae149c8863e0dc0f11e24fac91692eccfa34b4ad3e66b72f3bd2499c52204de4021dcb0ad", + "50e9e778031e323073ed64258f42ae12c7a7e56983a845d434af5dee10a23dbbbfd3d21caf4d604f759f10bf6aa2b3e0efe7a0e8fec315a1a39902d516598f3fa648f2514a25a26af0214d7be13ab3c924bb48a446836967958a55041538f05e8fada05891166e2fe1ec7f2b383af55cb6f426dc478f36004419c2d4834962a20cfe8ba00ab4020cfbfca91ad1c3f76dd0006289f3dd61d8c3c235dd8c2417f5ba0637aa6dddb78119cabe896507c8", + "50ea2545f41f9beb23e2bd1591f617d46f6080876c9f1d4af60ba3ad2aae72ba1f6ad57e431386640f635eca6808243fb21ef05125f97c725016d4091638065f485cd6347c0a8ffa1a3751a98ed6d135e3026e3f4b5bfbca87c28092572918529422d7e005323f792d9bbeb75895641b4ce1560b1e747d04c5f97a5a93f41d81fe0b8c5cecc81d04cfded9b43b1f654fc876c90caf63cafa2eba4421a326dc377bc8a4de595e703c9723c49e", + "50e5638c50bb752418cef3cc05c6357b3a0ad86c79e5f5a9ef9ffb871b04eaf4cfbbd7abd16a65a87a625d4f87af770c039e1121c8cd9e9b41a54bc4fc822f98ab34f20e684b55f1702ae5e6dc725bb165fecaa787b076981a934b5c2c0fe86ce6d9a5f2d82c490cabeaaf811c4b57e9f59eefc9be64d97022a416dc73657297f7c26d39f04fe9ecc6e1cdacddfa7e4c59f13d99e97169b69b1a53962724dc174a4f75ca0bf2e24e91385a70b2", + "50e5638c50a91fa376fe4209e4a06a40107bee6fa90d29b30bfd5e8db89cef776962dae7c5db72874737f92cc82d7b07e6d94657f7a7e3fa970af46abd8fec8b2fbf552e48c6ab91c5123431768d94667b08f2af074c8466b97ca02db5b074fd3f351f09bdf2ad45cf7580270ef0afb0c9be125a967b66a0fa400c96896888a6f37fdc8b9f911242bacabffb2afea8b3e192bf20937ebe49bac0196deb6eb69881ae0d849a034d69294917e1a3", + "50e5ea1b94eb403df19200cba4edc8d690d5b704357bdc3f8ca8784261efe7b93e26efe590cad8fc9ce40260b2d69e18fd0fe8efa45ed499ce2252c3abc5b7f20211a8d328cc17bf007b589c8fdfc713078df59b1817e6ed0d9aa41a3a120c13ab490193d59871e0a3f6a9bc4db64c6357d36c048e6294f0ed608d8de2104001c55f8a104602fee437648fa37e1c0d5814869eb76dbb5bcac8f48a8e0eecb29e5300b15235a7cfc819e0", + "50e6f65847a23b0ffde426c6b416cbc8895958e585485d179be63cdb4facfe1b65867cf3bd6826d7024e983872e1f31266a5e209cf615e6f99f754e1f61bd02487f99352c6857791cfa15db6283258c2403744de393bdbae9958935ad2599dd31773e66bdb08085db0b0f95920f03b68e6704bc9a109f578b6aa60417e903f66c0adcae6dfeba683e4e37eaab680939b981936db6ffddbcfd1ee342038953a8fd635df9331b8e4ffbc", + "50f366c7f2b9f34cd84f82a78cd861dc862089762f0cc492b5ee87f9c79817b6b2b5220944cc446e6e04fb7d2e4216a61ba76234026243f5cbf8f506f1391395f98fb1a3fee25348ae6084e4abb96b1198cd3254c8f9d2d8c69c9fa36f938c19b4171ca27e965743e5cdb77262b4cca5633a3dc67083e363124ba402b49a54acef55178dc0f8e9da091096452eec108b2adf1fb3511e1db943bd6cac7fb5015bc885cd2904276fbca8", + "50e8acb786eeb8ab98116061d151fbe781e1a8ac0f57ff80fd35d9e9c790fb5d8a0539638e57be1d58d2f1d9653679a1eb526f9437dfb013135cf7a0b223f88569a40b5e54754751f5d447f5977235dc61fd3da4bc493117f053d521be247919ac682ccf50ab032c01c9aa28d699365e5fe68e2b2870c1daefdf78f9ed04c345d17a619504412900bc44dc9584a53e8c8f0e862e", + "50e7c05da3f3142ad6a712bd9f45ad4e2f7041a30edf500883c856e9ea81a028c9d55355356f0ebcf7ba17f0ba4be35507a6ebe21d2708f0cd2e76e38f819c4992e7ff72cbdc24f2861e1e12e9d7843c2a3e513ecb45edb7a4c44169f73fad6ef5389107d0ea7f02e1c75fd5028942b3ff0ec8b21c5d0d42f893dcbfc1f97c7eab3b8f1b3c8e474e1c134ab45080", + "50ea243d73486d55606573fbfda83246aa57525afa1df418af40816ec9d7fdb550acb8ad213d893bf5214f610e55fbc95da703726be568e4f3fa38f8d731a4aee3394a452eee235ef5299cec314186c69aca0928d54036b3eec7458baf8857d4df2990e760f208b703cfa87e3375d7fe9af3cce8f1cb9309ef968a4b946020ceee3b322fb740", + "50ea36b10925c934ebb04a3834c28b5d883e3a53e2b9a7a7b82166affb7e429fff24c10914cf0d55b4f2d9071bf51305eaf7b79d79184a56a213a91b1764c56eff554c0c11f1f1801e52a32825e24fc22755bf1692b878589ffbb866dbf823bbd734b2482e3662d018852b4d6bdbecbfacccd8ee90b42e81a64842ccd25ac3de288061e11314fd34", + "50f443673a7bae9131342baa07602d0dc4a6bf0265af5f2e5d79f05d968e56356366fdbf134b8d93b46f55caba2f52d9a5c24d9e07bd90de37662ca07f415b6baea6dd96436122635ed6dd1e37204eab46557332de80b8362d8149a549b2b0f7f8208e015cc15945c04fda9e486579ae8ab5712cdbb7679dfcd678cd57c26b080c7dd680e3", + "50f443673a80c1e7941c7ed072b0f47b1fa548a7ff9152c04e861dda7479b33017cbc65e8495a1f594fde9f5d5b9d884c0986e1825b8420a32bbabbbdb2ceeb52477546b5843baf954dd9e1a3a07a97a531d427ba11c551113a523a397dc4bae1f2edf7d76cda5f222e92e426b6c7915e423bf7edf99fb5ed1aed8a0c7fc47a827d3014b133660999a5bf220", + "50e09f279d4102a5ac9410e54ab2da6bbaf74b8069fe61a76a58aeb8fbd350846614c0cbe909d09f29893c1106379d6b94924e1aac23fb0a3e2622cd3ae107b47c270f303b4a933f9afe1259b2ea007be1bba6da56937c188d8652b1c109fe9880f44121c00ada83966013590c256e7c7e92ee76688ab7237cf30bda9f9e7bafd892f0081b70", + "12f51c412dadfbfe585cf2119da01bbf61dbaaa45a7b23c647e033840c9adbbe2734006a9f36e940738bda5f377c2d8921cfde1b4aee80e7935d82de886f85dc8e23fcf3bb4861cc2cb5353bb73d9b8b13b619d483bfeeeac233927ea497b83bdd265481ca8bcf487af2807abeaf022a5ad2110910edfcb5de7275d8a4dae0562c2927e984", + "1036390ec9bb3915a9f7fcca54153aa862721ad6c2a36eb245f1d4db32d4f11c6e39c42353a2d8565f6c7b4845b77422fa32c1866472cff7054687b0f1535025d137be4ce8a95bc341b1c98e6667ad3dc3195a3e9266fc9c7cd0bb2473ab318a9470ee80c7d74893ec2ce2f218ae7a27b0ecc452af87f70c80", + "10321c2884b8f7e6779a2b35d5360885d4affbb342a40b99b492c1c198f6ac886d909b129fea769c905394076fb1f71c67c4f8405fdbe02911a030b7ebce9a12fddc94d42b68d29864cc8f2099578997fed8a3fa83388a2a26eace36b60463b426e1f6a6faa6808d87ee934536c016da58139174c7097eb10a449920", + "104289b75986a1c2567d9090334c27408f165c0f137b87e320e5033cfc74d36e22d1511446c2c014c7267f817ee3a0fc92f8b233d3f62f10e7490006407af21f193babe423385e9cc316349c6693ddaeaea0320b31b77cdb931e358d163c10eafc6b3521f2c8984f47ec7767a7d89a21ec789f1d5903e03009c0", + "503625c383f2601eb7090254bc232a15b4da3644e9b826b977be56f3f0cfd37f6092c2a24393c2f1a161ca111e0701b8f7dffee1791dac1d6f31a4b5ea2b45c4f94c56ceac5f2b680d6ed2d2c0e04461d019323614a7681deecb0945a34a7aa71ded7cc4ec53700ada9b1ba0549ebefcab5145a7d62acc46140bd1c40b97bdc573b599233c539e43206f4c897fb83fd8ba2554bf06172307f6bf601b0c3f4fe69b68e8e6e340", + "5038b3a279df1eb2ea5494fea944774cdb1dbcf9cba9a23e1090e9036e8401e25fc16b95469f966e006d844cf0786ee3ca0ed1d46e782a7f2fd16dae8245fc964b28ee1c109e64178c0aa4124e7cf767c07b25cbb6fda4a5da5ccf149454fc5bdac00d3c60cbf01d6b870a01e820131f7be1ba25a9821168bea81bb9189322cfe280fa47255cc7d93fd5f7b4394d10742affa8f5a8849975f32fc683609410717a7a3fca49a4739993", + "503567058a490f509ac791bc4b76da77089dd79cd1f73267e68d139a786fa75d4ecf7e7567374a0aa125c63d14ce102823872690aee833b4cd3dd4153987369c9e7358dde4658638ad7ec6b5e070b9b6cdb85af51ec174ccdeaa33db30032380fac056d2ac28b7ebd3c506a7250ffc7053ba50ee166823992fd9aee79b89b2f39b1660d04fb0793e85293b5b42f9adc72fc56974f794f5a673e4366930c4eadc94", + "5049ae20d44857d455b750a28eb82f18fb65ccef2cb13a52338deb097a737eba2bb3f93395a91748d884d53b17d0e974dd99ad2ce39da53196490dbbccf81a2f3b7c09545ab55a43c1b0bbb4c0241585d7048c022d8a77ee19333c8116adf38dd1d67496f409b70d7e5eba09a53d2f84c3f83a9c651d5dcd74beb64b7c625b02ba3ba79fc6076942a083c145d82843ae44ebd2704eb44ae76b44372114b0c55fc5093149f7", + "503bc44171cca5e5aa9d51e74d12c1264937d35017550b26dd77e6d2685f638813ab98eef3c44c57093f9132f42deed8aa3a50311b660b4172f95e5ce1b6a418a21a74765c2e0158bfa1ca3e076587fab377e38ef57e41048a1b06cf67ae551d41a759903d380e48fc0918e6e76d6cbaeb25e18a91218a9f0817266e49b2961e4ad60e1db778506df2e096ca7b34159ab4bbc0bc8692bde3ec295a5fd85980", + "50400d1e01b0eae30712f593a175e19c16ed3cfe846dd6dfe3079781f31953927f461cbbe96379761506d2225839999a9f76bea857427cfd0f2f2dbeabb3a3a3151518be7f3b66cd5bdaad46430de2078ee998ed2f07cf2d30ecec99d450f87c5644f45f3c7cd92680d8331b091174119d1cba3d9e1128684746a04498ae9fdc8785d83cc4d33fc94d5159bca73dc6dd3b7da22b10aaa77c3b378322575afe39afceed48379b3870", + "50390a79c284cfb3ca1092d36c9d985a1bf8ad7cf4ef022a1b4cf1900482d9e4262474454f3a5903f573f6fc5711c364de208171ffbbecb3e0d93f14ea09aa51f346373c096df8e28d70b032cccdffec99680317be8ebf174b99ae31fd6f7cbd9517ac65a74175725e864bdf8ed8983dacb8ec5fd097fb83833d18d7dc0b3d8e942d9787553728b5f7df3c08623fd7d0b336c1b44bdbf10e216e518619313f7ce0", + "50384e83fc47eeaa48acb0488cc7721d8e606b1958d51aa423a91d009c05ec98d921066871e1b067f8d3f3f9adfa234c9f539574bc77aa373159b4b3b36d29733f1036e3fba2939d1aed8cae376d948417b636c510216d1ffe9d20cd4e5111ce1f8bceba346afba8b06ffad00d844b2cd84a0500a03e2c8369df85c52a10991c970d9204c44cd429517b7863b9ab23a6cfed9ae8bff6ab3107ba1abf9ac7fa5f268583c0", + "50426f834a8bccbb7a3cf07370b52b5dbed901979d72ba110bae288e9fb40ccb3dd052b99d88e0f2edb6dc1b5c1e318eda479775600893fd8bb518d62a4ea8822add836b71a37ecd2378914324c07bcb1becde595138736e48142051d78926287bc9ed47760f988e2aae859c6aabda366198d33ed7255990570259e9b33bddb9c6cf101f186483fba01737d251bda4df7a2e87bf737334fa78e286b09d04259aa1a4d28f9be8", + "5033b78aae1608633e525b915d559ea959503597eb492d7711cda8c9be4773ebb18c9b4461787380443cd401552e65f7b47b4dc3ada68196352e2274418dd53d78c558c984eb69b5be7184608008e416837391b01e7c977aaf513a816810d910f85c7d58a5684bb0c04f25f25cd277086af19c7c63eb63297755715315d4329c767d8dff6a0ebba969b73e42d9f3542ae787fb1621fd825fcadbcd42", + "50e5631492d223d634486153ab58a7a35fd8c8eb45daaab5d3c5ac2dab730ab30c91a5c664eb4c0f505d847ab1c59cf162ae7eb42194a1a5c373aa57688383b675ddec9c7953b1cfad7b0f0967b748288f44df6ea8508f5bc317346900cd7079f7f02a26dec93cbfa9613aa284a6654ccb06a7a5f9a1ec358b190fbec01edf66c9ef9bd5b8b48891c6946f049de917c9f27948c9b1ba2bb05916c75e4a", + "50e47d06cb074ceccbe1e544c75be0363c7ea5ed33f1b2805a9e7de2957c3a5281e7f4d003c3266c6e4baaa4babc0bf26c78b39fbebbda357d188ad8bd7291ad147baa996741e78abf68d893d8f87017b342156eb5e80ca1bbbd28f0f33d54c271ed4ebde771ac4373329c4aa112b29443b03ded477df1f12e41c23614335d117e404225b43f9e632dee389081f40c270e3798b37748180b15422754abda2a27570a9b0afc83bf5280", + "50e1bd1c900bc17f81ec7d52ad434ff5ecfe5308c07f9ca720f9d7c4ebd57c85fb880c3cb1e0e759eff1787ad94e191c78091983f89d3c6245cd04a73e070181b4e2324f68c035d079f7911004da8bf29294e03010e994a80fdd9f2d8ae81637df72b307a4b6504c3782529800cdbae1cf4669526e054d33d71faf8f0dc3ab13dd3dbecf8a35a708a58c09dd07b2f8970ec094beaa8251d04dbeede3b0b3afe7fe1fe07454330c44c27780", + "50dca0b8c956bcb556395283c1ec43eadfc452bf7b8a02f755f0b8033f7238b3ccec926a356c458c15bde83fd5f7ea4a53b47634ec32f87a048c3f904c56c4c25ba9d83fd965deee2cb966660c9178b6a93fbb481f4474056149a8d93da2fd8b40a64cbbf310d3e2ce420b39892a30969981fe37969e01999f29924015b4ade9f0c2039e992979bf940a34e3ef8e317a70fb3f11307fd13566d1cc4728a88b9d1e61c8c4b306cee280", + "50e7dcf13180a4905db294fe4f60484e5b4c311013f951c346911bbb39373e5d97487cb994f3d45e1cccf10edf99ff6a3617c445a3a74df2494f4573ef05b5d4f1cfc7ab63f5b7cbd3c715dd53b3b76440476b976fd4dd2ca5926675708534a4dae546de87b020187435f51dc3608b7e15da695addb94c4cd0f0f4e8afe4cd5167f7b462f68b7184d8944edf1065f1160eb2bdeb8c38cf450e644c32ad3f6b28761460fe8870f61590", + "50f4416ecfb7d3d62520553172be98795fbc8982cc6906af292054a0a60f7f1d84c917fbb5d98dc35d81c9ef0c3fc56e98c9011ab80b51e55f6cec013eb95adc9087a905c7a8eb51cd11ddf7224fd84487098bcba6a1afd0101b6b52ce2f8871d981a279025d67170c9cd99efd919dc50ad303a76ed162e01e96854fa921b4241c87ac7f8f98e44d41444254d40403515f55b530153aadbbdb93c5fe06", + "50e2118d99149d299fd4f65f07927c8629af999e568059eae1434fe43f5d9e915187256d4cd461ca4b00cd2c9a7d68fc54ee155cdb42fc1b0c4d172aa7bb8f2d6b1dd999315dfcded31aba46aa0d9883ad009620938cac2a8316a47c4f9e121bbb89bfeb531f133beb559880df9800e3e16f18691e5a627d665dd42fe3ce366a07b162ac975bf36d7c4d5d0fe27622b54f95b1cce84a54b7cca1a7e4800bee84", + "50e90281947932e95ff90f45beeccfd1b1e0b025d10935f06ca44a4bf25d2913ee4075d82871e9eea3f1b6b0210fd312cb4a565d710c7b856762ae323e174b77cc59b8eea81fc97efaf4ef20395bd465a77c12af27d49dd994abcbb7ad4bb3ff0d2819376eda0ac30f6911196f9fb1bdb0383de03bad22b330400b455031f58f8c5769218ddd446260fdd566b59e983fa78cab70e15e276c1566f4e005d05de3a53125c0", + "50ea668ca92914f8e08abf93acfad0ee84dfc5f8b92cb9737f8ea0966092373cbff885b1adc496f5b1f81f95776859966f5be55b66793c5281028e7be175374e01016339a3b5b53f06b6e4c5e7667c5feea542a162bd4551ef7f40aadb84a66ef19124ddfb7d240a578ae260a288f3a65d6ab145f3c876310f4d2f2a80062a32ff476ff7e994bdf3d6a14de017f6ca4e94a7351687c427d93a9313712bd7aba6ec5e", + "50f37412d4238b18a57187d0621553540fccb707351e049a706d74240fe9f0fdbf58e00665f4eaddc3b7075ede399d6c827aa682bd42f51ee324949e815d365248db718b8823b8d368593f042d5e544b78ac2a6b84cc7b1d9bcf8c3908d8c87941ba0a64884f1395198a83a5721f0afe8b70a8fd9cc8a52d9cbb4213bc82550024508b348e9b5bacdea4fc5412b941d5e42c915a492e31e7ef7f29", + "50a233029670449cd04d1a3bea92f2a6029386e966936190dcd0cd0f507517c8ccd9c5d56464ef2c959b9ab070d54fc44665331685f8b6b0c658a5b48cbb3836b0acbda547192402d0d80f18aadbe9ca9263d8e9be6f83b62ecb5ff73b1ff9446cbb01a02baf0371df4b8f0eeeff0a56ad9b8ae1d5789e9f4f95c5550a763a6045b2771da475e79ab3032b27aecbe67590fee658472d78a39ca5263a4ccb6c", + "50e359d41836f652fa5658122823e947076fed25305f3acb1690c39654be439b75e73853b665f8d041086c3e2b6d36da5690ce564389ceca820e4845dd6087f600521f3a1f013ea8508f78cb91f7f3a4077d54d7639e05b845bbcb706c960e51759b61ea008c2af82e29635bf8f9600c879850650f61444aefdf21f2b0f7b94d9bee4d577d47384ff74b9e289197d7e07e0b42f03a0016eb15ced2bca1a059d140", + "50a226860ea307c5da9f4b96f76dec5fc360825f9dfb4b9b36b7f90c80ad0862076088e821b776fe7f025deac73e36b5b7165d4a49dfe896c65d1f917da4585a67583ba68d7fd6813b46f7301e52b86a0738251675fc7a3f1e14a75b12f6ac9c4b6d92f53c7fb8e5850e66ab7545e6394aa9a6ed430dfa723ebc2eeff51ce415741c7f71955368b02145167aa3aafb94fdb89d268bd0d28370", + "50e47bec4d20c23b9ece39fbc0de9f2c9772f4027578c8f9bb0f28bb433ac6efebce809c83c044850a82d35d53e16af41e3bcd3b0db3e310eb631e34c251b33027bafad725e42d46226ab8c3199d8904bfb8f60b6134ca5e3402f065839f26e169d22248736bc16daa803e24a2c411f4ca47b3e699b5587886d30228102cf14cef3dfddf3504a8f6d4b51b3f7f493f2fb1783ae87cfd99ecd32a4855c8", + "50f4852e1f104c1936cdcce1daacb3bd763ddc5cde3bc2e4a73112e2703056bc60069f96191d1fe8ed6d80e974b5a6e34bcfb25ea19509b5120e3e31bb3d4916ab7190ec0f54e117ab0c605f7270efac1521e57fb02371b5c85f9e3cf3fbcabcbe7af66589a28b263447abac54a0e66b2567624c68087fe7893dba053bb24b64705fe21f9b25ee3df332b3afd7b8cac421c4649c119e1489236814fa7ba0", + "50f49279f0a4c7cadf518799bf6ad08f109d8b880366a4847abb3cd07cf89702b4e86a8f8a62104f5e394256ce18bc5b95085d3cbedc5533f13515e08462e4ca87e980e369cd8f3c9829d1cd3ed42a1b3aa8cd5d8fe93fd687af1d021a8eb09db3686686d90ac9db668ef31766ed280fa5d6c8c399fda48140c4b09cc297738ca0dd84dde1c8238c4c8889b32d394d3a68223355a45170de6330c4", + "50ea3a497745c7093d5fb9a833a095ac94ca3ad1186a4534dd33ce16fe6e8db3441ce83ade70fc1f7463d7e0c6a3f6d56321bb7cb36c3cb798c33bfb42a7b4cefd3f2b081b79ee8f8cc24eb943ecaf4207fa0d04bc4a5e0e7ebff384aa3b1f8f1f39766141a38db6bbb2ac0e43db1a7235373758e4244087b7425956db1c597217f5ddb880d121395d57025b430d632e78796654ee53c330", + "50f438f4da2b57742719c6f12fbb844a36ad077412f12c2877cda6d318b78d3162a2621cb3dcb1e0faedb27817528bcc4d37706aac3af622970cfb7df374a64406384bf5bdcfd523b99e7e90d79008eb99bc65d28fff1a08771cb3ee872b664958591807903beaa35e510f2dcfcf6493b018dd4924ce55be3b6c96773b9a018cd3b02557c8479ba6487a83f61a8b5ad5a080", + "50f458c01254390330bdf31132149c7b3e7595a965155f219d75334a77c788e3eb6fe0b5028376802c12ed36574807efdb765484b1c50849617f7908a4fc51dca7e8b77898dca6b93e7dc96160ee2368f8b5819109360c4664dee76b591f47d87714fa726aa9c4c719de07a871fef6017efda69e4c23157fce03f194aa1d707f8669237011e14522b7c234f4", + "50a2383b5fcb6dc0993f2d65585f7b261c5eb5fec55f370cb9449e2ce36baa50a1081dc3e94a8593e98e35696e36451034d069ea20ede98b25cff6bb2da97c6bffdd0af1aedfaca85ee8f3f34098fe5ed82333992fafb3650ebc05b7f0f03aacc0d58de2899cfae893a68a5f9e76a5439316bc8163f7fd1d1fa4212b9045a986335439275614ea14c0", + "50e488f81b474e96c7275a7bb89bc0a9628e0c31b4ba55c042776b9d53ec386db0b05ed9e8aaae26d013504f07b728c38bb1a6fd1ca266603c83c76d8194a13a732082da1ce1ff86a78cd4e5a94d92653c4b04eeff625ebfd2519292de64f44b8a977318229e1e935519e166309069d3a2a9a75baaa17109a8d85ffae45b085494b77fd440205a59ee6403afbc0a", + "50e77ee3794a279e5d168cae5a05cdff97823a1cdfb30579ef8b229fcc7fce3151f2dd777e48d728ee5f70e104ee62bc0d2cf02cf4d25a41ed9f58e3ba246424a41f6cf5b38210c6b680485a9eaf7914feaaa6bf2f4644364c9a552a0143fa6aa0909003ce9e1bfe759e67723638d7147e2da121a90fddf136002ee0afb32fef13216e5624c6ded8f38a13e31f335980", + "50ea018800f136ac3df907a9f4d9025cd22e2af9be0f0af19916c90f80253d78ed0631e3235e7b88cabb0f9ffea64a135b54b233fa3b0d4727fa88f1c6f989ec44b29dedaa00d807fdbcba98fe27993e9ecf5c7436dcbb5567b2d0b4c41d3c31ce7b47862f34c50e89c23dbcbf5a2e200608c77f6b7882d052cb2e2e7b86123fd7940b223cfc9bcfe3e2a5583310", + "50eab262968430350a4d4805aec4e7e4c129b2ed7430a0803190bbffc524ee31b81c92696607dad5ba8ef286c61768726eb471635848d7fe0739bd140e1d49eabc245f350789ed095248e27e898e38bef6b912d7f701753bd4f9a033f168dd8315512fde16cf232c834b027c37c815511643f88201937ded29524e7d2d1693a033e1d4b7d7ca8a0a88", + "50e2a71b03f47a87f11b0f6edd18a820e311ec17e2ed9466344891178c5befbc7ac8c413d4fe7ddec3fc358b851ad37245bd25f35374ae8ae0e934039eadd7d0fc17ad942cdcc5eea1e0e61aff713ed9c75e1444e8e7fa90521ae60324e5692d03158e531632f8f0421255e2d54f8260edccaf207f08372b656d423270765cf47240c756a3d398dcba", + "50e1bd4ce5d7104fbf00f88caf35aa74aea8c85b8a6310c835f0802d73ad5feb46b2aabbf0afd8c17325a419edfab0df2a611972b7f2a73b9a3fc7584c6dfc7e8d824af0b32c5fa9da4bd803c4c76886dfd85ddeb9dc5531f9587b0288782aabe4f15aaf960b819384dc6f5c4180c7cfb595f244004fef1a3a947b05cf750e101430d688", + "50e7518c3fa657608708345fa755579b5a0e486c79981f6b1acf4081fd85ffb1588f77f06e0aa490ba1e1ca110530410a454c1133f7680d60a182e7c8c49c45e981d392a3b77075d74dc029cf5cafd05525cfdaaf6cd51f6858bdd551001c65f21b7bd7f6dad42e90084dac9e6968ac8fe3d2cf6ec47f36750df4d3ff0b5e7ce6af44770", + "50e434eec36ad52845b4fce32afdd5e1cea2bcc9176f1e758546f7306d48c3f3e96629f272612ac669b08bf10e5bf91b85558ef12c0a58bac38b9d815ac26a74361a1909a838cc0eeaadcf20afb25839094fddcfe7f1165181e82a4d512ced6643d81eaa398f1699f9d828a306b6035a42e3f3dc859e43ac5be7096a990303a3d433", + "50f493d67591df7b607d4d9e6b0f24a06393927cac65eeb49b0e6650f87012d8154dab178985d1791573399cddd0464bf9dcdc595ef773601ef1f6d7efcb3ddf9b8351a85260d6b06f5d786136b0739ea8aaf27ac4f291d02e9276ff40051f9959894a92ee056f89bf5d78d7c3a0e35479a220ff2fbb4892164597dd18c2f861f378", + "12a253992c9c5c356cd0fb92477300bac71201aaa5a376bef74068d1ebac77523d6196a8945a9dd274502b49bf37ec2763fb34b7cc15c99331bbdd4f9748f7df9cc73109c0d6a53405d29783ba8c32166b28b7a610eb1a71e54ac9fc863d59cda5249fb7d708dc8b1468db0bb53e47f547ded144decc1740bb14d4c262aa2565b976b16a10", + "1042cbc8a33dbbe09e4579031b5f15e0b7b4f5f3db80c69bc2813948", + "1036dfa9e113e02b48dd9004b64ac6a8", + "1099ff776336aa3a361ad005be212788c370", + "1097ca3f4c3692ae6d27092552e2c558c0", + "106ee58f245aa24009fa701da28e4166946408", + "1005228260ff8b58cc41da353e62a9317a080064", + "10b81414c2f17d3abf63e5947811", + "10b81414c2f17d3abf63e5947811", + "10b81414c2f17d3abf63e5947811", + "10b81414c2f17d3abf63e5947811", + "50414b1064fb62ae3625a57d87ca80722d7a26432a0ea73e7de3c3822bdbf798ff98daa2a5b45837c4a48c98ebd0105ae08527b0483d69bf05cfa4e577cbe3d98e1af2249e4f52a0605c503f1f92f7fa2b0dc56f9f7e8e4ed2ea4cef50d4dfefac7a5e2531343aff857cf6c03bd40df4bfa1b1b8733ef476fb26a9a1fc03dcbcb441b7ab69acc91ba4454aced98e5f2b17bf09d0f890634a1b2d0a22d390ad5d51efb9d0", + "50357565e62fdf7bdd575f88237ff9970875f9045ce7e31feabe113bb1e9538653e0c9373fc26a92c2e02b56f29d49038728c619b5892151e2d91fafcaf800cbe60bb50ee22e74b17776b3aee73081ec9b300b9794b7c98d407bd8491e80360c989f2a673a70f22ba4fedd9e6585b3fb6809cd85c4624e15de234a288a6f2343a196e4dd48b46c1deec4182fbb7be0734fa2e188b3950b3361a2ea675b7f889b3a", + "50423f2b4c822cbf191352f75c44c2d9727152ee0b13bfb9e9303dbdb284ed4d308c1dc932e5fe65bf16f21e6676f96f9a2251ecd790f6a1e8c3b6a34000d87e3ecbcf2d0dae9cf97f112af66acc3b4703ee2b5e267119c556c416da2a2f2ac2d6d29abcb6689b349726eefbc0a28cd417ef1eab47400c77bb061422416acd15e373e1143e691312b133d3133635035087d015b6d98681d751f75280", + "5036774c9e04ba160c601b83c9bcbbccb86d69f243b0871c2474c42bccebe52330593890b43a33ead8d9c0cdb69ccad9b8c6f8a676008b126e843e22d1509639ad731eb4be7b71a23c1007dcc33d4fdef2fd918f65d4f2b72984622d35ca43d17949387f676b84f02252202dcdd53931e78c8ba194cea595b6b5e1e805393138befd1cceb7687099778002b8118ac9b859e4fe403d32d2059e3f403bd2fa094a98", + "50358249518761abafb620e88092e62fe27fb497060b6980e954073f55f32328e30e049747a9f1c4375cf49506d5c37ce01c31ccee94705cfdfcbab793ad1207084509507b0562923abf78caeefe17859411810275105826ce34cec24b03b48ff72a4ffd7def240a5c3a38cf1376c04733ee4f3e0c010bcac0f068023e5d8702d98a78f048732ddceae741ecf8b8bf3bd59add7ce40562a505ee4fb960", + "50246f850eae36907caaea48e82186d9b0e054a11dd91b85f35dbbd8e043fcab01abb4954ff327bb64f74ad4231d1eb768c0f1128ae1bf7805a223427115b20beb1ff044395936fece2040d0a5a216220f1d0f47797030c5923ce0de2115a6e39723217a127dca284855d2c89423b1aaa328ae3f308d7a37e72ef38510ae45e977224fcb89d7789191ea155e86da0b2e95961c228e50cebef898e971e42812c4", + "5038bfe1552075cdc76df2bbc979524885e96a52dbd69403a6eb1b1d416a208d9c4d90335b69e70a76bc2384667d929caf85e7ad50277c60b4ee4ee3161890e5ae0d83bd4df264aca8567865f4e52d93aae1d1d87aed6030b931e38a6bee75c782effdbdeb1ed56980130a9bcce75721721df6751eb6499ccc4cf9f9de12f4022d5ea4521a8c934d85fc3436e8ca7fcf4bcbe3e58efa219127f8270d80", + "5049507ee8bd7ddae79e17a3f90833a64b626ba77f8797c3340092e0e01ff504c783fdfdd06a6d48bdb09a4009f56cde74f6adf109b0cf1a6720b0db29b860b7e329dd719f4c8bd7b5bb30a0569a14c484f1e2ce93d549ec97064b2b787f1a15e6bae9dd9785d41c5b4bb539dcf319fa13ab925d7d4ebde1346e49b2cfc67cc135182829cdde35b9ebdb3db85d29d0a3aca4f73c3faaee283b9b90", + "504203d7e380aae3777cca737e8e4417847893fe3725533d65c9a5996aa633e6dd91ee7a189fe645143471a0c2f6253dee34616fbc584d7e4c3d76d45758d0f258a987e7baf5e041cff94baf8a7ffee5d6b9d3ecc6bc39e869d1f1a1840cd4812e5e7e6e880238a970f89139b62430aa0a86ed29d88af06802c2d6e7a012aed7e0c90f6c9faad4ce0640520b95b17979a2461c840f63728f7e520aea154a9a999a90", + "5023fdb70f5526edbb23f285f79c51c2106eff1c621fd1d3d843cd270a896a4cae82873fdc1e649dee841cbdbc24e41122af74f3776ed4f33213657063118f32473edfa98c4ae7844e6255f07218d52c432c3bf12ee582f6234de04b1b83c3bb65f6d26d73ba309e6e3fc6540a71be20d9e195308dcb7fce38f5f91e18b05275ffb94a22548a9cce5832e5a92487468a5a03f166887858ba458798f680fb2aa20ab8", + "50101e8ad8bc022679955dfcea4a9d3f0cb22c17fda152a208f789e7c1f58830fe2d0196ec2a009b314dff35bd21f197cddd85c2390cf7922b108c1c92ce5f9b764eccebec6753da0f233641bdc2f81051c196e00e36904e2d94ad8e8fa5161ef16d9a763024a1673d96cc4302a66d4a7aa3b9cdfbc2a9693848ca2b6b9dca63d45cb8a2d48e5f2a76d293122c2a98d89d6ecd132359e2c6ca2d2c52aca2", + "50f4418b27e23e9d90b5427f4af172b2cab84c565408f09221ad7a2b05a70df8c2861027b25b888bd0eb875725ed0103ccbfce2163e0b0dafa5f9a0f0fbe273d91a83f43772e60cb121b1653cfd1bae61b39dafd0218b1d4c253233274eaa2e38e8129094c4df260d8878c73135197b0a6999e440462b0f069d9589dc4f68695fa8aa44f5ae7deaca90492285d3130432144228f70c9fa9b1a85daa2bd1e4af5172d6d01617b6ec9531a80", + "50e5ea1f8a1a2a5a601f1b11c5f609545b032b7637a8e6d1e2330c98bc4fa6104218297bb252c312352406106d89cc26c06b64b5bb98b6afd0f04782bf81ff236413be9be7c1c9db603362a7cdd5325fb383db051a5cf43e3d01b30873daea8429bc79c98137f7b656d5f5fe36be54f21cb36a108fb5736ccd817a0ec7fe6161445ff2d426e1a62aa19199edfc706b8664a83050d54cdacd6b9537876b5a7fb5e73c823ab886", + "50e56715106d77d393b3d6d921314038594806ff00c5798628fd12091c663024becc29dc4ab6590de7838e24593b420c97bcf1d1238c839c44af5e10521b8fcbcbf055ce7411f2c2bd55151d2484cf0a5e9caf7d9be4b8b92de2c537269314fb9c3c16d451ef6af4a57a58ce1141c3ff68fad189eeb4deef3e8414ae6fcaa8c51d884ae6b0538b316d2f751c8dbd9f488b2b9433273917562d68e5f6dc499a87ec5d08a3ab3c80", + "50ea3a64df236c2207d0f0ad3f195b256ca1746ab564495c1a0a9a8a805b407eb1815c654cca95578859e887efd121ad55c06b648220e3a856cb4bae98739e79ff7ba28e5959636244bcd4d187d1b6de0a57f45b2658a765b183cebb81f4abb8ba47c9c008b10445eceb1da865eea43a455a6b19af60bb179872ef8d42f95ecae082e171587b3ff2a66a0f764bf323c40fbf2f78bb902f2dcb096b40ed54a4440bc36eaa061e3d79c7f820", + "50ea0004cf097b6f932fbc42e731c80d53c0c74bf446454a43e7d3ce6bcbeeea1477ea7369174113655ac61bf0b8aa9bc692e34d96529f86c28fd0fa0ffa37f3cf940a66ddd6a592a45a988beaf0ec5a0fcfa4ffe99637221a48093417d5dc0dcc3415a672e9b601327e445354e4d6624ec0b01c462cae768c7115278c35c72a5749d93cd56ce0cc7388788713ac7be2ca584b0d3b14bc10b964b453e980a9f93b311205b0", + "50e6f5903d20f0eadb2584e4fa8eeacd39352a59d51aaecaa52c51eec010123e17e8c8d965e305e4f04e95420e60dfc8e4efb942daf8c14526a0e6771898e1faacd25b86cf3091ccc1c185507bc0328ac905a25446e27994bae46eed609f55c5f0e72cba8ce896c32a872d0c1901887d560ad9853065090fdc4cf897383e58c2749ee5bada2166e3b4ee6eec9a74d824bfe026a68c1aefdf57ddfbbbc95fdc22d3beab5caa559cb224", + "50e5d4b443e77e65dd969d2149ad6abbcb0ab3f160c3683b5c63ec6efc4e5ef675e0447ac1220e0d975e2d13c9edb06ee67591329bb7196b58ab87d6c9d8fead81ff15dc30f1d76362c62826d51f8f6ab273284a0a7f0365dc9b24107c972c26ea667f21cb1201cbb143823a9110b96e9e5f4788b04c0e1717c2dccc3c4d01ac398fbcda148e57d8d933283ad34c21484bffe0e8e93187d56734a86f4d7167c29876c9623c6780", + "50ea0004cf03c1811d479e8325f16a3ea5b8b03ade301a211b3444798c3a4f7df1bbf8bf81d0c3db662fb2e24b63e9529efa7e7cfd69e873778399864a04b9284053a7ca45ce7d1201ec1ac7a9f56244da7418f1aae82bf77302b3a5566a73a59459da932ba1175b838e27487a4d04b306a61ea995cfed1acb0de797f3bd39d9d4d73d0010e9ccacd28f9fe1fccc643a6aba87c63040dfe4f21430716141441e97e80df7fb1e0fc0", + "50ea39d23b47603ab1e776fab2f0e14d9d05d2a9079e753b1dd4f350359406b0ba2ec847b2cbfa3de215516d1b0f3d8ed11c31a6e9112c7626a8d5bac35d2896365b48c3e38cd3fab48f71e92170e96cb0c508de4a3073c6ff0a1bcf5b5f58f4a22f789b39483f12717ea565dfeae82ac536ee8e0504c43adcf174f65c1766bffcd8f6a8992778d8fbd489f3973537bb7eebfbedf38c9cf0bbb0e4e64e661e706bca7f3d20", + "5036246f911845f56a45ff25e524ca77bc99d43006a1ba60923d1824a0396e2dbd251eca8e6f3716e67ac858928bc9303475b00eac79bea4e5815b4ba504c284f80c93ec7f6827f1df59639277d2fd06c5cd435ea2373de04694c1d90167d508454a66f73dfd19ee608f744ce232a1a59f537f6f7e2396c5808f8476835d9efef0", + "50361b5810c0b70829a055bd36da6aa9871d1b5a519436bfb8208aa61482386655f082bf6b2b6e7e85db427e8fea034833fee5c776c853e86047748fe6634ed92c4cd81a217265f5bb1c6031398ec0aaed2a0c6aff7096a778bc361aaec52a35f55eaf97431d954f95470c6ecec68631b4d3aab025c7d8298061f4b50fc2e8a8ce75a860c8743035f832b34e3ecbb85645691a", + "5036453d9f30882d21d58066dc5ca209e999ba65837da1dc4002cc57b805df48c3639e39a5db59dce2911963e32677bb5554c56c4a81251571dbf617729c6455745698dabad8c20eabd7951ccbdbfc4e27183c6b64dab9c7f287830f071381cac6828bdf2d5151a658bf364adbf6c65b049d0e05a195d53204e4197ce9ad79997f51b5d6300e52a115e513098ebfff68240a", + "5049ac684146dfbde95e6cf50d9e9f96d2200dd8b3f82057ebd527bde16f6aa4c2d573668ce37da39ec8495e9574aed1daae26e4d8757ca2eb02661b7293c738ac4c68257b226cd17b1c596c8c9fb58129a297dc10d0f9cd9015d153a0c1b022481ddf5a2f87cf57814820bcffdea20e6d0e85dcb5376dce01a0248a1f6e743db9ab00bdeb1e9381d792d7aed8e2e818eadee4cbf70552061f4c6aa455", + "502cb9e4d06c600c83cf9dfed25bf46c0a1607601634396459d983cab0fe1bb18d48cbb55fb77ebe44edf3539c67b8d9407dbde0ff80d907d6dc65909e59bb86fa6a45e62b074d0b53afeaa81829083e380f15be5673d612a149a2cfcdda647c5a4ee6ddb3d528dc8ec7f16872d994601fd3115b5a14f58631168d568efafc94824111606d6714626e666bef722527e7c0", + "5033c4729233d4b6d3b095a0b167deffa866e4c08067da5d02d59ce6428dac2a72a10f5560048f9f6302481e8341e816905d71f51ccf9dd6cedad0c37d3f5aab5db4e7b1377e6e4baee9a3de754c181aa1db96a53ec8951b6f32e28d0c4c1cc2a9cf2f125e808bd6038ecaa8507f2d06816c42b4008bdc2963a48529c887122f2d482c3f1b7f7a509788a38271e9532c", + "504a6c63c9fe55b3a467f253cccb6ace81b05cfbbcbfe670b61583fef9e24b8092ab744a60e5541fd947ca1e7b7883431e5c1f871d61670c7e05af965e8a33ecde8e2c942d1b528e49766fabce87cbdb9a9d06a5d5a1111702b9be3e879c04559c0ffa092f6f819a270289917fb9cd9e8611fa34a6779e2a7bd5713c772d7f019044c8acbd3580c28db402", + "50354675ffe73e5a6fb3e83e6e8beaef17e5ea24067658b91af9372b2abc62a06c57dbbc7d0e4b7ff9251995d7235dd6eff7aa0df7820b60d47c5f605d1ea424ac80a010aed8843862d334ab8d481c91ee96253233235e1dfdb5fcc2670b8450207f3200026172cc72daba1dd93db53481db357ef8a569abc18bfd95c06a96455edfd3372321dcb4fdbfb83459445c", + "5037222092ad31066e453825b1f04188b28ca023c053ff1e77fcf95f08b97c78066ee8e9ea54cb4d27dd75dd898cad0176a73f125f1bca8070cbe9ac17b98dcafa752d444279e2b911f2ed31df38dd5208b9388a84e930d0f04d7eed88bf879ca94d0c33c8f10ba58e2d21da30441655a257099ae79acd672c08fa053f860b0eecd5ce4ed61958c6e864a90f06b9db4f68", + "5025d8c04e057d9e16fca3ee330d748b3fa207d71aa5dcc8f428c3f227414a9097de0687ce7b496d1408ad35f67be17f71c4e8c2f033a91cc71c1620b92aa9eb1b2943d880162d04e2f60c57c266497a13b1be70ad6695e4712855c73f5dc15cb0d839b1e8afa086bebab8ee20a7fb8c95d6b535a1ed321432df1a8b0f7a9c9a965da27dabbbd660b755519e63dcabac514888", + "50189f4497204029c7ee11f22ab95d1ebe62ea264af56dfd57703bec1d43b99c10d618cdaac914e63ecf4168467118fbe0b0850790283c30f0ebdef535659b710409db6516154754aa06fbca6df33a1d0f9ac5a9d3d4932d229231fa4b0fcf3918d16edb6f155da4208262a4109095464d321ab841581b0af60f9992470079b1e8d9ce5a5bad5cc375c6b8ef396d595393742a2df63c0b4cebbea68e80bbf8009b18", + "50a2351d536578cf217e7203aa042c5887e487898174965b2d7b5813a7ecd040771fdb0e37b3ed97ab620dced9794c84aa3e8dccef4f3048e890bf9d3c00b456302522444d583735448be90452f71a480050daac1a25493adbd1e9176c4c133ae0e1349a5c7a74bee0e413233f9aa878dec91ddfd6a092f323c738a6eddb18e8458fe530ca6db6cfd65f9373524cb25b25099443a21ed7127ba310497be935a4da54c0", + "50e47d3d7548a5299b245c7ebc26becb180786232850738afe41ae91b3c73d3a11388a786bc32ca7403b88664120cb508cdcb80c8cae165b839984f9b0c391425ec8ba9a147f1b4498165f961c5bdece9707265767a3e9dfcb832d5e432dad4cde18abd95a72f34cb116032e9eab45f4facf6dfb2cc2abb21196403f817362188a379b5b9665c80760c545896ef24e08823011579e8dd14eab8a2b9a9ae067cdbcd326e6e0", + "50e69ddab0a322c883b6509500c62218d6313d5766faae97ff2a0ab23ef49d29fc7fbfed4257bc9edb928b1603660e7e66060ddf0aad2ade048494aa9e70b45fefec820d941156ef6e7c1206b15077ca1756547da64d8b5dd42c675cae98fc2ecd2e95090c12e3a33e30b017c5362d7a9fad6a512b8bc8ddb5cf6e1abd0be3d824eb4d4a8cf01e0d8ed68ba01e5cc6a78b52f68401a9a4b5626cf32c434cda94651674cab959864fcd280580", + "50e4890f98fb79b3810aec9a9744065234c665502abbeec17ad6476b0a2d06acee6003fa245384c5b1adca0effc9f4b47ebeb512037240d2ee2321f86b88297cecc2e611701744e153b28b65d69d8898aa29c6502d45d045a2728543974d983c6f5ff5dad05f3914127917291e8fb9cf7aad03dc9907d36d0d274e55c6161b4ad5ae3e36194732ccefdc954dc62811639c4ccc33e7693ac3c8494a05aaa4d5a0d1f8fbca2e3a6f1580", + "50e92c303675635bb0087bddd51aaa8ba4d547fab6dbe355cd6015159f8e2d44e2d65645593ed653dc32a7f816a215abb7e71304af7739a16f030e7efb799238cd4c48fa62fc21402ddd926b46ed9a599435416da694e6789599c2cb498b91949c460b69caab0dcfbdd1ce21ba84e1edaf1842a224e8af334df37e2914391499ca90a356a3cac6e38b6ee4f1984b44377dfbbdf0a1ce9611c3a40d8f0eb1b4d9ca118680d8a2a0", + "50e488f81babd590f5279cb2e676bb1c5bd7fe5a709cd2e35cc50ce50b13e57e454be93f86a256fdcd6366a9cbd47a5f739f74177d071f3c871195b2af32fb4822cdd8ad497c55429efc9620ea5e6b026e210674084ceef0459f1849cfd99d57abd549e159d91900136231155fb618e05146d9264d8e07a303ea787d7bf3c169527809a0511d9b05b2aebb6b96e0c9a19163ea523fbf5835bf90d36e07d4c884a27ad880", + "50e54877eab7f74f210feabf189ef2a84a22caac2ad5ae6d936b4607909ffe72d7836e15520d37cac732d61a5f61821eedd8cd0f012efe432e9e44be560c83222c897a36ec7d3cad158854207819844bf40b7311cefffb4857b890f5e3b444bc6e6292a53c040449747a5a3ababbe02297ba494270449279e1480c0f847f56026671010f54b98a47321f672e7c05921cc108a4ab165103304111df58c1efa9d35d62699c", + "50e54a237217ff17189287ce499a089c7958b60757dc1460c2e74ff253b543aff7c8e3a58c127801aff0df84e5a20ec983f33b48b17400c1453bb83f6018e5eb0d0ae7b5173a43f2be50662926824f2d1483ab0ef5b5757950450f0760ad9f58e28be0fddb7649074dcb02cb0204dcc50118b9b7e9978709a4630814b7e482b69719c98cbce010c90bff9086fdd168100f1829d5cf7c2c298c8a3ea195c0", + "50e489cebae0e17a8843f7452c444a56a0b27488ef3e202024c16a1a0b1bee384e0d1c44479ed7c74b3011d201280b5186ce48b95cfeb78b325fd34ec527c9fb0acf100ceac181d676272b735a922de21ca7b11d45d8e2359c3d7b007f82b77ae80fc971d590eece28e508d9246f3e8d7dc7a2a9796460ef295b611a5abc0fa1bed0d36ec0f9aa8d72d219f9be7177d385a8312bf8ad7d638eacef59b140" +] \ No newline at end of file diff --git a/wacore/src/voip/mlow/testdata/ref_120ms_expected.raw b/wacore/src/voip/mlow/testdata/ref_120ms_expected.raw new file mode 100644 index 000000000..b52908acb Binary files /dev/null and b/wacore/src/voip/mlow/testdata/ref_120ms_expected.raw differ diff --git a/wacore/src/voip/mlow/testdata/ref_dtx_off_expected.raw b/wacore/src/voip/mlow/testdata/ref_dtx_off_expected.raw new file mode 100644 index 000000000..173d00f31 Binary files /dev/null and b/wacore/src/voip/mlow/testdata/ref_dtx_off_expected.raw differ