From 1b251fb290752948ea677ce55ae7e556f8319012 Mon Sep 17 00:00:00 2001 From: Anachronaut Date: Sat, 5 Sep 2026 14:42:45 -0400 Subject: [PATCH] M1: the tune keeps a beat it sets, instead of one it borrowed tune.asm counted the screen's frames, because until 6b51d63 the frame was the only regular beat on the machine. It is now driven by the timer. THE DURATIONS ARE NOT A CONVERSION OF THE OLD ONES. A frame is 16,667 cycles, so a sixteenth note at 120 beats a minute is seven and a half of them and could not be asked for at all: the arpeggio was written as seven whole frames, which is six and a half per cent fast, and the rest of the piece was written to a tempo picked so its subdivisions landed on whole frames. The tick is now 125,000 cycles, which IS that sixteenth note, and the durations are what the music wanted in the first place - an eighth is two ticks, a quarter four, the arpeggio one each. The player's loop did not change shape: a note table, a wait on each beat, the gate dropped at the end. Only what it waits on. The handler stays a bare RETI, because taking the interrupt is what brings the line down - a program that POLLED the timer would have to read 0x50, and the comment now says so, since that is the trap. Three checks in sound.sh, on a program that is the player's loop rather than the tune: eight ticks last eight periods to within a tenth of a per cent, and the seventh and eighth notes are measured where the ticks put them. The middle one is the one that fails on the old timing - at seven whole frames the boundary slides under the window and it reads 384 Hz, which is neither note and is what a crossing counter says when given two. Verified with break.sh by moving the timer's period a byte. tune.sbx grew 306 -> 318 bytes, which is why ten cosmos recordings moved: they quote a directory listing. Nothing else in them changed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW --- Programs/Examples/tune.asm | 183 +++++++++++++++------------- Tests/expected/cosmosCrossDisk.out | 2 +- Tests/expected/cosmosDrives.out | 2 +- Tests/expected/cosmosFault.out | 2 +- Tests/expected/cosmosFlip.out | 2 +- Tests/expected/cosmosGrid.out | 2 +- Tests/expected/cosmosMonitor.out | 2 +- Tests/expected/cosmosMonitorRun.out | 2 +- Tests/expected/cosmosRun.out | 2 +- Tests/expected/cosmosSlowDisk.out | 2 +- Tests/expected/cosmosSprite.out | 2 +- Tests/sound.sh | 66 ++++++++++ 12 files changed, 175 insertions(+), 94 deletions(-) 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."