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:
2026-08-31 17:17:59 +03:00
parent 2349481b86
commit df5071a967
17 changed files with 587 additions and 330 deletions
+45
View File
@@ -28,6 +28,47 @@
* либо резать, либо стримить, и это отдельная задача. */
#define POP_MUS_PAGES 20
/* --- ИНДЕКС НАБОРА (MUS/mus.idx, формат PMI1) ------------------------- *
*
* Длины треков и длительности реплик приезжают С ДИСКА, а не из EXE: так
* набор записей (flac/mp3/ogg/mt32) меняется без перекомпиляции игры.
* Формат пишет tools/pop_idx.py, он же покрыт make test-tools.
*
* Индекс живёт в ОДНОЙ странице EMM, а не в W2: таблица на 57 записей —
* 228 байт, а свободной кучи в резиденте порядка двух сотен. Наружу
* указатель на EMM не выдаётся: pop_music_info копирует запись в буфер
* вызывающего, на короткое время подключая страницу в W0. */
#define POP_MUS_COUNT 57 /* пространство id оригинала, 0..56 */
#define POP_MUS_IDX_REC 4 /* blocks (u16) + ticks60 (u16) */
#define POP_MUS_IDX_HDR 16
#define POP_MUS_IDX_SIZE 512 /* сектор DSS */
/* Индекс кладётся в страницу СО СМЕЩЕНИЯ 0x100, а не с нуля. Причина в
* gfx_w0_page_prepare: он делает страницу безопасной в W0, записывая в
* неё переход на IRQ-восстановитель (0x38) и RETN для NMI (0x66). С
* нуля эти байты легли бы прямо в записи (id 10 и 21) и испортили
* индекс; со смещением 0x100 стабы и данные не пересекаются, и работает
* штатная защита от прерываний, а не запрет их. Тот же приём и по той
* же причине — в pop_config.c (CFG_BASE). */
#define POP_MUS_IDX_BASE 0x100
typedef struct {
uint16_t blocks; /* длина трека в порциях насоса по 128 байт */
uint16_t ticks; /* та же длина в тиках сцены (60 Гц оригинала) */
} pop_mus_info_t;
/* Поднять индекс (один раз за запуск). 0 — ок, -1 — нет файла либо он
* не той версии; во втором случае музыки не будет, но игра работает. */
int8_t pop_music_index_load(void) __banked;
/* Запись трека в буфер вызывающего. -1 — индекса нет, id вне диапазона
* или трека нет в наборе (тогда поля нулевые). */
int8_t pop_music_info(uint8_t id, pop_mus_info_t *out) __banked;
/* Сколько EMM-страниц занимает трек такой длины. Считается, а НЕ хранится
* в индексе: дублирование однажды разъедется с blocks. */
#define pop_music_pages(blocks) \
(uint16_t)(((blocks) >> 7) + (((blocks) & 127) != 0))
/* Номера треков — нумерация оригинала (SDLPoP data/music/names.txt). */
#define POP_MUS_INTRO 54 /* тема заставки, звучит на титрах */
#define POP_MUS_STORY_1 55 /* «In the absence of the Sultan...» */
@@ -102,4 +143,8 @@ void pop_music_ring_stop(void) __banked;
/* Трек ещё звучит? 0 — доиграл или не запускался. */
uint8_t pop_music_busy(void) __banked;
/* «Трек в работе»: заявка/загрузка/звучание. Для мест, где ждут КОНЦА
* мелодии, — busy там ложен в окне запуска (см. pop_music.c). */
uint8_t pop_music_active(void) __banked;
#endif /* POP_MUSIC_H */