Add OPL4 playback and analyzer support
This commit is contained in:
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
Reference in New Issue
Block a user