Files
Sprinter-SDCC/applications/PoP/roomtest/pop_music.c
T
snark13 faec9a7d3a Финальная тема won: кольцевой стриминг с диска
Последний неозвученный кусок игры. 115 секунд и 1,2 МБ в память не влезают
никак, поэтому тема играется кольцом из шести страниц (96 КБ = 9 с): насос
идёт по кругу, а pop_music_service дочитывает файл в те страницы, которые
насос уже прошёл. Страница звучит 1,5 с и читается 33 мс — запас
сорокакратный; дистанция считается без отдельных счётчиков, потому что
страница это ровно 128 блоков насоса.

Три вещи, всплывшие при живой проверке:

- кольцо обязано сниматься при любом обычном запуске трека, иначе
  следующая тема играет по кругу первых шести страниц (поймано на титрах
  сразу после победы);
- живые сцены комнаты принцессы теперь открывают CBL сами (cut_begin):
  pop_ending_show глушит звук первым действием, и «arrived to princess»
  не звучал вовсе;
- тема дослушивается до конца с возможностью прервать клавишей — как
  seg001:637. У оригинала там стоит ввод имени в таблицу рекордов; когда
  он появится, ожидание переедет за него (задача HOF-ENTRY).

Проверено в MAME на сборке LEVEL=14: после встречи с принцессой звучит
тема победы (id 56, кольцо 6), курсор уходит далеко за размер кольца —
подкачка успевает. Host-тесты: 15 наборов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:32:40 +03:00

363 lines
16 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 "pop_music.h"
#include "pop_music_tbl.h"
#include "pop_sfx.h"
#include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */
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;
}
/* --- ПОТОКОВЫЙ ТРЕК: КОЛЬЦО СТРАНИЦ ---------------------------------- *
*
* Финальная тема (`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 кадрах в секунду сцена встала бы на пять кадров.
* Одна страница стоит 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 (ring_on) pop_music_ring_stop(); /* потоковый трек и слоты не дружат */
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 (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] = 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;
/* СНЯТЬ КОЛЬЦО. Если до этого играл потоковый трек (финал), насос всё
* ещё заворачивал бы страницы по кругу шести — обычный трек звучал бы
* своей первой третью без конца (поймано на титрах после победы). */
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];
uint8_t i, page;
if (id >= POP_MUS_COUNT || !pop_mus_tbl[id].pages) return -1;
pop_music_free(); /* обычные слоты кольцу не нужны */
pop_music_ring_stop();
mus_name(name, id);
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 = pop_mus_tbl[id].pages;
ring_total = pop_mus_tbl[id].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
{
return (uint8_t)(pop_mus_left != 0);
}