Add OPL4 playback and analyzer support

This commit is contained in:
2026-09-15 14:59:29 +03:00
commit 1686838e03
553 changed files with 50524 additions and 0 deletions
+293
View File
@@ -0,0 +1,293 @@
# 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/