diff --git a/Programs/CosmOS/Apps/Play.asm b/Programs/CosmOS/Apps/Play.asm index fb6058a..7b013d8 100644 --- a/Programs/CosmOS/Apps/Play.asm +++ b/Programs/CosmOS/Apps/Play.asm @@ -90,8 +90,13 @@ start: ; ---- An instrument for each voice ---- ; ; Four patches, designed in soundThing and converted by SoundPatch, which is the only thing - ; here that understands what a patch means. This reads a count and that many parameter and - ; value pairs and knows nothing else about them. + ; here that understands what a patch means. The player reads a count and that many parameter + ; and value pairs and knows nothing else about them. + ; + ; WHICH PATCH EACH VOICE STARTS ON IS DECLARED, in VoiceStart below, rather than being four + ; calls in a row here. That is the shape a tune read from a file will have - a starting + ; instrument is state and belongs in a header - and saying it once means the two forms do + ; not drift. ; ; A PATCH BELONGS TO ITS CHANNEL, which is why four of them can be up at once. It did not ; used to: the LFOs belonged to the whole device, so whichever patch was loaded last owned @@ -99,18 +104,7 @@ start: ; under the old device this piece would have played with the arpeggio's setting on all four ; parts - which is exactly the fault that made Lunar Porter's low fuel warning stop ; trilling after the first landing of a run. - SETD.0 OboePatch - RSTA - CALL loadPatch ; The melody sings, so it is the one with a voice. - SETD.0 StringsPatch - INIA 0d1 - CALL loadPatch ; A pad underneath it. - SETD.0 SquarePatch - INIA 0d2 - CALL loadPatch ; And a square wave for the bass, which is where one belongs. - SETD.0 KalimbaPatch - INIA 0d3 - CALL loadPatch ; Plucked, for the arpeggio. + CALL loadStartPatches INIA 0x60 ; Room over the top for four voices at once. OUTA 0x46 @@ -208,13 +202,32 @@ tick: ; 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 + 0d0 0d1 0d2 0d3 0xFF Order1: - Har1 Har2 Har3 Har4 0xFF 0xFF + 0d4 0d5 0d6 0d7 0xFF Order2: - BassC BassF BassG BassC 0xFF 0xFF + 0d8 0d9 0d10 0d8 0xFF ; BassC twice, written once. Order3: - ArpC ArpF ArpG Silent 0xFF 0xFF + 0d11 0d12 0d13 0d14 0xFF + +; ---- The tables, which are the only places an address lives ---- +; +; Writing these out by hand is exactly the tedium a compiler exists to remove: every sequence +; has to be counted into its place above, and moving one means renumbering. That it is +; unpleasant is the point of noticing it here rather than after a tool has baked the shape in. +PatchTable: + OboePatch StringsPatch SquarePatch KalimbaPatch + +SequenceTable: + Mel1 Mel2 Mel3 Mel4 ; 0 to 3 + Har1 Har2 Har3 Har4 ; 4 to 7 + BassC BassF BassG ; 8 to 10 + ArpC ArpF ArpG Silent ; 11 to 14 + +; The patch each voice starts on. Not assumed, and not four calls in a row: a starting +; instrument is state, and state belongs somewhere it can be read. +VoiceStart: + 0d0 0d1 0d2 0d3 ; ---- Four bars of C, F, G, C ---- ; @@ -239,7 +252,7 @@ Mel3: 0d71 0d8 ; B 0xFF Mel4: - 0x80 StringsPatch ; The last bar is a swell rather than a reed. + 0x80 0d1 ; Patch one is Strings: the last bar is a swell, not a reed. 0d72 0d16 ; and home 0xFF diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index 039a1a9..ac73584 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. 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. | +| 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. Sequence and patch names are INDICES through two tables, which are the only places an address lives - so a tune read from a file will need its base added to two arrays and nothing else, rather than a loader that walks every sequence looking for addresses to correct. 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 patch each voice starts on is declared rather than assumed, because a voice given no instrument would play on whatever the device woke up with. 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 index 07dc91c..59315aa 100644 --- a/Programs/Libraries/player.asm +++ b/Programs/Libraries/player.asm @@ -13,11 +13,19 @@ ; ; The caller must define, in its Data Segment: ; -; Order0 Order1 Order2 Order3 a table of sequence addresses each, ending in 0xFFFF +; PatchTable one two-byte address per patch +; SequenceTable one two-byte address per sequence +; VoiceStart four bytes, the patch index each voice starts on +; Order0..Order3 one-byte sequence indices each, ending in 0xFF ; -; 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. +; and 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 @@ -94,13 +102,17 @@ stepDone: ; 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. + 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 + 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. @@ -113,27 +125,34 @@ stepCommand: ; ---- 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. +; 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. ; -; 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. +; 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 + LDA.0 ; Which sequence comes next. 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. + SETD.2 SequenceTable + DPUA.2 + DPUA.2 + LDD.2.2 ; DP2 is the sequence that index names. + DPUP.1 0d2 STD.2.1 ; And the sequence cursor set to the new one. DPDN.1 0d2 @@ -153,6 +172,38 @@ stepOrderEnd: 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 VoiceStart + DPUA.2 + LDA.2 ; Which patch this voice starts on. + SETD.2 PatchTable + 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 + ; ---- A patch, onto the channel named by A ---- ; ; A count, then that many pairs of parameter and value: the format SoundPatch writes and the diff --git a/Programs/testPrograms/fourVoiceTest.asm b/Programs/testPrograms/fourVoiceTest.asm index da47073..6a7f358 100644 --- a/Programs/testPrograms/fourVoiceTest.asm +++ b/Programs/testPrograms/fourVoiceTest.asm @@ -28,16 +28,11 @@ #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 - ; the device rather than to its channel the second would overwrite the first and both voices - ; would sound the same. - SETD.0 PatchLow - RSTA - CALL loadPatch - SETD.0 PatchHigh - INIA 0d1 - CALL loadPatch + ; A patch each, in the format SoundPatch writes: a count and that many pairs. They differ in + ; ONE parameter - the octave - and VoiceStart gives the low one to voice 0 and the high one + ; to voice 1. If a patch belonged to the device rather than to its channel the second would + ; overwrite the first and both voices would sound the same. + CALL loadStartPatches INIA 0x60 OUTA 0x46 @@ -96,15 +91,24 @@ tick: ; voice 0 Beat Quiet Beat Switch ; voice 1 Quiet Beat Beat (its order list ends here) Order0: - Beat Quiet Beat Switch Tail 0xFF 0xFF + 0d0 0d1 0d0 0d2 0d3 0xFF Order1: - Quiet Beat Beat 0xFF 0xFF ; And this one ENDS, so the last bar has voice 1 stopped rather + 0d1 0d0 0d0 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. + ; whatever follows as a sequence index and play it. Order2: - Tail 0xFF 0xFF + 0d3 0xFF Order3: - Tail 0xFF 0xFF + 0d3 0xFF + +PatchTable: + PatchLow PatchHigh + +SequenceTable: + Beat Quiet Switch Tail ; 0 to 3 + +VoiceStart: + 0d0 0d1 0d0 0d0 Beat: 0d60 0d4 ; Note 60 on both voices. The octave between them is their patches. @@ -113,7 +117,7 @@ Quiet: 0d00 0d4 0xFF Switch: - 0x80 PatchHigh ; And voice 0 takes voice 1's instrument, mid-piece. + 0x80 0d1 ; Voice 0 takes patch one, which is voice 1's, mid-piece. 0d60 0d4 ; The same note number it has played all along. 0xFF Tail: diff --git a/Tests/expected/cosmosCrossDisk.out b/Tests/expected/cosmosCrossDisk.out index 60a9e63..05024f5 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 892 +Play.sbx 939 notes.txt 21 Apps