; 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. ; ; The counts start at one rather than zero, so the first pass through the loop runs every ; voice's count out and fetches its first event. Nothing has to be special-cased for the ; beginning, and the piece starts on the tick rather than one after it. #Include services.asm #Program #Base 0x5000 start: ; ---- A sound to play it with ---- ; ; The same on all four channels, which is exactly what M3 replaces: a patch per voice, out ; of the format SoundPatch already writes. Until then they are four of the same instrument, ; which is enough to hear four parts and not enough to enjoy them. RSTA CALL setUpChannel INIA 0d1 CALL setUpChannel INIA 0d2 CALL setUpChannel INIA 0d3 CALL setUpChannel 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 voices, pointed at their tracks ---- ; ; The cursors are written here rather than assembled into the table, because a label used ; as data does not come out as its address. SETD.1 Voice0 SETD.0 Track0 CALL startVoice SETD.1 Voice1 SETD.0 Track1 CALL startVoice SETD.1 Voice2 SETD.0 Track2 CALL startVoice SETD.1 Voice3 SETD.0 Track3 CALL startVoice INIA 0d4 SETD.1 Playing STA.1 ; Four tracks still running. ; ---- 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 ring rather than cutting it off, then give the timer back - stopped ; before the handler goes away, because an interrupt with nothing to catch it is a fault. INIB 0d8 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 ; DP1 is a voice and DP0 its track. Sets the cursor, and a count of one so that the first ; pass through the loop fetches the first event. startVoice: STD.0.1 DPUP.1 0d2 INIA 0d1 STA.1 ; Count: one tick, which runs out immediately. INCD.1 STA.1 ; Live. RET ; A plucked sound, on the channel named by A. setUpChannel: OUTA 0x41 RSTA OUTA 0x42 INIA 0d2 OUTA 0x43 ; A saw, which has harmonics enough to hear a filter on. INIA 0x01 OUTA 0x42 INIA 0xFF OUTA 0x43 ; Oscillator 0 at full gain INIA 0x05 OUTA 0x42 INIA 0x01 OUTA 0x43 ; and switched on, which is a separate write and the one that ; matters: a gain on an oscillator that is off does nothing. INIA 0x20 OUTA 0x42 INIA 0d8 OUTA 0x43 ; Almost no attack INIA 0x21 OUTA 0x42 INIA 0d140 OUTA 0x43 ; a long decay INIA 0x22 OUTA 0x42 INIA 0d60 OUTA 0x43 ; held quietly rather than at nothing, so a long note is still ; there when the next one starts INIA 0x23 OUTA 0x42 INIA 0d40 OUTA 0x43 ; and a short release when the gate drops. 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. Voice0: 0x00 0x00 0x00 0x00 Voice1: 0x00 0x00 0x00 0x00 Voice2: 0x00 0x00 0x00 0x00 Voice3: 0x00 0x00 0x00 0x00 Playing: 0x00 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 #Vectors Boot start Device 0x50 tick