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
This commit is contained in:
Anachronaut
2026-09-05 19:54:38 -04:00
co-authored by Claude Opus 5
parent f9b08cf7f9
commit 8029063bdc
16 changed files with 402 additions and 224 deletions
+116 -131
View File
@@ -14,29 +14,71 @@
; 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 track is ----
; ---- 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
; 0xFF the track is over
; 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 neither of those two 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 makes them separate parts rather than a chord.
; 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.
; 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 ----
;
; Four bytes a voice: two of cursor, one of count, one of live. The cursor is two bytes and
; first because that is what LDD and STD move - a pointer through a pointer, which is the
; whole reason this can be a loop over four voices instead of the same code written out four
; times.
; 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
@@ -140,89 +182,6 @@ lastRing:
OUTA 0x51
SWI osExit
; ---- One voice, one tick ----
;
; A is the channel and DP1 is the voice's four bytes. Both survive the CALL that got here,
; which is what lets the caller say which voice it means in two instructions.
stepVoice:
SETD.3 ThisChannel
STA.3 ; A is wanted for other things between here and using it.
DPUP.1 0d3
LDA.1 ; Live?
DPDN.1 0d3
BRA stepDone
DPUP.1 0d2
LDA.1
DECA
STA.1 ; One tick less of whatever is sounding.
DPDN.1 0d2
BNA stepDone ; Still holding it.
; ---- The count ran out, so this is a boundary ----
;
; Select the channel FIRST. Every sound port below writes to whichever channel was last
; named, so a voice that forgot would be playing somebody else's part.
LDA.3 ; DP3 has held the channel since the top of this routine.
OUTA 0x41
RSTA
OUTA 0x45 ; Let go of the note that just ended.
LDD.0.1 ; DP0 is now this voice's place in its track.
LDA.0
INIB 0xFF
XOR ; XOR answers in Q and leaves A holding the note.
BRQ stepEnded
INCD.0
LDB.0 ; How many ticks it lasts.
INCD.0
STD.0.1 ; The cursor, moved past this event.
DPUP.1 0d2
STB.1 ; And the count it will be held for.
; A is still the note. Zero is a rest, which is a duration with nothing started.
BRA stepDone
OUTA 0x44 ; Writing the note is what starts it.
stepDone:
RET
stepEnded:
; The gate is already down, so the last note is fading. Mark the voice finished and say so.
DPUP.1 0d3
RSTA
STA.1
SETD.3 Playing
LDA.3
DECA
STA.3
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
; one Lander already plays. B holds what is left, which costs nothing to keep - a CALL saves
; B, so a caller's count is not disturbed by a patch being loaded inside its loop.
;
; The channel is selected FIRST. Every parameter write below lands on whichever channel was
; named last, so a patch loaded without one would be quietly rewriting somebody else's voice.
loadPatch:
OUTA 0x41
LDB.0
INCD.0
loadPatchPair:
LDA.0
OUTA 0x42
INCD.0
LDA.0
OUTA 0x43
INCD.0
DECB
BNB loadPatchPair
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
@@ -234,78 +193,104 @@ tick:
#Base 0x3000
; Four bytes a voice: cursor high, cursor low, count, live.
; ---- The order lists ----
;
; A LABEL WRITTEN HERE COMES OUT AS ITS ADDRESS, two bytes, most significant first - which is
; exactly the shape LDD reads. So a voice starts pointed at its track without a line of code:
; nothing relocates on this machine, so the address the assembler wrote is the address it
; will have.
; 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.
;
; The count starts at one rather than zero so that the first pass through the loop runs it out
; and fetches the first event. Nothing has to be special-cased for the beginning, and the
; piece starts on the tick rather than one after it.
Voice0:
Track0 0d1 0d1
Voice1:
Track1 0d1 0d1
Voice2:
Track2 0d1 0d1
Voice3:
Track3 0d1 0d1
Playing:
0d4
ThisChannel:
0x00
; 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:
Mel1 Mel2 Mel3 Mel4 0xFF 0xFF
Order1:
Har1 Har2 Har3 Har4 0xFF 0xFF
Order2:
BassC BassF BassG BassC 0xFF 0xFF
Order3:
ArpC ArpF ArpG Silent 0xFF 0xFF
; ---- Four bars of C, F, G, C ----
;
; A bar is sixteen ticks. The parts move at four different rates on purpose: this is the
; thing one channel cannot do, so it is the thing worth playing.
; 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.
Track0:
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
0d72 0d16 ; and home
0xFF
Mel4:
0x80 StringsPatch ; The last bar is a swell rather than a reed.
0d72 0d16 ; and home
0xFF
; A second part underneath it, in halves.
Track1:
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.
Track2:
; 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
0d48 0d16 ; C
0xFF
; And an arpeggio in eighths, which is the part that proves the others are not waiting for it.
Track3:
ArpC:
0d72 0d2 0d76 0d2 0d79 0d2 0d76 0d2
0d72 0d2 0d76 0d2 0d79 0d2 0d76 0d2
0d77 0d2 0d81 0d2 0d84 0d2 0d81 0d2
0d77 0d2 0d81 0d2 0d84 0d2 0d81 0d2
0d79 0d2 0d83 0d2 0d86 0d2 0d83 0d2
0d79 0d2 0d83 0d2 0d86 0d2 0d83 0d2
0d00 0d16 ; and out, so the last bar is the other three
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.
+1 -1
View File
@@ -720,7 +720,7 @@ from every assembly file in it. Several are old programs written for the bare ma
| Life | Conway's Game of Life, which had to be taught to stop, since a program that never ends takes the shell with it. Polls the console between generations. |
| Snake | A game. Draws a whole screen with cursor addressing and steers with single keys, asking the console once a frame and never waiting. |
| Keys | The console interrupting rather than being asked. The only one that brings a vector of its own, which is what the version two format exists for. |
| Play | Four voices on one clock, which is what music is and one channel cannot be. The timer keeps a tick and every voice keeps its own place in its own track and its own count of how much longer the note it is holding lasts, so the parts move at four different rates and share nothing but the beat. A track is pairs of bytes, what to play and how many ticks it lasts: 1 to 127 is a MIDI note, zero is a rest, and 255 ends it - MIDI stops at 127, so neither of those had to be invented. A note's duration is its whole life and the gate goes down when the count runs out, which means a gap between two notes is written as a rest rather than invented by the player out of some fraction it decided on. Each voice loads an instrument of its own before a note is played - an oboe for the melody, strings under it, a square wave for the bass and a kalimba for the arpeggio - out of the format SoundPatch writes, which the player reads as a count and that many parameter and value pairs and understands nothing else about. Four patches can be up at once because a patch belongs to its channel; Kalimba has an LFO switched off and the other three have one on, and under a device where the LFOs belonged to the whole machine the last patch loaded would have imposed its setting on every part. The tune is assembled in for now; reading one from a file is what makes it a player rather than a program with one song in it. It spends over ninety nine per cent of its time asleep, because a beat is something to be woken by rather than counted up to. |
| Play | Four voices on one clock, which is what music is and one channel cannot be. The timer keeps a tick and every voice keeps its own place in its own track and its own count of how much longer the note it is holding lasts, so the parts move at four different rates and share nothing but the beat. A track is pairs of bytes, what to play and how many ticks it lasts: 1 to 127 is a MIDI note, zero is a rest, and 255 ends it - MIDI stops at 127, so neither of those had to be invented. A note's duration is its whole life and the gate goes down when the count runs out, which means a gap between two notes is written as a rest rather than invented by the player out of some fraction it decided on. Each voice loads an instrument of its own before a note is played - an oboe for the melody, strings under it, a square wave for the bass and a kalimba for the arpeggio - out of the format SoundPatch writes, which the player reads as a count and that many parameter and value pairs and understands nothing else about. Four patches can be up at once because a patch belongs to its channel; Kalimba has an LFO switched off and the other three have one on, and under a device where the LFOs belonged to the whole machine the last patch loaded would have imposed its setting on every part. A voice does not play one long track: it walks an ORDER LIST of its own, a table of sequence addresses, and takes the next one when a sequence runs out. That is where repetition comes from and it costs no notation - the bass plays the same sequence in the first bar and the last, written once. The four columns are how it reads and how a tracker would show it; per voice is how it is stored, because a voice's order cursor is then a pointer it advances by itself. A sequence can also carry commands, which take no time at all: 0x80 plays the rest of that voice on a different patch, which is how the melody's last bar becomes a swell rather than a reed. Nothing keeps the voices together except that their sequences add up to the same length, which is the first thing a compiler should check. The tune is assembled in for now; reading one from a file is what makes it a player rather than a program with one song in it, and the engine that plays it is Libraries/player.asm rather than this program. It spends over ninety nine per cent of its time asleep, because a beat is something to be woken by rather than counted up to. |
| Say | Prints whatever it was told, which is the shortest thing that shows osArgument working. |
| Reboot | Starts the machine again, in 45 bytes. Writes a port rather than asking the system, because a reset has to work when the system does not. |
| Once | Asks the loader to start something else on the next start, and only that one, in 569 bytes. |
+209
View File
@@ -0,0 +1,209 @@
; player.asm
; Four voices on one clock: the part of a music player that is not the music.
; Written by Anachronaut
;
; A program that includes this supplies the tune - four order lists called Order0 to Order3,
; and the sequences they name - and gets the scheduler, the patch loader and the state that
; goes with them. It does NOT supply the beat: setting the timer up and waiting on it belongs
; to the program, because how long a piece rings at the end and what stops it are its business
; and not this file's.
;
; What a sequence is, what a command is and where repetition comes from are all described in
; Apps/Play.asm, which is the first thing to read this.
;
; The caller must define, in its Data Segment:
;
; Order0 Order1 Order2 Order3 a table of sequence addresses each, ending in 0xFFFF
;
; 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.
#Program
; ---- One voice, one tick ----
;
; A is the channel and DP1 is the voice's six bytes. Both survive the CALL that got here,
; which is what lets the caller say which voice it means in two instructions.
;
; +0 order cursor +2 sequence cursor +4 count +5 live
stepVoice:
SETD.3 ThisChannel
STA.3 ; A is wanted for other things between here and using it.
DPUP.1 0d5
LDA.1 ; Live?
DPDN.1 0d5
BRA stepDone
DPUP.1 0d4
LDA.1
DECA
STA.1 ; One tick less of whatever is sounding.
DPDN.1 0d4
BNA stepDone ; Still holding it.
; ---- The count ran out, so this is a boundary ----
;
; Select the channel FIRST. Every sound port below writes to whichever channel was last
; named, so a voice that forgot would be playing somebody else's part.
LDA.3
OUTA 0x41
RSTA
OUTA 0x45 ; Let go of the note that just ended.
DPUP.1 0d2
LDD.0.1 ; DP0 is now this voice's place in its sequence.
DPDN.1 0d2
; ---- Read events until one of them takes time ----
;
; A command takes none, and neither does the end of a sequence, so both come back here.
; A sequence of nothing but commands would spin in this loop for ever, which is a hang
; rather than a wrong note - the one malformed sequence worth a compiler refusing.
stepEvent:
LDA.0
INIB 0xFF
XOR ; XOR answers in Q and leaves A holding the event.
BRQ stepSequenceEnd
; The top bit says command, and 0xFF is already dealt with above.
INIB 0x80
AND
BNQ stepCommand
; ---- A note, or a rest, and how long it lasts ----
INCD.0
LDB.0
INCD.0
DPUP.1 0d2
STD.0.1 ; The sequence cursor, moved past this event.
DPUP.1 0d2
STB.1 ; And the count it will be held for.
; A is still the event. Zero is a rest, which is a duration with nothing started.
BRA stepDone
OUTA 0x44 ; Writing the note is what starts it.
stepDone:
RET
; ---- 0x80: play the rest of this voice on that patch ----
;
; The address follows the command. The sequence cursor is put away before the patch is loaded
; and taken out again after, because loadPatch walks DP0 and a CALL hands back the pointer it
; 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.
DPUP.1 0d2
STD.0.1
DPDN.1 0d2
PSHD.2
POPD.0 ; DP0 is the patch.
LDA.3 ; And A the channel, which loadPatch selects.
CALL loadPatch
DPUP.1 0d2
LDD.0.1 ; Back to where the sequence had got to.
DPDN.1 0d2
BRI stepEvent
; ---- 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.
;
; 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.
stepSequenceEnd:
LDD.0.1 ; DP0 is the order cursor, which is the first thing in the record.
LDA.0
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.
DPUP.1 0d2
STD.2.1 ; And the sequence cursor set to the new one.
DPDN.1 0d2
PSHD.2
POPD.0
BRI stepEvent ; Which has events in it, so read one.
stepOrderEnd:
; Nothing left for this voice. The gate is already down, so its last note is fading.
DPUP.1 0d5
RSTA
STA.1
SETD.3 Playing
LDA.3
DECA
STA.3
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
; one Lander already plays. B holds what is left, which costs nothing to keep - a CALL saves
; B, so a caller's count is not disturbed by a patch being loaded inside its loop.
;
; The channel is selected FIRST. Every parameter write below lands on whichever channel was
; named last, so a patch loaded without one would be quietly rewriting somebody else's voice.
loadPatch:
OUTA 0x41
LDB.0
INCD.0
loadPatchPair:
LDA.0
OUTA 0x42
INCD.0
LDA.0
OUTA 0x43
INCD.0
DECB
BNB loadPatchPair
RET
#Data
; Six bytes a voice: order cursor, sequence cursor, count, live.
;
; A LABEL WRITTEN HERE COMES OUT AS ITS ADDRESS, two bytes, most significant first - which is
; exactly the shape LDD reads. So a voice starts pointed at its order list and at Empty
; without a line of code: nothing relocates on this machine, so the address the assembler
; wrote is the address it will have.
;
; Empty is a sequence of nothing but its end marker, and the count starts at one. The first
; tick runs the count out, finds the end, and goes to the order list for the real first
; sequence - so the beginning of a piece needs no special case anywhere.
Voice0:
Order0 Empty 0d1 0d1
Voice1:
Order1 Empty 0d1 0d1
Voice2:
Order2 Empty 0d1 0d1
Voice3:
Order3 Empty 0d1 0d1
Playing:
0d4
ThisChannel:
0x00
; The sequence a voice starts on, so that its first tick goes through the order list like
; every other bar does.
Empty:
0xFF
+39 -76
View File
@@ -26,6 +26,7 @@
; Written by Anachronaut
#Program
start:
; A patch each, in the format SoundPatch writes: a count and that many pairs. They differ
; in ONE parameter - the octave - and the low one is loaded first, so if a patch belonged to
@@ -53,8 +54,8 @@ start:
OUTA 0x51
SIF
; Twelve ticks. B survives the calls below, because a CALL saves it.
INIB 0d12
; Sixteen ticks. B survives the calls below, because a CALL saves it.
INIB 0d16
tickLoop:
SETD.1 Voice0
RSTA
@@ -68,7 +69,7 @@ tickLoop:
BRI tickLoop
lastTick:
; The twelfth step started a note and nothing has waited for it yet. Without this the last
; The last step started a note and nothing has waited for it yet. Without this the final
; tick is never rendered, and a window measuring it runs off the end of the samples.
WAIT
@@ -78,86 +79,46 @@ done:
OUTA 0x51
HALT
; A is the channel and DP1 the voice's four bytes: cursor high, cursor low, count, spare.
; Both survive the CALL that got here.
stepVoice:
SETD.3 ThisChannel
STA.3
DPUP.1 0d2
LDA.1
DECA
STA.1 ; One tick less of whatever is sounding.
DPDN.1 0d2
BNA stepDone
; The count ran out. Select the channel first: every sound port writes to whichever channel
; was named last, so a voice that forgot would be playing somebody else's part.
LDA.3
OUTA 0x41
RSTA
OUTA 0x45 ; Let go of the note that just ended.
LDD.0.1 ; DP0 is this voice's place in its track.
LDA.0
INCD.0
LDB.0 ; How many ticks it lasts.
INCD.0
STD.0.1
DPUP.1 0d2
STB.1
; A is still the note. Zero is a rest: the ticks pass with nothing started.
BRA stepDone
OUTA 0x44
stepDone:
RET
; ---- A patch, onto the channel named by A ----
;
; The channel is selected first: every parameter write lands on whichever channel was named
; last, so a patch loaded without one would be rewriting somebody else's voice.
loadPatch:
OUTA 0x41
LDB.0
INCD.0
loadPatchPair:
LDA.0
OUTA 0x42
INCD.0
LDA.0
OUTA 0x43
INCD.0
DECB
BNB loadPatchPair
RET
tick:
RETI
#Data
; Cursor high, cursor low, count, spare. A label written here comes out as its address, most
; significant first, which is the shape LDD reads - so a voice starts pointed at its track
; with no code at all. The count starts at one so the first pass runs it out and fetches.
Voice0:
Track0 0d1 0d0
Voice1:
Track1 0d1 0d0
ThisChannel:
0x00
; Middle C, then out of the way, then back - against the octave above doing the opposite.
Track0:
0d60 0d4
; ---- The order lists, and what they are for ----
;
; Four bars of four ticks each, and the SAME TWO SEQUENCES do nearly all of it. Beat is used
; twice by voice 0 and twice by voice 1 - four placings of one phrase, written once, and on
; two different voices, because the voice is a property of where a sequence is placed and not
; of the sequence.
;
; ticks 0-3 4-7 8-11 12-15
; voice 0 Beat Quiet Beat Switch
; voice 1 Quiet Beat Beat (its order list ends here)
Order0:
Beat Quiet Beat Switch Tail 0xFF 0xFF
Order1:
Quiet Beat Beat 0xFF 0xFF ; And this one ENDS, so the last bar has voice 1 stopped rather
; than resting. A player that missed the terminator would read
; whatever follows as a sequence address and play it.
Order2:
Tail 0xFF 0xFF
Order3:
Tail 0xFF 0xFF
Beat:
0d60 0d4 ; Note 60 on both voices. The octave between them is their patches.
0xFF
Quiet:
0d00 0d4
0d60 0d4
0d00 0xFF
Track1:
0d00 0d4
0d60 0d4
0d60 0d4
0d00 0xFF
0xFF
Switch:
0x80 PatchHigh ; And voice 0 takes voice 1's instrument, mid-piece.
0d60 0d4 ; The same note number it has played all along.
0xFF
Tail:
0d00 0xFF ; Long enough that nothing runs off the end of its order list.
0xFF
; Seven pairs each, and only the last of them differs: the octave, centred on 128. So the two
; voices below are asked for THE SAME NOTE NUMBER and answer an octave apart, which nothing
@@ -186,6 +147,8 @@ PatchHigh:
0x23 0d5
0x04 0d129 ; And this one an octave up.
#Include player.asm
#Vectors
Boot start
Device 0x50 tick