mirror of
https://github.com/bobbimanners/Applecorn.git
synced 2024-11-05 02:08:07 +00:00
898 lines
36 KiB
ArmAsm
898 lines
36 KiB
ArmAsm
* MAINMEM.AUDIO.S
|
|
* (c) Bobbi 2022 GPLv3
|
|
*
|
|
* Applecorn audio code
|
|
*
|
|
|
|
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 ; FOR 4 NOTES + 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: A1L,A1H points to start of buffer
|
|
* Called with interrupts disabled
|
|
GETBUFADDR LDA :BUFADDRL,X
|
|
STA A1L
|
|
LDA :BUFADDRH,X
|
|
STA A1H
|
|
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 A1L,A1H
|
|
PLA ; Get value to write back
|
|
STA (A1L),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 >>> ENTMAIN
|
|
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 A1L,A1H
|
|
LDA (A1L),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 A1L,A1H
|
|
LDA STARTINDICES,X ; Output pointer for buf X
|
|
TAY
|
|
LDA (A1L),Y ; Obtain Hold/Sync byte
|
|
AND #$F0 ; Set sync nybble to zero ..
|
|
STA (A1L),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 >>> ENTMAIN
|
|
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 from Ensoniq interrupt handler - process audio queue
|
|
* Should be called at 100Hz
|
|
ENSQISR 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
|
|
TYA ; HS byte
|
|
AND #$F0 ; Mask out sync nybble
|
|
BNE :HOLDSET ; Handle hold function
|
|
|
|
JSR CHECK4BYTES ; Check queue has a note
|
|
BCS :NEXT ; Less than 4 bytes, skip
|
|
|
|
JSR REMAUDIO ; Remove HS byte from queue
|
|
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 ENSQNOTE ; 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
|
|
RTL
|
|
:HOLDSET LDA CURRAMP,X ; Get current amplitude
|
|
BNE :NEXT ; If non zero, hold
|
|
JSR REMAUDIO ; Dequeue four bytes
|
|
JSR REMAUDIO
|
|
JSR REMAUDIO
|
|
JSR REMAUDIO
|
|
JMP :PEEK ; Immediately dispatch next note
|
|
:SYNCSET JSR CHORD ; See if chord can be released
|
|
BRA :NEXT
|
|
:CNT DB $05 ; Used to determine 20Hz cycles
|
|
|
|
|
|
* Helper function for ENSQISR - 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
|
|
LDY #$00 ; Zero volume
|
|
LDA #$00 ; Zero freq
|
|
JSR ENSQNOTE ; Silence channel Y
|
|
RTS
|
|
:RELEASE LDA #3 ; Phase 3 is release phase
|
|
STA AMPSECT,X ; Force release phase
|
|
RTS
|
|
|
|
|
|
* Helper function for ENSQISR
|
|
* 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
|
|
|
|
|
|
* 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 A1L,A1H
|
|
LDY #ENVT ; Parm for length of each step
|
|
LDA (A1L),Y ; Get value of parm
|
|
AND #$7F ; Mask out MSB
|
|
STA CHANCTR,X ; Reset counter
|
|
RTS
|
|
|
|
|
|
* On entry: Y is envelope number
|
|
* On return: A1L,A1H point to start of buffer for this envelope
|
|
* X is preserved
|
|
GETENVADDR LDA #<ENVBUF0 ; Copy ENVBUF0 to A1L,A1H
|
|
STA A1L
|
|
LDA #>ENVBUF0
|
|
STA A1H
|
|
:L1 CPY #$00 ; See if Y is zero
|
|
BEQ :DONE ; If so, we are done
|
|
LDA A1L ; Add 13 to A1L,A1H
|
|
CLC
|
|
ADC #13
|
|
STA A1L
|
|
LDA A1H
|
|
ADC #00
|
|
STA A1H
|
|
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 -> A1L,A1H
|
|
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 #ENVPI1 ; Parm: change pitch/step section 1
|
|
LDA (A1L),Y ; Get value of parm
|
|
JSR UPDPITCH ; Update the pitch
|
|
LDY #ENVPN1 ; Parm: num steps in section 1
|
|
LDA (A1L),Y ; Get value of parm
|
|
CMP PITCHSTEP,X ; Are we there yet?
|
|
BEQ :NXTSECT ; Yes!
|
|
INC PITCHSTEP,X ; One more step
|
|
RTS
|
|
:SECT2 LDY #ENVPI2 ; Parm: change pitch/step section 2
|
|
LDA (A1L),Y ; Get value of parm
|
|
JSR UPDPITCH ; Update the pitch
|
|
LDY #ENVPN2 ; Parm: num steps in section 2
|
|
LDA (A1L),Y ; Get value of parm
|
|
CMP PITCHSTEP,X ; Are we there yet?
|
|
BEQ :NXTSECT ; Yes!
|
|
INC PITCHSTEP,X ; One more step
|
|
RTS
|
|
:SECT3 LDY #ENVPI3 ; Parm: change pitch/step section 3
|
|
LDA (A1L),Y ; Get value of parm
|
|
JSR UPDPITCH ; Update the pitch
|
|
LDY #ENVPN3 ; Parm: num steps in section 3
|
|
LDA (A1L),Y ; Get value of parm
|
|
CMP PITCHSTEP,X ; Are we there yet?
|
|
BEQ :LASTSECT ; Yes!
|
|
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 (A1L),Y ; Get value of parm
|
|
AND #$80 ; MSB is auto-repeat flag
|
|
BEQ :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 ENSQFREQ ; 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 -> A1L,A1H
|
|
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 (A1L),Y ; Get value of parm
|
|
PHA
|
|
LDY #ENVALA ; Parm: level at end of attack
|
|
LDA (A1L),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 (A1L),Y ; Get value of parm
|
|
PHA
|
|
LDY #ENVALD ; Parm: level at end of delay
|
|
LDA (A1L),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 (A1L),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 (A1L),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
|
|
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 ENSQAMP
|
|
:DONE CLC ; CC to indicate phase continues
|
|
RTS
|
|
:TARGET DB $00
|
|
|
|
|
|
*****************************************************************************
|
|
* Ensoniq DOC Driver for Apple IIGS Follows ...
|
|
*****************************************************************************
|
|
|
|
* Ensoniq control registers
|
|
ENSQSNDCTL EQU $C03C
|
|
ENSQSNDDAT EQU $C03D
|
|
ENSQADDRL EQU $C03E
|
|
ENSQADDRH EQU $C03F
|
|
|
|
* Initialize Ensoniq
|
|
* Setup wavetable - one period of a square wave
|
|
* Start timer on oscillator #4, silence oscillators 0 to 3
|
|
ENSQINIT LDX #3
|
|
LDA #$80 ; Initialize sound queues
|
|
:L0 STZ SND0STARTIDX,X
|
|
STZ SND0ENDIDX,X
|
|
DEX
|
|
BNE :L0
|
|
|
|
LDA ENSQSNDCTL ; Get settings
|
|
ORA #$60 ; DOC RAM, autoincrement on
|
|
STA ENSQSNDCTL ; Set it
|
|
LDA #$00
|
|
STA ENSQADDRL ; DOC RAM addr $0000
|
|
STA ENSQADDRH ; DOC RAM addr $0000
|
|
LDA #120 ; High value of square wave
|
|
LDX #$00
|
|
:L1 STA ENSQSNDDAT ; 128 cycles of high value
|
|
INX
|
|
CPX #128
|
|
BNE :L1
|
|
LDA #80 ; Low value of square wave
|
|
:L2 STA ENSQSNDDAT ; 128 cycles of low value
|
|
INX
|
|
CPX #0
|
|
BNE :L2
|
|
|
|
LDA #$5C ; GS IRQ.SOUND initialization
|
|
STAL $E1002C
|
|
LDA #<ENSQISR
|
|
STAL $E1002D
|
|
LDA #>ENSQISR
|
|
STAL $E1002E
|
|
LDA #$00 ; Bank $00
|
|
STAL $E1002F
|
|
|
|
LDX #$E1 ; DOC Osc Enable register $E1
|
|
LDY #10 ; Five oscillators enabled
|
|
JSR ENSQWRTDOC
|
|
LDY #$00 ; Amplitude for osc #4 (timer)
|
|
LDA #33+1 ; Freq G2+1/8 tone = 99.46Hz
|
|
LDX #$04
|
|
JSR ENSQNOTE ; Start oscillator 4
|
|
LDX #$A4 ; Control register for osc #4
|
|
LDY #$08 ; Free run, with IRQ, start
|
|
JSR ENSQWRTDOC
|
|
; Fall through
|
|
* Silence all channels
|
|
ENSQSILENT LDY #$00 ; Amplitude
|
|
LDA #$80 ; Frequency
|
|
LDX #$03
|
|
:L1 JSR ENSQNOTE ; Initialize channel Y
|
|
STZ CHANTIMES,X ; No note playing
|
|
DEX
|
|
BPL :L1
|
|
RTS
|
|
|
|
|
|
* Configure an Ensoniq oscillator to play a note
|
|
* On entry: X - oscillator number 0-3 , A - frequency, Y - amplitude
|
|
* Preserves all registers
|
|
ENSQNOTE PHA
|
|
PHX
|
|
PHY
|
|
STX OSCNUM ; Stash oscillator number 0-3
|
|
|
|
PHA ; Stash orig freq
|
|
TAY
|
|
LDA FREQLOW,Y
|
|
TAY ; Frequency value LS byte
|
|
LDA #$00 ; DOC register base $00 (Freq Lo)
|
|
JSR ADDOSC ; Actual register in X
|
|
JSR ENSQWRTDOC
|
|
|
|
PLA ; Get orig freq back
|
|
TAY
|
|
LDA FREQHIGH,Y
|
|
TAY ; Frequency value MS byte
|
|
LDA #$20 ; DOC register base $20 (Freq Hi)
|
|
JSR ADDOSC ; Actual register in X
|
|
JSR ENSQWRTDOC
|
|
|
|
PLY ; Amplitude value
|
|
PHY
|
|
LDA #$40 ; DOC register base $40 (Volume)
|
|
JSR ADDOSC ; Actual register in X
|
|
JSR ENSQWRTDOC
|
|
|
|
LDY #$00 ; Wavetable pointer $00
|
|
LDA #$80 ; DOC register base $80 (Wavetable)
|
|
JSR ADDOSC ; Actual register in X
|
|
JSR ENSQWRTDOC
|
|
|
|
LDY #$00 ; Free run, no IRQ, start
|
|
LDA #$A0 ; DOC register base $A0 (Control)
|
|
JSR ADDOSC ; Actual register in X
|
|
JSR ENSQWRTDOC
|
|
|
|
LDY #$00 ; For 256 byte wavetable
|
|
LDA #$C0 ; DOC register base $C0 (WT size)
|
|
JSR ADDOSC ; Actual register in X
|
|
JSR ENSQWRTDOC
|
|
|
|
PLY
|
|
PLX
|
|
PLA
|
|
RTS
|
|
|
|
|
|
* Adjust frequency of note already playing
|
|
* On entry: Y - frequency to set
|
|
* Preserves X & Y
|
|
ENSQFREQ PHX
|
|
PHY ; Gonna need it again
|
|
LDA FREQLOW,Y
|
|
TAY ; Frequency value LS byte
|
|
LDA #$00 ; DOC register base $00 (Freq Lo)
|
|
JSR ADDOSC ; Actual register in X
|
|
JSR ENSQWRTDOC
|
|
PLY ; Get freq back
|
|
PHY
|
|
LDA FREQHIGH,Y
|
|
TAY ; Frequency value MS byte
|
|
LDA #$20 ; DOC register base $20 (Freq Hi)
|
|
JSR ADDOSC ; Actual register in X
|
|
JSR ENSQWRTDOC
|
|
PLY
|
|
PLX
|
|
RTS
|
|
|
|
|
|
* Adjust amplitude of note already playing
|
|
* On entry: Y - amplitude to set
|
|
* Preserves X & Y
|
|
ENSQAMP PHX
|
|
PHY ; Gonna need it again
|
|
LDA #$40 ; DOC register base $00 (Freq Lo)
|
|
JSR ADDOSC ; Actual register in X
|
|
JSR ENSQWRTDOC
|
|
PLY
|
|
PLX
|
|
RTS
|
|
|
|
|
|
* Add oscillator number to value in A, return sum in X
|
|
* Used by ENSQNOTE & ENSQFREQ
|
|
ADDOSC CLC
|
|
ADC OSCNUM
|
|
TAX
|
|
RTS
|
|
OSCNUM DB $00
|
|
|
|
|
|
* Wait for Ensoniq to be ready
|
|
ENSQWAIT LDA ENSQSNDCTL
|
|
BMI ENSQWAIT
|
|
RTS
|
|
|
|
* Write to a DOC register
|
|
* On entry: Value in Y, register in X
|
|
* Preserves all registers
|
|
ENSQWRTDOC PHA
|
|
JSR ENSQWAIT ; Wait for DOC to be ready
|
|
LDA ENSQSNDCTL
|
|
AND #$90 ; DOC register, no autoincr
|
|
ORA #$0F ; Master volume maximum
|
|
STA ENSQSNDCTL
|
|
STX ENSQADDRL ; Select DOC register
|
|
STZ ENSQADDRH
|
|
STY ENSQSNDDAT ; Write data
|
|
PLA
|
|
RTS
|
|
|