furnace/src/asm/6502/stream.s

587 lines
11 KiB
ArmAsm
Raw Normal View History

; Furnace Command Stream player for 6502 architecture
; written by tildearrow
; usage:
; call fcsInit - this sets A to 0 on success or 1 on failure
; call fcsTick on every frame/tick/whatever
; - call your dispatch implementation's tick function afterwards
; notes:
; - short pointers only
; - little-endian only!
.include "6502base.i"
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; constants
FCS_MAX_CHAN=8 ; maximum number of channels (up to 127, but see below!)
FCS_MAX_STACK=16 ; stack depth per channel (FCS_MAX_STACK*FCS_MAX_CHAN<256)
; player constants - change if necessary
fcsAddrBase=$20 ; player state address base
2025-04-15 19:48:51 -04:00
fcsZeroPage=$10 ; player state address base for zero-page-mandatory variables
fcsGlobalStack=$200 ; player stack (best placed in a page)
fcsPtr=$8000 ; pointer to command stream
fcsVolMax=fcsVolMaxExample ; pointer to max channel volume array
; calculated from player constants
fcsDelays=fcsPtr+8
fcsSpeedDial=fcsPtr+24
; command call table
; - see src/engine/dispatch.h for a list of commands to be potentially handled
; - do not implement HINT commands - the player will never send these
; - no need to implement commands not pertaining to the target system
; - a zero pointer means "don't handle"
2025-04-15 19:48:51 -04:00
fcsCmdTableLow=fcsCmdTableExample
fcsCmdTableHigh=fcsCmdTableExample
; variables
; these may be read by your command handling routines
fcsArg0=fcsAddrBase ; int
fcsArg1=fcsAddrBase+4 ; int
; something else
fcsTicks=fcsAddrBase+8 ; short
; temporary variables
fcsSendVolume=fcsAddrBase+10 ; char
fcsSendPitch=fcsAddrBase+11 ; char
2025-04-17 19:58:11 -04:00
fcsArpSpeed=fcsAddrBase+12 ; char
2025-04-15 19:48:51 -04:00
fcsCmd=fcsAddrBase+16 ; char[8]
fcsTempPtr=fcsZeroPage ; short
; channel state
2025-04-15 19:48:51 -04:00
chanPC=fcsZeroPage+2 ; short
chanTicks=fcsAddrBase+24 ; short
chanStackPtr=fcsAddrBase+24+(FCS_MAX_CHAN*2) ; char
chanNote=fcsAddrBase+24+(FCS_MAX_CHAN*2)+1 ; char
chanVibratoPos=fcsAddrBase+24+(FCS_MAX_CHAN*4) ; char
2025-04-17 19:58:11 -04:00
chanVibrato=fcsAddrBase+24+(FCS_MAX_CHAN*4)+1 ; char
chanPitch=fcsAddrBase+24+(FCS_MAX_CHAN*6) ; char
chanArp=fcsAddrBase+24+(FCS_MAX_CHAN*6)+1 ; char
chanPortaSpeed=fcsAddrBase+24+(FCS_MAX_CHAN*8) ; char
chanPortaTarget=fcsAddrBase+24+(FCS_MAX_CHAN*8)+1 ; char
chanVol=fcsAddrBase+24+(FCS_MAX_CHAN*10) ; short
chanVolSpeed=fcsAddrBase+24+(FCS_MAX_CHAN*12) ; short
chanPan=fcsAddrBase+24+(FCS_MAX_CHAN*14) ; short
; may be used for driver detection
fcsDriverInfo:
.db "Furnace"
.db 0
2025-04-15 19:48:51 -04:00
; note on null
fcsNoteOnNull:
lda #0
sta chanVibratoPos,x
jsr fcsDispatchCmd
rts
; note off, note off env, env release
fcsNoArgDispatch:
jsr fcsDispatchCmd
rts
fcsOneByteDispatch:
tya
pha
jsr fcsReadNext
sta fcsArg0
pla
tay
jsr fcsDispatchCmd
rts
2025-04-17 05:39:51 -04:00
fcsPrePorta:
jsr fcsReadNext
pha
and #$80
sta fcsArg0
pla
and #$40
sta fcsArg1
ldy #$0c
jsr fcsDispatchCmd
rts
2025-04-17 19:58:11 -04:00
fcsArpTime:
jsr fcsReadNext
sta fcsArpSpeed
rts
fcsVibrato:
jsr fcsReadNext
sta chanVibrato,x
rts
; TODO
fcsVibRange:
fcsVibShape:
jsr fcsReadNext
rts
fcsPitch:
jsr fcsReadNext
sta chanPitch,x
lda #1
sta fcsSendPitch
rts
fcsArpeggio:
jsr fcsReadNext
sta chanArp,x
rts
fcsVolume:
jsr fcsReadNext
sta chanVol+1,x
lda #0
sta chanVol,x
lda #1
sta fcsSendVolume
rts
fcsVolSlide:
jsr fcsReadNext
sta chanVolSpeed,x
jsr fcsReadNext
sta chanVolSpeed+1,x
rts
fcsPorta:
jsr fcsReadNext
sta chanPortaTarget,x
jsr fcsReadNext
sta chanPortaSpeed,x
rts
fcsLegato:
jsr fcsReadNext
sta chanNote,x
sta fcsArg0
ldy #11
jsr fcsDispatchCmd
rts
fcsVolSlideTarget:
jsr fcsReadNext
sta chanVolSpeed,x
jsr fcsReadNext
sta chanVolSpeed+1,x
; TODO: we don't support this yet...
jsr fcsReadNext
jsr fcsReadNext
rts
fcsNoOpOneByte:
jsr fcsReadNext
rts
fcsPan:
jsr fcsReadNext
sta chanPan,x
sta fcsArg0
jsr fcsReadNext
sta chanPan+1,x
sta fcsArg1
ldy #10
jsr fcsDispatchCmd
rts
fcsOptPlaceholder:
jsr fcsReadNext
jsr fcsReadNext
jsr fcsReadNext
rts
fcsCallI:
; get address
jsr fcsReadNext
pha
jsr fcsReadNext
pha
; ignore next two bytes
jsr fcsIgnoreNext
jsr fcsIgnoreNext
jsr fcsPushCall
pla
sta chanPC+1,x
pla
sta chanPC,x
rts
fcsOffWait:
ldy #0
sty chanTicks+1,x
iny
sty chanTicks,x
jsr fcsDispatchCmd
rts
; TODO
fcsFullCmd:
rts
fcsCall:
; get address
jsr fcsReadNext
pha
jsr fcsReadNext
pha
jsr fcsPushCall
pla
sta chanPC+1,x
pla
sta chanPC,x
rts
; push channel PC to stack
fcsPushCall:
lda chanStackPtr,x
tay
lda chanPC,x
sta fcsGlobalStack,y
iny
lda chanPC+1,x
sta fcsGlobalStack,y
iny
tya
sta chanStackPtr,x
rts
; retrieve channel PC from stack
fcsRet:
lda chanStackPtr,x
tay
dey
lda fcsGlobalStack,y
sta chanPC+1,x
dey
lda fcsGlobalStack,y
sta chanPC,x
tya
sta chanStackPtr,x
rts
fcsJump:
; get address
jsr fcsReadNext
tay
jsr fcsReadNext
; ignore next two bytes
jsr fcsIgnoreNext
jsr fcsIgnoreNext
; a has high byte
; y has low byte
sty chanPC,x
sta chanPC+1,x
rts
; TODO
fcsTickRate:
rts
fcsWaitS:
jsr fcsReadNext
sta chanTicks,x
jsr fcsReadNext
sta chanTicks+1,x
rts
fcsWaitC:
jsr fcsReadNext
sta chanTicks,x
lda #0
sta chanTicks+1,x
rts
fcsWait1:
ldy #1
sty chanTicks,x
dey
sty chanTicks+1,x
rts
fcsStop:
lda #0
sta chanPC,x
sta chanPC+1,x
rts
fcsNoOp:
rts
2025-04-17 05:39:51 -04:00
2025-04-15 19:48:51 -04:00
; x: channel*2
; y: command
fcsDispatchCmd:
; read command call table
lda fcsCmdTableLow,y
sta fcsTempPtr
lda fcsCmdTableHigh,y
sta fcsTempPtr+1
; check for zero
lda fcsTempPtr
ora fcsTempPtr
beq + ; get out
2025-04-15 19:48:51 -04:00
; handle command in dispatch code
jmp (fcsTempPtr)
; only if pointer is zero
+ rts
2025-04-15 19:48:51 -04:00
; x: channel*2
; a is set to next byte
fcsReadNext:
; a=chanPC[x]+fcsPtr
clc
lda chanPC,x
adc #>fcsPtr
sta fcsTempPtr
2025-04-15 19:48:51 -04:00
lda chanPC+1,x
adc #<fcsPtr
2025-04-15 19:48:51 -04:00
sta fcsTempPtr+1
; increase PC
inc chanPC,x
bne +
2025-04-15 19:48:51 -04:00
inc chanPC+1,x
; read byte and put it into a
; this is at the end to ensure flags are set properly
+ ldy #0
lda (fcsTempPtr),y
2025-04-15 19:48:51 -04:00
rts
; x: channel*2
fcsIgnoreNext:
; increase PC
inc chanPC,x
bne +
inc chanPC+1,x
+ rts
; x: channel*2 (for speed... char variables are interleaved)
; read commands
fcsChannelCmd:
; read next byte
2025-04-15 19:48:51 -04:00
jsr fcsReadNext
; process and read arguments
2025-04-15 19:48:51 -04:00
; if (a<0xb3)
bmi fcsNote ; handle $00-$7f
cmp #$b4
bpl fcsCheckOther
2025-04-15 19:48:51 -04:00
; this is a note
fcsNote:
sta fcsArg0
sta chanNote,x
lda #0
tay
sta chanVibratoPos,x
; call DIV_CMD_NOTE_ON
jsr fcsDispatchCmd
rts
; check other instructions
fcsCheckOther:
; check for preset delays
cmp #$f0
bmi fcsOther
; handler for preset delays
fcsPresetDelay:
; load preset delay and store it
tay
lda fcsPtr+8-240,y
sta chanTicks,x
lda #0
sta chanTicks+1,x
rts
2025-04-15 19:48:51 -04:00
; other instructions
fcsOther:
; call respective handler
tay
lda fcsInsTableLow-180,y
2025-04-15 19:48:51 -04:00
sta fcsTempPtr
lda fcsInsTableHigh-180,y
2025-04-15 19:48:51 -04:00
sta fcsTempPtr+1
jmp (fcsTempPtr)
; x: channel*2
; stuff that goes after command reading
fcsChannelPost:
; do volume
; do pitch
; do portamento
; do arpeggio
; end
rts
; x: channel*2
fcsDoChannel:
; initialize
lda #0
sta fcsSendVolume
sta fcsSendPitch
; check whether this channel is halted (PC = 0)
lda chanPC,x
ora chanPC+1,x
beq +
rts
; channel not halted... begin processing
; chanTicks--
+ lda chanTicks,x
sec
sbc #1
sta chanTicks,x
bne + ; skip if our counter isn't zero
; ticks lower is zero; check upper byte
ldy chanTicks+1,x
beq fcsDoChannelLoop ; go to read commands if it's zero as well
; decrease ticks upper
dey
sty chanTicks+1,x
; process channel stuff
jsr fcsChannelPost
rts
; ticks is zero... read commands until chanTicks is set
fcsDoChannelLoop:
lda chanTicks,x
ora chanTicks+1,x
bne + ; get out if chanTicks is no longer zero
jsr fcsChannelCmd ; read next command
jmp fcsDoChannelLoop
+ jsr fcsChannelPost
; end
rts
fcsTick:
; update channel state
; for (x=0; x<FCS_MAX_CHAN; x++)
ldx #0
- jsr fcsDoChannel
inx
inx
cpx #FCS_MAX_CHAN*2
bne -
; increase tick counter
inc fcsTicks
bne +
inc fcsTicks+1
2025-04-15 19:48:51 -04:00
; end
+ rts
fcsInit:
; set all tick counters to 1
lda #1
ldy #0
ldx #(FCS_MAX_CHAN*2)
- dex
2025-04-15 19:48:51 -04:00
sty chanTicks,x
dex
2025-04-15 19:48:51 -04:00
sta chanTicks,x
bne -
; set channel program counters
ldx #(FCS_MAX_CHAN*2)
- dex
2025-04-15 19:48:51 -04:00
lda fcsPtr+40,x
sta chanPC,x
bne -
; success
lda #0
rts
; floor(127*sin((x/64)*(2*pi)))
fcsVibTable:
.db 0, 12, 24, 36, 48, 59, 70, 80, 89, 98, 105, 112, 117, 121, 124, 126
.db 127, 126, 124, 121, 117, 112, 105, 98, 89, 80, 70, 59, 48, 36, 24, 12
.db 0, -12, -24, -36, -48, -59, -70, -80, -89, -98, -105, -112, -117, -121, -124, -126
.db -126, -126, -124, -121, -117, -112, -105, -98, -89, -80, -70, -59, -48, -36, -24, -12
; COMMAND TABLE
; $b4 fcsNoArgDispatch,
; $b5 fcsNoArgDispatch,
; $b6 fcsNoArgDispatch,
; $b7 fcsNoArgDispatch,
; $b8 fcsOneByteDispatch,
; $b9 fcsNoOp,
; $ba fcsNoOp,
; $bb fcsNoOp,
; $bc fcsNoOp,
; $bd fcsNoOp,
; $be fcsNoOp,
; $bf fcsNoOp,
; $c0 fcsPrePorta,
; $c1 fcsArpTime,
; $c2 fcsVibrato,
; $c3 fcsVibRange,
; $c4 fcsVibShape,
; $c5 fcsPitch,
; $c6 fcsArpeggio,
; $c7 fcsVolume,
; $c8 fcsVolSlide,
; $c9 fcsPorta,
; $ca fcsLegato,
; $cb fcsVolSlideTarget,
; $cc fcsNoOpOneByte,
; $cd fcsNoOpOneByte,
; $ce fcsNoOpOneByte,
; $cf fcsPan,
; $d0 fcsOptPlaceholder,
; $d1 fcsNoOp,
; $d2 fcsNoOp,
; $d3 fcsNoOp,
; $d4 fcsNoOp,
; $d5 fcsCallI,
; $d6 fcsOffWait,
; $d7 fcsFullCmd,
; $d8 fcsCall,
; $d9 fcsRet,
; $da fcsJump,
; $db fcsTickRate,
; $dc fcsWaitS,
; $dd fcsWaitC,
; $de fcsWait1,
; $df fcsStop,
fcsInsTableLow:
.db >fcsNoArgDispatch, >fcsNoArgDispatch, >fcsNoArgDispatch, >fcsNoArgDispatch, >fcsOneByteDispatch, >fcsNoOp, >fcsNoOp, >fcsNoOp, >fcsNoOp, >fcsNoOp, >fcsNoOp, >fcsNoOp
.db >fcsPrePorta, >fcsArpTime, >fcsVibrato, >fcsVibRange, >fcsVibShape, >fcsPitch, >fcsArpeggio, >fcsVolume, >fcsVolSlide, >fcsPorta, >fcsLegato, >fcsVolSlideTarget, >fcsNoOpOneByte, >fcsNoOpOneByte, >fcsNoOpOneByte, >fcsPan
.db >fcsOptPlaceholder, >fcsNoOp, >fcsNoOp, >fcsNoOp, >fcsNoOp, >fcsCallI, >fcsOffWait, >fcsFullCmd, >fcsCall, >fcsRet, >fcsJump, >fcsTickRate, >fcsWaitS, >fcsWaitC, >fcsWait1, >fcsStop
fcsInsTableHigh:
.db <fcsNoArgDispatch, <fcsNoArgDispatch, <fcsNoArgDispatch, <fcsNoArgDispatch, <fcsOneByteDispatch, <fcsNoOp, <fcsNoOp, <fcsNoOp, <fcsNoOp, <fcsNoOp, <fcsNoOp, <fcsNoOp
.db <fcsPrePorta, <fcsArpTime, <fcsVibrato, <fcsVibRange, <fcsVibShape, <fcsPitch, <fcsArpeggio, <fcsVolume, <fcsVolSlide, <fcsPorta, <fcsLegato, <fcsVolSlideTarget, <fcsNoOpOneByte, <fcsNoOpOneByte, <fcsNoOpOneByte, <fcsPan
.db <fcsOptPlaceholder, <fcsNoOp, <fcsNoOp, <fcsNoOp, <fcsNoOp, <fcsCallI, <fcsOffWait, <fcsFullCmd, <fcsCall, <fcsRet, <fcsJump, <fcsTickRate, <fcsWaitS, <fcsWaitC, <fcsWait1, <fcsStop
; "dummy" implementation - example only!
fcsDummyFunc:
rts
fcsVolMaxExample:
.db 127, 127, 127, 127, 127, 127, 127, 127
; first 64 commands
fcsCmdTableExample:
.db 0, 0, 0, 0, 0, 0, 0, 0
.db 0, 0, 0, 0, 0, 0, 0, 0
.db 0, 0, 0, 0, 0, 0, 0, 0
.db 0, 0, 0, 0, 0, 0, 0, 0
.db 0, 0, 0, 0, 0, 0, 0, 0
.db 0, 0, 0, 0, 0, 0, 0, 0
.db 0, 0, 0, 0, 0, 0, 0, 0
.db 0, 0, 0, 0, 0, 0, 0, 0