Files
SplitBit-Emulator/Programs/CosmOS/Apps/Play.asm
T
AnachronautandClaude Opus 5 41d65d6346 Play waits for its own sound to finish before handing back
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
2026-09-05 16:13:10 -04:00

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