#!/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-audio-conformance.sh - Audio conformance gate: every port must # sound alike. Runs AUDPAR (examples/audpar, a fixed timeline of tones, # voice and noise steps, sound effects, a module's end and tick rates, # logged as it plays) on one port, records what the port played, and grades # the recording against the audio contract with # tests/gate/audioGate.c: pitch, 2 dB attenuation steps, noise rate, # effect rates and pans, the module ending and saying so, tick counts, and # silence where there should be silence. # # How each port is recorded: # sdl3 SDL's disk audio driver, the mix exactly as it leaves the program # dos DOSBox through a PulseAudio null sink of its own (parec) # amiga FS-UAE (a bare A500) through the same # atarist Hatari in real time through the same; ste on an STe # x68000 MAME's -wavwrite, unthrottled, in the machine's own time # iigs the same, AUDPAR opened from the Finder by keys # dvx DVX in DOSBox Staging through a null sink, as dos # win16 Windows 3.11 in DOSBox through the same # win32 Wine through the same # web headless Chromium: a copy of every buffer the page plays # psp PPSSPPHeadless's --dump-audio, unthrottled, in the machine's # own time # haiku QEMU's wav audio driver: the VM's HD Audio card in the # machine's own time; haiku32 the same # beos none: BeOS R5's VM has no sound card, and with QEMU's ES1370 # or AC97 (R5's es137x and i801 drivers) R5's media server # hangs, AUDPAR with it # # PORT_STEREO lists the ports with stereo output, whose pans are graded. # DOS is stereo on DOSBox's own Sound Blaster, an SB16; AUDPAR_DOS_SBTYPE # runs it on another (sbpro2, sb2: the mono cards), graded in mono. Not # sbpro1: DOSBox makes its two OPL2s the halves of one OPL3, whose rhythm # mode is in the first half only, so its noise plays on the left alone. # Win16 mixes in mono; beos is listed for the Be backend it shares with # Haiku, whose stereo is graded there. The IIgs is graded in mono, as a # stock IIgs plays (its output sums every DOC channel): its stereo is a # card's, routing even DOC channels right and odd ones left, which the HAL # follows, and MAME 0.264's ES5503 does not record it so (a sound on # channel 0 alone comes out on the left, and one on both is 9.6 dB louder # on the left). # Checks a port's hardware cannot pass are graded SKIP, by name: the # Amiga's tone and voices each play on one Paula channel, which feeds one # side only, so none of them can be centred (its centred sound effects # take a channel each side and are graded). # A port whose chip cannot land every 2 dB step gets a level tolerance of # its own: the YM2149's sixteen levels are about 3.5 dB apart, so # the nearest one can be 1.75 dB off, and on the STe, whose voices start at # level 14 (the mix's level for them), 1.95: level 14 to 13 is 3.9 dB. # # Usage: scripts/verify-audio-conformance.sh PORT # Env: AUDPAR_KEEP set to keep the recording and the log in # build//audpar/ # AUDPAR_DOS_SBTYPE DOSBox's sbtype for the DOS run (default its # own, sb16) # Exit: 0 PASS, 1 FAIL, 2 cannot run set -uo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) source "$repo/scripts/lib/joeyEnv.sh" port=${1:-} label="verify-audio-conformance ($port)" grader=$(joeyTool audioGate) || exit 1 PORT_STEREO="amiga dos dvx ste sdl3 web win32 psp haiku haiku32 beos" DOS_SBTYPE=${AUDPAR_DOS_SBTYPE:-} if [ -n "$DOS_SBTYPE" ] && [ "$DOS_SBTYPE" != sb16 ]; then PORT_STEREO=${PORT_STEREO/ dos / } label="verify-audio-conformance ($port, $DOS_SBTYPE)" fi RUN_SECONDS=120 X68K_RUN_SECONDS=110 # emulated: Human68k's boot and AUDPAR's run IIGS_RUN_FRAMES=7800 # emulated, 60 a second: GS/OS, the Finder and AUDPAR HAIKU_BOOT_SECONDS=120 # a Haiku VM booting until its job runner starts RECORD_RATE=44100 QEMU_WAV_HEADER=44 # bytes: QEMU's wav audio driver's header, its sizes left 0 # Where Haiku's debug server reads what to do with a program that crashes. HAIKU_DEBUG_SETTINGS=/boot/home/config/settings/system/debug_server work=$(mktemp -d -t joeylib-audpar.XXXXXX) pids=() sinkModule="" # Set by a run function when the program went wrong where the grader does # not look (a crash as it ended): the recording is still graded, then the # gate fails. runFault="" cleanup() { local pid for pid in "${pids[@]}"; do kill "$pid" 2>/dev/null wait "$pid" 2>/dev/null done # The recorder too, when a run failed before pulseStop. if [ -n "${parecPid:-}" ]; then pulseStop fi if [ -n "$sinkModule" ]; then pulseUnloadSink "$sinkModule" fi # The web run's own sink (webSoundSink), should it have failed early. if [ -n "${WEB_SINK_MODULE:-}" ]; then pulseUnloadSink "$WEB_SINK_MODULE" fi if [ -n "${AUDPAR_KEEP:-}" ] && [ -n "$port" ]; then mkdir -p "$repo/build/$port/audpar" cp "$work"/out.wav "$work"/log.txt "$repo/build/$port/audpar/" 2>/dev/null echo "$label: kept the recording and the log in build/$port/audpar" >&2 fi rm -rf "$work" return 0 } trap cleanup EXIT fail() { echo "$label: FAIL - $1" exit 1 } cannot() { echo "$label: cannot run - $1" >&2 exit 2 } # Raw signed 16-bit stereo at RECORD_RATE -> out.wav. rawToWav() { "$grader" wav "$1" "$work/out.wav" "$RECORD_RATE" } xvfbStart() { local display=$1 Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp >/dev/null 2>&1 & pids+=($!) sleep 1 } pulseStart() { # shellcheck source=scripts/pulseRecordLib.sh . "$repo/scripts/pulseRecordLib.sh" pulseEnsureServer || cannot "no PulseAudio/PipeWire server to record from" sinkName="joeyAudpar$$" sinkModule=$(pulseCreateSink "$sinkName") || sinkModule="" [ -n "$sinkModule" ] || cannot "could not create a null sink to record from" # A stream of silence keeps the sink running: an emulator that stops # sending sound while it is quiet (DOSBox does) would otherwise leave # gaps the recording closes up, and the timeline would shrink. pacat --playback --raw --device="$sinkName" --format=s16le --rate="$RECORD_RATE" --channels=2 < /dev/zero > /dev/null 2>&1 & pids+=($!) # A short record latency: at the default, the last 1.8 s still in # parec's buffer were lost when it was stopped (measured). parec --latency-msec=20 --device="$sinkName.monitor" --format=s16le --rate="$RECORD_RATE" --channels=2 \ --file-format=wav "$work/out.wav" > "$work/parec.log" 2>&1 & parecPid=$! } # parec writes the WAV header lazily: it must exit before the file parses. pulseStop() { kill "$parecPid" 2>/dev/null wait "$parecPid" 2>/dev/null parecPid="" } # Wait for PID to end by itself, up to RUN_SECONDS. waitFor() { local pid=$1 local waited=0 while kill -0 "$pid" 2>/dev/null; do if [ "$waited" -ge "$RUN_SECONDS" ]; then fail "AUDPAR did not finish in $RUN_SECONDS s" fi sleep 1 waited=$((waited + 1)) done } runSdl3() { local dir=$repo/build/sdl3/probe local display=":$((90 + RANDOM % 9))" make -s -C "$repo" -f make/sdl3.mk program-AUDPAR > "$work/build.log" 2>&1 || fail "the build failed (make -f make/sdl3.mk program-AUDPAR)" rm -f "$dir/joeylog.txt" xvfbStart "$display" (cd "$dir" && 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="$RECORD_RATE" \ timeout "$RUN_SECONDS" ./AUDPAR > "$work/run.log" 2>&1) rawToWav "$work/out.raw" cp "$dir/joeylog.txt" "$work/log.txt" 2>/dev/null } runDos() { local dir=$repo/build/dos/probe local display=":$((90 + RANDOM % 9))" local dosPid joeyNeed DOSBOX dosbox make -s -C "$repo" -f make/dos.mk program-AUDPAR > "$work/build.log" 2>&1 || fail "the build failed (make -f make/dos.mk program-AUDPAR)" rm -f "$dir/JOEYLOG.TXT" xvfbStart "$display" pulseStart XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" PULSE_SINK="$sinkName" SDL_AUDIODRIVER=pulse \ "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" --conf "$repo/scripts/dosbox-386sx16.conf" ${DOS_SBTYPE:+--set "sbtype=$DOS_SBTYPE"} \ -c "MOUNT C $dir" -c "C:" -c "AUDPAR.EXE" -c "EXIT" > "$work/dosbox.log" 2>&1 & dosPid=$! pids+=("$dosPid") waitFor "$dosPid" pulseStop cp "$dir/JOEYLOG.TXT" "$work/log.txt" 2>/dev/null } # Wait until one of the LOG files holds AUDPAR's "done", up to RUN_SECONDS: # for emulators that keep running after the program ends. Prints the log. # Its callers take that through $(...), so a failure is reported on stderr # (a FAIL on stdout would be captured with the log name) and returns 1. waitDone() { local pid=$1 shift local waited=0 local log while true; do for log in "$@"; do if grep -qs 'AUDPAR [0-9]* done' "$log"; then sleep 1 echo "$log" return 0 fi done if [ "$waited" -ge "$RUN_SECONDS" ]; then fail "AUDPAR did not finish in $RUN_SECONDS s" >&2 fi kill -0 "$pid" 2>/dev/null || fail "the emulator exited before AUDPAR finished" >&2 sleep 1 waited=$((waited + 1)) done } runAmiga() { local dir=$repo/build/amiga/probe local display=":$((90 + RANDOM % 9))" local fsPid local log joeyNeed FSUAE fsuae source "$repo/scripts/lib/amigaGate.sh" amigaMachine make -s -C "$repo" -f make/amiga.mk program-AUDPAR > "$work/build.log" 2>&1 || fail "the build failed (make -f make/amiga.mk program-AUDPAR)" mkdir -p "$work/hd/s" cp -r "$dir/Audpar" "$dir/DATA" "$work/hd/" printf 'stack 32768\n:Audpar\n' > "$work/hd/s/startup-sequence" xvfbStart "$display" pulseStart # A bare A500, the port's floor, as the Amiga audio gate runs it. DISPLAY="$display" PULSE_SINK="$sinkName" ALSOFT_DRIVERS=pulse SDL_AUDIODRIVER=pulse \ "$FSUAE" "${AMIGA_MACHINE_ARGS[@]}" --hard_drive_0="$work/hd" --hard_drive_0_label=JOEYLIB \ --fullscreen=1 --initial_input_grab=1 --fade_out_duration=0 --fast_memory=0 --slow_memory=0 --stereo_separation=100 --floppy_drive_volume=0 \ > "$work/fsuae.log" 2>&1 & fsPid=$! pids+=("$fsPid") log=$(waitDone "$fsPid" "$work/hd/joeylog.txt") || exit 1 pulseStop cp "$log" "$work/log.txt" } # The ST (atarist) or the STe (ste) build on its machine, under Hatari in # real time (not fast-forwarded: the recording is its sound output). runAtari() { local build=$1 local machine=st local dir=$repo/build/$1/probe local hatariPid local log joeyNeed HATARI hatari joeyNeed EMUTOS_IMG emutos [ "$build" = ste ] && machine=ste make -s -C "$repo" -f "make/$build.mk" program-AUDPAR > "$work/build.log" 2>&1 || fail "the build failed (make -f make/$build.mk program-AUDPAR)" rm -f "$dir/joeylog.txt" "$dir/JOEYLOG.TXT" pulseStart SDL_VIDEODRIVER=dummy PULSE_SINK="$sinkName" SDL_AUDIODRIVER=pulse "$HATARI" \ --machine "$machine" --tos "$EMUTOS_IMG" --harddrive "$dir" --gemdos-drive C \ --drive-a off --drive-b off --sound "$RECORD_RATE" --memsize 4 \ --auto "C:\\AUDPAR.PRG" > "$work/hatari.log" 2>&1 & hatariPid=$! pids+=("$hatariPid") # GEMDOS writes the log in whichever case TOS gave the name. log=$(waitDone "$hatariPid" "$dir/joeylog.txt" "$dir/JOEYLOG.TXT") || exit 1 pulseStop cp "$log" "$work/log.txt" # Hatari ignores SIGTERM. kill -KILL "$hatariPid" 2>/dev/null } # A Lua script that ends MAME at emulated second $1. mameStopAt() { cat > "$work/stop.lua" <= $1 then manager.machine:exit() end end) LUA } # Under MAME, unthrottled: -wavwrite records the machine's sound in its own # time, so the recording and the program's clock agree exactly. The probe # and its log ride the second floppy (B:), as the X68000 golden gate does. runX68000() { local bin=$repo/build/x68000/probe/AUDPAR.X joeyNeed MAME mame joeyNeed X68K_DIR x68kFirmware source "$repo/scripts/lib/x68kDisk.sh" make -s -C "$repo" -f make/x68000.mk program-AUDPAR > "$work/build.log" 2>&1 || fail "the build failed (make -f make/x68000.mk program-AUDPAR)" x68kBootDisk "$work/boot.xdf" 'B:' 'B:\AUDPAR.X' || fail "could not make the boot disk" x68kDataDisk "$work/data.xdf" || fail "could not make the data disk" x68kXdf add "$work/data.xdf" "$bin" AUDPAR.X > /dev/null || fail "could not put AUDPAR on the data disk" x68kAddTree "$work/data.xdf" "$(dirname "$bin")/DATA" DATA || fail "could not put DATA on the data disk" mameStopAt "$X68K_RUN_SECONDS" (cd "$work" && SDL_AUDIODRIVER=dummy timeout -s KILL 900 "$MAME" x68000 -bios ipl10 -rompath "$X68K_DIR/roms" \ -flop1 boot.xdf -flop2 data.xdf -video none -sound none -nothrottle -skip_gameinfo \ -samplerate "$RECORD_RATE" -wavwrite out.wav -autoboot_script stop.lua < /dev/null > mame.log 2>&1) x68kXdf extract "$work/data.xdf" joeylog.txt "$work/log.txt" > /dev/null 2>&1 || fail "AUDPAR left no joeylog.txt on the disk" } # GS/OS boots from the 3.5 drive and the Finder opens AUDPAR from the # JOEYLIB disk by keys (J, Open Apple-O, its name, Open Apple-O), as the IIgs # audio gate opens its program; -wavwrite records, unthrottled. The log comes # off the disk with cadius. runIigs() { joeyNeed MAME mame joeyNeed GSOS_SYSTEM_DISK iigsFirmware joeyNeed CADIUS cadius make -s -C "$repo" -f make/iigs.mk program-AUDPAR > "$work/build.log" 2>&1 || fail "the build failed (make -f make/iigs.mk program-AUDPAR)" JOEY_DISK_EXAMPLES=AUDPAR BINDIR="$repo/build/iigs/probe/bin" JOEY_DISK_DATA="$repo/build/iigs/data" \ "$repo/scripts/make-iigs-disk.sh" "$work/joey.2mg" > "$work/disk.log" 2>&1 || fail "could not make the JOEYLIB disk" cp "$GSOS_SYSTEM_DISK" "$work/boot.po" cat > "$work/run.lua" <= steps[idx][1] do steps[idx][2]() idx = idx + 1 end end) LUA (cd "$work" && QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy timeout -s KILL 900 "$MAME" apple2gs \ -rompath "$IIGS_ROMPATH" -flop3 boot.po -flop4 joey.2mg -video none -sound none -nothrottle \ -samplerate "$RECORD_RATE" -wavwrite out.wav -autoboot_script run.lua < /dev/null > mame.log 2>&1) "$CADIUS" EXTRACTFILE "$work/joey.2mg" /JOEYLIB/JOEYLOG.TXT "$work/" > /dev/null 2>&1 tr -d '\r' < "$(find "$work" -maxdepth 1 -iname 'joeylog.txt*' | head -1)" > "$work/log.txt" 2>/dev/null || fail "AUDPAR left no JOEYLOG.TXT on the disk" } # A scratch DVX in DOSBox Staging with a Sound Blaster 16, AUDPAR its # desktop (scripts/lib/dvxGate.sh), recorded as the DVX audio gate records # it. DVX stays up when AUDPAR ends; the log is in AUDPAR's settings folder. runDvx() { local display=":$((90 + RANDOM % 9))" local dosPid local log joeyNeed DOSBOX dosbox joeyNeed DVX_ROOT dvx source "$repo/scripts/lib/dvxGate.sh" make -s -C "$repo" -f make/dvx.mk program-AUDPAR > "$work/build.log" 2>&1 || fail "the build failed (make -f make/dvx.mk program-AUDPAR)" dvxStage "$work/c" "$repo/build/dvx/probe/apps/$DVX_VENDOR/audpar" dvxDosboxConf "$work/dosbox.conf" "$work/c" sb16 xvfbStart "$display" pulseStart XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" PULSE_SINK="$sinkName" SDL_AUDIODRIVER=pulse \ "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" --conf "$work/dosbox.conf" > "$work/dosbox.log" 2>&1 & dosPid=$! pids+=("$dosPid") log=$(dvxAwaitLog "$work/c" audpar 'AUDPAR [0-9]* done' "$RUN_SECONDS" "$dosPid") || fail "AUDPAR did not finish in $RUN_SECONDS s" sleep 1 pulseStop cp "$log" "$work/log.txt" joeyStop "$dosPid" } # Windows 3.11 in DOSBox with Windows' own Sound Blaster driver, AUDPAR its # shell (scripts/lib/win16Gate.sh), recorded as the Win16 audio gate records # it. Windows, and DOSBox with it, ends when AUDPAR does; the log is beside # AUDPAR, on drive D:. runWin16() { local dir=$repo/build/win16/probe source "$repo/scripts/lib/win16Gate.sh" win16Need make -s -C "$repo" -f make/win16.mk program-AUDPAR > "$work/build.log" 2>&1 || fail "the build failed (make -f make/win16.mk program-AUDPAR)" rm -f "$dir/JOEYLOG.TXT" "$dir/joeylog.txt" win16Stage "$work" "$dir" AUDPAR.EXE pulseStart WIN16_SOUND=1 PULSE_SINK="$sinkName" SDL_AUDIODRIVER=pulse win16Start "$work" pids+=("$WIN16_PID" "$WIN16_GUARD" "$WIN16_XVFB_PID") waitFor "$WIN16_GUARD" pulseStop win16Stop cp "$(win16Log "$dir")" "$work/log.txt" 2>/dev/null } # Wine with a prefix and a display of its own (scripts/lib/wineGate.sh), # waveOut through Wine's PulseAudio driver, recorded as the Win32 audio gate # records it. Wine ends when AUDPAR does; the log is beside AUDPAR. runWin32() { local dir=$repo/build/win32/probe source "$repo/scripts/lib/wineGate.sh" wineNeed make -s -C "$repo" -f make/win32.mk program-AUDPAR > "$work/build.log" 2>&1 || fail "the build failed (make -f make/win32.mk program-AUDPAR)" rm -f "$dir/joeylog.txt" wineStart "$work" pids+=("$WINE_XVFB_PID") pulseStart PULSE_SINK="$sinkName" wineRun "$dir/AUDPAR.EXE" pids+=("$WINE_PID") waitFor "$WINE_PID" pulseStop wineStop cp "$dir/joeylog.txt" "$work/log.txt" 2>/dev/null } # Headless Chromium (scripts/lib/webGate.sh), whose driver keeps a copy of # every buffer the page plays, as the web audio gate records it, until the # log in the page's own file system says AUDPAR is done. runWeb() { source "$repo/scripts/lib/webGate.sh" webNeed make -s -C "$repo" -f make/web.mk program-AUDPAR > "$work/build.log" 2>&1 || fail "the build failed (make -f make/web.mk program-AUDPAR)" webServe "$repo/build/web/probe/AUDPAR" pids+=("$WEB_SERVER_PID") # Chromium plays to a null sink of its own, whose clock paces the page's # sound: muted, its audio runs on a fake clock that drifts off real time # (the timeline stretched by up to 15%). The page's own buffers are what # is recorded. webSoundSink || cannot "no PulseAudio/PipeWire null sink for Chromium to play to" webDrive AUDPAR.html --seconds "$RUN_SECONDS" --until " done" --audio "$work/out.wav" --log "$work/log.txt" > "$work/web.log" 2>&1 || fail "AUDPAR did not finish in $RUN_SECONDS s ($(tail -1 "$work/web.log"))" webStop } # PPSSPPHeadless (scripts/lib/pspGate.sh) and its --dump-audio, PPSSPP's own # recording on the emulated clock, as the PSP audio gate records it, with # AUDPAR run from its folder on a private memory stick. The run ends when # AUDPAR does; the log is in that folder. runPsp() { local dir=$repo/build/psp/probe/AUDPAR local folder source "$repo/scripts/lib/pspGate.sh" pspNeed make -s -C "$repo" -f make/psp.mk program-AUDPAR > "$work/build.log" 2>&1 || fail "the build failed (make -f make/psp.mk program-AUDPAR)" folder=$(pspStage "$work" "$dir") rm -f "$folder/joeylog.txt" pspRun "$work" "$folder" "$RUN_SECONDS" --dump-audio pids+=("$PSP_PID") pspWait pspStrictCheck "$work" "$dir" || fail "AUDPAR stopped at a bad memory access" if grep -q '^TIMEOUT' "$work/ppsspp.log"; then fail "AUDPAR did not finish in $RUN_SECONDS s" fi cp "$(find "$work/.ppsspp/PSP/AUDIO" -name '*.wav' -print -quit 2>/dev/null)" "$work/out.wav" 2>/dev/null cp "$folder/joeylog.txt" "$work/log.txt" 2>/dev/null } # A Haiku port (haiku, haiku32) in its VM under QEMU (scripts/lib/beGate.sh), # AUDPAR run by the job runner from the job disk. The VM's HD Audio card # plays to QEMU's wav audio driver, which records it in the machine's own # time from when the card first plays, but never fills in its header's # sizes: the samples are taken as they are. Haiku's debug server is told to # kill a program that crashes rather than ask what to do (its alert would # hold the job open); a crash still fails the gate. The machine powers off # when the job ends; the log comes back on the job disk. runHaiku() { local dir=$repo/build/$1/probe local exitStatus source "$repo/scripts/lib/beGate.sh" beNeed "$1" make -s -C "$repo" -f "make/$1.mk" program-AUDPAR > "$work/build.log" 2>&1 || fail "the build failed (make -f make/$1.mk program-AUDPAR)" mkdir -p "$work/job/AUDPAR" cp -r "$dir/AUDPAR" "$dir/DATA" "$work/job/AUDPAR/" { printf 'mkdir -p %s\n' "$HAIKU_DEBUG_SETTINGS" printf 'echo "default_action kill" > %s/settings\n' "$HAIKU_DEBUG_SETTINGS" beJobScript AUDPAR AUDPAR "" "$RUN_SECONDS" printf 'echo "AUDPAR exit status $?"\n' } > "$work/job/job.sh" beJobDisk "$work/job.img" "$work/job" || fail "could not make the job disk" BE_AUDIODEV="wav,id=beSound,path=$work/qemu.wav,out.frequency=$RECORD_RATE,out.channels=2,out.format=s16" beStart "$work" "$work/job.img" pids+=("$BE_PID") beWaitJob "$work" "$HAIKU_BOOT_SECONDS" || fail "the VM did not start AUDPAR in $HAIKU_BOOT_SECONDS s" beWaitExit "$work" "$RUN_SECONDS" || fail "AUDPAR did not finish in $RUN_SECONDS s" tail -c +$((QEMU_WAV_HEADER + 1)) "$work/qemu.wav" > "$work/out.raw" 2>/dev/null if [ -s "$work/out.raw" ]; then rawToWav "$work/out.raw" fi beJobGet "$work/job.img" AUDPAR/joeylog.txt "$work/log.txt" beJobGet "$work/job.img" job.log "$work/job.log" exitStatus=$(sed -n 's/^AUDPAR exit status //p' "$work/job.log" 2>/dev/null) if [ "$exitStatus" != 0 ]; then runFault="AUDPAR did not exit cleanly: exit status ${exitStatus:-unknown} (149 is a crash the debug server killed)" fi } case "$port" in sdl3) runSdl3 ;; iigs) runIigs ;; x68000) runX68000 ;; dos) runDos ;; amiga) runAmiga ;; atarist|ste) runAtari "$port" ;; dvx) runDvx ;; win16) runWin16 ;; win32) runWin32 ;; web) runWeb ;; psp) runPsp ;; haiku|haiku32) runHaiku "$port" ;; beos) cannot "BeOS R5's VM has no sound card to record (with QEMU's ES1370 or AC97, R5's media server hangs)" ;; *) cannot "no recorder for '$port' (sdl3, dos, amiga, atarist, ste, x68000, iigs, dvx, win16, win32, web, psp, haiku, haiku32)" ;; esac [ -s "$work/out.wav" ] || fail "nothing was recorded" [ -s "$work/log.txt" ] || fail "AUDPAR left no log" flags=() case " $PORT_STEREO " in *" $port "*) flags+=(--stereo) ;; esac case "$port" in atarist) flags+=(--level-tolerance 1.75) ;; ste) flags+=(--level-tolerance 1.95) ;; amiga) flags+=(--skip "tone centred" --skip "voice 0 centred" --skip "voice 1 centred" --skip "voice 2 centred") ;; esac "$grader" conformance "$port" "$work/log.txt" "$work/out.wav" "${flags[@]}" > "$work/grade.txt" status=$? grep -v '^PASS' "$work/grade.txt" summary=$(tail -1 "$work/grade.txt") if [ -n "$runFault" ]; then echo "$label: FAIL - $runFault ($summary)" exit 1 fi if [ "$status" -ne 0 ]; then echo "$label: FAIL ($summary)" exit 1 fi echo "$label: PASS ($summary: every tone, step, effect, the module's end and the ticks as the contract says)"