; Furnace Command Stream player for 6502 architecture ; written by tildearrow ; usage: ; define the following constants: ; - FCS_MAX_CHAN: the number of channels in your stream ; - FCS_MAX_STACK: the maximum stack size ; - fcsAddrBase: player state address base ; - fcsZeroPage: player state address base for zero-page-mandatory variables ; - fcsGlobalStack: player stack (best placed in a page) ; - fcsPtr: pointer to command stream ; - fcsVolMax: pointer to max channel volume array ; - fcsCmdTableLow: low address of command table ; - fcsCmdTableHigh: high address of command table ; - see stream_example.i for an example ; 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! ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; calculated from player constants fcsDelays=fcsPtr+8 fcsSpeedDial=fcsPtr+24 ; 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 fcsArpSpeed=fcsAddrBase+12 ; char fcsCmd=fcsAddrBase+16 ; char[8] fcsTempPtr=fcsZeroPage ; short ; channel state 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 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 ; 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 fcsPrePorta: jsr fcsReadNext pha and #$80 sta fcsArg0 pla and #$40 sta fcsArg1 ldy #$0c jsr fcsDispatchCmd rts 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 ; 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 ; handle command in dispatch code jmp (fcsTempPtr) ; only if pointer is zero + rts ; x: channel*2 ; a is set to next byte fcsReadNext: ; a=chanPC[x]+fcsPtr clc lda chanPC,x adc #>fcsPtr sta fcsTempPtr lda chanPC+1,x adc #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