Files
SplitBit-Emulator/Programs/testPrograms/fourVoiceTest.asm
T
AnachronautandClaude Opus 5 79d1e2639b M2: four voices on one clock
Play is the scheduler: one tick, four cursors. Every voice keeps its own
place in its own track and its own count of how much longer the note it
is holding lasts, so a voice playing whole notes and a voice playing
eighths cost the same and never have to know about each other. They
share the tick and nothing else.

That is what makes the tick the smallest subdivision in the piece rather
than a note length - it is the only unit four parts can agree on.

A track is pairs of bytes: what to play, then how many ticks it lasts.
MIDI notes stop at 127, so the top of the byte was free and neither the
rest nor the end marker had to be invented - 0 is a rest and 0xFF ends
the track. Examples/tune.asm spent zero on its end marker and so could
not write a rest at all, which one voice can live with and four cannot:
the silences are what make them separate parts rather than a chord.

The state is four bytes a voice, cursor first because that is what LDD
and STD move - a pointer through a pointer, which is what lets this be a
loop over four voices instead of the same code four times. CALL preserves
A and DP0-2, so a caller says which voice it means in two instructions.

The piece is four bars of C, F, G, C with the parts moving at four
different rates, because that is the thing one channel cannot do. All
four channels get the same instrument, which is exactly what M3 replaces.

fourVoiceTest staggers two voices so each gets a stretch alone: middle C
while the other rests, the octave while the first is silent, then both.
The first two are measured for pitch and the third for level, because
TWO NOTES CANNOT BE ASKED THEIR PITCH - the crossing counter adds them
and answers 785 hertz, which is 262 plus 523 and a fact about nothing.
Verified with break.sh: dropping the channel select trips one check,
pointing both voices at one cursor trips three.

It shares Play's design and not its code, and is smaller - no track ends
in it, so there is no live flag and no count of what is still playing.

Play.sbx on the disk is why ten recordings moved: one added line each,
and the file count with it. Nothing else in them changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 15:03:12 -04:00

182 lines
4.0 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
;
; 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:
RSTA
CALL setUpChannel
INIA 0d1
CALL setUpChannel
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
SETD.1 Voice0
SETD.0 Track0
CALL startVoice
SETD.1 Voice1
SETD.0 Track1
CALL startVoice
; 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
; DP1 a voice, DP0 its track. A count of one, so the first pass fetches the first event.
startVoice:
STD.0.1
DPUP.1 0d2
INIA 0d1
STA.1
RET
; Held at full and released quickly, so a note lasts exactly its ticks and the boundary
; between two of them is something a counter can find.
setUpChannel:
OUTA 0x41
RSTA
OUTA 0x42
INIA 0d2
OUTA 0x43 ; Saw
INIA 0x01
OUTA 0x42
INIA 0xFF
OUTA 0x43
INIA 0x05
OUTA 0x42
INIA 0x01
OUTA 0x43 ; Oscillator 0, on and loud.
INIA 0x20
OUTA 0x42
RSTA
OUTA 0x43 ; No attack
INIA 0x21
OUTA 0x42
RSTA
OUTA 0x43 ; no decay
INIA 0x22
OUTA 0x42
INIA 0xFF
OUTA 0x43 ; and held at full.
INIA 0x23
OUTA 0x42
INIA 0d5
OUTA 0x43
RET
tick:
RETI
#Data
Voice0:
0x00 0x00 0x00 0x00
Voice1:
0x00 0x00 0x00 0x00
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
0d72 0d4
0d72 0d4
0d00 0xFF
#Vectors
Boot start
Device 0x50 tick