Files
Sprinter-SDCC/applications/SprPoP/src/pop_music.c
T
snark13 6176f6da31 SprPoP: Restart Game возвращал заставку без музыки и вешал её на титуле
pop_boot_sound() был целиком под разовым флагом, а pop_sfx_start() стоял
внутри него.  До перестановки старта CBL открывал сам шаг 0 титров, и
делал это КАЖДЫЙ раз; теперь второй вызов уходил по раннему return, а
pop_title_show начинается с pop_sfx_pause() — значит после Restart Game
насос оставался выключенным и заставка шла молча.

Тем же корнем объясняется зависание: с закрытым CBL курсор трека не
двигается, pop_mus_left не убывает, и ожидания «дослушать тему» в титрах
и интро (while (pop_music_busy())) висели до нажатия клавиши — на экране
Prince of Persia.

- pop_boot_sound: под разовым флагом осталась только ЗАГРУЗКА (индекс
  звука, первая страница набора, индекс музыки, settings_apply);
  pop_sfx_start() зовётся всегда — он идемпотентен и уважает Ctrl+S;
- pop_music_busy(): признак теперь «звучит», а не «есть курсор» —
  спрашивает и про открытый вывод (pop_snd_ok).  Ждать неиграющую музыку
  нельзя в принципе, и эта строка закрывает весь класс подвисаний.

Проверено в MAME: уровень 1 -> ESC -> RESTART GAME -> титул с музыкой
(pop_snd_ok=1, курсор трека прошёл страницы 8..16, pop_mus_id=54), дальше
последовательность сама уходит в сцену с принцессой.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
2026-09-02 11:45:02 +03:00

489 lines
23 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.
/*
* pop_music.c — загрузка и запуск музыкального трека, БАНК 9.
*
* Здесь только холодная часть: выделить страницы EMM, вычитать в них куски
* m<id>_<nn>.bin и выставить курсор. Сам вывод делает насос CBL в
* резиденте (pop_sfx.c): когда эффект не звучит, он берёт байты отсюда, а
* не из блока тишины. Контракт и мотивация — в pop_music.h.
*
* Имя файла собираем вручную, без sprintf: printf-семейство в резидент не
* тянем, а тут и формат-то из двух чисел.
*/
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <sprinter_mem.h>
#include <string.h>
#include <sprite.h> /* gfx_w0_map/unmap — доступ к странице индекса */
#include "pop_music.h"
#include "pop_sfx.h"
#include "_pop_sfx.h" /* pop_snd_ok — открыт ли вывод (см. pop_music_busy) */
#include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */
#include "_pop_file.h"
uint8_t pop_mus_page[POP_MUS_PAGES];
/* --- ИНДЕКС НАБОРА ---------------------------------------------------- *
*
* Страница EMM с прочитанным mus.idx; 0 — индекса нет. В W2 от него
* остаются ровно эти два байта, сама таблица (228 Б) лежит в странице.
*
* Данные лежат со смещения POP_MUS_IDX_BASE, а не с начала страницы:
* gfx_w0_page_prepare пишет туда стабы прерываний (0x38 и 0x66), и с
* нуля они попали бы прямо в записи. Со смещением всё расходится, и
* страница подключается штатно — без запрета прерываний. */
static uint8_t mus_idx_blk;
static uint8_t mus_idx_page;
/* "MUS\mus.idx" — имя знает сам модуль (тот же приём, что в pop_arc.c:
* литерал лежит в rodata банка 9, и open() читает его, пока в W3 стоит
* этот банк). */
static const char mus_idx_path[] = "MUS\\mus.idx";
int8_t pop_music_index_load(void) __banked
{
uint8_t *w0 = (uint8_t *)POP_MUS_IDX_BASE;
uint8_t blk, page, ok;
int fd;
if (mus_idx_page) return 0;
POP_PATH_CALL(fd, open(mus_idx_path, O_RDONLY));
if (fd < 0) return -1;
blk = mem_alloc_pages(1);
if (!blk) { close(fd); return -1; }
page = mem_get_page(blk, 0);
if (!page ||
bank_read_page(fd, page, POP_MUS_IDX_BASE, POP_MUS_IDX_SIZE) < 0) {
close(fd);
mem_free_block(blk);
return -1;
}
close(fd);
gfx_w0_page_prepare(page); /* стабы лягут вне данных, см. BASE */
gfx_w0_map(page);
ok = (uint8_t)(memcmp(w0, "PMI1", 4) == 0 &&
w0[4] == POP_MUS_COUNT &&
w0[5] == POP_MUS_IDX_REC &&
w0[6] == 7 && /* log2(128) — блок насоса */
w0[7] == 1); /* u8 mono, CBL_FREQ_10K9 */
gfx_w0_unmap();
if (!ok) { mem_free_block(blk); return -1; }
mus_idx_blk = blk;
mus_idx_page = page;
return 0;
}
int8_t pop_music_info(uint8_t id, pop_mus_info_t *out) __banked
{
const uint8_t *rec;
const uint8_t *w0 = (const uint8_t *)POP_MUS_IDX_BASE;
out->blocks = 0;
out->ticks = 0;
if (!mus_idx_page || id >= POP_MUS_COUNT) return -1;
gfx_w0_map(mus_idx_page);
rec = w0 + POP_MUS_IDX_HDR + (uint16_t)id * POP_MUS_IDX_REC;
out->blocks = (uint16_t)(rec[0] | ((uint16_t)rec[1] << 8));
out->ticks = (uint16_t)(rec[2] | ((uint16_t)rec[3] << 8));
gfx_w0_unmap();
return out->blocks ? 0 : -1;
}
/* Два слота: играющий и подгружаемый. Своей таблицы страниц слот НЕ
* держит — её и так хранит блок EMM, и mem_get_page отдаёт номер по
* индексу. Резидентный pop_mus_page[] заполняется на pop_music_play, для
* одного слота: 40 байт W2 у игры лишних нет. */
#define MUS_SLOTS 2
static uint8_t slot_blk[MUS_SLOTS]; /* блок EMM, 0 — слот пуст */
static uint8_t slot_pages[MUS_SLOTS];
static uint16_t slot_blocks[MUS_SLOTS];
static uint8_t slot_id[MUS_SLOTS];
static uint8_t mus_cur; /* слот, что играет/играл */
static uint8_t mus_ready; /* слот+1, готовый к запуску */
/* "MUS\mNN.bin" — ровно 8.3, как требует DSS. */
static void mus_name(char *buf, uint8_t id)
{
buf[0] = 'M'; buf[1] = 'U'; buf[2] = 'S'; buf[3] = '\\';
buf[4] = 'm';
buf[5] = (char)('0' + id / 10);
buf[6] = (char)('0' + id % 10);
buf[7] = '.'; buf[8] = 'b'; buf[9] = 'i'; buf[10] = 'n';
buf[11] = 0;
}
/* ld_busy отдельно от ld_fd: инициализаторов у статиков в банке нет
* (crt0 зануляет _DATA), а «нет открытого файла» — это не ноль. */
static uint8_t ld_busy;
static int ld_fd;
static uint8_t ld_slot;
static uint8_t ld_next; /* следующая страница к чтению */
static uint8_t ld_half; /* 0 = впереди первая половина страницы */
static uint8_t ld_pages;
static uint8_t ld_id;
/* Длина трека из индекса, снятая при открытии: лезть в страницу индекса
* на каждом шаге загрузки незачем — она не меняется. */
static uint16_t ld_blocks;
static void slot_release(uint8_t n)
{
if (slot_blk[n]) {
mem_free_block(slot_blk[n]);
slot_blk[n] = 0;
}
slot_pages[n] = 0;
slot_blocks[n] = 0;
}
/* --- ПОТОКОВЫЙ ТРЕК: КОЛЬЦО СТРАНИЦ ---------------------------------- *
*
* Финальная тема (`won`, 115 с) — это 1,2 МБ и 78 страниц EMM. Держать её
* в памяти нельзя ни при каком бюджете, поэтому она играется из НЕСКОЛЬКИХ
* страниц по кругу: насос идёт по кольцу (pop_mus_ring), а загрузчик
* дочитывает файл в те страницы, которые насос уже прошёл.
*
* Кто кого догоняет. Страница звучит 1,5 с, а читается 33 мс — запас
* сорокакратный, так что «не успеть» можно только застряв на секунду.
* Дистанцию считаем без отдельных счётчиков: страница ровно 128 блоков
* насоса, поэтому проигранных страниц = (всего блоков - осталось) / 128.
* Пока прочитано меньше, чем проиграно плюс размер кольца, — в кольце есть
* свободный слот, и следующую страницу можно читать.
*
* Файл читаем ПОСЛЕДОВАТЕЛЬНО, без lseek: страницы идут по порядку, и fd
* сам стоит там, где надо. */
#define RING_PAGES 6 /* 96 КБ = 9 с звучания, запас с лихвой */
static uint8_t ring_on; /* идёт потоковый трек */
static uint8_t ring_blk; /* EMM-блок кольца */
static uint8_t ring_fd_open;
static int ring_fd;
static uint8_t ring_file_pages; /* страниц в файле */
static uint8_t ring_loaded; /* сколько уже прочитано */
static uint16_t ring_total; /* длина трека в блоках насоса */
void pop_music_ring_stop(void) __banked;
void pop_music_stop(void) __banked
{
pop_mus_left = 0;
}
void pop_music_free(void) __banked
{
if (ring_on) pop_music_ring_stop();
if (ld_busy) { close(ld_fd); ld_busy = 0; }
pop_mus_left = 0;
slot_release(0);
slot_release(1);
mus_ready = 0;
}
/* --- загрузка ---------------------------------------------------------
*
* ПОСТРАНИЧНО, А НЕ ЦЕЛИКОМ. Реплики сцены с принцессой идут встык, и
* следующую надо читать, пока звучит текущая. Трек — до 320 КБ, целиком
* это 0,7 с диска: на 7,5 кадрах в секунду сцена встала бы на пять кадров.
* Шаг стоит 16 мс (полстраницы, см. pop_music_load_step) — меньше одного
* кадрового интервала, и подкачка растворяется в анимации. Кто может позволить себе паузу (титры, чёрный экран
* между сценами), зовёт pop_music_load — тот же цикл, но целиком. */
static void load_abort(void)
{
if (ld_busy) close(ld_fd);
ld_busy = 0;
ld_half = 0;
slot_release(ld_slot);
}
int8_t pop_music_load_begin(uint8_t id) __banked
{
char name[12];
pop_mus_info_t info;
uint8_t pages, n;
if (ring_on) pop_music_ring_stop(); /* потоковый трек и слоты не дружат */
if (ld_busy) load_abort();
if (id >= POP_MUS_COUNT) return -1;
/* Длину и число страниц знает ИНДЕКС НАБОРА, а не EXE: у mt32 те же
* треки другой длины (docs/music_runtime_index_plan.md). */
if (pop_music_info(id, &info) != 0) return -1;
pages = pop_music_pages(info.blocks);
if (pages > POP_MUS_PAGES) return -1;
/* Грузим ВСЕГДА в не текущий слот: играющий трек читает свои страницы
* из прерывания, и отобрать их у него нельзя. */
n = (uint8_t)(mus_cur ^ 1);
slot_release(n);
mus_name(name, id);
/* Трек лежит ОДНИМ файлом, и fd остаётся открытым до последней
* страницы: `open` стоит дороже самого чтения (51,4 мс против 32,6 мс
* на 16 КБ), и платить его на каждую страницу — чистые потери. */
POP_PATH_CALL(ld_fd, open(name, O_RDONLY));
if (ld_fd < 0) return -1;
slot_blk[n] = mem_alloc_pages(pages);
if (!slot_blk[n]) { close(ld_fd); return -1; }
ld_busy = 1;
ld_slot = n;
ld_next = 0;
ld_half = 0;
ld_pages = pages;
ld_id = id;
ld_blocks = info.blocks;
return 0;
}
/* ШАГ = ПОЛСТРАНИЦЫ (8 КБ, ~16 мс), а не страница (~33 мс).
*
* Шаг вызывают раз в кадр из циклов, где кадр разложен на блоки по одному
* кадровому интервалу (20 мс): катсцены после перехода на блочную
* отрисовку, см. pop_intro.c/pv_restore_bg. Целая страница в интервал не
* влезала и незаметно растягивала кадр сцены; половина влезает, а цена —
* вдвое больше вызовов (11 страниц реплики = 22 кадра вместо 11, то есть
* меньше полусекунды). Игровому циклу это безразлично: там логический
* кадр 80-100 мс. */
int8_t pop_music_load_step(void) __banked
{
uint8_t p;
if (!ld_busy) return 1; /* нечего грузить */
p = mem_get_page(slot_blk[ld_slot], ld_next);
/* Последняя страница короче — читаем сколько есть (вторая половина
* может дать и ноль байт); ошибку ловим по отрицательному коду. */
if (!p || bank_read_page(ld_fd, p, ld_half ? 8192 : 0, 8192) < 0) {
load_abort();
return -1;
}
if (!ld_half) { ld_half = 1; return 0; } /* дочитаем в следующий раз */
ld_half = 0;
if (++ld_next < ld_pages) return 0;
close(ld_fd);
ld_busy = 0;
slot_pages[ld_slot] = ld_pages;
slot_blocks[ld_slot] = ld_blocks;
slot_id[ld_slot] = ld_id;
mus_ready = (uint8_t)(ld_slot + 1);
return 1;
}
uint8_t pop_music_loading(void) __banked
{
return ld_busy;
}
int8_t pop_music_load(uint8_t id) __banked
{
int8_t r;
if (pop_music_load_begin(id) != 0) return -1;
do {
r = pop_music_load_step();
} while (r == 0);
return (r < 0) ? -1 : 0;
}
/* ЗАПУСК С ПОДКАЧКОЙ НА ХОДУ — для игровых джинглов (смерть, зелья, меч).
* Ждать полной загрузки нельзя: это фриз на треть секунды посреди игры.
* Зато можно начать сразу после ПЕРВОЙ страницы: она звучит 1,5 с, а
* следующая читается 33 мс — запас сорокакратный. Дальше вызывающий
* доливает по странице за кадр (pop_music_load_step, pop_music_loading).
*
* Номера страниц известны сразу после mem_alloc_pages, поэтому таблицу для
* насоса заполняем целиком: данные в этих страницах появятся раньше, чем
* он до них доберётся.
* 0 — играет, -1 — трека нет или память кончилась. */
int8_t pop_music_stream(uint8_t id) __banked
{
uint8_t i, n;
if (ring_on) pop_music_ring_stop(); /* см. pop_music_play */
if (pop_music_load_begin(id) != 0) return -1;
if (pop_music_load_step() < 0) return -1; /* первая страница */
n = ld_slot;
pop_mus_left = 0; /* насос — на тишину */
for (i = 0; i < ld_pages; i++)
pop_mus_page[i] = mem_get_page(slot_blk[n], i);
if (n != mus_cur) slot_release(mus_cur);
mus_cur = n;
slot_pages[n] = ld_pages;
slot_blocks[n] = ld_blocks;
slot_id[n] = id;
pop_mus_id = id;
pop_mus_pg = 0;
pop_mus_ptr = 0;
pop_mus_left = slot_blocks[n];
return 0;
}
void pop_music_play(void) __banked
{
uint8_t n, i;
if (!mus_ready) return;
/* СНЯТЬ КОЛЬЦО. Если до этого играл потоковый трек (финал), насос всё
* ещё заворачивал бы страницы по кругу шести — обычный трек звучал бы
* своей первой третью без конца (поймано на титрах после победы). */
if (ring_on) pop_music_ring_stop();
n = (uint8_t)(mus_ready - 1);
/* Насос обязан замолчать ДО подмены таблицы страниц: иначе он успеет
* взять порцию по новому индексу из старого курсора. */
pop_mus_left = 0;
for (i = 0; i < slot_pages[n]; i++)
pop_mus_page[i] = mem_get_page(slot_blk[n], i);
if (n != mus_cur) slot_release(mus_cur);
mus_cur = n;
mus_ready = 0;
/* Порядок важен: сначала позиция, потом длина. Насос стартует ровно
* с той порции, где left стал ненулевым, и промежуточное состояние
* «играем, но курсор от прошлого трека» не возникает. */
pop_mus_id = slot_id[n]; /* до left: менеджер спросит готовое */
pop_mus_pg = 0;
pop_mus_ptr = 0;
pop_mus_left = slot_blocks[n];
}
static void ring_close_file(void)
{
if (ring_fd_open) { close(ring_fd); ring_fd_open = 0; }
}
void pop_music_ring_stop(void) __banked
{
pop_mus_left = 0;
pop_mus_ring = 0;
ring_close_file();
if (ring_blk) { mem_free_block(ring_blk); ring_blk = 0; }
ring_on = 0;
}
int8_t pop_music_ring_start(uint8_t id) __banked
{
char name[12];
pop_mus_info_t info;
uint8_t i, page;
if (id >= POP_MUS_COUNT || pop_music_info(id, &info) != 0) return -1;
/* Потоковый трек НЕ ограничен POP_MUS_PAGES: он и не помещается в
* память целиком, ради того и кольцо. Предел здесь — восьмибитные
* счётчики страниц (у mt32 финальная тема на 82 страницы против 78
* у flac). */
if (pop_music_pages(info.blocks) > 255) return -1;
pop_music_free(); /* обычные слоты кольцу не нужны */
pop_music_ring_stop();
mus_name(name, id);
POP_PATH_CALL(ring_fd, open(name, O_RDONLY));
if (ring_fd < 0) return -1;
ring_fd_open = 1;
ring_blk = mem_alloc_pages(RING_PAGES);
if (!ring_blk) { ring_close_file(); return -1; }
ring_file_pages = (uint8_t)pop_music_pages(info.blocks);
ring_total = info.blocks;
/* Заполняем кольцо целиком: старт с полным буфером даёт девять секунд
* форы, и первая же тяжёлая сцена финала не догонит насос. */
for (i = 0; i < RING_PAGES; i++) {
page = mem_get_page(ring_blk, i);
pop_mus_page[i] = page;
if (!page ||
(i < ring_file_pages && bank_read_page(ring_fd, page, 0, 16384) < 0)) {
pop_music_ring_stop();
return -1;
}
}
ring_loaded = (uint8_t)(ring_file_pages < RING_PAGES ? ring_file_pages
: RING_PAGES);
if (ring_loaded >= ring_file_pages) ring_close_file();
ring_on = 1;
pop_mus_id = id;
pop_mus_pg = 0;
pop_mus_ptr = 0;
pop_mus_ring = RING_PAGES; /* насос пойдёт по кругу */
pop_mus_left = ring_total;
return 0;
}
/* Один шаг подкачки: страница файла в слот, который насос уже прошёл. */
static void ring_step(void)
{
uint16_t played;
uint8_t played_pg, page;
if (!ring_on) return;
if (!pop_mus_left) { /* трек доиграл — отпустить память */
pop_music_ring_stop();
return;
}
if (ring_loaded >= ring_file_pages) return; /* файл прочитан весь */
played = (uint16_t)(ring_total - pop_mus_left);
played_pg = (uint8_t)(played >> 7); /* 128 блоков на страницу */
if ((uint8_t)(ring_loaded - played_pg) >= RING_PAGES) return; /* полно */
page = pop_mus_page[ring_loaded % RING_PAGES];
if (!page || bank_read_page(ring_fd, page, 0, 16384) < 0) {
pop_music_ring_stop();
return;
}
ring_loaded++;
if (ring_loaded >= ring_file_pages) ring_close_file();
}
void pop_music_service(void) __banked
{
/* Один вызов на кадр из ЛЮБОГО цикла — игрового, интро, катсцены.
* Заявку кладёт pop_sfx_play (звук без оцифровки = музыка оригинала),
* а диск трогаем только здесь. */
/* В ЗАСТАВОЧНОЙ ДЕМКЕ МУЗЫКИ НЕТ. Точечные условия оригинала (смерть,
* убитый страж) расставлены по своим местам, но гейт нужен и общий:
* демо проигрывает записанные нажатия, и любой музыкальный повод —
* подобранный меч, зелье — зазвучал бы там, где оригинал играет одни
* эффекты. */
if (ring_on) { ring_step(); return; } /* потоковый трек — свой путь */
if (!pop_mus_want || pop_app_state == POP_APP_DEMO) {
/* Выключили на ходу: оборвать поток и замолчать. Эффекты при этом
* продолжают звучать — у них свой тумблер. */
if (pop_mus_req) pop_mus_req = 0;
if (ld_busy || pop_mus_left) pop_music_free();
return;
}
if (pop_mus_req) {
uint8_t id = pop_mus_req;
pop_mus_req = 0;
(void)pop_music_stream(id);
} else if (ld_busy) {
(void)pop_music_load_step();
}
}
uint8_t pop_music_busy(void) __banked
{
/* «ЗВУЧИТ», А НЕ «ЕСТЬ КУРСОР». Курсор трека двигает насос CBL, и пока
* вывод закрыт (pop_sfx_pause на загрузке уровня, Ctrl+S, показ заставки
* после Restart Game) pop_mus_left не убывает НИКОГДА. Вызывающий,
* который ждёт конца мелодии, в этом состоянии висит вечно — ровно так
* заставка после Restart Game вставала на экране Prince of Persia
* (поймано пользователем 2026-09-02). Корень того случая был в другом
* (CBL не открывался заново, см. pop_boot_sound), но ждать неиграющую
* музыку нельзя в принципе, поэтому признак спрашивает и про вывод. */
return (uint8_t)(pop_snd_ok && pop_mus_left != 0);
}
/* ТРЕК В РАБОТЕ — заявка лежит, идёт загрузка или он звучит.
*
* Одного pop_music_busy() там, где ждут КОНЦА мелодии, мало: между
* заявкой (pop_sfx_play кладёт pop_mus_req) и первым звуком проходит
* 190-230 мс — открытие файла и чтение первой полустраницы (замер и
* разбор — docs/sound_plan.md §9). В этой щели busy ещё ложен, и
* ожидание закончилось бы, не начавшись.
*
* Три признака подряд закрывают её без единой подобранной константы:
* pop_music_service обнуляет req и тут же взводит ld_busy в load_begin,
* а тот держится до самой записи pop_mus_left. Снаружи переход
* «запрошен -> грузится -> звучит» непрерывен, потому что между этими
* операторами главный цикл не выполняется.
*
* И наоборот: если трека нет или память кончилась, все три ложны сразу —
* ждать нечего, как и должно быть. */
uint8_t pop_music_active(void) __banked
{
return (uint8_t)(pop_mus_req || ld_busy || pop_mus_left);
}