Files
VGMPLAY.WMF/src_sdcc/inc/vgm.h
T
alex 54112347f5 Add large-file (>1MB) OPL4 loading and fix track-end hang
- One-shot cold loader (asm/opl4_loader.asm, overlaid into plugin
  page 5) uploads embedded PCM sample blocks into ZXM-MoonSound SRAM
  and compacts the VGM stream in place, letting files over the normal
  64-page megabuffer budget play back through the existing pipeline.
- instant_abort() was filling only one of the two ISR command buffers
  with the shutdown chain; the chain (SILENCE_OPL + WAVE_OFF/TL +
  CMD_ISR_DONE) doesn't fit in one buffer, so after the first
  CMD_END_BUF the ISR swapped into stale data and never reached
  CMD_ISR_DONE, hanging at the end of long tracks instead of
  advancing to the next one. Now fills both buffers while frozen and
  syncs update_buffer()'s own ready-tracking state.
- Switch to 14MHz before the SRAM upload (previously ran at whatever
  speed WC's file I/O left active) and show "Loading..." for .vgz and
  files >= 1MB, since the busy-waited upload takes several seconds.
- Confirmed on real hardware: long OPL4 tracks (previously stuck
  looping at the end) now finish and advance normally, ESC exits
  cleanly.
2026-09-16 20:14:06 +03:00

334 lines
19 KiB
C
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.
/**
* @file vgm.h
* @brief VGM-парсер для OPL1/OPL2/OPL3 — заголовок + потоковое чтение
* @version 3.0
*
* Зависимости: types.h, isr.h (cmd_buf_a/b), wc_api.h (wc_mngcvpl).
* Реализация: lib/vgm.c.
*
* ═══════════════════════════════════════════════════════════════════
* ОПИСАНИЕ
* ═══════════════════════════════════════════════════════════════════
*
* Преобразует VGM-поток в ISR-команды (CMD_WRITE_B0/B1, CMD_WAIT,
* CMD_END_BUF). VGM данные хранятся в VPL-страницах WC, доступных
* через окно #C000.
*
* Поддерживаемые VGM-команды (OPL1 / OPL2 / OPL3 / OPL4 / AY / SAA):
* 0x5A rr dd — YM3812 (OPL2) write → CMD_WRITE_B0
* 0x5B rr dd — YM3526 (OPL1) write → CMD_WRITE_B0
* 0x5C rr dd — Y8950 (OPL) write → CMD_WRITE_B0
* 0x5E rr dd — YMF262 (OPL3) Bank 0 write → CMD_WRITE_B0
* 0x5F rr dd — YMF262 (OPL3) Bank 1 write → CMD_WRITE_B1
* 0xD0 pp aa dd — YMF278B (OPL4) write, port pp (бит7 = chip select, игн.):
* pp&0x7F==0 → Bank 0 (как 0x5E) → CMD_WRITE_B0
* pp&0x7F==1 → Bank 1 (как 0x5F) → CMD_WRITE_B1
* pp&0x7F==2 → Wave-часть (#7E/#7F) → CMD_WRITE_WAVE
* 0x55 rr dd — YM2203 PSG-часть (AY), reg 0-15 → CMD_WRITE_AY
* 0xA0 rr dd — AY8910 (чип 1/2 по [7]) → CMD_WRITE_AY / CMD_WRITE_AY2
* 0xBD rr dd — SAA1099 (чип 1/2 по [7]) → CMD_WRITE_SAA (bit7 = chip select)
* 0x61 nn nn — Wait N samples (16-bit LE)
* 0x62 — Wait 735 samples (1/60 сек)
* 0x63 — Wait 882 samples (1/50 сек)
* 0x66 — End of data
* 0x67 ... — Data block (пропускается)
* 0x70–0x7F — Wait 1–16 samples
* Прочие — пропускаются по таблице VGM spec (1-байт / 2-байт / 3-байт аргументы)
*
* Масштабирование задержек:
* VGM sample rate = 44100 Hz, ISR freq = 2734 Hz -> делим на 16.
* Задержки < 16 сэмплов игнорируются (округление вниз).
* Неизвестные команды пропускаются по таблице размеров VGM spec.
*
* ═══════════════════════════════════════════════════════════════════
* VGM ЗАГОЛОВОК — ключевые смещения (little-endian)
* ═══════════════════════════════════════════════════════════════════
*
* Offset Size Описание Мин.версия
* ------ ---- -------------------------------- ----------
* 0x00 4 Сигнатура "Vgm " (0x206D6756) 1.00
* 0x08 4 Версия (BCD): 0x0151 = v1.51 1.00
* 0x14 4 GD3 offset (отн. 0x14) 1.00
* 0x18 4 Total samples 1.00
* 0x1C 4 Loop offset (отн. 0x1C) 1.00
* 0x20 4 Loop samples 1.00
* 0x34 4 Data offset (отн. 0x34) 1.50
* 0x50 4 YM3812 (OPL2) clock 1.51
* 0x54 4 YM3526 (OPL1) clock 1.51
* 0x58 4 Y8950 (OPL) clock 1.51
* 0x5C 4 YMF262 (OPL3) clock 1.51
*
* Для версий < 1.50: data offset фиксирован = 0x40.
* Для версий < 1.51: поля чипов за пределами заголовка.
*
* ~93% OPL VGM файлов — YM3812 (OPL2), команда 0x5A.
* ~5% — YMF262 (OPL3), команды 0x5E/0x5F.
*
* ═══════════════════════════════════════════════════════════════════
* ПРИМЕР ИСПОЛЬЗОВАНИЯ
* ═══════════════════════════════════════════════════════════════════
*
* #include "vgm.h"
* #include "isr.h"
* #include "wc_api.h"
*
* // 1. Загрузить VGM в VPL-страницы
* wc_mngcvpl(0);
* wc_load512(0xC000, 32); // 16 КБ
*
* // 2. Разобрать заголовок
* if (vgm_parse_header() != VGM_OK) return;
*
* // 3. Заполнить оба буфера ISR
* vgm_fill_buffer(0); // буфер A
* vgm_fill_buffer(1); // буфер B
*
* // 4. В main loop:
* if (cur_buf != last_buf) {
* vgm_fill_buffer(cur_buf ^ 1); // свободный
* last_buf = cur_buf;
* }
* if (vgm_song_ended && !vgm_rewind_to_loop()) break;
*/
#ifndef VGM_H
#define VGM_H
#include "types.h"
/* ── Масштабирование частот PSG под реальное железо ─────────────────── */
#define FREQ_MODE_NATIVE 0 /* HW clock ≈ VGM clock (±2%), без пересчёта */
#define FREQ_MODE_TABLE 1 /* LUT table match (±5%) */
/** Текущий режим масштабирования (FREQ_MODE_xxx) */
extern uint8_t vgm_freq_mode;
/** Базовый адрес LUT таблицы в Window 0 (#0000-#3FFF).
* Используется в TABLE mode: freq_lut_base[period] → scaled period.
* NULL (0) если режим не TABLE.
* PSG: индекс 0..4095 (12-bit).
* Таблица в plugin page N, подключённой через wc_mng0_pl(N). */
extern uint16_t *freq_lut_base;
/** Clock (кГц) подобранной LUT таблицы (для отображения в UI).
* 0 если режим NATIVE. */
extern uint16_t vgm_freq_lut_khz;
/* ══════════════════════════════════════════════════════════════════════
* ID чипов = смещение clock-поля в VGM-заголовке.
* Имена определены только для поддерживаемых чипов.
* ══════════════════════════════════════════════════════════════════════ */
#define VGM_OFF_SN76489 0x0C /* PSG SN76489 / SN76496 */
#define VGM_OFF_YM2413 0x10 /* OPLL */
#define VGM_OFF_YM2203 0x44 /* OPN (PC-88) */
#define VGM_OFF_YM3812 0x50 /* OPL2 (AdLib / SB) */
#define VGM_OFF_YM3526 0x54 /* OPL (ISA AdLib) */
#define VGM_OFF_Y8950 0x58 /* MSX-AUDIO / OPL */
#define VGM_OFF_YMF262 0x5C /* OPL3 (SB Pro 2) */
#define VGM_OFF_YMF278B 0x60 /* OPL4 */
#define VGM_OFF_AY8910 0x74 /* AY-3-8910 / YM2149 */
#define VGM_OFF_SAA1099 0xC8 /* SAA1099 (SAM Coupé) */
/* ── Тип OPL-чипа (обратная совместимость) ───────────────────────── */
#define VGM_CHIP_NONE 0
#define VGM_CHIP_OPL 1 /* YM3526 (OPL1) + Y8950: cmd 0x5B/0x5C */
#define VGM_CHIP_OPL2 2 /* YM3812 (OPL2): cmd 0x5A */
#define VGM_CHIP_OPL3 3 /* YMF262 (OPL3): cmd 0x5E/5F */
#define VGM_CHIP_OPL4 4 /* YMF278B (OPL4): cmd 0xD0 (FM порты 0/1 —
* те же регистры, что и OPL3; порт 2 —
* wave-часть через #7E/#7F, ZXM-MoonSound) */
/* Приоритет определения типа: YMF278B > YMF262 > YM3812 > YM3526/Y8950
* Используется в build_playback_queue() (main.c) для выбора cmdblock init:
* VGM_CHIP_OPL4 → CMDBLK_INIT_OPL4 (NEW=1 + NEW2=1, разблокирует wave-часть)
* VGM_CHIP_OPL3 → CMDBLK_INIT_OPL3 (NEW=1, L/R через C0-C8)
* иначе → CMDBLK_INIT_OPL2 (NEW=0, L+R авто, OPL2-compat) */
/* ── Коды результата ─────────────────────────────────────────────── */
#define VGM_OK 0
#define VGM_ERR_HEADER 1 /* Нет сигнатуры "Vgm " или повреждён */
#define VGM_ERR_NOOPL 2 /* Нет OPL-чипа (clock = 0 для всех) */
/* ── Флаги чипа (vgm_chip_entry_t.flags) ─────────────────────────── */
#define VGM_CF_DUAL 0x01 /* Bit 30 clock: два экземпляра чипа */
/* ══════════════════════════════════════════════════════════════════════
* Структура обнаруженного чипа
* ══════════════════════════════════════════════════════════════════════ */
typedef struct {
uint8_t id; /* VGM_OFF_xxx (header offset of clock) */
uint8_t flags; /* VGM_CF_DUAL и пр. */
uint16_t clock_khz; /* clock / 1000 (0–65535 кГц) */
} vgm_chip_entry_t; /* 4 байта */
#define VGM_MAX_CHIPS 8 /* макс. чипов в vgm_chip_list[] */
/* ── Состояние парсера (глобальные переменные) ───────────────────── */
/** 1 = пауза (буферы заполняются тишиной) */
extern uint8_t vgm_paused;
/** Текущая VPL-страница, отображённая на #C000 */
extern uint8_t vgm_cur_page;
/** Указатель чтения VGM (#C000–#FFFF) */
extern uint16_t vgm_read_ptr;
/** Номер последней VPL-страницы (0-based) */
extern uint8_t vgm_end_page;
/** Адрес конца данных в последней странице */
extern uint16_t vgm_end_addr;
/** Версия VGM (BCD): 0x0151 = v1.51, 0x0171 = v1.71 */
extern uint16_t vgm_version;
/** Тип обнаруженного OPL-чипа (VGM_CHIP_xxx) */
extern uint8_t vgm_chip_type;
/** Массив обнаруженных чипов (для вывода информации о файле) */
extern uint8_t vgm_chip_count;
extern uint8_t vgm_opl4_preloaded_rom;
extern vgm_chip_entry_t vgm_chip_list[VGM_MAX_CHIPS];
/** Адрес точки петли (#C000–#FFFF); 0 = нет петли */
extern uint16_t vgm_loop_addr;
/** VPL-страница точки петли */
extern uint8_t vgm_loop_page;
/** Общая длительность воспроизведения (секунды), с учётом loop rewinds */
extern uint16_t vgm_total_seconds;
/** Флаг: использовать ли loop при воспроизведении.
* Устанавливается в vgm_parse_header() с учётом loop policy. */
extern uint8_t vgm_loop_enabled;
/* ── Loop/duration policy (читаются из INI, устанавливаются main.c) ── */
extern uint8_t cfg_loop_rewinds; /* кол-во повторов петли (default 1) */
extern uint8_t cfg_min_duration; /* ниже этой длительности (сек) петля запрещена (default 10) */
extern uint16_t cfg_max_duration; /* выше этой длительности (сек) петля запрещена (default 240) */
/* ── GD3 метаданные (только English, ASCII) ─────────────────────── */
#define VGM_GD3_LEN 48 /* макс. длина одного поля (+1 для '\0') */
extern char vgm_gd3_track[VGM_GD3_LEN]; /* Название трека (EN) */
extern char vgm_gd3_game[VGM_GD3_LEN]; /* Название игры (EN) */
extern char vgm_gd3_system[VGM_GD3_LEN]; /* Название системы (EN) */
extern char vgm_gd3_author[VGM_GD3_LEN]; /* Автор (EN) */
/* ── Функции ─────────────────────────────────────────────────────── */
/**
* Разобрать заголовок VGM из #C000 (страница 0 должна быть загружена).
* Заполняет vgm_read_ptr, vgm_cur_page, vgm_loop_addr/page,
* vgm_version, vgm_chip_type, vgm_chip_list[]/vgm_chip_count.
* @return VGM_OK (0) или VGM_ERR_HEADER (1)
*/
uint8_t vgm_parse_header(void);
/**
* Заполнить командный буфер buf_idx (0 = A, 1 = B).
*
* Поддерживаемые VGM-команды:
* 0x5A/0x5B/0x5C/0x5E → CMD_WRITE_B0 (OPL1/OPL2/Y8950/OPL3 bank 0)
* 0x5F → CMD_WRITE_B1 (OPL3 bank 1)
* 0xD0 (port 0/1/2) → CMD_WRITE_B0 / CMD_WRITE_B1 / CMD_WRITE_WAVE (YMF278B/OPL4)
* 0x55 → CMD_WRITE_AY (YM2203 PSG-часть, reg 0-15)
* 0xA0 → CMD_WRITE_AY / CMD_WRITE_AY2 (AY8910, dual)
* 0xBD → CMD_WRITE_SAA (SAA1099, bit7 = chip select)
* 0x61/0x62/0x63/0x70-0x7F → CMD_WAIT
* 0x66 → end; 0x67 → data block skip
*
* Гарантирует CMD_END_BUF в конце буфера.
* @param buf_idx 0 = cmd_buf_a, 1 = cmd_buf_b
*/
void vgm_fill_buffer(uint8_t buf_idx);
/**
* Перемотать VGM на точку петли (если установлена).
* @return 1 = петля есть (перемотано), 0 = нет петли
*/
uint8_t vgm_rewind_to_loop(void);
/**
* Разобрать GD3-тег (метаданные) из VGM файла.
* Вызывать после vgm_parse_header().
* Заполняет vgm_gd3_track/game/system/author (ASCII, EN only).
* Переключает VPL-страницу — после вызова нужно восстановить
* vgm_cur_page через wc_mngcvpl().
*/
void vgm_parse_gd3(void);
/**
* Получить имя чипа по header offset (VGM_OFF_xxx).
* @return указатель на строку (ROM), например "YM3812", "AY8910".
* Для неизвестных чипов возвращает NULL.
*/
const char *vgm_chip_name(uint8_t id);
/* ─── VGM-format command blocks (cmdblocks page) ────────────────── */
/** Plugin page содержащая VGM-format command blocks */
#define CMDBLK_PAGE 5
/** Индексы блоков в таблице указателей по адресу 0xC000 */
#define CMDBLK_INIT_OPL3 0 /* OPL3 init: NEW=1, 4-op=0, test=0 */
#define CMDBLK_INIT_OPL2 1 /* OPL2 compat: NEW=0, 4-op=0, test=0 */
#define CMDBLK_SILENCE_OPL 2 /* OPL silence: KeyOff+TL+waveform+BD */
#define CMDBLK_SILENCE_AY 3 /* AY chip 1: mixer off, vol=0 */
#define CMDBLK_SILENCE_AY2 4 /* AY chip 2 (TS): mixer off, vol=0 */
#define CMDBLK_SILENCE_SAA 5 /* SAA1099 chip 1: reset + amp=0 */
#define CMDBLK_SILENCE_SAA2 6 /* SAA1099 chip 2: reset + amp=0 */
#define CMDBLK_SAA_CLK_ON 7 /* MultiSound: SAA clock enable */
#define CMDBLK_SAA_CLK_OFF 8 /* MultiSound: SAA clock disable */
#define CMDBLK_SILENCE_YM2203 9 /* YM2203 chip 1: SSG+FM silence */
#define CMDBLK_SILENCE_YM2203_2 10 /* YM2203 chip 2 (TS): SSG+FM silence */
#define CMDBLK_SAA_INIT 11 /* SAA1099 chip 1: Sound Enable ON */
#define CMDBLK_SAA2_INIT 12 /* SAA1099 chip 2: Sound Enable ON+clk */
#define CMDBLK_INIT_OPL4 13 /* OPL4 init: NEW=1 + NEW2=1 (wave unlock) */
#define CMDBLK_SILENCE_WAVE_OFF 14 /* OPL4 wave: 24x KeyOff+Damp */
#define CMDBLK_SILENCE_WAVE_TL 15 /* OPL4 wave: 24x TL max */
/* ─── Очередь высокоуровневых команд (HL queue) ───────────────── */
/** Типы HL-команд */
#define HLCMD_CMDBLK 1 /* Выполнить VGM-format cmdblock (param = CMDBLK_xxx) */
#define HLCMD_PLAY 2 /* Воспроизведение VGM с текущей позиции до 0x66 */
#define HLCMD_LOOP 3 /* Перемотка к loop-точке, затем play (морфируется→PLAY) */
#define HLCMD_ISR_DONE 4 /* Записать CMD_ISR_DONE в буфер, ISR замирает */
#define HL_QUEUE_MAX 16
typedef struct { uint8_t cmd, param; } hl_entry_t;
/** Глобальные переменные HL queue — заполняются в main.c (build_playback_queue),
* используются внутри vgm_fill_buffer(). */
extern hl_entry_t vgm_hl_queue[HL_QUEUE_MAX];
extern uint8_t vgm_hl_len; /* всего записей */
extern uint8_t vgm_hl_pos; /* текущая запись */
extern uint8_t vgm_hl_abort_pos; /* переход при abort пользователя */
extern uint8_t vgm_loop_count; /* инкрементируется при каждом LOOP */
/* ─── VGZ (gzip-сжатый VGM) ─────────────────────────────────────── */
/**
* Копировать n_pages страниц из мегабуфера (#20+) в TAP (#A1+).
* DI на время работы, восстанавливает Win 0 / Win 3 и EI при выходе.
*
* @param n_pages количество страниц (макс. 31 — размер TAP области)
*/
void copy_pages_to_tap(uint8_t n_pages);
/**
* Распаковать VGZ из TAP-страниц в мегабуфер.
* Вызывается из C — ассемблерная обёртка (inflate_call.s).
*
* @param src_page первая физ. страница источника (TAP, напр. 0xA1)
* @param dst_page первая физ. страница назначения (мегабуфер, напр. 0x20)
* @return 0 — успех, 1 — ошибка (неверные данные / не gzip)
*/
uint8_t inflate_vgz(uint8_t src_page, uint8_t dst_page);
#endif /* VGM_H */