mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
235 lines
9.9 KiB
Python
235 lines
9.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate the embedded APU release-evidence assets for the apu_release app.
|
|
|
|
Reads the deterministic P5 APUMC microcode bundle, the frozen P9 APUC
|
|
coefficient image, the P7 APUM KWS model, and one locked KWS corpus utterance;
|
|
synthesizes a small deterministic
|
|
mono 48 kHz S16 WAV fixture; computes the frozen expected PCM through the
|
|
bit-accurate codec model; and emits a C header with every byte array and CRC
|
|
the LP/HP evidence flow needs. The generator is deterministic: no network, no
|
|
clock, and no randomness.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import struct
|
|
import zlib
|
|
from pathlib import Path
|
|
|
|
import sys
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
|
|
from apu_codecs import decode_wav, process_pcm # noqa: E402
|
|
from apu_kws import APUM_ABI, APUM_BYTES, APUM_MAGIC, APUM_PAYLOAD_CRC, APUM_SHA256 # noqa: E402
|
|
from apu_kws_coeff import APUC_PAYLOAD_CRC, validate_apuc # noqa: E402
|
|
|
|
APUMC_MAGIC = 0x41504D43
|
|
APUMC_ABI_VERSIONS = (0x00010000, 0x00020000)
|
|
APUMC_HEADER_BYTES = 64
|
|
APUMC_HEADER_CRC_WORD = 11
|
|
|
|
WAV_RATE = 48000
|
|
WAV_CHANNELS = 1
|
|
WAV_BITS = 16
|
|
WAV_FRAMES = 480
|
|
|
|
KWS_WINDOW_BYTES = 32000
|
|
KWS_EXPECTED_CLASS = 7 # "Stop" in the frozen class order
|
|
KWS_THRESHOLD = 1
|
|
KWS_DEBOUNCE = 1
|
|
# Locked corpus manifest row 000000 (docs/ip/apu-kws-corpus.tsv).
|
|
KWS_CORPUS_RELPATH = "stop/563aa4e6_nohash_3.wav"
|
|
KWS_WAV_SHA256 = "6ed51beca5b58fbbb06a16af68f120066218cf52c3fd4237f222a8ea9c972199"
|
|
KWS_PCM_SHA256 = "d843b234a0f585d143c88aed0acc1cca5ed02150f3cf5b77cb0489ad5429d725"
|
|
|
|
|
|
def _require(condition: bool, message: str) -> None:
|
|
if not condition:
|
|
raise ValueError(message)
|
|
|
|
|
|
def _crc32(data: bytes) -> int:
|
|
return zlib.crc32(data) & 0xFFFFFFFF
|
|
|
|
|
|
def _sha256(data: bytes) -> str:
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def _load_apumc(path: Path) -> tuple[bytes, int, int]:
|
|
image = path.read_bytes()
|
|
_require(len(image) >= APUMC_HEADER_BYTES, f"APUMC bundle too small: {path}")
|
|
magic, abi = struct.unpack_from("<II", image, 0)
|
|
_require(magic == APUMC_MAGIC, f"APUMC magic mismatch in {path}")
|
|
_require(abi in APUMC_ABI_VERSIONS, f"APUMC ABI 0x{abi:08x} is not supported")
|
|
header_crc = struct.unpack_from("<I", image, APUMC_HEADER_CRC_WORD * 4)[0]
|
|
payload_crc = _crc32(image[APUMC_HEADER_BYTES:])
|
|
_require(header_crc == payload_crc, f"APUMC header CRC disagrees with payload CRC in {path}")
|
|
return image, _crc32(image), payload_crc
|
|
|
|
|
|
def _load_apum(path: Path) -> tuple[bytes, int]:
|
|
image = path.read_bytes()
|
|
_require(len(image) == APUM_BYTES, f"APUM model is not {APUM_BYTES} bytes: {path}")
|
|
magic, abi = struct.unpack_from("<II", image, 0)
|
|
_require(magic == APUM_MAGIC, f"APUM magic mismatch in {path}")
|
|
_require(abi == APUM_ABI, f"APUM ABI 0x{abi:08x} mismatch in {path}")
|
|
payload_crc = struct.unpack_from("<I", image, 0x24)[0]
|
|
_require(payload_crc == APUM_PAYLOAD_CRC, f"APUM payload CRC mismatch in {path}")
|
|
_require(_sha256(image) == APUM_SHA256, f"APUM image SHA-256 mismatch in {path}")
|
|
return image, _crc32(image)
|
|
|
|
|
|
def _load_apuc(path: Path) -> tuple[bytes, int]:
|
|
image = path.read_bytes()
|
|
validate_apuc(image)
|
|
return image, _crc32(image)
|
|
|
|
|
|
def _synthesize_wav() -> bytes:
|
|
samples = [((index * 48271 + 12345) % 65536) - 32768 for index in range(WAV_FRAMES)]
|
|
payload = struct.pack(f"<{WAV_FRAMES}h", *samples)
|
|
alignment = WAV_CHANNELS * (WAV_BITS // 8)
|
|
fmt = struct.pack("<HHIIHH", 1, WAV_CHANNELS, WAV_RATE, WAV_RATE * alignment, alignment, WAV_BITS)
|
|
chunks = b"fmt " + struct.pack("<I", len(fmt)) + fmt
|
|
chunks += b"data" + struct.pack("<I", len(payload)) + payload
|
|
return b"RIFF" + struct.pack("<I", len(chunks) + 4) + b"WAVE" + chunks
|
|
|
|
|
|
def _expected_pcm(wav: bytes) -> tuple[int, int, int]:
|
|
audio = decode_wav(wav, strict=True)
|
|
processed = process_pcm(audio, output_bits=16)
|
|
_require(processed.rate == WAV_RATE, "fixture rate mismatch")
|
|
_require(processed.channels == WAV_CHANNELS, "fixture channel mismatch")
|
|
_require(processed.frames == WAV_FRAMES, "fixture frame mismatch")
|
|
_require(len(processed.payload) == WAV_FRAMES * 2, "fixture payload size mismatch")
|
|
info = audio.info
|
|
_require(info.input_used == len(wav), "fixture WAV is not fully consumed")
|
|
return _crc32(processed.payload), len(processed.payload), processed.frames
|
|
|
|
|
|
def _extract_mono16k_data(wav: bytes) -> bytes:
|
|
_require(len(wav) >= 12 and wav[0:4] == b"RIFF" and wav[8:12] == b"WAVE", "KWS source is not RIFF/WAVE")
|
|
fmt: bytes | None = None
|
|
data: bytes | None = None
|
|
offset = 12
|
|
while offset + 8 <= len(wav):
|
|
chunk_size = struct.unpack_from("<I", wav, offset + 4)[0]
|
|
body = wav[offset + 8 : offset + 8 + chunk_size]
|
|
_require(len(body) == chunk_size, "KWS source chunk is truncated")
|
|
if wav[offset : offset + 4] == b"fmt ":
|
|
fmt = body
|
|
elif wav[offset : offset + 4] == b"data":
|
|
data = body
|
|
offset += 8 + chunk_size + (chunk_size & 1)
|
|
_require(fmt is not None and len(fmt) >= 16 and data is not None, "KWS source lacks fmt/data")
|
|
audio_format, channels, rate, _byte_rate, _align, bits = struct.unpack_from("<HHIIHH", fmt)
|
|
_require(
|
|
(audio_format, channels, rate, bits) == (1, 1, 16000, 16),
|
|
"KWS source is not mono/16000/S16 PCM",
|
|
)
|
|
_require(len(data) <= KWS_WINDOW_BYTES and len(data) % 2 == 0, "KWS source exceeds one window")
|
|
return data
|
|
|
|
|
|
def _load_kws_pcm(corpus_root: Path) -> tuple[bytes, int]:
|
|
source = corpus_root / KWS_CORPUS_RELPATH
|
|
_require(source.is_file(), f"locked KWS corpus utterance missing: {source}")
|
|
wav = source.read_bytes()
|
|
_require(_sha256(wav) == KWS_WAV_SHA256, f"KWS source WAV SHA-256 mismatch: {source}")
|
|
padded = _extract_mono16k_data(wav)
|
|
padded = padded + bytes(KWS_WINDOW_BYTES - len(padded))
|
|
_require(_sha256(padded) == KWS_PCM_SHA256, f"KWS padded PCM SHA-256 mismatch: {source}")
|
|
return padded, _crc32(padded)
|
|
|
|
|
|
def _format_array(name: str, data: bytes) -> str:
|
|
lines = [f"static const uint8_t {name}[{len(data)}] __attribute__((aligned(64))) = {{"]
|
|
for offset in range(0, len(data), 16):
|
|
chunk = ",".join(f"0x{value:02X}" for value in data[offset : offset + 16])
|
|
lines.append(f" {chunk},")
|
|
lines.append("};")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _emit(args: argparse.Namespace) -> str:
|
|
apumc, apumc_crc, apumc_payload_crc = _load_apumc(args.apumc)
|
|
apuc, apuc_crc = _load_apuc(args.apuc)
|
|
apum, apum_crc = _load_apum(args.apum)
|
|
wav = _synthesize_wav()
|
|
pcm_crc, pcm_bytes, pcm_frames = _expected_pcm(wav)
|
|
kws_pcm, kws_crc = _load_kws_pcm(args.kws_corpus)
|
|
|
|
sections = [
|
|
"/* Generated by scripts/build_apu_release_assets.py; do not edit. */",
|
|
"#ifndef APU_RELEASE_ASSETS_H",
|
|
"#define APU_RELEASE_ASSETS_H",
|
|
"",
|
|
"#include <stdint.h>",
|
|
"",
|
|
f"#define RS_APU_RELEASE_APUMC_SIZE UINT32_C({len(apumc)})",
|
|
f"#define RS_APU_RELEASE_APUMC_CRC UINT32_C(0x{apumc_crc:08X})",
|
|
f"#define RS_APU_RELEASE_APUMC_PAYLOAD_CRC UINT32_C(0x{apumc_payload_crc:08X})",
|
|
f"#define RS_APU_RELEASE_APUC_SIZE UINT32_C({len(apuc)})",
|
|
f"#define RS_APU_RELEASE_APUC_CRC UINT32_C(0x{apuc_crc:08X})",
|
|
f"#define RS_APU_RELEASE_APUC_PAYLOAD_CRC UINT32_C(0x{APUC_PAYLOAD_CRC:08X})",
|
|
f"#define RS_APU_RELEASE_APUM_SIZE UINT32_C({len(apum)})",
|
|
f"#define RS_APU_RELEASE_APUM_CRC UINT32_C(0x{apum_crc:08X})",
|
|
f"#define RS_APU_RELEASE_APUM_PAYLOAD_CRC UINT32_C(0x{APUM_PAYLOAD_CRC:08X})",
|
|
f"#define RS_APU_RELEASE_WAV_SIZE UINT32_C({len(wav)})",
|
|
f"#define RS_APU_RELEASE_WAV_CRC UINT32_C(0x{_crc32(wav):08X})",
|
|
f"#define RS_APU_RELEASE_WAV_RATE UINT32_C({WAV_RATE})",
|
|
f"#define RS_APU_RELEASE_WAV_CHANNELS UINT32_C({WAV_CHANNELS})",
|
|
f"#define RS_APU_RELEASE_WAV_BITS UINT32_C({WAV_BITS})",
|
|
f"#define RS_APU_RELEASE_PCM_CRC UINT32_C(0x{pcm_crc:08X})",
|
|
f"#define RS_APU_RELEASE_PCM_BYTES UINT32_C({pcm_bytes})",
|
|
f"#define RS_APU_RELEASE_PCM_FRAMES UINT32_C({pcm_frames})",
|
|
f"#define RS_APU_RELEASE_KWS_SIZE UINT32_C({len(kws_pcm)})",
|
|
f"#define RS_APU_RELEASE_KWS_CRC UINT32_C(0x{kws_crc:08X})",
|
|
f"#define RS_APU_RELEASE_KWS_CLASS UINT32_C({KWS_EXPECTED_CLASS})",
|
|
f"#define RS_APU_RELEASE_KWS_THRESHOLD UINT32_C({KWS_THRESHOLD})",
|
|
f"#define RS_APU_RELEASE_KWS_DEBOUNCE UINT32_C({KWS_DEBOUNCE})",
|
|
"",
|
|
_format_array("rs_apu_release_apumc", apumc),
|
|
"",
|
|
_format_array("rs_apu_release_apuc", apuc),
|
|
"",
|
|
_format_array("rs_apu_release_apum", apum),
|
|
"",
|
|
_format_array("rs_apu_release_wav", wav),
|
|
"",
|
|
_format_array("rs_apu_release_kws_pcm", kws_pcm),
|
|
"",
|
|
"#endif",
|
|
"",
|
|
]
|
|
return "\n".join(sections)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--apumc", type=Path, required=True, help="P5 APUMC bundle path")
|
|
parser.add_argument("--apuc", type=Path, required=True, help="P9 APUC coefficient image path")
|
|
parser.add_argument("--apum", type=Path, required=True, help="P7 APUM KWS model path")
|
|
parser.add_argument(
|
|
"--kws-corpus",
|
|
type=Path,
|
|
required=True,
|
|
help="locked apu-kws-pcm corpus root (run the P7 reference setup first)",
|
|
)
|
|
parser.add_argument("--output", type=Path, required=True, help="generated C header path")
|
|
args = parser.parse_args()
|
|
content = _emit(args)
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(content, encoding="ascii", newline="\n")
|
|
print(f"apu_release assets: {args.output} ({len(content)} bytes)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|