include "hardware.inc"
|
|
include "huge.inc"
|
|
|
|
add_a_to_r16: MACRO
|
|
add \2
|
|
ld \2, a
|
|
adc \1
|
|
sub \2
|
|
ld \1, a
|
|
ENDM
|
|
|
|
add_a_to_hl: MACRO
|
|
add_a_to_r16 h, l
|
|
ENDM
|
|
|
|
add_a_to_de: MACRO
|
|
add_a_to_r16 d, e
|
|
ENDM
|
|
|
|
ret_dont_call_playnote: MACRO
|
|
pop hl
|
|
pop af
|
|
and a ; Clear carry to avoid calling `play_chX_note`
|
|
push af
|
|
jp hl
|
|
ENDM
|
|
|
|
add_a_ind_ret_hl: MACRO
|
|
ld hl, \1
|
|
add [hl]
|
|
inc hl
|
|
ld h, [hl]
|
|
ld l, a
|
|
adc h
|
|
sub l
|
|
ld h, a
|
|
ENDM
|
|
|
|
load_hl_ind: MACRO
|
|
ld hl, \1
|
|
ld a, [hl+]
|
|
ld h, [hl]
|
|
ld l, a
|
|
ENDM
|
|
|
|
load_de_ind: MACRO
|
|
ld a, [\1]
|
|
ld e, a
|
|
ld a, [\1+1]
|
|
ld d, a
|
|
ENDM
|
|
|
|
retMute: MACRO
|
|
bit \1, a
|
|
ret nz
|
|
ENDM
|
|
|
|
checkMute: MACRO
|
|
ld a, [mute_channels]
|
|
bit \1, a
|
|
jr nz, \2
|
|
ENDM
|
|
|
|
;; Maximum pattern length
|
|
PATTERN_LENGTH EQU 64
|
|
;; Amount to be shifted in order to skip a channel.
|
|
CHANNEL_SIZE_EXPONENT EQU 3
|
|
|
|
SECTION "Playback variables", WRAM0
|
|
;; Active song descriptor
|
|
order_cnt: db
|
|
_start_song_descriptor_pointers:
|
|
;; Pointers to the song's current four orders (one per channel)
|
|
order1: dw
|
|
order2: dw
|
|
order3: dw
|
|
order4: dw
|
|
|
|
;; Pointers to the instrument tables
|
|
duty_instruments: dw
|
|
wave_instruments: dw
|
|
noise_instruments: dw
|
|
|
|
;; Misc. pointers
|
|
routines: dw
|
|
waves: dw
|
|
_end_song_descriptor_pointers:
|
|
|
|
;; Pointers to the current patterns (sort of a cache)
|
|
pattern1: dw
|
|
pattern2: dw
|
|
pattern3: dw
|
|
pattern4: dw
|
|
|
|
;; How long a row lasts in ticks (1 = one row per call to `hUGE_dosound`, etc. 0 translates to 256)
|
|
ticks_per_row: db
|
|
|
|
_hUGE_current_wave::
|
|
hUGE_current_wave::
|
|
;; ID of the wave currently loaded into wave RAM
|
|
current_wave: db
|
|
hUGE_NO_WAVE equ 100
|
|
EXPORT hUGE_NO_WAVE
|
|
|
|
;; Everything between this and `end_zero` is zero-initialized by `hUGE_init`
|
|
start_zero:
|
|
|
|
mute_channels: db
|
|
current_order: db
|
|
next_order: db
|
|
row_break: db
|
|
|
|
temp_note_value: dw
|
|
row: db
|
|
tick: db
|
|
counter: db
|
|
|
|
channels:
|
|
;;;;;;;;;;;
|
|
;;Channel 1
|
|
;;;;;;;;;;;
|
|
channel1:
|
|
channel_period1: dw
|
|
toneporta_target1: dw
|
|
channel_note1: db
|
|
vibrato_tremolo_phase1: db
|
|
envelope1: db
|
|
highmask1: db
|
|
|
|
;;;;;;;;;;;
|
|
;;Channel 2
|
|
;;;;;;;;;;;
|
|
channel2:
|
|
channel_period2: dw
|
|
toneporta_target2: dw
|
|
channel_note2: db
|
|
vibrato_tremolo_phase2: db
|
|
envelope2: db
|
|
highmask2: db
|
|
|
|
;;;;;;;;;;;
|
|
;;Channel 3
|
|
;;;;;;;;;;;
|
|
channel3:
|
|
channel_period3: dw
|
|
toneporta_target3: dw
|
|
channel_note3: db
|
|
vibrato_tremolo_phase3: db
|
|
envelope3: db
|
|
highmask3: db
|
|
|
|
;;;;;;;;;;;
|
|
;;Channel 4
|
|
;;;;;;;;;;;
|
|
channel4:
|
|
channel_period4: dw
|
|
toneporta_target4: dw
|
|
channel_note4: db
|
|
vibrato_tremolo_phase4: db
|
|
envelope4: db
|
|
highmask4: db
|
|
|
|
end_zero:
|
|
|
|
SECTION "Sound Driver", ROM0
|
|
|
|
;;; Sets up hUGEDriver to play a song.
|
|
;;; !!! BE SURE THAT `hUGE_dosound` WILL NOT BE CALLED WHILE THIS RUNS !!!
|
|
;;; Param: HL = Pointer to the "song descriptor" you wish to load (typically exported by hUGETracker).
|
|
;;; Destroys: AF C DE HL
|
|
_hUGE_init::
|
|
hUGE_init::
|
|
ld a, [hl+] ; tempo
|
|
ld [ticks_per_row], a
|
|
|
|
ld a, [hl+]
|
|
ld e, a
|
|
ld a, [hl+]
|
|
ld d, a
|
|
ld a, [de]
|
|
ld [order_cnt], a
|
|
|
|
ld c, _end_song_descriptor_pointers - (_start_song_descriptor_pointers)
|
|
ld de, order1
|
|
|
|
.copy_song_descriptor_loop:
|
|
ld a, [hl+]
|
|
ld [de], a
|
|
inc de
|
|
dec c
|
|
jr nz, .copy_song_descriptor_loop
|
|
|
|
IF !DEF(PREVIEW_MODE)
|
|
;; Zero some ram
|
|
ld c, end_zero - start_zero
|
|
ld hl, start_zero
|
|
xor a
|
|
.fill_loop:
|
|
ld [hl+], a
|
|
dec c
|
|
jr nz, .fill_loop
|
|
ENDC
|
|
|
|
;; These two are zero-initialized by the loop above, so these two writes must come after
|
|
ld a, %11110000
|
|
ld [envelope1], a
|
|
ld [envelope2], a
|
|
|
|
;; Force loading the next wave
|
|
ld a, hUGE_NO_WAVE
|
|
ld [current_wave], a
|
|
|
|
;; Preview mode needs to load the order ID from memory
|
|
IF !DEF(PREVIEW_MODE)
|
|
ld c, 0
|
|
ELSE
|
|
ld a, [current_order]
|
|
ld c, a
|
|
ENDC
|
|
;; fallthrough (load the pattern pointers)
|
|
|
|
;;; Sets all 4 pattern pointers from a certain index in the respective 4 orders.
|
|
;;; Param: C = The index (in increments of 2)
|
|
;;; Destroy: AF DE HL
|
|
load_patterns:
|
|
IF DEF(PREVIEW_MODE)
|
|
db $fc ; signal order update to tracker
|
|
ENDC
|
|
|
|
ld hl, order1
|
|
ld de, pattern1
|
|
call .load_pattern
|
|
|
|
ld hl, order2
|
|
call .load_pattern
|
|
|
|
ld hl, order3
|
|
call .load_pattern
|
|
|
|
ld hl, order4
|
|
;; fallthrough
|
|
|
|
.load_pattern:
|
|
ld a, [hl+]
|
|
add c
|
|
ld h, [hl]
|
|
ld l, a
|
|
adc h
|
|
sub l
|
|
ld h, a
|
|
|
|
ld a, [hl+]
|
|
ld [de], a
|
|
inc de
|
|
ld a, [hl]
|
|
ld [de], a
|
|
inc de
|
|
ret
|
|
|
|
|
|
;;; Sets a channel's muting status.
|
|
;;; Muted channels are left entirely alone by the driver, so that you can repurpose them,
|
|
;;; for example for sound effects, CH3 sample playback, etc.
|
|
;;; If muting the channel, the note being played will be cut.
|
|
;;; Param: B = Which channel to enable; 0 for CH1, 1 for CH2, etc.
|
|
;;; Param: C = 0 to unmute the channel, 1 to mute it
|
|
;;; Destroy: A C E HL
|
|
hUGE_mute_channel::
|
|
ld e, $fe
|
|
ld a, b
|
|
or a
|
|
jr z, .enable_cut
|
|
.enable_loop:
|
|
sla c
|
|
rlc e
|
|
dec a
|
|
jr nz, .enable_loop
|
|
.enable_cut:
|
|
ld a, [mute_channels]
|
|
and e
|
|
or c
|
|
ld [mute_channels], a
|
|
and c
|
|
jp nz, note_cut
|
|
ret
|
|
|
|
|
|
;;; Reads a pattern's current row.
|
|
;;; Param: BC = Pointer to the pattern
|
|
;;; Param: [row] = Index of the current ro<
|
|
;;; Return: A = Note ID
|
|
;;; Return: B = Instrument (upper nibble) & effect code (lower nibble)
|
|
;;; Return: C = Effect parameter
|
|
;;; Destroy: HL
|
|
get_current_row:
|
|
ld a, [row]
|
|
ld h, a
|
|
;; Multiply by 3 for the note value
|
|
add h
|
|
add h
|
|
|
|
ld h, 0
|
|
ld l, a
|
|
add hl, bc ; HL now points at the 3rd byte of the note
|
|
ld a, [hl+]
|
|
ld b, [hl]
|
|
inc hl
|
|
ld c, [hl]
|
|
ret
|
|
|
|
;;; Gets the "period" of a pattern's current note.
|
|
;;; Param: HL = Pointer to the pattern pointer
|
|
;;; Param: [row] = Index of the current row
|
|
;;; Param: DE = Location to write the note's index to, if applicable
|
|
;;; Return: HL = Note's period
|
|
;;; Return: CF = Set if and only if a "valid" note (i.e. not a "rest")
|
|
;;; Return: [DE] = Note's ID, not updated if a "rest"
|
|
;;; Return: B = Instrument (upper nibble) & effect code (lower nibble)
|
|
;;; Return: C = Effect parameter
|
|
;;; Destroy: AF
|
|
get_current_note:
|
|
ld a, [hl+]
|
|
ld c, a
|
|
ld b, [hl]
|
|
|
|
call get_current_row
|
|
ld hl, 0
|
|
|
|
;; If the note we found is greater than LAST_NOTE, then it's not a valid note
|
|
;; and nothing needs to be updated.
|
|
cp LAST_NOTE
|
|
ret nc
|
|
|
|
;; Store the loaded note value in channel_noteX
|
|
ld [de], a
|
|
|
|
;;; Gets a note's "period", i.e. what should be written to NRx3 and NRx4.
|
|
;;; Param: A = Note ID
|
|
;;; Return: HL = Note's period
|
|
;;; Return: CF = 1
|
|
;;; Destroy: AF
|
|
get_note_period:
|
|
add a ;; double it to get index into hi/lo table
|
|
add LOW(note_table)
|
|
ld l, a
|
|
adc HIGH(note_table)
|
|
sub l
|
|
ld h, a
|
|
ld a, [hl+]
|
|
ld h, [hl]
|
|
ld l, a
|
|
|
|
scf
|
|
ret
|
|
|
|
;;; Gets a note's "polynomial counter", i.e. what should be written to NR44.
|
|
;;; Param: A = Note ID
|
|
;;; Return: A = Note's poly
|
|
;;; Destroy: F HL
|
|
get_note_poly:
|
|
;; Invert the order of the numbers
|
|
add 192 ; (255 - 63)
|
|
cpl
|
|
|
|
;; Thanks to RichardULZ for this formula
|
|
;; https://docs.google.com/spreadsheets/d/1O9OTAHgLk1SUt972w88uVHp44w7HKEbS/edit#gid=75028951
|
|
; if A > 7 then begin
|
|
; B := (A-4) div 4;
|
|
; C := (A mod 4)+4;
|
|
; A := (C or (B shl 4))
|
|
; end;
|
|
|
|
; if A < 7 then return
|
|
cp 7
|
|
ret c
|
|
|
|
ld h, a
|
|
|
|
; B := (A-4) div 4;
|
|
sub 4
|
|
srl a
|
|
srl a
|
|
ld l, a
|
|
|
|
; C := (A mod 4)+4;
|
|
ld a, h
|
|
and 3 ; mod 4
|
|
add 4
|
|
|
|
; A := (C or (B shl 4))
|
|
swap l
|
|
or l
|
|
ret
|
|
|
|
|
|
;;; Computes the pointer to a member of a channel.
|
|
;;; Param: B = Which channel (0 = CH1, 1 = CH2, etc.)
|
|
;;; Param: D = Offset within the channel struct
|
|
;;; Return: HL = Pointer to the channel's member
|
|
;;; Destroy: AF
|
|
ptr_to_channel_member:
|
|
ld a, b
|
|
REPT CHANNEL_SIZE_EXPONENT
|
|
add a
|
|
ENDR
|
|
add d
|
|
ld hl, channels
|
|
add LOW(channels)
|
|
ld l, a
|
|
adc HIGH(channels)
|
|
sub l
|
|
ld h, a
|
|
ret
|
|
|
|
|
|
;;; Updates a channel's frequency, and possibly restarts it.
|
|
;;; Note that CH4 is *never* restarted by this!
|
|
;;; Param: B = Which channel to update (0 = CH1, 1 = CH2, etc.)
|
|
;;; Param: (ignored for CH4) A = ORed to the value written to NRx4
|
|
;;; Param: (for CH4) E = Note ID
|
|
;;; Param: (otherwise) DE = Note period
|
|
;;; Destroy: AF B
|
|
;;; Destroy: (for CH4) HL
|
|
update_channel_freq:
|
|
ld c, a
|
|
ld a, [mute_channels]
|
|
dec b
|
|
jr z, .update_channel2
|
|
dec b
|
|
jr z, .update_channel3
|
|
dec b
|
|
jr z, .update_channel4
|
|
|
|
.update_channel1:
|
|
retMute 0
|
|
|
|
ld a, e
|
|
ldh [rAUD1LOW], a
|
|
ld a, d
|
|
or c
|
|
ldh [rAUD1HIGH], a
|
|
ret
|
|
|
|
.update_channel2:
|
|
retMute 1
|
|
|
|
ld a, e
|
|
ldh [rAUD2LOW], a
|
|
ld a, d
|
|
or c
|
|
ldh [rAUD2HIGH], a
|
|
ret
|
|
|
|
.update_channel3:
|
|
retMute 2
|
|
|
|
ld a, e
|
|
ldh [rAUD3LOW], a
|
|
ld a, d
|
|
or c
|
|
ldh [rAUD3HIGH], a
|
|
ret
|
|
|
|
.update_channel4:
|
|
retMute 3
|
|
|
|
ld a, e
|
|
call get_note_poly
|
|
ldh [rAUD4POLY], a
|
|
xor a
|
|
ldh [rAUD4GO], a
|
|
ret
|
|
|
|
|
|
play_note_routines:
|
|
jr play_ch1_note
|
|
jr play_ch2_note
|
|
jr play_ch3_note
|
|
jr play_ch4_note
|
|
|
|
play_ch1_note:
|
|
ld a, [mute_channels]
|
|
retMute 0
|
|
|
|
;; Play a note on channel 1 (square wave)
|
|
ld a, [temp_note_value]
|
|
ld [channel_period1], a
|
|
ldh [rAUD1LOW], a
|
|
|
|
ld a, [temp_note_value+1]
|
|
ld [channel_period1+1], a
|
|
|
|
;; Get the highmask and apply it.
|
|
ld hl, highmask1
|
|
or [hl]
|
|
ldh [rAUD1HIGH], a
|
|
|
|
ret
|
|
|
|
play_ch2_note:
|
|
ld a, [mute_channels]
|
|
retMute 1
|
|
|
|
;; Play a note on channel 2 (square wave)
|
|
ld a, [temp_note_value]
|
|
ld [channel_period2], a
|
|
ldh [rAUD2LOW], a
|
|
|
|
ld a, [temp_note_value+1]
|
|
ld [channel_period2+1], a
|
|
|
|
;; Get the highmask and apply it.
|
|
ld hl, highmask2
|
|
or [hl]
|
|
ldh [rAUD2HIGH], a
|
|
|
|
ret
|
|
|
|
play_ch3_note:
|
|
ld a, [mute_channels]
|
|
retMute 2
|
|
|
|
;; Triggering CH3 while it's reading a byte corrupts wave RAM.
|
|
;; To avoid this, we kill the wave channel (0 → NR30), then re-enable it.
|
|
;; This way, CH3 will be paused when we trigger it by writing to NR34.
|
|
;; TODO: what if `highmask3` bit 7 is not set, though?
|
|
xor a
|
|
ldh [rAUD3ENA], a
|
|
cpl
|
|
ldh [rAUD3ENA], a
|
|
|
|
;; Play a note on channel 3 (waveform)
|
|
ld a, [temp_note_value]
|
|
ld [channel_period3], a
|
|
ldh [rAUD3LOW], a
|
|
|
|
ld a, [temp_note_value+1]
|
|
ld [channel_period3+1], a
|
|
|
|
;; Get the highmask and apply it.
|
|
ld hl, highmask3
|
|
or [hl]
|
|
ldh [rAUD3HIGH], a
|
|
|
|
ret
|
|
|
|
play_ch4_note:
|
|
ld a, [mute_channels]
|
|
retMute 3
|
|
|
|
;; Play a "note" on channel 4 (noise)
|
|
ld a, [temp_note_value]
|
|
ld [channel_period4+1], a
|
|
ldh [rAUD4POLY], a
|
|
|
|
;; Get the highmask and apply it.
|
|
ld a, [highmask4]
|
|
ldh [rAUD4GO], a
|
|
|
|
ret
|
|
|
|
|
|
;;; Performs an effect on a given channel.
|
|
;;; Param: E = Channel ID (0 = CH1, 1 = CH2, etc.)
|
|
;;; Param: B = Effect type (upper 4 bits ignored)
|
|
;;; Param: C = Effect parameters (depend on FX type)
|
|
;;; Destroy: AF BC DE HL
|
|
do_effect:
|
|
;; Strip the instrument bits off leaving only effect code
|
|
ld a, b
|
|
and %00001111
|
|
;; Multiply by 2 to get offset into table
|
|
add a
|
|
|
|
add LOW(.jump)
|
|
ld l, a
|
|
adc HIGH(.jump)
|
|
sub l
|
|
ld h, a
|
|
|
|
ld a, [hl+]
|
|
ld h, [hl]
|
|
ld l, a
|
|
|
|
ld b, e
|
|
ld a, [tick]
|
|
or a ; We can return right off the bat if it's tick zero
|
|
jp hl
|
|
|
|
.jump:
|
|
;; Jump table for effect
|
|
dw fx_arpeggio ;0xy
|
|
dw fx_porta_up ;1xy
|
|
dw fx_porta_down ;2xy
|
|
dw fx_toneporta ;3xy
|
|
dw fx_vibrato ;4xy
|
|
dw fx_set_master_volume ;5xy ; global
|
|
dw fx_call_routine ;6xy
|
|
dw fx_note_delay ;7xy
|
|
dw fx_set_pan ;8xy ; global
|
|
dw fx_set_duty ;9xy
|
|
dw fx_vol_slide ;Axy
|
|
dw fx_pos_jump ;Bxy ; global
|
|
dw fx_set_volume ;Cxy
|
|
dw fx_pattern_break ;Dxy ; global
|
|
dw fx_note_cut ;Exy
|
|
dw fx_set_speed ;Fxy ; global
|
|
|
|
|
|
;;; Processes (global) effect 5, "set master volume".
|
|
;;; Param: C = Value to write to NR50
|
|
;;; Param: ZF = Set if and only if on tick 0
|
|
;;; Destroy: A
|
|
fx_set_master_volume:
|
|
ret nz
|
|
|
|
ld a, c
|
|
ldh [rAUDVOL], a
|
|
ret
|
|
|
|
|
|
;;; Processes effect 6, "call routine".
|
|
;;; Param: B = Current channel ID (0 = CH1, 1 = CH2, etc.)
|
|
;;; Param: C = Routine ID
|
|
;;; Param: A = Current tick
|
|
;;; Param: ZF = Set if and only if on tick 0
|
|
;;; Destroy: Anything the routine does
|
|
fx_call_routine:
|
|
sla c
|
|
ld a, [routines]
|
|
add c
|
|
ld l, a
|
|
ld a, [routines+1]
|
|
adc 0
|
|
ld h, a
|
|
|
|
ld a, [hl+]
|
|
ld h, [hl]
|
|
ld l, a
|
|
|
|
ld a, [tick]
|
|
or a ; set zero flag if tick 0 for compatibility
|
|
IF DEF(GBDK) ; Pass the tick counter as a SDCC call parameter
|
|
push af
|
|
inc sp
|
|
push bc
|
|
call .call_hl
|
|
add sp, 3
|
|
ret
|
|
|
|
.call_hl:
|
|
ENDC
|
|
jp hl
|
|
|
|
|
|
;;; Processes (global) effect 8, "set pan".
|
|
;;; Param: B = Current channel ID (0 = CH1, 1 = CH2, etc.)
|
|
;;; Param: C = Value to write to NR51
|
|
;;; Param: ZF = Set if and only if on tick 0
|
|
;;; Destroy: A
|
|
fx_set_pan:
|
|
ret nz
|
|
|
|
;; Pretty simple. The editor can create the correct value here without a bunch
|
|
;; of bit shifting manually.
|
|
ld a, c
|
|
ldh [rAUDTERM], a
|
|
ret
|
|
|
|
|
|
;;; Processes effect 9, "set duty cycle".
|
|
;;; Param: B = Current channel ID (0 = CH1, anything else = CH2)
|
|
;;; Param: C = Value to write to NRx1
|
|
;;; Param: ZF = Set if and only if on tick 0
|
|
;;; Destroy: AF
|
|
fx_set_duty:
|
|
ret nz
|
|
|
|
;; $900 = 12.5%
|
|
;; $940 = 25%
|
|
;; $980 = 50%
|
|
;; $9C0 = 75%
|
|
|
|
ld a, b
|
|
or a
|
|
ld a, [mute_channels]
|
|
jr z, .chan1
|
|
.chan2:
|
|
retMute 1
|
|
ld a, c
|
|
ldh [rAUD2LEN], a
|
|
ret
|
|
.chan1:
|
|
retMute 0
|
|
ld a, c
|
|
ldh [rAUD1LEN], a
|
|
ret
|
|
|
|
|
|
;;; Processes effect A, "volume slide".
|
|
;;; Param: B = Current channel ID (0 = CH1, 1 = CH2, etc.)
|
|
;;; Param: C = FX param; either nibble should be 0, otherwise weird (unspecified) behavior may arise
|
|
;;; Param: ZF = Set if and only if on tick 0
|
|
;;; Destroy: AF C DE HL
|
|
fx_vol_slide:
|
|
ret nz
|
|
|
|
;; This is really more of a "retrigger note with lower volume" effect and thus
|
|
;; isn't really that useful. Instrument envelopes should be used instead.
|
|
;; Might replace this effect with something different if a new effect is
|
|
;; ever needed.
|
|
|
|
;; check channel mute
|
|
|
|
;; 0 → $01, 1 → $02, 2 → $04, 3 → $05
|
|
;; Overall, these two instructions add 1 to the number.
|
|
;; However, the first instruction will generate a carry for inputs of $02 and $03;
|
|
;; the `adc` will pick the carry up, and "separate" 0 / 1 from 2 / 3 by an extra 1.
|
|
;; Luckily, this yields correct results for 0 ($01), 1 ($02), and 2 ($03 + 1 = $04).
|
|
;; We'll see about fixing 3 afterwards.
|
|
add -2
|
|
adc 3
|
|
;; After being shifted left, the inputs are $02, $04, $08 and $0A; all are valid BCD,
|
|
;; except for $0A. Since we just performed `add a`, DAA will correct the latter to $10.
|
|
;; (This should be correctly emulated everywhere, since the inputs are identical to
|
|
;; "regular" BCD.)
|
|
;; When shifting the results back, we'll thus get $01, $02, $04 and $08!
|
|
add a
|
|
daa
|
|
rra
|
|
ld d, a
|
|
ld a, [mute_channels]
|
|
and d
|
|
ret nz
|
|
|
|
;; setup the up and down params
|
|
ld a, c
|
|
and %00001111
|
|
ld d, a
|
|
|
|
ld a, c
|
|
and %11110000
|
|
ld e, a
|
|
swap e
|
|
|
|
; There are 5 bytes between each envelope register
|
|
ld a, b
|
|
add a
|
|
add a
|
|
add b
|
|
add LOW(rAUD1ENV)
|
|
ld c, a
|
|
|
|
ldh a, [c]
|
|
and %11110000
|
|
swap a
|
|
sub d
|
|
jr nc, .cont1
|
|
xor a
|
|
.cont1:
|
|
add e
|
|
cp $10
|
|
jr c, .cont2
|
|
ld a, $F
|
|
.cont2:
|
|
swap a
|
|
ldh [c], a
|
|
|
|
; Go to rAUDxGO, which is 2 bytes after
|
|
inc c
|
|
inc c
|
|
ldh a, [c]
|
|
or %10000000
|
|
ldh [c], a
|
|
|
|
jr play_note
|
|
|
|
|
|
;;; Processes effect 7, "note delay".
|
|
;;; Param: B = Current channel ID (0 = CH1, 1 = CH2, etc.)
|
|
;;; Param: C = Amount of ticks by which to delay the note
|
|
;;; Caveats: 0 never plays the note, and a delay longer than a row's duration skips the note entirely
|
|
;;; Param: ZF = Set if and only if on tick 0
|
|
;;; Destroy: AF D HL
|
|
fx_note_delay:
|
|
jr nz, .play_note
|
|
|
|
;; Just store the note into the channel period, and don't play a note.
|
|
ld d, 0
|
|
call ptr_to_channel_member
|
|
|
|
ld a, [temp_note_value]
|
|
ld [hl+], a
|
|
ld a, [temp_note_value+1]
|
|
ld [hl], a
|
|
|
|
;; Don't call _playnote. This is done by grabbing the return
|
|
;; address and manually skipping the next call instruction.
|
|
ret_dont_call_playnote
|
|
|
|
.play_note:
|
|
cp c
|
|
ret nz ; wait until the correct tick to play the note
|
|
|
|
;; fallthrough
|
|
|
|
|
|
;;; Plays a channel's current note.
|
|
;;; Param: B = Which channel (0 = CH1, 1 = CH2, etc.)
|
|
;;; Destroy: AF D HL
|
|
play_note:
|
|
ld d, 0
|
|
call ptr_to_channel_member
|
|
|
|
;; TODO: Change this to accept HL instead?
|
|
ld a, [hl+]
|
|
ld [temp_note_value], a
|
|
ld a, [hl]
|
|
ld [temp_note_value+1], a
|
|
|
|
ld a, b
|
|
add a
|
|
add LOW(play_note_routines)
|
|
ld l, a
|
|
adc HIGH(play_note_routines)
|
|
sub l
|
|
ld h, a
|
|
jp hl
|
|
|
|
|
|
;;; Processes (global) effect F, "set speed".
|
|
;;; Param: C = New amount of ticks per row
|
|
;;; Param: ZF = Set if and only if on tick 0
|
|
;;; Destroy: A
|
|
fx_set_speed:
|
|
ret nz
|
|
|
|
ld a, c
|
|
ld [ticks_per_row], a
|
|
ret
|
|
|
|
|
|
hUGE_set_position::
|
|
;;; Processes (global) effect B, "position jump".
|
|
;;; Param: C = ID of the order to jump to
|
|
;;; Destroy: A
|
|
fx_pos_jump:
|
|
ld a, 1
|
|
ld [row_break], a
|
|
ld a, c
|
|
ld [next_order], a
|
|
ret
|
|
|
|
|
|
;;; Processes (global) effect D, "pattern break".
|
|
;;; Param: C = ID of the next order's row to start on
|
|
;;; Destroy: A
|
|
fx_pattern_break:
|
|
ld a, c
|
|
ld [row_break], a
|
|
ret
|
|
|
|
|
|
;;; Processes effect E, "note cut".
|
|
;;; Param: B = Current channel ID (0 = CH1, 1 = CH2, etc.)
|
|
;;; Param: C = Tick to cut the note on (TODO: what does cutting on tick 0 do?)
|
|
;;; Param: A = Current tick
|
|
;;; Destroy: A
|
|
fx_note_cut:
|
|
cp c
|
|
ret nz
|
|
|
|
;; check channel mute
|
|
|
|
;; 0 → $01, 1 → $02, 2 → $04, 3 → $05
|
|
;; Overall, these two instructions add 1 to the number.
|
|
;; However, the first instruction will generate a carry for inputs of $02 and $03;
|
|
;; the `adc` will pick the carry up, and "separate" 0 / 1 from 2 / 3 by an extra 1.
|
|
;; Luckily, this yields correct results for 0 ($01), 1 ($02), and 2 ($03 + 1 = $04).
|
|
;; We'll see about fixing 3 afterwards.
|
|
add -2
|
|
adc 3
|
|
;; After being shifted left, the inputs are $02, $04, $08 and $0A; all are valid BCD,
|
|
;; except for $0A. Since we just performed `add a`, DAA will correct the latter to $10.
|
|
;; (This should be correctly emulated everywhere, since the inputs are identical to
|
|
;; "regular" BCD.)
|
|
;; When shifting the results back, we'll thus get $01, $02, $04 and $08!
|
|
add a
|
|
daa
|
|
rra
|
|
ld d, a
|
|
ld a, [mute_channels]
|
|
and d
|
|
ret nz
|
|
|
|
;; fallthrough
|
|
|
|
|
|
;;; Cuts note on a channel.
|
|
;;; Param: B = Current channel ID (0 = CH1, 1 = CH2, etc.)
|
|
;;; Destroy: AF HL
|
|
note_cut:
|
|
ld a, b
|
|
add a
|
|
add a
|
|
add b ; multiply by 5
|
|
add LOW(rAUD1ENV)
|
|
ld l, a
|
|
ld h, HIGH(rAUD1ENV)
|
|
xor a
|
|
ld [hl+], a
|
|
ld a, b
|
|
cp 2
|
|
ret z ; return early if CH3-- no need to retrigger note
|
|
|
|
;; Retrigger note
|
|
inc l ; Not `inc hl` because H stays constant (= $FF)
|
|
ld [hl], $FF
|
|
ret
|
|
|
|
|
|
;;; Processes effect C, "set volume".
|
|
;;; Param: B = Current channel ID (0 = CH1, 1 = CH2, etc.)
|
|
;;; Param: C = Volume to set the channel to
|
|
;;; Param: ZF = Set if and only if on tick 0
|
|
;;; Destroy: AF BC
|
|
fx_set_volume:
|
|
ret nz ; Return if we're not on tick zero.
|
|
|
|
swap c
|
|
ld a, [mute_channels]
|
|
dec b
|
|
jr z, .set_chn_2_vol
|
|
dec b
|
|
jr z, .set_chn_3_vol
|
|
dec b
|
|
jr z, .set_chn_4_vol
|
|
|
|
.set_chn_1_vol:
|
|
retMute 0
|
|
|
|
ldh a, [rAUD1ENV]
|
|
and %00001111
|
|
or c
|
|
ldh [rAUD1ENV], a
|
|
ret
|
|
|
|
.set_chn_2_vol:
|
|
retMute 1
|
|
|
|
ldh a, [rAUD2ENV]
|
|
and %00001111
|
|
or c
|
|
ldh [rAUD2ENV], a
|
|
ret
|
|
|
|
.set_chn_3_vol:
|
|
retMute 2
|
|
|
|
;; "Quantize" the more finely grained volume control down to one of 4 values.
|
|
ld a, c
|
|
cp 10 << 4
|
|
jr nc, .one
|
|
cp 5 << 4
|
|
jr nc, .two
|
|
or a
|
|
jr z, .done ; Zero maps to zero
|
|
.three:
|
|
ld a, %01100000
|
|
jr .done
|
|
.two:
|
|
ld a, %01000000
|
|
jr .done
|
|
.one:
|
|
ld a, %00100000
|
|
.done:
|
|
ldh [rAUD3LEVEL], a
|
|
ret
|
|
|
|
.set_chn_4_vol:
|
|
retMute 3
|
|
|
|
ld a, c
|
|
ldh [rAUD4ENV], a
|
|
ret
|
|
|
|
|
|
;;; Processes effect 4, "vibrato".
|
|
;;; Param: B = Current channel ID (0 = CH1, 1 = CH2, etc.)
|
|
;;; Param: C = FX param
|
|
;;; Param: ZF = Set if and only if on tick 0
|
|
;;; Destroy: AF B DE HL
|
|
fx_vibrato:
|
|
ret z
|
|
|
|
;; Extremely poor man's vibrato.
|
|
;; Speed values:
|
|
;; (0x0 = 1.0)
|
|
;; (0x1 = 0.5)
|
|
;; (0x3 = 0.25)
|
|
;; (0x7 = 0.125)
|
|
;; (0xf = 0.0625)
|
|
ld d, 4
|
|
call ptr_to_channel_member
|
|
|
|
ld a, c
|
|
and %11110000
|
|
swap a
|
|
ld e, a
|
|
|
|
ld a, [counter]
|
|
and e
|
|
ld a, [hl]
|
|
jr z, .go_up
|
|
.restore:
|
|
call get_note_period
|
|
jr .finish_vibrato
|
|
.go_up:
|
|
call get_note_period
|
|
ld a, c
|
|
and %00001111
|
|
add_a_to_hl
|
|
.finish_vibrato:
|
|
ld d, h
|
|
ld e, l
|
|
xor a
|
|
jp update_channel_freq
|
|
|
|
|
|
;;; Processes effect 8, "arpeggio".
|
|
;;; Param: B = Current channel ID (0 = CH1, 1 = CH2, etc.)
|
|
;;; Param: C = Offsets in semitones (each nibble)
|
|
;;; Param: ZF = Set if and only if on tick 0
|
|
;;; Destroy: AF B DE HL
|
|
fx_arpeggio:
|
|
ret z
|
|
|
|
ld d, 4
|
|
call ptr_to_channel_member
|
|
ld d, [hl]
|
|
|
|
ld a, [tick]
|
|
dec a
|
|
|
|
;; TODO: A crappy modulo, because it's not a multiple of four :(
|
|
|
|
jr .test_greater_than_two
|
|
.greater_than_two:
|
|
sub 3
|
|
.test_greater_than_two:
|
|
cp 3
|
|
jr nc, .greater_than_two
|
|
|
|
;; Multiply by 2 to get offset into table
|
|
add a
|
|
|
|
add LOW(.arp_options)
|
|
ld l, a
|
|
adc HIGH(.arp_options)
|
|
sub l
|
|
ld h, a
|
|
jp hl
|
|
|
|
.arp_options:
|
|
jr .set_arp1
|
|
jr .set_arp2
|
|
;; No `jr .reset_arp`
|
|
|
|
.reset_arp:
|
|
ld a, d
|
|
jr .finish_skip_add
|
|
|
|
.set_arp2:
|
|
ld a, c
|
|
swap a
|
|
db $FE ; cp <imm8> gobbles next byte
|
|
|
|
.set_arp1:
|
|
ld a, c
|
|
.finish_arp:
|
|
and %00001111
|
|
add d
|
|
.finish_skip_add:
|
|
call get_note_period
|
|
ld d, h
|
|
ld e, l
|
|
xor a
|
|
jp update_channel_freq
|
|
|
|
|
|
;;; Processes effect 1, "portamento up".
|
|
;;; Param: B = Current channel ID (0 = CH1, 1 = CH2, etc.)
|
|
;;; Param: C = How many units to slide the pitch by per tick
|
|
;;; Param: ZF = Set if and only if on tick 0
|
|
;;; Destroy: A B DE HL
|
|
fx_porta_up:
|
|
ret z
|
|
|
|
ld d, 0
|
|
call ptr_to_channel_member
|
|
|
|
;; Add C to 16-bit value at HL
|
|
ld a, [hl+]
|
|
add c
|
|
ld e, a
|
|
adc [hl]
|
|
sub e
|
|
|
|
;; Write back
|
|
.finish:
|
|
ld d, a ; Store A for call to `update_channel_freq`
|
|
ld [hl-], a
|
|
ld [hl], e
|
|
|
|
xor a
|
|
jp update_channel_freq
|
|
|
|
|
|
;;; Processes (global) effect 2, "portamento down".
|
|
;;; Param: B = Current channel ID (0 = CH1, 1 = CH2, etc.)
|
|
;;; Param: C = How many units to slide the pitch down by per tick
|
|
;;; Param: ZF = Set if and only if on tick 0
|
|
;;; Destroy: A B DE HL
|
|
fx_porta_down:
|
|
ret z
|
|
|
|
ld d, 0
|
|
call ptr_to_channel_member
|
|
|
|
;; Subtract C from 16-bit value at [HL]
|
|
ld a, [hl+]
|
|
sub c
|
|
ld e, a
|
|
sbc a
|
|
add [hl]
|
|
|
|
;; Write back
|
|
jr fx_porta_up.finish
|
|
|
|
|
|
;;; Processes effect 2, "tone portamento".
|
|
;;; Param: B = Current channel ID (0 = CH1, 1 = CH2, etc.)
|
|
;;; Param: C = Target note
|
|
;;; Param: ZF = Set if and only if on tick 0
|
|
;;; Destroy: A B DE HL
|
|
fx_toneporta:
|
|
jr nz, .do_toneporta
|
|
|
|
;; We're on tick zero, so just move the temp note value into the toneporta target.
|
|
ld d, 2
|
|
call ptr_to_channel_member
|
|
|
|
;; If the note is nonexistent, then just return
|
|
ld a, [temp_note_value]
|
|
or a
|
|
jr z, .return_skip
|
|
|
|
ld [hl+], a
|
|
ld a, [temp_note_value+1]
|
|
ld [hl], a
|
|
|
|
;; Don't call _playnote. This is done by grabbing the return
|
|
;; address and manually skipping the next call instruction.
|
|
.return_skip:
|
|
ret_dont_call_playnote
|
|
|
|
.do_toneporta:
|
|
ld d, 0
|
|
call ptr_to_channel_member
|
|
push hl
|
|
|
|
;; Read current period
|
|
ld a, [hl+]
|
|
ld e, a
|
|
ld a, [hl+]
|
|
ld d, a
|
|
|
|
;; Read target period
|
|
ld a, [hl+]
|
|
ld h, [hl]
|
|
ld l, a
|
|
|
|
;; Do we need to porta up, or down? Compute (current - target) and check carry to know
|
|
sub e
|
|
ld a, h
|
|
sbc d
|
|
jr c, .porta_down ; Current period (DE) is higher than target one (HL), so down we go!
|
|
|
|
;; Add offset to current freq
|
|
ld a, e
|
|
add c
|
|
ld e, a
|
|
adc d
|
|
sub e
|
|
ld d, a
|
|
;; We don't need to worry about overflow given the relatively low values we work with
|
|
|
|
ld c, 0 ; The overshoot comparison should yield no carry, like the above one
|
|
jr .check_overshoot
|
|
|
|
.porta_down:
|
|
;; Subtract offset from current freq
|
|
ld a, e
|
|
sub c
|
|
ld e, a
|
|
sbc a
|
|
add d
|
|
ld d, a
|
|
jr c, .overshot ; There will be no underflows under my watch!
|
|
|
|
ld c, $FF ; The overshoot comparison should yield carry, like the above one
|
|
|
|
.check_overshoot:
|
|
ld a, l
|
|
sub e
|
|
ld a, h
|
|
sbc d
|
|
rra ; Shift carry into bit 7
|
|
xor c ; XOR it with provided value
|
|
rla ; Shift maybe-toggled carry back
|
|
jr nc, .no_overshoot
|
|
.overshot:
|
|
;; Override computed new period with target
|
|
ld d, h
|
|
ld e, l
|
|
.no_overshoot:
|
|
|
|
pop hl
|
|
ld a, e
|
|
ld [hl+], a
|
|
ld [hl], d
|
|
|
|
;; Do not retrigger channel
|
|
ld a, 6
|
|
add_a_to_hl
|
|
ld a, [hl]
|
|
res 7, [hl]
|
|
;; B must be preserved for this
|
|
jp update_channel_freq
|
|
|
|
|
|
;; TODO: Find some way to de-duplicate this code!
|
|
;;; Computes the pointer to a CH4 instrument.
|
|
;;; Param: B = The instrument's ID
|
|
;;; Param: DE = Instrument pointer table
|
|
;;; Return: DE = Pointer to the instrument
|
|
;;; Return: ZF = Set if and only if there was no instrument (ID == 0)
|
|
;;; Destroy: AF
|
|
setup_instrument_pointer_ch4:
|
|
;; Call with:
|
|
;; Instrument/High nibble of effect in B
|
|
;; Stores whether the instrument was real in the Z flag
|
|
;; Stores the instrument pointer in DE
|
|
ld a, b
|
|
and %11110000
|
|
swap a
|
|
ret z ; If there's no instrument, then return early.
|
|
|
|
dec a ; Instrument 0 is "no instrument"
|
|
add a
|
|
jr setup_instrument_pointer.finish
|
|
|
|
;;; Computes the pointer to an instrument.
|
|
;;; Param: B = The instrument's ID
|
|
;;; Param: DE = Instrument pointer table
|
|
;;; Return: DE = Pointer to the instrument
|
|
;;; Return: ZF = Set if and only if there was no instrument (ID == 0)
|
|
;;; Destroy: AF
|
|
setup_instrument_pointer:
|
|
;; Call with:
|
|
;; Instrument/High nibble of effect in B
|
|
;; Stores whether the instrument was real in the Z flag
|
|
;; Stores the instrument pointer in DE
|
|
ld a, b
|
|
and %11110000
|
|
swap a
|
|
ret z ; If there's no instrument, then return early.
|
|
|
|
dec a ; Instrument 0 is "no instrument"
|
|
.finish:
|
|
;; Shift left twice to multiply by 4
|
|
add a
|
|
add a
|
|
|
|
add_a_to_de
|
|
|
|
rla ; reset the Z flag
|
|
ret
|
|
|
|
|
|
_hUGE_dosound_banked::
|
|
_hUGE_dosound::
|
|
;;; Ticks the sound engine once.
|
|
;;; Destroy: AF BC DE HL
|
|
hUGE_dosound::
|
|
ld a, [tick]
|
|
or a
|
|
jp nz, process_effects
|
|
|
|
;; Note playback
|
|
ld hl, pattern1
|
|
ld de, channel_note1
|
|
call get_current_note
|
|
push af
|
|
jr nc, .do_setvol1
|
|
|
|
load_de_ind duty_instruments
|
|
call setup_instrument_pointer
|
|
ld a, [highmask1]
|
|
res 7, a ; Turn off the "initial" flag
|
|
jr z, .write_mask1
|
|
|
|
checkMute 0, .do_setvol1
|
|
|
|
ld a, [de]
|
|
inc de
|
|
ldh [rAUD1SWEEP], a
|
|
ld a, [de]
|
|
inc de
|
|
ldh [rAUD1LEN], a
|
|
ld a, [de]
|
|
ldh [rAUD1ENV], a
|
|
inc de
|
|
ld a, [de]
|
|
|
|
.write_mask1:
|
|
ld [highmask1], a
|
|
|
|
.do_setvol1:
|
|
ld a, l
|
|
ld [temp_note_value], a
|
|
ld a, h
|
|
ld [temp_note_value+1], a
|
|
|
|
ld e, 0
|
|
call do_effect
|
|
|
|
pop af
|
|
call c, play_ch1_note
|
|
|
|
process_ch2:
|
|
;; Note playback
|
|
ld hl, pattern2
|
|
ld de, channel_note2
|
|
call get_current_note
|
|
push af
|
|
jr nc, .do_setvol2
|
|
|
|
load_de_ind duty_instruments
|
|
call setup_instrument_pointer
|
|
ld a, [highmask2]
|
|
res 7, a ; Turn off the "initial" flag
|
|
jr z, .write_mask2
|
|
|
|
checkMute 1, .do_setvol2
|
|
|
|
inc de
|
|
ld a, [de]
|
|
inc de
|
|
ldh [rAUD2LEN], a
|
|
ld a, [de]
|
|
ldh [rAUD2ENV], a
|
|
inc de
|
|
ld a, [de]
|
|
|
|
.write_mask2:
|
|
ld [highmask2], a
|
|
|
|
.do_setvol2:
|
|
ld a, l
|
|
ld [temp_note_value], a
|
|
ld a, h
|
|
ld [temp_note_value+1], a
|
|
|
|
ld e, 1
|
|
call do_effect
|
|
|
|
pop af
|
|
call c, play_ch2_note
|
|
|
|
process_ch3:
|
|
ld hl, pattern3
|
|
ld de, channel_note3
|
|
call get_current_note
|
|
|
|
ld a, l
|
|
ld [temp_note_value], a
|
|
ld a, h
|
|
ld [temp_note_value+1], a
|
|
|
|
push af
|
|
|
|
jr nc, .do_setvol3
|
|
|
|
load_de_ind wave_instruments
|
|
call setup_instrument_pointer
|
|
ld a, [highmask3]
|
|
res 7, a ; Turn off the "initial" flag
|
|
jr z, .write_mask3
|
|
|
|
checkMute 2, .do_setvol3
|
|
|
|
ld a, [de]
|
|
inc de
|
|
ldh [rAUD3LEN], a
|
|
ld a, [de]
|
|
inc de
|
|
ldh [rAUD3LEVEL], a
|
|
ld a, [de]
|
|
inc de
|
|
|
|
;; Check to see if we need to copy a wave and then do so
|
|
ld hl, current_wave
|
|
cp [hl]
|
|
jr z, .no_wave_copy
|
|
ld [hl], a
|
|
swap a
|
|
add_a_ind_ret_hl waves
|
|
|
|
xor a
|
|
ldh [rAUD3ENA], a
|
|
|
|
FOR OFS, 16
|
|
ld a, [hl+]
|
|
ldh [_AUD3WAVERAM + OFS], a
|
|
ENDR
|
|
|
|
ld a, %10000000
|
|
ldh [rAUD3ENA], a
|
|
|
|
.no_wave_copy:
|
|
ld a, [de]
|
|
|
|
.write_mask3:
|
|
ld [highmask3], a
|
|
|
|
.do_setvol3:
|
|
ld e, 2
|
|
call do_effect
|
|
|
|
pop af
|
|
call c, play_ch3_note
|
|
|
|
process_ch4:
|
|
ld hl, pattern4
|
|
ld a, [hl+]
|
|
ld c, a
|
|
ld b, [hl]
|
|
call get_current_row
|
|
ld [channel_note4], a
|
|
cp LAST_NOTE
|
|
push af
|
|
jr nc, .do_setvol4
|
|
|
|
call get_note_poly
|
|
ld [temp_note_value], a
|
|
|
|
ld de, 0
|
|
call setup_instrument_pointer
|
|
|
|
ld a, [highmask4]
|
|
res 7, a ; Turn off the "initial" flag
|
|
jr z, .write_mask4
|
|
|
|
checkMute 3, .do_setvol4
|
|
|
|
load_hl_ind noise_instruments
|
|
sla e
|
|
add hl, de
|
|
|
|
ld a, [hl+]
|
|
ldh [rAUD4ENV], a
|
|
|
|
ld a, [hl]
|
|
and %00111111
|
|
ldh [rAUD4LEN], a
|
|
|
|
ld a, [temp_note_value]
|
|
ld d, a
|
|
ld a, [hl]
|
|
and %10000000
|
|
swap a
|
|
or d
|
|
ld [temp_note_value], a
|
|
|
|
ld a, [hl]
|
|
and %01000000
|
|
or %10000000
|
|
.write_mask4:
|
|
ld [highmask4], a
|
|
|
|
.do_setvol4:
|
|
ld e, 3
|
|
call do_effect
|
|
|
|
pop af
|
|
call c, play_ch4_note
|
|
|
|
;; finally just update the tick/order/row values
|
|
jp process_tick
|
|
|
|
process_effects:
|
|
;; Only do effects if not on tick zero
|
|
checkMute 0, .after_effect1
|
|
|
|
ld hl, pattern1
|
|
ld a, [hl+]
|
|
ld c, a
|
|
ld b, [hl]
|
|
call get_current_row
|
|
|
|
ld a, c
|
|
or a
|
|
jr z, .after_effect1
|
|
|
|
ld e, 0
|
|
call do_effect ; make sure we never return with ret_dont_call_playnote macro
|
|
|
|
.after_effect1:
|
|
checkMute 1, .after_effect2
|
|
|
|
ld hl, pattern2
|
|
ld a, [hl+]
|
|
ld c, a
|
|
ld b, [hl]
|
|
call get_current_row
|
|
|
|
ld a, c
|
|
or a
|
|
jr z, .after_effect2
|
|
|
|
ld e, 1
|
|
call do_effect ; make sure we never return with ret_dont_call_playnote macro
|
|
|
|
.after_effect2:
|
|
checkMute 2, .after_effect3
|
|
|
|
ld hl, pattern3
|
|
ld a, [hl+]
|
|
ld c, a
|
|
ld b, [hl]
|
|
call get_current_row
|
|
|
|
ld a, c
|
|
or a
|
|
jr z, .after_effect3
|
|
|
|
ld e, 2
|
|
call do_effect ; make sure we never return with ret_dont_call_playnote macro
|
|
|
|
.after_effect3:
|
|
checkMute 3, .after_effect4
|
|
|
|
ld hl, pattern4
|
|
ld a, [hl+]
|
|
ld c, a
|
|
ld b, [hl]
|
|
call get_current_row
|
|
cp LAST_NOTE
|
|
jr nc, .done_macro
|
|
ld h, a
|
|
|
|
load_de_ind noise_instruments
|
|
call setup_instrument_pointer_ch4
|
|
jr z, .done_macro ; No instrument, thus no macro
|
|
|
|
ld a, [tick]
|
|
cp 7
|
|
jr nc, .done_macro
|
|
|
|
inc de
|
|
|
|
ld l, a
|
|
ld a, h
|
|
ld h, 0
|
|
add hl, de
|
|
add [hl]
|
|
call get_note_poly
|
|
ld l, a
|
|
ld a, [de]
|
|
ld e, a
|
|
and %10000000
|
|
swap a
|
|
or l
|
|
ldh [rAUD4POLY], a
|
|
|
|
ld a, e
|
|
and %01000000
|
|
ldh [rAUD4GO], a
|
|
|
|
.done_macro:
|
|
ld a, c
|
|
or a
|
|
jr z, .after_effect4
|
|
|
|
ld e, 3
|
|
call do_effect ; make sure we never return with ret_dont_call_playnote macro
|
|
|
|
.after_effect4:
|
|
|
|
process_tick:
|
|
ld hl, counter
|
|
inc [hl]
|
|
|
|
ld a, [ticks_per_row]
|
|
ld b, a
|
|
|
|
ld hl, tick
|
|
ld a, [hl]
|
|
inc a
|
|
|
|
cp b
|
|
jr z, .newrow
|
|
|
|
ld [hl], a
|
|
ret
|
|
|
|
.newrow:
|
|
;; Reset tick to 0
|
|
ld [hl], 0
|
|
|
|
;; Check if we need to perform a row break or pattern break
|
|
ld a, [row_break]
|
|
or a
|
|
jr z, .no_break
|
|
|
|
;; These are offset by one so we can check to see if they've
|
|
;; been modified
|
|
dec a
|
|
ld b, a
|
|
|
|
ld hl, row_break
|
|
xor a
|
|
ld [hl-], a
|
|
or [hl] ; a = [next_order], zf = ([next_order] == 0)
|
|
ld [hl], 0
|
|
|
|
jr z, .neworder
|
|
|
|
dec a
|
|
add a ; multiply order by 2 (they are words)
|
|
|
|
jr .update_current_order
|
|
|
|
.no_break:
|
|
;; Increment row.
|
|
ld a, [row]
|
|
inc a
|
|
ld b, a
|
|
cp PATTERN_LENGTH
|
|
jr nz, .noreset
|
|
|
|
ld b, 0
|
|
.neworder:
|
|
;; Increment order and change loaded patterns
|
|
ld a, [order_cnt]
|
|
ld c, a
|
|
ld a, [current_order]
|
|
add 2
|
|
cp c
|
|
jr nz, .update_current_order
|
|
xor a
|
|
.update_current_order:
|
|
;; Call with:
|
|
;; A: The order to load
|
|
;; B: The row for the order to start on
|
|
ld [current_order], a
|
|
ld c, a
|
|
call load_patterns
|
|
|
|
.noreset:
|
|
ld a, b
|
|
ld [row], a
|
|
|
|
IF DEF(PREVIEW_MODE)
|
|
db $fd ; signal row update to tracker
|
|
ENDC
|
|
ret
|
|
|
|
note_table:
|
|
include "huge_note_table.inc"
|
|
|
|
|
|
IF DEF(GBDK)
|
|
|
|
SECTION "hUGEDriver GBDK wrappers", ROM0
|
|
|
|
_hUGE_init_banked::
|
|
ld hl, sp+2+4
|
|
jr continue_init
|
|
_hUGE_init::
|
|
ld hl, sp+2
|
|
continue_init:
|
|
push bc
|
|
ld a, [hl+]
|
|
ld h, [hl]
|
|
ld l, a
|
|
call hUGE_init
|
|
pop bc
|
|
ret
|
|
|
|
_hUGE_mute_channel_banked::
|
|
ld hl, sp+3+4
|
|
jr continue_mute
|
|
_hUGE_mute_channel::
|
|
ld hl, sp+3
|
|
continue_mute:
|
|
push bc
|
|
ld a, [hl-]
|
|
and 1
|
|
ld c, a
|
|
ld b, [hl]
|
|
call hUGE_mute_channel
|
|
pop bc
|
|
ret
|
|
|
|
_hUGE_set_position_banked::
|
|
ld hl, sp+2+4
|
|
jr continue_set_position
|
|
_hUGE_set_position::
|
|
ld hl, sp+2
|
|
continue_set_position:
|
|
push bc
|
|
ld c, [hl]
|
|
call hUGE_set_position
|
|
pop bc
|
|
ret
|
|
|
|
ENDC
|