294 lines
13 KiB
Markdown
294 lines
13 KiB
Markdown
# VGM Format Reference
|
||
|
||
> **Источник истины:** `src_sdcc/inc/vgm.h`, `src_sdcc/lib/vgm.c`
|
||
|
||
## Обзор формата VGM
|
||
|
||
VGM (Video Game Music) — формат хранения логов регистров звуковых чипов.
|
||
|
||
### Основные характеристики
|
||
|
||
- Расширение файла: **.vgm** (raw) или **.vgz** (gzip-сжатый)
|
||
- Содержит команды записи в регистры звуковых чипов
|
||
- Поддерживаемые плагином чипы: **YMF262 (OPL3), YM3812 (OPL2), YM3526 (OPL1), Y8950, AY-3-8910/YM2149/YM2203, SAA1099**
|
||
- Частота семплирования фиксирована: **44100 Гц**
|
||
|
||
## Структура заголовка VGM (ключевые смещения)
|
||
|
||
> Все поля — little-endian 32 бит, если не указано иное.
|
||
> Минимальная поддерживаемая версия: **1.00** (`0x00000100`).
|
||
|
||
| Offset | Размер | Название | Описание | С версии |
|
||
|--------|--------|------------------|------------------------------------------------|----------|
|
||
| `0x00` | 4 | Ident | Сигнатура `"Vgm "` (0x56 0x67 0x6D 0x20) | 1.00 |
|
||
| `0x04` | 4 | EoF offset | Расстояние до EOF от позиции 0x04 | 1.00 |
|
||
| `0x08` | 4 | Version | BCD: `0x0150`=v1.50, `0x0151`=v1.51 | 1.00 |
|
||
| `0x0C` | 4 | SN76489 clock | Тактовая частота SN76489 (0 = нет) | 1.00 |
|
||
| `0x10` | 4 | YM2413 clock | Тактовая частота YM2413 / OPLL (0 = нет) | 1.00 |
|
||
| `0x14` | 4 | GD3 offset | Смещение GD3-тега от `0x14` (0 = нет тега) | 1.00 |
|
||
| `0x18` | 4 | Total samples | Полная длина трека в семплах (÷44100 = сек) | 1.00 |
|
||
| `0x1C` | 4 | Loop offset | Смещение точки петли от `0x1C` (0 = нет) | 1.00 |
|
||
| `0x20` | 4 | Loop samples | Длина тела петли в семплах | 1.00 |
|
||
| `0x24` | 4 | Rate | Кадровая частота (50/60 Гц, информативно) | 1.01 |
|
||
| `0x34` | 4 | **VGM data offset** | Смещение VGM-данных от `0x34`; 0 → данные с `0x40` | **1.50** |
|
||
| `0x44` | 4 | YM2203 clock | **YM2203 (OPN/PSG)** (0 = нет) | 1.51 |
|
||
| `0x50` | 4 | **YM3812 clock** | **YM3812 (OPL2)** — AdLib/SoundBlaster | **1.51** |
|
||
| `0x54` | 4 | **YM3526 clock** | **YM3526 (OPL1)** — ISA AdLib | **1.51** |
|
||
| `0x58` | 4 | **Y8950 clock** | **Y8950 (OPL+ADPCM)** — MSX-Audio | **1.51** |
|
||
| `0x5C` | 4 | **YMF262 clock** | **YMF262 (OPL3)** — SB Pro 2 / TSConfig | **1.51** |
|
||
| `0x60` | 4 | YMF278B clock | YMF278B (OPL4) (0 = нет) | 1.51 |
|
||
| `0x74` | 4 | AY8910 clock | **AY-3-8910 / YM2149** (0 = нет) | 1.51 |
|
||
| `0xC8` | 4 | SAA1099 clock | **SAA1099** — SAM Coupé (0 = нет) | 1.71 |
|
||
|
||
> ⚠️ **Частая ошибка в документации:** смещения чипов в диапазоне `0x50`–`0x5C` — не `0x5C`/`0x60`/`0x64`/`0x68`.
|
||
> Используйте константы: `VGM_OFF_YM3812=0x50`, `VGM_OFF_YM3526=0x54`, `VGM_OFF_Y8950=0x58`, `VGM_OFF_YMF262=0x5C`.
|
||
|
||
### Смещение данных VGM
|
||
|
||
```
|
||
Если заголовок[0x34] (VGM data offset) == 0:
|
||
данные начинаются с абсолютного адреса 0x40 (старые файлы, < v1.50)
|
||
Иначе:
|
||
данные начинаются с 0x34 + заголовок[0x34]
|
||
```
|
||
|
||
Типичное значение для v1.51+: `VGM_data_offset = 0x4C`, данные с `0x80`.
|
||
Реализация в `vgm_parse_header()`: хранит готовый адрес начала данных в `vgm_data_start`.
|
||
|
||
### Bit 30 поля clock — флаг dual-chip
|
||
|
||
Бит 30 любого clock-поля (`clock & 0x40000000`) означает **два экземпляра** чипа (`VGM_CF_DUAL`).
|
||
Реальная тактовая частота = `clock & 0x3FFFFFFF`.
|
||
|
||
### Обнаружение чипов — `vgm_parse_header()`
|
||
|
||
Приоритет определения `vgm_chip_type`: **YMF262 > YM3812 > YM3526/Y8950**:
|
||
|
||
```c
|
||
// vgm.h — константы
|
||
#define VGM_OFF_YM3812 0x50 // OPL2
|
||
#define VGM_OFF_YM3526 0x54 // OPL1
|
||
#define VGM_OFF_Y8950 0x58 // OPL+ADPCM
|
||
#define VGM_OFF_YMF262 0x5C // OPL3
|
||
#define VGM_OFF_AY8910 0x74 // AY-3-8910 / YM2149
|
||
#define VGM_OFF_YM2203 0x44 // OPN (PSG-часть = AY)
|
||
#define VGM_OFF_SAA1099 0xC8 // SAA1099
|
||
|
||
// Значения vgm_chip_type
|
||
#define VGM_CHIP_NONE 0
|
||
#define VGM_CHIP_OPL 1 // YM3526 или Y8950
|
||
#define VGM_CHIP_OPL2 2 // YM3812
|
||
#define VGM_CHIP_OPL3 3 // YMF262
|
||
```
|
||
|
||
## VGM-команды — полная таблица
|
||
|
||
| Код | Байт | Параметры | Описание |
|
||
|--------------|------|-----------|-----------------------------------------------|
|
||
| `0x55` | 3 | `rr dd` | YM2203 write (PSG-часть, reg 0–15) |
|
||
| `0x5A` | 3 | `rr dd` | YM3812 (OPL2) write |
|
||
| `0x5B` | 3 | `rr dd` | YM3526 (OPL1) write |
|
||
| `0x5C` | 3 | `rr dd` | Y8950 (OPL+ADPCM) write |
|
||
| `0x5E` | 3 | `rr dd` | YMF262 (OPL3) Bank 0 write |
|
||
| `0x5F` | 3 | `rr dd` | YMF262 (OPL3) Bank 1 write |
|
||
| `0x61` | 3 | `lo hi` | Задержка: `(hi<<8)\|lo` семплов |
|
||
| `0x62` | 1 | — | Задержка 735 семплов (1/60 с) |
|
||
| `0x63` | 1 | — | Задержка 882 семпла (1/50 с) |
|
||
| `0x66` | 1 | — | End of data → конец потока / loop |
|
||
| `0x67` | var | — | Data block (PCM и пр.) — пропускается |
|
||
| `0x70`–`0x7F`| 1 | — | Задержка 1–16 семплов (`(cmd & 0x0F) + 1`) |
|
||
| `0xA0` | 3 | `rr dd` | AY8910 write; бит 7 `rr` = чип 2 |
|
||
| `0xBD` | 3 | `rr dd` | SAA1099 write; бит 7 `rr` = чип 2 |
|
||
| `0xD0` | 4 | `pp aa dd`| YMF278B (OPL4) write, порт `pp` |
|
||
|
||
Прочие команды пропускаются по таблице длин VGM spec (размер 1, 2, или 3 байта).
|
||
|
||
### `0xD0` — YMF278B (OPL4) write
|
||
|
||
```
|
||
0xD0 pp aa dd
|
||
```
|
||
|
||
- `pp` бит 7 — выбор чипа (dual-chip); игнорируется, на ZXM-MoonSound один чип.
|
||
- `pp & 0x7F` — порт:
|
||
- `0` — FM Bank 0 (те же регистры/порты #C4-#C5, что и `0x5E` для OPL3)
|
||
- `1` — FM Bank 1 (те же регистры/порты #C6-#C7, что и `0x5F` для OPL3)
|
||
- `2` — wave-часть чипа (табличный синтез), порты #7E (адрес) / #7F (данные)
|
||
- `aa` — номер регистра, `dd` — значение.
|
||
|
||
Чтобы чип реально принимал запись в порт 2 (wave-часть), в Bank 1
|
||
регистре `0x05` должен быть установлен бит NEW2 (бит 1), в дополнение к
|
||
обычному биту NEW (бит 0) — см. `CMDBLK_INIT_OPL4` в `cmdblocks.asm`.
|
||
Без NEW2 запись в порты #7E/#7F реальным чипом YMF278B игнорируется.
|
||
|
||
---
|
||
|
||
## Масштабирование задержек
|
||
|
||
VGM sample rate = **44100 Гц**, ISR rate = **1367 Гц** (= 44100 / 32):
|
||
|
||
```c
|
||
// inc/isr.h
|
||
#define VGM_SAMPLE_SHIFT 5 // делитель = 2^5 = 32
|
||
#define VGM_SAMPLE_MASK 0x1Fu // маска остатка
|
||
```
|
||
|
||
Алгоритм `vgm_fill_buffer()`:
|
||
- Накапливает семплы задержек
|
||
- При накоплении ≥ 32 → конвертирует `samples >> 5` в ISR-тики → команда `CMD_WAIT`
|
||
- Задержки < 32 семплов (< 0.73 мс) **теряются** (rounded down, это нормально)
|
||
|
||
```
|
||
1 секунда = 44100 семплов = 1378 ISR-тиков ≈ 1367 Гц
|
||
1 фрейм (50Hz) = 882 семпла = 27 ISR-тиков
|
||
1 фрейм (60Hz) = 735 семплов = 22 ISR-тика
|
||
```
|
||
|
||
---
|
||
|
||
## GD3 — метаданные трека
|
||
|
||
GD3-тег содержит Unicode-строки (UCS-2 LE, нуль-терминированные):
|
||
|
||
| № | Содержимое |
|
||
|---|--------------------|
|
||
| 1 | Track title (EN) |
|
||
| 2 | Track title (JP) |
|
||
| 3 | Game name (EN) |
|
||
| 4 | Game name (JP) |
|
||
| 5 | System name (EN) |
|
||
| 6 | System name (JP) |
|
||
| 7 | Author (EN) |
|
||
| 8 | Author (JP) |
|
||
| 9 | Release date |
|
||
|10 | VGM pack author |
|
||
|11 | Notes |
|
||
|
||
В плагине читаются только **английские** поля (нечётные), конвертируются в ASCII: символы > 127 → `'?'`.
|
||
Макс. длина поля: `VGM_GD3_LEN = 48` байт.
|
||
|
||
```c
|
||
// inc/vgm.h
|
||
extern char vgm_gd3_track[VGM_GD3_LEN];
|
||
extern char vgm_gd3_game[VGM_GD3_LEN];
|
||
extern char vgm_gd3_system[VGM_GD3_LEN];
|
||
extern char vgm_gd3_author[VGM_GD3_LEN];
|
||
```
|
||
|
||
---
|
||
|
||
## Петля (loop)
|
||
|
||
```c
|
||
// inc/vgm.h
|
||
extern uint16_t vgm_loop_addr; // адрес точки петли в окне #C000 (0 = нет)
|
||
extern uint8_t vgm_loop_page; // VPL-страница точки петли
|
||
```
|
||
|
||
Если `loop_offset (0x1C) != 0`:
|
||
- Абсолютное смещение в файле = `0x1C + loop_offset`
|
||
- `vgm_rewind_to_loop()` переставляет `vgm_read_ptr` / `vgm_cur_page` на точку петли
|
||
|
||
---
|
||
|
||
## VGZ — gzip-сжатый VGM
|
||
|
||
Файл `.vgz` — стандартный gzip. Схема обработки в плагине:
|
||
|
||
```
|
||
1. Загрузить .vgz в мегабуфер (VPL-страницы выше #20)
|
||
2. copy_pages_to_tap(n) — скопировать во временную TAP-область (#A1–#BF, 31 стр.)
|
||
3. inflate_vgz(src, dst) — распаковать (inflate.asm) обратно в мегабуфер
|
||
4. Работать с VGM как обычно
|
||
```
|
||
|
||
Макс. размер распакованного VGM через TAP: 31 × 16 КБ = **496 КБ**.
|
||
|
||
```c
|
||
void copy_pages_to_tap(uint8_t n_pages);
|
||
uint8_t inflate_vgz(uint8_t src_page, uint8_t dst_page);
|
||
// возвращает: 0 = успех, 1 = ошибка (не gzip или CRC-ошибка)
|
||
```
|
||
|
||
---
|
||
|
||
## OPL2 vs OPL3 — проблема L/R output
|
||
|
||
**Проблема:** `opl3_init()` устанавливает `NEW=1` (OPL3 mode).
|
||
В OPL3 mode биты 4–5 регистров `C0–C8` управляют маршрутизацией L/R (`L=1, R=1` для звука).
|
||
Файлы OPL2/OPL1 (команды `0x5A`/`0x5B`) пишут `C0` без битов L/R → **тишина**.
|
||
|
||
**Решение в `start_playback()` (main.c):**
|
||
|
||
```c
|
||
opl3_init(); // NEW=1 по умолчанию
|
||
if (vgm_chip_type != VGM_CHIP_OPL3) {
|
||
opl3_write_b1(0x05, 0x00); // NEW=0: OPL2-совместимый режим
|
||
}
|
||
// В OPL2-compat звук всегда идёт на оба канала L+R
|
||
```
|
||
|
||
---
|
||
|
||
## Пример парсинга заголовка (SDCC C)
|
||
|
||
```c
|
||
#include "inc/vgm.h"
|
||
#include "inc/wc_api.h"
|
||
|
||
// Загрузить первую страницу файла в окно #C000 и разобрать заголовок
|
||
wc_mngcvpl(0); // Переключить Win3 (#C000) на VPL-страницу 0
|
||
wc_load512(0xC000, 32); // Загрузить 16 КБ с диска
|
||
|
||
uint8_t rc = vgm_parse_header();
|
||
|
||
switch (rc) {
|
||
case VGM_OK:
|
||
// vgm_chip_type → VGM_CHIP_OPL / OPL2 / OPL3
|
||
// vgm_chip_count → кол-во обнаруженных чипов
|
||
// vgm_version → 0x0151 для v1.51
|
||
// vgm_loop_addr → 0 = нет петли
|
||
break;
|
||
case VGM_ERR_HEADER:
|
||
// Нет сигнатуры "Vgm " или заголовок повреждён
|
||
break;
|
||
case VGM_ERR_NOOPL:
|
||
// Файл не содержит поддерживаемого чипа (OPL/AY/SAA)
|
||
break;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Полезные константы (из `vgm.h`)
|
||
|
||
```c
|
||
// Смещения clock-полей в заголовке (relative byte offsets)
|
||
#define VGM_OFF_SN76489 0x0C
|
||
#define VGM_OFF_YM2413 0x10
|
||
#define VGM_OFF_YM2203 0x44 // OPN
|
||
#define VGM_OFF_YM3812 0x50 // OPL2 ← основной
|
||
#define VGM_OFF_YM3526 0x54 // OPL1
|
||
#define VGM_OFF_Y8950 0x58 // MSX-Audio
|
||
#define VGM_OFF_YMF262 0x5C // OPL3 ← основной
|
||
#define VGM_OFF_YMF278B 0x60 // OPL4
|
||
#define VGM_OFF_AY8910 0x74 // AY-3-8910 / YM2149
|
||
#define VGM_OFF_SAA1099 0xC8 // SAA1099
|
||
|
||
// Коды результата vgm_parse_header()
|
||
#define VGM_OK 0
|
||
#define VGM_ERR_HEADER 1
|
||
#define VGM_ERR_NOOPL 2
|
||
|
||
// ISR / VGM sample rate mapping
|
||
#define VGM_SAMPLE_SHIFT 5 // 44100 / 32 ≈ 1367 Гц (ISR_FREQ)
|
||
```
|
||
|
||
---
|
||
|
||
## Ссылки
|
||
|
||
- VGM Format Specification: https://vgmrips.net/wiki/VGM_Specification
|
||
- YMF262 (OPL3) Datasheet: Yamaha YMF262 Application Manual
|
||
- Архив VGM-треков: https://vgmrips.net/
|