; tune.asm ; Playing a melody, which needs a sound device and a clock and has neither by halves. ; Written by Anachronaut ; ; ---- Two devices, because one is not enough ---- ; ; The sound device knows how to make a note and knows nothing about when. It has no timer and ; does not interrupt, so a program that only had the sound device could play a tune at ; whatever speed the machine happened to run at, which is not a tune. ; ; The timer is the other half. A period in cycles, a repeat bit, and a line when one has gone ; by: a beat this program SETS rather than one it borrows. Every duration below is a count of ; ticks, and the tick is named in the machine's own cycles, so this plays at the same speed ; whether the emulator is running at a megahertz or as fast as it can go. ; ; ---- The tick is a sixteenth note, and that is the whole point ---- ; ; 125,000 cycles, which is a sixteenth note at 120 beats a minute. ; ; This program used to count the SCREEN's frames, because until the timer existed the frame ; was the only regular beat on the machine. A frame is 16,667 cycles, so a sixteenth note is ; seven and a half of them and could not be asked for at all. The arpeggio below was written ; as seven frames, the nearest whole one, which is six and a half per cent fast; and the way ; round the rest of it was to pick a tempo whose subdivisions happened to land on whole ; frames, which is making the tune fit the machine rather than the other way round. ; ; SO THE DURATIONS HERE ARE NOT A CONVERSION OF THE OLD ONES. They are what the music wanted ; in the first place, now that it can be written down: an eighth is two ticks, a quarter is ; four, and the arpeggio is one each rather than a fast approximation of one. ; ; The reason the screen should not have been the clock is not that a frame is slow. It is that ; a display refresh and a music routine have no reason to share a rate. ; ; ---- What a patch costs and what a note costs ---- ; ; Setting the sound up is twenty-odd writes, done once before a single note is played. After ; that the inner loop is two: the note, and letting go of it. That split is what the selector ; and value registers are for - see Making A Noise in the Programming Manual. #Include services.asm #Program #Base 0x5000 start: ; ---- The instrument ---- ; ; Channel 0, selected once. Every parameter write below lands on it. RSTA OUTA 0x41 ; A saw wave, which has all the harmonics and so is the one to hear a filter on. Writing a ; port leaves A alone, so the nothing that selected the channel also selects parameter 0, ; which is oscillator 0's waveform. SplitLint will point out any attempt to put it there ; twice. OUTA 0x42 INIA 0d2 ; Saw OUTA 0x43 INIA 0x01 ; Oscillator 0, gain OUTA 0x42 INIA 0xFF ; All of it. A channel arrives at full gain already, so this is OUTA 0x43 ; saying so rather than changing it. ; A second oscillator a little out of tune with the first, which is the oldest trick there ; is for making one voice sound like more than one. ; ; SWITCHING IT ON IS A SEPARATE WRITE from setting its gain, and it is the one that matters: ; the two oscillators are averaged rather than added, so `active` is structural. Setting a ; gain on an oscillator that is off does nothing at all, silently, which is how the first ; draft of this program came to have a detune in it that could not be heard. INIA 0x15 ; Oscillator 1, on OUTA 0x42 INIA 0x01 OUTA 0x43 INIA 0x11 ; Oscillator 1, gain OUTA 0x42 INIA 0xC0 OUTA 0x43 INIA 0x13 ; Oscillator 1, detune OUTA 0x42 INIA 0d129 ; Centred on 128, and a step is about nine cents, so this is ; nine cents sharp - a shimmer rather than a wrong note. OUTA 0x43 ; Plucked: no attack to speak of, most of a second of decay, and nothing held. ; ; A sustain of nothing does NOT end the note. It goes quiet and keeps sounding, because a ; voice holding at nothing is what a held key is. Dropping the gate is the only thing that ; ends a note, which is why the loop below does it whether the sound has faded or not. INIA 0x20 ; Amplitude envelope, attack OUTA 0x42 INIA 0d10 OUTA 0x43 INIA 0x21 ; Decay OUTA 0x42 INIA 0d120 OUTA 0x43 INIA 0x22 ; Sustain: nothing OUTA 0x42 RSTA OUTA 0x43 INIA 0x23 ; Release OUTA 0x42 INIA 0d40 OUTA 0x43 ; A low pass with the modulation envelope opening it, so each note starts bright and closes ; down. This is what the second envelope is for, and it can only be spent this way because ; the level is shaped by the first one and not by whichever happens to be wired to the ; output. INIA 0x40 ; Filter, on OUTA 0x42 INIA 0x01 OUTA 0x43 INIA 0x42 ; Cutoff, low to start with OUTA 0x42 INIA 0d90 OUTA 0x43 INIA 0x43 ; A little resonance, to hear it move OUTA 0x42 INIA 0d150 OUTA 0x43 INIA 0x44 ; What opens it: the modulation envelope OUTA 0x42 INIA 0d2 OUTA 0x43 INIA 0x45 ; And how far, upwards from centre OUTA 0x42 INIA 0d220 OUTA 0x43 INIA 0x31 ; That envelope's decay, which is the sweep's length OUTA 0x42 INIA 0d70 OUTA 0x43 INIA 0x32 ; and it closes all the way OUTA 0x42 RSTA OUTA 0x43 INIA 0xC0 ; The device's volume, with room left over the top OUTA 0x46 ; ---- The beat ---- ; ; The period first and the control byte second, because writing the control byte with the ; run bit set is what LOADS the period. Doing it the other way round starts a timer on ; whatever it was holding before. INIA 0x01 OUTA 0x52 INIA 0xE8 OUTA 0x53 INIA 0x48 OUTA 0x54 ; 0x01E848 is 125,000. INIA 0x07 OUTA 0x51 ; Run, repeat, interrupt. SIF ; ---- The tune ---- ; ; Data Pointer 0 walks the table, and nothing in this loop is a CALL, so it stays where it ; was left without being saved anywhere. SETD.0 Theme nextNote: LDA.0 ; The note. Zero is the end of the tune. BRA finished OUTA 0x44 ; Writing the note is what starts it. INCD.0 LDB.0 ; How many ticks it lasts. INCD.0 holdNote: WAIT ; Nothing at all until the timer says a tick has gone by. DECB BNB holdNote ; Let go. The note is already fading on its own decay, but dropping the gate is what a ; keyboard does and what the release time is waiting for. RSTA OUTA 0x45 BRI nextNote finished: ; Let the last note ring out rather than cutting it off, then give the timer back - stopping ; it before taking away what catches it, because this program is about to stop existing and ; an interrupt with no handler installed is a fault. INIB 0d6 lastRing: WAIT DECB BNB lastRing CIF RSTA OUTA 0x51 SWI osExit ; 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. ; ; Nothing reads the status port here, and that is not an oversight. Taking the interrupt is ; what brings the line down. A program that POLLED the timer instead would have to read 0x50, ; because looking at it is the only thing that answers a tick nobody was interrupted by. tick: RETI #Data #Base 0x3000 ; ---- Notes and how long they last ---- ; ; Pairs: a MIDI note, then a count of ticks. 60 is middle C and every 12 is an octave. A zero ; note ends it, which is why there are no rests in here - a rest would want a duration with no ; note, and this table has no way to say that. Adding one is a byte of flag or a note number ; nothing plays, and this program did not need it. Tune: 0d60 0d2 ; C 0d64 0d2 ; E 0d67 0d2 ; G 0d72 0d4 ; C, an octave up, held twice as long 0d71 0d2 ; B 0d67 0d2 ; G 0d64 0d2 ; E 0d60 0d6 ; and home 0x00 Theme: 0x30 0d2 ; C 0x35 0d2 ; F 0x3C 0d2 ; C+ 0x40 0d4 ; E+ 0x48 0d4 ; C++ 0x45 0d4 ; A+ 0x47 0d4 ; B+ 0x43 0d6 ; G+ ; repeat four times. 0x2C 0d1 ; Gs 0x33 0d1 ; Ef 0x3C 0d1 ; C+ ; 0x2C 0d1 ; Gs 0x33 0d1 ; Ef 0x3C 0d1 ; C+ ; 0x2C 0d1 ; Gs 0x33 0d1 ; Ef 0x3C 0d1 ; C+ ; 0x2C 0d1 ; Gs 0x33 0d1 ; Ef 0x3C 0d1 ; C+ ; next chord 0x2E 0d1 ; Bf 0x35 0d1 ; F 0x3E 0d1 ; D+ ; next chord 0x2E 0d1 ; Bf 0x35 0d1 ; F 0x3E 0d1 ; D+ ; ; next chord 0x2E 0d1 ; Bf 0x35 0d1 ; F 0x3E 0d1 ; D+ ; ; next chord 0x2E 0d1 ; Bf 0x35 0d1 ; F 0x3E 0d1 ; D+ ; Finally on the C major 0x30 0d1 ; C 0x37 0d1 ; G 0x40 0d1 ; E+ ; 0x30 0d1 ; C 0x37 0d1 ; G 0x40 0d1 ; E+ ; 0x30 0d1 ; C 0x37 0d1 ; G 0x40 0d1 ; E+ ; 0x30 0d1 ; C 0x37 0d1 ; G 0x40 0d1 ; E+ ; 0x18 0d8 ; C bass 0x00 #Vectors Boot start Device 0x50 tick