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
403 lines
14 KiB
NASM
403 lines
14 KiB
NASM
; Play.asm
|
|
; Four voices on one clock, which is what music is and one channel cannot be.
|
|
; Written by Anachronaut
|
|
;
|
|
; ---- One tick, four cursors ----
|
|
;
|
|
; The sound device has four channels and no idea when. The timer has a period and no idea
|
|
; what. This is the thing between them: on every tick it walks four voices, and each voice
|
|
; counts down the note it is holding and reads the next one when the count runs out.
|
|
;
|
|
; THE VOICES SHARE NOTHING BUT THE TICK. Each keeps its own place in its own track and its
|
|
; own count of how much longer the current note lasts, so a voice playing whole notes and a
|
|
; voice playing eighths cost the same and never have to know about each other. That is why
|
|
; the tick is the smallest subdivision in the piece rather than a note length: it is the
|
|
; only unit all four can agree on.
|
|
;
|
|
; ---- What a sequence is ----
|
|
;
|
|
; Pairs of bytes: what to play, then how many ticks it lasts.
|
|
;
|
|
; 0x01 to 0x7F a MIDI note. 60 is middle C and every 12 is an octave.
|
|
; 0x00 a rest - the ticks pass with nothing sounding
|
|
; 0x80 to 0xFE a command, which takes no time at all. See below.
|
|
; 0xFF the sequence is over
|
|
;
|
|
; MIDI NOTES ONLY REACH 127, so the top of the byte was free and none of that had to be
|
|
; invented. Examples/tune.asm spent zero on its end marker and so had no way to write a rest
|
|
; at all, which one voice can just about live with and four cannot: voices do not all play at
|
|
; once, and the silences are what make them separate parts rather than a chord.
|
|
;
|
|
; A note's duration is its whole life. The gate goes down when the count runs out and the next
|
|
; event begins on the same tick, so a gap between two notes is WRITTEN, as a rest, rather than
|
|
; invented by the player out of some fraction it decided on.
|
|
;
|
|
; ---- Commands, of which there is one ----
|
|
;
|
|
; A command consumes no tick: the reader acts on it and reads the next event straight away, so
|
|
; commands sit BETWEEN notes in time rather than needing a place of their own.
|
|
;
|
|
; 0x80 <address> play the rest of this voice on that patch.
|
|
;
|
|
; A patch change reshapes whatever is still ringing on the voice, and nothing can be done
|
|
; about that here: a channel has one set of parameters and a note in its release is using
|
|
; them. Deferring the load to the next note would reshape the same tail. So it is a fact about
|
|
; the hardware rather than a choice about the format, and the cure is a rest long enough for
|
|
; the release, which is the composer's to write.
|
|
;
|
|
; ONE COMMAND, and the other 125 values left alone. A tempo change, a volume ramp and a note
|
|
; slide are all easy to add and impossible to remove, and no piece of music has asked for one.
|
|
;
|
|
; ---- Sequences, and the order they go in ----
|
|
;
|
|
; A sequence is one voice's phrase. Each voice has an ORDER LIST of its own - a table of
|
|
; sequence addresses, ending in a zero - and when a sequence runs out the voice takes the next
|
|
; address from it. The voice stops when the list does.
|
|
;
|
|
; THAT IS WHERE REPETITION COMES FROM, and it costs no notation: the bass below plays the same
|
|
; sequence in the first bar and the last, and it is written once. A thirty-two bar piece that
|
|
; reuses four phrases is four phrases and a list.
|
|
;
|
|
; PER VOICE RATHER THAN ONE SHARED LIST, because that is the shape this machine likes. A
|
|
; voice's order cursor is a pointer it advances by itself, which is the same LDD and STD move
|
|
; everything else here makes; a shared table of four-column rows would have every voice
|
|
; indexing into one place, which is a worse fit for a machine with four data pointers. The
|
|
; four columns are how it READS, and how a tracker would show it; they are not how it is
|
|
; stored. Nothing keeps the voices together except that their sequences add up to the same
|
|
; length, which is a thing a compiler can check and a hand-written tune has to get right.
|
|
;
|
|
; A voice silent through a bar has a sequence that rests for it. There is no need for a way to
|
|
; say "nothing here" when a rest already says it.
|
|
;
|
|
; ---- Where the state lives ----
|
|
;
|
|
; Six bytes a voice: two of order cursor, two of sequence cursor, one of count, one of live.
|
|
; Both cursors are two bytes and at the front because that is what LDD and STD move - a
|
|
; pointer through a pointer, which is the whole reason this is a loop over four voices instead
|
|
; of the same code written out four times.
|
|
;
|
|
; A voice starts pointed at Empty, which is a sequence of nothing but its end marker. The
|
|
; first tick runs its count out, finds the end, and goes to the order list for the real first
|
|
; sequence - so the beginning needs no special case at all.
|
|
|
|
#Include services.asm
|
|
|
|
#Program
|
|
|
|
#Base 0x5000
|
|
|
|
start:
|
|
; ---- An instrument for each voice ----
|
|
;
|
|
; Four patches, designed in soundThing and converted by SoundPatch, which is the only thing
|
|
; here that understands what a patch means. The player reads a count and that many parameter
|
|
; and value pairs and knows nothing else about them.
|
|
;
|
|
; WHICH PATCH EACH VOICE STARTS ON IS DECLARED, in VoiceStart below, rather than being four
|
|
; calls in a row here. That is the shape a tune read from a file will have - a starting
|
|
; instrument is state and belongs in a header - and saying it once means the two forms do
|
|
; not drift.
|
|
;
|
|
; A PATCH BELONGS TO ITS CHANNEL, which is why four of them can be up at once. It did not
|
|
; used to: the LFOs belonged to the whole device, so whichever patch was loaded last owned
|
|
; them for every voice. Kalimba has LFO 0 switched off and the other three have it on, so
|
|
; under the old device this piece would have played with the arpeggio's setting on all four
|
|
; parts - which is exactly the fault that made Lunar Porter's low fuel warning stop
|
|
; trilling after the first landing of a run.
|
|
; ---- A tune named on the line, or the one in here ----
|
|
;
|
|
; Nothing after the name plays what this program was built with. A name reads a file and
|
|
; plays that instead, which is what makes it a player rather than a program with one song.
|
|
SETD.0 TuneName
|
|
INIB 0d63
|
|
SWI osArgument
|
|
SETD.0 TuneName
|
|
LDA.0
|
|
BRA playBuiltIn ; Nothing was typed after the name.
|
|
|
|
SETD.1 TuneBuffer ; DP0 is still the name, which is what osFileRead wants.
|
|
SWI osFileRead
|
|
BNQ playNoFile
|
|
|
|
SETD.0 TuneBuffer
|
|
CALL useTune
|
|
BNQ playNotATune
|
|
BRI playReady
|
|
|
|
playBuiltIn:
|
|
CALL useBuiltIn
|
|
playReady:
|
|
|
|
INIA 0x60 ; Room over the top for four voices at once.
|
|
OUTA 0x46
|
|
|
|
; ---- The beat ----
|
|
;
|
|
; 125,000 cycles is a sixteenth note at 120 beats a minute, which is the smallest thing
|
|
; this piece divides a beat into. The period goes in before the control byte, because
|
|
; writing control with the run bit set is what loads it.
|
|
INIA 0x01
|
|
OUTA 0x52
|
|
INIA 0xE8
|
|
OUTA 0x53
|
|
INIA 0x48
|
|
OUTA 0x54
|
|
INIA 0x07
|
|
OUTA 0x51 ; Run, repeat, interrupt.
|
|
SIF
|
|
|
|
; ---- The loop ----
|
|
;
|
|
; Step every voice, see whether any of them is still going, and then sleep. Stepping before
|
|
; waiting is what makes the first note sound on the first tick rather than the second.
|
|
tickLoop:
|
|
SETD.1 Voice0
|
|
RSTA
|
|
CALL stepVoice
|
|
SETD.1 Voice1
|
|
INIA 0d1
|
|
CALL stepVoice
|
|
SETD.1 Voice2
|
|
INIA 0d2
|
|
CALL stepVoice
|
|
SETD.1 Voice3
|
|
INIA 0d3
|
|
CALL stepVoice
|
|
|
|
SETD.1 Playing
|
|
LDA.1
|
|
BRA finished
|
|
WAIT ; Nothing at all until the timer says a tick has gone by.
|
|
BRI tickLoop
|
|
|
|
finished:
|
|
; ---- Let the last note finish, and MEASURE how long that takes ----
|
|
;
|
|
; The gates all went down on the tick the tracks ended, but a gate down is a note released
|
|
; rather than a note stopped: the oboe and the strings have a release to run. Rendered and
|
|
; measured, the tail dies out eight tenths of a second after the last event, and eight ticks
|
|
; of ring is one second - which sounds like enough and is two tenths short of it.
|
|
;
|
|
; A program that exits with sound still in the air leaves nothing able to end it: the
|
|
; program is gone and cannot drop a gate. What that turns into depends on the front end -
|
|
; behind a window the tail simply finishes, and on a terminal EMULATED TIME STOPS while the
|
|
; machine blocks on a key, so the tail freezes and comes out a snippet per keystroke.
|
|
;
|
|
; Twelve ticks is a second and a half, which covers the measured tail with room over. It is
|
|
; a number about THESE FOUR INSTRUMENTS, and a patch with a longer release would want more -
|
|
; the general answer is for the system to quieten the device when a program stops, the way
|
|
; it puts the screen back, and that is not built.
|
|
INIB 0d12
|
|
lastRing:
|
|
WAIT
|
|
DECB
|
|
BNB lastRing
|
|
|
|
CIF
|
|
RSTA
|
|
OUTA 0x51
|
|
SWI osExit
|
|
|
|
; ---- What it says when it cannot ----
|
|
;
|
|
; Before the timer has been started, both of them, so there is nothing to stop and no handler
|
|
; to take away.
|
|
playNoFile:
|
|
SETD.0 NoFileSaid
|
|
SWI osPrintString
|
|
INIA 0x01
|
|
SWI osExit
|
|
|
|
playNotATune:
|
|
SETD.0 NotATuneSaid
|
|
SWI osPrintString
|
|
INIA 0x01
|
|
SWI osExit
|
|
|
|
; ---- The tune that is in this program ----
|
|
;
|
|
; What useTune does for a file, said in eight lines for a tune that is already in memory: the
|
|
; three tables the player asks for, and an order list for each voice. Nothing is relocated,
|
|
; because the assembler already wrote the addresses.
|
|
useBuiltIn:
|
|
SETD.0 BuiltInPatches
|
|
SETD.1 PatchTable
|
|
STD.0.1
|
|
SETD.0 BuiltInSequences
|
|
SETD.1 SequenceTable
|
|
STD.0.1
|
|
SETD.0 BuiltInVoiceStart
|
|
SETD.1 VoiceStartAt
|
|
STD.0.1
|
|
SETD.0 Order0
|
|
SETD.1 Voice0
|
|
STD.0.1
|
|
SETD.0 Order1
|
|
SETD.1 Voice1
|
|
STD.0.1
|
|
SETD.0 Order2
|
|
SETD.1 Voice2
|
|
STD.0.1
|
|
SETD.0 Order3
|
|
SETD.1 Voice3
|
|
STD.0.1
|
|
CALL startVoices
|
|
RET
|
|
|
|
; The tick has nothing to do: the loop above is the player and WAIT only needs something to
|
|
; have happened. A handler still has to exist, because an interrupt with nothing installed to
|
|
; catch it is a fault. Taking it is what brings the line down - a program that POLLED the
|
|
; timer would have to read 0x50 instead.
|
|
tick:
|
|
RETI
|
|
|
|
#Data
|
|
|
|
#Base 0x3000
|
|
|
|
; The name that followed "Play", and room for the tune it names. WHOLE BLOCKS: osFileRead
|
|
; puts 256 bytes down whatever the file's length, so the room here is a multiple of that and
|
|
; not a guess at how big a tune is.
|
|
TuneName:
|
|
#Reserve 0d64
|
|
TuneBuffer:
|
|
#Reserve 0d1024
|
|
|
|
NoFileSaid:
|
|
"no such tune"
|
|
NotATuneSaid:
|
|
"that is not a tune"
|
|
|
|
; ---- The order lists ----
|
|
;
|
|
; A table of sequence addresses each, ending in a zero. Read the four of them across and they
|
|
; are the bars; read one down and it is a part.
|
|
;
|
|
; bar 1 2 3 4
|
|
; melody Mel1 Mel2 Mel3 Mel4
|
|
; harmony Har1 Har2 Har3 Har4
|
|
; bass BassC BassF BassG BassC
|
|
; arp ArpC ArpF ArpG Silent
|
|
;
|
|
; THE BASS PLAYS BassC TWICE and it is written once. That is the whole return on having an
|
|
; order list, and it is worth noticing how little it cost: a table of labels the assembler
|
|
; fills in, and ten instructions in stepVoice.
|
|
Order0:
|
|
0d0 0d1 0d2 0d3 0xFF
|
|
Order1:
|
|
0d4 0d5 0d6 0d7 0xFF
|
|
Order2:
|
|
0d8 0d9 0d10 0d8 0xFF ; BassC twice, written once.
|
|
Order3:
|
|
0d11 0d12 0d13 0d14 0xFF
|
|
|
|
; ---- The tables, which are the only places an address lives ----
|
|
;
|
|
; Writing these out by hand is exactly the tedium a compiler exists to remove: every sequence
|
|
; has to be counted into its place above, and moving one means renumbering. That it is
|
|
; unpleasant is the point of noticing it here rather than after a tool has baked the shape in.
|
|
; The tables this tune uses. The player holds POINTERS to them rather than the tables, because
|
|
; a tune read from a file has its tables wherever the file was put - so a built-in one hands
|
|
; over the same three addresses a loaded one would.
|
|
BuiltInPatches:
|
|
OboePatch StringsPatch SquarePatch KalimbaPatch
|
|
|
|
BuiltInSequences:
|
|
Mel1 Mel2 Mel3 Mel4 ; 0 to 3
|
|
Har1 Har2 Har3 Har4 ; 4 to 7
|
|
BassC BassF BassG ; 8 to 10
|
|
ArpC ArpF ArpG Silent ; 11 to 14
|
|
|
|
; The patch each voice starts on. Not assumed, and not four calls in a row: a starting
|
|
; instrument is state, and state belongs somewhere it can be read.
|
|
BuiltInVoiceStart:
|
|
0d0 0d1 0d2 0d3
|
|
|
|
; ---- Four bars of C, F, G, C ----
|
|
;
|
|
; A bar is sixteen ticks, and every sequence in a column adds up to sixteen. Nothing enforces
|
|
; that here: get one wrong and the voices come apart, quietly, some bars later. It is the
|
|
; first thing a compiler should check.
|
|
|
|
; The melody, in quarters and halves.
|
|
Mel1:
|
|
0d64 0d4 ; E
|
|
0d67 0d4 ; G
|
|
0d72 0d8 ; C, held
|
|
0xFF
|
|
Mel2:
|
|
0d65 0d4 ; F
|
|
0d69 0d4 ; A
|
|
0d72 0d8 ; C
|
|
0xFF
|
|
Mel3:
|
|
0d62 0d4 ; D
|
|
0d67 0d4 ; G
|
|
0d71 0d8 ; B
|
|
0xFF
|
|
Mel4:
|
|
0x80 0d1 ; Patch one is Strings: the last bar is a swell, not a reed.
|
|
0d72 0d16 ; and home
|
|
0xFF
|
|
|
|
; A second part underneath it, in halves.
|
|
Har1:
|
|
0d60 0d8 ; C
|
|
0d64 0d8 ; E
|
|
0xFF
|
|
Har2:
|
|
0d57 0d8 ; A
|
|
0d60 0d8 ; C
|
|
0xFF
|
|
Har3:
|
|
0d59 0d8 ; B
|
|
0d62 0d8 ; D
|
|
0xFF
|
|
Har4:
|
|
0d64 0d16 ; E
|
|
0xFF
|
|
|
|
; The bass, one note to a bar - and the first bar's is the last bar's.
|
|
BassC:
|
|
0d48 0d16 ; C
|
|
0xFF
|
|
BassF:
|
|
0d41 0d16 ; F
|
|
0xFF
|
|
BassG:
|
|
0d43 0d16 ; G
|
|
0xFF
|
|
|
|
; And an arpeggio in eighths, which is the part that proves the others are not waiting for it.
|
|
ArpC:
|
|
0d72 0d2 0d76 0d2 0d79 0d2 0d76 0d2
|
|
0d72 0d2 0d76 0d2 0d79 0d2 0d76 0d2
|
|
0xFF
|
|
ArpF:
|
|
0d77 0d2 0d81 0d2 0d84 0d2 0d81 0d2
|
|
0d77 0d2 0d81 0d2 0d84 0d2 0d81 0d2
|
|
0xFF
|
|
ArpG:
|
|
0d79 0d2 0d83 0d2 0d86 0d2 0d83 0d2
|
|
0d79 0d2 0d83 0d2 0d86 0d2 0d83 0d2
|
|
0xFF
|
|
Silent:
|
|
0d00 0d16 ; Out for the last bar, so it is the other three.
|
|
0xFF
|
|
|
|
; The scheduler, the patch loader and the state a voice keeps. The tune above is this
|
|
; program's; everything that plays it is shared with whatever else wants to.
|
|
#Include player.asm
|
|
|
|
; The instruments, at the bottom because each brings its own #Data and a base has to come
|
|
; before anything is in the segment it bases.
|
|
#Include oboe.asm
|
|
#Include strings.asm
|
|
#Include square.asm
|
|
#Include kalimba.asm
|
|
|
|
#Vectors
|
|
|
|
Boot start
|
|
Device 0x50 tick
|