; 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: ; ; PatchTable one two-byte address per patch ; SequenceTable one two-byte address per sequence ; VoiceStart four bytes, the patch index each voice starts on ; Order0..Order3 one-byte sequence indices each, ending in 0xFF ; ; and must call loadStartPatches once, then 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. ; ; INDICES THROUGH TABLES, RATHER THAN ADDRESSES, because that is the shape a tune read from a ; file has to be: the two tables are the only places an address lives, so loading a tune means ; adding a base to them and nothing else. Keeping the assembled-in form the same shape is what ; makes reading one from a file change no code here at all. #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 LDA.0 ; Which patch: one byte of index, not an address. INCD.0 ; Past it, on whatever comes next. DPUP.1 0d2 STD.0.1 DPDN.1 0d2 SETD.2 PatchTable DPUA.2 DPUA.2 ; Twice, because an entry is two bytes. LDD.2.2 ; And DP2 follows the address it is now holding. 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 ---- ; ; AN ORDER LIST IS INDICES, one byte each, ending in 0xFF - the same byte that ends a sequence. ; SequenceTable is the only place a sequence's address lives. ; ; That is what makes a tune loadable without walking it: nothing inside a sequence or an order ; list is an address, so putting one in memory means adding the load address to two tables and ; nothing else. A loader that had to parse sequences looking for addresses to fix up is a ; loader that a malformed file can walk off a cliff. ; ; It was addresses here, terminated by a zero, on the reasoning that no sequence could live ; below this player's 0x3000 base. True of a loaded program and false of a boot image based 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. An address is not a good place to hide a flag unless the ; address is impossible in every program rather than in this one. stepSequenceEnd: LDD.0.1 ; DP0 is the order cursor, which is the first thing in the record. LDA.0 ; Which sequence comes next. INIB 0xFF XOR BRQ stepOrderEnd INCD.0 STD.0.1 ; The order cursor, moved past this entry. SETD.2 SequenceTable DPUA.2 DPUA.2 LDD.2.2 ; DP2 is the sequence that index names. 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 ; ---- The instrument each voice starts on ---- ; ; STARTING STATE IS DECLARED RATHER THAN ASSUMED. A voice whose instrument was never said ; would play on whatever the device woke up with, or worse on whatever the last tune left - ; and that is a fault this machine has met elsewhere, where a program run a second time starts ; with the memory the first run left, because loading is what initialises and running is not. loadStartPatches: RSTA loadStartOne: SETD.3 ThisChannel STA.3 SETD.2 VoiceStart DPUA.2 LDA.2 ; Which patch this voice starts on. SETD.2 PatchTable DPUA.2 DPUA.2 LDD.2.2 PSHD.2 POPD.0 LDA.3 CALL loadPatch ; A survives a CALL, so it is still the channel below. INCA INIB 0d4 CCF SUB BNQ loadStartOne 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