Commit Graph
3 Commits
Author SHA1 Message Date
AnachronautandClaude Opus 5 bfc46d982e 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
2026-09-05 20:58:24 -04:00
AnachronautandClaude Opus 5 5d9b39514b 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
2026-09-05 20:45:40 -04:00
AnachronautandClaude Opus 5 8029063bdc Sequences, order lists, and a command that changes the instrument
The rung between M3 and M4, and the point of doing it before the format:
the engine learns the tracker's model with the tune still assembled in,
so M4 becomes serialising a thing that exists rather than designing a
thing that does not.

A voice no longer walks one long track. 0xFF now means THIS SEQUENCE
ended, and the voice takes the next address from an order list of its
own. That is where repetition comes from, and it costs no notation: the
bass plays the same sequence in the first bar and the last and it is
written once. Per voice rather than one shared table of four-column rows,
because a voice's order cursor is then a pointer it advances by itself -
the same LDD and STD move everything else here makes. Four columns is how
it reads, not how it is stored.

Sequences also carry COMMANDS, which take no tick: the reader acts and
reads the next event on the same boundary. One is defined, 0x80, which
plays the rest of that voice on another patch, and the other 125 values
are left alone. A patch change reshapes whatever is still ringing on the
voice and nothing can be done about that - a channel has one set of
parameters and a note in its release is using them - so it is a fact
about the hardware and the cure is a rest, which is the composer's.

The engine moved to Libraries/player.asm rather than being copied into
the test a second time, now that it is big enough to drift. Play supplies
the tune and the beat; the library supplies the scheduler, the patch
loader and a voice's state. Verified by rendering: bar for bar identical
across the move.

AND THE TEST FOUND A REAL FLAW IN THE FORMAT, which is the whole argument
for building the reader first. The order list ended with 0x0000, on the
reasoning that no sequence could live below the 0x3000 this program is
based at. True of a loaded program, false of a boot image whose data
starts 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. It is 0xFFFF
now, which mirrors the 0xFF ending a sequence and is impossible
everywhere: a sequence at 0xFF00 or above has fewer bytes left than it
needs. An address is a poor place to hide a flag unless the address is
impossible in every program, not just this one.

One order list in the test now really ends, because otherwise nothing
reached the terminator at all: every voice sat on a long rest and the
break went unnoticed. With it, breaking the test reads garbage past the
end and the counter sums two notes at 781 hertz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 19:54:38 -04:00