M3: an instrument for each voice

Play loads a patch per channel 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. It reads a count and that many parameter and value pairs -
the format SoundPatch writes - and understands nothing else about them,
which keeps SoundPatch the only thing that knows what soundThing's JSON
means.

FOUR PATCHES CAN BE UP AT ONCE, and that is the whole rung. It is the
first use of d361ea1: before it the LFOs belonged to the whole device, so
whichever patch loaded last owned them for every voice. Kalimba has LFO 0
switched off and the other three have it on, so on the old device this
piece would have played all four parts with the arpeggio's setting -
which is exactly the fault that stopped Lunar Porter's low fuel warning
trilling after the first landing of a run.

The eight patches are the user's, brought over from soundThing; four are
used here and the rest are a palette for the next piece.

The test now asks BOTH VOICES FOR NOTE 60 and gets an octave, because
their patches differ in one parameter and the lower is loaded first. A
device where a patch was global would have the second overwrite the first
and the two would answer in unison. Verified with break.sh: loading both
patches onto one channel flips the octave onto the wrong voice and the
ratio inverts to 0.499.

The test patches carry a release, and that was measured rather than
assumed: without one the first voice's note was still fading into the
second voice's window, which read 634 hertz - not a note, not an octave,
and a reminder that a crossing counter given two notes answers with
neither.

Play.sbx went 379 -> 740 bytes, which is four patches of 47 pairs each,
and is why ten recordings moved. makedisks.sh needed the Sounds include
path that Lander's line already had.

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 16:02:06 -04:00
co-authored by Claude Opus 5
parent 1687422619
commit ce1c0517aa
27 changed files with 761 additions and 90 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ vars.script 50
blocks.script 343
loops.script 272
tune.sbx 318
Play.sbx 379
Play.sbx 740
notes.txt 21
Apps <dir>
hi.script 121
+1 -1
View File
@@ -22,7 +22,7 @@ vars.script 50
blocks.script 343
loops.script 272
tune.sbx 318
Play.sbx 379
Play.sbx 740
notes.txt 21
Apps <dir>
hi.script 121
+1 -1
View File
@@ -39,7 +39,7 @@ vars.script 50
blocks.script 343
loops.script 272
tune.sbx 318
Play.sbx 379
Play.sbx 740
notes.txt 21
Apps <dir>
hi.script 121
+1 -1
View File
@@ -29,7 +29,7 @@ vars.script 50
blocks.script 343
loops.script 272
tune.sbx 318
Play.sbx 379
Play.sbx 740
notes.txt 21
Apps <dir>
hi.script 121
+1 -1
View File
@@ -29,7 +29,7 @@ vars.script 50
blocks.script 343
loops.script 272
tune.sbx 318
Play.sbx 379
Play.sbx 740
notes.txt 21
Apps <dir>
hi.script 121
+1 -1
View File
@@ -115,7 +115,7 @@ vars.script 50
blocks.script 343
loops.script 272
tune.sbx 318
Play.sbx 379
Play.sbx 740
notes.txt 21
Apps <dir>
hi.script 121
+1 -1
View File
@@ -34,7 +34,7 @@ vars.script 50
blocks.script 343
loops.script 272
tune.sbx 318
Play.sbx 379
Play.sbx 740
notes.txt 21
Apps <dir>
hi.script 121
+1 -1
View File
@@ -22,7 +22,7 @@ vars.script 50
blocks.script 343
loops.script 272
tune.sbx 318
Play.sbx 379
Play.sbx 740
notes.txt 21
Apps <dir>
hi.script 121
+1 -1
View File
@@ -20,7 +20,7 @@ vars.script 50
blocks.script 343
loops.script 272
tune.sbx 318
Play.sbx 379
Play.sbx 740
notes.txt 21
Apps <dir>
hi.script 121
+1 -1
View File
@@ -29,7 +29,7 @@ vars.script 50
blocks.script 343
loops.script 272
tune.sbx 318
Play.sbx 379
Play.sbx 740
notes.txt 21
Apps <dir>
hi.script 121
+3 -2
View File
@@ -180,8 +180,9 @@ printf '#! script\nfor a in 1 2\n for b in x y\n echo $a$b\n end\nend\nset
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/Examples/tune.asm" -o "$WORK/tune.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/tune.sbx" >/dev/null
# And Play, which is the same beat carrying four voices instead of one.
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
# And Play, which is the same beat carrying four voices instead of one - each with an
# instrument of its own out of Sounds, which is what four patches at once looks like.
"$ROOT/Assembler" -I "$ROOT/Programs/Sounds" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Play.asm" -o "$WORK/Play.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Play.sbx" >/dev/null
printf 'this is not a program' > notes.txt
+23 -5
View File
@@ -650,11 +650,15 @@ near "$EIGHTH" 523.3 2 \
# track and its own count of how long the note it is holding lasts, so one can rest while
# another plays. That is the whole of what a channel cannot do on its own.
#
# The program stagger the two voices deliberately, so each gets a stretch where it is the only
# thing sounding and its pitch can be asked for. TWO NOTES AT ONCE CANNOT BE ASKED THEIR
# PITCH: the crossing counter adds them and answers with neither, which is 785 hertz for the
# third window below - the sum of 262 and 523 and a fact about nothing. So the overlap is
# checked for LEVEL instead, which is a question two notes can answer.
# The program staggers the two voices deliberately, so each gets a stretch where it is the
# only thing sounding and its pitch can be asked for. TWO NOTES AT ONCE CANNOT BE ASKED THEIR
# PITCH: a crossing counter given two answers with neither. So the overlap is checked for
# LEVEL instead, which is a question two notes can answer.
#
# The patches carry a release for the same reason. Without one the first voice's note was
# still fading into the second voice's stretch, and the window that should have read one note
# read 634 hertz - which is not a note, not an octave, and was measured before it was written
# down as an assertion.
run fourVoices 1700000 < "$ROOT/Programs/testPrograms/fourVoiceTest.asm" || exit 1
COUNT="$(measure fourVoices count)"
@@ -672,6 +676,20 @@ near "$SECOND" 523.3 2 \
&& result ok "and then they change places" "$SECOND hertz, an octave up, with the first one silent" \
|| result no "and then they change places" "$SECOND hertz, wanted the octave alone"
# ---- And a patch belongs to its channel ----
#
# M3. BOTH TRACKS NAME NOTE 60. The octave between them is in their patches, which differ in
# one parameter, and the lower one is loaded FIRST - so a device where a patch was global
# would have the second overwrite it and both voices would answer in unison.
#
# This is the thing d361ea1 made possible and nothing had used: before it the LFOs belonged to
# the whole device, so whichever patch loaded last owned them for every voice at once.
RATIO="$(python3 -c "import sys; print('%.3f' % (float(sys.argv[1]) / float(sys.argv[2])))" \
"$SECOND" "$FIRST")"
near "$RATIO" 2.0 3 \
&& result ok "a patch belongs to its channel" "both tracks say note 60 and one sounds $RATIO times the other" \
|| result no "a patch belongs to its channel" "the two voices are $RATIO apart, and their patches say two"
# Both voices sounding is louder than either of them alone. Against the QUIETER of the two so
# that the check cannot be passed by one voice getting louder, and by a margin well under the
# two-fold a clean sum would give, because two notes are not in phase and do not simply add.