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
+584
View File
@@ -0,0 +1,584 @@
; ============================================================================
; cmdblocks.asm — VGM-format command blocks for chip init/silence
;
; Compiled with sjasmplus → raw binary 16KB, appended as plugin page 5.
;
; Each block contains standard VGM opcodes identical to those in .vgm files:
; 0x5E rr vv — OPL3 bank 0 write
; 0x5F rr vv — OPL3 bank 1 write
; 0x55 rr vv — YM2203 write (also used for MultiSound control regs 0xF0+)
; 0xA0 rr vv — AY8910 write (bit7 of rr = chip 2)
; 0xBD rr vv — SAA1099 write (bit7 of rr = chip 2)
; 0x66 — end of block
;
; The standard vgm_fill_buffer() parses these blocks in cmdblk_mode,
; converting VGM opcodes to ISR commands exactly as it does for .vgm data.
; This eliminates the need for a separate command copier — budget counting,
; buffer overflow protection and command parsing all come for free.
;
; NOTE on SAA clock: MultiSound intercepts AY reg select 0xF0-0xFF on
; port 0xFFFD for hardware control. VGM opcode 0xA0 strips bit7 of
; the register byte for chip selection, so 0xA0 CANNOT send regs >= 0x80
; to chip 1. We use YM2203 opcode 0x55 instead — it sends the register
; byte raw to 0xFFFD, which MultiSound intercepts correctly.
;
; Layout:
; 0xC000 : pointer table (uint16 × N) — absolute addresses of blocks
; 0xC000+N*2.. : block data (variable-length VGM commands, 3 bytes each)
;
; Usage from C:
; wc_mngc_pl(CMDBLK_PAGE);
; vgm_read_ptr = ((uint16_t*)0xC000)[blk_idx];
; vgm_cmdblk_mode = 1; vgm_song_ended = 0;
; vgm_fill_buffer(buf_idx);
; vgm_cmdblk_mode = 0;
;
; Assembled with: sjasmplus asm\cmdblocks.asm
; → SAVEBIN "build/cmdblocks.bin", #C000, #4000
; ============================================================================
DEVICE NOSLOT64K
ORG #C000
; ── VGM opcodes ─────────────────────────────────────────────────────
VGM_OPL_B0 EQU #5E ; OPL3 bank 0: 5E rr vv
VGM_OPL_B1 EQU #5F ; OPL3 bank 1: 5F rr vv
VGM_YM2203 EQU #55 ; YM2203 SSG+FM chip 1: 55 rr vv
VGM_YM2203_2 EQU #A5 ; YM2203 SSG+FM chip 2: A5 rr vv
VGM_AY EQU #A0 ; AY8910: A0 rr vv (bit7 = chip 2)
VGM_SAA EQU #BD ; SAA1099: BD rr vv (bit7 = chip 2)
VGM_OPL4 EQU #D0 ; YMF278B (OPL4): D0 pp rr vv (pp = port 0/1/2)
VGM_END EQU #66 ; End of data
; ── Macros (each emits standard VGM bytes, 3 bytes per command) ─────
MACRO opl3_bank0 addr, val
db VGM_OPL_B0, addr, val
ENDM
MACRO opl3_bank1 addr, val
db VGM_OPL_B1, addr, val
ENDM
MACRO ym2203_write reg, val
db VGM_YM2203, reg, val
ENDM
MACRO ym2203_2_write reg, val
db VGM_YM2203_2, reg, val
ENDM
MACRO ay_write reg, val
db VGM_AY, reg, val
ENDM
MACRO ay2_write reg, val
db VGM_AY, reg | #80, val
ENDM
MACRO saa_write reg, val
db VGM_SAA, reg, val
ENDM
MACRO saa2_write reg, val
db VGM_SAA, reg | #80, val
ENDM
MACRO opl4_wave_write reg, val
db VGM_OPL4, #02, reg, val
ENDM
MACRO blk_end
db VGM_END
ENDM
; ═══════════════════════════════════════════════════════════════════════
; Pointer table (indices = CMDBLK_xxx from vgm.h)
; ═══════════════════════════════════════════════════════════════════════
ptr_table:
dw blk_init_opl3 ; 0 = CMDBLK_INIT_OPL3
dw blk_init_opl2 ; 1 = CMDBLK_INIT_OPL2
dw blk_silence_opl ; 2 = CMDBLK_SILENCE_OPL
dw blk_silence_ay ; 3 = CMDBLK_SILENCE_AY
dw blk_silence_ay2 ; 4 = CMDBLK_SILENCE_AY2
dw blk_silence_saa ; 5 = CMDBLK_SILENCE_SAA
dw blk_silence_saa2 ; 6 = CMDBLK_SILENCE_SAA2
dw blk_saa_clock_on ; 7 = CMDBLK_SAA_CLK_ON
dw blk_saa_clock_off ; 8 = CMDBLK_SAA_CLK_OFF
dw blk_silence_ym2203 ; 9 = CMDBLK_SILENCE_YM2203
dw blk_silence_ym2203_2 ; 10 = CMDBLK_SILENCE_YM2203_2
dw blk_saa_init ; 11 = CMDBLK_SAA_INIT
dw blk_saa2_init ; 12 = CMDBLK_SAA2_INIT
dw blk_init_opl4 ; 13 = CMDBLK_INIT_OPL4
dw blk_silence_wave ; 14 = CMDBLK_SILENCE_WAVE
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_INIT_OPL3 — OPL3 init (NEW=1, clean state)
;
; OPL3 NEW=1 enables full OPL3 register set. 4-op connections are
; cleared (reg 0x104=0) to avoid leftover pairing from a previous
; track. Test/WSE register 0x01 is zeroed — OPL3 does not use WSE;
; waveform select is implicit via E0-F5 registers.
;
; VGM stream: 5F 05 01 5F 04 00 5E 01 00 66
; ═══════════════════════════════════════════════════════════════════════
blk_init_opl3:
opl3_bank1 #05, #01 ; VGM: 5F 05 01 — OPL3 NEW=1
opl3_bank1 #04, #00 ; VGM: 5F 04 00 — 4-op OFF (clean)
opl3_bank0 #01, #00 ; VGM: 5E 01 00 — Test/WSE=0
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_INIT_OPL4 — YMF278B (OPL4, ZXM-MoonSound) init: NEW=1 + NEW2=1
;
; Как CMDBLK_INIT_OPL3 (NEW=1, L/R через C0-C8), но дополнительно взводит
; бит NEW2 (бит 1 регистра Bank1 #05). На реальном чипе YMF278B этот бит
; обязателен — без него чип игнорирует любые записи в wave-часть (порты
; #7E/#7F), т.е. VGM-команды 0xD0 с портом 2 останутся немы.
;
; VGM stream: 5F 05 03 5F 04 00 5E 01 00 66
; ═══════════════════════════════════════════════════════════════════════
blk_init_opl4:
opl3_bank1 #05, #03 ; VGM: 5F 05 03 — NEW=1 + NEW2=1 (разблокировать wave)
opl3_bank1 #04, #00 ; VGM: 5F 04 00 — 4-op OFF (clean)
opl3_bank0 #01, #00 ; VGM: 5E 01 00 — Test/WSE=0
opl4_wave_write #02, #10 ; WT headers 384-511 from 0x200000,
; memory access OFF (sound generation mode)
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_INIT_OPL2 — OPL2 compat init (NEW=0, clean state)
;
; Forces OPL2 compatibility mode. Critically: 0x104=0 clears any
; residual 4-op state that would persist after an OPL3 track (the
; most common cause of OPL2 artifacts). Test/WSE register = 0.
;
; VGM stream: 5F 05 00 5F 04 00 5E 01 00 66
; ═══════════════════════════════════════════════════════════════════════
blk_init_opl2:
opl3_bank1 #05, #00 ; VGM: 5F 05 00 — OPL3 NEW=0 (OPL2)
opl3_bank1 #04, #00 ; VGM: 5F 04 00 — 4-op OFF (clean)
opl3_bank0 #01, #00 ; VGM: 5E 01 00 — Test/WSE=0
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SILENCE_OPL — Full OPL3 silence (both banks)
;
; VGM stream (55 commands):
; 5E B0..B8 00 × 9 Key Off Bank 0 (ch 0-8)
; 5F B0..B8 00 × 9 Key Off Bank 1 (ch 0-8)
; 5E 4x 3F ×18 TL max Bank 0 (18 operators)
; 5F 4x 3F ×18 TL max Bank 1 (18 operators)
; 5E BD 00 Percussion Off
; 66 End
; ═══════════════════════════════════════════════════════════════════════
blk_silence_opl:
; ── Key Off: Bank 0 — VGM: 5E B0..B8 00 ──
opl3_bank0 #B0, #00
opl3_bank0 #B1, #00
opl3_bank0 #B2, #00
opl3_bank0 #B3, #00
opl3_bank0 #B4, #00
opl3_bank0 #B5, #00
opl3_bank0 #B6, #00
opl3_bank0 #B7, #00
opl3_bank0 #B8, #00
; ── Key Off: Bank 1 — VGM: 5F B0..B8 00 ──
opl3_bank1 #B0, #00
opl3_bank1 #B1, #00
opl3_bank1 #B2, #00
opl3_bank1 #B3, #00
opl3_bank1 #B4, #00
opl3_bank1 #B5, #00
opl3_bank1 #B6, #00
opl3_bank1 #B7, #00
opl3_bank1 #B8, #00
; ── TL max: Bank 0 — VGM: 5E {40-45,48-4D,50-55} 3F ──
opl3_bank0 #40, #3F
opl3_bank0 #41, #3F
opl3_bank0 #42, #3F
opl3_bank0 #43, #3F
opl3_bank0 #44, #3F
opl3_bank0 #45, #3F
opl3_bank0 #48, #3F
opl3_bank0 #49, #3F
opl3_bank0 #4A, #3F
opl3_bank0 #4B, #3F
opl3_bank0 #4C, #3F
opl3_bank0 #4D, #3F
opl3_bank0 #50, #3F
opl3_bank0 #51, #3F
opl3_bank0 #52, #3F
opl3_bank0 #53, #3F
opl3_bank0 #54, #3F
opl3_bank0 #55, #3F
; ── TL max: Bank 1 — VGM: 5F {40-45,48-4D,50-55} 3F ──
opl3_bank1 #40, #3F
opl3_bank1 #41, #3F
opl3_bank1 #42, #3F
opl3_bank1 #43, #3F
opl3_bank1 #44, #3F
opl3_bank1 #45, #3F
opl3_bank1 #48, #3F
opl3_bank1 #49, #3F
opl3_bank1 #4A, #3F
opl3_bank1 #4B, #3F
opl3_bank1 #4C, #3F
opl3_bank1 #4D, #3F
opl3_bank1 #50, #3F
opl3_bank1 #51, #3F
opl3_bank1 #52, #3F
opl3_bank1 #53, #3F
opl3_bank1 #54, #3F
opl3_bank1 #55, #3F
; ── Waveform reset: Bank 0 — VGM: 5E {E0-E5,E8-ED,F0-F5} 00 ──
; Clears non-sine waveforms that linger between tracks.
; Critical for clean OPL2 playback after OPL3 usage.
opl3_bank0 #E0, #00
opl3_bank0 #E1, #00
opl3_bank0 #E2, #00
opl3_bank0 #E3, #00
opl3_bank0 #E4, #00
opl3_bank0 #E5, #00
opl3_bank0 #E8, #00
opl3_bank0 #E9, #00
opl3_bank0 #EA, #00
opl3_bank0 #EB, #00
opl3_bank0 #EC, #00
opl3_bank0 #ED, #00
opl3_bank0 #F0, #00
opl3_bank0 #F1, #00
opl3_bank0 #F2, #00
opl3_bank0 #F3, #00
opl3_bank0 #F4, #00
opl3_bank0 #F5, #00
; ── Waveform reset: Bank 1 — VGM: 5F {E0-E5,E8-ED,F0-F5} 00 ──
opl3_bank1 #E0, #00
opl3_bank1 #E1, #00
opl3_bank1 #E2, #00
opl3_bank1 #E3, #00
opl3_bank1 #E4, #00
opl3_bank1 #E5, #00
opl3_bank1 #E8, #00
opl3_bank1 #E9, #00
opl3_bank1 #EA, #00
opl3_bank1 #EB, #00
opl3_bank1 #EC, #00
opl3_bank1 #ED, #00
opl3_bank1 #F0, #00
opl3_bank1 #F1, #00
opl3_bank1 #F2, #00
opl3_bank1 #F3, #00
opl3_bank1 #F4, #00
opl3_bank1 #F5, #00
; ── Percussion Off — VGM: 5E BD 00 ──
opl3_bank0 #BD, #00
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SILENCE_WAVE — YMF278B (OPL4) wave-часть silence, 24 канала
;
; CMDBLK_SILENCE_OPL глушит только FM-банки (0x5E/0x5F) — wave-каналы
; (табличный синтез, до 24 голосов) им не затрагиваются и продолжают
; звучать после выхода из плеера. Для каждого канала N (0-23):
; рег. (0x68+N), data=0x40 — Key Off + Damp (бит7:6=01, форсированный
; быстрый спад огибающей, EG_DMP)
; рег. (0x50+N), data=0xFF — TL=0x7F (макс. затухание) + LD=1
; (мгновенно, без интерполяции громкости)
; Оба варианта дублируют друг друга (как KeyOff+TL max у FM-банков) —
; гарантия тишины независимо от состояния огибающей канала.
;
; VGM stream: D0 02 68 40 ... D0 02 7F 40 (Key Off+Damp, 24 канала)
; D0 02 50 FF ... D0 02 67 FF (TL max, 24 канала)
; 66
; ═══════════════════════════════════════════════════════════════════════
blk_silence_wave:
opl4_wave_write #68, #40
opl4_wave_write #69, #40
opl4_wave_write #6A, #40
opl4_wave_write #6B, #40
opl4_wave_write #6C, #40
opl4_wave_write #6D, #40
opl4_wave_write #6E, #40
opl4_wave_write #6F, #40
opl4_wave_write #70, #40
opl4_wave_write #71, #40
opl4_wave_write #72, #40
opl4_wave_write #73, #40
opl4_wave_write #74, #40
opl4_wave_write #75, #40
opl4_wave_write #76, #40
opl4_wave_write #77, #40
opl4_wave_write #78, #40
opl4_wave_write #79, #40
opl4_wave_write #7A, #40
opl4_wave_write #7B, #40
opl4_wave_write #7C, #40
opl4_wave_write #7D, #40
opl4_wave_write #7E, #40
opl4_wave_write #7F, #40
opl4_wave_write #50, #FF
opl4_wave_write #51, #FF
opl4_wave_write #52, #FF
opl4_wave_write #53, #FF
opl4_wave_write #54, #FF
opl4_wave_write #55, #FF
opl4_wave_write #56, #FF
opl4_wave_write #57, #FF
opl4_wave_write #58, #FF
opl4_wave_write #59, #FF
opl4_wave_write #5A, #FF
opl4_wave_write #5B, #FF
opl4_wave_write #5C, #FF
opl4_wave_write #5D, #FF
opl4_wave_write #5E, #FF
opl4_wave_write #5F, #FF
opl4_wave_write #60, #FF
opl4_wave_write #61, #FF
opl4_wave_write #62, #FF
opl4_wave_write #63, #FF
opl4_wave_write #64, #FF
opl4_wave_write #65, #FF
opl4_wave_write #66, #FF
opl4_wave_write #67, #FF
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SILENCE_AY — Silence AY8910 chip 1
;
; VGM stream:
; A0 07 3F Mixer: all tone+noise off
; A0 08 00 Volume A = 0
; A0 09 00 Volume B = 0
; A0 0A 00 Volume C = 0
; 66 End
; ═══════════════════════════════════════════════════════════════════════
blk_silence_ay:
ay_write 7, #3F ; VGM: A0 07 3F
ay_write 8, #00 ; VGM: A0 08 00
ay_write 9, #00 ; VGM: A0 09 00
ay_write 10, #00 ; VGM: A0 0A 00
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SILENCE_AY2 — Silence AY8910 chip 2 (TurboSound)
;
; VGM stream (bit7 = chip 2):
; A0 87 3F Mixer off
; A0 88 00 Volume A = 0
; A0 89 00 Volume B = 0
; A0 8A 00 Volume C = 0
; 66 End
; ═══════════════════════════════════════════════════════════════════════
blk_silence_ay2:
ay2_write 7, #3F ; VGM: A0 87 3F
ay2_write 8, #00 ; VGM: A0 88 00
ay2_write 9, #00 ; VGM: A0 89 00
ay2_write 10, #00 ; VGM: A0 8A 00
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SILENCE_SAA — Silence SAA1099 chip 1
;
; VGM stream:
; BD 1C 02 Reset all (sound gen + internal state)
; BD 1C 00 Sound enable off
; BD 00..05 00 Amplitude ch 0-5 = 0
; 66 End
; ═══════════════════════════════════════════════════════════════════════
blk_silence_saa:
saa_write #1C, #02 ; VGM: BD 1C 02 — Reset all
saa_write #1C, #00 ; VGM: BD 1C 00 — Sound enable off
saa_write #00, #00 ; VGM: BD 00 00 — Amp ch0 = 0
saa_write #01, #00 ; VGM: BD 01 00 — Amp ch1 = 0
saa_write #02, #00 ; VGM: BD 02 00 — Amp ch2 = 0
saa_write #03, #00 ; VGM: BD 03 00 — Amp ch3 = 0
saa_write #04, #00 ; VGM: BD 04 00 — Amp ch4 = 0
saa_write #05, #00 ; VGM: BD 05 00 — Amp ch5 = 0
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SILENCE_SAA2 — Silence SAA1099 chip 2
;
; VGM stream (bit7 = chip 2):
; BD 9C 02 Reset all
; BD 9C 00 Sound enable off
; BD 80..85 00 Amplitude ch 0-5 = 0
; 66 End
; ═══════════════════════════════════════════════════════════════════════
blk_silence_saa2:
saa2_write #1C, #02 ; VGM: BD 9C 02 — Reset all
saa2_write #1C, #00 ; VGM: BD 9C 00 — Sound enable off
saa2_write #00, #00 ; VGM: BD 80 00 — Amp ch0 = 0
saa2_write #01, #00 ; VGM: BD 81 00 — Amp ch1 = 0
saa2_write #02, #00 ; VGM: BD 82 00 — Amp ch2 = 0
saa2_write #03, #00 ; VGM: BD 83 00 — Amp ch3 = 0
saa2_write #04, #00 ; VGM: BD 84 00 — Amp ch4 = 0
saa2_write #05, #00 ; VGM: BD 85 00 — Amp ch5 = 0
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SAA_CLK_ON — Enable SAA1099 + FM clock (MultiSound)
;
; Uses YM2203 opcode (0x55) so that register 0xF3 is sent raw to 0xFFFD.
; AY opcode 0xA0 would strip bit7 for chip selection → can't reach 0xF3.
; MultiSound intercepts reg select 0xF0-0xFF for hardware control.
; bit0=1(YM2) bit1=1(normal) bit2=0(FM ON) bit3=0(SAA ON)
;
; VGM stream: 55 F3 00 66
; ═══════════════════════════════════════════════════════════════════════
blk_saa_clock_on:
ym2203_write #F3, #00 ; VGM: 55 F3 00 — SAA+FM clock ON
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SAA_CLK_OFF — Disable SAA1099 + FM clock (MultiSound)
;
; VGM stream: 55 FF 00 66
; bit2=1(FM OFF) bit3=1(SAA OFF)
; ═══════════════════════════════════════════════════════════════════════
blk_saa_clock_off:
ym2203_write #FF, #00 ; VGM: 55 FF 00 — SAA+FM clock OFF
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SILENCE_YM2203 — Silence YM2203 (SSG + FM) + reset prescaler
;
; Prescaler reset (reg 0x2D, write-only, value ignored):
; 55 2D 00 Reset prescaler → defaults: /6 FM, /4 SSG
; Prevents state leak from tracks that set /3+/2 via 0x2E.
; SSG (AY-compatible):
; 55 07 3F Mixer: all tone+noise off
; 55 08 00 SSG Volume A = 0
; 55 09 00 SSG Volume B = 0
; 55 0A 00 SSG Volume C = 0
; FM — Key Off all 3 channels:
; 55 28 00 Key Off ch 0
; 55 28 01 Key Off ch 1
; 55 28 02 Key Off ch 2
; FM — TL max (0x7F) all 12 operators:
; 55 40..42 7F slot 1 (M1): ch 0,1,2
; 55 44..46 7F slot 3 (C1): ch 0,1,2
; 55 48..4A 7F slot 2 (M2): ch 0,1,2
; 55 4C..4E 7F slot 4 (C2): ch 0,1,2
; Total: 20 VGM commands + end = 61 bytes
; ═══════════════════════════════════════════════════════════════════════
blk_silence_ym2203:
; ── SSG silence — VGM: 55 rr vv ──
ym2203_write 7, #3F ; VGM: 55 07 3F — Mixer off
ym2203_write 8, #00 ; VGM: 55 08 00 — SSG Vol A = 0
ym2203_write 9, #00 ; VGM: 55 09 00 — SSG Vol B = 0
ym2203_write 10, #00 ; VGM: 55 0A 00 — SSG Vol C = 0
; ── FM Key Off — VGM: 55 28 xx ──
ym2203_write #28, #00 ; VGM: 55 28 00 — Key Off ch 0
ym2203_write #28, #01 ; VGM: 55 28 01 — Key Off ch 1
ym2203_write #28, #02 ; VGM: 55 28 02 — Key Off ch 2
; ── FM TL max — VGM: 55 4x 7F ──
ym2203_write #40, #7F ; VGM: 55 40 7F — TL slot1 ch0
ym2203_write #41, #7F ; VGM: 55 41 7F — TL slot1 ch1
ym2203_write #42, #7F ; VGM: 55 42 7F — TL slot1 ch2
ym2203_write #44, #7F ; VGM: 55 44 7F — TL slot3 ch0
ym2203_write #45, #7F ; VGM: 55 45 7F — TL slot3 ch1
ym2203_write #46, #7F ; VGM: 55 46 7F — TL slot3 ch2
ym2203_write #48, #7F ; VGM: 55 48 7F — TL slot2 ch0
ym2203_write #49, #7F ; VGM: 55 49 7F — TL slot2 ch1
ym2203_write #4A, #7F ; VGM: 55 4A 7F — TL slot2 ch2
ym2203_write #4C, #7F ; VGM: 55 4C 7F — TL slot4 ch0
ym2203_write #4D, #7F ; VGM: 55 4D 7F — TL slot4 ch1
ym2203_write #4E, #7F ; VGM: 55 4E 7F — TL slot4 ch2
; ── Prescaler reset — VGM: 55 2D 00 ──
ym2203_write #2F, #01 ; VGM: 55 2D 00
ym2203_write #2D, #01 ; VGM: A5 2E 00
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SILENCE_YM2203_2 — Silence YM2203 chip 2 (TurboSound)
;
; Uses VGM opcode 0xA5 (dual chip: 0x55 → 0xA5) → vgm_fill_buffer
; generates CMD_WRITE_AY2 which selects chip 2 via isr_ms_ctrl.
;
; Prescaler reset:
; A5 2D 00 Reset prescaler → defaults: /6 FM, /4 SSG
; SSG (chip 2):
; A5 07 3F Mixer: all tone+noise off
; A5 08 00 SSG Volume A = 0
; A5 09 00 SSG Volume B = 0
; A5 0A 00 SSG Volume C = 0
; FM — Key Off all 3 channels (chip 2):
; A5 28 00 Key Off ch 0
; A5 28 01 Key Off ch 1
; A5 28 02 Key Off ch 2
; FM — TL max (0x7F) all 12 operators (chip 2):
; A5 40..42 7F slot 1 (M1): ch 0,1,2
; A5 44..46 7F slot 3 (C1): ch 0,1,2
; A5 48..4A 7F slot 2 (M2): ch 0,1,2
; A5 4C..4E 7F slot 4 (C2): ch 0,1,2
; Total: 20 VGM commands + end = 61 bytes
; ═══════════════════════════════════════════════════════════════════════
blk_silence_ym2203_2:
; ── SSG silence (chip 2) — VGM: A5 rr vv ──
ym2203_2_write 7, #3F ; VGM: A5 07 3F — Mixer off
ym2203_2_write 8, #00 ; VGM: A5 08 00 — SSG Vol A = 0
ym2203_2_write 9, #00 ; VGM: A5 09 00 — SSG Vol B = 0
ym2203_2_write 10, #00 ; VGM: A5 0A 00 — SSG Vol C = 0
; ── FM Key Off (chip 2) — VGM: A5 28 xx ──
ym2203_2_write #28, #00 ; VGM: A5 28 00 — Key Off ch 0
ym2203_2_write #28, #01 ; VGM: A5 28 01 — Key Off ch 1
ym2203_2_write #28, #02 ; VGM: A5 28 02 — Key Off ch 2
; ── FM TL max (chip 2) — VGM: A5 4x 7F ──
ym2203_2_write #40, #7F ; VGM: A5 40 7F — TL slot1 ch0
ym2203_2_write #41, #7F ; VGM: A5 41 7F — TL slot1 ch1
ym2203_2_write #42, #7F ; VGM: A5 42 7F — TL slot1 ch2
ym2203_2_write #44, #7F ; VGM: A5 44 7F — TL slot3 ch0
ym2203_2_write #45, #7F ; VGM: A5 45 7F — TL slot3 ch1
ym2203_2_write #46, #7F ; VGM: A5 46 7F — TL slot3 ch2
ym2203_2_write #48, #7F ; VGM: A5 48 7F — TL slot2 ch0
ym2203_2_write #49, #7F ; VGM: A5 49 7F — TL slot2 ch1
ym2203_2_write #4A, #7F ; VGM: A5 4A 7F — TL slot2 ch2
ym2203_2_write #4C, #7F ; VGM: A5 4C 7F — TL slot4 ch0
ym2203_2_write #4D, #7F ; VGM: A5 4D 7F — TL slot4 ch1
ym2203_2_write #4E, #7F ; VGM: A5 4E 7F — TL slot4 ch2
; ── Prescaler reset (chip 2) — VGM: A5 2D 00 ──
; ── Prescaler reset — VGM: 55 2D 00 ──
ym2203_2_write #2F, #01 ; VGM: 55 2D 00
ym2203_2_write #2D, #01 ; VGM: A5 2E 00
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SAA_INIT — SAA1099 chip 1 Sound Enable ON
;
; Writes reg 0x1C = 0x01 to chip 0 (Sound Enable ON).
; Parsed via cmdblk → fb_is_vgm==0 → bypasses saa_mode filter.
;
; VGM stream: BD 1C 01 66
; ═══════════════════════════════════════════════════════════════════════
blk_saa_init:
saa_write #1C, #01 ; VGM: BD 1C 01 — Sound Enable ON (chip 0)
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SAA2_INIT — SAA1099 chip 2 Sound Enable ON (+ clock enable)
;
; Writes reg 0x1C|0x80 = 0x9C to chip 1 (bit7 selects chip 1).
; In HDL: address write 0x9C → saa_reg_is_1c=1, then data 0x01 with
; d[0]=1 triggers saa2_clk_en (one-way clock enable for chip 2).
; Parsed via cmdblk → fb_is_vgm==0 → bypasses saa_mode filter.
;
; VGM stream: BD 9C 01 66
; ═══════════════════════════════════════════════════════════════════════
blk_saa2_init:
saa2_write #1C, #01 ; VGM: BD 9C 01 — Sound Enable ON (chip 1 + clk)
blk_end
; ═══════════════════════════════════════════════════════════════════════
; Padding to 16 KB
; ═══════════════════════════════════════════════════════════════════════
IF $ < #10000
ds #10000 - $, #00
ENDIF
SAVEBIN "build/cmdblocks.bin", #C000, #4000
+160
View File
@@ -0,0 +1,160 @@
;==============================================================================
; @file crt0.s
; @brief Стартовый код WC-плагина (SDCC Z80, --no-std-crt0)
;
; Этот файл заменяет стандартный crt0 SDCC. Он должен линковаться
; ПЕРВЫМ в цепочке .rel-файлов, чтобы _plugin_entry оказался
; по адресу --code-loc (обычно #8000).
;
; Входные параметры WC (при CALL plugin_base):
; A' = индекс расширения файла
; BC = указатель на имя файла (z-строка)
; HL = размер файла (младшее слово)
; DE = размер файла (старшее слово)
; IX = указатель на структуру активной панели WC
;
; Выход: A = код возврата (0=ESC, 2=следующий, 4=предыдущий)
;
; Адресное пространство:
; #8000-#BFFF = код + данные плагина
; #C000-#FFFF = VPL окно (данные VGM через WC API)
;
; ВАЖНО: gsinit обнуляет _DATA и копирует _INITIALIZER → _INITIALIZED.
; Без этого при повторном запуске (NEXT/PREV) стат. переменные
; содержат мусор от предыдущего запуска.
;==============================================================================
.module crt0
.globl _main
.globl _wc_exit_code
.globl _wc_file_idx
.globl _wc_file_count
; Линкер-символы для runtime-инициализации C
.globl s__DATA
.globl l__DATA
.globl s__INITIALIZED
.globl s__INITIALIZER
.globl l__INITIALIZER
;------------------------------------------------------------------------------
; Entry point (WC вызывает по "CALL plugin_base")
; Вход:
; A' = индекс расширения файла
; BC = указатель на имя файла (z-строка)
; HL = размер файла (младшее слово)
; DE = размер файла (старшее слово)
; IX = указатель на структуру активной панели (нужен WC API)
;------------------------------------------------------------------------------
.area _CODE
_plugin_entry::
;--------------------------------------------------------------
; Сохранить параметры WC на стеке: gsinit обнулит _DATA,
; поэтому нельзя записывать в глобалы до инициализации.
;--------------------------------------------------------------
push hl ; size_lo
push de ; size_hi
push bc ; file_name ptr
ex af, af'
push af ; file_ext (в A)
ex af, af'
;--- Сохранить HL'/DE' (номер объекта / кол-во файлов) ---
exx
push hl ; file_idx (HL')
push de ; file_count (DE')
exx
;--------------------------------------------------------------
; Инициализировать C runtime (обнулить _DATA,
; скопировать начальные значения в _INITIALIZED)
;--------------------------------------------------------------
call gsinit
;--------------------------------------------------------------
; Восстановить параметры WC из стека в (теперь чистые) глобалы
;--------------------------------------------------------------
pop de ; file_count (DE')
ld (_wc_file_count), de
pop hl ; file_idx (HL')
ld (_wc_file_idx), hl
pop af
ld (_wc_file_ext), a
pop bc
ld (_wc_file_name), bc
pop de ; size_hi
pop hl ; size_lo
ld (_wc_file_size+0), hl
ld (_wc_file_size+2), de
; Вызвать main()
call _main
; main() вернула void, код выхода — в _wc_exit_code
ld a, (_wc_exit_code)
; IX не менялся — просто возврат
ret
;------------------------------------------------------------------------------
; Глобальные переменные параметров WC (читаются из main.c)
;------------------------------------------------------------------------------
.area _DATA
_wc_file_ext:: .db 0
_wc_file_name:: .dw 0
_wc_file_size:: .dw 0, 0 ; 32-bit LE
_wc_exit_code:: .db 0
_wc_file_idx:: .dw 0 ; HL': номер объекта в панели (от 1)
_wc_file_count::.dw 0 ; DE': кол-во объектов в панели (вкл. "..")
;------------------------------------------------------------------------------
; Инициализация C runtime (SDCC: _GSINIT выполняется при старте)
;
; 1) Обнулить _DATA — все глобальные/статические переменные без
; явного инициализатора (аналог BSS).
; 2) Скопировать _INITIALIZER → _INITIALIZED — переменные с явным
; начальным значением (static uint8_t x = 5; и т.п.).
;------------------------------------------------------------------------------
.area _GSINIT
gsinit::
; ── 1. Обнулить _DATA ────────────────────────────────────────
ld bc, #l__DATA
ld a, b
or a, c
jr z, 00001$ ; l__DATA == 0 → нечего обнулять
ld hl, #s__DATA
ld (hl), #0 ; первый байт = 0
dec bc
ld a, b
or a, c
jr z, 00001$ ; l__DATA == 1 → уже готово
ld d, h
ld e, l
inc de ; DE = s__DATA + 1
ldir ; заполнить остаток нулями
00001$:
; ── 2. Скопировать _INITIALIZER → _INITIALIZED ──────────────
ld bc, #l__INITIALIZER
ld a, b
or a, c
jr z, 00002$ ; нет инициализированных данных
ld hl, #s__INITIALIZER ; источник (в ROM / code page 1)
ld de, #s__INITIALIZED ; приёмник (в RAM / _DATA area)
ldir
00002$:
.area _GSFINAL
gsfinal::
ret
File diff suppressed because it is too large Load Diff
+330
View File
@@ -0,0 +1,330 @@
;==============================================================================
; inflate_call.s — SDCC обёртка (sdcccall(1)) для вызова inflate из C
;
; uint8_t inflate_vgz(uint8_t src_page, uint8_t dst_page);
; void copy_pages_to_tap(uint8_t n_pages);
;
; sdcccall(1): A = первый аргумент
; Возврат inflate_vgz: A = 0 (успех) / 1 (ошибка)
;
; Inflate binary живёт в экстра-странице WMF. Обёртка маппит её в Win 3
; (ORG 0xC000) и делает JP 0xC000. Decode buffer — в Win 2 (0x8000),
; настраивается самим inflate. При выходе inflate восстанавливает Win 2.
;
; ╔══════════════════════════════════════════════════════════════════════╗
; ║ Почему код inflate обязан быть в Win 3, а не Win 2: ║
; ║ DEFLATE backref вычисляет HL = write_ptr − distance. ║
; ║ При decode buffer в Win 2 (0x8000) → HL ∈ [0x0000..0xBFFF], ║
; ║ никогда не попадает в Win 3 (0xC000+), где живёт код. ║
; ║ Если поменять местами (код Win 2, decode Win 3) → HL может ║
; ║ попасть в 0x8000–0xBFFF, что потребует ремапа Win 2 во время ║
; ║ исполнения из неё — невозможно. ║
; ╚══════════════════════════════════════════════════════════════════════╝
;
; copy_pages_to_tap:
; Копирует N страниц из мегабуфера (#20+) в TAP-страницы (#A1+).
; DI на время копирования, восстанавливает Win 0/Win 3 из sys vars.
;
; inflate_vgz:
; 1. Сохранить аргументы, IX, SP, Win 1
; 2. DI, маппить Win 3 = inflate page через порт
; 3. SP = 0xFFF0, push return addr → JP 0xC000
; 4. По возврату: SP, Win 1 восстанавливаем, EI, IX → ret
; Win 0 / Win 3 — восстанавливает вызывающий C-код через WC API
;==============================================================================
.module inflate_call
.globl _inflate_vgz
.globl _copy_pages_to_tap
; Progress bar params (filled by C before inflate_vgz)
.globl _ifl_pb_text_pg
.globl _ifl_pb_attr_ofs
.globl _ifl_pb_total
.globl _ifl_pb_bw
.globl _ifl_pb_green
;--- Константы ---------------------------------------------------------------
INFLATE_PAGE_IDX = 4 ; индекс inflate в плагине (1 code + 3 LUT)
; физ. страница = base_page + INFLATE_PAGE_IDX
; base_page читается из WC sys var (0x6002)
PORT_W0 = 0x10AF ; TSConfig Win 0 page port
PORT_W1 = 0x11AF ; TSConfig Win 1 page port
PORT_W3 = 0x13AF ; TSConfig Win 3 page port
;--- DATA (0xB800+) ----------------------------------------------------------
.area _DATA
_ifl_src_pg: .ds 1 ; src_page (физ. стр. TAP)
_ifl_dst_pg: .ds 1 ; dst_page (физ. стр. мегабуфера)
_ifl_sv_pg1: .ds 1 ; saved Win 1 physical page
_ifl_sv_pg2: .ds 1 ; saved Win 2 physical page → inflate D reg
_ifl_sv_sp: .ds 2 ; saved SP
_ifl_inflate_pg: .ds 1 ; computed inflate phys page (debug/scan)
; Progress bar parameters (set by C, copied to inflate page at 0xC1F0)
_ifl_pb_text_pg: .ds 1 ; text screen physical page
_ifl_pb_attr_ofs: .ds 2 ; attr base offset within text page (lo, hi)
_ifl_pb_total: .ds 1 ; estimated total output pages (0=disabled)
_ifl_pb_bw: .ds 1 ; bar width in columns
_ifl_pb_green: .ds 1 ; green attr value
;--- CODE (0x8000+) ----------------------------------------------------------
.area _CODE
_inflate_vgz::
push ix ; callee-saved (SDCC)
;--- 1. сохранить аргументы ---
ld (_ifl_src_pg), a ; A = src_page
ld a, l
ld (_ifl_dst_pg), a ; L = dst_page
;--- 2. DI ---
di
;--- 3. сохранить Win 1, Win 2 из WC sys vars (@ 0x6001/0x6002) ---
ld a, (0x6001)
ld (_ifl_sv_pg1), a
ld a, (0x6002)
ld (_ifl_sv_pg2), a
;--- 4. сохранить SP ---
ld (_ifl_sv_sp), sp
.ifdef DBG_ENABLE
;--- DBG: 0x01 = saved context, about to map inflate page ---
ld bc, #0xFFAF
ld a, #0x01
out (c), a
.endif
;--- 5. маппить Win 3 = inflate page (dynamic: base + IDX) ---
ld a, (0x6002) ; Win 2 phys page = plugin base
.ifdef DBG_ENABLE
ld bc, #0xFEAF
out (c), a ; DBG: FEAF = base page from 0x6002
.endif
add a, #INFLATE_PAGE_IDX ; + 5 = inflate page
ld (_ifl_inflate_pg), a ; save computed inflate page
.ifdef DBG_ENABLE
ld bc, #0xFDAF
out (c), a ; DBG: FDAF = computed inflate page
.endif
ld bc, #PORT_W3
out (c), a
.ifdef DBG_ENABLE
;--- DBG: 0x02 = Win3 mapped to inflate page ---
ld bc, #0xFFAF
ld a, #0x02
out (c), a
ld a, (0xC000)
ld bc, #0xFEAF
out (c), a ; byte[0] — expect 0xF5 (push af)
ld a, (0xC001)
ld bc, #0xFDAF
out (c), a ; byte[1] — expect 0xC5 (push bc)
;=== DBG: scan pages base+3..base+7 looking for inflate ===
ld a, (0x6002)
add a, #3
ld d, a
ld e, #0
_ifl_scan_loop:
ld a, d
ld bc, #PORT_W3
out (c), a
ld a, #0xD0
add a, e
ld bc, #0xFFAF
out (c), a
ld a, d
ld bc, #0xFDAF
out (c), a
ld a, (0xC000)
ld bc, #0xFEAF
out (c), a
inc d
inc e
ld a, e
cp #5
jr nz, _ifl_scan_loop
;--- DBG: 0x04 = scan done, re-map Win3 to inflate page ---
ld bc, #0xFFAF
ld a, #0x04
out (c), a
ld a, (_ifl_inflate_pg)
ld bc, #PORT_W3
out (c), a
.endif
;--- 6. стек в Win 3, push return addr ---
ld sp, #0xFFF0
ld hl, #_ifl_return
push hl
.ifdef DBG_ENABLE
;--- DBG: 0x05 = SP set, return addr pushed, loading regs ---
ld bc, #0xFFAF
ld a, #0x05
out (c), a
ld a, (_ifl_src_pg)
ld bc, #0xFEAF
out (c), a
ld a, (_ifl_dst_pg)
ld bc, #0xFDAF
out (c), a
.endif
;--- 6b. Copy progress bar params to inflate page (0xC1F0+) ---
; Win 3 = inflate page (mapped in step 5)
; Win 2 = _CODE/_DATA (still accessible for reading _ifl_pb_*)
ld a, (_ifl_pb_text_pg)
ld (0xC1F0), a
ld hl, (_ifl_pb_attr_ofs)
ld (0xC1F1), hl
ld a, (_ifl_pb_total)
ld (0xC1F3), a
ld a, (_ifl_pb_bw)
ld (0xC1F4), a
ld a, (_ifl_pb_green)
ld (0xC1F5), a
; Initialize runtime state
xor a
ld (0xC1F6), a ; pb_error = 0
ld (0xC1F7), a ; pb_col = 0
;--- 7. входные регистры inflate: A=src, E=dst, D=saved_pg2 ---
ld a, (_ifl_sv_pg2)
ld d, a ; D = saved Win 2 page
ld a, (_ifl_dst_pg)
ld e, a ; E = dst_page
ld a, (_ifl_src_pg) ; A = src_page
.ifdef DBG_ENABLE
;--- DBG: 0x06 = about to JP 0xC000 ---
push af
push bc
ld bc, #0xFFAF
ld a, #0x06
out (c), a
pop bc
pop af
.endif
;--- 8. запуск inflate ---
; JP 0xC200 = inflate_init (safe zone, survives backref LDIR)
; НЕ JP 0xC000! Entry zone 0xC000–0xC1FF перезаписывается
; LDIR overflow при распаковке backref'ов.
jp 0xC200
;--- сюда вернёмся по RET из inflate ---
; A = 0 (ok) / 1 (error)
; Win 2 уже восстановлен inflate → _DATA и код доступны
_ifl_return:
ld e, a ; результат → E
.ifdef DBG_ENABLE
;--- DBG: 0x03 = returned from inflate, A=result ---
ld bc, #0xFFAF
ld a, #0x03
out (c), a
ld a, e
ld bc, #0xFEAF
out (c), a
.endif
;--- восстановить SP (→ C-стек, сразу после push ix) ---
ld sp, (_ifl_sv_sp)
;--- восстановить Win 1 (WC API точка входа @ 0x6006) ---
ld a, (_ifl_sv_pg1)
ld bc, #PORT_W1
out (c), a
;--- вернуть результат ---
ld a, e
pop ix ; восстановить IX
ei ; EI + задержка 1 instr → ret атомарен
ret
;==============================================================================
; void copy_pages_to_tap(uint8_t n_pages)
;
; Копирует n_pages страниц из мегабуфера (#20..#20+n-1) в TAP (#A1..#A1+n-1).
; Использует прямые port-записи в Win 0 / Win 3.
; DI на время работы; при выходе: Win 0, Win 3 восстановлены, EI.
;
; sdcccall(1): A = n_pages
;==============================================================================
MEGABUF_START = 0x20 ; первая физ. страница мегабуфера
TAP_START = 0xA1 ; первая физ. страница TAP
_copy_pages_to_tap::
or a
ret z ; n=0 → ничего делать
push ix
ld c, a ; C = n_pages (сохраним)
di
;--- сохранить текущие страницы Win 0 / Win 3 ---
ld a, (0x6000)
ld (_cpt_sv_pg0), a
ld a, (0x6003)
ld (_cpt_sv_pg3), a
;--- подготовить счётчики ---
ld d, #MEGABUF_START ; D = src phys page
ld e, #TAP_START ; E = dst phys page
ld b, c ; B = loop counter
_cpt_loop:
push bc
push de
;--- Map Win 0 = megabuf page D ---
ld a, d
ld bc, #PORT_W0
out (c), a
;--- Map Win 3 = TAP page E ---
ld a, e
ld b, #0x13 ; C ещё 0xAF от предыдущего BC
out (c), a
;--- LDIR 16 КБ: Win 0 → Win 3 ---
ld hl, #0x0000
ld de, #0xC000
ld bc, #0x4000
ldir
pop de
inc d ; следующая megabuf page
inc e ; следующая TAP page
pop bc
djnz _cpt_loop
;--- восстановить Win 0 ---
ld a, (_cpt_sv_pg0)
ld bc, #PORT_W0
out (c), a
;--- восстановить Win 3 ---
ld a, (_cpt_sv_pg3)
ld b, #0x13
out (c), a
ei
pop ix
ret
;--- static vars for copy (в CODE — не в DATA, чтобы не зависеть от Win 2) ---
_cpt_sv_pg0: .ds 1
_cpt_sv_pg3: .ds 1
+663
View File
@@ -0,0 +1,663 @@
;==============================================================================
; @file isr.s
; Формат записи: [VSINTH, VSINTL, HSINT, next_lo, next_hi]
; Читается тремя OUTI: BC=#24AF -> VSINTH, #23AF -> VSINTL, #22AF -> HSINT,
; затем читается next_ptr.
;
; Командные буферы: cmd_buf_a, cmd_buf_b (по 512 байт, 128 команд).
;
; Переменные ISR (volatile, доступны из C через isr.h):
; _isr_active_buf : 0=A, 1=B (пишет ISR, читает main)
; _isr_enabled : 0/1 (пишет main, читает ISR)
; _isr_play_seconds : uint16 (счётчик секунд, пишет ISR)
; _isr_read_ptr : uint16 (указатель чтения в активном буфере)
;
; Расход памяти _DATA:
; _im2_table_buf : 512 байт (гарантирует 256-aligned)
; _cmd_buf_a/b : 2 x 512 = 1024 байт
; Переменные : 14 байт
; Итого : ~1550 байт
;==============================================================================
.module isr_mod
.globl _isr_ms_ctrl
.globl _isr_active_buf
.globl _isr_enabled
.globl _isr_tick_ctr
.globl _isr_border_color
.globl _isr_play_seconds
.globl _isr_read_ptr
.globl _isr_done
.globl _cmd_buf_a
.globl _cmd_buf_b
.globl _isr_init
.globl _isr_deinit
.globl _call_wc_handler
.globl _spectrum_levels
.globl _vgm_chip_type
; Импорт таблицы позиций из pos_table.s
.globl pos_table
; Порты TSConfig
PORT_HSINT = 0x22AF
PORT_VSINTL = 0x23AF
PORT_VSINTH = 0x24AF
PORT_INTMASK = 0x2AAF
PORT_SYSCONF = 0x20AF ; System Config (turbo bits [1:0])
TURBO_7MHZ = 0x01 ; SysConf value: 7 MHz
TURBO_14MHZ = 0x02 ; SysConf value: 14 MHz
; Адрес вектора FRAME INT в IM2-таблице WC (I=#5B, vector=0xFF → #5BFF)
WC_IM2_VEC = 0x5BFF
; Коды команд (8 типов, шаг 0x20 + ISR_DONE)
CMD_WRITE_AY = 0x00
CMD_INC_SEC = 0x10
CMD_WRITE_AY2 = 0x20
CMD_CALL_WC = 0x30
CMD_WRITE_B0 = 0x40
CMD_SKIP_TICKS= 0x50
CMD_WRITE_SAA = 0x60
CMD_WRITE_B1 = 0x80
CMD_WRITE_WAVE= 0x70
CMD_WAIT = 0xC0
CMD_END_BUF = 0xE0
CMD_ISR_DONE = 0xF0
; Отладка: раскрасить бордюр во время ISR (0=выкл, 1=вкл)
DEBUG_BORDER = 0
; Порты OPL3
OPL3_ADDR0 = 0xC4
OPL3_DATA0 = 0xC5
OPL3_ADDR1 = 0xC6
OPL3_DATA1 = 0xC7
; Порты OPL4 Wave-часть (ZXM-MoonSound: MOON_WREG/MOON_WDAT в moon_driver.asm)
OPL4_WREG = 0x7E
OPL4_WDAT = 0x7F
; Порты AY-3-8910 / YM2149
AY_ADDR = 0xFD ; C-byte, B=0xFF → #FFFD
AY_DATA_B = 0xBF ; B-byte, C=#FD → #BFFD
; Порты SAA1099 (MultiSound card, dual-chip via bit7 of address)
; addr=#01FF data=#00FF
; bit7 of reg byte written to #01FF selects chip (0=chip0, 1=chip1)
; chip1 clk on: automatic on first write to chip1 (address with bit7=1)
; both clk off: OUT #FFFD, d[3]=1
ISR_FREQ = 683 ; informational only (actual timing from pos_table)
;==============================================================================
; isr_init() — Перехват вектора IM2 WC и настройка 683 Hz
;==============================================================================
.area _CODE
;==============================================================================
; Вместо собственной IM2-таблицы (512 байт) используем существующую
; таблицу WC. WC работает в IM2 с I=#5B; вектор FRAME (data bus=0xFF)
; читается из адреса #5BFF (2 байта: lo, hi).
;
; isr_init:
; 1. Сохранить текущий вектор (#5BFF) → _isr_saved_vec
; 2. Записать адрес _isr_handler в #5BFF
; 3. Настроить INTMASK, позицию INT, указатели
; 4. IM 2 (для надёжности)
; Вызывающий код делает EI.
;
; isr_deinit:
; 1. Восстановить сохранённый вектор WC
; 2. Сбросить позицию INT на line=0 (1 INT/кадр)
; Вызывающий код делает EI.
;==============================================================================
_isr_init::
di
;--- Сохранить оригинальный вектор WC ---
ld hl, (WC_IM2_VEC)
ld (_isr_saved_vec), hl
;--- Установить наш обработчик ---
ld hl, #_isr_handler
ld (WC_IM2_VEC), hl
;--- Разрешить только FRAME INT ---
ld bc, #PORT_INTMASK
ld a, #0x01
out (c), a
;--- Инициализировать указатель позиции ---
ld hl, #pos_table
ld (_pos_ptr), hl
;--- Инициализировать счётчик ожидания = 0 ---
ld hl, #0
ld (_isr_wait_ctr), hl
;--- Инициализировать read_ptr → cmd_buf_a ---
ld hl, #_cmd_buf_a
ld (_isr_read_ptr), hl
;--- Счётчик тиков ---
xor a
ld (_isr_tick_ctr), a
;--- Счётчик секунд ---
ld hl, #0
ld (_isr_play_seconds), hl
;--- Reset analyzer previous mask + levels (adjacent) ---
xor a
ld hl, #_spectrum_prev_mask
ld (hl), a
ld de, #(_spectrum_prev_mask + 1)
ld bc, #17
ldir
;--- Начальная позиция: строка 0, hpos 0 ---
ld bc, #PORT_HSINT
out (c), a
ld b, #>PORT_VSINTL
out (c), a
ld b, #>PORT_VSINTH
out (c), a
;--- Подтвердить IM2 (WC и так в IM2, но для надёжности) ---
im 2
ret
;==============================================================================
; isr_deinit() — Восстановить вектор WC и сбросить INT-позиции
;==============================================================================
_isr_deinit::
di
;--- Проверить: вызывался ли isr_init? ---
ld hl, (_isr_saved_vec)
ld a, h
or l
ret z ; saved_vec == 0 → init не вызывался
;--- Восстановить оригинальный вектор WC ---
ld (WC_IM2_VEC), hl
;--- Сбросить saved_vec (защита от повторного вызова) ---
ld hl, #0
ld (_isr_saved_vec), hl
;--- Сбросить позицию INT: line=0, hpos=0 (один INT/кадр) ---
ld bc, #PORT_HSINT
xor a
out (c), a
ld b, #>PORT_VSINTL
out (c), a
ld b, #>PORT_VSINTH
out (c), a
;--- INTMASK = FRAME only (WC по умолчанию) ---
ld bc, #PORT_INTMASK
ld a, #0x01
out (c), a
; Не делаем EI — вызывающий код решает сам
ret
;==============================================================================
; ISR_HANDLER — обработчик прерывания FRAME (2734 Гц)
; ─── OUTI-блок и таблицу НЕ ТРОГАТЬ! ────────────────────────────────────
;==============================================================================
.area _CODE
_isr_handler::
push af
.if DEBUG_BORDER
ld a, #0x06 ; DEBUG: yellow border
out (0xFE), a
.endif
push hl
push bc
push de
;--- OUTI×3: записать следующую позицию INT из таблицы ---
; Формат записи: [vsinth_byte, vsintl_byte, hsint_byte, next_lo, next_hi]
; BC стартует 0x24AF: OUTI→0x24AF(VSINTH), B→0x23; OUTI→0x23AF(VSINTL), B→0x22; OUTI→0x22AF(HSINT)
ld hl, (_pos_ptr)
ld bc, #0x25AF ; B=#25, C=#AF
outi ; → VSINTH (#24AF) outi predecrement B: B=0x24, C=0xAF
outi ; → VSINTL (#23AF)
outi ; → HSINT (#22AF)
; Следующий ptr (2 байта)
ld a, (hl)
inc hl
ld h, (hl)
ld l, a ; HL = next_ptr
ld (_pos_ptr), hl
;--- Счётчик тиков: простой INC byte (21T, всегда) ---
ld hl, #_isr_tick_ctr
inc (hl) ; 11T: wrap at 256 — main loop считает секунды
;--- WAIT_CTR: если 0 — выполнять команды ---
ld a, (_isr_enabled)
or a
jp z, _isr_exit ; не активен — быстрый выход
ld hl, (_isr_wait_ctr)
ld a, h
or l
jp z, _isr_exec_cmd
dec hl
ld (_isr_wait_ctr), hl
_isr_exit:
.if DEBUG_BORDER
ld a, (_isr_border_color) ; DEBUG: restore border from variable
out (0xFE), a
.endif
pop de
pop bc
pop hl
pop af
ei
ret
;--- Выполнение команд --------------------------------------------------
_isr_exec_cmd:
ld hl, (_isr_read_ptr)
ex de, hl ; DE = указатель команд
_isr_cmd_loop:
ld a, (de)
inc de
; Dispatch: command types — hot path first
; OPL Bank 0 writes (most common register writes)
cp #CMD_WRITE_B0
jp z, _isr_write_b0
; ★ CMD_SKIP_TICKS — every ISR tick ends with this (promoted from #12)
cp #CMD_SKIP_TICKS
jp z, _isr_skip_ticks
; OPL Bank 1 writes (OPL3 second bank)
cp #CMD_WRITE_B1
jp z, _isr_write_b1
; Wait (long pauses, less common than SKIP_TICKS)
cp #CMD_WAIT
jp z, _isr_do_wait
; AY/SAA/end commands (less frequent per-interrupt)
cp #CMD_WRITE_AY
jp z, _isr_write_ay
cp #CMD_WRITE_AY2
jp z, _isr_write_ay2
cp #CMD_WRITE_SAA
jp z, _isr_write_saa
; OPL4 Wave-часть (ZXM-MoonSound), редко используется
cp #CMD_WRITE_WAVE
jp z, _isr_write_wave
; Rare commands
cp #CMD_END_BUF
jp z, _isr_do_end_buf
cp #CMD_ISR_DONE
jp z, _isr_cmd_done
cp #CMD_INC_SEC
jp z, _isr_inc_sec
cp #CMD_CALL_WC
jp z, _isr_call_wc
; Unknown command: skip 3 bytes, continue
inc de
inc de
inc de
jp _isr_cmd_loop
_isr_do_end_buf:
ld a, (_isr_active_buf)
xor #1
ld (_isr_active_buf), a
jr nz, _isr_end_use_b
ld hl, #_cmd_buf_a
jr _isr_end_save_ptr
_isr_end_use_b:
ld hl, #_cmd_buf_b
_isr_end_save_ptr:
ld (_isr_read_ptr), hl
ex de, hl
jp _isr_cmd_loop
_isr_write_b0:
ld bc, #PORT_SYSCONF
ld a, #TURBO_7MHZ
out (c), a ; → 7 MHz
ld a, (de) ; reg
ld c, #OPL3_ADDR0 ; B=#20, don't-care for C4-C7
out (c), a
inc de
ld a, (de) ; val
inc c ; C5 = data port
out (c), a
ld c, #0xAF ; BC=#20AF (B still #20)
ld a, #TURBO_14MHZ
out (c), a ; → 14 MHz
inc de
inc de ; pad
jp _isr_cmd_loop
_isr_write_b1:
ld bc, #PORT_SYSCONF
ld a, #TURBO_7MHZ
out (c), a ; → 7 MHz
ld a, (de)
ld c, #OPL3_ADDR1 ; B=#20, don't-care for C4-C7
out (c), a
inc de
ld a, (de)
inc c ; C7 = data port
out (c), a
ld c, #0xAF ; BC=#20AF (B still #20)
ld a, #TURBO_14MHZ
out (c), a ; → 14 MHz
inc de
inc de
jp _isr_cmd_loop
_isr_do_wait:
ld a, (de) ; lo
inc de
ld l, a
ld a, (de) ; hi
inc de
ld h, a
inc de ; skip pad byte
ld (_isr_wait_ctr), hl
ex de, hl
ld (_isr_read_ptr), hl
jp _isr_exit
;--- AY8910 chip 1 write: [reg, val, pad] --------------------------------
_isr_write_ay:
ld bc, #PORT_SYSCONF
ld a, #TURBO_7MHZ
out (c), a ; → 7 MHz
ld bc, #0xFFFD
ld a, (_isr_ms_ctrl) ; MultiSound ctrl (bit0=0 → chip1)
out (c), a
ld a, (de) ; reg
out (c), a ; select AY register
inc de
ld a, (de) ; val
ld b, #0xBF ; BC = #BFFD
out (c), a ; write data
ld bc, #PORT_SYSCONF
ld a, #TURBO_14MHZ
out (c), a ; → 14 MHz
inc de
inc de ; pad
jp _isr_cmd_loop
;--- AY8910 chip 2 write (TurboSound): [reg, val, pad] -------------------
_isr_write_ay2:
ld bc, #PORT_SYSCONF
ld a, #TURBO_7MHZ
out (c), a ; → 7 MHz
ld bc, #0xFFFD
ld a, (_isr_ms_ctrl) ; MultiSound ctrl base
or a, #0x01 ; bit0=1 → chip2
out (c), a
ld a, (de) ; reg
out (c), a ; select register
inc de
ld a, (de) ; val
ld b, #0xBF ; BC = #BFFD
out (c), a
ld bc, #PORT_SYSCONF
ld a, #TURBO_14MHZ
out (c), a ; → 14 MHz
inc de
inc de ; pad
jp _isr_cmd_loop
;--- SAA1099 write: [reg, val, pad] ----------------------------------------
; reg bit7 = chip select (0=chip0, 1=chip1), passed directly to #01FF.
; HDL uses bit7 for CS routing; SAA1099 ignores upper address bits.
_isr_write_saa:
ld bc, #PORT_SYSCONF
ld a, #TURBO_7MHZ
out (c), a ; → 7 MHz
ld a, (de) ; reg (bit7 = chip select)
ld bc, #0x01FF ; SAA address port
out (c), a ; write register address (bit7 → chip CS)
inc de
ld a, (de) ; val
ld b, #0x00 ; BC = #00FF SAA data port
out (c), a
ld bc, #PORT_SYSCONF
ld a, #TURBO_14MHZ
out (c), a ; → 14 MHz
inc de
inc de ; pad
jp _isr_cmd_loop
;--- OPL4 Wave-часть write (ZXM-MoonSound): [reg, val, pad] --------------
; Порты #7E (addr) / #7F (data) — как MOON_WREG/MOON_WDAT в оригинальном
; Z80-драйвере карты (moon_driver.asm). B don't-care (карта декодирует
; только младший байт адреса), как и для OPL3_ADDR0..OPL3_DATA1.
;
; ВАЖНО: в отличие от FM-банков (isr_write_b0/b1), референсный NedoOS-
; драйвер (common/moonsound.asm: opl4_wait) ждёт busy-флаг (бит0 порта
; #C4) перед КАЖДОЙ записью в порты #7E/#7F, включая обычные регистры
; wave-канала (не только memory-access режим) — без этого запись может
; быть потеряна чипом. См. chip_wait()/wave_out_reg()/wave_out_dat() в
; lib/vgm.c (та же логика для синхронной заливки сэмплов).
_isr_write_wave:
ld bc, #PORT_SYSCONF
ld a, #TURBO_7MHZ
out (c), a ; → 7 MHz
ld c, #OPL4_WREG ; B=#20, don't-care (как MOON_WREG)
wwave_wait1:
in a, (0xC4) ; busy-флаг (бит0)
rrca
jr c, wwave_wait1
ld a, (de) ; reg
out (c), a
inc de
inc c ; C=#7F = data port (MOON_WDAT)
wwave_wait2:
in a, (0xC4)
rrca
jr c, wwave_wait2
ld a, (de) ; val
out (c), a
ld bc, #PORT_SYSCONF
ld a, #TURBO_14MHZ
out (c), a ; → 14 MHz
inc de
inc de ; pad
jp _isr_cmd_loop
;--- CMD_INC_SEC: инкремент счётчика секунд [0,0,0] ---------------------
_isr_inc_sec:
inc de
inc de
inc de ; пропустить 3 байта параметров
ld hl, (_isr_play_seconds)
inc hl
ld (_isr_play_seconds), hl
jp _isr_cmd_loop
;--- CMD_SKIP_TICKS: skip N + spectrum bitmask [N,smask_lo,smask_hi] --------
; CMD_SKIP_TICKS(N):
; N=0 — не перепрограммирует, просто exit (1 тик пауза)
; N>=1 — пропустить N записей, перепрограммировать INT (N+1 тиков пауза)
; Max N=55 (кадр минус 1).
_isr_skip_ticks:
ld a, (de) ; N = skip count (0..55)
inc de
;--- Spectrum: KeyOn attack + sustained activity floor ---
push af ; save N
ld a, (de) ; mask_lo
inc de
ld l, a ; L = mask_lo
ld a, (de) ; mask_hi
inc de
ld h, a ; H = mask_hi
push de ; save read_ptr
; OPL4 gets edge/sustain processing. Other chips keep the old
; event-analyzer behaviour by comparing every mask with zero.
ld a, (_vgm_chip_type)
cp #4 ; VGM_CHIP_OPL4
jr z, _skip_spec_opl4
ld de, #0
jr _skip_spec_masks_ready
_skip_spec_opl4:
ld de, (_spectrum_prev_mask)
ld (_spectrum_prev_mask), hl
_skip_spec_masks_ready:
; E/D are previous low/high, L/H are current low/high.
ld c, e ; previous low byte
ld e, h ; preserve current high byte
ld a, l ; current low byte
ld hl, #_spectrum_levels
call _skip_spec_byte
ld c, d ; previous high byte
ld a, e ; current high byte
call _skip_spec_byte
pop de ; restore read_ptr
pop af ; restore N
; Сохранить read_ptr
ex de, hl
ld (_isr_read_ptr), hl
; N=0: просто exit (как CMD_WAIT(0) без накладных на wait_ctr)
or a
jp z, _isr_exit
; Пропустить N записей pos_table: перейти по N-1 ссылкам,
; затем OUTI×3 из N-ой записи (перепрограммировать INT)
ld hl, (_pos_ptr)
dec a ; N-1 линков пропустить
jr z, _skip_program ; N=1: сразу программируем текущую
ld b, a
_skip_loop:
inc hl ; пропустить VSINTH
inc hl ; VSINTL
inc hl ; HSINT
ld a, (hl) ; next_lo
inc hl
ld h, (hl) ; next_hi
ld l, a
djnz _skip_loop
_skip_program:
; HL = запись для перепрограммирования INT портов
ld bc, #0x25AF
outi ; VSINTH (#24AF)
outi ; VSINTL (#23AF)
outi ; HSINT (#22AF)
; Следующий next_ptr
ld a, (hl)
inc hl
ld h, (hl)
ld l, a
ld (_pos_ptr), hl
jp _isr_exit
; Unpack one activity byte into eight spectrum bars.
; A=current bits, C=previous bits, HL=first level; advances HL by 8.
; Rising edge: peak 8. Sustained KeyOn: decay is allowed down to floor 3.
_skip_spec_byte:
ld b, #8
_skip_spec_bit:
rr c ; carry = previous activity bit
jr c, _skip_spec_was_on
rra ; carry = current activity bit
jr nc, _skip_spec_next
ld (hl), #8 ; new KeyOn attack
jr _skip_spec_next
_skip_spec_was_on:
rra ; carry = current activity bit
jr nc, _skip_spec_next ; KeyOff: ordinary decay
push af ; preserve remaining current bits
ld a, (hl)
cp #3
jr nc, _skip_spec_floor_ok
ld (hl), #3 ; sustained activity floor
_skip_spec_floor_ok:
pop af
_skip_spec_next:
inc hl
djnz _skip_spec_bit
ret
;--- CMD_ISR_DONE: заморозить ISR, выставить флаг [0,0,0] ------------------
; ISR зацикливается на этой команде, не переключает буферы.
; Main loop ждёт isr_done == 1 перед завершением.
_isr_cmd_done:
inc de
inc de
inc de ; пропустить 3 байта параметров
; Выставить флаг готовности к завершению
ld a, #1
ld (_isr_done), a
; Перемотать read_ptr на начало CMD_ISR_DONE (DE - 4)
ex de, hl
dec hl
dec hl
dec hl
dec hl
ld (_isr_read_ptr), hl
jp _isr_exit
;--- CMD_CALL_WC: вызов оригинального WC handler [0,0,0] ------------------
; Внутренняя команда ISR (не используется buffer filler, зарезервировано)
_isr_call_wc:
inc de
inc de
inc de ; пропустить 3 байта параметров
jp _isr_cmd_loop ; просто пропустить
;==============================================================================
; call_wc_handler() — Вызов WC ISR из main loop (C-callable)
;
; WC ожидает периодический вызов своего ISR для обновления TMN,
; клавиатуры, часов и др. Вызываем из main loop ~50 Гц.
;
; Механизм: DI → JP (saved_vec). WC handler делает EI+RET
; обратно к вызывающему коду (tail-call через JP).
;==============================================================================
_call_wc_handler::
ld hl, (_isr_saved_vec)
ld a, h
or l
ret z ; saved_vec==0 → isr_init не вызывался
di
jp (hl) ; tail-call: WC handler делает EI+RET
;==============================================================================
.area _DATA
_isr_ms_ctrl:: .db 0xFE ; MultiSound ctrl: FM OFF + SAA OFF (idle)
_isr_active_buf:: .db 0
_isr_enabled:: .db 0
_isr_tick_ctr:: .db 0 ; инкрементируется каждые ISR тик (2734/сек, wrap 256)
_isr_border_color:: .db 0 ; цвет бордюра при выходе из ISR (пишет main, читает ISR)
_isr_play_seconds:: .dw 0 ; инкрементируется CMD_INC_SEC
_isr_read_ptr:: .dw 0
_isr_wait_ctr:: .dw 0
_isr_done:: .db 0 ; 1 = ISR замер на CMD_ISR_DONE, готов к завершению
_pos_ptr: .dw 0 ; инициализируется в isr_init()
_isr_saved_vec: .dw 0 ; сохранённый вектор WC (#5BFF)
;==============================================================================
; Spectrum analyzer levels (16 bars, updated by 16-bit bitmask in CMD_SKIP_TICKS)
;==============================================================================
_spectrum_prev_mask:
.ds 2 ; previous CMD_SKIP activity bits
_spectrum_levels::
.ds 16 ; 16 bytes, one per bar (0-8)
;==============================================================================
; Командные буферы (512 байт каждый)
;==============================================================================
.even
_cmd_buf_a::
.ds 512
_cmd_buf_b::
.ds 512
+335
View File
@@ -0,0 +1,335 @@
;==============================================================================
; @file opl3.s
; @brief OPL3/AY/SAA register operations for SDCC Z80 (sdcccall(1))
;
; Порты TSConfig для OPL3:
; #C4 = Bank 0 Address #C5 = Bank 0 Data
; #C6 = Bank 1 Address #C7 = Bank 1 Data
;==============================================================================
.module opl3
.area _CODE
;----------------------------------------------------------------------
; opl3_write_b0(uint8_t addr, uint8_t data) — Bank 0
;----------------------------------------------------------------------
.globl _opl3_write_b0
_opl3_write_b0::
ld c, #0xC4
jr _opl3_write
;----------------------------------------------------------------------
; opl3_write_b1(uint8_t addr, uint8_t data) — Bank 1
;----------------------------------------------------------------------
.globl _opl3_write_b1
_opl3_write_b1::
ld c, #0xC6
_opl3_write:
out (c), a ; addr (B don't-care for C4-C7)
ld hl, #2
add hl, sp
ld a, (hl)
inc c
out (c), a ; data
ret
;----------------------------------------------------------------------
; tsconf_out(uint16_t port, uint8_t val)
;----------------------------------------------------------------------
.globl _tsconf_out
_tsconf_out::
ld b, d
ld c, e
ld hl, #2
add hl, sp
ld a, (hl)
out (c), a
ret
;----------------------------------------------------------------------
; opl3_init() — OPL3 enable + wavesel + reset (falls through)
;----------------------------------------------------------------------
.globl _opl3_init
_opl3_init::
ld a, #0x05
ld c, #0xC6
out (c), a ; B don't-care for C4-C7
ld a, #0x01 ; NEW bit only (OPL3 enable)
inc c
out (c), a
ld a, #0x01
ld c, #0xC4
out (c), a
ld a, #0x20
inc c
out (c), a
; fall through to opl3_reset
;----------------------------------------------------------------------
; opl3_reset() — Full OPL3 silence: zero ALL significant registers
; Both banks: Key-Off (B0-B8=0), Total Level max atten (40-55=3F),
; Percussion off (BD=0), F-Num (A0-A8=0), Feedback (C0-C8=0),
; AM/Vib/Mult (20-35=0), AR/DR (60-75=0), SL/RR (80-95=0),
; Waveform (E0-F5=0).
; Uses _opl3_zero_range and _opl3_fill_range helpers.
;----------------------------------------------------------------------
.globl _opl3_reset
_opl3_reset::
; ── Bank 0 ──
ld c, #0xC4
call _opl3_reset_bank
; Percussion off: BD = 0 (Bank 0 only)
ld a, #0xBD
out (c), a
inc c
xor a, a
out (c), a
dec c
; ── Bank 1 ──
ld c, #0xC6
; fall through to _opl3_reset_bank
;----------------------------------------------------------------------
; _opl3_reset_bank — zero all significant regs of one OPL3 bank
; C = addr port (0xC4 or 0xC6)
; Destroys: A, B, D, E
;----------------------------------------------------------------------
_opl3_reset_bank:
; KeyOff: B0-B8 = 0
ld d, #0xB0
ld e, #9
call _opl3_zero_range
; Total Level max attenuation: 40-55 = 0x3F
ld d, #0x40
ld e, #22
ld a, #0x3F
call _opl3_fill_range
; F-Num lo: A0-A8 = 0
ld d, #0xA0
ld e, #9
call _opl3_zero_range
; Feedback/Output: C0-C8 = 0
ld d, #0xC0
ld e, #9
call _opl3_zero_range
; AM/Vib/Mult: 20-35 = 0
ld d, #0x20
ld e, #22
call _opl3_zero_range
; AR/DR: 60-75 = 0
ld d, #0x60
ld e, #22
call _opl3_zero_range
; SL/RR: 80-95 = 0
ld d, #0x80
ld e, #22
call _opl3_zero_range
; Waveform: E0-F5 = 0
ld d, #0xE0
ld e, #22
jr _opl3_zero_range ; tail-call → ret
;----------------------------------------------------------------------
; _opl3_zero_range — fill E consecutive regs starting at D with 0
; C = port, D = start reg, E = count
;----------------------------------------------------------------------
_opl3_zero_range:
xor a, a
; fall through
;----------------------------------------------------------------------
; _opl3_fill_range — fill E consecutive regs starting at D with value A
; C = port (0xC4 for Bank0, 0xC6 for Bank1)
; D = start register, E = count, A = value to write
; Destroys: A, B, D, E
;----------------------------------------------------------------------
_opl3_fill_range:
ld b, a ; B = value to write
_opl3_fr_lp:
ld a, d
out (c), a ; write register address
inc c
ld a, b
out (c), a ; write value
dec c
inc d
dec e
jr nz, _opl3_fr_lp
ret
;----------------------------------------------------------------------
; Общая подпрограмма: silence текущего AY чипа. C уже = 0xFD.
; Reg 7 = 0x3F (mixer off), Regs 8-10 = 0 (volume off)
;----------------------------------------------------------------------
_ay_silence_core:
ld b, #0xFF
ld a, #7
out (c), a
ld b, #0xBF
ld a, #0x3F
out (c), a
ld d, #8
_ayc_lp:
ld b, #0xFF
ld a, d
out (c), a
ld b, #0xBF
xor a, a
out (c), a
inc d
ld a, d
cp a, #11
jr c, _ayc_lp
ret
;----------------------------------------------------------------------
; ay_write_reg(uint8_t reg, uint8_t val) — write AY chip 1 register
; reg in A, val on stack
;----------------------------------------------------------------------
.globl _ay_write_reg
_ay_write_reg::
ld c, #0xFD
ld b, #0xFF
out (c), a ; select register
ld hl, #2
add hl, sp
ld a, (hl)
ld b, #0xBF
out (c), a ; write value
ret
;----------------------------------------------------------------------
; ay2_write_reg(uint8_t reg, uint8_t val) — write AY chip 2 register
;----------------------------------------------------------------------
.globl _ay2_write_reg
_ay2_write_reg::
ld c, #0xFD
ld b, #0xFF
push af ; save reg
ld a, #0xFE
out (c), a ; select chip 2
pop af
ld b, #0xFF
out (c), a ; select register
ld hl, #2
add hl, sp
ld a, (hl)
ld b, #0xBF
out (c), a ; write value
ld b, #0xFF
ld a, #0xFF
out (c), a ; switch back to chip 1
ret
;----------------------------------------------------------------------
; saa_write_reg(uint8_t reg, uint8_t val) — SAA1099 chip 1
; addr=#00FF, data=#01FF
;----------------------------------------------------------------------
.globl _saa_write_reg
_saa_write_reg::
ld c, #0xFF
ld b, #0x00
out (c), a ; addr = reg
ld hl, #2
add hl, sp
ld a, (hl)
inc b ; B = 0x01
out (c), a ; data = val
ret
;----------------------------------------------------------------------
; saa2_write_reg(uint8_t reg, uint8_t val) — SAA1099 chip 2
; addr=#02FF, data=#03FF
;----------------------------------------------------------------------
.globl _saa2_write_reg
_saa2_write_reg::
ld c, #0xFF
ld b, #0x02
out (c), a ; addr = reg
ld hl, #2
add hl, sp
ld a, (hl)
inc b ; B = 0x03
out (c), a ; data = val
ret
;----------------------------------------------------------------------
; ay_silence() — AY chip 1 (FFFD/BFFD)
;----------------------------------------------------------------------
.globl _ay_silence
_ay_silence::
ld c, #0xFD
jr _ay_silence_core
;----------------------------------------------------------------------
; ay2_silence() — TurboSound chip 2: select → silence → switch back
;----------------------------------------------------------------------
.globl _ay2_silence
_ay2_silence::
ld c, #0xFD
ld b, #0xFF
ld a, #0xFE
out (c), a ; select chip 2
call _ay_silence_core
ld b, #0xFF
ld a, #0xFF
out (c), a ; switch back to chip 1
ret
;----------------------------------------------------------------------
; Общая подпрограмма: silence SAA чипа. C=0xFF, B=addr port hi.
; Data port = B+1 (inc b / dec b).
; Reset (0x1C=02), disable (0x1C=00), amplitude regs 0x00-0x05=0.
;----------------------------------------------------------------------
_saa_silence_core:
ld a, #0x1C
out (c), a ; addr
inc b
ld a, #0x02
out (c), a ; data: reset
dec b
ld a, #0x1C
out (c), a ; addr
inc b
xor a, a
out (c), a ; data: sound off
dec b
ld d, #0
_saac_lp:
ld a, d
out (c), a ; addr: reg N
inc b
xor a, a
out (c), a ; data: 0
dec b
inc d
ld a, d
cp a, #6
jr c, _saac_lp
ret
;----------------------------------------------------------------------
; saa_silence() — SAA1099 chip 1 (addr=#00FF, data=#01FF)
;----------------------------------------------------------------------
.globl _saa_silence
_saa_silence::
ld c, #0xFF
ld b, #0x00
jr _saa_silence_core
;----------------------------------------------------------------------
; saa2_silence() — SAA1099 chip 2 (addr=#02FF, data=#03FF)
;----------------------------------------------------------------------
.globl _saa2_silence
_saa2_silence::
ld c, #0xFF
ld b, #0x02
jr _saa_silence_core
+58
View File
@@ -0,0 +1,58 @@
;============================================================================
; pos_table.s — Таблица позиций INT (СГЕНЕРИРОВАНО, не редактировать)
;
; entries=14, step=5120, line_len=224
; ISR freq: 3,500,000 / 5120 = 684 Hz (~44100/16)
;============================================================================
.module pos_table_mod
.globl pos_table
.area _CODE
pos_table:
_pt_0:
.db 0x00, 0x00, 0x00 ; [ 0] line= 0, hpos= 0
.dw _pt_1
_pt_1:
.db 0x00, 0x16, 0xC0 ; [ 1] line= 22, hpos=192
.dw _pt_2
_pt_2:
.db 0x00, 0x2D, 0xA0 ; [ 2] line= 45, hpos=160
.dw _pt_3
_pt_3:
.db 0x00, 0x44, 0x80 ; [ 3] line= 68, hpos=128
.dw _pt_4
_pt_4:
.db 0x00, 0x5B, 0x60 ; [ 4] line= 91, hpos= 96
.dw _pt_5
_pt_5:
.db 0x00, 0x72, 0x40 ; [ 5] line=114, hpos= 64
.dw _pt_6
_pt_6:
.db 0x00, 0x89, 0x20 ; [ 6] line=137, hpos= 32
.dw _pt_7
_pt_7:
.db 0x00, 0xA0, 0x00 ; [ 7] line=160, hpos= 0
.dw _pt_8
_pt_8:
.db 0x00, 0xB6, 0xC0 ; [ 8] line=182, hpos=192
.dw _pt_9
_pt_9:
.db 0x00, 0xCD, 0xA0 ; [ 9] line=205, hpos=160
.dw _pt_10
_pt_10:
.db 0x00, 0xE4, 0x80 ; [10] line=228, hpos=128
.dw _pt_11
_pt_11:
.db 0x00, 0xFB, 0x60 ; [11] line=251, hpos= 96
.dw _pt_12
_pt_12:
.db 0x01, 0x12, 0x40 ; [12] line=274, hpos= 64
.dw _pt_13
_pt_13:
.db 0x01, 0x29, 0x20 ; [13] line=297, hpos= 32
.dw pos_table
.globl pos_table_end
pos_table_end:
+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
+561
View File
@@ -0,0 +1,561 @@
;==============================================================================
; txtlib.s — Text buffer library (hand-optimised Z80, sdcccall(1))
;
; Replaces lib/txtlib.c. Every public function matches the ABI
; generated by SDCC for the prototypes in inc/txtlib.h.
;
; Buffer layout: buf[0] = max capacity, buf[1] = current length,
; buf[2..] = character data.
;
; sdcccall(1) register conventions:
; 1st ptr/u16 → HL 2nd ptr/u16 → DE
; 1st u8 → A 2nd u8 (when 1st is u8) → L
; Further args → stack (callee-cleans)
; Return u8 → A Return u16 → HL
;==============================================================================
.module txtlib_mod
.globl _buf_append_str
.globl _buf_append_char
.globl _buf_append_u8_hex
.globl _buf_append_u16_dec
.globl _buf_append_u32_dec
.globl _buf_append_mmss
.globl _print_line
.globl _wc_prsrw_attr ; from wc_api.s
.area _CODE
;----------------------------------------------------------------------
; Helper: nibble (0–15 in A) → ASCII char ('0'–'F') in A
;----------------------------------------------------------------------
_nib2asc:
cp a, #10
jr c, 001$
add a, #7
001$: add a, #0x30
ret
;======================================================================
; uint8_t buf_append_str(char *buf, const char *src)
; HL=buf DE=src → A=added (no stack params)
;======================================================================
_buf_append_str::
ld c, (hl) ; C = max
inc hl
ld b, (hl) ; B = curlen
ld a, c
sub a, b ; A = remaining
jr z, bas_z
jr c, bas_z
push hl ; save &buf[1]
ld c, a ; C = remaining
inc hl ; HL → buf[2]
ld a, b
add a, l
ld l, a
jr nc, bas_nc
inc h
bas_nc: ld b, #0 ; B = added
bas_lp: ld a, (de)
or a, a
jr z, bas_dn
ld (hl), a
inc hl
inc de
inc b
dec c
jr nz, bas_lp
bas_dn: pop hl ; &buf[1]
ld a, (hl)
add a, b
ld (hl), a
ld a, b
ret
bas_z: xor a, a
ret
;======================================================================
; uint8_t buf_append_char(char *buf, char c)
; HL=buf stack: c(1) → A=0|1 callee-cleans 1
;======================================================================
_buf_append_char::
push ix
ld ix, #0
add ix, sp
; 4(ix) = c
ld c, (hl) ; max
inc hl
ld a, (hl) ; curlen
cp a, c
jr nc, bac_f
ld e, a
inc a
ld (hl), a ; buf[1]++
inc hl ; → buf[2]
ld a, e
add a, l
ld l, a
jr nc, bac_nc
inc h
bac_nc: ld a, 4 (ix)
ld (hl), a
ld a, #1
jr bac_r
bac_f: xor a, a
bac_r: pop ix
pop hl
inc sp ; skip c (1 byte)
jp (hl)
;======================================================================
; uint8_t buf_append_u8_hex(char *buf, uint8_t val)
; HL=buf stack: val(1) → A=0|2 callee-cleans 1
;======================================================================
_buf_append_u8_hex::
push ix
ld ix, #0
add ix, sp
; 4(ix) = val
ld c, (hl) ; max
inc hl
ld a, c
sub a, (hl) ; remaining
cp a, #2
jr c, bh8_f
ld b, (hl) ; curlen
ld a, b
add a, #2
ld (hl), a ; buf[1] += 2
inc hl
ld a, b
add a, l
ld l, a
jr nc, bh8_nc
inc h
bh8_nc: ld a, 4 (ix)
rrca
rrca
rrca
rrca
and a, #0x0F
call _nib2asc
ld (hl), a
inc hl
ld a, 4 (ix)
and a, #0x0F
call _nib2asc
ld (hl), a
ld a, #2
jr bh8_r
bh8_f: xor a, a
bh8_r: pop ix
pop hl
inc sp
jp (hl)
;----------------------------------------------------------------------
; Shared helper: emit one decimal digit.
; A = digit (0–9)
; B = "added" counter (0 = not-yet-started → suppress leading 0)
; C = remaining buffer space
; IY = destination pointer (advanced on write)
;----------------------------------------------------------------------
_emit_digit:
push af
or a, b ; z iff digit==0 AND not started
jr z, ed_skip
ld a, c
or a, a
jr z, ed_skip ; no space
dec c
pop af
add a, #0x30
ld 0 (iy), a
inc iy
inc b
ret
ed_skip:
pop af
ret
;----------------------------------------------------------------------
; Shared helper: extract one 32-bit decimal digit via repeated
; subtraction of a 4-byte constant taken from inline data after CALL.
;
; Usage: call _d32_digit
; .db lo, b1, b2, hi ; 32-bit subtrahend (LE)
;
; In: DEHL = 32-bit value, -3(ix) = scratch,
; B = started/added, C = remaining, IY = dest ptr
; Out: DEHL = remainder, digit emitted via _emit_digit
;----------------------------------------------------------------------
_d32_digit:
ex (sp), hl ; HL = inline-data ptr, (SP)=val_lo
ld a, (hl)
ld (d32c0 + 1), a
inc hl
ld a, (hl)
ld (d32c1 + 1), a
inc hl
ld a, (hl)
ld (d32c2 + 1), a
inc hl
ld a, (hl)
ld (d32c3 + 1), a
inc hl ; HL past data = true ret addr
ex (sp), hl ; (SP)=true ret, HL=val_lo restored
ld -3 (ix), #0xFF
d32d_lp:inc -3 (ix)
push de
push hl
ld a, l
d32c0: sub a, #0x00 ; patched: subtrahend byte 0
ld l, a
ld a, h
d32c1: sbc a, #0x00 ; patched: byte 1
ld h, a
ld a, e
d32c2: sbc a, #0x00 ; patched: byte 2
ld e, a
ld a, d
d32c3: sbc a, #0x00 ; patched: byte 3
ld d, a
jr c, d32d_un
pop af
pop af
jr d32d_lp
d32d_un:pop hl
pop de
ld a, -3 (ix)
jp _emit_digit ; tail-call, ret goes past inline data
;----------------------------------------------------------------------
; Shared 16-bit decimal division: 10000/1000/100/10/ones.
; Entry _div_10k_ones: full 16-bit decimal (10000..1).
; Entry _div_1k_ones: skip 10000, do 1000..1.
; In: DE=value, B=added counter, C=remaining, IY=dst.
; Out: B=updated, DE=remainder. A,HL clobbered.
;----------------------------------------------------------------------
_div_10k_ones:
;-- 10000 (neg 0xD8F0) --
ld a, #0xFF
_d10k: inc a
ld hl, #0xD8F0
add hl, de
jr nc, _d10ke
ex de, hl
jr _d10k
_d10ke: call _emit_digit
_div_1k_ones:
;-- 1000 (neg 0xFC18) --
ld a, #0xFF
_d1k: inc a
ld hl, #0xFC18
add hl, de
jr nc, _d1ke
ex de, hl
jr _d1k
_d1ke: call _emit_digit
;-- 100 (neg 0xFF9C) --
ld a, #0xFF
_d100: inc a
ld hl, #0xFF9C
add hl, de
jr nc, _d100e
ex de, hl
jr _d100
_d100e: call _emit_digit
;-- 10 (neg 0xFFF6) --
ld a, #0xFF
_d10: inc a
ld hl, #0xFFF6
add hl, de
jr nc, _d10e
ex de, hl
jr _d10
_d10e: call _emit_digit
;-- ones (always emit) --
ld a, c
or a, a
ret z
ld a, e
add a, #0x30
ld 0 (iy), a
inc iy
inc b
ret
;======================================================================
; uint8_t buf_append_u16_dec(char *buf, uint16_t val)
; HL=buf DE=val → A=added (no stack params)
;======================================================================
_buf_append_u16_dec::
push iy
ld c, (hl)
inc hl
ld a, c
sub a, (hl)
jr z, d16_z
jr c, d16_z
ld c, a ; C = remaining
ld b, (hl) ; B = curlen (temp)
push hl ; save &buf[1]
inc hl
ld a, b
add a, l
ld l, a
jr nc, d16_nc
inc h
d16_nc: push hl
pop iy ; IY = dst
ld b, #0 ; B = added/started
call _div_10k_ones
pop hl ; &buf[1]
ld a, (hl)
add a, b
ld (hl), a
ld a, b
pop iy
ret
d16_z: xor a, a
pop iy
ret
;======================================================================
; uint8_t buf_append_u32_dec(char *buf, uint32_t val)
; HL=buf stack: val(4) → A=added callee-cleans 4
; 4(ix)=byte0(LSB) 5(ix)=byte1 6(ix)=byte2 7(ix)=byte3(MSB)
;======================================================================
_buf_append_u32_dec::
push ix
ld ix, #0
add ix, sp
push iy ; -2,-1(ix)
push af ; -4,-3(ix) scratch
push af ; -6,-5(ix) scratch
; -3(ix) used as digit counter per power
ld c, (hl)
inc hl
ld a, c
sub a, (hl)
jp z, d32_z
jp c, d32_z
ld c, a ; C = remaining
ld b, (hl) ; B = curlen (temp)
push hl ; save &buf[1] → -8,-7(ix)
inc hl
ld a, b
add a, l
ld l, a
jr nc, d32_nc
inc h
d32_nc: push hl
pop iy ; IY = dst
; Load 32-bit val into DEHL
ld l, 4 (ix)
ld h, 5 (ix)
ld e, 6 (ix)
ld d, 7 (ix)
ld b, #0 ; B = added/started
;-- 1,000,000 / 100,000 / 10,000 via shared subroutine --
call _d32_digit
.db 0x40, 0x42, 0x0F, 0x00 ; 1,000,000
call _d32_digit
.db 0xA0, 0x86, 0x01, 0x00 ; 100,000
call _d32_digit
.db 0x10, 0x27, 0x00, 0x00 ; 10,000
;-- remainder < 10000, fits in HL → move to DE --
ex de, hl
call _div_1k_ones
pop hl ; &buf[1]
ld a, (hl)
add a, b
ld (hl), a
ld a, b ; return value
d32_exit:
; clean locals, restore, callee-clean 4
pop de ; eat -6,-5
pop de ; eat -4,-3
pop iy
pop ix
pop hl ; ret addr
pop de ; skip val lo
pop de ; skip val hi
jp (hl)
d32_z: xor a, a
jr d32_exit
;======================================================================
; void print_line(wc_window_t *win, uint8_t y, const char *buf,
; uint8_t attr)
; HL=win stack: y(1) buf(2) attr(1) callee-cleans 4
; Prints buf content + spaces padding to fill content width.
;======================================================================
_print_line::
push ix
ld ix, #0
add ix, sp
; 4(ix)=y 5,6(ix)=buf 7(ix)=attr
push af ; -2,-1(ix): cw at -1, slen at -2
push hl ; -4,-3(ix): win
;-- cw = get_content_width(win) --
ld de, #4
add hl, de
ld a, (hl)
or a, a
jr nz, pl_nz
ld a, #96
pl_nz: sub a, #4
ld -1 (ix), a
;-- slen = buf[1] --
ld l, 5 (ix)
ld h, 6 (ix)
inc hl
ld a, (hl)
ld -2 (ix), a
;-- print_len = min(slen, cw) --
cp a, -1 (ix)
jr c, pl_min
ld a, -1 (ix)
pl_min: ld c, a ; C = print_len
;-- 1st wc_prsrw_attr(win, data, y, 2, print_len, attr) --
ld a, 7 (ix)
push af
inc sp
ld b, #0
push bc
ld h, #2
ld l, 4 (ix)
push hl
; DE = buf + 2
ld a, 5 (ix)
add a, #2
ld e, a
ld a, 6 (ix)
adc a, #0
ld d, a
; HL = win
ld l, -4 (ix)
ld h, -3 (ix)
call _wc_prsrw_attr
;-- padding? --
ld a, -2 (ix)
cp a, -1 (ix)
jr nc, pl_done
; pad = cw − slen
ld a, -1 (ix)
sub a, -2 (ix)
ld c, a
; Fill buf[2..2+pad-1] with 0x20 (runtime space fill)
ld l, 5 (ix)
ld h, 6 (ix)
inc hl
inc hl ; HL = &buf[2]
ld b, c ; B = pad count
ld a, #0x20
pf_fill:ld (hl), a
inc hl
djnz pf_fill ; B=0 after loop
;-- 2nd wc_prsrw_attr(win, &buf[2], y, 2+slen, pad, attr) --
ld a, 7 (ix)
push af
inc sp
push bc ; len=pad (B=0, C=pad)
ld a, -2 (ix)
add a, #2
ld h, a ; x = 2 + slen
ld l, 4 (ix) ; y
push hl
ld e, 5 (ix)
ld d, 6 (ix)
inc de
inc de ; DE = &buf[2]
ld l, -4 (ix)
ld h, -3 (ix)
call _wc_prsrw_attr
pl_done:
ld sp, ix
pop ix
pop hl
pop af
pop af ; callee-clean 4
jp (hl)
;----------------------------------------------------------------------
; buf_append_mmss(buf, sec) — append "MM:SS" (5 chars) to buffer
;
; sdcccall(1): HL = buf, DE = sec (uint16_t, 0..5999)
; No division — subtraction loops only (~30 bytes code).
;----------------------------------------------------------------------
_buf_append_mmss:
push hl
pop ix ; IX = buf
; DE = total seconds. Divide by 60 via subtraction → B=min, E=sec
ld b, #0
mmss_d60:
ld a, e
sub a, #60
ld c, a
ld a, d
sbc a, #0
jr c, mmss_d60_done
ld d, a
ld e, c
inc b
jr mmss_d60
mmss_d60_done:
; B = minutes, E = seconds
push de ; save seconds (mmss_2dig clobbers DE)
ld a, b
call mmss_2dig ; appends 2 digits of A
; Append ':'
ld a, 1 (ix)
ld c, a
ld b, #0
inc a
ld 1 (ix), a
push ix
pop hl
add hl, bc
inc hl ; skip buf[0] (max)
inc hl ; skip buf[1] (len)
ld (hl), #0x3A ; ':'
; Append seconds
pop de ; restore E = seconds
ld a, e
; fall through to mmss_2dig — but IX is still buf
; call mmss_2dig
call mmss_2dig
ret
; Subroutine: append 2 ASCII digits of A (0..99) to buf at IX
; Destroys A, C, B, HL
mmss_2dig:
ld c, #0x30 ; tens digit = '0'
mmss_t10:
sub a, #10
jr c, mmss_t10_done
inc c
jr mmss_t10
mmss_t10_done:
add a, #10 ; undo last sub
add a, #0x30 ; units → ASCII
ld b, a ; save units
; Append tens (C) then units (B)
ld a, 1 (ix) ; len
ld l, a
ld h, #0
add a, #2
ld 1 (ix), a ; len += 2
push ix
pop de ; DE = buf base
add hl, de
inc hl ; skip buf[0] (max)
inc hl ; skip buf[1] (len)
ld (hl), c ; tens
inc hl
ld (hl), b ; units
ret
File diff suppressed because it is too large Load Diff