Applecorn/mainmem.audio.s

758 lines
30 KiB
ArmAsm

* MAINMEM.AUDIO.S
* (c) Bobbi 2022 GPLv3
*
* Applecorn audio code.
* This is the implementation of the audio-engine. The code is
* device-independent, using the Ensoniq driver in MAINMEM.ENSQ.S
* or the Mockingboard driver in MAINMEM.MOCK.S.
*
SYSCLOCK DB $00 ; Centisecond counter (5 bytes)
DB $00
DB $00
DB $00
DB $00
* Sound buffers
* Four bytes are enqueued for each note, as follows:
* - MS byte of channel number
* - LS byte of channel number
* - Frequency
* - Duration
SNDBUFSZ EQU 21 ; 5 notes, plus a spare byte
SNDBUF0 DS SNDBUFSZ
SNDBUF1 DS SNDBUFSZ
SNDBUF2 DS SNDBUFSZ
SNDBUF3 DS SNDBUFSZ
* Pointers for circular buffers
* Buffers 4-7 correspond to audio channels 0 to 3
* Buffers 0-3 are currently unused
SND0STARTIDX DB $00 ; Start indices for sound bufs
SND1STARTIDX DB $00
SND2STARTIDX DB $00
SND3STARTIDX DB $00
STARTINDICES EQU SND0STARTIDX - 4
SND0ENDIDX DB $00 ; End indices for sound bufs
SND1ENDIDX DB $00
SND2ENDIDX DB $00
SND3ENDIDX DB $00
ENDINDICES EQU SND0ENDIDX - 4
* Envelope buffers 0-15
ENVBUF0 DS 13 ; 13 bytes not including env num
ENVBUF1 DS 13
ENVBUF2 DS 13
ENVBUF3 DS 13
ENVBUF4 DS 13
ENVBUF5 DS 13
ENVBUF6 DS 13
ENVBUF7 DS 13
ENVBUF8 DS 13
ENVBUF9 DS 13
ENVBUF10 DS 13
ENVBUF11 DS 13
ENVBUF12 DS 13
ENVBUF13 DS 13
ENVBUF14 DS 13
ENVBUF15 DS 13
* Offsets of parameters in each envelope buffer
ENVT EQU 0 ; Len of step in 1/100 sec
ENVPI1 EQU 1 ; Change pitch/step section 1
ENVPI2 EQU 2 ; Change pitch/step section 2
ENVPI3 EQU 3 ; Change pitch/step section 3
ENVPN1 EQU 4 ; Num steps section 1
ENVPN2 EQU 5 ; Num steps section 2
ENVPN3 EQU 6 ; Num steps section 3
ENVAA EQU 7 ; Attack: change/step
ENVAD EQU 8 ; Decay: change/step
ENVAS EQU 9 ; Sustain: change/step
ENVAR EQU 10 ; Release: change/step
ENVALA EQU 11 ; Target at end of attack
ENVALD EQU 12 ; Target at end of decay
* Time remaining for current note, in 1/20th of second
* Does not include the release phase of the envelope (if any)
CHANTIMES DB $00
DB $00
DB $00
DB $00
* Envelope number for current note. $FF if no envelope.
CHANENV DB $FF
DB $FF
DB $FF
DB $FF
* Envelope step counter for current note.
* This is used in order to invoke the envelope processing at the requested
* rate in 1/100th of a second.
CHANCTR DB $00
DB $00
DB $00
DB $00
* Pitch envelope section (0..4)
PITCHSECT DB $00
DB $00
DB $00
DB $00
* Step within pitch envelope section
PITCHSTEP DB $00
DB $00
DB $00
DB $00
* Current pitch
CURRPITCH DB $00
DB $00
DB $00
DB $00
* Amplitude envelope section (0..3)
* 0: Attack
* 1: Decay
* 2: Sustain
* 3: Release
AMPSECT DB $00
DB $00
DB $00
DB $00
* Current amplitude
CURRAMP DB $00
DB $00
DB $00
DB $00
* Get address of sound buffer
* On entry: X is buffer number (4..7)
* On exit: ISRL,ISRH points to start of buffer
* Called with interrupts disabled
GETBUFADDR LDA :BUFADDRL,X
STA ISRL
LDA :BUFADDRH,X
STA ISRH
RTS
:BUFADDRL DB $00
DB $00
DB $00
DB $00
DB <SNDBUF0
DB <SNDBUF1
DB <SNDBUF2
DB <SNDBUF3
DB $00
:BUFADDRH DB $00
DB $00
DB $00
DB $00
DB >SNDBUF0
DB >SNDBUF1
DB >SNDBUF2
DB >SNDBUF3
DB $00
* Insert value into buffer (API same as Acorn MOS INSV)
* On entry: A is value, X is buffer number.
* On exit: A, X, Y preserved. C clear on success.
INS PHP ; Save flags, turn off interrupts
SEI
PHY
PHA
LDY ENDINDICES,X ; Get input pointer
INY ; Next byte
CPY #SNDBUFSZ
BNE :NOTEND ; See if it's the end
LDY #0 ; If so, wraparound
:NOTEND TYA ; New input pointer in A
CMP STARTINDICES,X ; See if buffer is full
BEQ :FULL
LDY ENDINDICES,X ; Current position
STA ENDINDICES,X ; Write updated input pointer
JSR GETBUFADDR ; Buffer address into ISRL,ISRH
PLA ; Get value to write back
STA (ISRL),Y ; Write to buffer
PLY
PLP ; Restore flags
CLC ; Exit with carry clear
RTS
:FULL PLA ; Restore A
PLY
PLP ; Restore flags
SEC ; Exit with carry set
RTS
* Entry point to INS for code running in aux
MAININS >>> IENTMAIN ; Do NOT enable interrupts!!
PHY ; Y->X after transfer
PLX
JSR INS
PHP ; Flags->A before transfer back
PLA
>>> XF2AUX,INSHNDRET
* Remove value from buffer or examine buffer (API same as Acorn MOS REMV)
* NOTE OS1.20 has a bug in the EXAMINE path
* On entry: X is buffer number, V=1 if only examination is requested
* On exit: If examination, A next byte, X preserved, Y=next byte
* If removal, A undef, X preserved, Y=value of byte removed
* If buffer already empty C=1, else C=0
REM PHP ; Save flags, turn off interrupts
SEI
LDA STARTINDICES,X ; Output pointer for buf X
CMP ENDINDICES,X
BEQ :EMPTY ; Buffer is empty
TAY ; Buffer pointer into Y
JSR GETBUFADDR ; Buffer address into ISRL,ISRH
LDA (ISRL),Y ; Read byte from buffer
PHA ; Stash for later
BVS :EXAM ; If only examination, done
INY ; Next byte
CPY #SNDBUFSZ
BNE :NOTEND ; See if it's the end
LDY #0 ; If so, wraparound
:NOTEND TYA
STA STARTINDICES,X ; Set output pointer
PLY ; Char read from buffer
PLP
CLC ; Success
RTS
:EXAM PLA ; Char read from buffer
TAY ; BUGFIX: Omitted on OS1.20
PLP
CLC ; Success
RTS
:EMPTY PLP
SEC ; Buffer already empty
RTS
* Remove value from buffer according to audio channel (0-4)
* On entry: X is audio channel number
* On exit: A undef, X preserved, Y value of byte removed
REMAUDIO PHX ; Preserve X
TXA ; Audio channel X->A
ORA #$04 ; Convert to queue number
TAX ; Queue number ->X
CLV ; Remove byte from queue
JSR REM
PLX ; Recover original X
RTS
* Inspect value in buffer according to audio channel (0-4)
* On entry: X is audio channel number
* On exit: A next byte, X preserved
PEEKAUDIO PHX ; Preserve X
TXA ; Audio channel X->A
ORA #$04 ; Convert to queue number
TAX ; Queue number ->X
BIT :RTS ; Set V, inspect queue
JSR REM
PLX ; Recover original X
:RTS RTS
* Release a suspended note by overwriting its sequence number with zero
* On entry: X is audio channel number
* On exit: X preserved
RELNOTE PHX ; Preserve X
TXA ; Audio channel X->A
ORA #$04 ; Convert to queue number
TAX ; Queue number ->X
JSR GETBUFADDR ; Buffer address into ISRL,ISRH
LDA STARTINDICES,X ; Output pointer for buf X
TAY
LDA (ISRL),Y ; Obtain Hold/Sync byte
AND #$F0 ; Set sync nybble to zero ..
STA (ISRL),Y ; .. to release the note
PLX ; Recover original X
:RTS RTS
* Count space in buffer or purge buffer (API same as Acorn MOS CNPV)
* On entry: X is buffer number. V set means purge, V clear means count.
* C set means space left, C clear means entries used
* On exit: For purge, X & Y are preserved.
* For count, value in X (Y=0).
* A undef. V,C flags preserved.
CNP PHP ; Preserve flags
BVS :PURGE ; Purge if V set
SEC ; Compute space used
LDA ENDINDICES,X
SBC STARTINDICES,X
BPL :POS ; No wrap-around
CLC ; Wrap-around - add SNDBUFSZ
ADC #SNDBUFSZ
:POS LDY #$00 ; MSB of count always zero
PLP ; Recover flags
BCS :CNTREM ; If C set on entry, count remainder
TAX ; Return value in X
RTS
:CNTREM EOR #$FF ; Negate and add SNDBUFSZ
SEC
ADC #SNDBUFSZ
TAX ; Return value in X
RTS
:PURGE LDA ENDINDICES,X ; Eat all buffer contents
STA STARTINDICES,X
STZ CHANTIMES-4,X ; Set to zero time remaining
PLP ; Recover flags
RTS
* Entry point to CNP for code running in aux
MAINCNP >>> IENTMAIN ; Do NOT enable interrupts
PHY ; Y->X after transfer
PLX
PHA ; A->flags after transfer
PLP
BVS :PURGE
JSR CNP ; Count space
PHX ; X->Y for transfer back
PLY
>>> XF2AUX,CNPHNDRET1 ; Return for counting
:PURGE JSR CNP ; Purge buffer
PHX ; X->Y for transfer back
PLY
>>> XF2AUX,CNPHNDRET2 ; Return for purging
* Process releasing of notes once chord is complete.
* On entry: A chord sequence number, X audio channel
* Preserves all registers
CHORD PHA
PHX
PHY
*
* Part 1: Count all notes at head of queues with seq number = A
*
STA :SEQ ; Sequence number looking for
STZ :CNT ; Initialize counter
LDX #3 ; Process all audio queues
:L1 JSR PEEKAUDIO ; See byte at head of queue
BCS :NEXT ; Empty queue
AND #$0F ; Mask out hold nybble
CMP :SEQ ; If matches ..
BNE :NEXT
INC :CNT ; .. count it
:NEXT DEX
BPL :L1 ; Next audio queue
*
* Part 2: If count = seq number + 1
*
INC :SEQ ; Seq number + 1
LDA :CNT ; Compare with counter
CMP :SEQ
BEQ :RELCHORD ; Release notes
:DONE PLY
PLX
PLA
RTS
*
* Part 3: Overwrite seq numbers with zero to release notes.
*
:RELCHORD DEC :SEQ ; Put seq back how it was
LDX #3 ; All audio queues
:L2 JSR PEEKAUDIO ; See byte at head of queue
BCS :NEXT2 ; Empty queue
AND #$0F ; Mask out hold nybble
CMP :SEQ ; See if matches
BNE :NEXT2 ; Nope, skip
JSR RELNOTE ; Release the note
:NEXT2 DEX
BPL :L2 ; Next audio queue
BRA :DONE
:SEQ DB $00 ; Sequence number
:CNT DB $00 ; Counter
* Called at 100Hz. Process audio queue.
AUDIOISR INC SYSCLOCK+0 ; Increment system clock
BNE :S1
INC SYSCLOCK+1
BNE :S1
INC SYSCLOCK+2
BNE :S1
INC SYSCLOCK+3
BNE :S1
INC SYSCLOCK+4
:S1 DEC :CNT ; Find every 5th cycle
BNE :AT100HZ
LDA #5
STA :CNT
LDX #3 ; Process four audio queues
:L1 LDA CHANTIMES,X ; Time remaining on current note
BEQ :NONOTE ; No note playing
DEC CHANTIMES,X
BRA :NEXT
:NONOTE JSR NONOTE ; Handle end note / release phase
:PEEK JSR PEEKAUDIO ; Inspect byte at head of queue
BCS :NEXT ; Nothing in queue
; NOTE: A contains HS byte of &HSFC
TAY ; Stash for later
AND #$0F ; Mask out hold nybble
BNE :SYNCSET ; Do not play if sync != 0
JSR CHECK4BYTES ; Check queue has a note
BCS :NEXT ; Less than 4 bytes, skip
JSR REMAUDIO ; Remove HS byte from queue
TYA ; HS byte
AND #$F0 ; Mask out sync nybble
BNE :HOLDSET ; Handle hold function
JSR REMAUDIO ; Remove amplitude byte from queue
TYA ; Amplitude or envelope -> A
DEC A
BPL :HASENV ; If +ve, value was 1,2,3..
INC A
EOR #$FF ; Negate A
INC A ; ..
ASL ; Multiply by 16
ASL
ASL
ASL
PHA ; Amplitude to stack
LDA #$FF ; $FF means 'no envelope'
STA CHANENV,X
BRA :S2
:HASENV STA CHANENV,X ; Store envelope number
LDA #$01
STA CHANCTR,X ; Set envelope step counter to 1
STZ PITCHSECT,X ; Start on pitch section 0
STZ PITCHSTEP,X ; Start on step 0
STZ AMPSECT,X ; Start on amplitude section 0
LDA #$00 ; Initial amplitude is zero
PHA ; Zero amplitude to stack
:S2 JSR REMAUDIO ; Remove freq byte from queue
PHY ; Frequency
JSR REMAUDIO ; Remove dur byte from queue
TYA ; Duration
DEC A ; EXPERIMENT
STA CHANTIMES,X
PLA ; Recover frequency
STA CURRPITCH,X ; Store for pitch envelope
PLY ; Recover amplitude
JSR AUDIONOTE ; Start note playing
:NEXT DEX
BPL :L1 ; Next audio queue
:AT100HZ ; Here on every call (100Hz)
LDX #3 ; Iterate through channels
:L2 LDA CHANENV,X ; Envelope for this channel?
BMI :NOENV ; $FF means no envelope
JSR ENVTICKS ; Handle envelope tick counter
BCC :NOENV ; This cycle is not a tick
JSR PITCHENV ; Process pitch envelope
JSR ADSRENV ; Process amplitude envelope
:NOENV DEX
BPL :L2 ; Next audio queue
CLC
RTS
:HOLDSET JSR REMAUDIO ; Dequeue amplitude/env (ignored)
JSR REMAUDIO ; Dequeue frequency (ignored)
JSR REMAUDIO ; Dequeue duration
TYA
STA CHANTIMES,X
BRA :NEXT
:SYNCSET JSR CHORD ; See if chord can be released
BRA :NEXT
:CNT DB $05 ; Used to determine 20Hz cycles
* Helper function for AUDIOISR - called when no note playing
* On entry: X is audio channel #
NONOTE LDA CHANENV,X ; See if envelope is in effect
CMP #$FF
BNE :RELEASE ; If envelope -> start rel phase
STZ CURRAMP,X ; Next env will start at zero vol
PHY ; NEEDED FOR MOCKINGBOARD (NOT SURE WHY)
LDY #$00 ; Zero volume
LDA #$00 ; Zero freq
JSR AUDIONOTE ; Silence channel Y
PLY ; NEEDED FOR MOCKINGBOARD (NOT SURE WHY)
RTS
:RELEASE LDA #3 ; Phase 3 is release phase
STA AMPSECT,X ; Force release phase
RTS
* Helper function for AUDIOISR
* On entry: X is audio channel #
* On return: CS if there are <= 4 bytes in queue, CC otherwise
* X is preserved
CHECK4BYTES PHX
INX ; Convert audio channel to buf num
INX
INX
INX
CLV ; Ask to count buffer
CLC ; Ask for space used
JSR CNP ; Go count it
TXA
PLX
CMP #3 ; At least 4 bytes used?
BMI :NO
CLC
RTS
:NO SEC
RTS
* Stop the sound device (no more interrupts)
AUDIOSTOP LDA AUDIOCARD
BEQ :MOCK
JMP ENSQSTOP
:MOCK JMP MOCKSTOP
* Configure an oscillator to play a note
* On entry: X - oscillator number 0-3 , A - frequency, Y - amplitude
* Preserves all registers
AUDIONOTE PHA
LDA AUDIOCARD
BEQ :MOCK
PLA
JMP ENSQNOTE
:MOCK PLA
JMP MOCKNOTE
* Adjust frequency of oscillator
* On entry: X - oscillator number 0-3 , Y - frequency to set
* Preserves X & Y
AUDIOFREQ LDA AUDIOCARD
BEQ :MOCK
JMP ENSQFREQ
:MOCK JMP MOCKFREQ
* Adjust amplitude of oscillator
* On entry: X - oscillator number 0-3 , Y - amplitude to set
* Preserves X & Y
AUDIOAMP LDA AUDIOCARD
BEQ :MOCK
JMP ENSQAMP
:MOCK JMP MOCKAMP
* Handle envelope tick counter
* On entry: X is audio channel #
* On return: CS if this cycle is an envelope tick, CC otherwise.
* X is preserved
ENVTICKS DEC CHANCTR,X ; Decrement counter
BEQ :ZERO ; Expired
CLC ; Not expired
RTS
:ZERO JSR RSTTICKS ; Reset counter
SEC ; Counter had expired
RTS
* Reset envelope tick counter
* On entry: X is audio channel #
* On return: Sets CHANCTR,X to length of each step in 1/100ths
RSTTICKS LDA CHANENV,X ; Get envelope number
TAY
JSR GETENVADDR ; Envelope address in ISRL,ISRH
LDY #ENVT ; Parm for length of each step
LDA (ISRL),Y ; Get value of parm
AND #$7F ; Mask out MSB
STA CHANCTR,X ; Reset counter
RTS
* On entry: Y is envelope number
* On return: ISRL,ISRH point to start of buffer for this envelope
* X is preserved
GETENVADDR LDA #<ENVBUF0 ; Copy ENVBUF0 to ISRL,ISRH
STA ISRL
LDA #>ENVBUF0
STA ISRH
:L1 CPY #$00 ; See if Y is zero
BEQ :DONE ; If so, we are done
LDA ISRL ; Add 13 to ISRL,ISRH
CLC
ADC #13
STA ISRL
LDA ISRH
ADC #00
STA ISRH
DEY ; Decr envelopes remaining
BRA :L1 ; Go again
:DONE RTS
* Process pitch envelope
* On entry: X is audio channel #
* X is preserved
PITCHENV LDA CHANENV,X ; Get envelope number
TAY
JSR GETENVADDR ; Addr of envelope -> ISRL,ISRH
LDA PITCHSECT,X ; See what section we are in
BEQ :SECT1 ; Section 1, encoded as 0
CMP #$01
BEQ :SECT2 ; Section 2, encoded as 1
CMP #$02
BEQ :SECT3 ; Section 3, encoded as 2
RTS ; Other section, do nothing
:SECT1
LDY #ENVPN1 ; Parm: num steps in section 1
LDA (ISRL),Y ; Get value of parm
CMP PITCHSTEP,X ; Are we there yet?
BEQ :NXTSECT ; Yes!
LDY #ENVPI1 ; Parm: change pitch/step section 1
LDA (ISRL),Y ; Get value of parm
JSR UPDPITCH ; Update the pitch
INC PITCHSTEP,X ; One more step
RTS
:SECT2
LDY #ENVPN2 ; Parm: num steps in section 2
LDA (ISRL),Y ; Get value of parm
CMP PITCHSTEP,X ; Are we there yet?
BEQ :NXTSECT ; Yes!
LDY #ENVPI2 ; Parm: change pitch/step section 2
LDA (ISRL),Y ; Get value of parm
JSR UPDPITCH ; Update the pitch
INC PITCHSTEP,X ; One more step
RTS
:SECT3
LDY #ENVPN3 ; Parm: num steps in section 3
LDA (ISRL),Y ; Get value of parm
CMP PITCHSTEP,X ; Are we there yet?
BEQ :LASTSECT ; Yes!
LDY #ENVPI3 ; Parm: change pitch/step section 3
LDA (ISRL),Y ; Get value of parm
JSR UPDPITCH ; Update the pitch
INC PITCHSTEP,X ; One more step
RTS
:NXTSECT INC PITCHSECT,X ; Next section
STZ PITCHSTEP,X ; Back to step 0 of section
RTS
:LASTSECT LDY #ENVT ; Parm: length/step + autorepeat
LDA (ISRL),Y ; Get value of parm
AND #$80 ; MSB is auto-repeat flag
BNE :NXTSECT ; Not repeating
STZ PITCHSECT,X ; Go back to section 1
STZ PITCHSTEP,X ; Back to step 0 of section
RTS
* Update pitch value. Called by PITCHENV.
* On entry: A - Change of pitch/step, X is audio channel #
* X is preserved
UPDPITCH STX OSCNUM
CLC
ADC CURRPITCH,X ; Add change to current
STA CURRPITCH,X ; Update
TAY
JSR AUDIOFREQ ; Update Ensoniq regs
RTS
* Process amplitude envelope
* On entry: X is audio channel #
* X is preserved
ADSRENV LDA CHANENV,X ; Get envelope number
TAY
JSR GETENVADDR ; Addr of envelope -> ISRL,ISRH
LDA AMPSECT,X ; See what section we are in
BEQ :ATTACK ; Attack, encoded as 0
CMP #$01
BEQ :DECAY ; Decay, encoded as 1
CMP #$02
BEQ :SUSTAIN ; Sustain, encoded as 2
CMP #$03
BEQ :RELEASE ; Release, encoded as 3
RTS ; Otherwise nothing to do
:ATTACK LDY #ENVAA ; Parm: attack change/step
LDA (ISRL),Y ; Get value of parm
PHA
LDY #ENVALA ; Parm: level at end of attack
LDA (ISRL),Y ; Get value of parm
PLY
JSR ADSRPHASE ; Generic ADSR phase handler
BCS :NEXTSECT ; Phase done -> decay
RTS
:DECAY LDY #ENVAD ; Parm: delay change/step
LDA (ISRL),Y ; Get value of parm
PHA
LDY #ENVALD ; Parm: level at end of delay
LDA (ISRL),Y ; Get value of parm
PLY
JSR ADSRPHASE ; Generic ADSR phase handler
BCS :NEXTSECT ; Phase done -> sustain
RTS
:SUSTAIN LDY #ENVAS ; Parm: delay change/step
LDA (ISRL),Y ; Get value of parm
TAY
LDA #$00 ; Target level zero
JSR ADSRPHASE ; Generic ADSR phase handler
RTS
:RELEASE LDY #ENVAR ; Parm: attack change/step
LDA (ISRL),Y ; Get value of parm
TAY
LDA #$00 ; Target level zero
JSR ADSRPHASE ; Generic ADSR phase handler
BCS :FINISH ; Level is zero
RTS
:NEXTSECT INC AMPSECT,X ; Next section
RTS
:FINISH LDA #$FF ; Finished with envelope
STA CHANENV,X
RTS
* Handle any individual phase of the ADSR envelope. Called by ADSRENV.
* On entry: A - level at end of phase, X - audio channel, Y - change/step
* On return: CS if end of phase, CC otherwise. X preserved.
ADSRPHASE STX OSCNUM
STA :TARGET ; Stash target level for later
CPY #$00 ; Check sign of change/step
BEQ :DONE ; Change/step is zero
BMI :DESCEND ; Descending amplitude
:ASCEND CMP CURRAMP,X ; Compare tgt with current level
BNE :S1 ; Not equal to target, keep going
SEC ; CS to indicate phase is done
RTS
:S1 TYA ; Change/step -> A
CLC
ADC CURRAMP,X ; Add change to current amp
CMP :TARGET ; Compare with target
BCS :CLAMP ; If target < sum, clamp to target
BRA :UPDATE
:DESCEND CMP CURRAMP,X ; Compare tgt with current level
BNE :S2 ; Not equal to target, keep going
SEC ; CS to indicate phase is done
RTS
:S2 TYA ; Change/step -> A
CLC
ADC CURRAMP,X ; Add change to current amp
BCS :CLAMP ; If overflow (-ve), clamp to target
CMP :TARGET ; Compare with target
BCC :CLAMP ; If target >= sum, clamp to target
BRA :UPDATE
:CLAMP LDA :TARGET ; Recover target level
:UPDATE STA CURRAMP,X ; Store updated amplitude
TAY ; Tell the Ensoniq
JSR AUDIOAMP
:DONE CLC ; CC to indicate phase continues
RTS
:TARGET DB $00