; ═══════════════════════════════════════════════════════════════════════════ ; 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.