- 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.
2166 lines
92 KiB
C
2166 lines
92 KiB
C
/**
|
||
* @file vgm.c
|
||
* @brief VGM-парсер для ISR-буферов (OPL/AY/SAA + dual-chip)
|
||
* @version 4.0
|
||
*
|
||
* Читает VGM-поток из VPL-страниц WC (окно #C000).
|
||
*
|
||
* Поддержка чипов (воспроизведение):
|
||
* YM3812 (OPL2): cmd 0x5A → CMD_WRITE_B0 (clock @ header 0x50)
|
||
* YM3526 (OPL1): cmd 0x5B → CMD_WRITE_B0 (clock @ header 0x54)
|
||
* Y8950 (OPL): cmd 0x5C → CMD_WRITE_B0 (clock @ header 0x58)
|
||
* YMF262 (OPL3): cmd 0x5E → CMD_WRITE_B0 (clock @ header 0x5C)
|
||
* cmd 0x5F → CMD_WRITE_B1
|
||
* AY8910 (PSG): cmd 0xA0 → CMD_WRITE_AY / CMD_WRITE_AY2 (dual)
|
||
* YM2203 (OPN): cmd 0x55 → CMD_WRITE_AY (PSG-часть)
|
||
* SAA1099: cmd 0xBD → CMD_WRITE_SAA (bit7 = chip select)
|
||
*
|
||
* Распознавание (parse_header, vgm_chip_list[]):
|
||
* Все 40 чипов VGM spec, включая dual-chip (бит 30 clock).
|
||
*
|
||
* Масштабирование: 44100 / ISR_FREQ ≈ 2^VGM_SAMPLE_SHIFT.
|
||
*
|
||
* Оптимизации:
|
||
* - vgm_read_byte() — чистый ASM, экономит ~20 байт и ~30T на вызов.
|
||
* - vgm_skip() — арифметический пропуск без чтения (data block).
|
||
* - chip_scan[] — компактная ROM-таблица (4 bytes × 40 entries).
|
||
*/
|
||
|
||
#include "../inc/types.h"
|
||
#include "../inc/vgm.h"
|
||
#include "../inc/isr.h"
|
||
#include "../inc/wc_api.h"
|
||
#include "../inc/freq_lut_map.h"
|
||
|
||
/* ── Состояние парсера ──────────────────────────────────────────────── */
|
||
uint8_t vgm_paused; // 1 = пауза (остановить таймер, не обнулять vgm_sec_budget)
|
||
uint8_t vgm_cur_page; // текущая VPL-страница для чтения данных (0..N)
|
||
uint16_t vgm_read_ptr; // смещение в окне #C000 для чтения следующего байта VGM-потока
|
||
uint8_t vgm_end_page; // страница, на которой заканчивается VGM-поток (для оптимизации проверки конца)
|
||
uint16_t vgm_end_addr; // смещение в окне #C000, на котором заканчивается VGM-поток (для оптимизации проверки конца)
|
||
uint16_t vgm_version; // версия VGM в BCD (например, 0x0151 для v1.51)
|
||
uint8_t vgm_chip_type; // битовая маска типа чипов (VGM_CHIP_xxx), например VGM_CHIP_OPL2 | VGM_CHIP_AY
|
||
uint8_t vgm_chip_count; // количество обнаруженных чипов (для вывода информации о файле)
|
||
uint8_t vgm_opl4_preloaded_rom; // cold loader already moved a 0x84 image
|
||
|
||
vgm_chip_entry_t vgm_chip_list[VGM_MAX_CHIPS];
|
||
|
||
uint16_t vgm_loop_addr; // адрес точки петли в окне #C000 (0 = нет петли)
|
||
uint8_t vgm_loop_page; // страница, на которой находится точка петли
|
||
uint16_t vgm_total_seconds; // общее время воспроизведения в секундах
|
||
uint8_t vgm_loop_enabled; // 1 = петля включена, 0 = нет
|
||
|
||
// параметры политики петли (читаются из INI, устанавливаются main.c)
|
||
uint8_t cfg_loop_rewinds = 1;
|
||
uint8_t cfg_min_duration = 10;
|
||
uint16_t cfg_max_duration = 240;
|
||
|
||
// очередь высокоуровневых команд для vgm_fill_buffer (play, loop, init, silence)
|
||
hl_entry_t vgm_hl_queue[HL_QUEUE_MAX];
|
||
uint8_t vgm_hl_len;
|
||
uint8_t vgm_hl_pos;
|
||
uint8_t vgm_hl_abort_pos;
|
||
uint8_t vgm_loop_count;
|
||
|
||
|
||
/* ── Аккумулятор остатка задержки (VGM samples, 0..15) ─────────────── */
|
||
static uint16_t vgm_wait_accum = 0;
|
||
|
||
/* ── Битовая маска spectrum analyzer (fill_buffer → pad bytes → ISR) ── */
|
||
/* Не static: используется ASM-хелперами spectrum (asm/spectrum.s) */
|
||
uint16_t spec_mask; /* 16-бит: bit N → полоса N на максимум */
|
||
uint16_t spec_ay_period[6]; /* AY period shadow: ch0-2(chip1), ch0-2(chip2) */
|
||
uint8_t spec_saa_oct[6]; /* SAA octave shadow: chip 0 каналы 0-5 */
|
||
uint8_t spec_saa_oct2[6]; /* SAA octave shadow: chip 1 каналы 0-5 */
|
||
uint8_t spec_saa_dual; /* 1=dual SAA: bars 0-7 / 8-15 split */
|
||
uint8_t cfg_saa_mode = 0; /* 0=chip0 only(default), 1=chip1 on chip0, 2=turbo(both) */
|
||
uint8_t spec_opl_bd; /* OPL 0xBD shadow для детекции фронта */
|
||
uint8_t spec_fm_block[6]; /* YM2203 FM block shadow: ch0-2(chip1/2)*/
|
||
uint8_t spec_wave_active[3]; /* 24-bit KeyOn mask, wave ch0..23 */
|
||
|
||
/* ── Масштабирование частот PSG/FM (LUT + asm) ──────────────────────── */
|
||
uint8_t vgm_freq_mode;
|
||
uint16_t *freq_lut_base;
|
||
uint16_t vgm_freq_lut_khz; /* clock (кГц) подобранной LUT таблицы */
|
||
static uint8_t freq_lut_page; /* plugin page для Window 0 (LUT) */
|
||
|
||
/* FM F-Number: множитель N для формулы fnum × N / 7.
|
||
* 0 = bypass (масштабирование не нужно), иначе 3/5/6/8. */
|
||
static uint8_t fm_mul;
|
||
|
||
/* Shadow-регистры PSG: полный 12-бит tone period на канал.
|
||
* Обновляются при записи lo/hi, используются для LUT lookup. */
|
||
static uint16_t psg_shadow[3]; /* каналы 0,1,2 — chip 1 */
|
||
static uint16_t psg_shadow2[3]; /* каналы 0,1,2 — chip 2 (0xA5) */
|
||
|
||
/* Shadow-регистры FM: 11-бит F-Number + 3-бит block на канал.
|
||
* Обновляются при записи 0xA0-0xA2 (lo) / 0xA4-0xA6 (hi+block). */
|
||
static uint16_t fm_fnum[3]; /* F-Number каналы 0,1,2 — chip 1 */
|
||
static uint16_t fm_fnum2[3]; /* F-Number каналы 0,1,2 — chip 2 */
|
||
static uint8_t fm_block[3]; /* block каналы 0,1,2 — chip 1 */
|
||
static uint8_t fm_block2[3]; /* block каналы 0,1,2 — chip 2 */
|
||
|
||
/* ── Бюджеты для вставки служебных ISR-команд ──────────────────────── */
|
||
static uint16_t vgm_sec_budget = ISR_FREQ; /* тиков до CMD_INC_SEC */
|
||
|
||
/* ── Статические локальные для vgm_fill_buffer / emit_wait ──────────
|
||
* Устраняет IX-frame SDCC (32 байта стека + 19T на ld r,-N(ix)).
|
||
* Static доступ: ld a,(nn) = 13T, ld hl,(nn) = 16T. */
|
||
static uint8_t *fb_buf; /* база текущего cmd_buf */
|
||
static uint8_t *fb_wp; /* указатель записи в cmd_buf */
|
||
static uint8_t *fb_rp; /* указатель чтения VGM (#C000-#FFFF) */
|
||
static uint8_t fb_cpg; /* локальная копия vgm_cur_page */
|
||
static uint8_t fb_budget; /* команд до принудительного yield */
|
||
static uint8_t fb_yield_ticks; /* тиков потрачено на budget yield-ы */
|
||
static uint16_t fb_wacc; /* аккумулятор задержки (0..VGM_SAMPLE_MASK) */
|
||
static uint8_t fb_op; /* текущий VGM opcode */
|
||
static uint8_t fb_b1; /* байт операнда 1 */
|
||
static uint8_t fb_b2; /* байт операнда 2 */
|
||
static uint8_t fb_is_vgm; /* 1 = VGM mega-buffer, 0 = cmdblk */
|
||
static uint16_t fb_tk; /* temp: счётчик тиков для emit_wait */
|
||
static uint32_t fb_t32; /* temp: 32-бит размер data-block (0x67) */
|
||
static uint16_t fb_w; /* temp: промежуточное значение emit_wait */
|
||
static uint8_t fb_n; /* temp: число байт для пропуска */
|
||
static uint8_t *fb_end; /* предвычисленный fb_buf + (CMD_BUF_SIZE-48) */
|
||
static hl_entry_t *fb_e; /* temp: указатель на запись HL queue */
|
||
static uint8_t fb_ch; /* temp: индекс канала PSG/FM */
|
||
static uint16_t fb_scaled; /* temp: масштабированная частота */
|
||
static uint8_t fb_reg; /* temp: AY рег. (0xA0) / YMF278B port (0xD0) /
|
||
* тип data-block (0x67) — ветки взаимоисключающие */
|
||
|
||
/* ── Патч заголовков wave-таблицы после загрузки блока 0x84 (ROM Image) ──
|
||
* См. wave_upload_block(): референсный NedoOS-драйвер (vgm/opl4.asm,
|
||
* opl4loadromdatablock) после заливки ROM-блока, чей адрес попадает в
|
||
* первые 64КБ RAM-региона (где живёт таблица заголовков сэмплов, 128
|
||
* записей по 12 байт), принудительно взводит бит RAM-адресации (#20) в
|
||
* первом байте КАЖДОЙ записи — иначе встроенные в заголовки адреса
|
||
* сэмплов остаются ROM-относительными, и чип читает не тот PCM-семпл. */
|
||
static uint8_t hdr_recs; /* оставшиеся записи таблицы (0-128); 0=не патчим */
|
||
static uint8_t hdr_sub; /* позиция внутри 12-байтной записи (0-11) */
|
||
static uint8_t opl4_rom_loaded; /* 0x84 ROM image перенесён в SRAM; номера
|
||
* волн надо отображать в диапазон 384-511 */
|
||
|
||
/* VGM_FILL_CMD_BUDGET определён в variant_cfg.h (через isr.h) */
|
||
|
||
/* ── Хелперы spectrum analyzer (ASM: asm/spectrum.s) ────────────── */
|
||
/* 16 полос = 8 октав × 2 поддиапазона (по OPL block*2+fnum_msb).
|
||
* Все реализации на оптимизированном Z80 ASM для горячего цикла. */
|
||
extern void spectrum_opl_b0(uint8_t reg, uint8_t val);
|
||
extern void spectrum_opl_b1(uint8_t reg, uint8_t val);
|
||
extern void spectrum_ay(uint8_t reg, uint8_t val);
|
||
extern void spectrum_ay2(uint8_t reg, uint8_t val);
|
||
extern void spectrum_saa(uint8_t reg, uint8_t val);
|
||
extern void spectrum_saa2(uint8_t reg, uint8_t val);
|
||
extern void spectrum_wave(uint8_t reg, uint8_t val);
|
||
|
||
|
||
/* ── Таблица сканирования чипов (ROM-const) ──────────────────────── */
|
||
/* Каждая запись: { offset_in_header, min_version_lo }
|
||
* offset — байтовое смещение 32-bit clock-поля в VGM заголовке.
|
||
* minv_lo — младший байт минимальной VGM-версии (старший всегда 0x01).
|
||
* chip ID в chip_list = offset. */
|
||
typedef struct
|
||
{
|
||
uint8_t off;
|
||
uint8_t minv_lo;
|
||
} chip_scan_t;
|
||
|
||
static const chip_scan_t chip_scan[] = {
|
||
{0x0C, 0x00}, {0x10, 0x00}, /* 1.00 */
|
||
{0x2C, 0x50}, {0x30, 0x50}, /* 1.50 */
|
||
{0x38, 0x51}, {0x40, 0x51}, {0x44, 0x51}, {0x48, 0x51}, /* 1.51 */
|
||
{0x4C, 0x51}, {0x50, 0x51}, {0x54, 0x51}, {0x58, 0x51},
|
||
{0x5C, 0x51}, {0x60, 0x51}, {0x64, 0x51}, {0x68, 0x51},
|
||
{0x6C, 0x51}, {0x70, 0x51}, {0x74, 0x51},
|
||
{0x80, 0x61}, {0x84, 0x61}, {0x88, 0x61}, {0x8C, 0x61}, /* 1.61 */
|
||
{0x90, 0x61}, {0x98, 0x61}, {0x9C, 0x61}, {0xA0, 0x61},
|
||
{0xA4, 0x61}, {0xA8, 0x61}, {0xAC, 0x61}, {0xB0, 0x61},
|
||
{0xB4, 0x61},
|
||
{0xB8, 0x71}, {0xC0, 0x71}, {0xC4, 0x71}, {0xC8, 0x71}, /* 1.71 */
|
||
{0xCC, 0x71}, {0xD0, 0x71}, {0xD8, 0x71}, {0xDC, 0x71},
|
||
{0xE0, 0x71},
|
||
};
|
||
#define CHIP_SCAN_COUNT (sizeof(chip_scan)/sizeof(chip_scan[0]))
|
||
|
||
/* ── Локальный хелпер ───────────────────────────────────────────────── */
|
||
|
||
/* Пропустить n байт в VGM-потоке (арифметически, без чтения).
|
||
* Используется для data block 0x67, PCM RAM write 0x68 и др. */
|
||
static void vgm_skip(uint32_t n) __naked
|
||
{
|
||
(void)n;
|
||
__asm
|
||
; sdcccall(1): n = HL:DE (HL high word, DE low word).
|
||
; Convert the mapped address C000h..FFFFh to a 14-bit page offset,
|
||
; add it to n, then split the result back into page and address.
|
||
ld bc, (_vgm_read_ptr)
|
||
ld a, b
|
||
and a, #0x3F
|
||
ld b, a
|
||
|
||
ex de, hl ; HL = n.low, DE = n.high
|
||
add hl, bc
|
||
ex de, hl ; DE = total.low, HL = total.high
|
||
jr nc, vsk_no_carry
|
||
inc hl
|
||
vsk_no_carry:
|
||
|
||
; Remember whether at least one 16K boundary was crossed.
|
||
ld a, d
|
||
and a, #0xC0
|
||
or a, l
|
||
or a, h
|
||
ld b, a
|
||
|
||
; page_delta = (total >> 14) & 0xFF.
|
||
ld a, d
|
||
rlca
|
||
rlca
|
||
and a, #0x03
|
||
ld c, a
|
||
ld a, l
|
||
add a, a
|
||
add a, a
|
||
or a, c
|
||
ld c, a
|
||
|
||
; Address within the selected page remains in total.low bits 0..13.
|
||
ld a, d
|
||
and a, #0x3F
|
||
or a, #0xC0
|
||
ld d, a
|
||
ld (_vgm_read_ptr), de
|
||
|
||
ld a, (_vgm_cur_page)
|
||
add a, c
|
||
ld (_vgm_cur_page), a
|
||
|
||
; The old C implementation remapped only after crossing a boundary.
|
||
ld a, b
|
||
or a
|
||
ret z
|
||
ld a, (_vgm_cur_page)
|
||
call _wc_mngcvpl
|
||
ret
|
||
__endasm;
|
||
}
|
||
|
||
/* Деление числа VGM-сэмплов на 44100 вызывается только при разборе
|
||
* заголовка. Небольшой цикл вычитания заметно компактнее универсальных
|
||
* 32-битных __divulong/__mullong SDCC (358 байт резидентного кода). */
|
||
static uint16_t samples_to_seconds(uint32_t samples)
|
||
{
|
||
uint16_t seconds = 0;
|
||
|
||
while (samples >= 44100UL) {
|
||
samples -= 44100UL;
|
||
if (++seconds == 0xFFFF)
|
||
break;
|
||
}
|
||
return seconds;
|
||
}
|
||
|
||
/* clock/1000 с ошибкой менее 0.2% для типичных звуковых частот:
|
||
* 1/1024 + 1/65536 + 1/131072 = 0.00099945. Для выбора LUT допуск
|
||
* равен 2%, зато не нужна 131-байтная библиотечная 32-битная делилка. */
|
||
static uint16_t clock_to_khz(uint32_t clock)
|
||
{
|
||
uint16_t hi = (uint16_t)(clock >> 16);
|
||
return (uint16_t)(clock >> 10) + hi + (hi >> 1);
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* vgm_parse_header
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
uint8_t vgm_parse_header(void)
|
||
{
|
||
uint8_t *base = (uint8_t *)0xC000;
|
||
uint32_t data_off_field;
|
||
uint32_t data_start_abs;
|
||
uint32_t loop_off_field;
|
||
uint32_t loop_start_abs;
|
||
uint32_t eof_off_field;
|
||
uint32_t end_file_off;
|
||
uint8_t i;
|
||
uint8_t off_hdr;
|
||
uint32_t clk;
|
||
vgm_chip_entry_t *e;
|
||
|
||
vgm_paused = 0;
|
||
vgm_chip_count = 0;
|
||
vgm_chip_type = VGM_CHIP_NONE;
|
||
opl4_rom_loaded = vgm_opl4_preloaded_rom;
|
||
vgm_opl4_preloaded_rom = 0;
|
||
for (i = 0; i < 3; i++)
|
||
spec_wave_active[i] = 0;
|
||
vgm_loop_addr = 0;
|
||
vgm_loop_page = 0;
|
||
vgm_wait_accum = 0;
|
||
vgm_sec_budget = ISR_FREQ;
|
||
|
||
/* Сигнатура "Vgm " */
|
||
if (base[0] != 'V' || base[1] != 'g' || base[2] != 'm' || base[3] != ' ')
|
||
return VGM_ERR_HEADER;
|
||
|
||
/* Версия */
|
||
vgm_version = (uint16_t)base[0x08] | ((uint16_t)base[0x09] << 8);
|
||
|
||
/* ── Data offset ──────────────────────────────────────────────────
|
||
* VGM >= 1.50: поле в 0x34 — относительное смещение от 0x34.
|
||
* Если поле == 0, для совместимости используем 0x40.
|
||
* VGM < 1.50: data начинается с 0x40.
|
||
*/
|
||
if (vgm_version >= 0x0150) {
|
||
data_off_field = (uint32_t)base[0x34]
|
||
| ((uint32_t)base[0x35] << 8)
|
||
| ((uint32_t)base[0x36] << 16)
|
||
| ((uint32_t)base[0x37] << 24);
|
||
|
||
data_start_abs = data_off_field ? (0x34UL + data_off_field) : 0x40UL;
|
||
} else {
|
||
data_start_abs = 0x40UL;
|
||
}
|
||
|
||
/* Если data_start выходит за EOF — файл сломан */
|
||
eof_off_field = (uint32_t)base[0x04]
|
||
| ((uint32_t)base[0x05] << 8)
|
||
| ((uint32_t)base[0x06] << 16)
|
||
| ((uint32_t)base[0x07] << 24);
|
||
end_file_off = eof_off_field + 4UL;
|
||
|
||
if (data_start_abs >= end_file_off)
|
||
return VGM_ERR_HEADER;
|
||
|
||
vgm_cur_page = (uint8_t)(data_start_abs >> 14);
|
||
vgm_read_ptr = 0xC000 + (uint16_t)(data_start_abs & 0x3FFF);
|
||
|
||
/* ── Конец файла ────────────────────────────────────────────────── */
|
||
vgm_end_page = (uint8_t)(end_file_off >> 14);
|
||
vgm_end_addr = 0xC000 + (uint16_t)(end_file_off & 0x3FFF);
|
||
|
||
/* ── Scan чипов ─────────────────────────────────────────────────── */
|
||
for (i = 0; i < CHIP_SCAN_COUNT; i++) {
|
||
off_hdr = chip_scan[i].off;
|
||
|
||
if (vgm_version < (0x0100u | chip_scan[i].minv_lo))
|
||
continue;
|
||
|
||
/* Clock fields должны лежать до начала VGM data stream */
|
||
if ((uint32_t)off_hdr + 4UL > data_start_abs)
|
||
continue;
|
||
|
||
/* Читаем clock (32-bit LE) */
|
||
clk = (uint32_t)base[off_hdr];
|
||
clk |= (uint32_t)base[off_hdr + 1] << 8;
|
||
clk |= (uint32_t)base[off_hdr + 2] << 16;
|
||
clk |= (uint32_t)base[off_hdr + 3] << 24;
|
||
|
||
if (clk == 0)
|
||
continue;
|
||
|
||
/* Сохраняем */
|
||
if (vgm_chip_count < VGM_MAX_CHIPS) {
|
||
e = &vgm_chip_list[vgm_chip_count];
|
||
e->id = off_hdr;
|
||
e->flags = (clk & 0x40000000UL) ? VGM_CF_DUAL : 0;
|
||
clk &= 0x3FFFFFFFUL;
|
||
e->clock_khz = clock_to_khz(clk);
|
||
vgm_chip_count++;
|
||
}
|
||
|
||
/* Определяем тип OPL для fill_buffer.
|
||
* chip_scan[] идёт по возрастанию offset, поэтому YMF278B (0x60)
|
||
* всегда проверяется после YMF262 (0x5C) и корректно перекрывает
|
||
* его результат — приоритет YMF278B > YMF262 соблюдён. */
|
||
if (off_hdr == VGM_OFF_YMF278B)
|
||
vgm_chip_type = VGM_CHIP_OPL4;
|
||
else if (off_hdr == VGM_OFF_YMF262)
|
||
vgm_chip_type = VGM_CHIP_OPL3;
|
||
else if ((off_hdr == VGM_OFF_YM3812 || off_hdr == VGM_OFF_Y8950) && vgm_chip_type < VGM_CHIP_OPL2)
|
||
vgm_chip_type = VGM_CHIP_OPL2;
|
||
else if (off_hdr == VGM_OFF_YM3526 && vgm_chip_type < VGM_CHIP_OPL)
|
||
vgm_chip_type = VGM_CHIP_OPL;
|
||
}
|
||
|
||
/* ── Подбор LUT для пересчёта частот PSG/FM ────────────────────────
|
||
* YM2203: psg_khz = полный master clock → ищем в freq_lut_map_ym[]
|
||
* AY8910: psg_khz = clock как есть → ищем в freq_lut_map_ay[]
|
||
*
|
||
* PSG таблица в 16KB странице (2 per page, PSG_A@0x0000, PSG_B@0x2000).
|
||
* PSG ratio = target/source (period scaling).
|
||
* FM ratio = source/target (F-Number scaling, ОБРАТНЫЙ!) — ASM ×N/7. */
|
||
vgm_freq_mode = FREQ_MODE_NATIVE;
|
||
freq_lut_base = (uint16_t *)0;
|
||
freq_lut_page = 0;
|
||
vgm_freq_lut_khz = 0;
|
||
fm_mul = 0;
|
||
psg_shadow[0] = 0; psg_shadow[1] = 0; psg_shadow[2] = 0;
|
||
psg_shadow2[0] = 0; psg_shadow2[1] = 0; psg_shadow2[2] = 0;
|
||
fm_fnum[0] = 0; fm_fnum[1] = 0; fm_fnum[2] = 0;
|
||
fm_fnum2[0] = 0; fm_fnum2[1] = 0; fm_fnum2[2] = 0;
|
||
fm_block[0] = 0; fm_block[1] = 0; fm_block[2] = 0;
|
||
fm_block2[0] = 0; fm_block2[1] = 0; fm_block2[2] = 0;
|
||
|
||
for (i = 0; i < vgm_chip_count; i++) {
|
||
uint16_t psg_khz;
|
||
uint16_t hw_khz;
|
||
uint16_t bypass_tol;
|
||
const freq_lut_entry_t *lut_map;
|
||
uint8_t lut_count;
|
||
uint8_t is_ym;
|
||
|
||
if (vgm_chip_list[i].id == VGM_OFF_YM2203) {
|
||
psg_khz = vgm_chip_list[i].clock_khz; /* полный master clock */
|
||
hw_khz = FREQ_LUT_HW_YM_KHZ; /* 3500 */
|
||
bypass_tol = FREQ_LUT_BYPASS_TOL_YM; /* 70 */
|
||
lut_map = freq_lut_map_ym;
|
||
lut_count = FREQ_LUT_YM_COUNT;
|
||
is_ym = 1;
|
||
} else if (vgm_chip_list[i].id == VGM_OFF_AY8910) {
|
||
psg_khz = vgm_chip_list[i].clock_khz;
|
||
hw_khz = FREQ_LUT_HW_KHZ; /* 1750 */
|
||
bypass_tol = FREQ_LUT_BYPASS_TOL; /* 35 */
|
||
lut_map = freq_lut_map_ay;
|
||
lut_count = FREQ_LUT_AY_COUNT;
|
||
is_ym = 0;
|
||
} else {
|
||
continue;
|
||
}
|
||
|
||
/* ── Определяем режим масштабирования ─────────────────────
|
||
* 1) bypass: |psg_khz - HW| ≤ 2% → native
|
||
* 2) LUT table match (±5%) → TABLE mode (Window 0)
|
||
* Нет match → остаётся native (экзотика <1% файлов) */
|
||
{
|
||
uint16_t diff = (psg_khz > hw_khz)
|
||
? (psg_khz - hw_khz)
|
||
: (hw_khz - psg_khz);
|
||
if (diff <= bypass_tol) {
|
||
/* ±2% — native, пересчёт не нужен */
|
||
vgm_freq_mode = FREQ_MODE_NATIVE;
|
||
} else {
|
||
/* Ищем подходящую предрасчитанную таблицу (±5%) */
|
||
uint8_t j;
|
||
for (j = 0; j < lut_count; j++) {
|
||
uint16_t d = (psg_khz > lut_map[j].clk_khz)
|
||
? (psg_khz - lut_map[j].clk_khz)
|
||
: (lut_map[j].clk_khz - psg_khz);
|
||
if (d <= lut_map[j].tol_khz) {
|
||
/* Матч! Подключаем LUT страницу в Window 0 */
|
||
freq_lut_page = lut_map[j].page;
|
||
wc_mng0_pl(freq_lut_page);
|
||
freq_lut_base = (uint16_t *)lut_map[j].offset;
|
||
/* FM множитель: fnum × N / 7 (в ASM) */
|
||
if (is_ym) {
|
||
fm_mul = freq_lut_fm_mul[j];
|
||
}
|
||
vgm_freq_lut_khz = lut_map[j].clk_khz;
|
||
vgm_freq_mode = FREQ_MODE_TABLE;
|
||
break;
|
||
}
|
||
}
|
||
/* Нет match → native (без масштабирования) */
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
|
||
/* ── Loop offset ──────────────────────────────────────────────────
|
||
* Relative offset from 0x1C. Если значение невалидно — loop отключаем.
|
||
*/
|
||
loop_off_field = (uint32_t)base[0x1C]
|
||
| ((uint32_t)base[0x1D] << 8)
|
||
| ((uint32_t)base[0x1E] << 16)
|
||
| ((uint32_t)base[0x1F] << 24);
|
||
|
||
if (loop_off_field) {
|
||
loop_start_abs = 0x1CUL + loop_off_field;
|
||
|
||
if (loop_start_abs >= data_start_abs && loop_start_abs < end_file_off) {
|
||
vgm_loop_page = (uint8_t)(loop_start_abs >> 14);
|
||
vgm_loop_addr = 0xC000 + (uint16_t)(loop_start_abs & 0x3FFF);
|
||
} else {
|
||
vgm_loop_addr = 0;
|
||
vgm_loop_page = 0;
|
||
}
|
||
}
|
||
|
||
/* ── Общая длительность + политика лупа ───────────────────────────
|
||
* total_samples (0x18) = число сэмплов всего файла (до лупа).
|
||
* loop_samples (0x20) = число сэмплов одной итерации лупа.
|
||
*
|
||
* Loop отключается если:
|
||
* 1) loop_addr == 0 (нет лупа в файле)
|
||
* 2) full-track loop (loop_start == data_start)
|
||
* 3) базовый трек < cfg_min_duration
|
||
* 4) с лупом получается > cfg_max_duration
|
||
*/
|
||
{
|
||
uint32_t ts = (uint32_t)base[0x18]
|
||
| ((uint32_t)base[0x19] << 8)
|
||
| ((uint32_t)base[0x1A] << 16)
|
||
| ((uint32_t)base[0x1B] << 24);
|
||
uint16_t base_sec = samples_to_seconds(ts);
|
||
|
||
vgm_loop_enabled = 0;
|
||
|
||
if (vgm_loop_addr &&
|
||
!(vgm_loop_page == vgm_cur_page &&
|
||
vgm_loop_addr == vgm_read_ptr) &&
|
||
base_sec >= cfg_min_duration) {
|
||
/* Частичный loop, трек достаточно длинный — пробуем */
|
||
uint32_t ls = (uint32_t)base[0x20]
|
||
| ((uint32_t)base[0x21] << 8)
|
||
| ((uint32_t)base[0x22] << 16)
|
||
| ((uint32_t)base[0x23] << 24);
|
||
uint32_t total_with_loop = ts;
|
||
uint8_t repeats = cfg_loop_rewinds;
|
||
while (repeats--)
|
||
total_with_loop += ls;
|
||
uint16_t total_sec = samples_to_seconds(total_with_loop);
|
||
|
||
if (total_sec <= cfg_max_duration) {
|
||
vgm_loop_enabled = 1;
|
||
base_sec = total_sec;
|
||
}
|
||
}
|
||
|
||
vgm_total_seconds = base_sec;
|
||
}
|
||
|
||
return VGM_OK;
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* vgm_chip_name — имя чипа по header offset
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
const char *vgm_chip_name(uint8_t id)
|
||
{
|
||
switch (id) {
|
||
case 0x0C: return "SN76489";
|
||
case 0x10: return "YM2413";
|
||
case 0x44: return "YM2203";
|
||
case 0x50: return "YM3812";
|
||
case 0x54: return "YM3526";
|
||
case 0x58: return "Y8950";
|
||
case 0x5C: return "YMF262";
|
||
case 0x60: return "YMF278B";
|
||
case 0x74: return "AY8910";
|
||
case 0xC8: return "SAA1099";
|
||
default: return 0;
|
||
}
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* asm_wait_63 / asm_wait_62 — добавляет 882/735 к fb_wacc, делает
|
||
* >> VGM_SAMPLE_SHIFT, пишет fb_tk и fb_wacc.
|
||
*
|
||
* Заменяет цепочку: C-сложение → fb_t → asm_shift_mask.
|
||
* ~165 T (62) / ~175 T (63) vs ~246 T (C + shift_mask).
|
||
*
|
||
* VGM_SAMPLE_SHIFT = 6. Максимальный результат: (63+882)>>6 = 14.
|
||
* Clobbers: A, B, D, E, H, L.
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
static void asm_wait_62(void) __naked;
|
||
static void asm_wait_63(void) __naked;
|
||
|
||
static void asm_wait_63(void) __naked {
|
||
__asm
|
||
ld de, #882 ;10T
|
||
jp _asm_wait_common ;10T
|
||
__endasm;
|
||
}
|
||
static void asm_wait_62(void) __naked {
|
||
__asm
|
||
ld de, #735 ;10T
|
||
_asm_wait_common:
|
||
ld hl, (_fb_wacc) ;16T
|
||
add hl, de ;11T max 63+882=945=0x03B1, CF=0
|
||
;; fb_wacc = L & VGM_SAMPLE_MASK
|
||
ld a, l ; 4T
|
||
and a, #VGM_SAMPLE_MASK ; 7T
|
||
ld (_fb_wacc), a ;13T
|
||
;; fb_tk = HL >> VGM_SAMPLE_SHIFT via H<<(8-SHIFT) | L>>(SHIFT)
|
||
ld a, h ; 4T
|
||
add a, a ; 4T <<1
|
||
add a, a ; 4T <<2
|
||
#if VGM_SAMPLE_SHIFT < 6
|
||
add a, a ; 4T <<3
|
||
#endif
|
||
#if VGM_SAMPLE_SHIFT < 5
|
||
add a, a ; 4T <<4
|
||
#endif
|
||
ld b, a ; 4T B = H<<(8-SHIFT)
|
||
ld a, l ; 4T
|
||
rlca ; 4T
|
||
rlca ; 4T
|
||
#if VGM_SAMPLE_SHIFT < 6
|
||
rlca ; 4T
|
||
#endif
|
||
#if VGM_SAMPLE_SHIFT < 5
|
||
rlca ; 4T
|
||
#endif
|
||
#if VGM_SAMPLE_SHIFT == 6
|
||
and a, #0x03 ; 7T L>>6
|
||
#elif VGM_SAMPLE_SHIFT == 5
|
||
and a, #0x07 ; 7T L>>5
|
||
#elif VGM_SAMPLE_SHIFT == 4
|
||
and a, #0x0F ; 7T L>>4
|
||
#endif
|
||
or a, b ; 4T A = fb_tk (max 14, fits u8)
|
||
ld (_fb_tk), a ;13T
|
||
xor a, a ; 4T
|
||
ld (_fb_tk+1), a ;13T
|
||
ld (_fb_wacc+1), a ;13T
|
||
ret ;10T
|
||
__endasm;
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* asm_short_wait — для 0x70-0x7F: fb_wacc += (fb_op & 0x0F) + 1,
|
||
* >> VGM_SAMPLE_SHIFT, пишет fb_tk и fb_wacc.
|
||
*
|
||
* Максимум fb_wacc(63) + 16 = 79 → H=0, fb_tk = 0 или 1.
|
||
* ~175 T total vs ~304 T (C + shift_mask).
|
||
* Clobbers: A, D, E, H, L.
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
static void asm_short_wait(void) __naked {
|
||
__asm
|
||
ld a, (_fb_op) ;13T
|
||
and a, #0x0F ; 7T
|
||
inc a ; 4T A = (op & 0x0F) + 1 (1..16)
|
||
ld e, a ; 4T
|
||
ld d, #0 ; 7T DE = addend
|
||
ld hl, (_fb_wacc) ;16T
|
||
add hl, de ;11T max 79, CF=0, H=0
|
||
;; fb_wacc = L & VGM_SAMPLE_MASK
|
||
ld a, l ; 4T
|
||
and a, #VGM_SAMPLE_MASK ; 7T
|
||
ld (_fb_wacc), a ;13T
|
||
;; fb_tk = L >> VGM_SAMPLE_SHIFT (H guaranteed 0)
|
||
ld a, l ; 4T
|
||
rlca ; 4T
|
||
rlca ; 4T
|
||
#if VGM_SAMPLE_SHIFT < 6
|
||
rlca ; 4T
|
||
#endif
|
||
#if VGM_SAMPLE_SHIFT < 5
|
||
rlca ; 4T
|
||
#endif
|
||
#if VGM_SAMPLE_SHIFT == 6
|
||
and a, #0x03 ; 7T
|
||
#elif VGM_SAMPLE_SHIFT == 5
|
||
and a, #0x07 ; 7T
|
||
#elif VGM_SAMPLE_SHIFT == 4
|
||
and a, #0x0F ; 7T
|
||
#endif
|
||
ld (_fb_tk), a ;13T
|
||
xor a, a ; 4T
|
||
ld (_fb_tk+1), a ;13T
|
||
ld (_fb_wacc+1), a ;13T
|
||
ret ;10T
|
||
__endasm;
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* asm_arb_wait — для 0x61: fb_wacc += fb_b2:fb_b1 (16-бит),
|
||
* >> VGM_SAMPLE_SHIFT (= 6) с учётом переноса (17-бит).
|
||
* Записывает fb_tk (max 1024), fb_wacc.
|
||
*
|
||
* ~236 T total vs SDCC 32-бит IX-frame + DJNZ ~725 T. (3× быстрее)
|
||
*
|
||
* Вызывать ПОСЛЕ asm_read_2bytes() (fb_b1, fb_b2 уже прочитаны).
|
||
* Clobbers: A, B, C, D, E, H, L.
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
static void asm_arb_wait(void) __naked {
|
||
__asm
|
||
;; sample_count = fb_b2:fb_b1
|
||
ld a, (_fb_b2) ;13T
|
||
ld h, a ; 4T
|
||
ld a, (_fb_b1) ;13T
|
||
ld l, a ; 4T
|
||
;; sum = fb_wacc + sample_count (17-bit: CF:H:L)
|
||
ld de, (_fb_wacc) ;20T
|
||
add hl, de ;11T CF = bit16
|
||
;; Save carry to B (ld does not touch flags!)
|
||
ld b, #0 ; 7T
|
||
jr nc, 80$ ;12/7T usually no carry
|
||
#if VGM_SAMPLE_SHIFT == 6
|
||
ld b, #4 ; 7T CF << (8-6) = CF<<2 = 4
|
||
#elif VGM_SAMPLE_SHIFT == 5
|
||
ld b, #8 ; 7T CF << (8-5) = CF<<3 = 8
|
||
#elif VGM_SAMPLE_SHIFT == 4
|
||
ld b, #16 ; 7T CF << (8-4) = CF<<4 = 16
|
||
#endif
|
||
80$:
|
||
;; fb_wacc = L & VGM_SAMPLE_MASK
|
||
ld a, l ; 4T
|
||
and a, #VGM_SAMPLE_MASK ; 7T
|
||
ld (_fb_wacc), a ;13T
|
||
xor a, a ; 4T
|
||
ld (_fb_wacc+1), a ;13T
|
||
;; fb_tk = (CF:H:L) >> VGM_SAMPLE_SHIFT
|
||
;; low byte: (H<<(8-SHIFT)) | (L>>SHIFT)
|
||
ld a, l ; 4T
|
||
rlca ; 4T
|
||
rlca ; 4T
|
||
#if VGM_SAMPLE_SHIFT < 6
|
||
rlca ; 4T
|
||
#endif
|
||
#if VGM_SAMPLE_SHIFT < 5
|
||
rlca ; 4T
|
||
#endif
|
||
#if VGM_SAMPLE_SHIFT == 6
|
||
and a, #0x03 ; 7T L>>6
|
||
#elif VGM_SAMPLE_SHIFT == 5
|
||
and a, #0x07 ; 7T L>>5
|
||
#elif VGM_SAMPLE_SHIFT == 4
|
||
and a, #0x0F ; 7T L>>4
|
||
#endif
|
||
ld c, a ; 4T
|
||
ld a, h ; 4T
|
||
rlca ; 4T
|
||
rlca ; 4T
|
||
#if VGM_SAMPLE_SHIFT < 6
|
||
rlca ; 4T
|
||
#endif
|
||
#if VGM_SAMPLE_SHIFT < 5
|
||
rlca ; 4T
|
||
#endif
|
||
ld d, a ; 4T D = rotated H
|
||
#if VGM_SAMPLE_SHIFT == 6
|
||
and a, #0xFC ; 7T H<<2
|
||
#elif VGM_SAMPLE_SHIFT == 5
|
||
and a, #0xF8 ; 7T H<<3
|
||
#elif VGM_SAMPLE_SHIFT == 4
|
||
and a, #0xF0 ; 7T H<<4
|
||
#endif
|
||
or a, c ; 4T lo = (H<<(8-SHIFT))|(L>>SHIFT)
|
||
ld l, a ; 4T
|
||
;; high byte: (CF<<(8-SHIFT)) | (H>>SHIFT)
|
||
ld a, d ; 4T rotated H
|
||
#if VGM_SAMPLE_SHIFT == 6
|
||
and a, #0x03 ; 7T H>>6
|
||
#elif VGM_SAMPLE_SHIFT == 5
|
||
and a, #0x07 ; 7T H>>5
|
||
#elif VGM_SAMPLE_SHIFT == 4
|
||
and a, #0x0F ; 7T H>>4
|
||
#endif
|
||
or a, b ; 4T + CF<<(8-SHIFT)
|
||
ld h, a ; 4T
|
||
ld (_fb_tk), hl ;16T
|
||
ret ;10T
|
||
__endasm;
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* asm_read_byte — чтение одного байта из fb_rp, обработка page-cross
|
||
* Возвращает A = byte. Clobbers C, HL.
|
||
* Fast-path: 74 T (+ call 17 + ret 10 = 101 T итого)
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
static uint8_t asm_read_byte(void) __naked
|
||
{
|
||
__asm
|
||
ld hl, (_fb_rp) ;16 load read pointer
|
||
ld c, (hl) ; 7 byte -> C
|
||
inc hl ; 6
|
||
ld a, h ; 4
|
||
or a, l ; 4 Z if HL wrapped to 0x0000
|
||
jr Z, 150$ ; 7 page-cross (rare)
|
||
ld (_fb_rp), hl ;16 store updated pointer
|
||
ld a, c ; 4 result -> A
|
||
ret ;10 fast-path 74 T
|
||
150$:
|
||
ld hl, #_fb_cpg ;10
|
||
inc (hl) ;11
|
||
ld a, (hl) ; 7
|
||
push bc ;11 save result
|
||
call _wc_mngcvpl ;17+ switch VPL page (A = fb_cpg)
|
||
pop bc ;10
|
||
ld hl, #0xC000 ;10
|
||
ld (_fb_rp), hl ;16
|
||
ld a, c ; 4 result -> A
|
||
ret ;10
|
||
__endasm;
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* asm_read_2bytes — чтение двух последовательных байт → fb_b1, fb_b2
|
||
* Пишет напрямую в static переменные. Обрабатывает page-cross побайтно.
|
||
* Fast-path: 124 T (+ call 17 = 141 T итого)
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
static void asm_read_2bytes(void) __naked
|
||
{
|
||
__asm
|
||
ld hl, (_fb_rp) ;16
|
||
ld a, (hl) ; 7 byte1
|
||
ld (_fb_b1), a ;13
|
||
inc hl ; 6
|
||
ld a, h ; 4
|
||
or a, l ; 4
|
||
jr Z, 161$ ; 7 page-cross after byte1 (rare)
|
||
160$:
|
||
ld a, (hl) ; 7 byte2
|
||
ld (_fb_b2), a ;13
|
||
inc hl ; 6
|
||
ld a, h ; 4
|
||
or a, l ; 4
|
||
jr Z, 163$ ; 7 page-cross after byte2 (rare)
|
||
162$:
|
||
ld (_fb_rp), hl ;16
|
||
ret ;10 fast-path 124 T
|
||
161$:
|
||
ld hl, #_fb_cpg ;10
|
||
inc (hl) ;11
|
||
ld a, (hl) ; 7
|
||
call _wc_mngcvpl ;17+
|
||
ld hl, #0xC000 ;10
|
||
jr 160$ ;12
|
||
163$:
|
||
ld hl, #_fb_cpg ;10
|
||
inc (hl) ;11
|
||
ld a, (hl) ; 7
|
||
call _wc_mngcvpl ;17+
|
||
ld hl, #0xC000 ;10
|
||
ld (_fb_rp), hl ;16
|
||
ret ;10
|
||
__endasm;
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* OPL4 (ZXM-MoonSound) — прямая (не через cmd-buffer) запись в wave-порты
|
||
* #7E (выбор регистра) / #7F (данные), плюс переключение turbo-режима.
|
||
* Используются ТОЛЬКО в wave_upload_block() — разовая синхронная заливка
|
||
* PCM-сэмплов при загрузке трека, до старта ISR-плейбека (не hot-path).
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
/* out (n),a — короткая форма (2 байта). Старший байт адреса (A15-A8)
|
||
* при этом равен A (значению-аргументу), но плата его не видит: линия
|
||
* CA[7..0] у CPLD DD2 — единственный адресный вход декодера портов
|
||
* #7E/#7F/#C4-#C7 (8 бит), верхняя половина шины к нему не подключена
|
||
* (см. схему ZXM-MoonSound, dd2.tdf) — как и в родном moon_driver.asm.
|
||
*
|
||
* Перед каждой записью — опрос busy-флага (бит0 порта #C4 — тот же
|
||
* порт, что и FM Bank1 addr-select, но на чтение отдаёт статус чипа).
|
||
* Без этого ожидания запись может быть потеряна чипом (особенно в
|
||
* режиме прямого доступа к памяти) — подтверждено рабочим NedoOS-
|
||
* драйвером (common/moonsound.asm: opl4_wait), а не только datasheet. */
|
||
static void chip_wait(void) __naked {
|
||
__asm
|
||
push af
|
||
cw_loop:
|
||
in a, (0xC4)
|
||
rrca
|
||
jr c, cw_loop
|
||
pop af
|
||
ret
|
||
__endasm;
|
||
}
|
||
|
||
static void wave_out_reg(uint8_t reg) __naked {
|
||
__asm
|
||
call _chip_wait
|
||
out (0x7E), a
|
||
ret
|
||
__endasm;
|
||
(void)reg;
|
||
}
|
||
|
||
static void wave_out_dat(uint8_t val) __naked {
|
||
__asm
|
||
call _chip_wait
|
||
out (0x7F), a
|
||
ret
|
||
__endasm;
|
||
(void)val;
|
||
}
|
||
|
||
/* Быстрая заливка fb_w байт из текущей VPL-страницы. В отличие от
|
||
* asm_read_byte()+wave_out_dat() указатели и счётчик держатся в регистрах;
|
||
* обязательный busy-wait перед каждым байтом сохранён. Вызывающий код не
|
||
* даёт циклу пересечь границу страницы. */
|
||
static void wave_upload_fast(void) __naked {
|
||
__asm
|
||
ld hl, (_fb_rp)
|
||
ld bc, (_fb_w)
|
||
wuf_loop:
|
||
in a, (0xC4)
|
||
rrca
|
||
jr c, wuf_loop
|
||
ld a, (hl)
|
||
out (0x7F), a
|
||
inc hl
|
||
dec bc
|
||
ld a, b
|
||
or a, c
|
||
jr nz, wuf_loop
|
||
ld (_fb_rp), hl
|
||
ret
|
||
__endasm;
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* wave_upload_block — залить payload VGM data-block 0x67 типа 0x84
|
||
* (YMF278B ROM Image) / 0x87 (RAM Image) в SRAM карты ZXM-MoonSound.
|
||
*
|
||
* Вызывается из обработчика 0x67 в vgm_fill_buffer() ДО начала реальной
|
||
* ISR-подачи звука (все такие блоки в реальных файлах идут одним пакетом
|
||
* в самом начале потока) — поэтому синхронная блокирующая заливка здесь
|
||
* безопасна: играть ещё нечему.
|
||
*
|
||
* Формат payload (VGM spec, "ROM/RAM Image" data block, offset от начала
|
||
* data-payload, т.е. после opcode+0x66+type+4-байт общего размера):
|
||
* [0..3] ROM/RAM Size (информационное, не используется)
|
||
* [4..7] Start Address — смещение внутри SRAM карты (0-based)
|
||
* [8..] сами байты сэмпла/инструмент-заголовков
|
||
*
|
||
* Адресация чипа YMF278B: 2МБ встроенный ROM (General MIDI) + 1МБ SRAM,
|
||
* физически идущие в чипе одним адресным пространством — SRAM начинается
|
||
* сразу по границе 0x200000 (аппаратный CS чипа по адресному биту A21,
|
||
* см. datasheet, от платы не зависит). wavetblhdr=4 (4×0x80000=0x200000)
|
||
* выставлен так, чтобы номер сэмпла (wave) 384 указывал ровно на начало
|
||
* этой SRAM — тем самым абсолютный адрес = 0x200000 + Start Address.
|
||
*
|
||
* len = полный размер data-block (4-байтовое поле "size" из потока),
|
||
* включает 8-байтовый под-заголовок выше.
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
static void wave_upload_block(void) __naked
|
||
{
|
||
__asm
|
||
; ROM/RAM Size (4 bytes) is informational only.
|
||
call _asm_read_2bytes
|
||
call _asm_read_2bytes
|
||
|
||
; NEW2: FM bank 1, register 05h = 03h. Wave registers ignore writes
|
||
; until this bit is enabled, and the command ISR has not started yet.
|
||
call _chip_wait
|
||
ld a, #0x05
|
||
out (0xC6), a
|
||
call _chip_wait
|
||
ld a, #0x03
|
||
out (0xC7), a
|
||
|
||
; VGM start address is little-endian; YMF278B wants high/mid/low.
|
||
call _asm_read_byte
|
||
ld (_fb_b2), a
|
||
call _asm_read_byte
|
||
ld (_fb_b1), a
|
||
call _asm_read_byte
|
||
ld (_fb_ch), a
|
||
call _asm_read_byte ; unused top byte of the 32-bit address
|
||
|
||
; Patch 128 twelve-byte headers only for a ROM image beginning at 0.
|
||
xor a
|
||
ld (_hdr_recs), a
|
||
ld (_hdr_sub), a
|
||
ld a, (_fb_reg)
|
||
cp a, #0x84
|
||
jr nz, wub_header_ready
|
||
ld a, (_fb_ch)
|
||
ld hl, #_fb_b1
|
||
or a, (hl)
|
||
inc hl
|
||
or a, (hl)
|
||
jr nz, wub_header_ready
|
||
ld a, #0x80
|
||
ld (_hdr_recs), a
|
||
wub_header_ready:
|
||
|
||
; Memory address = 200000h + start address. Register order matters.
|
||
ld a, #0x03
|
||
call _wave_out_reg
|
||
ld a, (_fb_ch)
|
||
add a, #0x20
|
||
call _wave_out_dat
|
||
ld a, #0x04
|
||
call _wave_out_reg
|
||
ld a, (_fb_b1)
|
||
call _wave_out_dat
|
||
ld a, #0x05
|
||
call _wave_out_reg
|
||
ld a, (_fb_b2)
|
||
call _wave_out_dat
|
||
|
||
; 11h = memory access mode, WT header base 200000h; 06h = data port.
|
||
ld a, #0x02
|
||
call _wave_out_reg
|
||
ld a, #0x11
|
||
call _wave_out_dat
|
||
ld a, #0x06
|
||
call _wave_out_reg
|
||
|
||
; fb_t32 contains the whole block payload, including its 8-byte header.
|
||
ld hl, (_fb_t32)
|
||
ld de, #0x0008
|
||
or a
|
||
sbc hl, de
|
||
ld (_fb_t32), hl
|
||
jr nc, wub_len_ready
|
||
ld hl, (_fb_t32 + 2)
|
||
dec hl
|
||
ld (_fb_t32 + 2), hl
|
||
wub_len_ready:
|
||
|
||
ld a, (_hdr_recs)
|
||
or a
|
||
jr z, wub_fast_test
|
||
ld a, #0x01
|
||
ld (_opl4_rom_loaded), a
|
||
|
||
; Patch address bit 21 in byte 0 of each of the first 128 headers.
|
||
wub_patch_test:
|
||
ld a, (_hdr_recs)
|
||
or a
|
||
jr z, wub_fast_test
|
||
ld hl, #_fb_t32
|
||
ld a, (hl)
|
||
inc hl
|
||
or a, (hl)
|
||
inc hl
|
||
or a, (hl)
|
||
inc hl
|
||
or a, (hl)
|
||
jp z, wub_finish
|
||
|
||
call _asm_read_byte
|
||
ld e, a
|
||
ld a, (_hdr_sub)
|
||
or a
|
||
jr nz, wub_patch_count
|
||
ld a, e
|
||
or a, #0x20
|
||
ld e, a
|
||
wub_patch_count:
|
||
ld a, (_hdr_sub)
|
||
inc a
|
||
cp a, #0x0C
|
||
jr nz, wub_store_sub
|
||
xor a
|
||
ld (_hdr_sub), a
|
||
ld hl, #_hdr_recs
|
||
dec (hl)
|
||
jr wub_patch_write
|
||
wub_store_sub:
|
||
ld (_hdr_sub), a
|
||
wub_patch_write:
|
||
ld a, e
|
||
call _wave_out_dat
|
||
|
||
; --fb_t32. The high word changes only when the low word wraps.
|
||
ld hl, (_fb_t32)
|
||
dec hl
|
||
ld (_fb_t32), hl
|
||
ld a, h
|
||
and a, l
|
||
inc a
|
||
jr nz, wub_patch_test
|
||
ld hl, (_fb_t32 + 2)
|
||
dec hl
|
||
ld (_fb_t32 + 2), hl
|
||
jr wub_patch_test
|
||
|
||
; Upload the rest in chunks that never cross a mapped 16K page.
|
||
wub_fast_test:
|
||
ld hl, #_fb_t32
|
||
ld a, (hl)
|
||
inc hl
|
||
or a, (hl)
|
||
inc hl
|
||
or a, (hl)
|
||
inc hl
|
||
or a, (hl)
|
||
jr z, wub_finish
|
||
|
||
; fb_w = 0 - fb_rp: number of bytes through FFFFh inclusive.
|
||
ld hl, (_fb_rp)
|
||
xor a
|
||
sub a, l
|
||
ld e, a
|
||
ld a, #0x00
|
||
sbc a, h
|
||
ld d, a
|
||
ld (_fb_w), de
|
||
|
||
; If the remaining 32-bit length is smaller, use its low word.
|
||
ld hl, (_fb_t32 + 2)
|
||
ld a, h
|
||
or a, l
|
||
jr nz, wub_fast_upload
|
||
ld hl, (_fb_t32)
|
||
or a
|
||
sbc hl, de
|
||
jr nc, wub_fast_upload
|
||
ld hl, (_fb_t32)
|
||
ld (_fb_w), hl
|
||
wub_fast_upload:
|
||
call _wave_upload_fast
|
||
|
||
; fb_t32 -= fb_w, propagating carry into the high word.
|
||
ld hl, (_fb_t32)
|
||
ld de, (_fb_w)
|
||
or a
|
||
sbc hl, de
|
||
ld (_fb_t32), hl
|
||
jr nc, wub_after_subtract
|
||
ld hl, (_fb_t32 + 2)
|
||
dec hl
|
||
ld (_fb_t32 + 2), hl
|
||
wub_after_subtract:
|
||
|
||
; wave_upload_fast leaves fb_rp at zero exactly on a page boundary.
|
||
ld hl, (_fb_rp)
|
||
ld a, h
|
||
or a, l
|
||
jr nz, wub_fast_test
|
||
ld hl, #_fb_cpg
|
||
inc (hl)
|
||
ld a, (hl)
|
||
call _wc_mngcvpl
|
||
ld hl, #0xC000
|
||
ld (_fb_rp), hl
|
||
jr wub_fast_test
|
||
|
||
wub_finish:
|
||
ld a, #0x02
|
||
call _wave_out_reg
|
||
ld a, #0x10 ; sound generation, WT base 200000h
|
||
call _wave_out_dat
|
||
ret
|
||
__endasm;
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* asm_emit_skip_spec — запись CMD_SKIP_TICKS + spec_mask в cmd buffer
|
||
*
|
||
* Пишет: [CMD_SKIP_TICKS, val, activity_lo, activity_hi].
|
||
* OPL4 KeyOn mask складывается с обычным spec_mask: каналы 16..23
|
||
* сворачиваются на полосы 0..7, каналы 8..15 остаются на 8..15.
|
||
* Обнуляет spec_mask, продвигает fb_wp на 4.
|
||
* Заменяет 6-строчный C-паттерн, встречающийся 9× в fill_buffer/emit_wait.
|
||
*
|
||
* Вход: A = byte1 (tick count минус 1, или литерал).
|
||
* Clobbers: A, C, HL.
|
||
* Стоимость: ~119 T (+ call 17 = 136 T итого).
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
static void asm_emit_skip_spec(uint8_t val) __naked {
|
||
__asm
|
||
ld c, a ; 4 C = val (byte1)
|
||
ld hl, (_fb_wp) ;16
|
||
ld (hl), #0x50 ;10 CMD_SKIP_TICKS = 0x50
|
||
inc hl ; 6
|
||
ld (hl), c ; 7 val
|
||
inc hl ; 6
|
||
ld a, (_spec_wave_active)
|
||
ld c, a
|
||
ld a, (_spec_wave_active+2)
|
||
or c ; wave ch0..7 | ch16..23
|
||
ld c, a
|
||
ld a, (_spec_mask)
|
||
or c
|
||
ld (hl), a ; activity bits 0..7
|
||
inc hl ; 6
|
||
ld a, (_spec_mask+1)
|
||
ld c, a
|
||
ld a, (_spec_wave_active+1)
|
||
or c
|
||
ld (hl), a ; activity bits 8..15
|
||
inc hl ; 6
|
||
ld (_fb_wp), hl ;16 advance write pointer
|
||
;; spec_mask = 0
|
||
ld hl, #0x0000 ;10
|
||
ld (_spec_mask), hl ;16
|
||
ret ;10 total: 153 T
|
||
__endasm;
|
||
(void)val;
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* asm_emit_opl_b0 — запись CMD_WRITE_B0 + tail-call spectrum_opl_b0
|
||
*
|
||
* Читает fb_b1, fb_b2 (установлены asm_read_2bytes).
|
||
* Пишет [CMD_WRITE_B0, reg, val, x] в fb_wp, продвигает fb_wp += 4.
|
||
* Tail-call spectrum_opl_b0(A=reg, L=val) — его RET возвращает
|
||
* управление вызывающему.
|
||
*
|
||
* Стоимость: ~128 T тело + 17 call + 10 jp = 155 T итого.
|
||
* Было ~223 T (SDCC codegen): экономия ~68 T на каждую запись OPL Bank0.
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
static void asm_emit_opl_b0(void) __naked {
|
||
__asm
|
||
ld hl, (_fb_wp) ;16
|
||
ld (hl), #0x40 ;10 CMD_WRITE_B0
|
||
inc hl ; 6
|
||
ld a, (_fb_b1) ;13
|
||
ld d, a ; 4 save reg
|
||
ld (hl), a ; 7 [1] = reg
|
||
inc hl ; 6
|
||
ld a, (_fb_b2) ;13
|
||
ld (hl), a ; 7 [2] = val
|
||
inc hl ; 6
|
||
inc hl ; 6 skip [3] padding
|
||
ld (_fb_wp), hl ;16 fb_wp += 4
|
||
ld l, a ; 4 L = val (fb_b2 still in A)
|
||
ld a, d ; 4 A = reg (fb_b1)
|
||
jp _spectrum_opl_b0 ;10 tail-call
|
||
__endasm;
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* asm_emit_opl_b1 — запись CMD_WRITE_B1 + tail-call spectrum_opl_b1
|
||
* Аналогично asm_emit_opl_b0, но для OPL3 Bank 1.
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
static void asm_emit_opl_b1(void) __naked {
|
||
__asm
|
||
ld hl, (_fb_wp) ;16
|
||
ld (hl), #0x80 ;10 CMD_WRITE_B1
|
||
inc hl ; 6
|
||
ld a, (_fb_b1) ;13
|
||
ld d, a ; 4
|
||
ld (hl), a ; 7
|
||
inc hl ; 6
|
||
ld a, (_fb_b2) ;13
|
||
ld (hl), a ; 7
|
||
inc hl ; 6
|
||
inc hl ; 6
|
||
ld (_fb_wp), hl ;16
|
||
ld l, a ; 4
|
||
ld a, d ; 4
|
||
jp _spectrum_opl_b1 ;10 tail-call
|
||
__endasm;
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* asm_emit_saa — SAA1099 0xBD handler (inline asm)
|
||
*
|
||
* Вызывается ПОСЛЕ asm_read_2bytes() (fb_b1 = reg, fb_b2 = val).
|
||
*
|
||
* Логика:
|
||
* fb_is_vgm==0 (cmdblk) → emit raw CMD_WRITE_SAA, return
|
||
* VGM mode:
|
||
* bit7=1 (chip1):
|
||
* cfg_saa_mode==0 → skip (return)
|
||
* cfg_saa_mode==1 → strip bit7, emit, spectrum_saa
|
||
* cfg_saa_mode==2 → emit raw, spectrum_saa2
|
||
* bit7=0 (chip0):
|
||
* cfg_saa_mode==1 → skip (return)
|
||
* else → emit raw, spectrum_saa
|
||
*
|
||
* CMD_WRITE_SAA = 0x60. spectrum_saa/saa2: A=reg, L=val.
|
||
* Clobbers: A, B, C, D, E, H, L.
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
static void asm_emit_saa(void) __naked {
|
||
__asm
|
||
|
||
;; D = reg (fb_b1), E = val (fb_b2)
|
||
ld a, (_fb_b1) ;13
|
||
ld d, a ; 4
|
||
ld a, (_fb_b2) ;13
|
||
ld e, a ; 4
|
||
|
||
;; cmdblk (fb_is_vgm==0) → emit raw, no spectrum
|
||
ld a, (_fb_is_vgm) ;13
|
||
or a, a ; 4
|
||
jr nz, 200$ ;12/7
|
||
call 250$
|
||
ret
|
||
|
||
200$:
|
||
;; VGM mode — A = cfg_saa_mode
|
||
ld a, (_cfg_saa_mode) ;13
|
||
bit 7, d ; 8
|
||
jr nz, 210$ ;12/7 chip 1
|
||
|
||
;; ── Chip 0: skip if mode==1 ──
|
||
cp a, #1 ; 7
|
||
ret z ; skip chip0 in mode 1
|
||
jr 230$ ; → emit + spectrum_saa
|
||
|
||
210$:
|
||
;; ── Chip 1: skip if mode==0 ──
|
||
or a, a ; 4
|
||
ret z ; skip chip1 in mode 0
|
||
cp a, #2 ; 7
|
||
jr z, 220$ ; → turbo
|
||
|
||
;; mode 1: strip bit7 → play on chip 0
|
||
res 7, d ; 8
|
||
;; fall through to 230$ (emit + spectrum_saa)
|
||
|
||
230$:
|
||
;; emit D,E → spectrum_saa(A=reg, L=val)
|
||
call 250$
|
||
ld a, d ; 4
|
||
ld l, e ; 4
|
||
jp _spectrum_saa ;10
|
||
|
||
220$:
|
||
;; turbo: emit raw → spectrum_saa2(A=reg&0x7F, L=val)
|
||
call 250$
|
||
ld a, d ; 4
|
||
and a, #0x7F ; 7
|
||
ld l, e ; 4
|
||
jp _spectrum_saa2 ;10
|
||
|
||
;; ── shared: write [0x60, D, E, pad] to fb_wp ──
|
||
250$:
|
||
ld hl, (_fb_wp) ;16
|
||
ld (hl), #0x60 ;10 CMD_WRITE_SAA
|
||
inc hl ; 6
|
||
ld (hl), d ; 7
|
||
inc hl ; 6
|
||
ld (hl), e ; 7
|
||
inc hl ; 6
|
||
inc hl ; 6
|
||
ld (_fb_wp), hl ;16
|
||
ret ;10
|
||
|
||
__endasm;
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* asm_scale_fm — FM F-Number масштабирование: fb_scaled = fb_scaled × fm_mul / 7
|
||
*
|
||
* Все используемые FM ratios сводятся к N/7:
|
||
* fm_mul=0 → bypass (NTSC 45/44 ≈ 1.023, в рамках 5% допуска)
|
||
* fm_mul=3 → ×3/7 (YM2203 @ 1.5 МГц)
|
||
* fm_mul=5 → ×5/7 (YM2203 @ 2.5 МГц)
|
||
* fm_mul=6 → ×6/7 (YM2203 @ 3 МГц)
|
||
* fm_mul=8 → ×8/7 (YM2203 @ 4 МГц)
|
||
*
|
||
* Умножение: shifts + adds (~30 T).
|
||
* Деление на 7: binary long division 16 итераций (~770 T).
|
||
* Итого ~840 T — против ~1200 T у SDCC __muluint + __divuint.
|
||
*
|
||
* Вход: fb_scaled = fnum (11-bit, 0..2047).
|
||
* Выход: fb_scaled = fnum × fm_mul / 7.
|
||
* Clobbers: A, B, D, E, H, L.
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
static void asm_scale_fm(void) __naked {
|
||
__asm
|
||
ld a, (_fm_mul)
|
||
or a
|
||
ret z ; fm_mul=0 → bypass
|
||
|
||
ld hl, (_fb_scaled) ; HL = fnum
|
||
|
||
;; ── Умножение fnum x N (dispatch по fm_mul) ──
|
||
cp #3
|
||
jr z, sfm_mul3
|
||
cp #5
|
||
jr z, sfm_mul5
|
||
cp #6
|
||
jr z, sfm_mul6
|
||
;; fm_mul=8: x8
|
||
add hl, hl ; x2
|
||
add hl, hl ; x4
|
||
add hl, hl ; x8
|
||
jr sfm_div7
|
||
|
||
sfm_mul3:
|
||
ld d, h
|
||
ld e, l ; DE = fnum
|
||
add hl, hl ; x2
|
||
add hl, de ; x3
|
||
jr sfm_div7
|
||
|
||
sfm_mul5:
|
||
ld d, h
|
||
ld e, l ; DE = fnum
|
||
add hl, hl ; x2
|
||
add hl, hl ; x4
|
||
add hl, de ; x5
|
||
jr sfm_div7
|
||
|
||
sfm_mul6:
|
||
ld d, h
|
||
ld e, l ; DE = fnum
|
||
add hl, hl ; x2
|
||
add hl, de ; x3
|
||
add hl, hl ; x6
|
||
;; fall through
|
||
|
||
;; ── Деление HL на 7, результат в HL ──────────
|
||
;; Binary long division: 16-bit dividend, 8-bit divisor.
|
||
;; Остаток в A (всегда 0..6, помещается в 8 бит).
|
||
;; Максимум: 2047x8 = 16376; 16376/7 = 2339.
|
||
sfm_div7:
|
||
xor a ; A = remainder = 0
|
||
ld b, #16 ; 16 бит
|
||
sfm_dloop:
|
||
add hl, hl ; shift dividend/quotient left, MSB → CF
|
||
rla ; CF → remainder bit 0
|
||
cp #7 ; remainder >= 7?
|
||
jr c, sfm_dskip ; нет → пропуск
|
||
sub #7 ; remainder -= 7
|
||
inc l ; set quotient bit
|
||
sfm_dskip:
|
||
djnz sfm_dloop ; next bit
|
||
|
||
ld (_fb_scaled), hl ; store result
|
||
ret
|
||
__endasm;
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* emit_wait — вставить паузу tk тиков (ISR)
|
||
*
|
||
* Использует два механизма паузы:
|
||
* CMD_SKIP_TICKS(N) — N=0..ISR_TICKS_PER_FRAME-1, пауза N+1 тик.
|
||
* ISR не вызывается (экономия CPU for main loop).
|
||
* CMD_WAIT(val) — обычная пауза val+1 тик (ISR функционирует).
|
||
*
|
||
* ИНВАРИАНТ: значение N для CMD_SKIP_TICKS строго < ISR_TICKS_PER_FRAME.
|
||
* N ≥ ISR_TICKS_PER_FRAME → полный оборот pos_table → ISR промахивается
|
||
* мимо своей INT-позиции → каскадный пропуск кадров, падение темпа ×10+.
|
||
*
|
||
* Стратегия:
|
||
* Для пауз ≤ ISR_TICKS_PER_FRAME: CMD_SKIP_TICKS (1 команда, ISR молчит)
|
||
* Для пауз > ISR_TICKS_PER_FRAME: CMD_WAIT(основная часть) + CMD_SKIP_TICKS(≤ ISR_TICKS_PER_FRAME-1)
|
||
* Перед секундным рубежом: аналогично + CMD_INC_SEC
|
||
*
|
||
* Макс. расход буфера на один вызов (для tk≤4095):
|
||
* 2 sec boundary × 12 + remainder 8 = 32 байт
|
||
* Зазор: вызывающий обязан обеспечить off < CMD_BUF_SIZE - 48
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
static void emit_wait(uint16_t tk)
|
||
{
|
||
while (tk > 0)
|
||
{
|
||
/* ── SEC-рубеж: бюджет помещается в остаток ─────────── */
|
||
if (vgm_sec_budget <= tk)
|
||
{
|
||
fb_w = vgm_sec_budget;
|
||
|
||
if (fb_w > ISR_TICKS_PER_FRAME) {
|
||
/* Большой бюджет: CMD_WAIT(основная часть) + CMD_SKIP_TICKS */
|
||
fb_w -= ISR_TICKS_PER_FRAME;
|
||
fb_wp[0] = CMD_WAIT;
|
||
fb_wp[1] = (uint8_t)(fb_w - 1);
|
||
fb_wp[2] = (uint8_t)((fb_w - 1) >> 8);
|
||
fb_wp += 4;
|
||
asm_emit_skip_spec(ISR_TICKS_PER_FRAME - 1);
|
||
} else if (fb_w >= 1) {
|
||
asm_emit_skip_spec((uint8_t)(fb_w - 1));
|
||
}
|
||
/* CMD_INC_SEC */
|
||
fb_wp[0] = CMD_INC_SEC;
|
||
fb_wp += 4;
|
||
|
||
tk -= vgm_sec_budget;
|
||
vgm_sec_budget = ISR_FREQ;
|
||
continue;
|
||
}
|
||
|
||
/* ── Остаток: tk < sec_budget ───────────────────────── */
|
||
if (tk > ISR_TICKS_PER_FRAME) {
|
||
/* Более кадра: CMD_WAIT(основная часть) + CMD_SKIP_TICKS(остаток) */
|
||
fb_w = tk - ISR_TICKS_PER_FRAME;
|
||
fb_wp[0] = CMD_WAIT;
|
||
fb_wp[1] = (uint8_t)(fb_w - 1);
|
||
fb_wp[2] = (uint8_t)((fb_w - 1) >> 8);
|
||
fb_wp += 4;
|
||
vgm_sec_budget -= fb_w;
|
||
tk = ISR_TICKS_PER_FRAME;
|
||
}
|
||
/* Последние ≤ISR_TICKS_PER_FRAME тиков через SKIP (экономия CPU) */
|
||
asm_emit_skip_spec((uint8_t)(tk - 1));
|
||
vgm_sec_budget -= tk;
|
||
tk = 0;
|
||
}
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* vgm_fill_buffer (v5 — HL queue integrated)
|
||
*
|
||
* Заполняет один командный буфер ISR целиком (512 байт), управляя
|
||
* переходами между высокоуровневыми командами (HL queue):
|
||
*
|
||
* HLCMD_CMDBLK — подключает страницу cmdblocks, парсит VGM-опкоды
|
||
* блока в ISR-команды; при 0x66 переходит к следующей
|
||
* HL-команде и продолжает заполнять ТОТ ЖЕ буфер.
|
||
* HLCMD_PLAY — воспроизводит VGM из мегабуфера; при заполнении
|
||
* буфера сохраняет состояние и выходит; при 0x66
|
||
* переходит дальше по очереди.
|
||
* HLCMD_LOOP — перематывает к loop-точке, морфирует в PLAY.
|
||
* HLCMD_ISR_DONE— пишет CMD_ISR_DONE в буфер, выходит досрочно.
|
||
*
|
||
* EOF-проверка удалена из горячего цикла — при загрузке в конец
|
||
* VGM-данных вписывается 0x66-сентинел (см. main.c write_vgm_sentinel).
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
|
||
void vgm_fill_buffer(uint8_t buf_idx)
|
||
{
|
||
fb_buf = buf_idx ? cmd_buf_b : cmd_buf_a;
|
||
fb_wp = fb_buf;
|
||
fb_end = fb_buf + (CMD_BUF_SIZE - 48);
|
||
|
||
/* Пауза — заполнить тишиной */
|
||
if (vgm_paused) {
|
||
fb_buf[0] = CMD_SKIP_TICKS; fb_buf[1] = ISR_TICKS_PER_FRAME - 1;
|
||
fb_buf[4] = CMD_END_BUF;
|
||
return;
|
||
}
|
||
|
||
fb_wacc = vgm_wait_accum;
|
||
|
||
/* ── Макрос: проверка границы страницы после fb_rp++ ───────────
|
||
* При inc HL через #FFFF -> #0000 переключаем VPL-страницу.
|
||
* Fast-path (99.99 %): одна проверка HL==0, ~15 T-states. */
|
||
#define PAGE_CHK() do { \
|
||
if (!(uint16_t)(fb_rp)) { fb_cpg++; wc_mngcvpl(fb_cpg); fb_rp = (uint8_t *)0xC000; } \
|
||
} while(0)
|
||
|
||
/* ═══════════════════════════════════════════════════════════════
|
||
* next_hl — диспетчер высокоуровневых команд
|
||
* Вызывается при входе и при каждом 0x66 (конец блока/файла).
|
||
* НЕ выходит из функции — продолжает заполнять тот же буфер.
|
||
* ═══════════════════════════════════════════════════════════════ */
|
||
next_hl:
|
||
if (vgm_hl_pos >= vgm_hl_len) {
|
||
/* Очередь исчерпана — заполнить остаток тишиной */
|
||
if (fb_wp == fb_buf) {
|
||
fb_buf[0] = CMD_SKIP_TICKS; fb_buf[1] = ISR_TICKS_PER_FRAME - 1;
|
||
fb_wp = fb_buf + 4;
|
||
}
|
||
goto finish;
|
||
}
|
||
|
||
fb_e = &vgm_hl_queue[vgm_hl_pos];
|
||
|
||
// обработка высокоуровневой команды — установка fb_rp, fb_cpg, fb_is_vgm
|
||
switch (fb_e->cmd) {
|
||
|
||
case HLCMD_CMDBLK:
|
||
/* Подключить страницу cmdblocks, найти адрес блока */
|
||
wc_mngc_pl(CMDBLK_PAGE);
|
||
fb_rp = (uint8_t *)((const uint16_t *)0xC000)[fb_e->param];
|
||
fb_cpg = 0; /* не используется для cmdblk, PAGE_CHK не сработает */
|
||
fb_is_vgm = 0;
|
||
break;
|
||
|
||
case HLCMD_PLAY:
|
||
fb_cpg = vgm_cur_page;
|
||
fb_rp = (uint8_t *)vgm_read_ptr;
|
||
wc_mngcvpl(fb_cpg);
|
||
/* Переподключить LUT: WC API (print_line и т.д.) мог перемапить Window 0 */
|
||
if (vgm_freq_mode == FREQ_MODE_TABLE)
|
||
wc_mng0_pl(freq_lut_page);
|
||
fb_is_vgm = 1;
|
||
break;
|
||
|
||
case HLCMD_LOOP:
|
||
if (!vgm_rewind_to_loop()) {
|
||
/* Нет loop-точки — пропустить */
|
||
vgm_hl_pos++;
|
||
goto next_hl;
|
||
}
|
||
vgm_loop_count++;
|
||
fb_e->cmd = HLCMD_PLAY; /* чтобы не перематывать снова */
|
||
fb_cpg = vgm_cur_page;
|
||
fb_rp = (uint8_t *)vgm_read_ptr;
|
||
fb_wacc = vgm_wait_accum;
|
||
wc_mngcvpl(fb_cpg);
|
||
if (vgm_freq_mode == FREQ_MODE_TABLE)
|
||
wc_mng0_pl(freq_lut_page);
|
||
fb_is_vgm = 1;
|
||
break;
|
||
|
||
case HLCMD_ISR_DONE:
|
||
fb_wp[0] = CMD_ISR_DONE;
|
||
fb_wp += 4;
|
||
vgm_hl_pos++;
|
||
goto finish;
|
||
|
||
default:
|
||
vgm_hl_pos++;
|
||
goto next_hl;
|
||
}
|
||
|
||
fb_budget = VGM_FILL_CMD_BUDGET;
|
||
fb_yield_ticks = 0;
|
||
|
||
/* ═══════════════════════════════════════════════════════════════
|
||
* Горячий цикл — разбор VGM-опкодов → ISR-команды
|
||
* EOF-проверка удалена (гарантирован 0x66 в данных).
|
||
* ═══════════════════════════════════════════════════════════════ */
|
||
while (fb_wp < fb_end) // зазор 48 байт для emit_wait (макс. 32 байта) + CMD_END_BUF (4 байта) + safety
|
||
{
|
||
/* ── Читаем опкод ──────────────────────────────────────── */
|
||
fb_op = asm_read_byte();
|
||
|
||
/* ═══════ OPL2 запись (0x5A) — самая частая ═══════ */
|
||
if (fb_op == 0x5A)
|
||
{
|
||
asm_read_2bytes();
|
||
asm_emit_opl_b0();
|
||
goto do_budget;
|
||
}
|
||
|
||
/* ═══════ OPL3 Bank 1 (0x5F) ═══════ */
|
||
if (fb_op == 0x5F)
|
||
{
|
||
asm_read_2bytes();
|
||
asm_emit_opl_b1();
|
||
goto do_budget;
|
||
}
|
||
|
||
/* ═══════ OPL Bank 0: 0x5B(YM3526) / 0x5C(Y8950) / 0x5E(OPL3bk0) ═══════
|
||
* Диапазон 0x5B..0x5E — одна проверка, все → bank0.
|
||
* 0x5D (YMZ280B) не поддерживается — если встретится, запись в bank0 безвредна. */
|
||
if ((uint8_t)(fb_op - 0x5B) <= (0x5E - 0x5B))
|
||
{
|
||
asm_read_2bytes();
|
||
asm_emit_opl_b0();
|
||
goto do_budget;
|
||
}
|
||
|
||
/* ═══════ YMF278B / OPL4 (0xD0) — port pp, reg aa, data dd ═══════
|
||
* pp bit7 = chip select (dual-chip; на ZXM-MoonSound один чип — игн.)
|
||
* pp & 0x7F: 0 = FM Bank0 (те же регистры, что 0x5E) → CMD_WRITE_B0
|
||
* 1 = FM Bank1 (те же регистры, что 0x5F) → CMD_WRITE_B1
|
||
* 2 = Wave-часть (порты #7E/#7F) → CMD_WRITE_WAVE
|
||
* прочее — зарезервировано, молча пропускается */
|
||
if (fb_op == 0xD0)
|
||
{
|
||
fb_reg = asm_read_byte();
|
||
asm_read_2bytes();
|
||
fb_reg &= 0x7F;
|
||
if (fb_reg == 0)
|
||
asm_emit_opl_b0();
|
||
else if (fb_reg == 1)
|
||
asm_emit_opl_b1();
|
||
else if (fb_reg == 2) {
|
||
/* Управляющие регистры #00-#07 не отдаём из VGM на карту:
|
||
* загрузчик сам держит #02=0x10. После переноса ROM image
|
||
* отображаем исходные номера волн на 128 SRAM-заголовков
|
||
* (точно как opl4writewavemusiconly в NedoOS). */
|
||
if (fb_b1 < 0x08)
|
||
goto do_budget;
|
||
if (opl4_rom_loaded) {
|
||
if (fb_b1 < 0x20)
|
||
fb_b2 |= 0x80; /* low byte: 0x80..0xFF */
|
||
else if (fb_b1 < 0x38)
|
||
fb_b2 |= 0x01; /* high bit: wave 384..511 */
|
||
}
|
||
fb_wp[0] = CMD_WRITE_WAVE;
|
||
fb_wp[1] = fb_b1;
|
||
fb_wp[2] = fb_b2;
|
||
fb_wp += 4;
|
||
spectrum_wave(fb_b1, fb_b2);
|
||
}
|
||
goto do_budget;
|
||
}
|
||
|
||
/* ═══════ Frame wait 1/60 (0x62) ═══════ */
|
||
if (fb_op == 0x62)
|
||
{ asm_wait_62(); goto do_wait; }
|
||
|
||
/* ═══════ Frame wait 1/50 (0x63) ═══════ */
|
||
if (fb_op == 0x63)
|
||
{ asm_wait_63(); goto do_wait; }
|
||
|
||
/* ═══════ Короткая пауза 1-16 samples (0x70-0x7F) ═══════ */
|
||
#ifndef VGM_NO_SHORT_WAITS
|
||
if ((fb_op & 0xF0) == 0x70)
|
||
{ asm_short_wait(); goto do_wait; }
|
||
#else
|
||
/* VGM_NO_SHORT_WAITS: пропуск коротких пауз 0x70-0x7F */
|
||
if ((fb_op & 0xF0) == 0x70) continue;
|
||
#endif
|
||
|
||
/* ═══════ Произвольная пауза (0x61) ═══════ */
|
||
if (fb_op == 0x61)
|
||
{
|
||
asm_read_2bytes();
|
||
asm_arb_wait();
|
||
goto do_wait;
|
||
}
|
||
|
||
/* ═══════ YM2203 SSG+FM (0x55) ═══════ */
|
||
if (fb_op == 0x55)
|
||
{
|
||
asm_read_2bytes();
|
||
spectrum_ay(fb_b1, fb_b2);
|
||
if (vgm_freq_mode != FREQ_MODE_NATIVE) {
|
||
/* ── PSG tone period (reg 0x00-0x05): 12-bit, парами lo/hi ── */
|
||
if (fb_b1 <= 0x05) {
|
||
fb_ch = fb_b1 >> 1; /* 0,1,2 */
|
||
if (fb_b1 & 1) {
|
||
/* hi-nibble → обновить shadow, пересчитать полные 12 бит */
|
||
psg_shadow[fb_ch] = (psg_shadow[fb_ch] & 0x00FF)
|
||
| ((uint16_t)(fb_b2 & 0x0F) << 8);
|
||
} else {
|
||
/* lo-byte → обновить shadow */
|
||
psg_shadow[fb_ch] = (psg_shadow[fb_ch] & 0x0F00) | fb_b2;
|
||
}
|
||
fb_scaled = freq_lut_base[psg_shadow[fb_ch]];
|
||
if (fb_scaled > 0x0FFF) fb_scaled = 0x0FFF;
|
||
/* Отправляем оба lo+hi: scaled меняет оба байта одновременно */
|
||
fb_wp[0] = CMD_WRITE_AY;
|
||
fb_wp[1] = fb_ch << 1; /* reg = 0,2,4 */
|
||
fb_wp[2] = (uint8_t)(fb_scaled & 0xFF);
|
||
fb_wp += 4;
|
||
fb_wp[0] = CMD_WRITE_AY;
|
||
fb_wp[1] = (fb_ch << 1) | 1; /* reg = 1,3,5 */
|
||
fb_wp[2] = (uint8_t)((fb_scaled >> 8) & 0x0F);
|
||
fb_wp += 4;
|
||
goto do_budget;
|
||
}
|
||
/* ── Noise period (reg 0x06): 5-bit, clamp ── */
|
||
else if (fb_b1 == 0x06) {
|
||
fb_b2 = (uint8_t)freq_lut_base[fb_b2 & 0x1F];
|
||
if (fb_b2 > 31) fb_b2 = 31;
|
||
}
|
||
/* ── FM F-Number (regs 0xA0-0xA2 lo, 0xA4-0xA6 hi+block) ── */
|
||
else if (fm_mul && fb_b1 >= 0xA0 && fb_b1 <= 0xA6) {
|
||
uint8_t fmr = fb_b1 - 0xA0; /* 0..6 */
|
||
if (fmr <= 2) {
|
||
fb_ch = fmr;
|
||
fm_fnum[fb_ch] = (fm_fnum[fb_ch] & 0x0700) | fb_b2;
|
||
} else if (fmr >= 4) {
|
||
fb_ch = fmr - 4;
|
||
fm_block[fb_ch] = (fb_b2 >> 3) & 0x07;
|
||
fm_fnum[fb_ch] = (fm_fnum[fb_ch] & 0x00FF)
|
||
| ((uint16_t)(fb_b2 & 0x07) << 8);
|
||
} else {
|
||
goto ym1_fm_passthru; /* 0xA3 */
|
||
}
|
||
{
|
||
uint8_t blk = fm_block[fb_ch];
|
||
fb_scaled = fm_fnum[fb_ch];
|
||
asm_scale_fm();
|
||
if (fb_scaled > 0x7FF) {
|
||
fb_scaled >>= 1;
|
||
if (fb_scaled > 0x7FF) fb_scaled = 0x7FF;
|
||
blk++;
|
||
if (blk > 7) blk = 7;
|
||
}
|
||
fb_wp[0] = CMD_WRITE_AY;
|
||
fb_wp[1] = 0xA4 + fb_ch;
|
||
fb_wp[2] = (blk << 3) | (uint8_t)((fb_scaled >> 8) & 0x07);
|
||
fb_wp += 4;
|
||
if (fmr <= 2) {
|
||
fb_wp[0] = CMD_WRITE_AY;
|
||
fb_wp[1] = 0xA0 + fb_ch;
|
||
fb_wp[2] = (uint8_t)(fb_scaled & 0xFF);
|
||
fb_wp += 4;
|
||
}
|
||
goto do_budget;
|
||
}
|
||
}
|
||
ym1_fm_passthru:;
|
||
}
|
||
fb_wp[0] = CMD_WRITE_AY;
|
||
fb_wp[1] = fb_b1; fb_wp[2] = fb_b2;
|
||
fb_wp += 4;
|
||
goto do_budget;
|
||
}
|
||
|
||
/* ═══════ YM2203 #2 SSG+FM (0xA5) ═══════
|
||
* VGM dual chip: 0x55 → 0xA5 (последняя цифра 0x5n → 0xAn) */
|
||
if (fb_op == 0xA5)
|
||
{
|
||
asm_read_2bytes();
|
||
spectrum_ay2(fb_b1, fb_b2);
|
||
if (vgm_freq_mode != FREQ_MODE_NATIVE) {
|
||
if (fb_b1 <= 0x05) {
|
||
fb_ch = fb_b1 >> 1;
|
||
if (fb_b1 & 1) {
|
||
psg_shadow2[fb_ch] = (psg_shadow2[fb_ch] & 0x00FF)
|
||
| ((uint16_t)(fb_b2 & 0x0F) << 8);
|
||
} else {
|
||
psg_shadow2[fb_ch] = (psg_shadow2[fb_ch] & 0x0F00) | fb_b2;
|
||
}
|
||
fb_scaled = freq_lut_base[psg_shadow2[fb_ch]];
|
||
if (fb_scaled > 0x0FFF) fb_scaled = 0x0FFF;
|
||
fb_wp[0] = CMD_WRITE_AY2;
|
||
fb_wp[1] = fb_ch << 1;
|
||
fb_wp[2] = (uint8_t)(fb_scaled & 0xFF);
|
||
fb_wp += 4;
|
||
fb_wp[0] = CMD_WRITE_AY2;
|
||
fb_wp[1] = (fb_ch << 1) | 1;
|
||
fb_wp[2] = (uint8_t)((fb_scaled >> 8) & 0x0F);
|
||
fb_wp += 4;
|
||
goto do_budget;
|
||
}
|
||
else if (fb_b1 == 0x06) {
|
||
fb_b2 = (uint8_t)freq_lut_base[fb_b2 & 0x1F];
|
||
if (fb_b2 > 31) fb_b2 = 31;
|
||
}
|
||
/* ── FM F-Number чип 2 (regs 0xA0-0xA6) ── */
|
||
else if (fm_mul && fb_b1 >= 0xA0 && fb_b1 <= 0xA6) {
|
||
uint8_t fmr = fb_b1 - 0xA0;
|
||
if (fmr <= 2) {
|
||
fb_ch = fmr;
|
||
fm_fnum2[fb_ch] = (fm_fnum2[fb_ch] & 0x0700) | fb_b2;
|
||
} else if (fmr >= 4) {
|
||
fb_ch = fmr - 4;
|
||
fm_block2[fb_ch] = (fb_b2 >> 3) & 0x07;
|
||
fm_fnum2[fb_ch] = (fm_fnum2[fb_ch] & 0x00FF)
|
||
| ((uint16_t)(fb_b2 & 0x07) << 8);
|
||
} else {
|
||
goto ym2_fm_passthru;
|
||
}
|
||
{
|
||
uint8_t blk = fm_block2[fb_ch];
|
||
fb_scaled = fm_fnum2[fb_ch];
|
||
asm_scale_fm();
|
||
if (fb_scaled > 0x7FF) {
|
||
fb_scaled >>= 1;
|
||
if (fb_scaled > 0x7FF) fb_scaled = 0x7FF;
|
||
blk++;
|
||
if (blk > 7) blk = 7;
|
||
}
|
||
fb_wp[0] = CMD_WRITE_AY2;
|
||
fb_wp[1] = 0xA4 + fb_ch;
|
||
fb_wp[2] = (blk << 3) | (uint8_t)((fb_scaled >> 8) & 0x07);
|
||
fb_wp += 4;
|
||
if (fmr <= 2) {
|
||
fb_wp[0] = CMD_WRITE_AY2;
|
||
fb_wp[1] = 0xA0 + fb_ch;
|
||
fb_wp[2] = (uint8_t)(fb_scaled & 0xFF);
|
||
fb_wp += 4;
|
||
}
|
||
goto do_budget;
|
||
}
|
||
}
|
||
ym2_fm_passthru:;
|
||
}
|
||
fb_wp[0] = CMD_WRITE_AY2;
|
||
fb_wp[1] = fb_b1; fb_wp[2] = fb_b2;
|
||
fb_wp += 4;
|
||
goto do_budget;
|
||
}
|
||
|
||
/* ═══════ AY8910 dual (0xA0) ═══════ */
|
||
if (fb_op == 0xA0)
|
||
{
|
||
asm_read_2bytes();
|
||
if (fb_b1 & 0x80)
|
||
spectrum_ay2(fb_b1 & 0x7F, fb_b2);
|
||
else
|
||
spectrum_ay(fb_b1 & 0x7F, fb_b2);
|
||
if (vgm_freq_mode != FREQ_MODE_NATIVE) {
|
||
fb_reg = fb_b1 & 0x7F;
|
||
/* PSG tone period (reg 0-5): полный 12-бит через shadow */
|
||
if (fb_reg <= 0x05) {
|
||
fb_ch = fb_reg >> 1;
|
||
if (fb_reg & 1) {
|
||
psg_shadow[fb_ch] = (psg_shadow[fb_ch] & 0x00FF)
|
||
| ((uint16_t)(fb_b2 & 0x0F) << 8);
|
||
} else {
|
||
psg_shadow[fb_ch] = (psg_shadow[fb_ch] & 0x0F00) | fb_b2;
|
||
}
|
||
fb_scaled = freq_lut_base[psg_shadow[fb_ch]];
|
||
if (fb_scaled > 0x0FFF) fb_scaled = 0x0FFF;
|
||
/* AY (0xA0): отправляем оба байта сразу */
|
||
fb_wp[0] = (fb_b1 & 0x80) ? CMD_WRITE_AY2 : CMD_WRITE_AY;
|
||
fb_wp[1] = fb_ch << 1; /* lo reg */
|
||
fb_wp[2] = (uint8_t)(fb_scaled & 0xFF);
|
||
fb_wp += 4;
|
||
fb_wp[0] = (fb_b1 & 0x80) ? CMD_WRITE_AY2 : CMD_WRITE_AY;
|
||
fb_wp[1] = (fb_ch << 1) | 1; /* hi reg */
|
||
fb_wp[2] = (uint8_t)((fb_scaled >> 8) & 0x0F);
|
||
fb_wp += 4;
|
||
goto do_budget;
|
||
}
|
||
/* Noise period (reg 6) */
|
||
else if (fb_reg == 0x06) {
|
||
fb_b2 = (uint8_t)freq_lut_base[fb_b2 & 0x1F];
|
||
if (fb_b2 > 31) fb_b2 = 31;
|
||
}
|
||
}
|
||
|
||
fb_wp[0] = (fb_b1 & 0x80) ? CMD_WRITE_AY2 : CMD_WRITE_AY;
|
||
fb_wp[1] = fb_b1 & 0x7F;
|
||
fb_wp[2] = fb_b2;
|
||
fb_wp += 4;
|
||
goto do_budget;
|
||
}
|
||
|
||
/* ═══════ SAA1099 dual (0xBD) ═══════ */
|
||
/* Bit7 of reg byte selects chip (0=chip0, 1=chip1).
|
||
* cfg_saa_mode: 0=chip0 only(default), 1=chip1 on chip0, 2=turbo(both).
|
||
* cmdblk mode (fb_is_vgm==0): bypass filtering, pass raw. */
|
||
if (fb_op == 0xBD)
|
||
{
|
||
asm_read_2bytes();
|
||
asm_emit_saa();
|
||
goto do_budget;
|
||
}
|
||
|
||
/* ═══════ Конец данных (0x66) ═══════ */
|
||
if (fb_op == 0x66)
|
||
{
|
||
if (fb_is_vgm) {
|
||
vgm_cur_page = fb_cpg;
|
||
vgm_read_ptr = (uint16_t)fb_rp;
|
||
vgm_wait_accum = fb_wacc;
|
||
}
|
||
vgm_hl_pos++;
|
||
/* Финальная silence-цепочка должна начинаться с пустого
|
||
* ISR-буфера. Иначе большой cmdblock может быть разрезан
|
||
* границей буфера и при следующем fill стартует сначала. */
|
||
if (fb_is_vgm && vgm_hl_pos == vgm_hl_abort_pos) {
|
||
/* ISR замрёт здесь после исполнения всего музыкального
|
||
* хвоста. Main затем запустит shutdown как при ESC. */
|
||
fb_wp[0] = CMD_FINALIZE;
|
||
fb_wp += 4;
|
||
goto finish;
|
||
}
|
||
goto next_hl;
|
||
}
|
||
|
||
/* ═══════ Data block (0x67) — редко ═══════ */
|
||
if (fb_op == 0x67)
|
||
{
|
||
fb_rp++; PAGE_CHK(); /* 0x66 (compat байт) */
|
||
fb_reg = asm_read_byte(); /* тип блока */
|
||
asm_read_2bytes();
|
||
fb_t32 = (uint16_t)fb_b1 | ((uint16_t)fb_b2 << 8);
|
||
asm_read_2bytes();
|
||
fb_t32 |= ((uint32_t)fb_b1 << 16) | ((uint32_t)fb_b2 << 24);
|
||
|
||
if (fb_reg == 0x84 || fb_reg == 0x87) {
|
||
/* YMF278B ROM/RAM Image — залить payload в SRAM карты */
|
||
wave_upload_block();
|
||
} else {
|
||
/* сохранить → vgm_skip → восстановить */
|
||
vgm_read_ptr = (uint16_t)fb_rp;
|
||
vgm_cur_page = fb_cpg;
|
||
vgm_skip(fb_t32);
|
||
fb_rp = (uint8_t *)vgm_read_ptr;
|
||
fb_cpg = vgm_cur_page;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
/* ═══════ PCM RAM write (0x68) — редко ═══════ */
|
||
if (fb_op == 0x68)
|
||
{
|
||
vgm_read_ptr = (uint16_t)fb_rp;
|
||
vgm_cur_page = fb_cpg;
|
||
vgm_skip(11);
|
||
fb_rp = (uint8_t *)vgm_read_ptr;
|
||
fb_cpg = vgm_cur_page;
|
||
continue;
|
||
}
|
||
|
||
/* ═══════ DAC Stream (0x90-0x95) — редко ═══════ */
|
||
if (fb_op >= 0x90 && fb_op <= 0x95)
|
||
{
|
||
static const uint8_t dac_len[] = {4, 4, 5, 10, 1, 4};
|
||
fb_n = dac_len[fb_op - 0x90];
|
||
while (fb_n--) { fb_rp++; PAGE_CHK(); }
|
||
continue;
|
||
}
|
||
|
||
/* ═══════ Общий пропуск (все остальные команды) ═══════ */
|
||
/* Размеры операндов по VGM spec v1.71.
|
||
* Все явно обработанные опкоды (0x5A,0x5B,..,0xA0,0xBD,
|
||
* 0x61-0x63,0x66-0x68,0x7n,0x90-0x95) сюда не попадают. */
|
||
{
|
||
fb_n = 0;
|
||
if (fb_op < 0x30) fb_n = 0; /* ниже 0x30 — нет данных */
|
||
else if (fb_op < 0x40) fb_n = 1; /* 0x30-0x3F: reserved 1-op */
|
||
else if (fb_op < 0x4F) fb_n = 2; /* 0x40-0x4E: reserved 2-op */
|
||
else if (fb_op <= 0x50) fb_n = 1; /* 0x4F GG PSG / 0x50 SN76489 */
|
||
else if (fb_op < 0x60) fb_n = 2; /* 0x51-0x5F: chip writes 2-op */
|
||
else if (fb_op == 0x64) fb_n = 3; /* 0x64: wait override (cc nn nn) */
|
||
else if (fb_op < 0xA0) fb_n = 0; /* 0x8n DAC+wait, прочее 0x6x/9x */
|
||
else if (fb_op < 0xC0) fb_n = 2; /* 0xA0-0xBF: chip writes 2-op */
|
||
else if (fb_op < 0xE0) fb_n = 3; /* 0xC0-0xDF: chip writes 3-op */
|
||
else fb_n = 4; /* 0xE0-0xFF: chip writes 4-op */
|
||
while (fb_n--) { fb_rp++; PAGE_CHK(); }
|
||
}
|
||
continue;
|
||
|
||
/* ═══════ Обработка задержек (wait paths устанавливают fb_tk и fb_wacc напрямую) ═══════ */
|
||
do_wait:
|
||
/* Компенсация: бюджетные yield-ы уже потратили реальные ISR-тики.
|
||
* Вычитаем их из VGM-задержки, чтобы общий темп не замедлялся. */
|
||
if (fb_tk > fb_yield_ticks)
|
||
// реальная пауза меньше заявленной на fb_yield_ticks, компенсируем
|
||
fb_tk -= fb_yield_ticks;
|
||
else
|
||
// пауза полностью съедена накладными расходами, не ждем вообще
|
||
fb_tk = 0;
|
||
|
||
// сбросить счетчик накладных расходов, они уже компенсированы
|
||
fb_yield_ticks = 0;
|
||
|
||
if (fb_tk) {
|
||
// есть что ждать
|
||
|
||
if (fb_tk <= ISR_TICKS_PER_FRAME && fb_tk < vgm_sec_budget) {
|
||
// короткая пауза, помещается в один кадр — пропустить через SKIP
|
||
asm_emit_skip_spec((uint8_t)(fb_tk - 1));
|
||
vgm_sec_budget -= fb_tk; // учесть в сек. бюджете
|
||
} else {
|
||
emit_wait(fb_tk);
|
||
}
|
||
|
||
// после паузы бюджет обновлен, можно продолжать заполнять буфер
|
||
fb_budget = VGM_FILL_CMD_BUDGET;
|
||
}
|
||
continue;
|
||
|
||
do_budget:
|
||
// Бюджет команд: после каждого опкода уменьшаем, при 0 — пауза или yield
|
||
if (--fb_budget == 0)
|
||
{
|
||
// бюджет исчерпан — сделать паузу, восстановить бюджет
|
||
|
||
if (vgm_sec_budget > 1) {
|
||
// Остаток бюджета больше одного кадра — сделать yield (пауза через CMD_INC_SEC)
|
||
asm_emit_skip_spec(0);
|
||
vgm_sec_budget--;
|
||
fb_yield_ticks++; /* запомнить: этот тик — накладной расход */
|
||
} else {
|
||
// Остаток бюджета меньше одного кадра — сделать реальную паузу
|
||
emit_wait(1);
|
||
}
|
||
fb_budget = VGM_FILL_CMD_BUDGET;
|
||
}
|
||
}
|
||
|
||
#undef PAGE_CHK
|
||
|
||
/* Буфер заполнен — сохранить VGM-состояние если читали файл */
|
||
if (fb_is_vgm) {
|
||
vgm_cur_page = fb_cpg;
|
||
vgm_read_ptr = (uint16_t)fb_rp;
|
||
vgm_wait_accum = fb_wacc;
|
||
}
|
||
|
||
finish:
|
||
/* Принудительная пауза перед CMD_END_BUF — гарантирует, что ISR
|
||
* не обработает хвост этого буфера + голову следующего за один тик
|
||
* (иначе пропуск INT-позиции и падение темпа в десятки раз). */
|
||
asm_emit_skip_spec(0);
|
||
|
||
fb_wp[0] = CMD_END_BUF;
|
||
}
|
||
|
||
/* ── Буферы GD3 metadata ─────────────────────────────────────────── */
|
||
char vgm_gd3_track[VGM_GD3_LEN];
|
||
char vgm_gd3_game[VGM_GD3_LEN];
|
||
char vgm_gd3_system[VGM_GD3_LEN];
|
||
char vgm_gd3_author[VGM_GD3_LEN];
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* vgm_parse_gd3 — разбор GD3-тега (UTF-16LE → ASCII, только English)
|
||
*
|
||
* GD3 v1.00 содержит 11 UTF-16LE строк:
|
||
* 0: Track title (EN) 1: Track title (JP)
|
||
* 2: Game name (EN) 3: Game name (JP)
|
||
* 4: System name (EN) 5: System name (JP)
|
||
* 6: Author (EN) 7: Author (JP)
|
||
* 8: Date 9: Ripper 10: Notes
|
||
*
|
||
* Нам нужны только EN: индексы 0, 2, 4, 6.
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
void vgm_parse_gd3(void)
|
||
{
|
||
uint8_t *base = (uint8_t *)0xC000;
|
||
uint32_t gd3_off_field;
|
||
uint32_t gd3_abs;
|
||
uint8_t page;
|
||
uint16_t ptr;
|
||
uint8_t str_idx;
|
||
char *dst;
|
||
uint8_t dst_max;
|
||
uint8_t dst_pos;
|
||
|
||
/* Очистить все поля */
|
||
vgm_gd3_track[0] = '\0';
|
||
vgm_gd3_game[0] = '\0';
|
||
vgm_gd3_system[0] = '\0';
|
||
vgm_gd3_author[0] = '\0';
|
||
|
||
/* Страница 0 для чтения заголовка */
|
||
wc_mngcvpl(0);
|
||
base = (uint8_t *)0xC000;
|
||
|
||
/* GD3 offset: 32-bit LE в 0x14, относительно 0x14 */
|
||
gd3_off_field = (uint32_t)base[0x14]
|
||
| ((uint32_t)base[0x15] << 8)
|
||
| ((uint32_t)base[0x16] << 16)
|
||
| ((uint32_t)base[0x17] << 24);
|
||
if (gd3_off_field == 0)
|
||
return;
|
||
|
||
gd3_abs = 0x14UL + gd3_off_field;
|
||
|
||
/* Переключаемся на страницу с GD3 */
|
||
page = (uint8_t)(gd3_abs >> 14);
|
||
wc_mngcvpl(page);
|
||
base = (uint8_t *)0xC000;
|
||
ptr = (uint16_t)(gd3_abs & 0x3FFF);
|
||
|
||
/* Проверяем сигнатуру "Gd3 " */
|
||
if (base[ptr] != 'G' || base[ptr+1] != 'd' ||
|
||
base[ptr+2] != '3' || base[ptr+3] != ' ')
|
||
return;
|
||
|
||
/* Пропускаем: signature(4) + version(4) + data_length(4) = 12 */
|
||
ptr += 12;
|
||
if (ptr >= 0x4000) { page++; wc_mngcvpl(page); ptr -= 0x4000; }
|
||
|
||
/* Читаем UTF-16LE строки, нам нужны EN: #0, #2, #4, #6 */
|
||
for (str_idx = 0; str_idx < 8; str_idx++) {
|
||
/* Выбираем буфер для EN-строк (чётные) */
|
||
dst = 0;
|
||
dst_max = 0;
|
||
if (str_idx == 0) { dst = vgm_gd3_track; dst_max = VGM_GD3_LEN - 1; }
|
||
if (str_idx == 2) { dst = vgm_gd3_game; dst_max = VGM_GD3_LEN - 1; }
|
||
if (str_idx == 4) { dst = vgm_gd3_system; dst_max = VGM_GD3_LEN - 1; }
|
||
if (str_idx == 6) { dst = vgm_gd3_author; dst_max = VGM_GD3_LEN - 1; }
|
||
dst_pos = 0;
|
||
|
||
/* Читаем UTF-16LE до нулевого терминатора 0x0000 */
|
||
for (;;) {
|
||
uint8_t lo, hi;
|
||
lo = base[ptr];
|
||
hi = base[ptr + 1];
|
||
ptr += 2;
|
||
if (ptr >= 0x4000) { page++; wc_mngcvpl(page); ptr -= 0x4000; }
|
||
|
||
if (lo == 0 && hi == 0)
|
||
break; /* конец строки */
|
||
|
||
/* ASCII: берём только lo если hi==0 и lo < 128 */
|
||
if (dst && dst_pos < dst_max) {
|
||
if (hi == 0 && lo >= 0x20 && lo < 0x7F)
|
||
dst[dst_pos++] = (char)lo;
|
||
else
|
||
dst[dst_pos++] = '?'; /* non-ASCII → '?' */
|
||
}
|
||
}
|
||
if (dst)
|
||
dst[dst_pos] = '\0';
|
||
}
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────
|
||
* vgm_rewind_to_loop
|
||
* Перематывает к loop-точке, если она есть, и сбрасывает флаг окончания песни.
|
||
* Возвращает 1 если перемотано, 0 если loop-точки нет.
|
||
* Loop-точка устанавливается при загрузке VGM-файла, если в нём есть loop_offset.
|
||
* Loop-точка — абсолютный адрес в VGM-данных, который может быть в любом месте файла
|
||
* (обычно после заголовка, но может быть и в середине данных).
|
||
* При перемотке к loop-точке нужно восстановить состояние чтения (текущая страница, указатель чтения, накопленные паузы),
|
||
* чтобы продолжить воспроизведение оттуда без сбоев.
|
||
* ───────────────────────────────────────────────────────────────────── */
|
||
uint8_t vgm_rewind_to_loop(void)
|
||
{
|
||
if (!vgm_loop_addr)
|
||
return 0;
|
||
|
||
vgm_cur_page = vgm_loop_page;
|
||
vgm_read_ptr = vgm_loop_addr;
|
||
vgm_wait_accum = 0;
|
||
vgm_sec_budget = ISR_FREQ;
|
||
wc_mngcvpl(vgm_cur_page);
|
||
return 1;
|
||
}
|