useTune took the period out of a file's header and then Play wrote its own straight over it, so every tune played at a sixteenth note at 120 beats a minute whatever it asked for. A tune with a #Tick of 0d250000 lasted half as long as it said. Nothing noticed because every fixture in the suite asked for exactly the tick Play had written into itself. A test that agrees with the bug by coincidence is not a test, and the way to find out is a fixture that wants something else - so slow.tune is two.tune with twice the period and nothing else changed, and it has to last twice as long. The period now belongs to whoever supplied the tune: useTune sets it from the header, useBuiltIn sets its own, and the start code writes only the control byte - which has to come after either of them, because writing control with the run bit set is what loads the period. Found while reading Play to see how a splash screen would drive the player, which is a reminder that the second reader of a piece of code is worth more than the first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
412 lines
14 KiB
NASM
412 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 ----
|
|
;
|
|
; ONLY THE CONTROL BYTE. The period was set by whoever supplied the tune, because a tick
|
|
; belongs to the tune and not to this program: a file says what it wants in its header.
|
|
;
|
|
; It used to be written here as well, AFTER useTune had taken it out of the file - so every
|
|
; tune played at a sixteenth note at 120 beats a minute whatever its header said. The only
|
|
; reason nothing noticed is that the one fixture in the suite asks for exactly that.
|
|
;
|
|
; Writing control with the run bit set is what loads the period, which is why this comes
|
|
; after whoever set it and not before.
|
|
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:
|
|
; The tick this tune wants. For a file it comes out of the header; for this one it is here,
|
|
; so that both hand the player the same things.
|
|
INIA 0x01
|
|
OUTA 0x52
|
|
INIA 0xE8
|
|
OUTA 0x53
|
|
INIA 0x48
|
|
OUTA 0x54 ; 0x01E848 is 125,000: a sixteenth note at 120.
|
|
|
|
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
|