The gates all go down on the tick the tracks end, but a gate down is a note RELEASED and not a note stopped: the oboe and the strings have a release left to run. Rendered and measured, the tail dies eight tenths of a second after the last event, and the ring was eight ticks - one second, which sounds like enough and was two tenths short. 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 becomes depends on the front end. Behind a window the tail finishes on its own. On a terminal it does not, because emulated time stops while the machine blocks on a key - measured, and the reason the leftover sound came out a snippet per keystroke rather than fading. Twelve ticks, which is a second and a half and covers the measured tail with room over. It is a number about THESE FOUR INSTRUMENTS: a patch with a longer release would want more, and the general answer is for the system to quieten the device when a program stops, the way it puts the screen back. That is not built and is worth deciding on rather than guessing at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
321 lines
10 KiB
NASM
321 lines
10 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 track 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
|
|
;
|
|
; 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.
|
|
;
|
|
; 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.
|
|
;
|
|
; ---- 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.
|
|
|
|
#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. This reads a count and that many parameter and
|
|
; value pairs and knows nothing else about them.
|
|
;
|
|
; 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.
|
|
SETD.0 OboePatch
|
|
RSTA
|
|
CALL loadPatch ; The melody sings, so it is the one with a voice.
|
|
SETD.0 StringsPatch
|
|
INIA 0d1
|
|
CALL loadPatch ; A pad underneath it.
|
|
SETD.0 SquarePatch
|
|
INIA 0d2
|
|
CALL loadPatch ; And a square wave for the bass, which is where one belongs.
|
|
SETD.0 KalimbaPatch
|
|
INIA 0d3
|
|
CALL loadPatch ; Plucked, for the arpeggio.
|
|
|
|
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
|
|
|
|
; ---- 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
|
|
; timer would have to read 0x50 instead.
|
|
tick:
|
|
RETI
|
|
|
|
#Data
|
|
|
|
#Base 0x3000
|
|
|
|
; Four bytes a voice: cursor high, cursor low, 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 track without a line of code:
|
|
; nothing relocates on this machine, so the address the assembler wrote is the address it
|
|
; will have.
|
|
;
|
|
; 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
|
|
|
|
; ---- 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.
|
|
|
|
; The melody, in quarters and halves.
|
|
Track0:
|
|
0d64 0d4 ; E
|
|
0d67 0d4 ; G
|
|
0d72 0d8 ; C, held
|
|
0d65 0d4 ; F
|
|
0d69 0d4 ; A
|
|
0d72 0d8 ; C
|
|
0d62 0d4 ; D
|
|
0d67 0d4 ; G
|
|
0d71 0d8 ; B
|
|
0d72 0d16 ; and home
|
|
0xFF
|
|
|
|
; A second part underneath it, in halves.
|
|
Track1:
|
|
0d60 0d8 ; C
|
|
0d64 0d8 ; E
|
|
0d57 0d8 ; A
|
|
0d60 0d8 ; C
|
|
0d59 0d8 ; B
|
|
0d62 0d8 ; D
|
|
0d64 0d16 ; E
|
|
0xFF
|
|
|
|
; The bass, one note to a bar.
|
|
Track2:
|
|
0d48 0d16 ; C
|
|
0d41 0d16 ; F
|
|
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:
|
|
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
|
|
|
|
; 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
|