254 lines
10 KiB
Bash
Executable file
254 lines
10 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-x68000-audio.sh - X68000 audio gate, read off the chips.
|
|
#
|
|
# Boots a built example on a Human68k floppy pair under headless MAME and
|
|
# watches the two sound devices through memory taps, so the checks are on
|
|
# what the hardware was actually told, not on a log line:
|
|
#
|
|
# 1. The MSM6258's rate and channels, as IOCS sets them from the mode
|
|
# audioPcm.c asks for: PPI port C ($E9A005) bits 3,2 pick the divider
|
|
# (1024, 768, 512) and bits 1,0 gate the channels (0 both), and the
|
|
# YM2151's CT1 pin (register $1B bit 7) the clock (0 8 MHz, the chip's
|
|
# reset state; 1 4 MHz). The gate wants 7812 Hz, the rate the mixer
|
|
# renders at, on both channels. (A mode byte where IOCS wants a word
|
|
# -- this port's old 0x05 -- is 3906 Hz out of one side.)
|
|
# 2. Every YM2151 noise write ($E90000 register 0x0F). The low five
|
|
# bits must be OPM_NOISE_PERIODS - pitch for a pitch of 1 to
|
|
# NOISE_PITCH_MAX (0 plays as 1), both read out of
|
|
# src/x68000/audio.c rather than duplicated: that is the YM2149's
|
|
# 125000 / pitch Hz on the X68000's 4 MHz OPM, and a code outside it
|
|
# is the bug this gate exists for (a plain `31 - pitch` ran the
|
|
# generator an octave bright).
|
|
# 3. The ADPCM byte feed rate WHILE A SOUND IS PLAYING. Two samples per
|
|
# byte at 7812 Hz is 3906 bytes/s; half that means the divider is
|
|
# wrong again and every sample the port plays is an octave low. It
|
|
# is the rate inside one burst of feeding that counts, not the
|
|
# average: the port stops the chip between sounds, so an average
|
|
# over a mostly silent run would say nothing.
|
|
#
|
|
# The default subject is JoeyLib's audio probe, examples/audio built to
|
|
# press its own keys: it sweeps the noise generator through all 32 pitches,
|
|
# which reaches every noise code, plays its tracker song's
|
|
# noise drums, and keeps a phrase streaming through the ADPCM.
|
|
#
|
|
# Usage: scripts/verify-x68000-audio.sh [program] [seconds]
|
|
# program the built program: its path (a project's, say), or an
|
|
# example's name. Default build/x68000/probe/AUDIOPRB.X
|
|
# (make -f make/x68000.mk x68000-verify-audio builds it
|
|
# and runs this). Its DATA folder, if it has one beside
|
|
# it, goes on the floppy with it.
|
|
# Env: X68K_DIR dir holding HUMAN302.XDF + roms/ (default: the installer's toolchains/firmware/x68000)
|
|
# Requires: the program built, MAME with x68000.
|
|
set -uo pipefail
|
|
|
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
source "$repo/scripts/lib/joeyEnv.sh"
|
|
joeyNeed MAME mame
|
|
joeyNeed X68K_DIR x68kFirmware
|
|
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
|
|
source "$repo/scripts/lib/x68kDisk.sh"
|
|
bin=$(joeyProgramPath "${1:-$repo/build/x68000/probe/AUDIOPRB.X}" x68000) || exit 2
|
|
seconds=${2:-160}
|
|
PROG=$(basename "$bin" .X)
|
|
source=$repo/src/x68000/audio.c
|
|
|
|
[[ -f $bin ]] || { echo "verify-x68000-audio: $bin not built" >&2; exit 2; }
|
|
[[ -f $X68K_SYSTEM_DISK ]] || { echo "verify-x68000-audio: missing $X68K_SYSTEM_DISK" >&2; exit 2; }
|
|
|
|
# The noise codes, from the port's own constants so the two cannot drift
|
|
# apart: OPM_NOISE_PERIODS - pitch, pitch 1 .. NOISE_PITCH_MAX.
|
|
table=$(awk '
|
|
$1 == "#define" && $2 == "OPM_NOISE_PERIODS" && periods == "" { periods = $3 + 0 }
|
|
$1 == "#define" && $2 == "NOISE_PITCH_MAX" && pitches == "" { pitches = $3 + 0 }
|
|
END {
|
|
if (periods == "" || pitches == "") {
|
|
exit 1
|
|
}
|
|
for (p = 1; p <= pitches; p++) {
|
|
printf "%s%d", (p > 1 ? "," : ""), periods - p
|
|
}
|
|
print ""
|
|
}' "$source"
|
|
) || { echo "verify-x68000-audio: cannot read the noise constants from $source" >&2; exit 2; }
|
|
|
|
work=$(mktemp -d -t joey-x68audio.XXXXXX)
|
|
trap 'rm -rf "$work"' EXIT
|
|
|
|
x68kBootDisk "$work/boot.xdf" 'B:' "B:\\$PROG.X" || exit 1
|
|
x68kDataDisk "$work/data.xdf" || exit 1
|
|
x68kXdf add "$work/data.xdf" "$bin" "$PROG.X" >/dev/null || exit 1
|
|
# The program's DATA tree sits next to it: its own folder's for a program
|
|
# that ships in one (ID_FOLDER), the shared bin/DATA for the rest.
|
|
if [[ -d $(dirname "$bin")/DATA ]]; then
|
|
x68kAddTree "$work/data.xdf" "$(dirname "$bin")/DATA" DATA || exit 1
|
|
fi
|
|
|
|
# The taps keep to arithmetic and file writes: a tap that reads memory
|
|
# takes MAME down with it.
|
|
cat > "$work/tap.lua" <<LUA
|
|
local cpu = manager.machine.devices[":maincpu"]
|
|
local mem = cpu.spaces["program"]
|
|
local out = io.open("audio.txt", "w")
|
|
local sel = -1
|
|
local pcmWrites = 0
|
|
local burstCount = 0
|
|
local burstStart = 0
|
|
local burstLast = -1
|
|
local bestRate = 0
|
|
local stop = $seconds
|
|
-- A local tap object is garbage collected and stops firing. Keep globals.
|
|
opmTap = mem:install_write_tap(0xE90000, 0xE90003, "opm", function(offset, data, mask)
|
|
local v = data & 0xFF
|
|
if (mask & 0xFF) == 0 then v = (data >> 8) & 0xFF end
|
|
if offset < 0xE90002 then
|
|
sel = v
|
|
elseif sel == 0x0F then
|
|
out:write(string.format("NOISE %02X\\n", v))
|
|
elseif sel == 0x1B then
|
|
out:write(string.format("CT %02X\\n", v))
|
|
end
|
|
return data
|
|
end)
|
|
ppiTap = mem:install_write_tap(0xE9A004, 0xE9A005, "ppi", function(offset, data, mask)
|
|
local v = data & 0xFF
|
|
if (mask & 0xFF) == 0 then v = (data >> 8) & 0xFF end
|
|
out:write(string.format("PORTC %02X\\n", v))
|
|
return data
|
|
end)
|
|
-- The port stops the chip between sounds, so the honest figure is the rate
|
|
-- inside one burst of feeding, not an average across the silence. A gap of
|
|
-- more than 100 ms ends a burst; bursts shorter than 300 ms are too short to
|
|
-- time accurately and are skipped.
|
|
local function closeBurst()
|
|
if burstLast > burstStart + 0.3 and burstCount > 0 then
|
|
local r = burstCount / (burstLast - burstStart)
|
|
if r > bestRate then bestRate = r end
|
|
end
|
|
burstCount = 0
|
|
end
|
|
pcmTap = mem:install_write_tap(0xE92000, 0xE92003, "pcm", function(offset, data, mask)
|
|
local t = manager.machine.time:as_double()
|
|
pcmWrites = pcmWrites + 1
|
|
if burstLast < 0 or (t - burstLast) > 0.1 then
|
|
closeBurst()
|
|
burstStart = t
|
|
end
|
|
burstLast = t
|
|
burstCount = burstCount + 1
|
|
return data
|
|
end)
|
|
emu.register_frame_done(function()
|
|
local t = manager.machine.time:as_double()
|
|
if t >= stop then
|
|
closeBurst()
|
|
out:write(string.format("PCMTOTAL %d\\n", pcmWrites))
|
|
out:write(string.format("PCMRATE %.0f\\n", bestRate))
|
|
out:write("DONE\\n")
|
|
out:close()
|
|
manager.machine:exit()
|
|
end
|
|
end)
|
|
LUA
|
|
|
|
( cd "$work" && SDL_AUDIODRIVER=dummy timeout 1800 "$MAME" x68000 \
|
|
-bios ipl10 -rompath "$X68K_DIR/roms" \
|
|
-flop1 boot.xdf -flop2 data.xdf \
|
|
-video none -sound none -nothrottle -skip_gameinfo \
|
|
-autoboot_script tap.lua </dev/null >mame.log 2>&1 ) || true
|
|
|
|
[[ -f $work/audio.txt ]] || { echo "verify-x68000-audio: FAIL (MAME produced no tap output)" >&2; exit 1; }
|
|
|
|
awk -v table="$table" -v prog="$PROG" '
|
|
BEGIN {
|
|
tableCount = split(table, codes, ",")
|
|
for (i = 1; i <= tableCount; i++) {
|
|
allowed[codes[i] + 0] = 1
|
|
}
|
|
}
|
|
$1 == "NOISE" {
|
|
noise[++noiseCount] = strtonum("0x" $2)
|
|
}
|
|
$1 == "PORTC" {
|
|
portc[++portcCount] = strtonum("0x" $2)
|
|
}
|
|
$1 == "CT" {
|
|
ct[++ctCount] = strtonum("0x" $2)
|
|
}
|
|
$1 == "PCMRATE" {
|
|
rate = $2 + 0.0
|
|
rateSeen = 1
|
|
}
|
|
$1 == "DONE" {
|
|
done = 1
|
|
}
|
|
END {
|
|
if (!done) {
|
|
fails[++failCount] = "the run never reached its time limit"
|
|
}
|
|
# 1. The ADPCM rate and channels: the last values written win; the
|
|
# ROM'"'"'s own boot value of port C (0x0B) does not count as the port'"'"'s.
|
|
for (i = 1; i <= portcCount; i++) {
|
|
if (portc[i] != 11) {
|
|
ours[++oursCount] = portc[i]
|
|
}
|
|
}
|
|
if (oursCount == 0) {
|
|
fails[++failCount] = "PPI port C was never programmed, so the ADPCM never played"
|
|
} else {
|
|
got = and(ours[oursCount], 15)
|
|
clock = ctCount > 0 && and(ct[ctCount], 128) ? 4000000 : 8000000
|
|
split("1024 768 512 512", dividers, " ")
|
|
hz = int(clock / dividers[and(rshift(got, 2), 3) + 1])
|
|
if (hz != 7812 || and(got, 3)) {
|
|
fails[++failCount] = sprintf("port C low nibble 0x%02x with a %d MHz clock: %d Hz, channels %d; want 7812 Hz on both (0)", got, int(clock / 1000000), hz, and(got, 3))
|
|
}
|
|
}
|
|
# 2. Every enabled noise write must be one of the port'"'"'s codes.
|
|
for (i = 1; i <= noiseCount; i++) {
|
|
if (and(noise[i], 128) && !(and(noise[i], 31) in on)) {
|
|
on[and(noise[i], 31)] = 1
|
|
onCount++
|
|
}
|
|
}
|
|
if (onCount == 0) {
|
|
fails[++failCount] = "no noise writes seen at all"
|
|
}
|
|
stray = ""
|
|
for (code = 0; code < 32; code++) {
|
|
if ((code in on) && !(code in allowed)) {
|
|
stray = stray (stray == "" ? "" : ", ") code
|
|
}
|
|
}
|
|
if (stray != "") {
|
|
fails[++failCount] = "noise codes no pitch makes: [" stray "]"
|
|
}
|
|
unused = 0
|
|
for (code in allowed) {
|
|
if (!(code in on)) {
|
|
unused++
|
|
}
|
|
}
|
|
# 3. Byte feed rate: two samples per byte at 7812 Hz.
|
|
if (!rateSeen) {
|
|
fails[++failCount] = "the ADPCM channel was never fed"
|
|
} else if (!(rate >= 3500.0 && rate <= 4300.0)) {
|
|
fails[++failCount] = sprintf("ADPCM in-burst feed %.0f bytes/s, want ~3906 (%s)", rate, rate < 2600 ? "half rate, the divider is wrong" : "off rate")
|
|
}
|
|
portcText = "never set"
|
|
if (oursCount > 0) {
|
|
portcText = (oursCount > 1 ? sprintf("%02X/", ours[oursCount - 1]) : "") sprintf("%02X", ours[oursCount])
|
|
}
|
|
rateText = rateSeen ? sprintf("%.0f bytes/s in-burst", rate) : "silent"
|
|
printf "verify-x68000-audio: %s noise writes %d (%d distinct codes, %d of the port'"'"'s unused), port C %s, CT writes %d, ADPCM %s\n", prog, noiseCount, onCount, unused, portcText, ctCount, rateText
|
|
if (failCount > 0) {
|
|
for (i = 1; i <= failCount; i++) {
|
|
printf "verify-x68000-audio: FAIL (%s)\n", fails[i] > "/dev/stderr"
|
|
}
|
|
exit 1
|
|
}
|
|
print "verify-x68000-audio: PASS"
|
|
}' "$work/audio.txt"
|