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
+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: