#!/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-sdl3-audio.sh - SDL3 audio gate, graded on the sound itself. # # The SDL3 port mixes everything in software (src/hosted/audio.c), so the # gate records the mix exactly as it leaves the program: SDL's disk audio # driver writes it to a file at the real-time rate (signed 16-bit stereo at # 44.1 kHz, pinned by SDL_AUDIO_FORMAT/CHANNELS/FREQUENCY), with no sound # server involved. tools/gradeAudioPhases.py grades it, as the Amiga and DOS # gates grade their recordings. # # The AUDIO example runs on a private Xvfb display (keys through XTEST, # HELD for 0.4 s: the demo tests jlKeyPressed, an edge predicate, once a # frame) through the DOS gate's four phases and two more: # # music the baked tracker song on entry (the square-wave voices), loud # paused P: near silence # sfx SPACE during the pause, so any sound is the digital SFX # resumed P again: loud again # noise P, then N: the noise voice swept through its 32 pitches alone # mod M: the .MOD through libxmp-lite # # The file starts when the program opens its audio, a fraction of a second # after launch, so each phase is graded inside its window with margin. # # Usage: scripts/verify-sdl3-audio.sh # Env: SDL3_AUDIO_KEEP set to keep the recording as build/sdl3/headless-audio.wav # Requires: Xvfb, xdotool, python3; build/sdl3/bin/AUDIO (make sdl3). set -uo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) source "$repo/scripts/lib/joeyEnv.sh" grader=$repo/tools/gradeAudioPhases.py label=verify-sdl3-audio program=$(joeyExampleProgram audio sdl3) [ -x "$program" ] || { echo "$label: build AUDIO first (make sdl3)" >&2; exit 2; } for tool in Xvfb xdotool python3; do command -v "$tool" >/dev/null || { echo "$label: $tool not installed" >&2; exit 2; } done work=$(mktemp -d -t joeylib-sdl3-audio.XXXXXX) displayNum=$((RANDOM % 500 + 300)) display=":$displayNum" cleanup() { joeyStop "${audioPid:-}" [ -n "${xvfbPid:-}" ] && kill "$xvfbPid" 2>/dev/null rm -rf "$work" return 0 } trap cleanup EXIT Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp >/dev/null 2>&1 & xvfbPid=$! sleep 1 DISPLAY="$display" SDL_VIDEO_DRIVER=x11 SDL_AUDIO_DRIVER=disk SDL_AUDIO_DISK_OUTPUT_FILE="$work/out.raw" \ SDL_AUDIO_FORMAT=S16LE SDL_AUDIO_CHANNELS=2 SDL_AUDIO_FREQUENCY=44100 \ "$program" > "$work/audio.log" 2>&1 & audioPid=$! sleep 1 win=$(joeyFindWindow "$display" "$audioPid") || true if [ -z "$win" ]; then echo "$label: FAIL - no AUDIO window to send keys to" >&2 exit 1 fi DISPLAY="$display" xdotool windowfocus --sync "$win" hold() { # $1 = key name DISPLAY="$display" xdotool keydown "$1" sleep 0.4 DISPLAY="$display" xdotool keyup "$1" } # The phase windows below are graded against these sleeps (seconds since # launch); keep them in step. sleep 5 # 0-6s music hold p # -> paused sleep 8 # 6-14s paused for _ in 1 2 3; do # 14-24s sfx into silence hold space sleep 1.5 done sleep 4 hold p # -> resumed sleep 5.6 # 24-30s resumed hold p # -> paused again sleep 0.6 hold n # 31-35s noise sweep alone sleep 4.6 hold m # 36-44s the MOD sleep 7.6 hold Escape sleep 1 [ -s "$work/out.raw" ] || { echo "$label: FAIL - recorded no audio at all" >&2; exit 1; } python3 - "$work/out.raw" "$work/out.wav" <<'PY' import sys import wave raw = open(sys.argv[1], 'rb').read() with wave.open(sys.argv[2], 'wb') as w: w.setnchannels(2) w.setsampwidth(2) w.setframerate(44100) w.writeframes(raw) PY # Thresholds sit at about half the measured levels (music ~3070, paused 0, # sfx ~1240, noise ~1630, mod ~7980; the grader averages the two channels) # so the verdict has room either way. python3 "$grader" "$label" "$work/out.wav" \ "music:1:5:1500:" \ "paused:8:13::100" \ "sfx:15:22:600:" \ "resumed:25:29:1500:" \ "noise:31.5:34:800:" \ "mod:37:43:4000:" status=$? if [ -n "${SDL3_AUDIO_KEEP:-}" ]; then cp "$work/out.wav" "$repo/build/sdl3/headless-audio.wav" fi exit $status