954 lines
31 KiB
ArmAsm
954 lines
31 KiB
ArmAsm
;==============================================================================
|
||
; spectrum.s — Spectrum analyzer: render + hot-loop helpers (Z80 ASM)
|
||
;
|
||
; Hand-optimised replacements for C functions in main.c / vgm.c:
|
||
; spectrum_render() — 4-row bar graph, direct VRAM write
|
||
; spectrum_opl_b0(r,v) — OPL B0-B8 key-on + 0xBD percussion
|
||
; spectrum_opl_b1(r,v) — OPL Bank1 B0-B6 key-on
|
||
; spectrum_ay(r,v) — AY period shadow + vol trigger + YM2203 FM (chip1)
|
||
; spectrum_ay2(r,v) — same for chip2 (base offset = 3)
|
||
; spectrum_saa(r,v) — SAA octave shadow + amp trigger
|
||
;
|
||
; sdcccall(1) conventions:
|
||
; 1st u8 → A, 2nd u8 → L, 3rd u8 → stack+2
|
||
; 1st u16 → HL, return u8 → A, return u16 → HL
|
||
;
|
||
; Port 0x13AF = Page3 window (TSConfig)
|
||
;==============================================================================
|
||
|
||
.module spectrum
|
||
|
||
; ── Exports ──────────────────────────────────────────────────────────
|
||
.globl _spectrum_render
|
||
.globl _spectrum_decay
|
||
.globl _spectrum_decay_reset
|
||
.globl _spectrum_opl_b0
|
||
.globl _spectrum_opl_b1
|
||
.globl _spectrum_ay
|
||
.globl _spectrum_ay2
|
||
.globl _spectrum_saa
|
||
.globl _spectrum_saa2
|
||
.globl _spectrum_wave
|
||
.globl _spec_period_to_bar
|
||
.globl _spectrum_font_init
|
||
.globl _spectrum_font_restore
|
||
|
||
; ── Imports ──────────────────────────────────────────────────────────
|
||
.globl _spectrum_levels ; uint8_t[16] in isr.s _DATA
|
||
.globl _isr_tick_ctr ; volatile uint8_t in isr.s _DATA
|
||
.globl _s_pb_text_pg ; uint8_t in main.c _DATA
|
||
.globl _s_spec_char_ofs ; uint16_t[4] in main.c _DATA
|
||
.globl _spec_mask ; uint16_t in vgm.c _DATA
|
||
.globl _spec_ay_period ; uint16_t[6] in vgm.c _DATA
|
||
.globl _spec_saa_oct ; uint8_t[6] in vgm.c _DATA
|
||
.globl _spec_saa_oct2 ; uint8_t[6] in vgm.c _DATA (chip 2)
|
||
.globl _spec_saa_dual ; uint8_t in vgm.c _DATA
|
||
.globl _spec_opl_bd ; uint8_t in vgm.c _DATA
|
||
.globl _spec_fm_block ; uint8_t[6] in vgm.c _DATA
|
||
.globl _spec_wave_active ; uint8_t[3]: 24-bit KeyOn mask
|
||
|
||
; ── Constants ────────────────────────────────────────────────────────
|
||
PORT_PAGE3 = 0x13AF
|
||
SPEC_BARS = 16
|
||
SPEC_LEFT_PAD = 5
|
||
SPEC_BAR_W = 3 ; 2 filled + 1 space
|
||
CHAR_FULL = 0x02 ; custom glyph: full-height striped bar
|
||
CHAR_HALF = 0x01 ; custom glyph: half-height striped bar
|
||
CLR_WIN = 0x70 ; WC_COLOR(WC_WHITE, WC_BLACK)
|
||
WC_PAGE_FONT0 = 0x01
|
||
FONT_BASE = 0xC000
|
||
; ── Timing: must match variant_cfg.h ────────────────────────────────
|
||
ISR_TICKS_PER_FRAME = 14 ; = ISR_TICKS_PER_FRAME in variant_cfg.h
|
||
; ── Decay: must match DECAY_FRAMES_* in variant_cfg.h ────────────────
|
||
DECAY_FAST = 1 ; = DECAY_FRAMES_FAST
|
||
DECAY_MED = 2 ; = DECAY_FRAMES_MED
|
||
DECAY_SLOW = 4 ; = DECAY_FRAMES_SLOW
|
||
|
||
;======================================================================
|
||
; Scratch variables in _DATA (kept minimal)
|
||
;======================================================================
|
||
.area _DATA
|
||
|
||
say_base: .db 0 ; temp: chip base index (0 or 3)
|
||
sr_row_ctr: .db 0 ; temp: row iteration counter
|
||
sr_rp_ptr: .dw 0 ; temp: current row-params pointer
|
||
sd_last_tick: .db 0 ; isr_tick_ctr snapshot for frame gate
|
||
sd_fast_ctr: .db DECAY_FAST ; countdown: decay levels >= 6
|
||
sd_med_ctr: .db DECAY_MED ; countdown: decay levels 3-5
|
||
sd_slow_ctr: .db DECAY_SLOW ; countdown: decay levels 1-2
|
||
sr_prev_hash: .db 0 ; XOR-rotate hash of spectrum_levels from previous render
|
||
|
||
.area _CODE
|
||
|
||
; Row parameter table: [thresh_full, thresh_half, color] × 4 rows
|
||
; Row 0 (top=peak): thresholds 8,7 color=BRIGHT_RED
|
||
; Row 1: thresholds 6,5 color=BRIGHT_YELLOW
|
||
; Row 2: thresholds 4,3 color=BRIGHT_GREEN
|
||
; Row 3 (bottom): thresholds 2,1 color=GREEN
|
||
sr_row_params:
|
||
.db 8, 7, 0x7A ; row 0: white bg + bright red
|
||
.db 6, 5, 0x7E ; row 1: white bg + bright yellow
|
||
.db 4, 3, 0x7C ; row 2: white bg + bright green
|
||
.db 2, 1, 0x74 ; row 3: white bg + green
|
||
|
||
;======================================================================
|
||
; spectrum_render(void)
|
||
;
|
||
; Optimised Z80 register layout — zero push/pop in inner bar loop:
|
||
; IX = char_ptr (stride +3 per bar via: inc ix x3)
|
||
; IY = attr_ptr (stride +3 per bar via: inc iy x3)
|
||
; HL = spectrum_levels + bar_index
|
||
; B = bar counter (DJNZ)
|
||
; C = thresh_full (row-constant, set ONCE before bar loop)
|
||
; D = thresh_half (row-constant, set ONCE before bar loop)
|
||
; E = color (row-constant, set ONCE before bar loop)
|
||
;
|
||
; Old inner loop loaded 0(ix)/1(ix)/2(ix) on every bar = 3x19=57 T
|
||
; per bar x 16 bars x 4 rows = 3648 wasted T-states per frame.
|
||
; New inner loop: C/D/E loaded once per row, never reloaded per bar.
|
||
; Total savings: ~3500 T-states per spectrum_render call.
|
||
;======================================================================
|
||
|
||
_spectrum_render::
|
||
; XOR-rotate hash over spectrum_levels[16] (~100T).
|
||
; Detects ANY change versus previous frame, not just zero/non-zero.
|
||
; Hash = seed, then for each byte: RLCA + XOR (hl).
|
||
ld hl, #_spectrum_levels
|
||
xor a
|
||
.rept 16
|
||
rlca
|
||
xor (hl)
|
||
inc hl
|
||
.endm
|
||
; A = rolling hash of current levels
|
||
ld hl, #sr_prev_hash
|
||
cp (hl) ; same as last frame?
|
||
ret z ; unchanged → skip render (~7000T saved)
|
||
ld (hl), a ; store new hash
|
||
|
||
push ix
|
||
push iy
|
||
|
||
ld bc, #PORT_PAGE3
|
||
in a, (c)
|
||
push af
|
||
push bc
|
||
|
||
ld a, (_s_pb_text_pg)
|
||
out (c), a
|
||
|
||
ld a, #4
|
||
ld (sr_row_ctr), a
|
||
ld hl, #sr_row_params
|
||
ld (sr_rp_ptr), hl
|
||
|
||
sr_row_loop:
|
||
;--------------------------------------------------------------
|
||
; row_index = 4 - row_ctr (0..3)
|
||
; Compute IX = char_ptr, IY = attr_ptr for this row.
|
||
;--------------------------------------------------------------
|
||
ld a, (sr_row_ctr)
|
||
ld e, a
|
||
ld a, #4
|
||
sub e
|
||
add a, a ; A = row_index * 2
|
||
ld e, a
|
||
ld d, #0
|
||
|
||
; IX = 0xC000 + s_spec_char_ofs[row_index] + SPEC_LEFT_PAD
|
||
ld hl, #_s_spec_char_ofs
|
||
add hl, de
|
||
ld a, (hl)
|
||
inc hl
|
||
ld h, (hl)
|
||
ld l, a ; HL = s_spec_char_ofs[row_index]
|
||
ld bc, #(0xC000 + SPEC_LEFT_PAD)
|
||
add hl, bc
|
||
push hl
|
||
pop ix ; IX = char_ptr
|
||
|
||
; IY = IX + 0x0080 (attr_ofs = char_ofs | 0x0080 in TSConf)
|
||
push ix
|
||
pop hl
|
||
ld de, #0x0080
|
||
add hl, de
|
||
push hl
|
||
pop iy ; IY = attr_ptr
|
||
|
||
;--------------------------------------------------------------
|
||
; Load row params ONCE into C/D/E: all three are row-constants.
|
||
; C, D, E survive the bar loop untouched (djnz only changes B).
|
||
;--------------------------------------------------------------
|
||
ld hl, (sr_rp_ptr)
|
||
ld c, (hl) ; C = thresh_full
|
||
inc hl
|
||
ld d, (hl) ; D = thresh_half
|
||
inc hl
|
||
ld e, (hl) ; E = color
|
||
inc hl
|
||
ld (sr_rp_ptr), hl ; advance for next row
|
||
|
||
;--------------------------------------------------------------
|
||
; Bar loop: 16 bars — all comparisons use stable C,D,E regs.
|
||
; No push/pop needed: all state lives in IX, IY, HL, B, C, D, E.
|
||
;--------------------------------------------------------------
|
||
ld hl, #_spectrum_levels
|
||
ld b, #SPEC_BARS
|
||
|
||
sr_bar_loop:
|
||
ld a, (hl)
|
||
cp c ; NC if level >= thresh_full
|
||
jr nc, sr_full
|
||
cp d ; NC if level >= thresh_half
|
||
jr nc, sr_half
|
||
|
||
sr_empty:
|
||
ld a, #' '
|
||
ld 0(ix), a
|
||
ld 1(ix), a
|
||
ld a, #CLR_WIN
|
||
ld 0(iy), a
|
||
ld 1(iy), a
|
||
jr sr_next
|
||
|
||
sr_half:
|
||
ld a, #CHAR_HALF
|
||
ld 0(ix), a
|
||
ld 1(ix), a
|
||
ld 0(iy), e ; E = row color (stable per row)
|
||
ld 1(iy), e
|
||
jr sr_next
|
||
|
||
sr_full:
|
||
ld a, #CHAR_FULL
|
||
ld 0(ix), a
|
||
ld 1(ix), a
|
||
ld 0(iy), e ; E = row color (stable per row)
|
||
ld 1(iy), e
|
||
|
||
sr_next:
|
||
inc hl ; next spectrum level
|
||
inc ix ; char_ptr += SPEC_BAR_W (3)
|
||
inc ix
|
||
inc ix
|
||
inc iy ; attr_ptr += SPEC_BAR_W (3)
|
||
inc iy
|
||
inc iy
|
||
djnz sr_bar_loop
|
||
|
||
ld a, (sr_row_ctr)
|
||
dec a
|
||
ld (sr_row_ctr), a
|
||
jp nz, sr_row_loop
|
||
|
||
pop bc
|
||
pop af
|
||
out (c), a
|
||
|
||
pop iy
|
||
pop ix
|
||
ret
|
||
|
||
;======================================================================
|
||
; spectrum_decay_reset() — initialize decay timers at song start
|
||
;======================================================================
|
||
_spectrum_decay_reset::
|
||
ld a, (_isr_tick_ctr)
|
||
ld (sd_last_tick), a
|
||
ld a, #DECAY_FAST
|
||
ld (sd_fast_ctr), a
|
||
ld a, #DECAY_MED
|
||
ld (sd_med_ctr), a
|
||
ld a, #DECAY_SLOW
|
||
ld (sd_slow_ctr), a
|
||
; Reset render hash so first frame always renders
|
||
ld a, #0xFF
|
||
ld (sr_prev_hash), a
|
||
ret
|
||
|
||
;======================================================================
|
||
; spectrum_decay() — frame-gated non-linear level decay
|
||
;
|
||
; Called every main-loop iteration; returns immediately if less than
|
||
; ISR_TICKS_PER_FRAME ticks have elapsed since the last frame.
|
||
;
|
||
; Three countdown timers control decay speed per level range:
|
||
; Levels >= 6 : -1 every 3 frames (~60 ms)
|
||
; Levels 3-5 : -1 every 6 frames (~180 ms)
|
||
; Levels 1-2 : -1 every 10 frames (~200 ms)
|
||
;
|
||
; Register layout:
|
||
; C = decay-flags bitmask (bit2=fast, bit1=med, bit0=slow)
|
||
; B = bar loop counter
|
||
; HL = pointer into spectrum_levels[]
|
||
;======================================================================
|
||
_spectrum_decay::
|
||
; ── Frame gate ──────────────────────────────────────────────
|
||
ld a, (_isr_tick_ctr)
|
||
ld hl, #sd_last_tick
|
||
sub (hl) ; elapsed = now - last
|
||
cp #ISR_TICKS_PER_FRAME
|
||
ret c ; < 1 frame, skip
|
||
|
||
; Advance by one frame
|
||
ld a, (hl)
|
||
add a, #ISR_TICKS_PER_FRAME
|
||
ld (hl), a
|
||
|
||
; ── Build decay-flags in C ──────────────────────────────────
|
||
ld c, #0
|
||
|
||
ld hl, #sd_fast_ctr ; 5-frame timer (levels >= 6)
|
||
dec (hl)
|
||
jr nz, sd_nf
|
||
ld (hl), #DECAY_FAST
|
||
set 2, c
|
||
sd_nf:
|
||
ld hl, #sd_med_ctr ; med-frame timer (levels 3-5)
|
||
dec (hl)
|
||
jr nz, sd_nm
|
||
ld (hl), #DECAY_MED
|
||
set 1, c
|
||
sd_nm:
|
||
ld hl, #sd_slow_ctr ; slow-frame timer (levels 1-2)
|
||
dec (hl)
|
||
jr nz, sd_ns
|
||
ld (hl), #DECAY_SLOW
|
||
set 0, c
|
||
sd_ns:
|
||
ld a, c
|
||
or a
|
||
ret z ; no tier expired → nothing to decay
|
||
|
||
; ── Single loop over 16 bars ────────────────────────────────
|
||
ld hl, #_spectrum_levels
|
||
; Quick all-zero check: if all bars are 0, skip decay loop
|
||
ld a, (hl)
|
||
inc hl
|
||
.rept 15
|
||
or (hl)
|
||
inc hl
|
||
.endm
|
||
ret z ; all bars zero → nothing to decay
|
||
|
||
ld hl, #_spectrum_levels
|
||
ld b, #SPEC_BARS
|
||
sd_bar_loop:
|
||
ld a, (hl)
|
||
or a
|
||
jr z, sd_next ; level 0 → skip
|
||
|
||
cp #6
|
||
jr nc, sd_high ; level >= 6 → fast tier
|
||
|
||
cp #3
|
||
jr nc, sd_mid ; level >= 3 → medium tier
|
||
|
||
; level 1-2: slow tier
|
||
bit 0, c
|
||
jr z, sd_next
|
||
dec (hl)
|
||
jr sd_next
|
||
|
||
sd_mid:
|
||
bit 1, c
|
||
jr z, sd_next
|
||
dec (hl)
|
||
jr sd_next
|
||
|
||
sd_high:
|
||
bit 2, c
|
||
jr z, sd_next
|
||
dec (hl)
|
||
|
||
sd_next:
|
||
inc hl
|
||
djnz sd_bar_loop
|
||
ret
|
||
|
||
;======================================================================
|
||
; spectrum_opl_b0(reg, val) — A=reg, L=val
|
||
;
|
||
; OPL Bank0: B0-B8 key-on + 0xBD percussion (rising edge).
|
||
; Sets bits in spec_mask.
|
||
;======================================================================
|
||
_spectrum_opl_b0::
|
||
; Save reg and val
|
||
ld d, a ; D = reg
|
||
ld e, l ; E = val
|
||
|
||
; ── Check B0-B8 key-on ──
|
||
cp a, #0xB0
|
||
jr c, sob0_check_bd ; reg < 0xB0
|
||
cp a, #0xB9
|
||
jr nc, sob0_check_bd ; reg > 0xB8
|
||
bit 5, e ; KeyOn bit in val
|
||
jr z, sob0_check_bd ; not keyed on
|
||
|
||
; bar = (val >> 1) & 0x0F
|
||
ld a, e
|
||
srl a
|
||
and a, #0x0F
|
||
; spec_mask |= (1 << bar)
|
||
call _set_spec_bit
|
||
; Fall through to check BD
|
||
|
||
sob0_check_bd:
|
||
ld a, d
|
||
cp a, #0xBD
|
||
ret nz ; not reg 0xBD, done
|
||
|
||
; ── 0xBD percussion handling ──
|
||
bit 5, e ; rhythm mode ON?
|
||
jr z, sob0_save_bd ; no → just save shadow
|
||
|
||
; rising = val & ~old & 0x1F
|
||
ld a, (_spec_opl_bd)
|
||
cpl ; ~old
|
||
and a, e ; val & ~old
|
||
and a, #0x1F ; mask lower 5 bits
|
||
jr z, sob0_save_bd ; no rising edges
|
||
|
||
; Check each percussion bit
|
||
ld b, a ; B = rising edges mask
|
||
bit 4, b ; BD?
|
||
jr z, sob0_no_bd
|
||
ld a, #2 ; BD → bar 2
|
||
call _set_spec_bit
|
||
sob0_no_bd:
|
||
bit 3, b ; SD?
|
||
jr z, sob0_no_sd
|
||
ld a, #8 ; SD → bar 8
|
||
call _set_spec_bit
|
||
sob0_no_sd:
|
||
bit 2, b ; TOM?
|
||
jr z, sob0_no_tom
|
||
ld a, #5 ; TOM → bar 5
|
||
call _set_spec_bit
|
||
sob0_no_tom:
|
||
bit 1, b ; CY?
|
||
jr z, sob0_no_cy
|
||
ld a, #12 ; CY → bar 12
|
||
call _set_spec_bit
|
||
sob0_no_cy:
|
||
bit 0, b ; HH?
|
||
jr z, sob0_save_bd
|
||
ld a, #14 ; HH → bar 14
|
||
call _set_spec_bit
|
||
|
||
sob0_save_bd:
|
||
ld a, e
|
||
ld (_spec_opl_bd), a
|
||
ret
|
||
|
||
;======================================================================
|
||
; spectrum_opl_b1(reg, val) — A=reg, L=val
|
||
;
|
||
; OPL Bank1: B0-B6 key-on only. Same logic, smaller range.
|
||
;======================================================================
|
||
_spectrum_opl_b1::
|
||
cp a, #0xB0
|
||
ret c ; reg < 0xB0
|
||
cp a, #0xB7
|
||
ret nc ; reg > 0xB6
|
||
bit 5, l ; KeyOn bit
|
||
ret z
|
||
; bar = (val >> 1) & 0x0F
|
||
ld a, l
|
||
srl a
|
||
and a, #0x0F
|
||
jp _set_spec_bit ; tail-call
|
||
|
||
;======================================================================
|
||
; spectrum_wave(reg, val) — A=reg, L=val
|
||
;
|
||
; YMF278B wave activity tracker. Key registers 0x68-0x7F maintain a
|
||
; 24-bit KeyOn mask. asm_emit_skip_spec folds channels 16-23 onto bars
|
||
; 0-7 and transports the current activity together with every wait.
|
||
;======================================================================
|
||
_spectrum_wave::
|
||
ld e, l ; E = key register value
|
||
cp #0x68
|
||
ret c
|
||
cp #0x80
|
||
ret nc
|
||
sub #0x68 ; A = channel 0..23
|
||
ld d, a
|
||
and #0x07
|
||
ld hl, #sw_bit_mask
|
||
add a, l
|
||
ld l, a
|
||
jr nc, sw_mask_nc
|
||
inc h
|
||
sw_mask_nc:
|
||
ld c, (hl) ; C = bit within mask byte
|
||
|
||
ld a, d
|
||
rrca
|
||
rrca
|
||
rrca
|
||
and #0x03 ; byte index 0..2
|
||
ld hl, #_spec_wave_active
|
||
add a, l
|
||
ld l, a
|
||
jr nc, sw_ptr_nc
|
||
inc h
|
||
sw_ptr_nc:
|
||
bit 7, e
|
||
jr z, sw_key_off
|
||
ld a, (hl)
|
||
or c
|
||
ld (hl), a
|
||
ret
|
||
sw_key_off:
|
||
ld a, c
|
||
cpl
|
||
and (hl)
|
||
ld (hl), a
|
||
ret
|
||
|
||
sw_bit_mask:
|
||
.db 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80
|
||
|
||
;======================================================================
|
||
; _set_spec_bit — set bit A (0..15) in spec_mask
|
||
; Input: A = bit number (0..15)
|
||
; Trashes: A, D, E, HL
|
||
; Preserves: B, C (no push/pop needed)
|
||
; Reuses the 8-byte wave-channel bit table; bit3 selects low/high byte.
|
||
;======================================================================
|
||
_set_spec_bit:
|
||
ld d, a ; preserve bar number
|
||
and #0x07
|
||
ld hl, #sw_bit_mask
|
||
add a, l
|
||
ld l, a
|
||
jr nc, ssb_ptr_ok
|
||
inc h
|
||
ssb_ptr_ok:
|
||
ld e, (hl) ; E = 8-bit mask
|
||
bit 3, d
|
||
jr z, ssb_low_byte
|
||
ld d, e ; bars 8..15: mask in high byte
|
||
ld e, #0
|
||
jr ssb_merge
|
||
ssb_low_byte:
|
||
ld d, #0 ; bars 0..7: mask in low byte
|
||
ssb_merge:
|
||
ld hl, (_spec_mask)
|
||
ld a, l
|
||
or e
|
||
ld l, a
|
||
ld a, h
|
||
or d
|
||
ld h, a
|
||
ld (_spec_mask), hl
|
||
ret
|
||
|
||
;======================================================================
|
||
; spectrum_ay(reg, val) — A=reg, L=val (chip1, base=0)
|
||
; spectrum_ay2(reg, val) — A=reg, L=val (chip2, base=3)
|
||
;
|
||
; AY: shadow tone period (regs 0-5), trigger bar on volume (regs 8-10)
|
||
; YM2203 FM: shadow block (0xA4-0xA6), key-on (0x28)
|
||
;======================================================================
|
||
_spectrum_ay2::
|
||
ld d, a ; D = reg
|
||
ld e, l ; E = val
|
||
ld a, #3
|
||
jr say_base_set
|
||
|
||
_spectrum_ay::
|
||
ld d, a ; D = reg
|
||
ld e, l ; E = val
|
||
xor a, a ; base = 0
|
||
say_base_set:
|
||
ld (say_base), a ; save base
|
||
|
||
; ── Check reg <= 5 (tone period shadow) ──
|
||
ld a, d ; A = reg
|
||
cp a, #6
|
||
jr nc, say_check_vol
|
||
|
||
; ch = base + (reg >> 1)
|
||
srl a ; A = reg >> 1
|
||
ld hl, #say_base
|
||
add a, (hl) ; A = base + (reg>>1) = ch index
|
||
; Offset into spec_ay_period[ch] (uint16_t array)
|
||
add a, a ; A = ch * 2
|
||
ld hl, #_spec_ay_period
|
||
add a, l
|
||
ld l, a
|
||
jr nc, say_nc1
|
||
inc h
|
||
say_nc1:
|
||
; HL → spec_ay_period[ch]
|
||
bit 0, d ; reg & 1 ?
|
||
jr nz, say_hi_nibble
|
||
|
||
; Low byte: period = (period & 0x0F00) | val
|
||
ld a, e ; A = val
|
||
ld (hl), a ; write low byte
|
||
ret
|
||
|
||
say_hi_nibble:
|
||
; High nibble: period = (period & 0x00FF) | ((val & 0x0F) << 8)
|
||
inc hl ; point to high byte
|
||
ld a, e
|
||
and a, #0x0F
|
||
ld (hl), a
|
||
ret
|
||
|
||
; ── Check reg 8-10 (volume → trigger bar) ──
|
||
say_check_vol:
|
||
ld a, d
|
||
cp a, #8
|
||
jr c, say_check_fm_block
|
||
cp a, #11
|
||
jr nc, say_check_fm_block
|
||
; Check val & 0x0F != 0
|
||
ld a, e
|
||
and a, #0x0F
|
||
ret z ; volume=0, skip
|
||
|
||
; ch_index = base + (reg - 8)
|
||
ld a, d
|
||
sub a, #8
|
||
ld hl, #say_base
|
||
add a, (hl) ; A = array index
|
||
; Load spec_ay_period[index]
|
||
add a, a ; * 2
|
||
ld hl, #_spec_ay_period
|
||
add a, l
|
||
ld l, a
|
||
jr nc, say_nc2
|
||
inc h
|
||
say_nc2:
|
||
ld a, (hl)
|
||
inc hl
|
||
ld h, (hl)
|
||
ld l, a ; HL = period
|
||
; Fall through to spec_period_to_bar + set_spec_bit
|
||
call _spec_period_to_bar ; A = bar index
|
||
jp _set_spec_bit ; tail-call
|
||
|
||
; ── Check FM block shadow (regs 0xA4-0xA6) ──
|
||
say_check_fm_block:
|
||
ld a, d
|
||
cp a, #0xA4
|
||
jr c, say_check_keyon
|
||
cp a, #0xA7
|
||
jr nc, say_check_keyon
|
||
; spec_fm_block[base + (reg - 0xA4)] = (val >> 3) & 0x07
|
||
sub a, #0xA4
|
||
ld hl, #say_base
|
||
add a, (hl) ; A = index
|
||
ld hl, #_spec_fm_block
|
||
add a, l
|
||
ld l, a
|
||
jr nc, say_nc3
|
||
inc h
|
||
say_nc3:
|
||
ld a, e
|
||
srl a
|
||
srl a
|
||
srl a
|
||
and a, #0x07
|
||
ld (hl), a
|
||
ret
|
||
|
||
; ── Check FM key-on (reg 0x28) ──
|
||
say_check_keyon:
|
||
ld a, d
|
||
cp a, #0x28
|
||
ret nz
|
||
; Check operators keyed on (val & 0xF0)
|
||
ld a, e
|
||
and a, #0xF0
|
||
ret z
|
||
; ch = val & 0x03
|
||
ld a, e
|
||
and a, #0x03
|
||
cp a, #3
|
||
ret nc ; ch > 2 invalid
|
||
; bar = spec_fm_block[base + ch] << 1
|
||
ld hl, #say_base
|
||
add a, (hl) ; A = index
|
||
ld hl, #_spec_fm_block
|
||
add a, l
|
||
ld l, a
|
||
jr nc, say_nc4
|
||
inc h
|
||
say_nc4:
|
||
ld a, (hl) ; A = block (0..7)
|
||
add a, a ; A = block * 2 = bar
|
||
jp _set_spec_bit ; tail-call
|
||
|
||
;======================================================================
|
||
; spec_period_to_bar(period) — HL=period, returns A=bar (0..15)
|
||
;
|
||
; Cascaded compare on high byte first, then low byte.
|
||
; Optimised: single compare chain, no function calls.
|
||
;======================================================================
|
||
_spec_period_to_bar::
|
||
ld a, h ; A = period >> 8
|
||
cp a, #12
|
||
jr c, sptb_lt12
|
||
xor a, a ; return 0
|
||
ret
|
||
sptb_lt12:
|
||
cp a, #8
|
||
jr c, sptb_lt8
|
||
ld a, #1
|
||
ret
|
||
sptb_lt8:
|
||
cp a, #6
|
||
jr c, sptb_lt6
|
||
ld a, #2
|
||
ret
|
||
sptb_lt6:
|
||
cp a, #4
|
||
jr c, sptb_lt4
|
||
ld a, #3
|
||
ret
|
||
sptb_lt4:
|
||
cp a, #3
|
||
jr c, sptb_lt3
|
||
ld a, #4
|
||
ret
|
||
sptb_lt3:
|
||
cp a, #2
|
||
jr c, sptb_lt2
|
||
ld a, #5
|
||
ret
|
||
sptb_lt2:
|
||
cp a, #1
|
||
jr c, sptb_lt1
|
||
; h == 1: check low byte
|
||
ld a, l
|
||
cp a, #128
|
||
ld a, #6
|
||
ret nc ; period >= 384 (1*256+128)
|
||
ld a, #7
|
||
ret ; period >= 256
|
||
|
||
sptb_lt1:
|
||
; h == 0: use low byte
|
||
ld a, l
|
||
cp a, #192
|
||
jr c, sptb_p1
|
||
ld a, #8
|
||
ret
|
||
sptb_p1:
|
||
cp a, #128
|
||
jr c, sptb_p2
|
||
ld a, #9
|
||
ret
|
||
sptb_p2:
|
||
cp a, #96
|
||
jr c, sptb_p3
|
||
ld a, #10
|
||
ret
|
||
sptb_p3:
|
||
cp a, #64
|
||
jr c, sptb_p4
|
||
ld a, #11
|
||
ret
|
||
sptb_p4:
|
||
cp a, #48
|
||
jr c, sptb_p5
|
||
ld a, #12
|
||
ret
|
||
sptb_p5:
|
||
cp a, #32
|
||
jr c, sptb_p6
|
||
ld a, #13
|
||
ret
|
||
sptb_p6:
|
||
cp a, #16
|
||
jr c, sptb_p7
|
||
ld a, #14
|
||
ret
|
||
sptb_p7:
|
||
ld a, #15
|
||
ret
|
||
|
||
;======================================================================
|
||
; spectrum_saa(reg, val) — A=reg, L=val
|
||
;
|
||
; SAA1099 chip 0: shadow octave (regs 0x10-0x12), trigger on amplitude (0-5).
|
||
; Bar mapping: if dual → bar=octave (0-7); if single → bar=octave*2 (0-14).
|
||
;======================================================================
|
||
_spectrum_saa::
|
||
ld d, a ; D = reg
|
||
ld e, l ; E = val
|
||
|
||
; ── Check octave shadow (regs 0x10-0x12) ──
|
||
cp a, #0x10
|
||
jr c, ssaa_check_amp
|
||
cp a, #0x13
|
||
jr nc, ssaa_check_amp
|
||
|
||
; ch = (reg - 0x10) * 2
|
||
sub a, #0x10
|
||
add a, a ; A = (reg-0x10)*2 = ch_base
|
||
ld hl, #_spec_saa_oct
|
||
add a, l
|
||
ld l, a
|
||
jr nc, ssaa_nc1
|
||
inc h
|
||
ssaa_nc1:
|
||
; spec_saa_oct[ch] = val & 0x07
|
||
ld a, e
|
||
and a, #0x07
|
||
ld (hl), a
|
||
; spec_saa_oct[ch+1] = (val >> 4) & 0x07
|
||
inc hl
|
||
ld a, e
|
||
rrca
|
||
rrca
|
||
rrca
|
||
rrca
|
||
and a, #0x07
|
||
ld (hl), a
|
||
ret
|
||
|
||
; ── Check amplitude (regs 0x00-0x05) ──
|
||
ssaa_check_amp:
|
||
ld a, d
|
||
cp a, #0x06
|
||
ret nc ; reg >= 6, not amplitude
|
||
; Check val != 0
|
||
ld a, e
|
||
or a, a
|
||
ret z
|
||
; bar = spec_saa_oct[reg]
|
||
ld a, d ; A = reg (0..5)
|
||
ld hl, #_spec_saa_oct
|
||
add a, l
|
||
ld l, a
|
||
jr nc, ssaa_nc2
|
||
inc h
|
||
ssaa_nc2:
|
||
ld a, (hl) ; A = octave (0..7)
|
||
; if dual: bar = octave; if single: bar = octave * 2
|
||
ld hl, #_spec_saa_dual
|
||
bit 0, (hl)
|
||
jr nz, ssaa_bar_done
|
||
add a, a ; A = octave * 2
|
||
ssaa_bar_done:
|
||
jp _set_spec_bit ; tail-call
|
||
|
||
;======================================================================
|
||
; spectrum_saa2(reg, val) — A=reg, L=val
|
||
;
|
||
; SAA1099 chip 1: shadow octave to spec_saa_oct2[],
|
||
; trigger on amplitude with bar = octave + 8 (bars 8-15).
|
||
;======================================================================
|
||
_spectrum_saa2::
|
||
ld d, a ; D = reg
|
||
ld e, l ; E = val
|
||
|
||
; ── Check octave shadow (regs 0x10-0x12) ──
|
||
cp a, #0x10
|
||
jr c, ssaa2_check_amp
|
||
cp a, #0x13
|
||
jr nc, ssaa2_check_amp
|
||
|
||
; ch = (reg - 0x10) * 2
|
||
sub a, #0x10
|
||
add a, a
|
||
ld hl, #_spec_saa_oct2
|
||
add a, l
|
||
ld l, a
|
||
jr nc, ssaa2_nc1
|
||
inc h
|
||
ssaa2_nc1:
|
||
ld a, e
|
||
and a, #0x07
|
||
ld (hl), a
|
||
inc hl
|
||
ld a, e
|
||
rrca
|
||
rrca
|
||
rrca
|
||
rrca
|
||
and a, #0x07
|
||
ld (hl), a
|
||
ret
|
||
|
||
; ── Check amplitude (regs 0x00-0x05) ──
|
||
ssaa2_check_amp:
|
||
ld a, d
|
||
cp a, #0x06
|
||
ret nc
|
||
ld a, e
|
||
or a, a
|
||
ret z
|
||
; bar = spec_saa_oct2[reg] + 8
|
||
ld a, d
|
||
ld hl, #_spec_saa_oct2
|
||
add a, l
|
||
ld l, a
|
||
jr nc, ssaa2_nc2
|
||
inc h
|
||
ssaa2_nc2:
|
||
ld a, (hl) ; A = octave (0..7)
|
||
add a, #8 ; bars 8-15 for chip 2
|
||
jp _set_spec_bit ; tail-call
|
||
|
||
;======================================================================
|
||
; spectrum_font_init() — install custom glyphs for chars 0x01-0x02
|
||
;
|
||
; Maps font page into page3 window, saves original 16 bytes (2 glyphs),
|
||
; writes striped-bar patterns from glyph_half / glyph_full.
|
||
; Must be called once before playback. Pairs with spectrum_font_restore.
|
||
;======================================================================
|
||
_spectrum_font_init::
|
||
ld bc, #PORT_PAGE3
|
||
in a, (c)
|
||
push af
|
||
push bc
|
||
ld a, #WC_PAGE_FONT0
|
||
out (c), a
|
||
ld hl, #(FONT_BASE + 8)
|
||
ld de, #sf_font_save
|
||
ld bc, #16
|
||
ldir
|
||
ld hl, #sf_glyph_half
|
||
ld de, #(FONT_BASE + 8)
|
||
ld bc, #16
|
||
ldir
|
||
pop bc
|
||
pop af
|
||
out (c), a
|
||
ret
|
||
|
||
;======================================================================
|
||
; spectrum_font_restore() — restore original glyphs for chars 0x01-0x02
|
||
;
|
||
; Must be called on exit to avoid corrupting the system font.
|
||
;======================================================================
|
||
_spectrum_font_restore::
|
||
ld bc, #PORT_PAGE3
|
||
in a, (c)
|
||
push af
|
||
push bc
|
||
ld a, #WC_PAGE_FONT0
|
||
out (c), a
|
||
ld hl, #sf_font_save
|
||
ld de, #(FONT_BASE + 8)
|
||
ld bc, #16
|
||
ldir
|
||
pop bc
|
||
pop af
|
||
out (c), a
|
||
ret
|
||
|
||
sf_glyph_half:
|
||
.db 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00
|
||
sf_glyph_full:
|
||
.db 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00
|
||
sf_font_save:
|
||
.ds 16
|