There is no assembler bug. I reported one and was wrong. A label written in the Data Segment does come out as its address, two bytes, most significant first - implemented in populateOutputBuffers, documented in the Assembler Manual, and correct. What misled me was the test I checked it with: the label was the first thing in an unbased Data Segment, so its address really was 0x0000, and I read the right answer as an unfilled placeholder. So Play was doing at run time what the assembler had already offered to do at assembly time. The voice records now carry their track labels directly, which is exactly the shape LDD reads, and nothing relocates on this machine so the address written is the address it will have. That takes out startVoice, its four call sites, and the eight SETDs that fed them: 450 bytes to 379, and the initial state of a voice is now something you can read rather than something you have to follow the code to work out. The comment claiming otherwise is gone from Play.asm, and the same change is made in fourVoiceTest. The music is unchanged - bar by bar the render matches to within one per cent, which is the program loading a shade sooner because it is smaller. Ten recordings moved for the same reason: 446 to 379, and nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
170 lines
4.0 KiB
NASM
170 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
|
|
|
|
; 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
|
|
|
|
; 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
|
|
|
|
; 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
|
|
0d72 0d4
|
|
0d72 0d4
|
|
0d00 0xFF
|
|
|
|
#Vectors
|
|
Boot start
|
|
Device 0x50 tick
|