; 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