diff --git a/Programs/CosmOS/Apps/Play.asm b/Programs/CosmOS/Apps/Play.asm index 7b013d8..4dcc871 100644 --- a/Programs/CosmOS/Apps/Play.asm +++ b/Programs/CosmOS/Apps/Play.asm @@ -104,7 +104,29 @@ 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. - CALL loadStartPatches + ; ---- A tune named on the line, or the one in here ---- + ; + ; Nothing after the name plays what this program was built with. A name reads a file and + ; plays that instead, which is what makes it a player rather than a program with one song. + SETD.0 TuneName + INIB 0d63 + SWI osArgument + SETD.0 TuneName + LDA.0 + BRA playBuiltIn ; Nothing was typed after the name. + + SETD.1 TuneBuffer ; DP0 is still the name, which is what osFileRead wants. + SWI osFileRead + BNQ playNoFile + + SETD.0 TuneBuffer + CALL useTune + BNQ playNotATune + BRI playReady + +playBuiltIn: + CALL useBuiltIn +playReady: INIA 0x60 ; Room over the top for four voices at once. OUTA 0x46 @@ -176,6 +198,52 @@ lastRing: OUTA 0x51 SWI osExit +; ---- What it says when it cannot ---- +; +; Before the timer has been started, both of them, so there is nothing to stop and no handler +; to take away. +playNoFile: + SETD.0 NoFileSaid + SWI osPrintString + INIA 0x01 + SWI osExit + +playNotATune: + SETD.0 NotATuneSaid + SWI osPrintString + INIA 0x01 + SWI osExit + +; ---- The tune that is in this program ---- +; +; What useTune does for a file, said in eight lines for a tune that is already in memory: the +; three tables the player asks for, and an order list for each voice. Nothing is relocated, +; because the assembler already wrote the addresses. +useBuiltIn: + SETD.0 BuiltInPatches + SETD.1 PatchTable + STD.0.1 + SETD.0 BuiltInSequences + SETD.1 SequenceTable + STD.0.1 + SETD.0 BuiltInVoiceStart + SETD.1 VoiceStartAt + STD.0.1 + SETD.0 Order0 + SETD.1 Voice0 + STD.0.1 + SETD.0 Order1 + SETD.1 Voice1 + STD.0.1 + SETD.0 Order2 + SETD.1 Voice2 + STD.0.1 + SETD.0 Order3 + SETD.1 Voice3 + STD.0.1 + CALL startVoices + 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 @@ -187,6 +255,19 @@ tick: #Base 0x3000 +; The name that followed "Play", and room for the tune it names. WHOLE BLOCKS: osFileRead +; puts 256 bytes down whatever the file's length, so the room here is a multiple of that and +; not a guess at how big a tune is. +TuneName: + #Reserve 0d64 +TuneBuffer: + #Reserve 0d1024 + +NoFileSaid: + "no such tune" +NotATuneSaid: + "that is not a tune" + ; ---- The order lists ---- ; ; A table of sequence addresses each, ending in a zero. Read the four of them across and they @@ -215,10 +296,13 @@ Order3: ; 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: +; The tables this tune uses. The player holds POINTERS to them rather than the tables, because +; a tune read from a file has its tables wherever the file was put - so a built-in one hands +; over the same three addresses a loaded one would. +BuiltInPatches: OboePatch StringsPatch SquarePatch KalimbaPatch -SequenceTable: +BuiltInSequences: Mel1 Mel2 Mel3 Mel4 ; 0 to 3 Har1 Har2 Har3 Har4 ; 4 to 7 BassC BassF BassG ; 8 to 10 @@ -226,7 +310,7 @@ SequenceTable: ; 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: +BuiltInVoiceStart: 0d0 0d1 0d2 0d3 ; ---- Four bars of C, F, G, C ---- diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index ac73584..6019130 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. 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. | +| 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. `Play ` reads a tune and plays that; `Play` on its own plays the one built into it. A tune file is "SBTU", a version, the tick in cycles, and offsets to a patch table, a sequence table and four order lists - everything in it an OFFSET from wherever it was put, so loading one is adding the base to two tables and pointing four voices at their order lists. No sequence is walked and nothing inside one is an address, which is what makes a malformed tune something that plays wrongly rather than something that takes the loader with it; the magic is checked first, because the loader follows what the offsets name. The patch each voice starts on is in the header, because a starting instrument is state. 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 59315aa..76a36cd 100644 --- a/Programs/Libraries/player.asm +++ b/Programs/Libraries/player.asm @@ -109,6 +109,7 @@ stepCommand: 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. @@ -149,9 +150,10 @@ stepSequenceEnd: 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 ; DP2 is the sequence that index names. + LDD.2.2 ; And the sequence that index names. DPUP.1 0d2 STD.2.1 ; And the sequence cursor set to the new one. @@ -184,10 +186,12 @@ loadStartOne: SETD.3 ThisChannel STA.3 - SETD.2 VoiceStart + 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 @@ -204,6 +208,186 @@ loadStartOne: 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 @@ -253,6 +437,24 @@ Playing: 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. + ; The sequence a voice starts on, so that its first tick goes through the order list like ; every other bar does. Empty: diff --git a/Programs/testPrograms/fourVoiceTest.asm b/Programs/testPrograms/fourVoiceTest.asm index 6a7f358..727355e 100644 --- a/Programs/testPrograms/fourVoiceTest.asm +++ b/Programs/testPrograms/fourVoiceTest.asm @@ -32,7 +32,28 @@ start: ; 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 + SETD.0 MyPatches + SETD.1 PatchTable + STD.0.1 + SETD.0 MySequences + SETD.1 SequenceTable + STD.0.1 + SETD.0 MyVoiceStart + SETD.1 VoiceStartAt + STD.0.1 + SETD.0 Order0 + SETD.1 Voice0 + STD.0.1 + SETD.0 Order1 + SETD.1 Voice1 + STD.0.1 + SETD.0 Order2 + SETD.1 Voice2 + STD.0.1 + SETD.0 Order3 + SETD.1 Voice3 + STD.0.1 + CALL startVoices INIA 0x60 OUTA 0x46 @@ -101,13 +122,13 @@ Order2: Order3: 0d3 0xFF -PatchTable: +MyPatches: PatchLow PatchHigh -SequenceTable: +MySequences: Beat Quiet Switch Tail ; 0 to 3 -VoiceStart: +MyVoiceStart: 0d0 0d1 0d0 0d0 Beat: diff --git a/Tests/expected/cosmosCrossDisk.out b/Tests/expected/cosmosCrossDisk.out index 05024f5..ca58fb1 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 939 +Play.sbx 2525 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosDrives.out b/Tests/expected/cosmosDrives.out index b5bd45a..bb360ba 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 939 +Play.sbx 2525 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosFault.out b/Tests/expected/cosmosFault.out index edb55e4..faee994 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 939 +Play.sbx 2525 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosFlip.out b/Tests/expected/cosmosFlip.out index 2aec771..0636b2e 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 939 +Play.sbx 2525 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosGrid.out b/Tests/expected/cosmosGrid.out index 3c3022c..a5ced45 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 939 +Play.sbx 2525 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosMonitor.out b/Tests/expected/cosmosMonitor.out index 46f41e9..5a7856e 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 939 +Play.sbx 2525 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosMonitorRun.out b/Tests/expected/cosmosMonitorRun.out index 7fb8e23..7d71961 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 939 +Play.sbx 2525 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosRun.out b/Tests/expected/cosmosRun.out index 1bcac2e..8dc4138 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 939 +Play.sbx 2525 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosSlowDisk.out b/Tests/expected/cosmosSlowDisk.out index 0ba6777..cc42e68 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 939 +Play.sbx 2525 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosSprite.out b/Tests/expected/cosmosSprite.out index 35b013d..041a1a9 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 939 +Play.sbx 2525 notes.txt 21 Apps hi.script 121 diff --git a/Tests/makedisks.sh b/Tests/makedisks.sh index 446c5f2..5a3dd5c 100755 --- a/Tests/makedisks.sh +++ b/Tests/makedisks.sh @@ -703,6 +703,21 @@ printf 'system /System/Boot/cosmos.bin\n' > "$WORK/lonely.cfg" "$ROOT/Programs/testPrograms/pauseTest.asm" -o "$WORK/Pause.sbx" >/dev/null "$TOOL" put "$DISKS/quiet.img" "$WORK/Pause.sbx" /Apps/Pause.sbx >/dev/null +# ---- A tune in a file, and the player that reads one ---- +# +# maketune.py lays the bytes out by hand. It is NOT the compiler: the sequences and the +# patches are written as literal bytes and the only thing computed is where each piece lands, +# which is what makes it a fixture that tests the loader rather than one that tests itself. +# +# On this disk rather than cosmos.img for the usual reason - everything there shows in a +# directory listing that ten recorded tests quote. +python3 "$ROOT/Tests/maketune.py" "$WORK/two.tune" >/dev/null +"$TOOL" put "$DISKS/quiet.img" "$WORK/two.tune" two.tune >/dev/null +"$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/quiet.img" "$WORK/Play.sbx" /Apps/Play.sbx >/dev/null + # ---- What a program made of it ---- # # A status is only worth having if it survives the program that set it, so this runs three diff --git a/Tests/maketune.py b/Tests/maketune.py new file mode 100644 index 0000000..cde033e --- /dev/null +++ b/Tests/maketune.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# Lays out a .tune by hand, byte by byte, so that Play's loader has something to read before +# there is a compiler to write one. +# +# THIS IS NOT THE COMPILER. It is the hex editor: the sequences and the patches are written +# out as literal bytes and the only thing computed is where each piece lands, because counting +# offsets by hand is how you get a fixture that tests your arithmetic instead of the loader's. +# +# Written by Anachronaut + +import sys + +def patch(pairs): + out = [len(pairs)] + for parameter, value in pairs: + out += [parameter, value] + return out + +# Two instruments differing in one parameter, the octave, so that the same note number sounds +# an octave apart depending on which is loaded. +def voicePatch(octave): + return patch([(0x00, 2), # saw + (0x01, 0xFF), # at full gain + (0x05, 1), # and on + (0x20, 0), # no attack + (0x21, 0), # no decay + (0x22, 0xFF), # held at full + (0x23, 5), # and gone quickly when the gate drops + (0x04, octave)]) + +patches = [voicePatch(128), voicePatch(129)] + +# Note 60 for four ticks. The second sequence says it again, having changed instrument first - +# so the octave between them is the tune's doing and not the note's. +sequences = [[60, 4, 0xFF], + [0x80, 1, 60, 4, 0xFF]] + +orders = [[0, 1, 0xFF], # voice 0 plays both, one after the other + [0xFF], # and the other three have no part + [0xFF], + [0xFF]] + +starts = [0, 0, 0, 0] +tick = 125000 # a sixteenth note at 120 beats a minute + +HEADER = 28 +body, at = [], HEADER + +def place(blob): + global at + where = at + body.extend(blob) + at += len(blob) + return where + +# The tables come first so that their own offsets are known before anything they point at. +patchTableAt = at; at += 2 * len(patches) +sequenceTableAt = at; at += 2 * len(sequences) + +patchAt = [place(p) for p in patches] +sequenceAt = [place(s) for s in sequences] +orderAt = [place(o) for o in orders] + +def word(n): + return [(n >> 8) & 0xFF, n & 0xFF] + +tables = [] +for p in patchAt: + tables += word(p) +for q in sequenceAt: + tables += word(q) +body[0:0] = tables + +header = [ord(c) for c in "SBTU"] + [1] +header += [(tick >> 16) & 0xFF, (tick >> 8) & 0xFF, tick & 0xFF] +header += [len(patches), len(sequences)] +header += word(patchTableAt) + word(sequenceTableAt) +for o in orderAt: + header += word(o) +header += starts +header += [0, 0] +assert len(header) == HEADER, "the header is %d bytes and the loader reads %d" % (len(header), HEADER) + +open(sys.argv[1], "wb").write(bytes(header + body)) +print("%s: %d bytes, %d patches, %d sequences" % (sys.argv[1], len(header) + len(body), + len(patches), len(sequences))) diff --git a/Tests/sound.sh b/Tests/sound.sh index d6f08aa..7d2926f 100755 --- a/Tests/sound.sh +++ b/Tests/sound.sh @@ -775,6 +775,60 @@ else || result no "the system quietens what a program left sounding" "peak $LEFT, so it is still ringing" fi +# ---- A tune read from a file ---- +# +# M4's loader. Everything in a tune is an OFFSET from wherever it was put, so loading one is +# adding the base to two tables and pointing four voices at their order lists - no sequence is +# walked and nothing inside one is an address to be found and corrected. +# +# BOTH NOTES ARE NUMBER 60. The octave between them is a 0x80 command in the second sequence, +# loading the second patch out of the file, so what this measures is the whole path at once: +# the header parsed, the tick taken from it, the tables relocated, an order list followed, and +# a patch that came out of a file rather than out of the program. +# +# The fixture is laid out byte by byte by Tests/maketune.py, which is the hex editor rather +# than the compiler - the point being that the loader is tested by something that does not +# share its idea of the format. +if [ ! -f "$ROOT/Tests/build/disks/quiet.img" ]; then + result no "a tune read from a file" "quiet.img is missing; run Tests/makedisks.sh" +else + printf 'Play two.tune\n\n\nexit\n' > "$BUILD/tune.in" + timeout 60 "$EMU" --fast --cycles 6000000 --sound "$BUILD/loaded.raw" \ + --disk "$ROOT/Tests/build/disks/quiet.img" "$BUILD/cosmos.bin" \ + < "$BUILD/tune.in" > "$BUILD/tune.out" 2>&1 + + STARTS="$(python3 - "$BUILD/loaded.raw" <<'PY2' +import struct, sys +d = open(sys.argv[1], "rb").read(); n = len(d) // 2 +v = struct.unpack("<%dh" % n, d) +print(next((i for i, x in enumerate(v) if abs(x) > 500), -1)) +PY2 +)" + if [ "$STARTS" -lt 0 ]; then + result no "a tune read from a file" "nothing sounded at all" + else + FIRST="$(measure loaded pitch $(( STARTS + 1000 )) $(( STARTS + 22000 )))" + SECOND="$(measure loaded pitch $(( STARTS + 26000 )) $(( STARTS + 46000 )))" + near "$FIRST" 261.6 2 \ + && result ok "a tune read from a file" "$FIRST hertz, from a header and two tables" \ + || result no "a tune read from a file" "$FIRST hertz, wanted middle C" + near "$SECOND" 523.3 2 \ + && result ok "and a command in it loads a patch from it" "$SECOND hertz from note 60 again" \ + || result no "and a command in it loads a patch from it" "$SECOND hertz, wanted the octave" + fi + + # ---- And a file that is not a tune is refused ---- + # + # The loader reads offsets out of a file and follows what they name, so a file that is not + # one has to be turned away at the magic rather than a few instructions later. + printf 'Play Apps/Hum.sbx\nexit\n' > "$BUILD/notune.in" + timeout 30 "$EMU" --fast --cycles 3000000 --disk "$ROOT/Tests/build/disks/quiet.img" \ + "$BUILD/cosmos.bin" < "$BUILD/notune.in" > "$BUILD/notune.out" 2>&1 + grep -q "not a tune" "$BUILD/notune.out" \ + && result ok "and a file that is not a tune is refused" "it said so rather than playing it" \ + || result no "and a file that is not a tune is refused" "$(tail -2 "$BUILD/notune.out" | tr '\n' ' ')" +fi + echo if [ "$FAIL" -eq 0 ]; then echo "All $PASS sound checks passed."