diff --git a/Programs/Examples/tune.asm b/Programs/Examples/tune.asm index 44d9f88..1997bc0 100644 --- a/Programs/Examples/tune.asm +++ b/Programs/Examples/tune.asm @@ -8,26 +8,28 @@ ; 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 screen finishes a frame sixty times a second and will say so. That is the only regular -; beat on this machine, and it is counted 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. Every -; duration below is in frames: 30 is half a second. +; 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. ; -; A programmable timer is the device that ought to be doing this, and it does not exist yet. -; Borrowing the screen's frame costs nothing and works, which is the whole reason to notice -; that a beat is a beat wherever it comes from. +; ---- The tick is a sixteenth note, and that is the whole point ---- ; -; ---- What borrowing it costs ---- +; 125,000 cycles, which is a sixteenth note at 120 beats a minute. ; -; The frame is not slow. It is FIXED, and that is the different complaint. Every duration here -; is a whole number of 16.67 ms, so a note worth a third of a beat cannot be written at all - -; and the way round it is to pick a tempo whose subdivisions happen to land on whole frames, -; which is making the tune fit the machine rather than the other way round. The theme below -; was written to the frame and is a few cents of tempo away from what it wants to be. +; 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 what the timer wants is an ARBITRARY tick rather than a faster fixed one, and the reason -; the screen should not be the clock is that a display refresh and a music routine have no -; reason to share a rate. +; 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 ---- ; @@ -143,10 +145,18 @@ start: ; ---- The beat ---- ; - ; Ask the screen to interrupt at each frame, and let interrupts in. The screen does not do - ; this unless it is asked. + ; 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 0x35 + OUTA 0x52 + INIA 0xE8 + OUTA 0x53 + INIA 0x48 + OUTA 0x54 ; 0x01E848 is 125,000. + + INIA 0x07 + OUTA 0x51 ; Run, repeat, interrupt. SIF ; ---- The tune ---- @@ -160,11 +170,11 @@ nextNote: BRA finished OUTA 0x44 ; Writing the note is what starts it. INCD.0 - LDB.0 ; How many frames it lasts. + LDB.0 ; How many ticks it lasts. INCD.0 holdNote: - WAIT ; Nothing at all until the screen says a frame has gone by. + WAIT ; Nothing at all until the timer says a tick has gone by. DECB BNB holdNote @@ -175,9 +185,10 @@ holdNote: BRI nextNote finished: - ; Let the last note ring out rather than cutting it off, then put the screen back the way it - ; was found - and stop asking to be interrupted before taking away what catches it. - INIB 0d45 + ; 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 @@ -185,13 +196,17 @@ lastRing: CIF RSTA - OUTA 0x35 + OUTA 0x51 SWI osExit -; Sixty times a second, and it has nothing to do. WAIT only needs something to have happened, -; and this is the something. A handler still has to exist: an interrupt with nothing installed -; to catch it is a fault. -frame: +; 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 @@ -200,85 +215,85 @@ frame: ; ---- Notes and how long they last ---- ; -; Pairs: a MIDI note, then a count of frames. 60 is middle C and every 12 is an octave. A zero +; 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 0d15 ; C - 0d64 0d15 ; E - 0d67 0d15 ; G - 0d72 0d30 ; C, an octave up, held twice as long - 0d71 0d15 ; B - 0d67 0d15 ; G - 0d64 0d15 ; E - 0d60 0d45 ; and home + 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 0d15 ; C - 0x35 0d15 ; F - 0x3C 0d15 ; C+ - 0x40 0d30 ; E+ - 0x48 0d30 ; C++ - 0x45 0d30 ; A+ - 0x47 0d30 ; B+ - 0x43 0d45 ; G+ + 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. - 0x2A 0d7 ; Gs - 0x33 0d7 ; Ef - 0x3C 0d7 ; C+ + 0x2A 0d1 ; Gs + 0x33 0d1 ; Ef + 0x3C 0d1 ; C+ ; - 0x2A 0d7 ; Gs - 0x33 0d7 ; Ef - 0x3C 0d7 ; C+ + 0x2A 0d1 ; Gs + 0x33 0d1 ; Ef + 0x3C 0d1 ; C+ ; - 0x2A 0d7 ; Gs - 0x33 0d7 ; Ef - 0x3C 0d7 ; C+ + 0x2A 0d1 ; Gs + 0x33 0d1 ; Ef + 0x3C 0d1 ; C+ ; - 0x2A 0d7 ; Gs - 0x33 0d7 ; Ef - 0x3C 0d7 ; C+ + 0x2A 0d1 ; Gs + 0x33 0d1 ; Ef + 0x3C 0d1 ; C+ ; next chord - 0x2E 0d7 ; Bf - 0x35 0d7 ; F - 0x3E 0d7 ; D+ + 0x2E 0d1 ; Bf + 0x35 0d1 ; F + 0x3E 0d1 ; D+ ; next chord - 0x2E 0d7 ; Bf - 0x35 0d7 ; F - 0x3E 0d7 ; D+ + 0x2E 0d1 ; Bf + 0x35 0d1 ; F + 0x3E 0d1 ; D+ ; ; next chord - 0x2E 0d7 ; Bf - 0x35 0d7 ; F - 0x3E 0d7 ; D+ + 0x2E 0d1 ; Bf + 0x35 0d1 ; F + 0x3E 0d1 ; D+ ; ; next chord - 0x2E 0d7 ; Bf - 0x35 0d7 ; F - 0x3E 0d7 ; D+ + 0x2E 0d1 ; Bf + 0x35 0d1 ; F + 0x3E 0d1 ; D+ ; Finally on the C major - 0x30 0d7 ; C - 0x37 0d7 ; G - 0x40 0d7 ; E+ + 0x30 0d1 ; C + 0x37 0d1 ; G + 0x40 0d1 ; E+ ; - 0x30 0d7 ; C - 0x37 0d7 ; G - 0x40 0d7 ; E+ + 0x30 0d1 ; C + 0x37 0d1 ; G + 0x40 0d1 ; E+ ; - 0x30 0d7 ; C - 0x37 0d7 ; G - 0x40 0d7 ; E+ + 0x30 0d1 ; C + 0x37 0d1 ; G + 0x40 0d1 ; E+ ; - 0x30 0d7 ; C - 0x37 0d7 ; G - 0x40 0d7 ; E+ + 0x30 0d1 ; C + 0x37 0d1 ; G + 0x40 0d1 ; E+ ; - 0x18 0d60 ; C bass + 0x18 0d8 ; C bass 0x00 #Vectors Boot start - Device 0x30 frame + Device 0x50 tick diff --git a/Tests/expected/cosmosCrossDisk.out b/Tests/expected/cosmosCrossDisk.out index 09f5559..f1d45e8 100644 --- a/Tests/expected/cosmosCrossDisk.out +++ b/Tests/expected/cosmosCrossDisk.out @@ -31,7 +31,7 @@ Crash.sbx 632 vars.script 50 blocks.script 343 loops.script 272 -tune.sbx 306 +tune.sbx 318 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosDrives.out b/Tests/expected/cosmosDrives.out index 607e7b9..92c73c6 100644 --- a/Tests/expected/cosmosDrives.out +++ b/Tests/expected/cosmosDrives.out @@ -21,7 +21,7 @@ Crash.sbx 632 vars.script 50 blocks.script 343 loops.script 272 -tune.sbx 306 +tune.sbx 318 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosFault.out b/Tests/expected/cosmosFault.out index d557d6c..88a87b9 100644 --- a/Tests/expected/cosmosFault.out +++ b/Tests/expected/cosmosFault.out @@ -38,7 +38,7 @@ Crash.sbx 632 vars.script 50 blocks.script 343 loops.script 272 -tune.sbx 306 +tune.sbx 318 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosFlip.out b/Tests/expected/cosmosFlip.out index b6ba903..f13f075 100644 --- a/Tests/expected/cosmosFlip.out +++ b/Tests/expected/cosmosFlip.out @@ -28,7 +28,7 @@ Crash.sbx 632 vars.script 50 blocks.script 343 loops.script 272 -tune.sbx 306 +tune.sbx 318 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosGrid.out b/Tests/expected/cosmosGrid.out index 65f372d..07f1adc 100644 --- a/Tests/expected/cosmosGrid.out +++ b/Tests/expected/cosmosGrid.out @@ -28,7 +28,7 @@ Crash.sbx 632 vars.script 50 blocks.script 343 loops.script 272 -tune.sbx 306 +tune.sbx 318 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosMonitor.out b/Tests/expected/cosmosMonitor.out index 13b5e00..51bcb5f 100644 --- a/Tests/expected/cosmosMonitor.out +++ b/Tests/expected/cosmosMonitor.out @@ -114,7 +114,7 @@ Crash.sbx 632 vars.script 50 blocks.script 343 loops.script 272 -tune.sbx 306 +tune.sbx 318 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosMonitorRun.out b/Tests/expected/cosmosMonitorRun.out index 2ae0d28..58136d6 100644 --- a/Tests/expected/cosmosMonitorRun.out +++ b/Tests/expected/cosmosMonitorRun.out @@ -33,7 +33,7 @@ Crash.sbx 632 vars.script 50 blocks.script 343 loops.script 272 -tune.sbx 306 +tune.sbx 318 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosRun.out b/Tests/expected/cosmosRun.out index 1626970..4133ec1 100644 --- a/Tests/expected/cosmosRun.out +++ b/Tests/expected/cosmosRun.out @@ -21,7 +21,7 @@ Crash.sbx 632 vars.script 50 blocks.script 343 loops.script 272 -tune.sbx 306 +tune.sbx 318 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosSlowDisk.out b/Tests/expected/cosmosSlowDisk.out index da684c1..8a3ffaf 100644 --- a/Tests/expected/cosmosSlowDisk.out +++ b/Tests/expected/cosmosSlowDisk.out @@ -19,7 +19,7 @@ Crash.sbx 632 vars.script 50 blocks.script 343 loops.script 272 -tune.sbx 306 +tune.sbx 318 notes.txt 21 Apps hi.script 121 diff --git a/Tests/expected/cosmosSprite.out b/Tests/expected/cosmosSprite.out index 7232959..dd0cdba 100644 --- a/Tests/expected/cosmosSprite.out +++ b/Tests/expected/cosmosSprite.out @@ -28,7 +28,7 @@ Crash.sbx 632 vars.script 50 blocks.script 343 loops.script 272 -tune.sbx 306 +tune.sbx 318 notes.txt 21 Apps hi.script 121 diff --git a/Tests/sound.sh b/Tests/sound.sh index a1cbbce..aba6cd1 100755 --- a/Tests/sound.sh +++ b/Tests/sound.sh @@ -578,6 +578,72 @@ EOT && result ok "and its own channel's patch does take it away" "$FIRST became $THIRD, which is the note itself" \ || result no "and its own channel's patch does take it away" "$FIRST then $THIRD" +# ---- A tune keeps the timer's beat, not the screen's ---- +# +# M1 of the music player. The sound device knows how to make a note and nothing about when; +# the timer is the other half. What is checked here is that a note's LENGTH is the period that +# was asked for - which is the one claim a program timing music on frames cannot make. +# +# 125,000 cycles is a sixteenth note at 120 beats a minute, and a frame is 16,667: seven and a +# half of them. It could not be asked for at all until the timer existed, and Examples/tune.asm +# spent years writing its arpeggio as seven whole frames instead, six and a half per cent fast. +# +# THE PROGRAM IS THE PLAYER'S LOOP, deliberately: a note table walked by a pointer, a wait on +# each tick, the gate dropped at the end. Eight notes of one tick each, seven of them middle C +# and the last an octave up, so where the boundary between them falls can be MEASURED rather +# than assumed. +{ printf '#Program\nstart:\n' + port 0x41 0x00 + param 0x00 0x02 # Saw, which has edges enough to count + param 0x01 0xFF # at full gain + param 0x05 0x01 # and switched on. + param 0x20 0x00 # No attack, no decay, held at full: a note lasts its whole tick + param 0x21 0x00 # and stops when the gate drops, rather than fading on its own - + param 0x22 0xFF # which is what makes the boundary a thing a counter can find. + param 0x23 0x05 + port 0x46 0xC0 + port 0x52 0x01 + port 0x53 0xE8 + port 0x54 0x48 # 0x01E848, which is 125,000 cycles: 6,000 samples. + port 0x51 0x07 # Run, repeat, interrupt. + printf ' SIF\n' + printf ' SETD.0 Notes\nnextNote:\n LDA.0\n BRA done\n OUTA 0x44\n INCD.0\n' + printf ' LDB.0\n INCD.0\nhold:\n WAIT\n DECB\n BNB hold\n' + printf ' RSTA\n OUTA 0x45\n BRI nextNote\ndone:\n CIF\n RSTA\n OUTA 0x51\n HALT\n' + printf 'tick:\n RETI\n' + printf '#Data\nNotes:\n' + printf ' 0d60 0d1\n 0d60 0d1\n 0d60 0d1\n 0d60 0d1\n' + printf ' 0d60 0d1\n 0d60 0d1\n 0d60 0d1\n 0d72 0d1\n 0x00\n' + printf '#Vectors\n Boot start\n Device 0x50 tick\n'; } \ + | run timedNotes 1200000 || exit 1 + +# Eight ticks of 125,000 cycles is 1,000,000, and the machine halts when the table runs out +# rather than when the cycle limit does. So the length of what came out is the beat, counted. +# A TENTH OF A PER CENT, which is forty eight samples. That is not fussiness: the machine +# halts within a few hundred cycles of the last tick, so anything wider than this stops +# measuring the timer and starts measuring nothing in particular. +COUNT="$(measure timedNotes count)" +near "$COUNT" 48000 0.1 \ + && result ok "eight ticks last eight periods" "$COUNT samples, and 8 x 125,000 cycles is 48,000" \ + || result no "eight ticks last eight periods" "$COUNT samples, wanted about 48,000" + +# ---- And the notes change where the ticks do ---- +# +# The seventh note occupies samples 36,000 to 42,000 and the eighth 42,000 to 48,000, so a +# window inside each should hear one note and not a blend of two. +# +# THIS IS THE ONE THAT FAILS ON THE OLD TIMING. At seven whole frames a tick is 5,600 samples, +# the eighth note starts at 39,200, and the seventh window straddles the boundary: measured, it +# reads 384 Hz, which is neither note and is what a crossing counter says when it is given two. +SEVENTH="$(measure timedNotes pitch 37000 41000)" +EIGHTH="$(measure timedNotes pitch 43000 47000)" +near "$SEVENTH" 261.6 2 \ + && result ok "a note begins where a tick begins" "the seventh note reads $SEVENTH hertz, and middle C is 261.6" \ + || result no "a note begins where a tick begins" "the seventh window reads $SEVENTH hertz, which is not middle C alone" +near "$EIGHTH" 523.3 2 \ + && result ok "and the one after it is the next note" "$EIGHTH hertz, an octave up as written" \ + || result no "and the one after it is the next note" "$EIGHTH hertz, wanted 523.3" + echo if [ "$FAIL" -eq 0 ]; then echo "All $PASS sound checks passed."