Play reads a tune from a file
The loader, and the reason it is small: everything in a tune is an offset from wherever it was put, so taking one means adding the base to two tables and pointing four voices at their order lists. No sequence is walked. Nothing inside one is an address to be found and corrected, which is the difference between a malformed tune that plays wrongly and one that takes the loader with it. "SBTU", version, the tick in cycles, how many patches and sequences, offsets to the two tables, an order list each, and the patch each voice starts on. The magic is checked before anything else, because from there on the loader follows what the offsets name. break.sh shows what that guard is worth: without it, handing Play a PROGRAM runs the machine away until the cycle limit, rather than saying it is not a tune. PatchTable, SequenceTable and VoiceStart became pointers, so the engine does not care whether a tune came out of a file or was assembled in. Play's built-in tune now hands over the same three addresses a loaded one would, in eight lines - which is what keeps the two paths from drifting, and what made this rung change no scheduler code at all. Tests/maketune.py lays the fixture out byte by byte. IT IS NOT THE COMPILER: the sequences and the patches are literal bytes and the only thing computed is where each piece lands. That is the point - the loader is checked by something that does not share its idea of the format, which is the same reason SplitDisk and sbfs.asm share nothing but a specification. Both notes in the fixture are number 60, so the octave between them is a 0x80 command loading the second patch out of the file: the header, the tick, the relocation, an order list and a patch from a file, measured in one go. Play and the tune live on quiet.img rather than cosmos.img, so a fixture does not move ten recordings every time it changes size. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
5d9b39514b
commit
bfc46d982e
@@ -32,7 +32,7 @@ vars.script 50
|
||||
blocks.script 343
|
||||
loops.script 272
|
||||
tune.sbx 318
|
||||
Play.sbx 939
|
||||
Play.sbx 2525
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
|
||||
@@ -22,7 +22,7 @@ vars.script 50
|
||||
blocks.script 343
|
||||
loops.script 272
|
||||
tune.sbx 318
|
||||
Play.sbx 939
|
||||
Play.sbx 2525
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
|
||||
@@ -39,7 +39,7 @@ vars.script 50
|
||||
blocks.script 343
|
||||
loops.script 272
|
||||
tune.sbx 318
|
||||
Play.sbx 939
|
||||
Play.sbx 2525
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
|
||||
@@ -29,7 +29,7 @@ vars.script 50
|
||||
blocks.script 343
|
||||
loops.script 272
|
||||
tune.sbx 318
|
||||
Play.sbx 939
|
||||
Play.sbx 2525
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
|
||||
@@ -29,7 +29,7 @@ vars.script 50
|
||||
blocks.script 343
|
||||
loops.script 272
|
||||
tune.sbx 318
|
||||
Play.sbx 939
|
||||
Play.sbx 2525
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
|
||||
@@ -115,7 +115,7 @@ vars.script 50
|
||||
blocks.script 343
|
||||
loops.script 272
|
||||
tune.sbx 318
|
||||
Play.sbx 939
|
||||
Play.sbx 2525
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
|
||||
@@ -34,7 +34,7 @@ vars.script 50
|
||||
blocks.script 343
|
||||
loops.script 272
|
||||
tune.sbx 318
|
||||
Play.sbx 939
|
||||
Play.sbx 2525
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
|
||||
@@ -22,7 +22,7 @@ vars.script 50
|
||||
blocks.script 343
|
||||
loops.script 272
|
||||
tune.sbx 318
|
||||
Play.sbx 939
|
||||
Play.sbx 2525
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
|
||||
@@ -20,7 +20,7 @@ vars.script 50
|
||||
blocks.script 343
|
||||
loops.script 272
|
||||
tune.sbx 318
|
||||
Play.sbx 939
|
||||
Play.sbx 2525
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
|
||||
@@ -29,7 +29,7 @@ vars.script 50
|
||||
blocks.script 343
|
||||
loops.script 272
|
||||
tune.sbx 318
|
||||
Play.sbx 939
|
||||
Play.sbx 2525
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
|
||||
@@ -703,6 +703,21 @@ printf 'system /System/Boot/cosmos.bin\n' > "$WORK/lonely.cfg"
|
||||
"$ROOT/Programs/testPrograms/pauseTest.asm" -o "$WORK/Pause.sbx" >/dev/null
|
||||
"$TOOL" put "$DISKS/quiet.img" "$WORK/Pause.sbx" /Apps/Pause.sbx >/dev/null
|
||||
|
||||
# ---- A tune in a file, and the player that reads one ----
|
||||
#
|
||||
# maketune.py lays the bytes out by hand. It is NOT the compiler: the sequences and the
|
||||
# patches are written as literal bytes and the only thing computed is where each piece lands,
|
||||
# which is what makes it a fixture that tests the loader rather than one that tests itself.
|
||||
#
|
||||
# On this disk rather than cosmos.img for the usual reason - everything there shows in a
|
||||
# directory listing that ten recorded tests quote.
|
||||
python3 "$ROOT/Tests/maketune.py" "$WORK/two.tune" >/dev/null
|
||||
"$TOOL" put "$DISKS/quiet.img" "$WORK/two.tune" two.tune >/dev/null
|
||||
"$ROOT/Assembler" -I "$ROOT/Programs/Sounds" -I "$ROOT/Programs/Libraries" \
|
||||
-I "$ROOT/Programs/CosmOS/Source" \
|
||||
"$ROOT/Programs/CosmOS/Apps/Play.asm" -o "$WORK/Play.sbx" >/dev/null
|
||||
"$TOOL" put "$DISKS/quiet.img" "$WORK/Play.sbx" /Apps/Play.sbx >/dev/null
|
||||
|
||||
# ---- What a program made of it ----
|
||||
#
|
||||
# A status is only worth having if it survives the program that set it, so this runs three
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
# Lays out a .tune by hand, byte by byte, so that Play's loader has something to read before
|
||||
# there is a compiler to write one.
|
||||
#
|
||||
# THIS IS NOT THE COMPILER. It is the hex editor: the sequences and the patches are written
|
||||
# out as literal bytes and the only thing computed is where each piece lands, because counting
|
||||
# offsets by hand is how you get a fixture that tests your arithmetic instead of the loader's.
|
||||
#
|
||||
# Written by Anachronaut
|
||||
|
||||
import sys
|
||||
|
||||
def patch(pairs):
|
||||
out = [len(pairs)]
|
||||
for parameter, value in pairs:
|
||||
out += [parameter, value]
|
||||
return out
|
||||
|
||||
# Two instruments differing in one parameter, the octave, so that the same note number sounds
|
||||
# an octave apart depending on which is loaded.
|
||||
def voicePatch(octave):
|
||||
return patch([(0x00, 2), # saw
|
||||
(0x01, 0xFF), # at full gain
|
||||
(0x05, 1), # and on
|
||||
(0x20, 0), # no attack
|
||||
(0x21, 0), # no decay
|
||||
(0x22, 0xFF), # held at full
|
||||
(0x23, 5), # and gone quickly when the gate drops
|
||||
(0x04, octave)])
|
||||
|
||||
patches = [voicePatch(128), voicePatch(129)]
|
||||
|
||||
# Note 60 for four ticks. The second sequence says it again, having changed instrument first -
|
||||
# so the octave between them is the tune's doing and not the note's.
|
||||
sequences = [[60, 4, 0xFF],
|
||||
[0x80, 1, 60, 4, 0xFF]]
|
||||
|
||||
orders = [[0, 1, 0xFF], # voice 0 plays both, one after the other
|
||||
[0xFF], # and the other three have no part
|
||||
[0xFF],
|
||||
[0xFF]]
|
||||
|
||||
starts = [0, 0, 0, 0]
|
||||
tick = 125000 # a sixteenth note at 120 beats a minute
|
||||
|
||||
HEADER = 28
|
||||
body, at = [], HEADER
|
||||
|
||||
def place(blob):
|
||||
global at
|
||||
where = at
|
||||
body.extend(blob)
|
||||
at += len(blob)
|
||||
return where
|
||||
|
||||
# The tables come first so that their own offsets are known before anything they point at.
|
||||
patchTableAt = at; at += 2 * len(patches)
|
||||
sequenceTableAt = at; at += 2 * len(sequences)
|
||||
|
||||
patchAt = [place(p) for p in patches]
|
||||
sequenceAt = [place(s) for s in sequences]
|
||||
orderAt = [place(o) for o in orders]
|
||||
|
||||
def word(n):
|
||||
return [(n >> 8) & 0xFF, n & 0xFF]
|
||||
|
||||
tables = []
|
||||
for p in patchAt:
|
||||
tables += word(p)
|
||||
for q in sequenceAt:
|
||||
tables += word(q)
|
||||
body[0:0] = tables
|
||||
|
||||
header = [ord(c) for c in "SBTU"] + [1]
|
||||
header += [(tick >> 16) & 0xFF, (tick >> 8) & 0xFF, tick & 0xFF]
|
||||
header += [len(patches), len(sequences)]
|
||||
header += word(patchTableAt) + word(sequenceTableAt)
|
||||
for o in orderAt:
|
||||
header += word(o)
|
||||
header += starts
|
||||
header += [0, 0]
|
||||
assert len(header) == HEADER, "the header is %d bytes and the loader reads %d" % (len(header), HEADER)
|
||||
|
||||
open(sys.argv[1], "wb").write(bytes(header + body))
|
||||
print("%s: %d bytes, %d patches, %d sequences" % (sys.argv[1], len(header) + len(body),
|
||||
len(patches), len(sequences)))
|
||||
@@ -775,6 +775,60 @@ else
|
||||
|| result no "the system quietens what a program left sounding" "peak $LEFT, so it is still ringing"
|
||||
fi
|
||||
|
||||
# ---- A tune read from a file ----
|
||||
#
|
||||
# M4's loader. Everything in a tune is an OFFSET from wherever it was put, so loading one is
|
||||
# adding the base to two tables and pointing four voices at their order lists - no sequence is
|
||||
# walked and nothing inside one is an address to be found and corrected.
|
||||
#
|
||||
# BOTH NOTES ARE NUMBER 60. The octave between them is a 0x80 command in the second sequence,
|
||||
# loading the second patch out of the file, so what this measures is the whole path at once:
|
||||
# the header parsed, the tick taken from it, the tables relocated, an order list followed, and
|
||||
# a patch that came out of a file rather than out of the program.
|
||||
#
|
||||
# The fixture is laid out byte by byte by Tests/maketune.py, which is the hex editor rather
|
||||
# than the compiler - the point being that the loader is tested by something that does not
|
||||
# share its idea of the format.
|
||||
if [ ! -f "$ROOT/Tests/build/disks/quiet.img" ]; then
|
||||
result no "a tune read from a file" "quiet.img is missing; run Tests/makedisks.sh"
|
||||
else
|
||||
printf 'Play two.tune\n\n\nexit\n' > "$BUILD/tune.in"
|
||||
timeout 60 "$EMU" --fast --cycles 6000000 --sound "$BUILD/loaded.raw" \
|
||||
--disk "$ROOT/Tests/build/disks/quiet.img" "$BUILD/cosmos.bin" \
|
||||
< "$BUILD/tune.in" > "$BUILD/tune.out" 2>&1
|
||||
|
||||
STARTS="$(python3 - "$BUILD/loaded.raw" <<'PY2'
|
||||
import struct, sys
|
||||
d = open(sys.argv[1], "rb").read(); n = len(d) // 2
|
||||
v = struct.unpack("<%dh" % n, d)
|
||||
print(next((i for i, x in enumerate(v) if abs(x) > 500), -1))
|
||||
PY2
|
||||
)"
|
||||
if [ "$STARTS" -lt 0 ]; then
|
||||
result no "a tune read from a file" "nothing sounded at all"
|
||||
else
|
||||
FIRST="$(measure loaded pitch $(( STARTS + 1000 )) $(( STARTS + 22000 )))"
|
||||
SECOND="$(measure loaded pitch $(( STARTS + 26000 )) $(( STARTS + 46000 )))"
|
||||
near "$FIRST" 261.6 2 \
|
||||
&& result ok "a tune read from a file" "$FIRST hertz, from a header and two tables" \
|
||||
|| result no "a tune read from a file" "$FIRST hertz, wanted middle C"
|
||||
near "$SECOND" 523.3 2 \
|
||||
&& result ok "and a command in it loads a patch from it" "$SECOND hertz from note 60 again" \
|
||||
|| result no "and a command in it loads a patch from it" "$SECOND hertz, wanted the octave"
|
||||
fi
|
||||
|
||||
# ---- And a file that is not a tune is refused ----
|
||||
#
|
||||
# The loader reads offsets out of a file and follows what they name, so a file that is not
|
||||
# one has to be turned away at the magic rather than a few instructions later.
|
||||
printf 'Play Apps/Hum.sbx\nexit\n' > "$BUILD/notune.in"
|
||||
timeout 30 "$EMU" --fast --cycles 3000000 --disk "$ROOT/Tests/build/disks/quiet.img" \
|
||||
"$BUILD/cosmos.bin" < "$BUILD/notune.in" > "$BUILD/notune.out" 2>&1
|
||||
grep -q "not a tune" "$BUILD/notune.out" \
|
||||
&& result ok "and a file that is not a tune is refused" "it said so rather than playing it" \
|
||||
|| result no "and a file that is not a tune is refused" "$(tail -2 "$BUILD/notune.out" | tr '\n' ' ')"
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$FAIL" -eq 0 ]; then
|
||||
echo "All $PASS sound checks passed."
|
||||
|
||||
Reference in New Issue
Block a user