The loader, and the reason it is small: everything in a tune is an offset from wherever it was put, so taking one means adding the base to two tables and pointing four voices at their order lists. No sequence is walked. Nothing inside one is an address to be found and corrected, which is the difference between a malformed tune that plays wrongly and one that takes the loader with it. "SBTU", version, the tick in cycles, how many patches and sequences, offsets to the two tables, an order list each, and the patch each voice starts on. The magic is checked before anything else, because from there on the loader follows what the offsets name. break.sh shows what that guard is worth: without it, handing Play a PROGRAM runs the machine away until the cycle limit, rather than saying it is not a tune. PatchTable, SequenceTable and VoiceStart became pointers, so the engine does not care whether a tune came out of a file or was assembled in. Play's built-in tune now hands over the same three addresses a loaded one would, in eight lines - which is what keeps the two paths from drifting, and what made this rung change no scheduler code at all. Tests/maketune.py lays the fixture out byte by byte. IT IS NOT THE COMPILER: the sequences and the patches are literal bytes and the only thing computed is where each piece lands. That is the point - the loader is checked by something that does not share its idea of the format, which is the same reason SplitDisk and sbfs.asm share nothing but a specification. Both notes in the fixture are number 60, so the octave between them is a 0x80 command loading the second patch out of the file: the header, the tick, the relocation, an order list and a patch from a file, measured in one go. Play and the tune live on quiet.img rather than cosmos.img, so a fixture does not move ten recordings every time it changes size. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
180 lines
5.4 KiB
NASM
180 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 VoiceStart gives the low one to voice 0 and the high one
|
|
; to voice 1. 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 MyPatches
|
|
SETD.1 PatchTable
|
|
STD.0.1
|
|
SETD.0 MySequences
|
|
SETD.1 SequenceTable
|
|
STD.0.1
|
|
SETD.0 MyVoiceStart
|
|
SETD.1 VoiceStartAt
|
|
STD.0.1
|
|
SETD.0 Order0
|
|
SETD.1 Voice0
|
|
STD.0.1
|
|
SETD.0 Order1
|
|
SETD.1 Voice1
|
|
STD.0.1
|
|
SETD.0 Order2
|
|
SETD.1 Voice2
|
|
STD.0.1
|
|
SETD.0 Order3
|
|
SETD.1 Voice3
|
|
STD.0.1
|
|
CALL startVoices
|
|
|
|
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
|
|
|
|
; Sixteen ticks. B survives the calls below, because a CALL saves it.
|
|
INIB 0d16
|
|
tickLoop:
|
|
SETD.1 Voice0
|
|
RSTA
|
|
CALL stepVoice
|
|
SETD.1 Voice1
|
|
INIA 0d1
|
|
CALL stepVoice
|
|
DECB
|
|
BRB lastTick
|
|
WAIT
|
|
BRI tickLoop
|
|
|
|
lastTick:
|
|
; The last step started a note and nothing has waited for it yet. Without this the final
|
|
; tick is never rendered, and a window measuring it runs off the end of the samples.
|
|
WAIT
|
|
|
|
done:
|
|
CIF
|
|
RSTA
|
|
OUTA 0x51
|
|
HALT
|
|
|
|
tick:
|
|
RETI
|
|
|
|
#Data
|
|
|
|
; Middle C, then out of the way, then back - against the octave above doing the opposite.
|
|
; ---- The order lists, and what they are for ----
|
|
;
|
|
; Four bars of four ticks each, and the SAME TWO SEQUENCES do nearly all of it. Beat is used
|
|
; twice by voice 0 and twice by voice 1 - four placings of one phrase, written once, and on
|
|
; two different voices, because the voice is a property of where a sequence is placed and not
|
|
; of the sequence.
|
|
;
|
|
; ticks 0-3 4-7 8-11 12-15
|
|
; voice 0 Beat Quiet Beat Switch
|
|
; voice 1 Quiet Beat Beat (its order list ends here)
|
|
Order0:
|
|
0d0 0d1 0d0 0d2 0d3 0xFF
|
|
Order1:
|
|
0d1 0d0 0d0 0xFF ; And this one ENDS, so the last bar has voice 1 stopped rather
|
|
; than resting. A player that missed the terminator would read
|
|
; whatever follows as a sequence index and play it.
|
|
Order2:
|
|
0d3 0xFF
|
|
Order3:
|
|
0d3 0xFF
|
|
|
|
MyPatches:
|
|
PatchLow PatchHigh
|
|
|
|
MySequences:
|
|
Beat Quiet Switch Tail ; 0 to 3
|
|
|
|
MyVoiceStart:
|
|
0d0 0d1 0d0 0d0
|
|
|
|
Beat:
|
|
0d60 0d4 ; Note 60 on both voices. The octave between them is their patches.
|
|
0xFF
|
|
Quiet:
|
|
0d00 0d4
|
|
0xFF
|
|
Switch:
|
|
0x80 0d1 ; Voice 0 takes patch one, which is voice 1's, mid-piece.
|
|
0d60 0d4 ; The same note number it has played all along.
|
|
0xFF
|
|
Tail:
|
|
0d00 0xFF ; Long enough that nothing runs off the end of its order list.
|
|
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.
|
|
|
|
#Include player.asm
|
|
|
|
#Vectors
|
|
Boot start
|
|
Device 0x50 tick
|