SprPoP: музыка без перелинковки — длины и длительности уехали на диск
Часть I плана music_runtime_index_plan.md (MI0..MI5). gen/pop_music_tbl.h и gen/pop_music_ticks.h УДАЛЕНЫ: длины треков и длительности реплик читаются из MUS/mus.idx (формат PMI1, tools/pop_idx.py, тесты в make test-tools). Один и тот же sprpop.exe работает с любым из четырёх наборов записей — sha256 бинарника при смене MUSIC_FMT не меняется. ГДЕ ЖИВЁТ ИНДЕКС. 228 байт таблицы в W2 не положить (свободной кучи там порядка двух сотен), поэтому индекс лежит в одной странице EMM, а в резиденте от него два байта. Данные в странице — со смещения 0x100: gfx_w0_page_prepare пишет в неё стабы прерываний (0x38 и 0x66), и с нуля они попали бы прямо в записи id 10 и 21. Со смещением работает штатная защита, а не запрет прерываний (тот же приём, что CFG_BASE в pop_config.c). Число страниц в индексе не хранится — считается из blocks, чтобы не разъехалось. ПАУЗА КОНЦА УРОВНЯ — СОСТОЯНИЕМ, А НЕ СЧЁТЧИКОМ. pop_endmus_left и POP_MUS_TICKS_32/41 удалены; главный цикл ждёт pop_music_active() — «заявка лежит, идёт загрузка или трек звучит». Одного busy мало: между заявкой и первой нотой 190-230 мс (замер в sound_plan §9). Прежний счётчик закрывал эту щель ценой зависимости EXE от набора и жёсткого делителя /4, который врал в режимах FAST/FASTEST (там логический кадр 3 кадра луча, а не 4). Побочно исправилось расхождение с SDLPoP: при выключенном звуке заявка не кладётся, и уровень меняется сразу, как в оригинале (seg006:651 + seg003:387) — раньше игра держала пройденный уровень лишние 12 секунд в тишине. PV-СЦЕНА — на четырёх якорях (8 байт статики), которые считаются из индекса при входе в сцену; прежние выражения шкалы не изменились. План предлагал протащить структуру времён через пять функций — для сцены, которая идёт раз за запуск, это того не стоит. ПАМЯТЬ. За обе фазы резидент не вырос, а освободился: _CODE 23865 -> 23544, куча 239 -> 256 Б. Банк 9 похудел на 118 Б (ушла pop_mus_tbl из rodata), банк 11 — на длительности реплик. ПРОВЕРЕНО В MAME: exe побайтово одинаков для flac и mt32; все 22 трека в индексах различаются, и контрольные значения совпали с предсказанными планом (m41 732->685, m50 831->867, m53 985->1044, m56 9865->10462 блоков, 78->82 страницы); на mt32 PV-сцена проходит целиком по его длительностям; без mus.idx музыки нет, эффекты работают, игра проходима. НЕ ПРОВЕРЕНО: потоковый m56 на 82 страницах — до финала надо дойти в игре. Единственный оставшийся пункт приёмки, отмечен в sound_plan §11.5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
This commit is contained in:
@@ -13,14 +13,81 @@
|
||||
#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_music_tbl.h"
|
||||
#include "pop_sfx.h"
|
||||
#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, для
|
||||
@@ -53,6 +120,9 @@ 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)
|
||||
{
|
||||
@@ -126,13 +196,17 @@ static void load_abort(void)
|
||||
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;
|
||||
pages = pop_mus_tbl[id].pages;
|
||||
if (!pages || pages > POP_MUS_PAGES) 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;
|
||||
|
||||
/* Грузим ВСЕГДА в не текущий слот: играющий трек читает свои страницы
|
||||
* из прерывания, и отобрать их у него нельзя. */
|
||||
@@ -152,6 +226,7 @@ int8_t pop_music_load_begin(uint8_t id) __banked
|
||||
ld_half = 0;
|
||||
ld_pages = pages;
|
||||
ld_id = id;
|
||||
ld_blocks = info.blocks;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -183,7 +258,7 @@ int8_t pop_music_load_step(void) __banked
|
||||
close(ld_fd);
|
||||
ld_busy = 0;
|
||||
slot_pages[ld_slot] = ld_pages;
|
||||
slot_blocks[ld_slot] = pop_mus_tbl[ld_id].blocks;
|
||||
slot_blocks[ld_slot] = ld_blocks;
|
||||
slot_id[ld_slot] = ld_id;
|
||||
mus_ready = (uint8_t)(ld_slot + 1);
|
||||
return 1;
|
||||
@@ -229,7 +304,7 @@ int8_t pop_music_stream(uint8_t id) __banked
|
||||
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_blocks[n] = ld_blocks;
|
||||
slot_id[n] = id;
|
||||
pop_mus_id = id;
|
||||
pop_mus_pg = 0;
|
||||
@@ -283,9 +358,15 @@ void pop_music_ring_stop(void) __banked
|
||||
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_mus_tbl[id].pages) return -1;
|
||||
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);
|
||||
@@ -294,8 +375,8 @@ int8_t pop_music_ring_start(uint8_t id) __banked
|
||||
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;
|
||||
ring_file_pages = (uint8_t)pop_music_pages(info.blocks);
|
||||
ring_total = info.blocks;
|
||||
/* Заполняем кольцо целиком: старт с полным буфером даёт девять секунд
|
||||
* форы, и первая же тяжёлая сцена финала не догонит насос. */
|
||||
for (i = 0; i < RING_PAGES; i++) {
|
||||
@@ -375,3 +456,24 @@ uint8_t pop_music_busy(void) __banked
|
||||
{
|
||||
return (uint8_t)(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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user