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
192 lines
5.4 KiB
NASM
192 lines
5.4 KiB
NASM
; Four voices on one tick, staggered so that each can be heard on its own.
|
|
;
|
|
; M2 of the music player. What is being checked is that the voices share the tick and NOTHING
|
|
; ELSE: each keeps its own place in its own track and its own count of how long the note it is
|
|
; holding lasts, so one can rest while another plays.
|
|
;
|
|
; Twelve ticks of 125,000 cycles, which is a sixteenth note at 120 beats a minute:
|
|
;
|
|
; ticks 0-3 voice 0 plays middle C voice 1 rests
|
|
; ticks 4-7 voice 0 rests voice 1 plays the C above it
|
|
; ticks 8-11 voice 0 plays middle C voice 1 plays the C above it
|
|
;
|
|
; BOTH VOICES ARE ASKED FOR NOTE 60. The octave between them is in their patches, which is
|
|
; M3: a patch belongs to its channel, so four of them can be up at once. Under a device where
|
|
; it did not, the second patch loaded would own both voices and they would sound in unison.
|
|
;
|
|
; So the first two windows have one voice in them and can be measured for pitch, and the third
|
|
; has both and can be measured for level. Two voices sounding at once cannot be asked their
|
|
; pitch - a crossing counter given two notes answers with neither.
|
|
;
|
|
; THIS IS DELIBERATELY NOT Apps/Play.asm. It shares the design and not the code, and it is
|
|
; smaller: no track ever ends here, so there is no live flag and no count of what is still
|
|
; playing. What it has is the part worth pinning - a cursor and a countdown per voice, moved
|
|
; with LDD and STD.
|
|
;
|
|
; Written by Anachronaut
|
|
|
|
#Program
|
|
start:
|
|
; A patch each, in the format SoundPatch writes: a count and that many pairs. They differ
|
|
; in ONE parameter - the octave - and the low one is loaded first, so if a patch belonged to
|
|
; the device rather than to its channel the second would overwrite the first and both voices
|
|
; would sound the same.
|
|
SETD.0 PatchLow
|
|
RSTA
|
|
CALL loadPatch
|
|
SETD.0 PatchHigh
|
|
INIA 0d1
|
|
CALL loadPatch
|
|
|
|
INIA 0x60
|
|
OUTA 0x46
|
|
|
|
; 125,000 cycles a tick. The period before the control byte, because writing control with
|
|
; the run bit set is what loads it.
|
|
INIA 0x01
|
|
OUTA 0x52
|
|
INIA 0xE8
|
|
OUTA 0x53
|
|
INIA 0x48
|
|
OUTA 0x54
|
|
INIA 0x07
|
|
OUTA 0x51
|
|
SIF
|
|
|
|
; Twelve ticks. B survives the calls below, because a CALL saves it.
|
|
INIB 0d12
|
|
tickLoop:
|
|
SETD.1 Voice0
|
|
RSTA
|
|
CALL stepVoice
|
|
SETD.1 Voice1
|
|
INIA 0d1
|
|
CALL stepVoice
|
|
DECB
|
|
BRB lastTick
|
|
WAIT
|
|
BRI tickLoop
|
|
|
|
lastTick:
|
|
; The twelfth step started a note and nothing has waited for it yet. Without this the last
|
|
; tick is never rendered, and a window measuring it runs off the end of the samples.
|
|
WAIT
|
|
|
|
done:
|
|
CIF
|
|
RSTA
|
|
OUTA 0x51
|
|
HALT
|
|
|
|
; A is the channel and DP1 the voice's four bytes: cursor high, cursor low, count, spare.
|
|
; Both survive the CALL that got here.
|
|
stepVoice:
|
|
SETD.3 ThisChannel
|
|
STA.3
|
|
|
|
DPUP.1 0d2
|
|
LDA.1
|
|
DECA
|
|
STA.1 ; One tick less of whatever is sounding.
|
|
DPDN.1 0d2
|
|
BNA stepDone
|
|
|
|
; The count ran out. Select the channel first: every sound port writes to whichever channel
|
|
; was named last, so a voice that forgot would be playing somebody else's part.
|
|
LDA.3
|
|
OUTA 0x41
|
|
RSTA
|
|
OUTA 0x45 ; Let go of the note that just ended.
|
|
|
|
LDD.0.1 ; DP0 is this voice's place in its track.
|
|
LDA.0
|
|
INCD.0
|
|
LDB.0 ; How many ticks it lasts.
|
|
INCD.0
|
|
STD.0.1
|
|
DPUP.1 0d2
|
|
STB.1
|
|
|
|
; A is still the note. Zero is a rest: the ticks pass with nothing started.
|
|
BRA stepDone
|
|
OUTA 0x44
|
|
stepDone:
|
|
RET
|
|
|
|
; ---- A patch, onto the channel named by A ----
|
|
;
|
|
; The channel is selected first: every parameter write lands on whichever channel was named
|
|
; last, so a patch loaded without one would be rewriting somebody else's voice.
|
|
loadPatch:
|
|
OUTA 0x41
|
|
LDB.0
|
|
INCD.0
|
|
loadPatchPair:
|
|
LDA.0
|
|
OUTA 0x42
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0x43
|
|
INCD.0
|
|
DECB
|
|
BNB loadPatchPair
|
|
RET
|
|
|
|
tick:
|
|
RETI
|
|
|
|
#Data
|
|
|
|
; Cursor high, cursor low, count, spare. A label written here comes out as its address, most
|
|
; significant first, which is the shape LDD reads - so a voice starts pointed at its track
|
|
; with no code at all. The count starts at one so the first pass runs it out and fetches.
|
|
Voice0:
|
|
Track0 0d1 0d0
|
|
Voice1:
|
|
Track1 0d1 0d0
|
|
ThisChannel:
|
|
0x00
|
|
|
|
; Middle C, then out of the way, then back - against the octave above doing the opposite.
|
|
Track0:
|
|
0d60 0d4
|
|
0d00 0d4
|
|
0d60 0d4
|
|
0d00 0xFF
|
|
Track1:
|
|
0d00 0d4
|
|
0d60 0d4
|
|
0d60 0d4
|
|
0d00 0xFF
|
|
|
|
; Seven pairs each, and only the last of them differs: the octave, centred on 128. So the two
|
|
; voices below are asked for THE SAME NOTE NUMBER and answer an octave apart, which nothing
|
|
; but a patch of their own could do.
|
|
PatchLow:
|
|
0d8
|
|
0x00 0d2 ; Saw
|
|
0x01 0xFF ; at full gain
|
|
0x05 0d1 ; and switched on.
|
|
0x20 0d0 ; No attack
|
|
0x21 0d0 ; no decay
|
|
0x22 0xFF ; and held at full, so a note lasts exactly its ticks
|
|
0x23 0d5 ; and goes quickly when the gate drops, which the windows below
|
|
; depend on: a note still fading into the next voice's stretch is
|
|
; a second pitch in it, and a crossing counter given two answers
|
|
; with neither.
|
|
0x04 0d128 ; Octave, centred.
|
|
PatchHigh:
|
|
0d8
|
|
0x00 0d2
|
|
0x01 0xFF
|
|
0x05 0d1
|
|
0x20 0d0
|
|
0x21 0d0
|
|
0x22 0xFF
|
|
0x23 0d5
|
|
0x04 0d129 ; And this one an octave up.
|
|
|
|
#Vectors
|
|
Boot start
|
|
Device 0x50 tick
|