diff --git a/Programs/CosmOS/Apps/Play.asm b/Programs/CosmOS/Apps/Play.asm index ad2e7b4..fb6058a 100644 --- a/Programs/CosmOS/Apps/Play.asm +++ b/Programs/CosmOS/Apps/Play.asm @@ -14,29 +14,71 @@ ; 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 ---- +; ---- What a sequence 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 +; 0x80 to 0xFE a command, which takes no time at all. See below. +; 0xFF the sequence 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. +; MIDI NOTES ONLY REACH 127, so the top of the byte was free and none of that 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 make 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. +; 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. +; +; ---- Commands, of which there is one ---- +; +; A command consumes no tick: the reader acts on it and reads the next event straight away, so +; commands sit BETWEEN notes in time rather than needing a place of their own. +; +; 0x80
play the rest of this voice on that patch. +; +; A patch change reshapes whatever is still ringing on the voice, and nothing can be done +; about that here: a channel has one set of parameters and a note in its release is using +; them. Deferring the load to the next note would reshape the same tail. So it is a fact about +; the hardware rather than a choice about the format, and the cure is a rest long enough for +; the release, which is the composer's to write. +; +; ONE COMMAND, and the other 125 values left alone. A tempo change, a volume ramp and a note +; slide are all easy to add and impossible to remove, and no piece of music has asked for one. +; +; ---- Sequences, and the order they go in ---- +; +; A sequence is one voice's phrase. Each voice has an ORDER LIST of its own - a table of +; sequence addresses, ending in a zero - and when a sequence runs out the voice takes the next +; address from it. The voice stops when the list does. +; +; THAT IS WHERE REPETITION COMES FROM, and it costs no notation: the bass below plays the same +; sequence in the first bar and the last, and it is written once. A thirty-two bar piece that +; reuses four phrases is four phrases and a list. +; +; PER VOICE RATHER THAN ONE SHARED LIST, because that is the shape this machine likes. A +; voice's order cursor is a pointer it advances by itself, which is the same LDD and STD move +; everything else here makes; a shared table of four-column rows would have every voice +; indexing into one place, which is a worse fit for a machine with four data pointers. The +; four columns are how it READS, and how a tracker would show it; they are not how it is +; stored. Nothing keeps the voices together except that their sequences add up to the same +; length, which is a thing a compiler can check and a hand-written tune has to get right. +; +; A voice silent through a bar has a sequence that rests for it. There is no need for a way to +; say "nothing here" when a rest already says it. ; ; ---- 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. +; Six bytes a voice: two of order cursor, two of sequence cursor, one of count, one of live. +; Both cursors are two bytes and at the front because that is what LDD and STD move - a +; pointer through a pointer, which is the whole reason this is a loop over four voices instead +; of the same code written out four times. +; +; A voice starts pointed at Empty, which is a sequence of nothing but its end marker. The +; first tick runs its count out, finds the end, and goes to the order list for the real first +; sequence - so the beginning needs no special case at all. #Include services.asm @@ -140,89 +182,6 @@ lastRing: 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 - -; ---- 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 - ; 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 @@ -234,78 +193,104 @@ tick: #Base 0x3000 -; Four bytes a voice: cursor high, cursor low, count, live. +; ---- The order lists ---- ; -; 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 track without a line of code: -; nothing relocates on this machine, so the address the assembler wrote is the address it -; will have. +; A table of sequence addresses each, ending in a zero. Read the four of them across and they +; are the bars; read one down and it is a part. ; -; The count starts at one rather than zero so that the first pass through the loop runs it out -; and fetches the first event. Nothing has to be special-cased for the beginning, and the -; piece starts on the tick rather than one after it. -Voice0: - Track0 0d1 0d1 -Voice1: - Track1 0d1 0d1 -Voice2: - Track2 0d1 0d1 -Voice3: - Track3 0d1 0d1 - -Playing: - 0d4 -ThisChannel: - 0x00 +; bar 1 2 3 4 +; melody Mel1 Mel2 Mel3 Mel4 +; harmony Har1 Har2 Har3 Har4 +; bass BassC BassF BassG BassC +; arp ArpC ArpF ArpG Silent +; +; THE BASS PLAYS BassC TWICE and it is written once. That is the whole return on having an +; order list, and it is worth noticing how little it cost: a table of labels the assembler +; fills in, and ten instructions in stepVoice. +Order0: + Mel1 Mel2 Mel3 Mel4 0xFF 0xFF +Order1: + Har1 Har2 Har3 Har4 0xFF 0xFF +Order2: + BassC BassF BassG BassC 0xFF 0xFF +Order3: + ArpC ArpF ArpG Silent 0xFF 0xFF ; ---- 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. +; A bar is sixteen ticks, and every sequence in a column adds up to sixteen. Nothing enforces +; that here: get one wrong and the voices come apart, quietly, some bars later. It is the +; first thing a compiler should check. ; The melody, in quarters and halves. -Track0: +Mel1: 0d64 0d4 ; E 0d67 0d4 ; G 0d72 0d8 ; C, held + 0xFF +Mel2: 0d65 0d4 ; F 0d69 0d4 ; A 0d72 0d8 ; C + 0xFF +Mel3: 0d62 0d4 ; D 0d67 0d4 ; G 0d71 0d8 ; B - 0d72 0d16 ; and home + 0xFF +Mel4: + 0x80 StringsPatch ; The last bar is a swell rather than a reed. + 0d72 0d16 ; and home 0xFF ; A second part underneath it, in halves. -Track1: +Har1: 0d60 0d8 ; C 0d64 0d8 ; E + 0xFF +Har2: 0d57 0d8 ; A 0d60 0d8 ; C + 0xFF +Har3: 0d59 0d8 ; B 0d62 0d8 ; D + 0xFF +Har4: 0d64 0d16 ; E 0xFF -; The bass, one note to a bar. -Track2: +; The bass, one note to a bar - and the first bar's is the last bar's. +BassC: 0d48 0d16 ; C + 0xFF +BassF: 0d41 0d16 ; F + 0xFF +BassG: 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: +ArpC: 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 +ArpF: + 0d77 0d2 0d81 0d2 0d84 0d2 0d81 0d2 + 0d77 0d2 0d81 0d2 0d84 0d2 0d81 0d2 + 0xFF +ArpG: + 0d79 0d2 0d83 0d2 0d86 0d2 0d83 0d2 + 0d79 0d2 0d83 0d2 0d86 0d2 0d83 0d2 + 0xFF +Silent: + 0d00 0d16 ; Out for the last bar, so it is the other three. + 0xFF + +; The scheduler, the patch loader and the state a voice keeps. The tune above is this +; program's; everything that plays it is shared with whatever else wants to. +#Include player.asm ; The instruments, at the bottom because each brings its own #Data and a base has to come ; before anything is in the segment it bases. diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index e101399..039a1a9 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -720,7 +720,7 @@ from every assembly file in it. Several are old programs written for the bare ma | Life | Conway's Game of Life, which had to be taught to stop, since a program that never ends takes the shell with it. Polls the console between generations. | | Snake | A game. Draws a whole screen with cursor addressing and steers with single keys, asking the console once a frame and never waiting. | | Keys | The console interrupting rather than being asked. The only one that brings a vector of its own, which is what the version two format exists for. | -| Play | Four voices on one clock, which is what music is and one channel cannot be. The timer keeps a tick and every voice keeps its own place in its own track and its own count of how much longer the note it is holding lasts, so the parts move at four different rates and share nothing but the beat. A track is pairs of bytes, what to play and how many ticks it lasts: 1 to 127 is a MIDI note, zero is a rest, and 255 ends it - MIDI stops at 127, so neither of those had to be invented. A note's duration is its whole life and the gate goes down when the count runs out, which means a gap between two notes is written as a rest rather than invented by the player out of some fraction it decided on. Each voice loads an instrument of its own before a note is played - an oboe for the melody, strings under it, a square wave for the bass and a kalimba for the arpeggio - out of the format SoundPatch writes, which the player reads as a count and that many parameter and value pairs and understands nothing else about. Four patches can be up at once because a patch belongs to its channel; Kalimba has an LFO switched off and the other three have one on, and under a device where the LFOs belonged to the whole machine the last patch loaded would have imposed its setting on every part. The tune is assembled in for now; reading one from a file is what makes it a player rather than a program with one song in it. It spends over ninety nine per cent of its time asleep, because a beat is something to be woken by rather than counted up to. | +| Play | Four voices on one clock, which is what music is and one channel cannot be. The timer keeps a tick and every voice keeps its own place in its own track and its own count of how much longer the note it is holding lasts, so the parts move at four different rates and share nothing but the beat. A track is pairs of bytes, what to play and how many ticks it lasts: 1 to 127 is a MIDI note, zero is a rest, and 255 ends it - MIDI stops at 127, so neither of those had to be invented. A note's duration is its whole life and the gate goes down when the count runs out, which means a gap between two notes is written as a rest rather than invented by the player out of some fraction it decided on. Each voice loads an instrument of its own before a note is played - an oboe for the melody, strings under it, a square wave for the bass and a kalimba for the arpeggio - out of the format SoundPatch writes, which the player reads as a count and that many parameter and value pairs and understands nothing else about. Four patches can be up at once because a patch belongs to its channel; Kalimba has an LFO switched off and the other three have one on, and under a device where the LFOs belonged to the whole machine the last patch loaded would have imposed its setting on every part. A voice does not play one long track: it walks an ORDER LIST of its own, a table of sequence addresses, and takes the next one when a sequence runs out. That is where repetition comes from and it costs no notation - the bass plays the same sequence in the first bar and the last, written once. The four columns are how it reads and how a tracker would show it; per voice is how it is stored, because a voice's order cursor is then a pointer it advances by itself. A sequence can also carry commands, which take no time at all: 0x80 plays the rest of that voice on a different patch, which is how the melody's last bar becomes a swell rather than a reed. Nothing keeps the voices together except that their sequences add up to the same length, which is the first thing a compiler should check. The tune is assembled in for now; reading one from a file is what makes it a player rather than a program with one song in it, and the engine that plays it is Libraries/player.asm rather than this program. It spends over ninety nine per cent of its time asleep, because a beat is something to be woken by rather than counted up to. | | Say | Prints whatever it was told, which is the shortest thing that shows osArgument working. | | Reboot | Starts the machine again, in 45 bytes. Writes a port rather than asking the system, because a reset has to work when the system does not. | | Once | Asks the loader to start something else on the next start, and only that one, in 569 bytes. | diff --git a/Programs/Libraries/player.asm b/Programs/Libraries/player.asm new file mode 100644 index 0000000..07dc91c --- /dev/null +++ b/Programs/Libraries/player.asm @@ -0,0 +1,209 @@ +; 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 + diff --git a/Programs/testPrograms/fourVoiceTest.asm b/Programs/testPrograms/fourVoiceTest.asm index e887cf3..da47073 100644 --- a/Programs/testPrograms/fourVoiceTest.asm +++ b/Programs/testPrograms/fourVoiceTest.asm @@ -26,6 +26,7 @@ ; Written by Anachronaut #Program + start: ; A patch each, in the format SoundPatch writes: a count and that many pairs. They differ ; in ONE parameter - the octave - and the low one is loaded first, so if a patch belonged to @@ -53,8 +54,8 @@ start: OUTA 0x51 SIF - ; Twelve ticks. B survives the calls below, because a CALL saves it. - INIB 0d12 + ; Sixteen ticks. B survives the calls below, because a CALL saves it. + INIB 0d16 tickLoop: SETD.1 Voice0 RSTA @@ -68,7 +69,7 @@ tickLoop: BRI tickLoop lastTick: - ; The twelfth step started a note and nothing has waited for it yet. Without this the last + ; The last step started a note and nothing has waited for it yet. Without this the final ; tick is never rendered, and a window measuring it runs off the end of the samples. WAIT @@ -78,86 +79,46 @@ done: OUTA 0x51 HALT -; A is the channel and DP1 the voice's four bytes: cursor high, cursor low, count, spare. -; Both survive the CALL that got here. -stepVoice: - SETD.3 ThisChannel - STA.3 - - DPUP.1 0d2 - LDA.1 - DECA - STA.1 ; One tick less of whatever is sounding. - DPDN.1 0d2 - BNA stepDone - - ; The count ran out. Select the channel first: every sound port writes to whichever channel - ; was named last, 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. - - LDD.0.1 ; DP0 is this voice's place in its track. - LDA.0 - INCD.0 - LDB.0 ; How many ticks it lasts. - INCD.0 - STD.0.1 - DPUP.1 0d2 - STB.1 - - ; A is still the note. Zero is a rest: the ticks pass with nothing started. - BRA stepDone - OUTA 0x44 -stepDone: - RET - -; ---- A patch, onto the channel named by A ---- -; -; The channel is selected first: every parameter write lands on whichever channel was named -; last, so a patch loaded without one would be 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 - tick: RETI #Data -; Cursor high, cursor low, count, spare. A label written here comes out as its address, most -; significant first, which is the shape LDD reads - so a voice starts pointed at its track -; with no code at all. The count starts at one so the first pass runs it out and fetches. -Voice0: - Track0 0d1 0d0 -Voice1: - Track1 0d1 0d0 -ThisChannel: - 0x00 - ; Middle C, then out of the way, then back - against the octave above doing the opposite. -Track0: - 0d60 0d4 +; ---- The order lists, and what they are for ---- +; +; Four bars of four ticks each, and the SAME TWO SEQUENCES do nearly all of it. Beat is used +; twice by voice 0 and twice by voice 1 - four placings of one phrase, written once, and on +; two different voices, because the voice is a property of where a sequence is placed and not +; of the sequence. +; +; ticks 0-3 4-7 8-11 12-15 +; voice 0 Beat Quiet Beat Switch +; voice 1 Quiet Beat Beat (its order list ends here) +Order0: + Beat Quiet Beat Switch Tail 0xFF 0xFF +Order1: + Quiet Beat Beat 0xFF 0xFF ; And this one ENDS, so the last bar has voice 1 stopped rather + ; than resting. A player that missed the terminator would read + ; whatever follows as a sequence address and play it. +Order2: + Tail 0xFF 0xFF +Order3: + Tail 0xFF 0xFF + +Beat: + 0d60 0d4 ; Note 60 on both voices. The octave between them is their patches. + 0xFF +Quiet: 0d00 0d4 - 0d60 0d4 - 0d00 0xFF -Track1: - 0d00 0d4 - 0d60 0d4 - 0d60 0d4 - 0d00 0xFF + 0xFF +Switch: + 0x80 PatchHigh ; And voice 0 takes voice 1's instrument, mid-piece. + 0d60 0d4 ; The same note number it has played all along. + 0xFF +Tail: + 0d00 0xFF ; Long enough that nothing runs off the end of its order list. + 0xFF ; Seven pairs each, and only the last of them differs: the octave, centred on 128. So the two ; voices below are asked for THE SAME NOTE NUMBER and answer an octave apart, which nothing @@ -186,6 +147,8 @@ PatchHigh: 0x23 0d5 0x04 0d129 ; And this one an octave up. +#Include player.asm + #Vectors Boot start Device 0x50 tick diff --git a/Tests/expected/cosmosCrossDisk.out b/Tests/expected/cosmosCrossDisk.out index a031019..60a9e63 100644 --- a/Tests/expected/cosmosCrossDisk.out +++ b/Tests/expected/cosmosCrossDisk.out @@ -32,7 +32,7 @@ vars.script 50 blocks.script 343 loops.script 272 tune.sbx 318 -Play.sbx 740 +Play.sbx 892 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosDrives.out b/Tests/expected/cosmosDrives.out index 49b15de..c37ef91 100644 --- a/Tests/expected/cosmosDrives.out +++ b/Tests/expected/cosmosDrives.out @@ -22,7 +22,7 @@ vars.script 50 blocks.script 343 loops.script 272 tune.sbx 318 -Play.sbx 740 +Play.sbx 892 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosFault.out b/Tests/expected/cosmosFault.out index a631077..55db3fe 100644 --- a/Tests/expected/cosmosFault.out +++ b/Tests/expected/cosmosFault.out @@ -39,7 +39,7 @@ vars.script 50 blocks.script 343 loops.script 272 tune.sbx 318 -Play.sbx 740 +Play.sbx 892 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosFlip.out b/Tests/expected/cosmosFlip.out index f86bde0..485d549 100644 --- a/Tests/expected/cosmosFlip.out +++ b/Tests/expected/cosmosFlip.out @@ -29,7 +29,7 @@ vars.script 50 blocks.script 343 loops.script 272 tune.sbx 318 -Play.sbx 740 +Play.sbx 892 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosGrid.out b/Tests/expected/cosmosGrid.out index 5619267..6dfc717 100644 --- a/Tests/expected/cosmosGrid.out +++ b/Tests/expected/cosmosGrid.out @@ -29,7 +29,7 @@ vars.script 50 blocks.script 343 loops.script 272 tune.sbx 318 -Play.sbx 740 +Play.sbx 892 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosMonitor.out b/Tests/expected/cosmosMonitor.out index 9f72c48..b9a51f8 100644 --- a/Tests/expected/cosmosMonitor.out +++ b/Tests/expected/cosmosMonitor.out @@ -115,7 +115,7 @@ vars.script 50 blocks.script 343 loops.script 272 tune.sbx 318 -Play.sbx 740 +Play.sbx 892 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosMonitorRun.out b/Tests/expected/cosmosMonitorRun.out index 41cf9ca..0f8e8c8 100644 --- a/Tests/expected/cosmosMonitorRun.out +++ b/Tests/expected/cosmosMonitorRun.out @@ -34,7 +34,7 @@ vars.script 50 blocks.script 343 loops.script 272 tune.sbx 318 -Play.sbx 740 +Play.sbx 892 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosRun.out b/Tests/expected/cosmosRun.out index eef06bb..2e5bbd5 100644 --- a/Tests/expected/cosmosRun.out +++ b/Tests/expected/cosmosRun.out @@ -22,7 +22,7 @@ vars.script 50 blocks.script 343 loops.script 272 tune.sbx 318 -Play.sbx 740 +Play.sbx 892 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosSlowDisk.out b/Tests/expected/cosmosSlowDisk.out index 9043dda..d7208ad 100644 --- a/Tests/expected/cosmosSlowDisk.out +++ b/Tests/expected/cosmosSlowDisk.out @@ -20,7 +20,7 @@ vars.script 50 blocks.script 343 loops.script 272 tune.sbx 318 -Play.sbx 740 +Play.sbx 892 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosSprite.out b/Tests/expected/cosmosSprite.out index d42367b..1738b32 100644 --- a/Tests/expected/cosmosSprite.out +++ b/Tests/expected/cosmosSprite.out @@ -29,7 +29,7 @@ vars.script 50 blocks.script 343 loops.script 272 tune.sbx 318 -Play.sbx 740 +Play.sbx 892 notes.txt 21 Apps hi.script 121 diff --git a/Tests/makedisks.sh b/Tests/makedisks.sh index 6991377..446c5f2 100755 --- a/Tests/makedisks.sh +++ b/Tests/makedisks.sh @@ -182,7 +182,8 @@ printf '#! script\nfor a in 1 2\n for b in x y\n echo $a$b\n end\nend\nset "$TOOL" put "$DISKS/cosmos.img" "$WORK/tune.sbx" >/dev/null # And Play, which is the same beat carrying four voices instead of one - each with an # instrument of its own out of Sounds, which is what four patches at once looks like. -"$ROOT/Assembler" -I "$ROOT/Programs/Sounds" -I "$ROOT/Programs/CosmOS/Source" \ +"$ROOT/Assembler" -I "$ROOT/Programs/Sounds" -I "$ROOT/Programs/Libraries" \ + -I "$ROOT/Programs/CosmOS/Source" \ "$ROOT/Programs/CosmOS/Apps/Play.asm" -o "$WORK/Play.sbx" >/dev/null "$TOOL" put "$DISKS/cosmos.img" "$WORK/Play.sbx" >/dev/null printf 'this is not a program' > notes.txt diff --git a/Tests/sound.sh b/Tests/sound.sh index 1f7c52d..d6f08aa 100755 --- a/Tests/sound.sh +++ b/Tests/sound.sh @@ -94,7 +94,8 @@ run() { # run local name="$1" cat > "$BUILD/$name.asm" - "$ASM" "$BUILD/$name.asm" -o "$BUILD/$name.bin" >"$BUILD/$name.log" 2>&1 || { + "$ASM" -I "$ROOT/Programs/Libraries" -I "$ROOT/Programs/Sounds" \ + "$BUILD/$name.asm" -o "$BUILD/$name.bin" >"$BUILD/$name.log" 2>&1 || { echo "could not assemble $name"; sed 's/^/ /' "$BUILD/$name.log"; return 1; } timeout 20 "$EMU" --fast --cycles "$2" --sound "$BUILD/$name.raw" "$BUILD/$name.bin" \ > "$BUILD/$name.out" 2>&1 @@ -659,12 +660,12 @@ near "$EIGHTH" 523.3 2 \ # still fading into the second voice's stretch, and the window that should have read one note # read 634 hertz - which is not a note, not an octave, and was measured before it was written # down as an assertion. -run fourVoices 1700000 < "$ROOT/Programs/testPrograms/fourVoiceTest.asm" || exit 1 +run fourVoices 2200000 < "$ROOT/Programs/testPrograms/fourVoiceTest.asm" || exit 1 COUNT="$(measure fourVoices count)" -near "$COUNT" 72000 0.1 \ - && result ok "twelve ticks of four voices" "$COUNT samples, and 12 x 125,000 cycles is 72,000" \ - || result no "twelve ticks of four voices" "$COUNT samples, wanted about 72,000" +near "$COUNT" 96000 0.1 \ + && result ok "sixteen ticks of four voices" "$COUNT samples, and 16 x 125,000 cycles is 96,000" \ + || result no "sixteen ticks of four voices" "$COUNT samples, wanted about 96,000" FIRST="$(measure fourVoices pitch 2000 22000)" near "$FIRST" 261.6 2 \ @@ -690,6 +691,25 @@ near "$RATIO" 2.0 3 \ && result ok "a patch belongs to its channel" "both tracks say note 60 and one sounds $RATIO times the other" \ || result no "a patch belongs to its channel" "the two voices are $RATIO apart, and their patches say two" +# ---- A voice goes on to the next sequence its order list names ---- +# +# Sixteen ticks of four bars, and one phrase does nearly all of it: Beat is placed twice by +# voice 0 and twice by voice 1, written once, on two different voices. A voice is a property +# of where a sequence is PLACED and not of the sequence, which is what a per-voice order list +# buys and what a sequence carrying its own channels would have cost. +# +# The three windows above already ran through it - each of them is a different bar of an order +# list rather than a stretch of one long track - so what is left to check is the command. +# +# THE FOURTH BAR IS THE SAME NOTE NUMBER AS THE FIRST. Voice 0's last sequence begins with +# 0x80, which changes its patch mid-piece, and the octave that comes out is the new patch's +# and not the note's. A command takes no tick: the reader acts on it and reads the note behind +# it on the same boundary. +SWITCHED="$(measure fourVoices pitch 74000 94000)" +near "$SWITCHED" 523.3 2 \ + && result ok "a command changes the instrument mid-piece" "$SWITCHED hertz from note 60, which is the patch and not the note" \ + || result no "a command changes the instrument mid-piece" "$SWITCHED hertz, wanted the octave the new patch gives" + # Both voices sounding is louder than either of them alone. Against the QUIETER of the two so # that the check cannot be passed by one voice getting louder, and by a margin well under the # two-fold a clean sum would give, because two notes are not in phase and do not simply add.