joeylib2/scripts/check-sfxmix.sh

151 lines
5.6 KiB
Bash
Executable file

#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# check-sfxmix.sh - Host-cc differential harness for the shared SFX
# overlay mixer (src/core/audioSfxMix.c), built on the check-blank.sh /
# check-millis.sh pattern. Compiles the CURRENT mixer TU with the host
# cc plus tests/host/sfxMixHost.c and runs a fixed-seed scenario set
# (fixed-buffer + streaming slots, rate up/down, multi-slot, mid-mix
# refill and end-of-stream, amplitude 0/max, >64k read positions).
#
# First run captures tests/goldens/sfxmix-baseline.txt (capture it
# against the CURRENT mixer (the go-forward reference)); later runs diff per scenario:
# default exact mode: FNV-1a hash + raw bytes must match
# the baseline (gate for findings #7 and #33,
# which must be bit-identical).
# JL_SFX_TOLERANCE=1 per-sample mode for finding #6: prints each
# scenario's max-abs-delta and allows a deviation
# of tolerance * slots per output sample (the
# 1-LSB interp deviation stacks across
# simultaneous slots before the clamp).
#
# The harness binary is also run twice and the outputs compared, so a
# nondeterministic harness (or mixer reading uninitialized memory)
# fails loudly instead of producing a flaky baseline.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cc=${CC:-cc}
work=$(mktemp -d -t joeylib-sfxmix.XXXXXX)
trap 'rm -rf "$work"' EXIT
baseline="$repo/tests/goldens/sfxmix-baseline.txt"
cd "$repo"
# BLANK platform: audioSfxMix.c is portable C with no jlp* hooks; the
# stream fill callback is mocked inside sfxMixHost.c.
"$cc" -DJOEYLIB_PLATFORM_BLANK \
-Iinclude -Iinclude/joey -Isrc/core -Wall \
src/core/audioSfxMix.c tests/host/sfxMixHost.c \
-o "$work/sfxMixHost"
"$work/sfxMixHost" > "$work/out1.txt"
"$work/sfxMixHost" > "$work/out2.txt"
if ! cmp -s "$work/out1.txt" "$work/out2.txt"; then
echo "check-sfxmix: FAIL - harness output differs across two runs (nondeterministic)" >&2
exit 1
fi
if [ ! -f "$baseline" ]; then
mkdir -p "$(dirname "$baseline")"
cp "$work/out1.txt" "$baseline"
count=$(grep -c '^name=' "$baseline")
echo "check-sfxmix: baseline captured ($count scenarios) -> tests/goldens/sfxmix-baseline.txt"
exit 0
fi
tolerance="${JL_SFX_TOLERANCE:-}"
if [ -n "$tolerance" ]; then
echo "check-sfxmix: comparing against baseline (per-sample, tolerance ${tolerance} LSB * slots)"
else
echo "check-sfxmix: comparing against baseline (exact hash mode)"
fi
if awk -v tolerance="$tolerance" '
BEGIN {
for (i = 0; i < 256; i++) {
byteOf[sprintf("%02X", i)] = i
}
}
FNR == 1 {
name = ""
}
{
gsub(/^[ \t\r]+|[ \t\r]+$/, "")
}
/^name=/ {
delete fields
for (i = 1; i <= NF; i++) {
at = index($i, "=")
fields[substr($i, 1, at - 1)] = substr($i, at + 1)
}
name = fields["name"]
order[ARGIND, ++count[ARGIND]] = name
hash[ARGIND, name] = fields["hash"]
slots[ARGIND, name] = fields["slots"] + 0
raw[ARGIND, name] = ""
seen[ARGIND, name] = 1
}
/^raw=/ && name != "" {
raw[ARGIND, name] = toupper(substr($0, 5))
}
END {
same = count[1] == count[2]
for (i = 1; i <= count[1] && same; i++) {
same = order[1, i] == order[2, i]
}
if (!same) {
print "check-sfxmix: scenario list changed vs baseline"
line = " baseline:"
for (i = 1; i <= count[1]; i++) {
line = line " " order[1, i]
}
print (count[1] > 0 ? line : line " ")
line = " current: "
for (i = 1; i <= count[2]; i++) {
line = line " " order[2, i]
}
print (count[2] > 0 ? line : line " ")
failed = 1
}
for (i = 1; i <= count[1]; i++) {
name = order[1, i]
if (!((2, name) in seen)) {
continue
}
label = sprintf(" %-24s", name ":")
baseLen = length(raw[1, name]) / 2
curLen = length(raw[2, name]) / 2
if (baseLen != curLen) {
printf "%s FAIL (length %d -> %d)\n", label, baseLen, curLen
failed = 1
continue
}
if (tolerance == "") {
if (hash[1, name] == hash[2, name] && raw[1, name] == raw[2, name]) {
printf "%s PASS (hash %s)\n", label, hash[2, name]
} else {
printf "%s FAIL (hash %s -> %s)\n", label, hash[1, name], hash[2, name]
failed = 1
}
continue
}
maxDelta = 0
for (b = 1; b <= baseLen; b++) {
delta = byteOf[substr(raw[1, name], 2 * b - 1, 2)] - byteOf[substr(raw[2, name], 2 * b - 1, 2)]
delta = delta < 0 ? -delta : delta
maxDelta = delta > maxDelta ? delta : maxDelta
}
allowed = tolerance * slots[1, name]
printf "%s maxAbsDelta=%d (allowed %d) %s\n", label, maxDelta, allowed, (maxDelta <= allowed ? "PASS" : "FAIL")
failed = failed || maxDelta > allowed
}
exit failed ? 1 : 0
}' "$baseline" "$work/out1.txt"
then
echo "check-sfxmix: PASS"
else
echo "check-sfxmix: FAIL (mixer output diverged from tests/goldens/sfxmix-baseline.txt)" >&2
exit 1
fi