Add OPL4 playback and analyzer support

This commit is contained in:
2026-09-15 14:59:29 +03:00
commit 1686838e03
553 changed files with 50524 additions and 0 deletions
+953
View File
@@ -0,0 +1,953 @@
;==============================================================================
; 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