Files
VGMPLAY.WMF/src_sdcc/main.c
T

1181 lines
44 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* main.c — VGM Player Plugin для Wild Commander (SDCC/Z80)
*
* Поддерживаемые форматы:
* VGM (Raw) — OPL1/OPL2/OPL3/OPL4(YMF278B, cmd 0xD0), AY-3-8910/YM2149/YM2203, SAA1099
* VGZ (gzip) — автораспаковка через inflate
*
* Функции:
* - Отображение метаданных (GD3: автор, игра, трек, система)
* - Воспроизведение с двойной буферизацией (ISR 1367 Гц)
* - OPL3/OPL4: NEW=1 (L/R через C0-C8); OPL1/OPL2: NEW=0 (compat mode)
* - OPL4 (ZXM-MoonSound): NEW2=1 дополнительно разблокирует wave-часть
* (порты #7E/#7F); FM-часть (порты 0/1 команды 0xD0) идёт через те же
* регистры/порты #C4-#C7, что и OPL3
*
* Управление (PS/2 клавиатура):
* ESC — выход
* SPACE — следующий файл в каталоге
* P — предыдущий файл
* 1/2 — SAA режим (chip0 / chip1) для dual музыки
*/
#include "inc/types.h"
#include "inc/tsconf.h"
#include "inc/wc_api.h"
#include "inc/txtlib.h"
#include "inc/vgm.h"
#include "inc/isr.h"
#include "inc/keys.h"
#include "inc/version.h"
#include <string.h>
/* Хелперы stringify для compile-time ISR_FREQ / budget в заголовке */
#define _STR(x) #x
#define STR(x) _STR(x)
/* ── Параметры progress bar для inflate (определены в inflate_call.s _DATA) ── */
extern uint8_t ifl_pb_text_pg;
extern uint16_t ifl_pb_attr_ofs;
extern uint8_t ifl_pb_total;
extern uint8_t ifl_pb_bw;
extern uint8_t ifl_pb_green;
/* ── Бордюр-отладка ─────────────────────────────────────────────────── */
/* Включить DEBUG_BORDER для отображения времени fill_buffer через цвет бордюра */
/* #define DEBUG_BORDER */
#ifdef DEBUG_BORDER
static void border(uint8_t c) { sfr_zx_fe = c; }
#endif
/* ── Отладка через порт (emulator "Illegal port!" log) ────────────── */
/* Включите DBG_ENABLE для отладочного вывода в порт; по умолчанию выкл */
/* #define DBG_ENABLE */
#ifdef DBG_ENABLE
#define DBG_STATUS 0xFFAF
#define DBG_DATA1 0xFEAF
#define DBG_DATA2 0xFDAF
static void dbg_trace(uint8_t code) __naked {
(void)code;
__asm
; A = code (sdcccall)
ld bc, #0xFFAF
out (c), a
ret
__endasm;
}
static void dbg_trace2(uint8_t code, uint8_t d1, uint8_t d2) {
(void)d1; (void)d2;
__asm
; A=code, stack: d1, d2 (sdcccall: A=first, stack rest)
ld bc, #0xFFAF
out (c), a
; d1 = 2(sp), d2 = 3(sp) — behind ret addr
ld hl, #2
add hl, sp
ld a, (hl)
ld bc, #0xFEAF
out (c), a
inc hl
ld a, (hl)
ld bc, #0xFDAF
out (c), a
__endasm;
}
#else
#define dbg_trace(code) ((void)0)
#define dbg_trace2(code, d1, d2) ((void)0)
#endif
/* ── Кешированные флаги активных чипов ────────────────────────────── */
static uint8_t s_has_opl, s_has_ay, s_has_ay2;
static uint8_t s_has_saa, s_has_saa2;
static uint8_t s_has_ym2203, s_has_ym2203_2;
/* SAA dual mode config (defined in vgm.c) */
extern uint8_t cfg_saa_mode;
static uint8_t s_saa_mode_user; /* user setting from INI (preserved across tracks) */
static uint8_t s_board_dual_saa; /* 0=одночиповая(default), 1=двухчиповая (INI param 4) */
static void detect_active_chips(void)
{
s_has_opl = s_has_ay = s_has_ay2 = 0;
s_has_saa = s_has_saa2 = 0;
s_has_ym2203 = s_has_ym2203_2 = 0;
for (uint8_t i = 0; i < vgm_chip_count; i++) {
uint8_t id = vgm_chip_list[i].id;
uint8_t dual = vgm_chip_list[i].flags & VGM_CF_DUAL;
switch (id) {
case VGM_OFF_YM2203:
s_has_ym2203 = 1;
if (dual) s_has_ym2203_2 = 1;
/* fallthrough (проваливаемся) */
case VGM_OFF_AY8910:
s_has_ay = 1;
if (dual) s_has_ay2 = 1;
break;
case VGM_OFF_YM3526:
case VGM_OFF_YM3812:
case VGM_OFF_Y8950:
case VGM_OFF_YMF262:
case VGM_OFF_YMF278B:
s_has_opl = 1;
break;
case VGM_OFF_SAA1099:
s_has_saa = 1;
if (dual) s_has_saa2 = 1;
break;
default:
break;
}
}
}
/* cfg_loop_rewinds, cfg_min_duration, cfg_max_duration определены в vgm.c */
/* ── HL queue helpers ────────────────────────────────────────────────
* The queue itself and its state live in vgm.c (vgm_hl_queue etc.)
* vgm_fill_buffer() reads them internally; main.c only builds
* the queue and sets vgm_hl_pos on abort (Q/N/P). */
static void hl_push(uint8_t cmd, uint8_t param)
{
if (vgm_hl_len < HL_QUEUE_MAX) {
vgm_hl_queue[vgm_hl_len].cmd = cmd;
vgm_hl_queue[vgm_hl_len].param = param;
vgm_hl_len++;
}
}
/* CMDBLK_SILENCE_OPL (+ CMDBLK_SILENCE_WAVE для OPL4) — общий кусок,
* нужен и в начале трека, и в abort-последовательности выхода. */
static void hl_push_opl_silence(void)
{
hl_push(HLCMD_CMDBLK, CMDBLK_SILENCE_OPL);
if (vgm_chip_type == VGM_CHIP_OPL4)
hl_push(HLCMD_CMDBLK, CMDBLK_SILENCE_WAVE);
}
/* ── Построение очереди воспроизведения по обнаруженным чипам / loop info ─ */
static void build_playback_queue(void)
{
vgm_hl_len = 0;
/* Фаза инициализации: silence (очистка остаточного состояния) + init */
if (s_has_opl) {
hl_push_opl_silence();
hl_push(HLCMD_CMDBLK,
(vgm_chip_type == VGM_CHIP_OPL4) ? CMDBLK_INIT_OPL4
: (vgm_chip_type == VGM_CHIP_OPL3) ? CMDBLK_INIT_OPL3
: CMDBLK_INIT_OPL2);
}
if (s_has_ym2203)
hl_push(HLCMD_CMDBLK, CMDBLK_SILENCE_YM2203);
if (s_has_ym2203_2)
hl_push(HLCMD_CMDBLK, CMDBLK_SILENCE_YM2203_2);
/* SAA: гасим saa2, потом saa1 (даже на одночиповой плате),
* затем clock ON, затем Sound Enable по режиму.
* cmdblk bypass гарантирует прохождение команд chip2. */
if (s_has_saa) {
hl_push(HLCMD_CMDBLK, CMDBLK_SILENCE_SAA2);
hl_push(HLCMD_CMDBLK, CMDBLK_SILENCE_SAA);
hl_push(HLCMD_CMDBLK, CMDBLK_SAA_CLK_ON);
hl_push(HLCMD_CMDBLK, CMDBLK_SAA_INIT);
if (cfg_saa_mode == 2)
hl_push(HLCMD_CMDBLK, CMDBLK_SAA2_INIT);
}
/* Основное воспроизведение */
hl_push(HLCMD_PLAY, 0);
/* Loop repeats — только если vgm_parse_header разрешил loop.
* Policy: без full-track loop, без коротких (<10с), без длинных (>4мин). */
if (vgm_loop_enabled) {
for (uint8_t i = 0; i < cfg_loop_rewinds; i++)
hl_push(HLCMD_LOOP, 0);
}
/* ── Последовательность завершения (abort target) ──────────────── */
vgm_hl_abort_pos = vgm_hl_len;
if (s_has_opl)
hl_push_opl_silence();
if (s_has_ym2203)
hl_push(HLCMD_CMDBLK, CMDBLK_SILENCE_YM2203);
else if (s_has_ay)
hl_push(HLCMD_CMDBLK, CMDBLK_SILENCE_AY);
if (s_has_ym2203_2)
hl_push(HLCMD_CMDBLK, CMDBLK_SILENCE_YM2203_2);
else if (s_has_ay2)
hl_push(HLCMD_CMDBLK, CMDBLK_SILENCE_AY2);
if (s_has_saa) {
hl_push(HLCMD_CMDBLK, CMDBLK_SILENCE_SAA2);
hl_push(HLCMD_CMDBLK, CMDBLK_SILENCE_SAA);
hl_push(HLCMD_CMDBLK, CMDBLK_SAA_CLK_OFF);
}
hl_push(HLCMD_ISR_DONE, 0);
vgm_hl_pos = 0;
}
/* ── Размеры и цвета окна ───────────────────────────────────────────── */
/* Координаты вычисляются динамически в main() через wc_get_height().
* TextMode=0: 80×25, TextMode=1: 80×30, TextMode=2: 90×36. */
#define WND_W 62
#define WND_H 22
#define CLR_WIN WC_COLOR(WC_WHITE, WC_BLACK)
#define CLR_TITLE WC_COLOR(WC_BLUE, WC_YELLOW)
static const char s_win_title[] = " VGM Player " APP_VERSION " ";
/* ── Фиксированные строки окна (1-based content rows) ───────────────
* Высота=23 → строки контента 1..21, рамка 0 и 22.
* Стр 1: Заголовок
* Стр 2-4: Информация о файле
* Стр 5-6: (пусто)
* Стр 7: ← разделитель 1
* Стр 8-14: Информация VGM (ver, чипы, луп, freq, GD3)
* Стр 15: ← разделитель 2
* Стр 16: Прогресс-бар
* Стр 17-20: Спектро-анализатор (4 строки)
* Стр 21: Подсказки клавиш */
#define ROW_TITLE 1
#define ROW_FILE_START 2
#define ROW_VGM_START 6
#define ROW_VGM_END 13 /* максимальная строка для VGM/GD3 info */
#define ROW_PROGRESS 15
#define ROW_SPECTRUM 16 /* 4 rows: 16,17,18,19 */
#define ROW_HELP 20
#define DIV1_FROM_BOTTOM 16 /* row 7 = 23-16 */
#define DIV2_FROM_BOTTOM 7 /* row 15 = 23-8 */
static wc_window_t s_wnd;
static uint8_t s_pages;
/* pbinfo_start_row заменён константой ROW_PROGRESS */
static uint8_t s_playback_inited = 0;
static uint8_t s_last_active_buf = 0;
static uint8_t s_buf_ready[2] = {0, 0};
static uint8_t s_last_loop_count = 0;
static uint16_t s_last_displayed_sec = 0; /* 0 = совпадает с начальным sec */
/* BCD-цифры времени (инкремент вместо делений) ─────────────────────── */
static uint8_t s_pb_d_min10 = '0';
static uint8_t s_pb_d_min1 = '0';
static uint8_t s_pb_d_sec10 = '0';
static uint8_t s_pb_d_sec1 = '0';
/* ── Progress bar state (предвычисленные параметры) ───────────────── */
uint8_t s_pb_text_pg; /* физ. страница текстового экрана (не static: ASM) */
static uint16_t s_pb_char_ofs; /* смещение символов в текстовой странице */
static uint16_t s_pb_attr_ofs; /* смещение атрибутов в текстовой странице */
static uint8_t s_pb_width; /* ширина полосы в колонках */
static uint16_t s_pb_err; /* аккумулятор ошибки Bresenham */
static uint8_t s_pb_col; /* текущее кол-во зелёных колонок */
/* Позиции изменяемых символов внутри строки progress bar:
* "Playing MM:SS / MM:SS Loop N"
* 0 9 17 27 29 */
#define PB_OFS_MIN10 9
#define PB_OFS_MIN1 10
#define PB_OFS_SEC10 12
#define PB_OFS_SEC1 13
#define PB_OFS_LOOP 29
/* ── Рендеринг spectrum analyzer (ASM: asm/spectrum.s) ──────────────
* 16 полос, 4 строки (ROW_SPECTRUM..ROW_SPECTRUM+3), снизу вверх.
* spectrum_render() и хелперы горячего цикла — в asm/spectrum.s.
* Переменные ниже не static, чтобы ASM мог обращаться по _symbol. */
/* Предвычисленные адреса для прямого доступа к текстовой странице */
uint16_t s_spec_char_ofs[4]; /* смещения символов для 4 строк */
uint16_t s_spec_attr_ofs[4]; /* смещения атрибутов для 4 строк */
/* spectrum_render/font/decay() находятся в asm/spectrum.s */
void spectrum_render(void);
void spectrum_decay(void);
void spectrum_decay_reset(void);
void spectrum_font_init(void);
void spectrum_font_restore(void);
/* Информация VGZ для отображения */
static uint8_t s_is_vgz;
static uint32_t s_vgm_unpacked_size;
static uint8_t s_vgz_compressed_pages;
static inline void ints_disable(void)
{
__asm di
__endasm;
}
static inline void ints_enable(void)
{
__asm ei
__endasm;
}
/* ── Имя OPL-типа ────────────────────────────────────────────────────── */
static const char *opl_name(void)
{
if (vgm_chip_type == VGM_CHIP_OPL)
return "OPL1 YM3526";
if (vgm_chip_type == VGM_CHIP_OPL2)
return "OPL2 YM3812";
if (vgm_chip_type == VGM_CHIP_OPL3)
return "OPL3 YMF262";
if (vgm_chip_type == VGM_CHIP_OPL4)
return "OPL4 YMF278B";
return "--- (no OPL)";
}
char work_buf[96 + 2];
char error_buf[96 + 2];
enum state_t
{
STATE_INIT,
STATE_LOADING,
STATE_ERROR,
STATE_PARSE_HEADER,
STATE_PLAYBACK
} state;
/* ── Предварительный вывод информации (до загрузки/распаковки) ──────── */
static void draw_pre_load_info(void)
{
uint8_t row = ROW_FILE_START;
buf_clear(work_buf);
buf_append_str(work_buf, "File name : ");
buf_append_str(work_buf, wc_file_name);
print_line(&s_wnd, row++, work_buf, CLR_WIN);
buf_clear(work_buf);
buf_append_str(work_buf, "File size : ");
buf_append_u32_dec(work_buf, wc_file_size);
print_line(&s_wnd, row++, work_buf, CLR_WIN);
buf_clear(work_buf);
buf_append_str(work_buf, "16Kb pages : ");
buf_append_u16_dec(work_buf, (uint16_t)((wc_file_size + 16383U) >> 14));
print_line(&s_wnd, row, work_buf, CLR_WIN);
}
uint8_t drow_ui(void)
{
uint8_t row;
uint8_t gd3_count = 0;
/* ── Секция 1: Информация о файле (строки 1-3) ────────────── */
row = ROW_FILE_START;
buf_clear(work_buf);
buf_append_str(work_buf, "File name : ");
buf_append_str(work_buf, wc_file_name);
print_line(&s_wnd, row++, work_buf, CLR_WIN);
buf_clear(work_buf);
buf_append_str(work_buf, "File size : ");
buf_append_u32_dec(work_buf, wc_file_size);
if (s_is_vgz) {
buf_append_str(work_buf, " / ");
buf_append_u32_dec(work_buf, s_vgm_unpacked_size);
}
print_line(&s_wnd, row++, work_buf, CLR_WIN);
buf_clear(work_buf);
buf_append_str(work_buf, "16Kb pages : ");
if (s_is_vgz) {
buf_append_u16_dec(work_buf, s_vgz_compressed_pages);
buf_append_str(work_buf, " / ");
}
buf_append_u16_dec(work_buf, s_pages);
print_line(&s_wnd, row, work_buf, CLR_WIN);
/* ── Секция 2: Данные VGM (строки 5..ROW_VGM_END) ────────── */
row = ROW_VGM_START;
buf_clear(work_buf);
buf_append_str(work_buf, "VGM version : ");
buf_append_u8_hex(work_buf, vgm_version >> 8);
buf_append_char(work_buf, '.');
buf_append_u8_hex(work_buf, vgm_version & 0xFF);
print_line(&s_wnd, row++, work_buf, CLR_WIN);
for (uint8_t i = 0; i < vgm_chip_count && row <= ROW_VGM_END; i++)
{
/* Компактно: макс. 2 полных строки чипов; остальные как "+ N more" */
if (i >= 2 && vgm_chip_count > 3) {
buf_clear(work_buf);
buf_append_str(work_buf, " + ");
buf_append_u16_dec(work_buf, (uint16_t)(vgm_chip_count - 2));
buf_append_str(work_buf, " more");
print_line(&s_wnd, row++, work_buf, CLR_WIN);
break;
}
const vgm_chip_entry_t *chip = &vgm_chip_list[i];
buf_clear(work_buf);
buf_append_str(work_buf, "Chip : ");
{
const char *cname = vgm_chip_name(chip->id);
if (cname) {
buf_append_str(work_buf, cname);
} else {
buf_append_str(work_buf, "? (0x");
buf_append_u8_hex(work_buf, chip->id);
buf_append_char(work_buf, ')');
}
}
buf_append_str(work_buf, " @ ");
buf_append_u16_dec(work_buf, chip->clock_khz);
buf_append_str(work_buf, " kHz");
if (chip->flags & VGM_CF_DUAL)
buf_append_str(work_buf, " x2");
print_line(&s_wnd, row++, work_buf, CLR_WIN);
}
/* Информация о loop */
if (row <= ROW_VGM_END) {
buf_clear(work_buf);
buf_append_str(work_buf, "Loop : ");
if (vgm_loop_addr && vgm_loop_enabled)
buf_append_str(work_buf, "Yes");
else if (vgm_loop_addr && !vgm_loop_enabled)
buf_append_str(work_buf, "Skip");
else
buf_append_str(work_buf, "No");
print_line(&s_wnd, row++, work_buf,
(vgm_loop_addr && !vgm_loop_enabled)
? WC_COLOR(WC_YELLOW, WC_BLACK)
: CLR_WIN);
}
/* Информация о режиме частот */
if (row <= ROW_VGM_END) {
buf_clear(work_buf);
buf_append_str(work_buf, "Freq : ");
if (vgm_freq_mode == FREQ_MODE_TABLE) {
buf_append_str(work_buf, "Table");
} else {
buf_append_str(work_buf, "Native");
}
print_line(&s_wnd, row++, work_buf,
(vgm_freq_mode == FREQ_MODE_NATIVE)
? CLR_WIN
: WC_COLOR(WC_WHITE, WC_BLUE));
}
/* ── GD3 метаданные (English, max 4) ──────────────────────────── */
if (vgm_gd3_track[0] && row <= ROW_VGM_END && gd3_count < 4) {
buf_clear(work_buf);
buf_append_str(work_buf, "Title : ");
buf_append_str(work_buf, vgm_gd3_track);
print_line(&s_wnd, row++, work_buf, WC_COLOR(WC_YELLOW, WC_BLACK));
gd3_count++;
}
if (vgm_gd3_game[0] && row <= ROW_VGM_END && gd3_count < 4) {
buf_clear(work_buf);
buf_append_str(work_buf, "Game : ");
buf_append_str(work_buf, vgm_gd3_game);
print_line(&s_wnd, row++, work_buf, WC_COLOR(WC_YELLOW, WC_BLACK));
gd3_count++;
}
if (vgm_gd3_author[0] && row <= ROW_VGM_END && gd3_count < 4) {
buf_clear(work_buf);
buf_append_str(work_buf, "Author : ");
buf_append_str(work_buf, vgm_gd3_author);
print_line(&s_wnd, row++, work_buf, WC_COLOR(WC_YELLOW, WC_BLACK));
gd3_count++;
}
if (vgm_gd3_system[0] && row <= ROW_VGM_END && gd3_count < 4) {
buf_clear(work_buf);
buf_append_str(work_buf, "System : ");
buf_append_str(work_buf, vgm_gd3_system);
print_line(&s_wnd, row++, work_buf, WC_COLOR(WC_YELLOW, WC_BLACK));
gd3_count++;
}
/* ── Строка подсказок (фиксированная позиция) ─────────────── */
buf_clear(work_buf);
buf_append_str(work_buf, " [Space] Next [P]rev [Esc] Exit");
if (s_has_saa2 && !s_board_dual_saa) {
buf_append_str(work_buf, " SAA:");
buf_append_char(work_buf, '1' + cfg_saa_mode);
}
print_line(&s_wnd, ROW_HELP, work_buf, CLR_TITLE);
return ROW_PROGRESS;
}
/* render_static_info() удалена — заменена на drow_ui() */
/* Проверить расширение файла: .vgz / .VGZ */
static uint8_t is_vgz_filename(void)
{
const char *p = wc_file_name;
const char *dot = 0;
while (*p) {
if (*p == '.') dot = p;
p++;
}
if (!dot) return 0;
dot++;
if ((dot[0]=='v'||dot[0]=='V') &&
(dot[1]=='g'||dot[1]=='G') &&
(dot[2]=='z'||dot[2]=='Z') &&
dot[3]==0)
return 1;
return 0;
}
uint8_t load_vgm(void)
{
uint8_t i;
uint32_t fsize = wc_file_size;
/* Минимальный размер — хотя бы 256 байт */
if (fsize < 256UL)
return 0;
s_pages = (uint8_t)((fsize + 16383UL) >> 14);
if (!s_pages)
s_pages = 1;
/* vgm_end_page / vgm_end_addr устанавливаются в vgm_parse_header()
из поля EOF offset заголовка VGM — здесь задавать не нужно. */
for (i = 0; i < s_pages; i++)
{
uint8_t rc;
wc_mngcvpl(i);
rc = wc_load512(0xC000, 32);
/* DBG: 0xB0 = loaded page i, rc */
dbg_trace2(0xB0, i, rc);
/* WC_EOF нормален на последнем секторе */
if (rc && rc != WC_EOF)
return 0;
}
wc_mngcvpl(0); /* страница 0 для проверки заголовка */
/* ── VGZ: gzip magic 0x1F 0x8B ──────────────────────────────── */
/* ВАЖНО: между wc_mngcvpl и inflate_vgz НЕ ДОЛЖНО быть WC API *
* вызовов (print_line и др.), иначе WC сбрасывает Win3 mapping *
* и inflate может упасть. */
if (*(volatile uint8_t *)0xC000 == 0x1F &&
*(volatile uint8_t *)0xC001 == 0x8B) {
uint8_t vgz_pages = s_pages;
uint32_t vgm_size;
uint8_t ifl_rc;
s_is_vgz = 1;
s_vgz_compressed_pages = s_pages;
/* DBG: 0xA0 = VGZ detected, pages count */
dbg_trace2(0xA0, vgz_pages, 0);
if (vgz_pages > 31) /* TAP: макс. 31 страница */
return 0;
/* DBG: 0xA1 = before copy_pages_to_tap */
dbg_trace(0xA1);
/* Копировать VGZ из мегабуфера → TAP-страницы (DI/EI внутри) */
copy_pages_to_tap(vgz_pages);
/* Установить total для progress bar (оценка выходных страниц) */
{
uint8_t est = (uint8_t)(vgz_pages * 3u);
if (est > 64) est = 64;
ifl_pb_total = est;
}
/* DBG: 0xA2 = after copy_pages_to_tap, before inflate_vgz */
dbg_trace2(0xA2, WC_PAGE_TAP_START, WC_PAGE_TVBPG);
/* Inflate: TAP (#A1+) → мегабуфер (#20+) */
ifl_rc = inflate_vgz(WC_PAGE_TAP_START, WC_PAGE_TVBPG);
/* DBG: 0xA3 = after inflate_vgz, result */
dbg_trace2(0xA3, ifl_rc, 0);
if (ifl_rc != 0)
return 0;
/* DBG: 0xA4 = inflate OK, reading VGM size */
dbg_trace(0xA4);
/* Прочитать размер распакованного VGM из заголовка */
wc_mngcvpl(0);
{
/* VGM offset 0x04: EOF offset (отн. 0x04) — LE uint32 */
uint16_t lo = *(volatile uint16_t *)0xC004;
uint16_t hi = *(volatile uint16_t *)0xC006;
vgm_size = ((uint32_t)hi << 16) | lo;
vgm_size += 4; /* total = eof_offset + 4 */
}
s_vgm_unpacked_size = vgm_size;
s_pages = (uint8_t)((vgm_size + 16383UL) >> 14);
if (!s_pages)
s_pages = 1;
/* DBG: 0xA5 = VGZ done, final pages */
dbg_trace2(0xA5, s_pages, 0);
}
return s_pages;
}
void start_playback(void)
{
ints_disable();
/* Включение 512-байт кэша для Window 2 (#8000-#BFFF).
* Ускоряет code fetch и доступ к static fb_* переменным.
* ДОЛЖНО быть после inflate/load — DMA/page I/O испортят cache.
* SysConfig bit 2 = global enable, CacheConfig bit 2 = win2. */
//tsconf_set_cache(TSCONF_CACHE_EN_8000);
//tsconf_set_cpu_cache(TSCONF_CPU_14MHZ, 1);
vgm_paused = 0;
if (!s_playback_inited)
{
s_playback_inited = 1;
/* Сброс счётчиков */
isr_play_seconds = 0;
s_last_displayed_sec = 0;
s_pb_d_min10 = '0';
s_pb_d_min1 = '0';
s_pb_d_sec10 = '0';
s_pb_d_sec1 = '0';
vgm_loop_count = 0;
/* ISR-состояние */
isr_active_buf = 0;
isr_wait_ctr = 0;
isr_done = 0;
/* MultiSound ctrl: включить FM/SAA только для присутствующих чипов.
* 0xF2 = FM ON + SAA ON + normal read + chip1 (base).
* bit2: 1 = FM OFF (установить если нет YM2203)
* bit3: 1 = SAA OFF (установить если нет SAA1099) */
isr_ms_ctrl = 0xF2
| (s_has_ym2203 ? 0x00 : 0x04)
| (s_has_saa ? 0x00 : 0x08);
/* Применить конфиг MultiSound к железу немедленно
* (нужно для SAA-only музыки, где AY-записей нет) */
__asm
ld bc, #0xFFFD
ld a, (_isr_ms_ctrl)
out (c), a
__endasm;
// SAA1099: effective mode computation.
// saa1/saa2 = микросхемы на плате, chip0/chip1 = чипы в VGM.
// Двухчиповая (saa_chips=2): single→saa1, dual→saa1+saa2 (turbo).
// Одночиповая (saa_chips=1): всегда saa1; dual → chip0 или chip1 по выбору.
if (s_board_dual_saa)
cfg_saa_mode = s_has_saa2 ? 2 : 0;
else
cfg_saa_mode = s_has_saa2 ? s_saa_mode_user : 0;
// Set spectrum dual mode flag (only in turbo mode with dual VGM).
extern uint8_t spec_saa_dual;
spec_saa_dual = (s_has_saa2 && cfg_saa_mode == 2) ? 1 : 0;
/* SAA init/deinit is now handled entirely via cmdblocks
* in build_playback_queue() — no inline asm needed. */
/* Построить очередь HL-команд для этого трека */
build_playback_queue();
/* Инициализация IM2: перехват вектора WC #5BFF */
isr_init();
/* Заполнить оба буфера из HL queue.
* vgm_fill_buffer обрабатывает HL-переходы внутри —
* init cmdblocks + начало VGM-данных могут попасть в buf 0. */
vgm_fill_buffer(0);
s_buf_ready[0] = 1;
vgm_fill_buffer(1);
s_buf_ready[1] = 1;
s_last_active_buf = 0;
}
isr_enabled = 1;
ints_enable();
}
void stop_playback(void)
{
if (!s_playback_inited) {
ints_disable();
ints_enable();
return;
}
ints_disable();
isr_enabled = 0;
isr_deinit(); /* восстановить вектор WC */
/* MultiSound: отключить FM + SAA, выбрать chip1 (железо + переменная) */
isr_ms_ctrl = 0xFE;
__asm
ld bc, #0xFFFD
ld a, #0xFE ;; chip1 + FM OFF + SAA CLK OFF
out (c), a
__endasm;
/* Отключить cache перед возвратом в WC.
* CacheConfig = 0 отключает все per-window caches.
* SysConfig: оставить 14 МГц, очистить global cache bit. */
//tsconf_set_cpu_cache(TSCONF_CPU_14MHZ, 0);
//tsconf_set_cache(TSCONF_CACHE_NONE);
vgm_paused = 0;
s_playback_inited = 0;
s_buf_ready[0] = 0;
s_buf_ready[1] = 0;
s_last_active_buf = 0;
isr_done = 0;
ints_enable();
}
/**
* Мгновенный abort воспроизведения.
*
* При обычном abort через vgm_hl_abort_pos main loop ждёт, пока ISR
* доиграет активный буфер (включая CMD_WAIT, до ~1 сек задержки).
*
* instant_abort() обходит это за ~1.5 мс:
* 1. isr_enabled = 0 → ISR пропускает команды (pos_table работает).
* 2. HALT — CPU спит до следующего INT. После пробуждения ISR
* гарантированно отработал один проход с enabled=0.
* 3. Обнуляем isr_wait_ctr (отменяем CMD_WAIT).
* 4. Заполняем НЕАКТИВНЫЙ буфер shutdown-цепочкой
* (silence + CMD_ISR_DONE) — ISR его не читает.
* 5. Переключаем isr_active_buf + isr_read_ptr на этот буфер.
* 6. isr_enabled = 1 → ISR мгновенно выполняет shutdown.
*
* Вызывать ПОСЛЕ установки vgm_hl_pos = vgm_hl_abort_pos.
* После возврата isr_done будет установлен через 1-2 ISR-тика.
*
* Нулевой overhead на горячем пути ISR — без дополнительных флагов.
*/
static void instant_abort(void)
{
uint8_t free_idx;
/* ── 1. Заморозить ISR (команды не выполняются) ─────────── */
isr_enabled = 0;
/* ── 2. HALT: спать до следующего INT ──────────────────── *
* После HALT ISR гарантированно отработал с enabled=0, *
* не тронул ни один буфер. ~1.5 мс максимум. */
__asm__( "halt" );
/* ── 3. Обнулить wait_ctr (отменить CMD_WAIT) ─────────── */
isr_wait_ctr = 0;
/* ── 4. Заполнить НЕАКТИВНЫЙ буфер shutdown-цепочкой ───── *
* vgm_hl_pos уже установлен на vgm_hl_abort_pos → *
* vgm_fill_buffer выдаст silence + CMD_ISR_DONE. */
free_idx = (isr_active_buf & 1) ^ 1;
vgm_fill_buffer(free_idx);
/* ── 5. Переключить ISR на заполненный буфер ─────────── */
isr_active_buf = free_idx;
isr_read_ptr = (uint16_t)(free_idx ? cmd_buf_b : cmd_buf_a);
/* ── 6. Разморозить → ISR обработает shutdown ───────── */
isr_enabled = 1;
}
void update_buffer(void)
{
uint8_t active;
uint8_t free_idx;
if (!s_playback_inited || !isr_enabled || isr_done)
return;
active = (uint8_t)(isr_active_buf & 1U);
/* Если ISR переключился на другой буфер — старый считается освобождённым */
if (active != s_last_active_buf)
{
s_buf_ready[s_last_active_buf] = 0;
s_last_active_buf = active;
}
/* Всегда держим готовым неактивный буфер */
free_idx = (uint8_t)(active ^ 1U);
if (s_buf_ready[free_idx])
return;
/* Заполнить свободный буфер — vgm_fill_buffer обрабатывает HL-переходы */
#ifdef DEBUG_BORDER
isr_border_color = 2;
border(2);
#endif
vgm_fill_buffer(free_idx);
s_buf_ready[free_idx] = 1;
#ifdef DEBUG_BORDER
isr_border_color = 0;
border(0);
#endif
}
void update_playback_info(void)
{
uint16_t sec;
uint8_t sec_changed, loop_changed;
if (state != STATE_PLAYBACK)
return;
sec = isr_play_seconds;
/* Страховка для VGM без разрешённого loop. Нормальный путь
* заканчивается на opcode 0x66; если нестандартный хвост до него не
* дошёл, не оставляем OPL4 удерживать последнюю ноту бесконечно.
* Две секунды запаса покрывают округление total_samples и уже
* поставленные в ISR команды. */
if (!vgm_loop_enabled && vgm_total_seconds &&
sec >= (uint16_t)(vgm_total_seconds + 2u))
{
vgm_hl_pos = vgm_hl_abort_pos;
instant_abort();
}
sec_changed = (sec != s_last_displayed_sec);
loop_changed = (vgm_loop_count != s_last_loop_count);
if (!sec_changed && !loop_changed)
return;
/* ── Переключаем Window3 на текстовую страницу напрямую через порт.
* Не используем wc_mngcvpl — она вызывает WC_ENTRY и обновляет
* WC_SYS_PGC, что может корраптить состояние WC при перехваченном ISR.
* sfr_page3 = прямая запись в порт 0x13AF. */
{
uint8_t saved_pg = sfr_page3;
sfr_page3 = s_pb_text_pg;
volatile uint8_t *base =
(volatile uint8_t *)(0xC000 + s_pb_char_ofs);
/* ── Время: инкрементальный BCD-счётчик.
* Секунды всегда растут на 1 → вместо 6 делений (sec/60,
* sec%60, min/10, min%10, s/10, s%10) — только INC + CP.
* ~20 тактов вместо ~6000. */
if (sec_changed) {
s_last_displayed_sec = sec;
if (++s_pb_d_sec1 > '9') {
s_pb_d_sec1 = '0';
if (++s_pb_d_sec10 > '5') {
s_pb_d_sec10 = '0';
if (++s_pb_d_min1 > '9') {
s_pb_d_min1 = '0';
s_pb_d_min10++;
}
}
}
base[PB_OFS_MIN10] = s_pb_d_min10;
base[PB_OFS_MIN1] = s_pb_d_min1;
base[PB_OFS_SEC10] = s_pb_d_sec10;
base[PB_OFS_SEC1] = s_pb_d_sec1;
}
if (loop_changed) {
s_last_loop_count = vgm_loop_count;
base[PB_OFS_LOOP] = '1' + vgm_loop_count;
}
/* ── Progress bar: Bresenham-аккумулятор.
* Каждую секунду добавляем s_pb_width к ошибке,
* когда ошибка >= total — красим колонку и вычитаем.
* Ноль делений. Точно для любого соотношения sec/width. */
if (sec_changed && vgm_total_seconds) {
s_pb_err += s_pb_width;
while (s_pb_col < s_pb_width &&
s_pb_err >= vgm_total_seconds) {
*(volatile uint8_t *)(0xC000 + s_pb_attr_ofs + s_pb_col) =
WC_COLOR(WC_GREEN, WC_BLACK);
s_pb_col++;
s_pb_err -= vgm_total_seconds;
}
}
sfr_page3 = saved_pg;
}
}
/* ── main() ──────────────────────────────────────────────────────────── */
void main(void)
{
// Инициализация буферов для строк
buf_init(work_buf, 96);
buf_init(error_buf, 96);
state = STATE_INIT;
wc_turbopl(WC_TURBO_CPU, 0x02); /* 14 МГц */
/* ── Чтение INI-параметров ───────────────────────────────────────
* Формат в wc.ini: VGMPLAY.WMF -min_dur=10 -max_dur=4 -loops=1 -saa=1 -saa_chips=1
* param 0: min_duration секунд (default 10)
* param 1: max_duration в минутах (default 4 → 240 сек)
* param 2: loop_count (default 1)
* param 3: saa mode 1=chip0(default), 2=chip1 (только 1-чип + dual музыка)
* param 4: saa_chips 1=один SAA(default), 2=два SAA (эксп. плата)
* wc_prm_pl() возвращает 0xFF если параметр не задан. */
{
uint8_t v;
v = wc_prm_pl(0);
if (v != 0xFF) cfg_min_duration = v;
v = wc_prm_pl(1);
if (v != 0xFF) cfg_max_duration = (uint16_t)v * 60u;
v = wc_prm_pl(2);
if (v != 0xFF) cfg_loop_rewinds = v;
v = wc_prm_pl(3);
if (v >= 1 && v <= 2) cfg_saa_mode = v - 1;
s_saa_mode_user = cfg_saa_mode;
v = wc_prm_pl(4);
s_board_dual_saa = (v == 2) ? 1 : 0;
}
wc_strset((char *)&s_wnd, sizeof(s_wnd), 0);
// SOW — рамка с тенью (TYPE3, заголовок рисуем вручную)
s_wnd.type = WC_WIN_TYPE3 | WC_WIN_SHADOW;
s_wnd.cur_mask = 0x05;
/* Динамическое центрирование по WC text mode */
{
uint8_t scr_h = wc_get_height(); /* 25/30/36 */
uint8_t scr_w = (scr_h >= 36) ? 90 : 80;
s_wnd.x = (scr_w > WND_W) ? (scr_w - WND_W) / 2 : 0;
s_wnd.y = (scr_h > WND_H) ? (scr_h - WND_H) / 2 : 0;
}
s_wnd.width = WND_W;
s_wnd.height = WND_H;
s_wnd.color = CLR_WIN;
s_wnd.buf_addr = 0;
s_wnd.divider1 = DIV1_FROM_BOTTOM;
s_wnd.divider2 = DIV2_FROM_BOTTOM;
/* Дождаться отпускания всех клавиш с точки зрения WC.
* WC ISR ещё работает штатно (PSD2 обновляет TAB нормально),
* поэтому wc_key_wait_release даст WC привести TAB в чистое
* состояние. Только после этого отключаем WC PS/2 handler —
* FIFO переходит под наш контроль без артефактов. */
wc_key_wait_release();
wc_int_pl(WC_INT_NO_PS2);
// Восстановить дисплей WC и нарисовать окно
wc_gedpl();
wc_prwow(&s_wnd);
/* Заголовок окна синим текстом */
buf_clear(work_buf);
buf_append_str(work_buf, s_win_title);
print_line(&s_wnd, ROW_TITLE, work_buf, CLR_TITLE);
/* Предварительный вывод: имя, размер, страницы (до загрузки) */
s_is_vgz = 0;
s_vgm_unpacked_size = 0;
s_vgz_compressed_pages = 0;
draw_pre_load_info();
/* Если файл .vgz — показать "Unpacking..." ДО начала загрузки. *
* Это последний WC API вызов перед inflate — после него load_vgm *
* работает только через порты, без обращений к WC. */
if (is_vgz_filename()) {
buf_clear(work_buf);
buf_append_str(work_buf, " Unpacking...");
print_line(&s_wnd, ROW_VGM_START, work_buf, WC_COLOR(WC_YELLOW, WC_BLACK));
/* Настройка progress bar: получить адрес текст. экрана для VGM секции col 2 */
{
uint16_t bar_addr = wc_gadrw(&s_wnd, ROW_VGM_START, 2);
ifl_pb_text_pg = wc_get_pgc(); /* физ. страница текст. экрана */
ifl_pb_attr_ofs = (bar_addr & 0x3FFF) | 0x0080; /* SET 7,L = область атрибутов */
ifl_pb_bw = get_content_width(&s_wnd);
ifl_pb_green = WC_COLOR(WC_GREEN, WC_BLACK);
ifl_pb_total = 0; /* будет установлено в load_vgm() */
}
}
uint8_t loaded_pages = load_vgm();
if (!loaded_pages)
{
state = STATE_ERROR;
wc_exit_code = WC_EXIT_ESC;
buf_append_str(error_buf, "Failed to load VGM file.");
}
else
{
state = STATE_PLAYBACK;
if (vgm_parse_header() != VGM_OK)
{
state = STATE_ERROR;
wc_exit_code = WC_EXIT_ESC;
buf_append_str(error_buf, "Invalid VGM header.");
}
else
{
/* Парсим GD3 и восстанавливаем VPL-страницу */
vgm_parse_gd3();
/* ── 0x66 sentinel ───────────────────────────────────
* Пишем VGM end-marker (0x66) сразу после конца данных,
* чтобы hot-loop в vgm_fill_buffer мог работать без
* проверки EOF на каждой итерации. */
wc_mngcvpl(vgm_end_page);
*(volatile uint8_t *)vgm_end_addr = 0x66;
wc_mngcvpl(vgm_cur_page);
/* Определяем типы чипов и длительность трека */
detect_active_chips();
}
}
drow_ui();
/* Кешируем параметры progress bar (строка playback info) */
{
uint16_t bar_addr = wc_gadrw(&s_wnd, ROW_PROGRESS, 2);
s_pb_text_pg = wc_get_pgc();
s_pb_char_ofs = bar_addr & 0x3FFF;
s_pb_attr_ofs = s_pb_char_ofs | 0x0080;
s_pb_width = get_content_width(&s_wnd);
s_pb_col = 0;
s_pb_err = 0;
}
/* Кешируем адреса строк спектроанализатора */
{
for (uint8_t r = 0; r < 4; r++) {
uint16_t a = wc_gadrw(&s_wnd, ROW_SPECTRUM + r, 2);
s_spec_char_ofs[r] = a & 0x3FFF;
s_spec_attr_ofs[r] = s_spec_char_ofs[r] | 0x0080;
}
spectrum_font_init();
spectrum_decay_reset();
}
/* Печатаем полную строку progress bar ОДИН РАЗ через WC API.
* "Playing 00:00 / MM:SS Loop 1" + чёрный фон/белый текст.
* В главном цикле обновляются только 5 символов + атрибуты. */
{
buf_clear(work_buf);
buf_append_str(work_buf, "Playing 00:00 / ");
buf_append_mmss(work_buf, vgm_total_seconds);
buf_append_str(work_buf, " Loop 1");
print_line(&s_wnd, ROW_PROGRESS, work_buf, WC_COLOR(WC_BRIGHT_WHITE, WC_BLACK));
}
if (state == STATE_PLAYBACK)
start_playback();
uint8_t key = 0;
wc_exit_code = (wc_file_idx == wc_file_count) ? WC_EXIT_ESC : WC_EXIT_NEXT;
/* PS/2 FSM: init выполняется один раз в main(), здесь
* только ре-drain для слива событий от предыдущего трека. */
ps2_init();
/* Главный цикл: приоритет — update_buffer (буфер не должен голодать),
клавиатура + UI — по остаточному принципу. */
while (state == STATE_PLAYBACK)
{
/* ── 1. Буфер: максимальный приоритет ──────────────── */
update_buffer();
/* ISR замер на CMD_ISR_DONE → чистый выход */
if (isr_done)
break;
/* ── 2. Клавиатура: PS/2 FIFO (~1 раз в кадр) ────── */
key = ps2_poll();
if (key == KEY_NEXT)
{
vgm_hl_pos = vgm_hl_abort_pos;
wc_exit_code = WC_EXIT_NEXT;
instant_abort();
}
else if (key == KEY_PREV)
{
vgm_hl_pos = vgm_hl_abort_pos;
wc_exit_code = WC_EXIT_PREV;
instant_abort();
}
else if (key == KEY_ESC)
{
vgm_hl_pos = vgm_hl_abort_pos;
wc_exit_code = WC_EXIT_ESC;
instant_abort();
}
else if (key >= KEY_SAA1 && key <= KEY_SAA2 && s_has_saa2 && !s_board_dual_saa)
{
uint8_t new_mode = key - KEY_SAA1; /* 0 or 1 */
s_saa_mode_user = new_mode;
cfg_saa_mode = new_mode;
extern uint8_t spec_saa_dual;
spec_saa_dual = 0;
/* Обновить подсказку SAA mode */
buf_clear(work_buf);
buf_append_str(work_buf, " [Space] Next [P]rev [Esc] Exit SAA:");
buf_append_char(work_buf, '1' + cfg_saa_mode);
print_line(&s_wnd, ROW_HELP, work_buf, CLR_TITLE);
}
if (isr_play_seconds != s_last_displayed_sec || vgm_loop_count != s_last_loop_count)
update_playback_info();
/* ── 3. Спектроанализатор ──────────────────────────── */
spectrum_decay();
spectrum_render();
}
ps2_deinit();
stop_playback();
spectrum_font_restore();
wc_rresb(&s_wnd);
}