The player speaks indices, which is the shape a file has to be

Order lists hold one-byte sequence indices and 0x80 holds a one-byte
patch index. Two tables, PatchTable and SequenceTable, are the only
places an address lives.

THAT IS WHAT MAKES A TUNE LOADABLE WITHOUT WALKING IT. Nothing inside a
sequence or an order list is an address, so putting one in memory means
adding the load address to two arrays and nothing else. The alternative
is a loader that parses every sequence looking for addresses to correct,
which is a loader a malformed file can walk off a cliff.

Doing it now, while the tune is still assembled in, means the file form
and the assembled form are the same shape - so reading a tune from a file
will change no engine code at all. The whole point of the rung.

The patch each voice starts on moved from four calls in a row into
VoiceStart, four declared bytes, and loadStartPatches reads them. A
starting instrument is state and belongs where state goes: the user's
ruling is that a voice with undefined state is an error, prompted by
noticing that a program run a second time starts with the memory the
first run left, because loading is what initialises and running is not.

Order lists also halved in size, which was not the reason but is welcome.

Hand-writing the two tables is exactly the tedium the compiler exists to
remove - every sequence counted into its place, and moving one means
renumbering. Better to feel that here than after a tool has baked the
shape in.

Verified by rendering: bar for bar the same piece. break.sh confirms the
scaling, since an index is doubled to reach a two-byte entry and halving
that step lands on the wrong sequence and fails four checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-05 20:45:40 -04:00
co-authored by Claude Opus 5
parent 8029063bdc
commit 5d9b39514b
14 changed files with 132 additions and 64 deletions
+69 -18
View File
@@ -13,11 +13,19 @@
;
; The caller must define, in its Data Segment:
;
; Order0 Order1 Order2 Order3 a table of sequence addresses each, ending in 0xFFFF
; PatchTable one two-byte address per patch
; SequenceTable one two-byte address per sequence
; VoiceStart four bytes, the patch index each voice starts on
; Order0..Order3 one-byte sequence indices each, ending in 0xFF
;
; each of which is a table of sequence addresses ending in 0xFFFF, and must call stepVoice once
; a tick for each voice, with A holding the channel and DP1 the voice's record. Playing counts down as voices run out, and reaching nought is the piece
; being over.
; and must call loadStartPatches once, then stepVoice once a tick for each voice, with A
; holding the channel and DP1 the voice's record. Playing counts down as voices run out, and
; reaching nought is the piece being over.
;
; INDICES THROUGH TABLES, RATHER THAN ADDRESSES, because that is the shape a tune read from a
; file has to be: the two tables are the only places an address lives, so loading a tune means
; adding a base to them and nothing else. Keeping the assembled-in form the same shape is what
; makes reading one from a file change no code here at all.
#Program
@@ -94,13 +102,17 @@ stepDone:
; was given rather than the one this needs next.
stepCommand:
INCD.0
LDD.2.0 ; DP2 is the patch named after the command.
INCD.0
INCD.0 ; Past the address, on whatever comes next.
LDA.0 ; Which patch: one byte of index, not an address.
INCD.0 ; Past it, on whatever comes next.
DPUP.1 0d2
STD.0.1
DPDN.1 0d2
SETD.2 PatchTable
DPUA.2
DPUA.2 ; Twice, because an entry is two bytes.
LDD.2.2 ; And DP2 follows the address it is now holding.
PSHD.2
POPD.0 ; DP0 is the patch.
LDA.3 ; And A the channel, which loadPatch selects.
@@ -113,27 +125,34 @@ stepCommand:
; ---- The sequence ended, so take the next one from this voice's order list ----
;
; 0xFFFF ENDS THE LIST, which mirrors the 0xFF that ends a sequence, and testing the high byte
; alone is enough: a sequence beginning at 0xFF00 or above has fewer bytes of memory left than
; it needs, so no real one can be there.
; AN ORDER LIST IS INDICES, one byte each, ending in 0xFF - the same byte that ends a sequence.
; SequenceTable is the only place a sequence's address lives.
;
; It was a zero to begin with, on the reasoning that this player's data is based at 0x3000 and
; nothing could live below it. That is true of a loaded program and false of a boot image,
; whose data starts at 0x0000 - so the first test written against it read its own first
; sequence as the end of the list and played nothing at all. An address is not a good place to
; hide a flag unless the address is impossible everywhere.
; That is what makes a tune loadable without walking it: nothing inside a sequence or an order
; list is an address, so putting one in memory means adding the load address to two tables and
; nothing else. A loader that had to parse sequences looking for addresses to fix up is a
; loader that a malformed file can walk off a cliff.
;
; It was addresses here, terminated by a zero, on the reasoning that no sequence could live
; below this player's 0x3000 base. True of a loaded program and false of a boot image based at
; zero, so the first test written against it read its own first sequence as the end of the
; list and played nothing at all. An address is not a good place to hide a flag unless the
; address is impossible in every program rather than in this one.
stepSequenceEnd:
LDD.0.1 ; DP0 is the order cursor, which is the first thing in the record.
LDA.0
LDA.0 ; Which sequence comes next.
INIB 0xFF
XOR
BRQ stepOrderEnd
LDD.2.0 ; DP2 is the sequence it names.
INCD.0
INCD.0
STD.0.1 ; The order cursor, moved past this entry.
SETD.2 SequenceTable
DPUA.2
DPUA.2
LDD.2.2 ; DP2 is the sequence that index names.
DPUP.1 0d2
STD.2.1 ; And the sequence cursor set to the new one.
DPDN.1 0d2
@@ -153,6 +172,38 @@ stepOrderEnd:
STA.3
RET
; ---- The instrument each voice starts on ----
;
; STARTING STATE IS DECLARED RATHER THAN ASSUMED. A voice whose instrument was never said
; would play on whatever the device woke up with, or worse on whatever the last tune left -
; and that is a fault this machine has met elsewhere, where a program run a second time starts
; with the memory the first run left, because loading is what initialises and running is not.
loadStartPatches:
RSTA
loadStartOne:
SETD.3 ThisChannel
STA.3
SETD.2 VoiceStart
DPUA.2
LDA.2 ; Which patch this voice starts on.
SETD.2 PatchTable
DPUA.2
DPUA.2
LDD.2.2
PSHD.2
POPD.0
LDA.3
CALL loadPatch ; A survives a CALL, so it is still the channel below.
INCA
INIB 0d4
CCF
SUB
BNQ loadStartOne
RET
; ---- A patch, onto the channel named by A ----
;
; A count, then that many pairs of parameter and value: the format SoundPatch writes and the