6176f6da31
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
489 lines
23 KiB
C
489 lines
23 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 <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);
|
||
}
|