332 lines
19 KiB
C
332 lines
19 KiB
C
/**
|
||
* @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 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 14 /* OPL4 wave-часть silence: 24 канала KeyOff+Damp+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 */
|