4a939e42f8
ТАЙМИНГИ. Треки заставок между уровнями длиннее самих сцен (27 — 10,7 с против 2,6 с картинки), и оригинал их не обрывает: load_intro гасит экран и возвращает управление, а звук продолжает играть поверх загрузки уровня и первых секунд игры (seg001:690). Мы глушили CBL на выходе — музыка обрывалась на полуслове. Больше не глушим. «ВРЕМЯ ВЫШЛО». Оригинал (seg001:04D3) не зовёт ни одного init_*: на экране пустая комната и ОСТАНОВИВШИЕСЯ часы (state 7, струйки песка нет). Мы показывали готовую композицию с принцессой и полными часами — противоположное по смыслу. Теперь это живая сцена без персонажей, 102 кадра, и трек 36 дослушивается под чёрным экраном, как в оригинале. ПЕСОЧНЫЕ ЧАСЫ. Сцена «времени мало» (cutscene_12) шла статической композицией, куда упаковщик запёк res953 — ПОЛНЫЕ часы. Отсюда и наблюдение «чем меньше времени, тем полнее часы»: картинка была одна и та же независимо от таймера. Сделана живой (cut_scene_12_short: принцесса стоит правее, через два кадра оборачивается), часы берутся от pre_hourglass_state. Заодно: при state 7 струйка песка не рисуется. МУЗЫКА — СВОЙ ТУМБЛЕР. Settings -> MUSIC, Ctrl+M и статус в debug bar. Флаг pop_mus_want отдельный от звукового: музыка у нас поток с диска, и выключают её по другим причинам, чем эффекты; выключение обрывает текущий трек, эффекты продолжают звучать. Строка-заглушка GAMEPLAY PROFILE («VANILLA ONLY») уступила место MUSIC — на десятую строку экрана не хватает. Подписи тумблеров в debug bar сокращены до S:/M:/I:. БАГИ. После меню персонаж оставался невидимым до первого движения: снимок слота продолжал утверждать, что кадр на странице уже нарисован. Общая pop_cd_forget() теперь зовётся после меню, QuickLoad и переворота экрана. «QUICKLOAD» мигал через кадр — pop_status_show_now снимал бит видимой страницы, а её успевало переписать восстановление комнаты. Host-тесты: 15 наборов. Проверено в MAME: debug bar, выход из меню. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
243 lines
9.6 KiB
C
243 lines
9.6 KiB
C
/*
|
|
* 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 "pop_music.h"
|
|
#include "pop_music_tbl.h"
|
|
#include "pop_sfx.h"
|
|
|
|
uint8_t pop_mus_page[POP_MUS_PAGES];
|
|
|
|
/* Два слота: играющий и подгружаемый. Своей таблицы страниц слот НЕ
|
|
* держит — её и так хранит блок 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_pages;
|
|
static uint8_t ld_id;
|
|
|
|
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;
|
|
}
|
|
|
|
void pop_music_stop(void) __banked
|
|
{
|
|
pop_mus_left = 0;
|
|
}
|
|
|
|
void pop_music_free(void) __banked
|
|
{
|
|
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 кадрах в секунду сцена встала бы на пять кадров.
|
|
* Одна страница стоит 33 мс — меньше четверти кадра, и подкачка растворя-
|
|
* ется в анимации. Кто может позволить себе паузу (титры, чёрный экран
|
|
* между сценами), зовёт pop_music_load — тот же цикл, но целиком. */
|
|
static void load_abort(void)
|
|
{
|
|
if (ld_busy) close(ld_fd);
|
|
ld_busy = 0;
|
|
slot_release(ld_slot);
|
|
}
|
|
|
|
int8_t pop_music_load_begin(uint8_t id) __banked
|
|
{
|
|
char name[12];
|
|
uint8_t pages, n;
|
|
|
|
if (ld_busy) load_abort();
|
|
if (id >= POP_MUS_COUNT) return -1;
|
|
pages = pop_mus_tbl[id].pages;
|
|
if (!pages || 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 КБ), и платить его на каждую страницу — чистые потери. */
|
|
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_pages = pages;
|
|
ld_id = id;
|
|
return 0;
|
|
}
|
|
|
|
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);
|
|
/* Последняя страница короче — читаем сколько есть; ошибку ловим по
|
|
* отрицательному коду, а не по «не 16384». */
|
|
if (!p || bank_read_page(ld_fd, p, 0, 16384) < 0) {
|
|
load_abort();
|
|
return -1;
|
|
}
|
|
if (++ld_next < ld_pages) return 0;
|
|
|
|
close(ld_fd);
|
|
ld_busy = 0;
|
|
slot_pages[ld_slot] = ld_pages;
|
|
slot_blocks[ld_slot] = pop_mus_tbl[ld_id].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 (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] = pop_mus_tbl[id].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;
|
|
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];
|
|
}
|
|
|
|
void pop_music_service(void) __banked
|
|
{
|
|
/* Один вызов на кадр из ЛЮБОГО цикла — игрового, интро, катсцены.
|
|
* Заявку кладёт pop_sfx_play (звук без оцифровки = музыка оригинала),
|
|
* а диск трогаем только здесь. */
|
|
if (!pop_mus_want) {
|
|
/* Выключили на ходу: оборвать поток и замолчать. Эффекты при этом
|
|
* продолжают звучать — у них свой тумблер. */
|
|
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
|
|
{
|
|
return (uint8_t)(pop_mus_left != 0);
|
|
}
|