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
+45 -39
View File
@@ -45,19 +45,30 @@
#Base 0x5000
start:
; ---- A sound to play it with ----
; ---- An instrument for each voice ----
;
; The same on all four channels, which is exactly what M3 replaces: a patch per voice, out
; of the format SoundPatch already writes. Until then they are four of the same instrument,
; which is enough to hear four parts and not enough to enjoy them.
; Four patches, designed in soundThing and converted by SoundPatch, which is the only thing
; here that understands what a patch means. This reads a count and that many parameter and
; value pairs and knows nothing else about them.
;
; A PATCH BELONGS TO ITS CHANNEL, which is why four of them can be up at once. It did not
; used to: the LFOs belonged to the whole device, so whichever patch was loaded last owned
; them for every voice. Kalimba has LFO 0 switched off and the other three have it on, so
; 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.
SETD.0 OboePatch
RSTA
CALL setUpChannel
CALL loadPatch ; The melody sings, so it is the one with a voice.
SETD.0 StringsPatch
INIA 0d1
CALL setUpChannel
CALL loadPatch ; A pad underneath it.
SETD.0 SquarePatch
INIA 0d2
CALL setUpChannel
CALL loadPatch ; And a square wave for the bass, which is where one belongs.
SETD.0 KalimbaPatch
INIA 0d3
CALL setUpChannel
CALL loadPatch ; Plucked, for the arpeggio.
INIA 0x60 ; Room over the top for four voices at once.
OUTA 0x46
@@ -175,39 +186,27 @@ stepEnded:
STA.3
RET
; A plucked sound, on the channel named by A.
setUpChannel:
; ---- A patch, onto the channel named by A ----
;
; A count, then that many pairs of parameter and value: the format SoundPatch writes and the
; one Lander already plays. B holds what is left, which costs nothing to keep - a CALL saves
; B, so a caller's count is not disturbed by a patch being loaded inside its loop.
;
; The channel is selected FIRST. Every parameter write below lands on whichever channel was
; named last, so a patch loaded without one would be quietly rewriting somebody else's voice.
loadPatch:
OUTA 0x41
RSTA
LDB.0
INCD.0
loadPatchPair:
LDA.0
OUTA 0x42
INIA 0d2
OUTA 0x43 ; A saw, which has harmonics enough to hear a filter on.
INIA 0x01
OUTA 0x42
INIA 0xFF
OUTA 0x43 ; Oscillator 0 at full gain
INIA 0x05
OUTA 0x42
INIA 0x01
OUTA 0x43 ; and switched on, which is a separate write and the one that
; matters: a gain on an oscillator that is off does nothing.
INIA 0x20
OUTA 0x42
INIA 0d8
OUTA 0x43 ; Almost no attack
INIA 0x21
OUTA 0x42
INIA 0d140
OUTA 0x43 ; a long decay
INIA 0x22
OUTA 0x42
INIA 0d60
OUTA 0x43 ; held quietly rather than at nothing, so a long note is still
; there when the next one starts
INIA 0x23
OUTA 0x42
INIA 0d40
OUTA 0x43 ; and a short release when the gate drops.
INCD.0
LDA.0
OUTA 0x43
INCD.0
DECB
BNB loadPatchPair
RET
; The tick has nothing to do: the loop above is the player and WAIT only needs something to
@@ -294,6 +293,13 @@ Track3:
0d00 0d16 ; and out, so the last bar is the other three
0xFF
; The instruments, at the bottom because each brings its own #Data and a base has to come
; before anything is in the segment it bases.
#Include oboe.asm
#Include strings.asm
#Include square.asm
#Include kalimba.asm
#Vectors
Boot start