zvidlib 0.5.0

Frame-accurate video and audio I/O for native and WebAssembly applications
Documentation
#!/usr/bin/env python3
"""Capture unit-test fixtures from the C reference encoder.

Writes crates/zvidlib-vorbis-encoder/src/vorbis_encoder/test_fixtures.rs (test-only data, @generated):

* HEADERS: identification + comment header bytes and setup-header length /
  FNV-1a-64 hash for a few configurations (build_ref.sh's ref_enc_posix);
* ENCODES: packet count, total bytes, last granule and FNV-1a-64 over all
  audio packets (u32 len LE, bytes, u64 granule LE) for a deterministic
  synthetic signal (integer LCG noise + triangle + bursts, exactly
  representable in f32; `synth()` below and in tests.rs must match);
* BITRATES: libvorbis' bitrate -> quality mapping (build_ref.sh's ref_bitrate).

Run tools/vorbis_encoder/build_ref.sh first, which builds the reference
encoders into target/vorbis-ref/. Usage: python3 tools/vorbis_encoder/make_fixtures.py
"""
import os
import struct
import subprocess

import numpy as np

# The repository root: this file is tools/vorbis_encoder/make_fixtures.py.
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
REF_DIR = os.path.join(ROOT, "target", "vorbis-ref")
REF = os.path.join(REF_DIR, "ref_enc_posix.exe")
OUT = os.path.join(REF_DIR, "fixtures")


def fnv64(data, h=0xCBF29CE484222325):
    for b in data:
        h ^= b
        h = (h * 0x100000001B3) & 0xFFFFFFFFFFFFFFFF
    return h


def synth(frames, channels, seed):
    """Must match `synth` in crates/zvidlib-vorbis-encoder/src/vorbis_encoder/tests.rs bit for bit."""
    out = np.zeros(frames * channels, dtype=np.float32)
    state = seed & 0xFFFFFFFF
    k = 0
    for i in range(frames):
        for c in range(channels):
            state = (state * 1664525 + 1013904223) & 0xFFFFFFFF
            noise = ((state >> 16) & 0x7FF) - 1024
            p = 97 + 13 * c
            ph = i % p
            tri = ph if ph < p // 2 else p - ph
            x = noise * 3 + tri * 300 - (p // 4) * 300
            if (i // 64) % 150 == 0:
                x *= 8
            out[k] = np.float32(x) / np.float32(32768.0)
            k += 1
    return out


def run_ref(rate, ch, q, pcm, chunk=1024):
    os.makedirs(OUT, exist_ok=True)
    fin = os.path.join(OUT, "in.f32")
    fout = os.path.join(OUT, "out.pkt")
    pcm.astype("<f4").tofile(fin)
    subprocess.run([REF, str(rate), str(ch), repr(q), fin, fout, str(chunk)], check=True,
                   stderr=subprocess.DEVNULL)
    b = open(fout, "rb").read()
    pos = 8
    pk = []
    while pos < len(b):
        n, g, _f = struct.unpack("<Iqb", b[pos:pos + 13])
        pk.append((b[pos + 13:pos + 13 + n], g))
        pos += 13 + n
    return pk


def rust_bytes(b):
    return "&[" + ", ".join("0x%02x" % x for x in b) + "]"


def main():
    lines = [
        "// @generated by tools/vorbis_encoder/make_fixtures.py from the C reference encoder",
        "// (libvorbis 1.3.7, tools/vorbis_encoder/build_ref.sh's ref_enc_posix and ref_bitrate). Test-only data.",
        "",
        "/// (rate, channels, quality, id header, comment header, setup len, setup FNV-1a-64)",
        "#[allow(clippy::type_complexity)]",
        "pub(super) const HEADERS: &[(u32, u16, f32, &[u8], &[u8], usize, u64)] = &[",
    ]
    silent = np.zeros(2048 * 2, dtype=np.float32)
    for rate, ch, q in [(48000, 2, 0.5), (44100, 1, -0.1), (44100, 2, 1.0), (22050, 2, 0.3),
                        (8000, 1, 0.0), (96000, 2, 0.7), (32000, 1, 0.45), (11025, 2, 0.9)]:
        pk = run_ref(rate, ch, q, silent[:2048 * ch])
        lines.append("    (%d, %d, %r, %s, %s, %d, 0x%016x)," % (
            rate, ch, q, rust_bytes(pk[0][0]), rust_bytes(pk[1][0]), len(pk[2][0]), fnv64(pk[2][0])))
    lines.append("];")
    lines.append("")
    lines.append("/// (rate, channels, quality, frames, seed, chunk, packets, bytes, last granule, FNV-1a-64)")
    lines.append("#[allow(clippy::type_complexity)]")
    lines.append("pub(super) const ENCODES: &[(u32, u16, f32, usize, u32, usize, usize, usize, u64, u64)] = &[")
    for rate, ch, q, frames, seed, chunk in [(44100, 2, 0.5, 30000, 1, 1024), (48000, 1, -0.1, 20000, 2, 512),
                                              (48000, 2, 1.0, 12000, 3, 4096), (22050, 1, 0.3, 9000, 4, 100),
                                              (44100, 2, 0.0, 1500, 5, 1024), (8000, 2, 0.6, 7000, 6, 333)]:
        pcm = synth(frames, ch, seed)
        pk = run_ref(rate, ch, q, pcm, chunk)
        h = 0xCBF29CE484222325
        nbytes = 0
        for data, g in pk[3:]:
            h = fnv64(struct.pack("<I", len(data)) + data + struct.pack("<Q", g & 0xFFFFFFFFFFFFFFFF), h)
            nbytes += len(data)
        lines.append("    (%d, %d, %r, %d, %d, %d, %d, %d, %d, 0x%016x)," % (
            rate, ch, q, frames, seed, chunk, len(pk) - 3, nbytes, pk[-1][1], h))
    lines.append("];")
    lines.append("")
    lines.append("/// (rate, channels, bits/s, quality) from libvorbis' vorbis_encode_setup_managed")
    lines.append("pub(super) const BITRATES: &[(u32, u16, u32, Option<f32>)] = &[")
    out = subprocess.run([os.path.join(REF_DIR, "ref_bitrate.exe")], check=True,
                         capture_output=True, text=True).stdout
    for ln in out.split("\n"):
        f = ln.split()
        if not f:
            continue
        if f[3] == "none":
            q = "None"
        else:
            v = np.float32(float(f[4]))
            q = "Some(%s_f32)" % np.format_float_positional(v, unique=True, trim="0")
        lines.append("    (%s, %s, %s, %s)," % (f[0], f[1], f[2], q))
    lines.append("];")
    path = os.path.join(
        ROOT, "crates", "zvidlib-vorbis-encoder", "src", "vorbis_encoder", "test_fixtures.rs"
    )
    with open(path, "w", newline="\n") as fh:
        fh.write("\n".join(lines) + "\n")
    print("wrote", path)


if __name__ == "__main__":
    main()