Files
VGMPLAY.WMF/old_asm_src/vgmplayer.asm
T

521 lines
20 KiB
NASM
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
; ═══════════════════════════════════════════════════════════════════════════
; 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: