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
+32
View File
@@ -0,0 +1,32 @@
@echo off
REM Build script for VGM Player plugin
REM Requires sjasm assembler in tools/sjasm directory
echo Building VGM Player plugin...
REM Check if sjasm exists
if not exist "tools\sjasm\sjasmplus.exe" (
echo Error: sjasmplus.exe not found in tools\sjasm\
echo Please install sjasm assembler
exit /b 1
)
REM Compile the plugin
"tools\sjasm\sjasmplus.exe" src\vgmplayer.asm
if errorlevel 1 (
echo.
echo Compilation failed!
exit /b 1
)
echo.
echo Build successful!
echo Output: vgmplay.wmf
echo.
REM Create disk image
echo Creating disk image...
call create_disk.bat
exit /b 0
+52
View File
@@ -0,0 +1,52 @@
@echo off
REM Create disk image with VGM Player plugin for TSConfig/WC
REM Image size: 512MB = 524288 KB
echo Creating TSConfig disk image with VGM Player...
echo.
REM Check if plugin was built
if not exist "vgmplay.wmf" (
echo Error: vgmplay.wmf not found!
echo Please run build.bat first.
exit /b 1
)
REM Step 1: Copy plugin to DiskRef\WC directory
echo Step 1: Copying plugin to DiskRef\WC...
copy /Y vgmplay.wmf DiskRef\WC\VGMPLAY.WMF
if errorlevel 1 (
echo Error: Failed to copy plugin
exit /b 1
)
echo Done: vgmplay.wmf copied to DiskRef\WC\
REM Step 2: Create disk image
echo.
echo Step 2: Creating 512MB disk image...
echo Using robimg.exe to create tsconf.img
del tsconf.img 2>nul
tools\robimg.exe -p="tsconf.img" -s=524288 -C="DiskRef"
if errorlevel 1 (
echo Error: Failed to create disk image
exit /b 1
)
echo.
echo ========================================
echo Disk image created successfully!
echo ========================================
echo.
echo Output file: tsconf.img (512MB)
echo Contains: DiskRef contents + VGM Player plugin
echo.
echo To use:
echo 1. Mount tsconf.img in your TSConfig emulator
echo 2. Boot Wild Commander
echo 3. Navigate to WC directory
echo 4. Find VGMPLAY.WMF and press Enter to load
echo 5. Select a .VGM file to play
echo.
+224
View File
@@ -0,0 +1,224 @@
; ═══════════════════════════════════════════════════════════════════════════
; Модуль ISR - 2734 Гц, обработчик прерывания
; ═══════════════════════════════════════════════════════════════════════════
;
; Архитектура: ВСЕ значения для регистров INT предрассчитаны в таблице.
; ISR читает 3 байта через 3×OUTI — НИКАКИХ вычислений!
;
; Частота: 3,500,000 / 1280 = 2734.375 Hz (~44100/16, разница ~0.8%)
; Цикл: 56 тиков × 1280 = 71680 позиций = 1 видеокадр → ИДЕАЛЬНОЕ замыкание
; Таблица: 56 × 5 = 280 байт
;
; При 14 МГц с wait-state (~x2.5): ~3200 эффективных тактов между тиками.
;
; Коды команд:
; CMD_WRITE_B0 = #40 [#40, reg, val, pad] → OUT OPL3_B0_ADDR/DATA
; CMD_WRITE_B1 = #80 [#80, reg, val, pad] → OUT OPL3_B1_ADDR/DATA
; CMD_WAIT = #C0 [#C0, lo, hi, pad] → ISR_WAIT_CTR = HL
; CMD_END_BUF = #E0 [#E0, pad,pad, pad] → переключить буфер
; ═══════════════════════════════════════════════════════════════════════════
; Коды команд (= смещение от ISR_HANDLER, определены в vgmplayer.asm)
; CMD_WRITE_B0 = #40, CMD_WRITE_B1 = #80, CMD_WAIT = #C0, CMD_END_BUF = #E0
; ═══════════════════════════════════════════════════════════════════════════
; ISR_INIT - Инициализация таблицы векторов IM2 и указателя позиции
; ═══════════════════════════════════════════════════════════════════════════
ISR_INIT:
DI
; --- Заполнить таблицу векторов IM2 (257 байт, выровнено по 256) ---
LD HL,IM2_TABLE
LD DE,IM2_TABLE+1
LD A,LOW ISR_HANDLER
LD (HL),A
LD BC,256
LDIR
LD HL,IM2_TABLE+#100
LD (HL),HIGH ISR_HANDLER
; Установить регистр I
LD A,HIGH IM2_TABLE
LD I,A
; --- Разрешить только прерывание FRAME ---
LD BC,PORT_INTMASK
LD A,%00000001 ; только FRAME
OUT (C),A
; --- Инициализировать указатель позиции ---
LD HL,POS_TABLE
LD (POS_PTR),HL ; храним в переменной
; --- Инициализировать счётчик ожидания ---
LD HL,0
LD (ISR_WAIT_CTR),HL
; --- Записать начальную позицию в железо: строка 0, hpos 0 ---
LD BC,PORT_HSINT
XOR A
OUT (C),A ; HSINT = 0
LD B,HIGH PORT_VSINTL
OUT (C),A ; VSINTL = 0
LD B,HIGH PORT_VSINTH
OUT (C),A ; VSINTH = 0
; Переключиться в IM2
IM 2
RET
; ═══════════════════════════════════════════════════════════════════════════
; ISR_HANDLER - прерывание FRAME (2734 Гц)
; ─── OUTI-блок и таблицу не трогать! ────────────────────────────────────
; Команды: CMD_WRITE_B0=#40, CMD_WRITE_B1=#80, CMD_WAIT=#C0, CMD_END_BUF=#E0
; ═══════════════════════════════════════════════════════════════════════════
ISR_HANDLER:
PUSH AF
PUSH HL
PUSH BC
PUSH DE
; ─── OUTI×3: вывести позицию INT из таблицы (не трогать!) ───────
LD HL,(POS_PTR)
LD BC,#25AF ; B=#25, C=#AF
OUTI ; B→#24, OUT #24AF,(HL++) → VSINTH
OUTI ; B→#23, OUT #23AF,(HL++) → VSINTL
OUTI ; B→#22, OUT #22AF,(HL++) → HSINT
LD A,(HL) ; next_lo
INC HL
LD H,(HL) ; next_hi
LD L,A ; HL = next_ptr
LD (POS_PTR),HL
; ─── SECOND_TIMER: счётчик секунд (ВСЕГДА, независимо от WAIT_CTR) ──
LD HL,(SECOND_TIMER)
DEC HL
LD A,H
OR L
LD (SECOND_TIMER),HL
JR NZ,ISR_NO_SEC
; Секунда истекла
LD HL,ISR_FREQ
LD (SECOND_TIMER),HL
LD HL,(PLAY_SECONDS)
INC HL
LD (PLAY_SECONDS),HL
ISR_NO_SEC:
; ─── WAIT_CTR: если 0 — идти выполнять команды ──────────────────
LD HL,(ISR_WAIT_CTR)
LD A,H
OR L
JP Z,ISR_EXEC_CMD
DEC HL
LD (ISR_WAIT_CTR),HL
ISR_EXIT:
XOR A
OUT (#FE),A ; DEBUG: чёрный = выход из ISR
POP DE
POP BC
POP HL
POP AF
EI
RET
; ─── Exec-путь ───────────────────────────────────────────────────────────
ISR_EXEC_CMD:
LD A,(ISR_ENABLED)
OR A
JP Z,ISR_EXIT
LD HL,(ISR_READ_PTR)
EX DE,HL ; DE = указатель потока команд
ISR_CMD_LOOP:
LD A,(DE)
INC DE
CP CMD_WRITE_B0 ; #40
JP Z,ISR_WRITE_B0
CP CMD_WRITE_B1 ; #80
JP Z,ISR_WRITE_B1
CP CMD_WAIT ; #C0
JP Z,ISR_DO_WAIT
; CMD_END_BUF (#E0) — любое другое значение тоже сюда
ISR_DO_END_BUF:
LD A,2
OUT (#FE),A ; DEBUG: красный = смена буфера
LD A,(ISR_ACTIVE_BUF)
XOR 1
LD (ISR_ACTIVE_BUF),A
JR NZ,ISR_END_USE_B
LD DE,CMD_BUF_A
JP ISR_CMD_LOOP ; продолжать в новом буфере!
ISR_END_USE_B:
LD DE,CMD_BUF_B
JP ISR_CMD_LOOP ; продолжать в новом буфере!
ISR_WRITE_B0:
LD A,6
OUT (#FE),A ; DEBUG: жёлтый = запись OPL3
LD A,(DE) ; reg
LD BC,OPL3_ADDR0 ; B=0, C=#C4 → полный адрес порта #00C4
OUT (C),A
INC DE
LD A,(DE) ; val
LD BC,OPL3_DATA0 ; B=0, C=#C5 → полный адрес порта #00C5
OUT (C),A
INC DE
INC DE ; pad
JP ISR_CMD_LOOP
ISR_WRITE_B1:
LD A,6
OUT (#FE),A ; DEBUG: жёлтый = запись OPL3
LD A,(DE) ; reg
LD BC,OPL3_ADDR1 ; B=0, C=#C6 → полный адрес порта #00C6
OUT (C),A
INC DE
LD A,(DE) ; val
LD BC,OPL3_DATA1 ; B=0, C=#C7 → полный адрес порта #00C7
OUT (C),A
INC DE
INC DE ; pad
JP ISR_CMD_LOOP
ISR_DO_WAIT:
LD A,(DE) ; wait_lo
INC DE
LD L,A
LD A,(DE) ; wait_hi
INC DE
LD H,A ; HL = wait counter
INC DE ; pad
LD (ISR_WAIT_CTR),HL
EX DE,HL
LD (ISR_READ_PTR),HL ; сохранить позицию потока
JP ISR_EXIT
; ─── Переменные ISR ──────────────────────────────────────────────────────
POS_PTR: DW POS_TABLE ; указатель на текущую запись в таблице
SECOND_TIMER: DW ISR_FREQ ; обратный счётчик 2734 → 0 = 1 секунда
; ═══════════════════════════════════════════════════════════════════════════
; Таблица векторов IM2 (выравнивание по 256 байт, размер 257 байт)
; ═══════════════════════════════════════════════════════════════════════════
ALIGN 256
IM2_TABLE:
DS 257
; ═══════════════════════════════════════════════════════════════════════════
; Командные буферы (двойная буферизация)
; ═══════════════════════════════════════════════════════════════════════════
ALIGN 4
CMD_BUF_A:
DS CMD_BUF_SIZE
CMD_BUF_B:
DS CMD_BUF_SIZE
; ═══════════════════════════════════════════════════════════════════════════
; Таблица позиций - 2734 Гц (шаг=1280, 56 записей, чистый цикл)
; Формат: [VSINTH, VSINTL, HSINT] для последовательности 3×OUTI
; Размер: 280 байт
; ═══════════════════════════════════════════════════════════════════════════
INCLUDE "src/pos_table_43750.inc"
+109
View File
@@ -0,0 +1,109 @@
; ═══════════════════════════════════════════════════════════════════════════
; Keyboard Module - Direct ZX Spectrum matrix reading
; ═══════════════════════════════════════════════════════════════════════════
; Reads keyboard via IN port #FE (no WC API, no interrupts needed).
; TSConfig translates PS/2 → ZX matrix in hardware.
;
; ZX Spectrum keyboard matrix (active low):
; Port #FEFE: CAPS, Z, X, C, V (row 0)
; Port #FDFE: A, S, D, F, G (row 1)
; Port #FBFE: Q, W, E, R, T (row 2)
; Port #F7FE: 1, 2, 3, 4, 5 (row 3)
; Port #EFFE: 0, 9, 8, 7, 6 (row 4)
; Port #DFFE: P, O, I, U, Y (row 5)
; Port #BFFE: ENTER, L, K, J, H (row 6)
; Port #7FFE: SPACE, SYM, M, N, B (row 7)
;
; Each row: bit 0 = rightmost key, bit 4 = leftmost key
; Bit = 0 means key pressed, bit = 1 means not pressed
; ═══════════════════════════════════════════════════════════════════════════
; Debounce counter (simple frame-based debouncing)
KBD_DEBOUNCE: DB 0
; ═══════════════════════════════════════════════════════════════════════════
; KBD_READ - Read keyboard and return action code
; ═══════════════════════════════════════════════════════════════════════════
; Output: A = action code
; 0 = nothing pressed
; 1 = ESC (Caps Shift + Space)
; 2 = next track (Space alone)
; 3 = prev track (P key)
; 4 = pause/unpause (M key)
; ═══════════════════════════════════════════════════════════════════════════
KBD_READ:
; Simple debouncing: skip reads for N frames after a keypress
LD A,(KBD_DEBOUNCE)
OR A
JR Z,KBD_DO_READ
DEC A
LD (KBD_DEBOUNCE),A
XOR A ; return 0
RET
KBD_DO_READ:
; --- Check ESC = Caps Shift + Space ---
; Caps Shift = row 0 bit 0 (#FEFE)
LD A,#FE
IN A,(#FE)
BIT 0,A ; Caps Shift
JR NZ,KBD_CHECK_SPACE ; not pressed
; Caps Shift is held. Check Space = row 7 bit 0 (#7FFE)
LD A,#7F
IN A,(#FE)
BIT 0,A ; Space
JR NZ,KBD_CHECK_SPACE ; not pressed
; ESC detected!
LD A,16
LD (KBD_DEBOUNCE),A
LD A,1
RET
KBD_CHECK_SPACE:
; --- Check SPACE alone (without Caps Shift) ---
; Space = row 7 bit 0 (#7FFE)
LD A,#7F
IN A,(#FE)
BIT 0,A
JR NZ,KBD_CHECK_P
; Check that Caps Shift is NOT held (to distinguish from ESC)
LD A,#FE
IN A,(#FE)
BIT 0,A ; CShift=0 means held
JR Z,KBD_CHECK_P ; CShift held → this is ESC, not SPACE
; SPACE alone = next track
LD A,12
LD (KBD_DEBOUNCE),A
LD A,2
RET
KBD_CHECK_P:
; --- Check P key = row 5 bit 0 (#DFFE) ---
LD A,#DF
IN A,(#FE)
BIT 0,A
JR NZ,KBD_CHECK_M
; P pressed = previous track
LD A,12
LD (KBD_DEBOUNCE),A
LD A,3
RET
KBD_CHECK_M:
; --- Check M key = row 7 bit 2 (#7FFE) ---
LD A,#7F
IN A,(#FE)
BIT 2,A
JR NZ,KBD_NOTHING
; M pressed = pause
LD A,12
LD (KBD_DEBOUNCE),A
LD A,4
RET
KBD_NOTHING:
XOR A
RET
+206
View File
@@ -0,0 +1,206 @@
; ═══════════════════════════════════════════════════════════════════════════
; OPL3 (YMF262) Hardware Driver - Simplified
; ═══════════════════════════════════════════════════════════════════════════
;
; OPL3_INIT - Detect and initialize OPL3
; OPL3_RESET - Silence all channels
;
; Ports: #C4/#C5 = bank 0 addr/data, #C6/#C7 = bank 1 addr/data
; ═══════════════════════════════════════════════════════════════════════════
; ═══════════════════════════════════════════════════════════════════════════
; OPL3_INIT - Detect OPL3 and enable OPL3 mode
; ═══════════════════════════════════════════════════════════════════════════
; Output: OPL3_FOUND = 1 if found, 0 if not
OPL3_INIT:
PUSH AF
PUSH BC
PUSH DE
; --- Detect OPL3 using timer method ---
; 1. Reset timers
LD BC,OPL3_ADDR0
LD A,#04 ; Timer control register
OUT (C),A
CALL OPL3_DELAY_SHORT
LD BC,OPL3_DATA0
LD A,#60 ; Reset T1 and T2 flags
OUT (C),A
CALL OPL3_DELAY_LONG
; 2. Read status - should be 0
LD BC,OPL3_ADDR0
IN A,(C)
AND #E0
JR NZ,OPL3_NOT_FOUND
; 3. Start timer 1 with max value
LD BC,OPL3_ADDR0
LD A,#02 ; Timer 1 register
OUT (C),A
CALL OPL3_DELAY_SHORT
LD BC,OPL3_DATA0
LD A,#FF
OUT (C),A
CALL OPL3_DELAY_LONG
LD BC,OPL3_ADDR0
LD A,#04
OUT (C),A
CALL OPL3_DELAY_SHORT
LD BC,OPL3_DATA0
LD A,#21 ; Start timer 1, unmask
OUT (C),A
; 4. Wait for timer to overflow (~100 µs)
LD B,200 ; generous delay at 14 MHz
OPL3_DET_WAIT:
DJNZ OPL3_DET_WAIT
; 5. Read status
LD BC,OPL3_ADDR0
IN A,(C)
AND #E0
JR Z,OPL3_NOT_FOUND ; timer didnt fire = no OPL
; 6. Reset timers again
LD BC,OPL3_ADDR0
LD A,#04
OUT (C),A
CALL OPL3_DELAY_SHORT
LD BC,OPL3_DATA0
LD A,#60
OUT (C),A
CALL OPL3_DELAY_LONG
; --- OPL3 Found! ---
LD A,1
LD (OPL3_FOUND),A
; Enable OPL3 mode (register 0x105 = bit 0)
LD BC,OPL3_ADDR1
LD A,#05
OUT (C),A
CALL OPL3_DELAY_SHORT
LD BC,OPL3_DATA1
LD A,#01
OUT (C),A
CALL OPL3_DELAY_LONG
; Waveform select enable (register 0x01 = 0x20)
LD BC,OPL3_ADDR0
LD A,#01
OUT (C),A
CALL OPL3_DELAY_SHORT
LD BC,OPL3_DATA0
LD A,#20
OUT (C),A
CALL OPL3_DELAY_LONG
POP DE
POP BC
POP AF
RET
OPL3_NOT_FOUND:
XOR A
LD (OPL3_FOUND),A
POP DE
POP BC
POP AF
RET
; ═══════════════════════════════════════════════════════════════════════════
; OPL3_RESET - Silence all OPL3 channels
; ═══════════════════════════════════════════════════════════════════════════
OPL3_RESET:
PUSH AF
PUSH BC
PUSH DE
LD A,(OPL3_FOUND)
OR A
JR Z,OPL3_RESET_DONE
; Write 0 to all registers in both banks
; Bank 0: registers #20-#F5
LD D,#20 ; start register
OPL3_RESET_BANK0:
LD BC,OPL3_ADDR0
LD A,D
OUT (C),A
CALL OPL3_DELAY_SHORT
LD BC,OPL3_DATA0
XOR A
OUT (C),A
CALL OPL3_DELAY_LONG
INC D
LD A,D
CP #F6
JR C,OPL3_RESET_BANK0
; Bank 1: registers #20-#F5
LD D,#20
OPL3_RESET_BANK1:
LD BC,OPL3_ADDR1
LD A,D
OUT (C),A
CALL OPL3_DELAY_SHORT
LD BC,OPL3_DATA1
XOR A
OUT (C),A
CALL OPL3_DELAY_LONG
INC D
LD A,D
CP #F6
JR C,OPL3_RESET_BANK1
; Disable OPL3 mode on exit
LD BC,OPL3_ADDR1
LD A,#05
OUT (C),A
CALL OPL3_DELAY_SHORT
LD BC,OPL3_DATA1
XOR A
OUT (C),A
CALL OPL3_DELAY_LONG
OPL3_RESET_DONE:
POP DE
POP BC
POP AF
RET
; ═══════════════════════════════════════════════════════════════════════════
; OPL3 Delays
; ═══════════════════════════════════════════════════════════════════════════
; At 14 MHz: ~3.3 µs for address = 46 cycles, ~22 µs for data = 308 cycles
; DJNZ = 13 cycles per iteration at 14 MHz (or 8+5)
OPL3_DELAY_SHORT:
; ~3.5 µs at 14 MHz = ~49 cycles
PUSH BC
LD B,5
OPL3_DS_LOOP:
DJNZ OPL3_DS_LOOP
POP BC
RET
OPL3_DELAY_LONG:
; ~22 µs at 14 MHz = ~308 cycles
PUSH BC
LD B,25
OPL3_DL_LOOP:
DJNZ OPL3_DL_LOOP
POP BC
RET
+66
View File
@@ -0,0 +1,66 @@
; Position table for 2734 Hz (step=1280, 56 entries, perfect 1-frame cycle)
; Format: [VSINTH, VSINTL, HSINT, next_lo, next_hi] - 5 bytes per entry
; Frequency: 3,500,000 / 1280 = 2734.375 Hz (~44100/16)
; At 14MHz with wait-states: ~3200 effective T-states between interrupts
POS_TABLE_ENTRIES EQU 56
ISR_FREQ EQU 2734 ; used for SECOND_TIMER countdown
POS_TABLE:
DB #00, #00, #00 : DW $+2 ; [ 0] line= 0, hpos= 0
DB #00, #05, #A0 : DW $+2 ; [ 1] line= 5, hpos=160
DB #00, #0B, #60 : DW $+2 ; [ 2] line= 11, hpos= 96
DB #00, #11, #20 : DW $+2 ; [ 3] line= 17, hpos= 32
DB #00, #16, #C0 : DW $+2 ; [ 4] line= 22, hpos=192
DB #00, #1C, #80 : DW $+2 ; [ 5] line= 28, hpos=128
DB #00, #22, #40 : DW $+2 ; [ 6] line= 34, hpos= 64
DB #00, #28, #00 : DW $+2 ; [ 7] line= 40, hpos= 0
DB #00, #2D, #A0 : DW $+2 ; [ 8] line= 45, hpos=160
DB #00, #33, #60 : DW $+2 ; [ 9] line= 51, hpos= 96
DB #00, #39, #20 : DW $+2 ; [ 10] line= 57, hpos= 32
DB #00, #3E, #C0 : DW $+2 ; [ 11] line= 62, hpos=192
DB #00, #44, #80 : DW $+2 ; [ 12] line= 68, hpos=128
DB #00, #4A, #40 : DW $+2 ; [ 13] line= 74, hpos= 64
DB #00, #50, #00 : DW $+2 ; [ 14] line= 80, hpos= 0
DB #00, #55, #A0 : DW $+2 ; [ 15] line= 85, hpos=160
DB #00, #5B, #60 : DW $+2 ; [ 16] line= 91, hpos= 96
DB #00, #61, #20 : DW $+2 ; [ 17] line= 97, hpos= 32
DB #00, #66, #C0 : DW $+2 ; [ 18] line=102, hpos=192
DB #00, #6C, #80 : DW $+2 ; [ 19] line=108, hpos=128
DB #00, #72, #40 : DW $+2 ; [ 20] line=114, hpos= 64
DB #00, #78, #00 : DW $+2 ; [ 21] line=120, hpos= 0
DB #00, #7D, #A0 : DW $+2 ; [ 22] line=125, hpos=160
DB #00, #83, #60 : DW $+2 ; [ 23] line=131, hpos= 96
DB #00, #89, #20 : DW $+2 ; [ 24] line=137, hpos= 32
DB #00, #8E, #C0 : DW $+2 ; [ 25] line=142, hpos=192
DB #00, #94, #80 : DW $+2 ; [ 26] line=148, hpos=128
DB #00, #9A, #40 : DW $+2 ; [ 27] line=154, hpos= 64
DB #00, #A0, #00 : DW $+2 ; [ 28] line=160, hpos= 0
DB #00, #A5, #A0 : DW $+2 ; [ 29] line=165, hpos=160
DB #00, #AB, #60 : DW $+2 ; [ 30] line=171, hpos= 96
DB #00, #B1, #20 : DW $+2 ; [ 31] line=177, hpos= 32
DB #00, #B6, #C0 : DW $+2 ; [ 32] line=182, hpos=192
DB #00, #BC, #80 : DW $+2 ; [ 33] line=188, hpos=128
DB #00, #C2, #40 : DW $+2 ; [ 34] line=194, hpos= 64
DB #00, #C8, #00 : DW $+2 ; [ 35] line=200, hpos= 0
DB #00, #CD, #A0 : DW $+2 ; [ 36] line=205, hpos=160
DB #00, #D3, #60 : DW $+2 ; [ 37] line=211, hpos= 96
DB #00, #D9, #20 : DW $+2 ; [ 38] line=217, hpos= 32
DB #00, #DE, #C0 : DW $+2 ; [ 39] line=222, hpos=192
DB #00, #E4, #80 : DW $+2 ; [ 40] line=228, hpos=128
DB #00, #EA, #40 : DW $+2 ; [ 41] line=234, hpos= 64
DB #00, #F0, #00 : DW $+2 ; [ 42] line=240, hpos= 0
DB #00, #F5, #A0 : DW $+2 ; [ 43] line=245, hpos=160
DB #00, #FB, #60 : DW $+2 ; [ 44] line=251, hpos= 96
DB #01, #01, #20 : DW $+2 ; [ 45] line=257, hpos= 32
DB #01, #06, #C0 : DW $+2 ; [ 46] line=262, hpos=192
DB #01, #0C, #80 : DW $+2 ; [ 47] line=268, hpos=128
DB #01, #12, #40 : DW $+2 ; [ 48] line=274, hpos= 64
DB #01, #18, #00 : DW $+2 ; [ 49] line=280, hpos= 0
DB #01, #1D, #A0 : DW $+2 ; [ 50] line=285, hpos=160
DB #01, #23, #60 : DW $+2 ; [ 51] line=291, hpos= 96
DB #01, #29, #20 : DW $+2 ; [ 52] line=297, hpos= 32
DB #01, #2E, #C0 : DW $+2 ; [ 53] line=302, hpos=192
DB #01, #34, #80 : DW $+2 ; [ 54] line=308, hpos=128
DB #01, #3A, #40 : DW POS_TABLE ; [ 55] line=314, hpos= 64
POS_TABLE_END:
+213
View File
@@ -0,0 +1,213 @@
; ═══════════════════════════════════════════════════════════════════════════
; Модуль UI - текстовый интерфейс плеера
; ═══════════════════════════════════════════════════════════════════════════
; Использует WC API для рисования окна.
;
; Обязательный порядок при старте плагина:
; 1. CALL GEDPL ; восстановить дисплей/палитру WC (обязательно!)
; 2. CALL UI_INIT ; нарисовать окно (IX = PLWND)
; 3. CALL UI_DRAW_INFO ; заполнить содержимое
;
; Все оконные функции (PRWOW, RRESB, TXTPR) требуют IX = SOW (PLWND)
; ═══════════════════════════════════════════════════════════════════════════
; ─── Структура окна SOW (Structure Of Window) ─────────────────────────────
; Должна лежать в #8000-#BFFF или #0000-#3FFF.
; +8 (DW) = 0 при первом вызове PRWOW, WC заполнит адресом буфера.
PLWND:
DB 0 ; +0: тип 0 = стандартное окно с рамкой
DB 0 ; +1: маска цвета курсора
DB WND_X ; +2: X позиция на экране
DB WND_Y ; +3: Y позиция на экране
DB WND_W ; +4: ширина окна
DB WND_H ; +5: высота окна
DB CLR_MAIN ; +6: цвет (paper+ink)
DB 0 ; +7: резерв (всегда 0!)
DW 0 ; +8,+9: буфер (0 = не выведено, WC заполнит)
DB 0 ; +10: разделитель 1 (0 = нет)
DB 0 ; +11: разделитель 2 (0 = нет)
; ═══════════════════════════════════════════════════════════════════════════
; UI_INIT - Вывести окно плеера на экран
; ═══════════════════════════════════════════════════════════════════════════
UI_INIT:
PUSH AF
PUSH BC
PUSH DE
PUSH HL
LD IX,PLWND ; IX = SOW (обязательно для PRWOW!)
CALL PRWOW ; нарисовать окно
POP HL
POP DE
POP BC
POP AF
RET
; ═══════════════════════════════════════════════════════════════════════════
; UI_DRAW_INFO - Нарисовать информацию о файле в окне
; ═══════════════════════════════════════════════════════════════════════════
UI_DRAW_INFO:
PUSH AF
PUSH BC
PUSH DE
PUSH HL
; Заголовок плеера
LD IX,PLWND ; IX = SOW
LD D,1 ; Y = 1 (внутри окна)
LD E,2 ; X = 2 (внутри окна)
LD HL,STR_TITLE
CALL TXTPR
; Строка подсказки клавиш
LD IX,PLWND
LD D,2
LD E,2
LD HL,STR_KEYS
CALL TXTPR
; Статус (Playing/Paused)
CALL UI_DRAW_STATUS
POP HL
POP DE
POP BC
POP AF
RET
; ═══════════════════════════════════════════════════════════════════════════
; UI_DRAW_STATUS - Показать статус воспроизведения
; ═══════════════════════════════════════════════════════════════════════════
UI_DRAW_STATUS:
PUSH AF
PUSH BC
PUSH DE
PUSH HL
LD A,(PAUSED)
OR A
JR NZ,UI_SHOW_PAUSED
LD HL,STR_PLAYING
JR UI_SHOW_STATUS
UI_SHOW_PAUSED:
LD HL,STR_PAUSED
UI_SHOW_STATUS:
LD IX,PLWND ; IX = SOW
LD D,4 ; Y = 4 (внутри окна)
LD E,2
CALL TXTPR
POP HL
POP DE
POP BC
POP AF
RET
; ═══════════════════════════════════════════════════════════════════════════
; UI_UPDATE - Периодическое обновление (вызывается из главного цикла)
; ═══════════════════════════════════════════════════════════════════════════
UI_UPDATE:
PUSH AF
PUSH BC
PUSH DE
PUSH HL
; --- Вычислить MM:SS из PLAY_SECONDS ---
LD HL,(PLAY_SECONDS)
LD B,0 ; B = minutes
UI_DIV60:
LD A,H
OR L
JR Z,UI_DIV_DONE ; HL=0 → B=min, HL=0=sec
LD DE,-60 ; 0xFFC4
ADD HL,DE ; HL -= 60; carry set ← HL_orig >= 60
JR NC,UI_DIV_UNDO ; нет carry → HL_orig < 60 → восстановить
INC B ; засчитать минуту
JR UI_DIV60
UI_DIV_UNDO:
LD DE,60
ADD HL,DE ; восстановить HL = остаток (секунды)
UI_DIV_DONE:
LD C,L ; C = секунды (0-59)
; --- Записать MM в TIME_BUF+6,7 ---
LD A,B
LD HL,TIME_BUF+6
CALL UI_BYTE2DEC
; --- Записать SS в TIME_BUF+9,10 ---
LD A,C
LD HL,TIME_BUF+9
CALL UI_BYTE2DEC
; --- Отобразить ---
LD IX,PLWND
LD D,6
LD E,2
LD HL,TIME_BUF
CALL TXTPR
POP HL
POP DE
POP BC
POP AF
RET
; UI_BYTE2DEC: конвертировать A (0-99) в 2 ASCII-цифры по адресу (HL)
; Меняет: A, F
UI_BYTE2DEC:
LD B,0
UI_B2D_L:
CP 10
JR C,UI_B2D_END
SUB 10
INC B
JR UI_B2D_L
UI_B2D_END:
; B=десятки, A=единицы
PUSH AF
LD A,B
ADD A,'0'
LD (HL),A
INC HL
POP AF
ADD A,'0'
LD (HL),A
RET
; ═══════════════════════════════════════════════════════════════════════════
; UI_REMOVE - Закрыть окно плеера (восстановить экран)
; ═══════════════════════════════════════════════════════════════════════════
UI_REMOVE:
PUSH AF
PUSH BC
PUSH DE
PUSH HL
LD IX,PLWND ; IX = SOW (обязательно для RRESB!)
CALL RRESB ; восстановить экран под окном
POP HL
POP DE
POP BC
POP AF
RET
; ═══════════════════════════════════════════════════════════════════════════
; SHOW_ERROR / SHOW_WARNING - Заглушка (не реализовано)
; ═══════════════════════════════════════════════════════════════════════════
SHOW_ERROR:
SHOW_WARNING:
RET
; ═══════════════════════════════════════════════════════════════════════════
; Строки UI (завершаются нулём — формат WC TXTPR)
; ═══════════════════════════════════════════════════════════════════════════
STR_TITLE: DB "VGM Player v2.0 (OPL3)",0
STR_KEYS: DB "ESC-Exit SPC-Next P-Prev M-Pause",0
STR_PLAYING: DB "Playing... ",0
STR_PAUSED: DB "** PAUSED ** ",0
; TIME_BUF: динамический буфер для отображения времени (изменяется в UI_UPDATE)
TIME_BUF: DB "Time: 00:00",0
+615
View File
@@ -0,0 +1,615 @@
; ═══════════════════════════════════════════════════════════════════════════
; VGM Parser & Buffer Filler
; ═══════════════════════════════════════════════════════════════════════════
;
; - VGM_PARSE_HEADER: Parse VGM file header from VGMBUF
; - VGM_FILL_BUFFER: Fill a command buffer (A=0 for BUF_A, A=1 for BUF_B)
; Reads VGM stream, converts to ISR commands
;
; VGM opcodes processed:
; 0x5E rr dd - YMF262 port 0 write (addr=rr, data=dd)
; 0x5F rr dd - YMF262 port 1 write (addr=rr, data=dd)
; 0x61 nn nn - Wait N samples (16-bit LE)
; 0x62 - Wait 735 samples (1/60 sec)
; 0x63 - Wait 882 samples (1/50 sec)
; 0x66 - End of sound data
; 0x70-0x7F - Wait 1-16 samples
;
; Command buffer format (4 bytes each, aligned):
; CMD_WAIT (0): [0x00, ticks_lo, ticks_hi, 0x00]
; CMD_WRITE (1): [0x01, port, value, 0x00]
; CMD_END (FF): [0xFF, 0x00, 0x00, 0x00]
;
; Wait counter mapping: VGM "wait N samples" → ISR wait = N-1
; (because the ISR tick that processes the WAIT command is tick 0,
; and counter=0 means "process next tick")
; ═══════════════════════════════════════════════════════════════════════════
; ═══════════════════════════════════════════════════════════════════════════
; VGM_PARSE_HEADER
; ═══════════════════════════════════════════════════════════════════════════
; Parse VGM header at VGMBUF.
; Output: Z = success, NZ = error
VGM_PARSE_HEADER:
PUSH HL
PUSH DE
PUSH BC
; Check "Vgm " signature
LD HL,VGMBUF
LD A,(HL)
CP 'V'
JP NZ,VGM_PARSE_FAIL
INC HL
LD A,(HL)
CP 'g'
JP NZ,VGM_PARSE_FAIL
INC HL
LD A,(HL)
CP 'm'
JP NZ,VGM_PARSE_FAIL
INC HL
LD A,(HL)
CP ' '
JP NZ,VGM_PARSE_FAIL
; Read total samples (offset 0x18, 32-bit)
LD HL,VGMBUF+#18
LD E,(HL)
INC HL
LD D,(HL)
LD (VGM_TOTAL_SAMPLES),DE
INC HL
LD E,(HL)
INC HL
LD D,(HL)
LD (VGM_TOTAL_SAMPLES+2),DE
; Read OPL3 clock (offset 0x5C, 32-bit) - must be non-zero
LD HL,VGMBUF+#5C
LD E,(HL)
INC HL
LD D,(HL)
LD (VGM_CLOCK),DE
INC HL
LD E,(HL)
INC HL
LD D,(HL)
LD (VGM_CLOCK+2),DE
; Check OPL3 clock != 0 (at least low word should be non-zero for OPL3)
LD HL,(VGM_CLOCK)
LD A,H
OR L
JR NZ,VGM_HAS_OPL3
LD HL,(VGM_CLOCK+2)
LD A,H
OR L
; OPL3 clock = 0: no OPL3 data, but well allow it (might still have data)
VGM_HAS_OPL3:
; Read data offset (offset 0x34, relative to 0x34)
; For VGM version < 1.50, data starts at 0x40
LD HL,VGMBUF+#08 ; version
LD A,(HL) ; version low byte (BCD minor)
LD B,A
INC HL
LD A,(HL) ; version byte 2
; Check if version >= 1.50
; Version bytes: [08]=minor_lo, [09]=minor_hi, [0A]=major_lo, [0B]=major_hi
; For v1.50: bytes are 50 01 00 00 or similar
; Simplified: if offset 0x34 has non-zero value and version >= 1.50, use it
LD HL,VGMBUF+#34
LD E,(HL)
INC HL
LD D,(HL)
; data_offset = value at 0x34 (relative to 0x34)
; If 0, default to 0x0C (data starts at 0x40 = 0x34+0x0C)
LD A,D
OR E
JR NZ,VGM_USE_DATA_OFF
LD DE,#0C ; default: 0x40 - 0x34 = 0x0C
VGM_USE_DATA_OFF:
; VGM_DATA_START = VGMBUF + 0x34 + data_offset
LD HL,VGMBUF+#34
ADD HL,DE
LD (VGM_DATA_START),HL
LD (VGM_READ_PTR),HL
; Read loop offset (offset 0x1C, relative to 0x1C)
LD HL,VGMBUF+#1C
LD E,(HL)
INC HL
LD D,(HL)
LD A,D
OR E
JR Z,VGM_NO_LOOP
; VGM_LOOP_ADDR = #C000 + (abs_offset & #3FFF), VGM_LOOP_PAGE = abs_offset >> 14
; abs_offset = #1C + value_at_1C (16-битное; достаточно для loop-поинтов < 64KB от начала)
LD HL,#001C
ADD HL,DE ; HL = абсолютное смещение точки петли
LD A,H
SRL A:SRL A:SRL A:SRL A:SRL A:SRL A ; A = H >> 6 = номер VPL-страницы
LD (VGM_LOOP_PAGE),A
LD A,H
AND #3F
OR #C0
LD H,A ; HL = #C000 + (offset & #3FFF)
LD (VGM_LOOP_ADDR),HL
JR VGM_LOOP_DONE
VGM_NO_LOOP:
LD HL,0
LD (VGM_LOOP_ADDR),HL ; 0 = нет петли
VGM_LOOP_DONE:
; VGM_END_ADDR и VGM_END_PAGE вычисляются в PLUGIN при загрузке файла
; Success
POP BC
POP DE
POP HL
XOR A ; Z = success
RET
VGM_PARSE_FAIL:
POP BC
POP DE
POP HL
OR 1 ; NZ = error
RET
; ═══════════════════════════════════════════════════════════════════════════
; VGM_FILL_BUFFER
; ═══════════════════════════════════════════════════════════════════════════
; Fill a command buffer with ISR commands from VGM stream.
; Input: A = buffer index (0=BUF_A, 1=BUF_B)
; Reads from VGM_READ_PTR, updates it.
; ═══════════════════════════════════════════════════════════════════════════
VGM_FILL_BUFFER:
PUSH HL
PUSH DE
PUSH BC
PUSH IX
; If song ended or paused, fill with silence
LD B,A ; save buffer index
LD A,(SONG_ENDED)
OR A
JP NZ,VGM_FILL_SILENCE
LD A,(PAUSED)
OR A
JP NZ,VGM_FILL_SILENCE
LD A,B ; restore buffer index
; Get buffer base address
OR A
JR NZ,VGM_FILL_USE_B
LD IX,CMD_BUF_A
JR VGM_FILL_START
VGM_FILL_USE_B:
LD IX,CMD_BUF_B
VGM_FILL_START:
; ВАЖНО: WC API (напр. UI_UPDATE) могла переключить страницу #C000!
; Восстановить правильную VPL-страницу перед чтением VGM.
PUSH IX ; MNGCVPL (WLD) может затереть IX
LD A,(VGM_CUR_PAGE)
CALL MNGCVPL ; отобразить нужную страницу VGM на #C000
POP IX
; DE = write pointer (offset from buffer start)
LD DE,0 ; offset 0
LD HL,(VGM_READ_PTR) ; HL = VGM read pointer
VGM_FILL_LOOP:
; Check if buffer is almost full (leave room for END_BUF + 1 WAIT)
; Buffer = 512 bytes, reserve last 8 bytes
; When D>=1 offset>=256, close immediately
; When D=0, close if E >= 248 (#F8)
LD A,D
OR A
JP NZ,VGM_FILL_CLOSE ; offset >= 256, definitely close to end
LD A,E
CP #F8 ; 248 - leave room in low page
JP NC,VGM_FILL_CLOSE
; Проверка конца VGM: сравниваем CUR_PAGE с END_PAGE, затем HL с END_ADDR
PUSH DE
LD A,(VGM_CUR_PAGE)
LD D,A
LD A,(VGM_END_PAGE)
CP D ; A=END_PAGE, D=CUR_PAGE
JR C,VGM_FILL_AT_END ; END_PAGE < CUR_PAGE → прошли конец
JR NZ,VGM_FILL_NOT_END ; END_PAGE > CUR_PAGE → ещё не конец
; На последней странице: сравниваем HL с VGM_END_ADDR
LD DE,(VGM_END_ADDR)
LD A,H
CP D
JR C,VGM_FILL_NOT_END ; H < D
JR NZ,VGM_FILL_AT_END ; H > D
LD A,L
CP E
JR C,VGM_FILL_NOT_END ; L < E
VGM_FILL_AT_END:
POP DE
; Song ended
LD A,1
LD (SONG_ENDED),A
JP VGM_FILL_CLOSE
VGM_FILL_NOT_END:
POP DE
; Read next VGM opcode (+ проверка границы страницы)
LD A,(HL)
INC HL
CALL VGM_CHK_PAGE
; === 0x5E: YMF262 port 0 write (addr, data) ===
CP #5E
JP Z,VGM_OP_OPL_PORT0
; === 0x5F: YMF262 port 1 write (addr, data) ===
CP #5F
JP Z,VGM_OP_OPL_PORT1
; === 0x61: Wait N samples (16-bit) ===
CP #61
JP Z,VGM_OP_WAIT_N
; === 0x62: Wait 735 samples (1/60 sec) ===
CP #62
JP Z,VGM_OP_WAIT_60HZ
; === 0x63: Wait 882 samples (1/50 sec) ===
CP #63
JP Z,VGM_OP_WAIT_50HZ
; === 0x66: End of sound data ===
CP #66
JP Z,VGM_OP_END
; === 0x70-0x7F: Wait 1-16 samples ===
LD B,A
AND #F0
CP #70
JP Z,VGM_OP_WAIT_SHORT
; Unknown opcode: try to skip it
; Most VGM opcodes are 1-3 bytes. Common skips:
LD A,B ; restore opcode
; 0x3x, 0x4x - single byte (reserved)
CP #50
JP C,VGM_FILL_LOOP ; < 0x50: skip 1 byte total (already read)
; 0x5x - 3 bytes total, we need to skip 2 more
AND #F0
CP #50
JR Z,VGM_SKIP2
; 0xAx, 0xBx - 3 bytes
LD A,B
AND #F0
CP #A0
JR Z,VGM_SKIP2
CP #B0
JR Z,VGM_SKIP2
; 0xCx, 0xDx - 4 bytes
CP #C0
JR Z,VGM_SKIP3
CP #D0
JR Z,VGM_SKIP3
; 0xEx, 0xFx - 5 bytes
CP #E0
JR Z,VGM_SKIP4
CP #F0
JR Z,VGM_SKIP4
; Default: skip nothing more
JP VGM_FILL_LOOP
VGM_SKIP2:
INC HL
CALL VGM_CHK_PAGE
INC HL
CALL VGM_CHK_PAGE
JP VGM_FILL_LOOP
VGM_SKIP3:
INC HL
CALL VGM_CHK_PAGE
INC HL
CALL VGM_CHK_PAGE
INC HL
CALL VGM_CHK_PAGE
JP VGM_FILL_LOOP
VGM_SKIP4:
INC HL
CALL VGM_CHK_PAGE
INC HL
CALL VGM_CHK_PAGE
INC HL
CALL VGM_CHK_PAGE
INC HL
CALL VGM_CHK_PAGE
JP VGM_FILL_LOOP
; --- OPL3 port 0 write: emit WRITE addr0, WRITE data0 ---
VGM_OP_OPL_PORT0:
; Read register number and value
LD B,(HL) ; register number
INC HL
CALL VGM_CHK_PAGE
LD C,(HL) ; value
INC HL
CALL VGM_CHK_PAGE
; Emit: WRITE_B0 reg+val (банк 0)
PUSH HL
PUSH IX
POP HL ; HL = buffer base
ADD HL,DE ; HL = current write position
LD (HL),CMD_WRITE_B0 ; command
INC HL
LD (HL),B ; register number
INC HL
LD (HL),C ; value
INC HL
LD (HL),0 ; padding
POP HL ; restore VGM read ptr
; Advance write offset by 4
LD A,E
ADD A,4
LD E,A
JP NC,VGM_FILL_LOOP
INC D
JP VGM_FILL_LOOP
; --- OPL3 port 1 write: emit WRITE addr1, WRITE data1 ---
VGM_OP_OPL_PORT1:
LD B,(HL) ; register number
INC HL
CALL VGM_CHK_PAGE
LD C,(HL) ; value
INC HL
CALL VGM_CHK_PAGE
PUSH HL
PUSH IX
POP HL
ADD HL,DE
LD (HL),CMD_WRITE_B1 ; command
INC HL
LD (HL),B ; register number (банк 1)
INC HL
LD (HL),C ; value
INC HL
LD (HL),0 ; padding
POP HL
LD A,E
ADD A,4
LD E,A
JP NC,VGM_FILL_LOOP
INC D
JP VGM_FILL_LOOP
; --- Wait N samples (16-bit LE) ---
; ISR freq = 2734 Hz, VGM samples = 44100 Hz. Factor = 44100/2734 ~ 16.13.
; Approximation: ISR_ticks = VGM_samples >> 4 (divide by 16)
; Ignore delays < 16 samples (too small to represent), emit nothing.
VGM_OP_WAIT_N:
LD C,(HL) ; ticks low
INC HL
CALL VGM_CHK_PAGE
LD B,(HL) ; ticks high
INC HL
CALL VGM_CHK_PAGE
; BC = N samples. Divide by 16: shift right 4.
SRL B
RR C
SRL B
RR C
SRL B
RR C
SRL B
RR C ; ISR_ticks in BC (= N/16)
; If BC == 0 (N < 16): skip
LD A,B
OR C
JP Z,VGM_FILL_LOOP
; ISR_WAIT_CTR = ticks - 1
DEC BC
PUSH HL
PUSH IX
POP HL
ADD HL,DE
LD (HL),CMD_WAIT
INC HL
LD (HL),C ; ticks low
INC HL
LD (HL),B ; ticks high
INC HL
LD (HL),0 ; padding
POP HL
LD A,E
ADD A,4
LD E,A
JP NC,VGM_FILL_LOOP
INC D
JP VGM_FILL_LOOP
; --- Wait 735 samples (1/60 sec) ---
VGM_OP_WAIT_60HZ:
; 735/16 = 45 ISR ticks, -1 = 44 = #2C
PUSH HL
PUSH IX
POP HL
ADD HL,DE
LD (HL),CMD_WAIT
INC HL
LD (HL),#2C ; 44 low
INC HL
LD (HL),#00 ; 44 high
INC HL
LD (HL),0
POP HL
LD A,E
ADD A,4
LD E,A
JP NC,VGM_FILL_LOOP
INC D
JP VGM_FILL_LOOP
; --- Wait 882 samples (1/50 sec) ---
VGM_OP_WAIT_50HZ:
; 882/16 = 55 ISR ticks, -1 = 54 = #36
PUSH HL
PUSH IX
POP HL
ADD HL,DE
LD (HL),CMD_WAIT
INC HL
LD (HL),#36 ; 54 low
INC HL
LD (HL),#00 ; 54 high
INC HL
LD (HL),0
POP HL
LD A,E
ADD A,4
LD E,A
JP NC,VGM_FILL_LOOP
INC D
JP VGM_FILL_LOOP
; --- Wait 1-16 samples (opcode 0x70-0x7F) ---
; N = low nibble (0-15) = wait (N+1) samples.
; (N+1) >> 2: if < 1, skip; otherwise emit WAIT with value-1.
VGM_OP_WAIT_SHORT:
LD A,B ; restore opcode
AND #0F ; N = 0..15, actual wait = N+1 samples
INC A ; A = 1..16 samples
SRL A ; /2
SRL A ; /4
SRL A ; /8
SRL A ; /16
JP Z,VGM_FILL_LOOP ; < 16 samples: skip entirely
; WAIT_CTR = A - 1
DEC A
LD C,A
PUSH HL
PUSH IX
POP HL
ADD HL,DE
LD (HL),CMD_WAIT
INC HL
LD (HL),C ; ticks low
INC HL
LD (HL),0 ; ticks high = 0
INC HL
LD (HL),0
POP HL
LD A,E
ADD A,4
LD E,A
JP NC,VGM_FILL_LOOP
INC D
JP VGM_FILL_LOOP
; --- End of VGM data ---
VGM_OP_END:
LD A,1
LD (SONG_ENDED),A
; Fall through to close buffer
; --- Close buffer: emit CMD_END_BUF ---
VGM_FILL_CLOSE:
LD (VGM_READ_PTR),HL ; save VGM read position
; Write CMD_END_BUF at current write position
PUSH IX
POP HL
ADD HL,DE
LD (HL),CMD_END_BUF
INC HL
LD (HL),0
INC HL
LD (HL),0
INC HL
LD (HL),0
POP IX
POP BC
POP DE
POP HL
RET
; --- Fill silence buffer (for paused/ended state) ---
VGM_FILL_SILENCE:
; Buffer index in B
LD A,B
OR A
JR NZ,VGM_SIL_USE_B
LD IX,CMD_BUF_A
JR VGM_SIL_FILL
VGM_SIL_USE_B:
LD IX,CMD_BUF_B
VGM_SIL_FILL:
; Fill with: WAIT 54 (~20ms at 2734Hz), END_BUF
PUSH IX
POP HL
LD (HL),CMD_WAIT
INC HL
LD (HL),#36 ; 54 low (= ~20ms)
INC HL
LD (HL),#00 ; 54 high
INC HL
LD (HL),0
INC HL
LD (HL),CMD_END_BUF
INC HL
LD (HL),0
INC HL
LD (HL),0
INC HL
LD (HL),0
POP IX
POP BC
POP DE
POP HL
RET
; ─── VGM_CHK_PAGE: проверка границы VPL-страницы после INC HL ─────────────
; Если HL = #0000 (переход #FFFF→#0000), отображает следующую VPL-страницу
; на #C000 и устанавливает HL = #C000.
; Сохраняет A (через PUSH/POP AF). Может изменить HL при смене страницы.
VGM_CHK_PAGE:
PUSH AF
LD A,H
OR L
JR NZ,VGM_CHK_RET
; Граница страницы: VGM_CUR_PAGE++, новая страница → #C000
; WC API (MNGCVPL) может затирать BC и DE — сохраняем их!
PUSH BC
PUSH DE
PUSH IX ; WLD (WC API) затирает IX!
LD A,(VGM_CUR_PAGE)
INC A
LD (VGM_CUR_PAGE),A
CALL MNGCVPL ; A = новый VPL-индекс → отобразить на #C000
POP IX
POP DE
POP BC
LD HL,#C000
VGM_CHK_RET:
POP AF
RET
; ═══════════════════════════════════════════════════════════════════════════
; VGM_FILL_INACTIVE_BUF (called from main loop)
; ═══════════════════════════════════════════════════════════════════════════
; Check which buffer ISR is reading, fill the other one if it changed.
; This is just a wrapper; main loop handles tracking in MAIN_LOOP.
; Kept for potential future use.
+520
View File
@@ -0,0 +1,520 @@
; ═══════════════════════════════════════════════════════════════════════════
; VGM Player Plugin for Wild Commander
; OPL3 (YMF262) playback on TSConfig
; Architecture v2: ISR-driven at 44100 Hz, double-buffered commands
; (C) 2026
; ═══════════════════════════════════════════════════════════════════════════
DEVICE NOSLOT64K ; плоская 64К память, без ограничений слотов
OUTPUT "vgmplay.wmf"
PLUGHEAD EQU 0 ; заголовок в начале файла (как в PLUGBP)
PLUGSTAR EQU PLUGHEAD+512 ; код с байта #200 в файле
PLUGLEN EQU END-PLUGIN
PAGES EQU (PLUGLEN/16384)+1
BLKSIZE EQU (PLUGLEN+511)/512
ORG PLUGHEAD ; = 0, заголовок физически с байта 0 в файле
; ═══════════════════════════════════════════════════════════════════════════
; Plugin Header (512 bytes, exact WC format)
; ═══════════════════════════════════════════════════════════════════════════
; +0: Reserved (16 bytes)
DS 16
; +16: Magic signature (16 bytes)
DB "WildCommanderMDL"
; +32: Plugin system version
DB #10
; +33: Reserved
DB 0
; +34: Number of 16KB pages
DB PAGES
; +35: Start page (0-based, mapped to #8000)
DB 0
; +36: Data blocks (6 x 2 bytes = 12 bytes)
DB 0,BLKSIZE ; Block 0: plugin code, page 0
DB 0,0 ; Block 1: unused
DB 0,0 ; Block 2: unused
DB 0,0 ; Block 3: unused
DB 0,0 ; Block 4: unused
DB 0,0 ; Block 5: unused
; +48: Reserved (15 bytes)
DS 15
; +63: Extension flags (0 = launch by ENTER and F3)
DB 0
; +64: Supported extensions (32 slots x 3 bytes = 96 bytes)
DB "VGM"
DS 31*3 ; Remaining slots empty
; +160: End marker
DB 0
; +161: Max file size (32-bit LE: DW low_word, high_word)
DW #FFFF,#FFFF ; Unlimited
; +165: Plugin name (32 bytes, space-padded)
DB "VGM Player for OPL3 v2.0 "
; +197: Activation type (0 = by extension only)
DB 0
; +198: F2/F4 button text (6 bytes)
DS 6
; +204: Viewer menu text (24 bytes)
DS 24
; +228: Reserved (32 bytes)
DS 32
; +260: INI Parameters
DB 0 ; No parameters
; Pad to 512 bytes
DS PLUGSTAR-$
; Заголовок занимает 512 байт (PLUGSTAR = #200),
; код физически с байта #200, WC отображает страницу на #8000.
; DISP #8000 — все метки кода резолвятся в диапазон #8000+
DISP #8000
; ═══════════════════════════════════════════════════════════════════════════
; Constants
; ═══════════════════════════════════════════════════════════════════════════
WLD EQU #6006 ; WC API entry point
VPL EQU 32 ; WC VPL constant
; --- TSConfig Ports (port = reg_number * 256 + #AF) ---
PORT_SYSCONFIG EQU #20AF ; SysConfig: bit1:0 = CPU freq, bit2 = cache
PORT_HSINT EQU #22AF ; Horizontal INT position (0-223)
PORT_VSINTL EQU #23AF ; Vertical INT position low (bits 7:0)
PORT_VSINTH EQU #24AF ; Vertical INT position high (bit0=VCNT[8], bits7:4=VINT_INC)
PORT_INTMASK EQU #2AAF ; INT mask: bit0=FRAME, bit1=LINE, bit2=DMA
; --- CPU speed values for SysConfig bits 1:0 ---
CPU_3_5MHZ EQU %00000000
CPU_7MHZ EQU %00000001
CPU_14MHZ EQU %00000010
; --- OPL3 Ports ---
OPL3_ADDR0 EQU #C4
OPL3_DATA0 EQU #C5
OPL3_ADDR1 EQU #C6
OPL3_DATA1 EQU #C7
; --- ISR Command Types (код = смещение в странице ISR_HANDLER) ---
CMD_WRITE_B0 EQU #40 ; [#40, reg, val, pad] OPL3 банк 0
CMD_WRITE_B1 EQU #80 ; [#80, reg, val, pad] OPL3 банк 1
CMD_WAIT EQU #C0 ; [#C0, lo, hi, pad] ждать N тиков
CMD_END_BUF EQU #E0 ; [#E0, pad, pad, pad] переключить буфер
; --- Command Buffer sizes ---
CMD_BUF_SIZE EQU 512 ; bytes per buffer (128 commands)
; --- Window layout ---
WND_X EQU 9
WND_Y EQU 5
WND_W EQU 62
WND_H EQU 14
; --- Colors ---
CLR_MAIN EQU %01111111
; --- Коды ошибок для ERROR_BORDER ---
; Формат: биты [5:3]=категория, биты [2:0]=код. Оба поля ≠ 0 (цвета бордюра 1-7)
; Цвета ZX: 1=синий, 2=красный, 3=маджента, 4=зелёный, 5=циан, 6=жёлтый, 7=белый
;
; Категории (старшие 3 бита):
; 1 (синий) = файловые ошибки
; 2 (красный) = ошибки формата VGM
; 3 (маджента) = аппаратные ошибки
;
; Конкретные коды (младшие 3 бита) — внутри категории:
; 1 = первая ошибка, 2 = вторая, и т.д.
;
ERR_FILE_SMALL EQU (1<<3)|1 ; файл <= 255 байт: черный, синий(1), синий(1)
ERR_VGM_HEADER EQU (2<<3)|1 ; плохой заголовок: черный, синий(1), красный(2)
ERR_FILE_LOAD EQU (1<<3)|2 ; ошибка загрузки: черный, красный(2), синий(1)
; --- Memory addresses ---
VGMBUF EQU #C000 ; VGM данные через WC VPL (окно 3: #C000-#FFFF)
; ═══════════════════════════════════════════════════════════════════════════
; Plugin Entry Point
; ═══════════════════════════════════════════════════════════════════════════
PLUGIN:
PUSH IX ; Сохранить IX (WC передаёт структуру активной панели)
; Сохранить входные параметры от WC
; A` = индекс расширения, BC = указатель на имя файла,
; HL = размер файла (младшее слово), DE = размер файла (старшее слово)
EXA
LD (FILE_EXT),A
EXA
LD (FILE_NAME),BC
LD (FILE_SIZE),HL
LD (FILE_SIZE+2),DE
; Init variables
XOR A
LD (EXIT_CODE),A
LD (PAUSED),A
LD (SONG_ENDED),A
LD (ISR_ENABLED),A
LD (ISR_ACTIVE_BUF),A
LD (UNDERRUN_FLAG),A
LD (OPL3_FOUND),A
LD (UI_FRAME_CTR),A
LD (LAST_ISR_BUF),A
LD HL,0
LD (PLAY_SECONDS),HL
; Переключиться на 14 МГц (PORT_SYSCONFIG только на запись)
; При выходе всегда восстанавливаем 3.5 МГц
LD BC,PORT_SYSCONFIG
LD A,CPU_14MHZ
OUT (C),A
; Проверка размера файла: достаточно > 256 байт
; (VGM заголовок минимум 64 байта, но реальные файлы от 8КБ)
LD A,(FILE_SIZE+1) ; старший байт младшего слова
OR A
LD A,ERR_FILE_SMALL
JP Z,ERROR_BORDER ; <= 255 байт → ошибка
FILE_SIZE_OK:
; === Загрузить VGM файл в VPL-страницы WC (окно #C000, 16KB каждая) ===
; Шаг 1: вычислить число страниц B = ceil(FILE_SIZE / 16384)
LD HL,(FILE_SIZE)
LD DE,(FILE_SIZE+2)
LD BC,16383 ; добавляем для округления вверх
ADD HL,BC
JR NC,VGM_LOAD_NC
INC DE
VGM_LOAD_NC:
; pages = (E << 2) | (H >> 6) [файлы < 4МБ]
LD A,H
SRL A:SRL A:SRL A:SRL A:SRL A:SRL A ; A = H >> 6
LD C,A
LD A,E
ADD A,A:ADD A,A
OR C
LD B,A ; B = число страниц
; Шаг 2: вычислить VGM_END_PAGE и VGM_END_ADDR
DEC A ; номер последней страницы (0-based)
LD (VGM_END_PAGE),A
; VGM_END_ADDR = #C000 + (FILE_SIZE & #3FFF)
LD HL,(FILE_SIZE)
LD A,H
AND #3F
OR #C0
LD H,A
LD (VGM_END_ADDR),HL
; Шаг 3: загружать страницы по одной
LD A,B
LD (VGM_PAGES_TOTAL),A
XOR A
LD (VGM_CUR_PAGE),A
VGM_LOAD_LOOP:
LD A,(VGM_PAGES_TOTAL)
LD B,A
LD A,(VGM_CUR_PAGE)
CP B
JR NC,VGM_LOAD_DONE
CALL MNGCVPL ; A = VPL-индекс страницы → отобразить на #C000
LD HL,#C000
LD B,32 ; 32 × 512 = 16384 байт
CALL LOAD512
LD A,(VGM_CUR_PAGE)
INC A
LD (VGM_CUR_PAGE),A
JR VGM_LOAD_LOOP
VGM_LOAD_DONE:
; Отобразить страницу 0 для разбора заголовка
XOR A
LD (VGM_CUR_PAGE),A
CALL MNGCVPL
; Разобрать VGM заголовок
CALL VGM_PARSE_HEADER
LD A,ERR_VGM_HEADER
JP NZ,ERROR_BORDER
; Init OPL3
CALL OPL3_INIT
; Восстановить дисплей (ОБЯЗАТЕЛЬНО перед любыми вызовами WC UI API)
CALL GEDPL
; Init UI (uses WC API, still in normal interrupt mode)
CALL UI_INIT
CALL UI_DRAW_INFO
; Disable WC interrupts (A'=0 via INT_PL)
; WC will auto-restore on plugin exit
XOR A
EXA ; A' = 0
CALL INT_PL
; Prepare initial data in both buffers (до ISR_INIT!)
LD A,0
CALL VGM_FILL_BUFFER ; fill BUF_A
LD A,1
CALL VGM_FILL_BUFFER ; fill BUF_B
; Set ISR read pointer to start of BUF_A
LD HL,CMD_BUF_A
LD (ISR_READ_PTR),HL
; Enable ISR processing
LD A,1
LD (ISR_ENABLED),A
; Setup IM2 + start timer (после заполнения буферов!)
CALL ISR_INIT
EI
; ═══════════════════════════════════════════════════════════════════════════
; Main Loop
; ═══════════════════════════════════════════════════════════════════════════
MAIN_LOOP:
EI ; гарантируем прерывания (WC API мог сделать DI)
; 1. Check if ISR switched buffers -> fill the free one
LD A,(ISR_ACTIVE_BUF)
LD B,A
LD A,(LAST_ISR_BUF)
CP B
JR Z,ML_NO_FILL ; same buffer, nothing to do
; ISR switched! Update tracking and fill the now-free buffer
LD A,B
LD (LAST_ISR_BUF),A
; The free buffer is the one ISR is NOT reading
XOR 1 ; 0->1, 1->0 (A = free buffer index)
LD C,A ; save free buffer index
LD A,1:OUT (#FE),A ; DEBUG: синий → входим в VGM_FILL_BUFFER
LD A,C ; restore free buffer index
CALL VGM_FILL_BUFFER
XOR A:OUT (#FE),A ; DEBUG: чёрный → VGM_FILL_BUFFER вернулся
ML_NO_FILL:
; 2. Update UI periodically
LD A,(UI_FRAME_CTR)
INC A
LD (UI_FRAME_CTR),A
AND #1F ; every 32 iterations
JR NZ,ML_NO_UI
CALL UI_UPDATE
ML_NO_UI:
; 3. Read keyboard (direct ZX matrix, no interrupts needed)
CALL KBD_READ
OR A
JR NZ,GOT_KEY
; 4. Check if song ended
LD A,(SONG_ENDED)
OR A
JP NZ,SONG_DONE
JP MAIN_LOOP
GOT_KEY:
; A = action code: 1=ESC, 2=next, 3=prev, 4=pause
CP 1
JR Z,DO_EXIT
CP 2
JR Z,DO_NEXT
CP 3
JR Z,DO_PREV
CP 4
JR Z,DO_PAUSE
JR MAIN_LOOP
DO_EXIT:
XOR A
LD (EXIT_CODE),A
JR EXIT_PLAYER
DO_NEXT:
LD A,2
LD (EXIT_CODE),A
JR EXIT_PLAYER
DO_PREV:
LD A,4
LD (EXIT_CODE),A
JR EXIT_PLAYER
DO_PAUSE:
LD A,(PAUSED)
XOR 1
LD (PAUSED),A
; When pausing, ISR keeps running but main loop stops filling buffers
; When unpausing, ISR was just replaying silence/waits
CALL UI_DRAW_STATUS
JP MAIN_LOOP
SONG_DONE:
; Проверить петлю (VGM_LOOP_ADDR = 0 → нет петли)
LD HL,(VGM_LOOP_ADDR)
LD A,H
OR L
JR Z,SONG_NO_LOOP
; Переключиться на страницу петли и перезапустить
LD A,(VGM_LOOP_PAGE)
LD (VGM_CUR_PAGE),A
CALL MNGCVPL
LD HL,(VGM_LOOP_ADDR)
LD (VGM_READ_PTR),HL
XOR A
LD (SONG_ENDED),A
JP MAIN_LOOP
SONG_NO_LOOP:
LD A,2 ; next file
LD (EXIT_CODE),A
; fall through
; ═══════════════════════════════════════════════════════════════════════════
; Exit
; ═══════════════════════════════════════════════════════════════════════════
EXIT_PLAYER:
DI
; Stop ISR
XOR A
LD (ISR_ENABLED),A
; Silence OPL3
CALL OPL3_RESET
; Restore IM1
IM 1
EI
; Восстановить 3.5 МГц
LD BC,PORT_SYSCONFIG
LD A,CPU_3_5MHZ
OUT (C),A
; Remove UI
CALL UI_REMOVE
; Return exit code to WC
LD A,(EXIT_CODE)
POP IX ; Восстановить IX для WC
RET
ERROR_EXIT:
; Восстановить 3.5 МГц
LD BC,PORT_SYSCONFIG
LD A,CPU_3_5MHZ
OUT (C),A
XOR A
POP IX ; Восстановить IX для WC
RET
; ─── Отладка: вывод кода ошибки бордюром, зависание ───────────────────
; Вход: A = код ошибки (биты [2:0] = младший, [5:3] = старший; оба ≠ 0)
; Цикл: чёрный → цвет(биты[2:0]) → цвет(биты[5:3]) → повтор
; Задержка каждого цвета ≈ 300мс при 14МГц
; TODO: убрать в релизной версии
ERROR_BORDER:
LD C,A ; C = код ошибки на всё время
ERR_LOOP:
; 1. Чёрный бордюр (разделитель)
XOR A
OUT (#FE),A
CALL ERR_DELAY
; 2. Младшие 3 бита → цвет
LD A,C
AND %00000111
OUT (#FE),A
CALL ERR_DELAY
; 3. Биты [5:3] → цвет (сдвиг вправо на 3)
LD A,C
RRCA
RRCA
RRCA
AND %00000111
OUT (#FE),A
CALL ERR_DELAY
JR ERR_LOOP
ERR_DELAY:
LD D,4
ERR_DLY_OUT:
LD B,0
ERR_DLY_INN:
DJNZ ERR_DLY_INN
DEC D
JR NZ,ERR_DLY_OUT
RET
; ═══════════════════════════════════════════════════════════════════════════
; WC API Wrappers
; CALL WLD + IM 2 + RET: IM2 восстанавливается после каждого вызова WC
; (WC может переключать процессор обратно на IM1)
; ═══════════════════════════════════════════════════════════════════════════
PRWOW: LD A,1: CALL WLD:IM 2:EI:RET ; Print window
RRESB: LD A,2: CALL WLD:IM 2:EI:RET ; Remove window
PRSRW: LD A,3: CALL WLD:IM 2:EI:RET ; Print string in window
PRIAT: EXA:LD A,4:CALL WLD:IM 2:EI:RET ; Print attributes
TXTPR: LD A,11: CALL WLD:IM 2:EI:RET ; Print text
GEDPL: LD A,15: CALL WLD:IM 2:EI:RET ; Get display
LOAD512:LD A,#30:CALL WLD:IM 2:EI:RET ; Load 512-byte blocks
MNGCVPL:EXA:LD A,#41:CALL WLD:IM 2:EI:RET; Map VPL page to #C000
INT_PL: EXA:LD A,86:CALL WLD:IM 2:EI:RET ; Interrupt control
; ═══════════════════════════════════════════════════════════════════════════
; Global Variables
; ═══════════════════════════════════════════════════════════════════════════
FILE_EXT: DB 0
FILE_NAME: DW 0
FILE_SIZE: DW 0,0 ; 32-bit little-endian
EXIT_CODE: DB 0
PAUSED: DB 0
SONG_ENDED: DB 0
; VGM header data
VGM_DATA_START: DW 0 ; адрес первого байта VGM команд (#C000-#FFFF)
VGM_LOOP_ADDR: DW 0 ; адрес точки петли (#C000-#FFFF; 0 = нет петли)
VGM_LOOP_PAGE: DB 0 ; VPL-страница (0-based) точки петли
VGM_READ_PTR: DW 0 ; текущий адрес чтения в #C000-#FFFF
VGM_CUR_PAGE: DB 0 ; текущая VPL-страница отображённая на #C000
VGM_END_ADDR: DW 0 ; адрес конца данных в последней странице
VGM_END_PAGE: DB 0 ; номер последней страницы (VPL, 0-based)
VGM_PAGES_TOTAL: DB 0 ; суммарное число загруженных страниц
VGM_TOTAL_SAMPLES: DW 0,0 ; 32-bit total samples
VGM_CLOCK: DW 0,0 ; OPL3 clock value
; Playback time
PLAY_SECONDS: DW 0 ; секунды воспроизведения (обновляется в ISR)
; ISR state (shared between ISR and main loop)
ISR_ENABLED: DB 0 ; 0=ISR does nothing, 1=active
ISR_ACTIVE_BUF: DB 0 ; 0=reading BUF_A, 1=reading BUF_B
ISR_READ_PTR: DW 0 ; current read pointer in active buffer
ISR_WAIT_CTR: DW 0 ; tick countdown (0=process commands)
; ISR_STEP_IDX, ISR_HPOS, ISR_LINE are in isr.asm (local to ISR)
LAST_ISR_BUF: DB 0 ; main loop tracking
UNDERRUN_FLAG: DB 0
; OPL3 state
OPL3_FOUND: DB 0
; UI
UI_FRAME_CTR: DB 0
; ═══════════════════════════════════════════════════════════════════════════
; Include Modules
; ═══════════════════════════════════════════════════════════════════════════
INCLUDE "src/isr.asm"
INCLUDE "src/vgm.asm"
INCLUDE "src/opl3.asm"
INCLUDE "src/ui.asm"
INCLUDE "src/keyboard.asm"
END: