#!/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), # .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"