joeylib2/scripts/verify-atarist-audio.sh

202 lines
8.7 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
#
# verify-atarist-audio.sh - Atari ST audio gate, read off the YM2149.
#
# The ST has ONE sound chip and two software layers competing for it, so
# this gate traces every PSG register write and grades the competition
# rather than listening to the output. Four things are checked:
#
# 1. The chip-tracker is alive. jlMusicPlay drives the three PSG voices
# through reg 0-5 (12-bit tone period) and reg 8/9/A (4-bit volume),
# gated by reg 7. Writes must come from the APPLICATION, not TOS --
# the trace carries the pc of every write and TOS lives at $Exxxxx,
# which is the discriminator used throughout.
#
# 2. The digital pump runs at its REAL rate. Sampled sound on the ST is
# PWM through channel A's volume nibble: MFP Timer A at 2.4576 MHz /
# 200 = 12288 Hz, and src/atarist/audio.c writes reg 8 on every one
# of those ticks. Per frame that is 12288/60 = 205 (measured) or
# 12288/50 = 246, against the tracker's ONE. The two populations
# cannot be confused, so a per-frame count answers both "did the
# pump start" and "is the divider right" -- a halved divider would
# show ~103 and still sound like something.
#
# 3. Reg 8 has exactly ONE writer while the pump runs. This is the bug
# the gate exists for: the tracker's voice 0 and the PWM pump both
# own reg 8, and whoever wrote last wins, so a tracker that keeps
# writing it chops the sample into silence. The pc in the trace
# makes the writers countable. Each pump run's first and last frame
# are excluded: pumpStart and uninstallTimerA legitimately touch
# reg 8 from outside the ISR (the latter replays the latched voice).
#
# 4. They actually OVERLAP. "No collision" is worthless if it was
# bought by silencing the music, so the tracker must still be
# driving its own channel B inside the pump window.
#
# Subject: the AUDIO example, which starts the baked tracker song on
# entry and fires a digital SFX on SPACE -- precisely the overlap. Note
# SPACE is HELD (keydown, wait, keyup) rather than tapped: the demo tests
# jlKeyPressed, an edge predicate polled once a frame, and a make/break
# delivered between two polls is never seen. A bare `keypress` produces a
# run with the tracker alone and no pump at all.
#
# A program that stops its music while it plays a sample (as a game may
# for speech) is a poor subject here despite exercising both layers:
# check 4 can never pass on it.
#
# --machine ste runs the program's STe build (make ste, build/ste) on an
# STe, where src/atarist/audio.c plays digital sound through the 8-bit DMA
# sound chip instead of the PSG. Hatari's dmasound trace shows the chip
# being started and every sample it plays, so the STe checks are:
#
# 1. The tracker is alive (as above).
# 2. The DMA chip ran, as the HAL programs it: stereo at 12517 Hz, play
# + repeat.
# 3. It played the SFX: each run pulls a stretch of non-silent samples
# from the mix buffer, near half scale (the mix's unit for effects).
# A chip looping over silence (a mix that never reached the buffer,
# or unsigned samples left unconverted, which play as a DC step and
# not a wave) fails here.
# 4. No PWM pump: reg 8 is never written at the pump's rate. The YM is
# not a DAC on this machine.
# 5. Channel A is the tracker's while the chip plays: voice 0's period
# and volume registers (0, 1, 8) are written from the application
# inside the DMA runs. In the ST build voice 0 stands down for the
# pump, so this is the collision gone, not merely avoided.
#
# Usage: scripts/verify-atarist-audio.sh [--machine st|ste] [example]
# st (default): the ST build (build/atarist/bin) on a plain ST
# ste: the STe build (build/ste/bin) on an STe
# example PRG base name, default AUDIO
# Env: ST_AUDIO_TAPS how many held SPACE presses (default 6)
# ST_AUDIO_KEEP set to keep the work dir for inspection
# Requires: ./install.sh install emulator:atarist (Hatari, EmuTOS),
# <example>.PRG in that build's bin (make atarist, make ste)
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed HATARI hatari
joeyNeed EMUTOS_IMG emutos
stAudioGrade=$(joeyTool stAudioGrade) || exit 1
machine=st
if [ "${1:-}" = "--machine" ]; then
machine=${2:-}
shift 2
fi
case $machine in
st) build=atarist; traceFlags=psg_write; traceKeep='^ym write data' ;;
ste) build=ste; traceFlags=psg_write,dmasound; traceKeep='^ym write data|^DMA snd (control write|mode write|fifo pull)' ;;
*) echo "usage: scripts/verify-atarist-audio.sh [--machine st|ste] [example]" >&2; exit 2 ;;
esac
prog=${1:-AUDIO}
taps=${ST_AUDIO_TAPS:-6}
tos=$EMUTOS_IMG
binDir=$repo/build/$build/bin
# A program that ships in a folder of its own (ID_FOLDER) is found there;
# everything else is a bare PRG.
if [ -d "$binDir/$prog" ]; then
driveDir=$binDir/$prog
else
driveDir=$binDir
fi
[ -f "$driveDir/$prog.PRG" ] || { echo "verify-atarist-audio: missing $driveDir/$prog.PRG (run 'make $build')" >&2; exit 2; }
[ -f "$tos" ] || { echo "verify-atarist-audio: missing $tos" >&2; exit 2; }
work=$(mktemp -d -t joeylib-staudio.XXXXXX)
hatariPid=""
cleanup() {
# SIGKILL, by PID. Three things are deliberate here:
# * SIGKILL, because Hatari IGNORES SIGTERM -- measured: a plain
# kill, and `pkill -x hatari`, both leave it running forever.
# * by PID, not by name, so a Hatari the user has open survives.
# * no `timeout -s KILL` wrapper, because that would make $! the
# timeout and killing THAT orphans Hatari. The trap is the only
# thing that needs to work, and it covers EXIT/INT/TERM.
# The launch uses process substitution so $! is Hatari itself rather
# than the filter at the end of a pipe.
[ -n "$hatariPid" ] && { kill -KILL "$hatariPid"; wait "$hatariPid"; } 2>/dev/null
[ -n "${ST_AUDIO_KEEP:-}" ] || rm -rf "$work"
return 0
}
trap cleanup EXIT INT TERM
[ -n "${ST_AUDIO_KEEP:-}" ] && echo "verify-atarist-audio: work dir $work" >&2
# 4 MB, not the stock 1 MB: a big program's loader may call 1 MB a fatal
# asset failure, and this gate is about sound, not about memory.
#
# The psg_write trace runs ~800 KB/s, so it is filtered as it is produced
# -- keeping the raw stream would put hundreds of MB in the work dir. The
# dmasound trace keeps only the chip's control and mode writes and the
# samples it plays (its FIFO refills would double the volume for nothing).
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy "$HATARI" \
--machine "$machine" \
--tos "$tos" \
--harddrive "$driveDir" \
--gemdos-drive C \
--drive-a off --drive-b off \
--sound 44100 \
--memsize 4 \
--fast-forward on \
--cmd-fifo "$work/cmd.fifo" \
--trace "$traceFlags" \
--auto "C:\\$prog.PRG" \
> >(grep --line-buffered -E "$traceKeep" > "$work/ym.log") 2>&1 &
hatariPid=$!
# Hatari CREATES the fifo; a write to one with no reader blocks forever,
# so every command goes through a timeout.
for _ in $(seq 1 25); do
[ -p "$work/cmd.fifo" ] && break
sleep 1
done
if [ ! -p "$work/cmd.fifo" ]; then
echo "verify-atarist-audio: FAIL - hatari never created the command fifo" >&2
exit 1
fi
send() { timeout 5 bash -c "echo \"\$1\" > \"$work/cmd.fifo\"" _ "$1"; }
# Wait for the APPLICATION to write the PSG. TOS writes it too (bell,
# floppy seek), and every TOS pc starts with 'e'.
waited=0
until grep -qE 'pc=[^e]' "$work/ym.log" 2>/dev/null; do
sleep 2
waited=$((waited + 2))
if [ "$waited" -ge 40 ]; then
echo "verify-atarist-audio: FAIL - $prog never wrote the PSG in ${waited}s (the tracker never started)" >&2
exit 1
fi
if ! kill -0 "$hatariPid" 2>/dev/null; then
echo "verify-atarist-audio: FAIL - Hatari exited early" >&2
exit 1
fi
done
# Let the tracker settle, then snapshot a pump-free baseline.
sleep 8
cp "$work/ym.log" "$work/baseline.log"
# HOLD space so the once-a-frame jlKeyPressed poll can see the edge.
i=0
while [ "$i" -lt "$taps" ]; do
send 'hatari-event keydown 57' || true # $39 = SPACE
sleep 1
send 'hatari-event keyup 57' || true
sleep 1
i=$((i + 1))
done
sleep 3
# The braces + wait suppress bash's "Killed" job notice on stderr, which
# would otherwise look like a gate error in the output.
{ kill -KILL "$hatariPid"; wait "$hatariPid"; } 2>/dev/null
hatariPid=""
sleep 1
[ -s "$work/ym.log" ] || { echo "verify-atarist-audio: FAIL - empty PSG trace" >&2; exit 1; }
"$stAudioGrade" "$work/ym.log" "$work/baseline.log" "$prog" "$taps" "$machine"