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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-05 14:42:45 -04:00
co-authored by Claude Opus 5
parent 35c6708e46
commit 1b251fb290
12 changed files with 175 additions and 94 deletions
+99 -84
View File
@@ -8,26 +8,28 @@
; does not interrupt, so a program that only had the sound device could play a tune at ; 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. ; 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 ; The timer is the other half. A period in cycles, a repeat bit, and a line when one has gone
; beat on this machine, and it is counted in the machine's own cycles, so this plays at the ; by: a beat this program SETS rather than one it borrows. Every duration below is a count of
; same speed whether the emulator is running at a megahertz or as fast as it can go. Every ; ticks, and the tick is named in the machine's own cycles, so this plays at the same speed
; duration below is in frames: 30 is half a second. ; 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. ; ---- The tick is a sixteenth note, and that is the whole point ----
; 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.
; ;
; ---- 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 ; This program used to count the SCREEN's frames, because until the timer existed the frame
; is a whole number of 16.67 ms, so a note worth a third of a beat cannot be written at all - ; was the only regular beat on the machine. A frame is 16,667 cycles, so a sixteenth note is
; and the way round it is to pick a tempo whose subdivisions happen to land on whole frames, ; seven and a half of them and could not be asked for at all. The arpeggio below was written
; which is making the tune fit the machine rather than the other way round. The theme below ; as seven frames, the nearest whole one, which is six and a half per cent fast; and the way
; was written to the frame and is a few cents of tempo away from what it wants to be. ; 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 ; SO THE DURATIONS HERE ARE NOT A CONVERSION OF THE OLD ONES. They are what the music wanted
; the screen should not be the clock is that a display refresh and a music routine have no ; in the first place, now that it can be written down: an eighth is two ticks, a quarter is
; reason to share a rate. ; 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 ---- ; ---- What a patch costs and what a note costs ----
; ;
@@ -143,10 +145,18 @@ start:
; ---- The beat ---- ; ---- The beat ----
; ;
; Ask the screen to interrupt at each frame, and let interrupts in. The screen does not do ; The period first and the control byte second, because writing the control byte with the
; this unless it is asked. ; 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 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 SIF
; ---- The tune ---- ; ---- The tune ----
@@ -160,11 +170,11 @@ nextNote:
BRA finished BRA finished
OUTA 0x44 ; Writing the note is what starts it. OUTA 0x44 ; Writing the note is what starts it.
INCD.0 INCD.0
LDB.0 ; How many frames it lasts. LDB.0 ; How many ticks it lasts.
INCD.0 INCD.0
holdNote: 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 DECB
BNB holdNote BNB holdNote
@@ -175,9 +185,10 @@ holdNote:
BRI nextNote BRI nextNote
finished: finished:
; Let the last note ring out rather than cutting it off, then put the screen back the way it ; Let the last note ring out rather than cutting it off, then give the timer back - stopping
; was found - and stop asking to be interrupted before taking away what catches it. ; it before taking away what catches it, because this program is about to stop existing and
INIB 0d45 ; an interrupt with no handler installed is a fault.
INIB 0d6
lastRing: lastRing:
WAIT WAIT
DECB DECB
@@ -185,13 +196,17 @@ lastRing:
CIF CIF
RSTA RSTA
OUTA 0x35 OUTA 0x51
SWI osExit SWI osExit
; Sixty times a second, and it has nothing to do. WAIT only needs something to have happened, ; The tick has nothing to do: the loop above is the player, and WAIT only needs something to
; and this is the something. A handler still has to exist: an interrupt with nothing installed ; have HAPPENED. A handler still has to exist, because an interrupt with nothing installed to
; to catch it is a fault. ; catch it is a fault.
frame: ;
; 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 RETI
#Data #Data
@@ -200,85 +215,85 @@ frame:
; ---- Notes and how long they last ---- ; ---- 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 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 ; 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. ; nothing plays, and this program did not need it.
Tune: Tune:
0d60 0d15 ; C 0d60 0d2 ; C
0d64 0d15 ; E 0d64 0d2 ; E
0d67 0d15 ; G 0d67 0d2 ; G
0d72 0d30 ; C, an octave up, held twice as long 0d72 0d4 ; C, an octave up, held twice as long
0d71 0d15 ; B 0d71 0d2 ; B
0d67 0d15 ; G 0d67 0d2 ; G
0d64 0d15 ; E 0d64 0d2 ; E
0d60 0d45 ; and home 0d60 0d6 ; and home
0x00 0x00
Theme: Theme:
0x30 0d15 ; C 0x30 0d2 ; C
0x35 0d15 ; F 0x35 0d2 ; F
0x3C 0d15 ; C+ 0x3C 0d2 ; C+
0x40 0d30 ; E+ 0x40 0d4 ; E+
0x48 0d30 ; C++ 0x48 0d4 ; C++
0x45 0d30 ; A+ 0x45 0d4 ; A+
0x47 0d30 ; B+ 0x47 0d4 ; B+
0x43 0d45 ; G+ 0x43 0d6 ; G+
; repeat four times. ; repeat four times.
0x2A 0d7 ; Gs 0x2A 0d1 ; Gs
0x33 0d7 ; Ef 0x33 0d1 ; Ef
0x3C 0d7 ; C+ 0x3C 0d1 ; C+
; ;
0x2A 0d7 ; Gs 0x2A 0d1 ; Gs
0x33 0d7 ; Ef 0x33 0d1 ; Ef
0x3C 0d7 ; C+ 0x3C 0d1 ; C+
; ;
0x2A 0d7 ; Gs 0x2A 0d1 ; Gs
0x33 0d7 ; Ef 0x33 0d1 ; Ef
0x3C 0d7 ; C+ 0x3C 0d1 ; C+
; ;
0x2A 0d7 ; Gs 0x2A 0d1 ; Gs
0x33 0d7 ; Ef 0x33 0d1 ; Ef
0x3C 0d7 ; C+ 0x3C 0d1 ; C+
; next chord ; next chord
0x2E 0d7 ; Bf 0x2E 0d1 ; Bf
0x35 0d7 ; F 0x35 0d1 ; F
0x3E 0d7 ; D+ 0x3E 0d1 ; D+
; next chord ; next chord
0x2E 0d7 ; Bf 0x2E 0d1 ; Bf
0x35 0d7 ; F 0x35 0d1 ; F
0x3E 0d7 ; D+ 0x3E 0d1 ; D+
; ;
; next chord ; next chord
0x2E 0d7 ; Bf 0x2E 0d1 ; Bf
0x35 0d7 ; F 0x35 0d1 ; F
0x3E 0d7 ; D+ 0x3E 0d1 ; D+
; ;
; next chord ; next chord
0x2E 0d7 ; Bf 0x2E 0d1 ; Bf
0x35 0d7 ; F 0x35 0d1 ; F
0x3E 0d7 ; D+ 0x3E 0d1 ; D+
; Finally on the C major ; Finally on the C major
0x30 0d7 ; C 0x30 0d1 ; C
0x37 0d7 ; G 0x37 0d1 ; G
0x40 0d7 ; E+ 0x40 0d1 ; E+
; ;
0x30 0d7 ; C 0x30 0d1 ; C
0x37 0d7 ; G 0x37 0d1 ; G
0x40 0d7 ; E+ 0x40 0d1 ; E+
; ;
0x30 0d7 ; C 0x30 0d1 ; C
0x37 0d7 ; G 0x37 0d1 ; G
0x40 0d7 ; E+ 0x40 0d1 ; E+
; ;
0x30 0d7 ; C 0x30 0d1 ; C
0x37 0d7 ; G 0x37 0d1 ; G
0x40 0d7 ; E+ 0x40 0d1 ; E+
; ;
0x18 0d60 ; C bass 0x18 0d8 ; C bass
0x00 0x00
#Vectors #Vectors
Boot start Boot start
Device 0x30 frame Device 0x50 tick
+1 -1
View File
@@ -31,7 +31,7 @@ Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
loops.script 272 loops.script 272
tune.sbx 306 tune.sbx 318
notes.txt 21 notes.txt 21
Apps <dir> Apps <dir>
hi.script 121 hi.script 121
+1 -1
View File
@@ -21,7 +21,7 @@ Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
loops.script 272 loops.script 272
tune.sbx 306 tune.sbx 318
notes.txt 21 notes.txt 21
Apps <dir> Apps <dir>
hi.script 121 hi.script 121
+1 -1
View File
@@ -38,7 +38,7 @@ Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
loops.script 272 loops.script 272
tune.sbx 306 tune.sbx 318
notes.txt 21 notes.txt 21
Apps <dir> Apps <dir>
hi.script 121 hi.script 121
+1 -1
View File
@@ -28,7 +28,7 @@ Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
loops.script 272 loops.script 272
tune.sbx 306 tune.sbx 318
notes.txt 21 notes.txt 21
Apps <dir> Apps <dir>
hi.script 121 hi.script 121
+1 -1
View File
@@ -28,7 +28,7 @@ Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
loops.script 272 loops.script 272
tune.sbx 306 tune.sbx 318
notes.txt 21 notes.txt 21
Apps <dir> Apps <dir>
hi.script 121 hi.script 121
+1 -1
View File
@@ -114,7 +114,7 @@ Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
loops.script 272 loops.script 272
tune.sbx 306 tune.sbx 318
notes.txt 21 notes.txt 21
Apps <dir> Apps <dir>
hi.script 121 hi.script 121
+1 -1
View File
@@ -33,7 +33,7 @@ Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
loops.script 272 loops.script 272
tune.sbx 306 tune.sbx 318
notes.txt 21 notes.txt 21
Apps <dir> Apps <dir>
hi.script 121 hi.script 121
+1 -1
View File
@@ -21,7 +21,7 @@ Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
loops.script 272 loops.script 272
tune.sbx 306 tune.sbx 318
notes.txt 21 notes.txt 21
Apps <dir> Apps <dir>
hi.script 121 hi.script 121
+1 -1
View File
@@ -19,7 +19,7 @@ Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
loops.script 272 loops.script 272
tune.sbx 306 tune.sbx 318
notes.txt 21 notes.txt 21
Apps <dir> Apps <dir>
hi.script 121 hi.script 121
+1 -1
View File
@@ -28,7 +28,7 @@ Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
loops.script 272 loops.script 272
tune.sbx 306 tune.sbx 318
notes.txt 21 notes.txt 21
Apps <dir> Apps <dir>
hi.script 121 hi.script 121
+66
View File
@@ -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 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" || 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 echo
if [ "$FAIL" -eq 0 ]; then if [ "$FAIL" -eq 0 ]; then
echo "All $PASS sound checks passed." echo "All $PASS sound checks passed."