; 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 ; ; and point each voice's order cursor at a list of one-byte sequence indices ending in 0xFF. ; A tune read from a file needs none of that: useTune does all of it out of the header. ; ; It 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 LDD.2.2 ; The table, wherever this tune put it. 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 LDD.2.2 ; The table, wherever this tune put it. DPUA.2 DPUA.2 LDD.2.2 ; And 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 VoiceStartAt LDD.2.2 DPUA.2 LDA.2 ; Which patch this voice starts on. SETD.2 PatchTable LDD.2.2 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 ; ---- base + offset, into wherever the caller wants it ---- ; ; DP0 names two bytes of offset and DP1 two bytes to put the address in. They may be the same ; place, which is how a table is relocated in position: the low byte is written before the ; high byte is read, so nothing is clobbered under itself. ; ; The low bytes carry into the high ones, which is the whole reason ADD takes the Carry Flag. tuneAddr: SETD.2 TuneBase CCF INCD.0 LDA.0 ; The offset, low byte. INCD.2 LDB.2 ; And the base, low byte. ADD INCD.1 STQ.1 DECD.0 LDA.0 ; The offset, high byte. DECD.2 LDB.2 ADD ; With the carry the low bytes made. DECD.1 STQ.1 RET ; Every entry of the table at DP0 turned from an offset into an address. B is how many. tuneReloc: PSHD.0 POPD.1 CALL tuneAddr ; A CALL hands DP0 and DP1 back, so both are still the entry. INCD.0 INCD.0 DECB BNB tuneReloc RET ; ---- A tune, from wherever it was put ---- ; ; DP0 names its first byte. EVERYTHING IN A TUNE IS AN OFFSET FROM THERE, so this adds the ; base to the two tables and the four order lists and nothing else in the file is touched. No ; sequence is walked, and nothing inside one is an address to be found and corrected - which ; is what makes a malformed tune something that plays wrongly rather than something that takes ; the loader with it. ; ; Q is nought if the tune was taken, and anything else if the file was not one. useTune: SETD.1 TuneBase STD.0.1 ; "SBTU", and version one. A file that is not a tune has to be refused here, because ; everything below reads offsets out of it and jumps to what they name. SETD.1 TuneMagic INIB 0d5 useTuneMagic: LDA.0 PSHB LDB.1 XOR POPB BNQ useTuneNo INCD.0 INCD.1 DECB BNB useTuneMagic ; The tick, straight into the timer. Three bytes, most significant first, which is the ; order the ports take them in. LDA.0 OUTA 0x52 INCD.0 LDA.0 OUTA 0x53 INCD.0 LDA.0 OUTA 0x54 ; How many of each, kept before the pointers move. INCD.0 LDA.0 SETD.1 TunePatches STA.1 INCD.0 LDA.0 SETD.1 TuneSequences STA.1 ; The two tables. INCD.0 SETD.1 PatchTable CALL tuneAddr INCD.0 INCD.0 SETD.1 SequenceTable CALL tuneAddr ; And an order list each. INCD.0 INCD.0 SETD.1 Voice0 CALL tuneAddr INCD.0 INCD.0 SETD.1 Voice1 CALL tuneAddr INCD.0 INCD.0 SETD.1 Voice2 CALL tuneAddr INCD.0 INCD.0 SETD.1 Voice3 CALL tuneAddr ; The starting instruments are not an offset but a place IN the file, so they are found by ; counting rather than by adding. SETD.0 TuneBase LDD.0.0 DPUP.0 0d22 SETD.1 VoiceStartAt STD.0.1 ; Now the tables themselves, whose entries are offsets like everything else. SETD.0 PatchTable LDD.0.0 SETD.1 TunePatches LDB.1 CALL tuneReloc SETD.0 SequenceTable LDD.0.0 SETD.1 TuneSequences LDB.1 CALL tuneReloc CALL startVoices ; Q is nought, which is how this says it worked. There is no instruction that sets Q: it is ; the ALU's output and nothing else, so saying nought means doing a sum that comes to it. RSTA RSTB XOR RET useTuneNo: ; Q is already not nought, because that is what got here. RET ; ---- Four voices at the beginning of their order lists ---- ; ; Whoever supplied the tune has set the order cursors; this sets everything else. A voice ; starts on Empty with a count of one, so its first tick runs the count out, finds the end of ; a sequence, and goes to the order list for the real first one. The beginning of a piece ; needs no special case anywhere. startVoices: SETD.1 Voice0 CALL startOneVoice SETD.1 Voice1 CALL startOneVoice SETD.1 Voice2 CALL startOneVoice SETD.1 Voice3 CALL startOneVoice INIA 0d4 SETD.1 Playing STA.1 CALL loadStartPatches RET startOneVoice: SETD.0 Empty DPUP.1 0d2 STD.0.1 INCD.1 INCD.1 INIA 0d1 STA.1 ; A count of one, which runs out on the first tick. INCD.1 STA.1 ; And live. 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: NoOrder Empty 0d1 0d1 Voice1: NoOrder Empty 0d1 0d1 Voice2: NoOrder Empty 0d1 0d1 Voice3: NoOrder Empty 0d1 0d1 Playing: 0d4 ThisChannel: 0x00 ; Where this tune's tables are. Pointers rather than the tables themselves, because a tune ; read from a file puts them wherever it was put, and the engine must not care which. PatchTable: 0x00 0x00 SequenceTable: 0x00 0x00 VoiceStartAt: 0x00 0x00 TuneBase: 0x00 0x00 TunePatches: 0x00 TuneSequences: 0x00 TuneMagic: 0x53 0x42 0x54 0x55 0x01 ; "SBTU" and version one. ; An order list of nothing, which every voice starts pointed at. WHOEVER SUPPLIES THE TUNE ; SETS THE REAL ONE - useTune out of a file's header, or a program with a tune of its own in ; eight lines - so the records below name nothing that a caller has to have defined. They used ; to name Order0 to Order3, which made four order lists part of the contract even for a caller ; whose tune comes from a file and never has any. NoOrder: 0xFF ; The sequence a voice starts on, so that its first tick goes through the order list like ; every other bar does. Empty: 0xFF