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
+73 -42
View File
@@ -15,7 +15,6 @@
#include "pop_shadow.h"
#include "pop_sfx.h"
#include "pop_music.h"
#include "pop_music_ticks.h" /* POP_MUS_TICKS_<id> — длина реплик набора */
#include "pop_arc.h"
#include "pv_arc.h"
#include "pop_cutscene.h"
@@ -60,9 +59,10 @@
* своего куска (например, всё поведение Джафара — от PV_DIALOG1_START).
* 2) «ЖДЁМ КОНЦА РЕПЛИКИ» — PV_M50_END и PV_EXIT_START. Это целиком
* длина НАШЕЙ записи, и она у каждого набора своя: m50 — 831 тик на
* flac против 867 на mt32, m53 — 985 против 1044. Поэтому берём их
* из POP_MUS_TICKS_<id>, которые печатает упаковщик того набора,
* которым собрана сборка (gen/pop_music_tbl.h).
* flac против 867 на mt32, m53 — 985 против 1044. Раньше их
* печатал упаковщик в gen/pop_music_ticks.h, из-за чего EXE был
* привязан к набору; теперь они приезжают из индекса набора
* (MUS/mus.idx) при входе в сцену — см. ЯКОРЯ ниже.
*
* Всё, что стоит ПОСЛЕ реплики, сдвигается вместе с её концом — отсюда
* сложения от PV_M50_END и PV_EXIT_START вместо литералов. */
@@ -72,46 +72,76 @@ enum {
/* Конец m50: кадр становится 8. Прежний литерал 846 отвечал длине 834
* тика — на три больше нашей записи (831); расхождение осталось с
* времён, когда длину подставляли руками. */
PV_M50_END = PV_MUS_2_START + POP_MUS_TICKS_50,
PV_WAIT_END = PV_M50_END + 40, /* +frame(5) -> створка ворот */
PV_GATE_END = PV_WAIT_END + 48,
PV_DOOR_END = PV_GATE_END + 24,
PV_TURN_START = PV_DOOR_END,
PV_WALK1_START = PV_DOOR_END + 40,
PV_DIALOG1_START = PV_WALK1_START + 48, /* Джафар входит — реплика m53 */
/* Ниже — биты сценария внутри реплики Джафара, все от её начала. */
PV_MUS_4_LOAD = PV_DIALOG1_START + 54, /* подкачка m52 */
PV_WALK2_START = PV_DIALOG1_START + 176,
PV_DIALOG2_START = PV_DIALOG1_START + 416,
/* ПОДГОНКА ЗАКЛИНАНИЯ ПОД МУЗЫКУ СНЯТА (2026-08-28): теперь 0.
*
* История: сцена была render-bound — кадр рисовался дороже своего
* бюджета, кадровая ветка пейсинга догона не имела, и анимация шла
* ~49 тиков в секунду вместо 60 (замер 2026-08-27). Реплика Джафара
* при этом играет по РЕАЛЬНОМУ времени, поэтому кода приходила
* примерно на секунду раньше молнии, и жест целиком (замах, шаг
* назад, вспышка) сдвигали раньше сценария на подобранные на слух
* 100 тиков.
*
* Причину убрали: отрисовка разложена по кадровым интервалам
* (pv_restore_bg), сцена идёт по единственным часам — кадрам луча, —
* и подкачка трека оплачивается интервалом (pop_music_load_step).
* После этого подгонка стала вредной: молния била больше чем на
* секунду раньше коды (проверка пользователем 2026-08-28). Константу
* оставляем на месте — если запись другого набора (mt32/ogg) снова
* разъедется, крутить надо её, а не тайминги сценария. */
PV_MAGIC_LEAD = 0,
PV_RAISE_START = PV_DIALOG1_START + 696 - PV_MAGIC_LEAD,
PV_STEPBACK_START = PV_DIALOG1_START + 703 - PV_MAGIC_LEAD,
PV_MAGIC_START = PV_DIALOG1_START + 822 - PV_MAGIC_LEAD,
/* Конец m53 — снова длина записи, а не бит сценария. */
PV_EXIT_START = PV_DIALOG1_START + POP_MUS_TICKS_53,
PV_MUS_4_START = PV_EXIT_START + 42, /* +frame(6) -> «Джафар уходит» */
PV_GLASS_DONE = PV_EXIT_START + 210,
PV_SLUMP_START = PV_EXIT_START + 273,
PV_ANIM_TICKS = PV_EXIT_START + 469
PV_MAGIC_LEAD = 0
};
/* ПОДГОНКА ЗАКЛИНАНИЯ ПОД МУЗЫКУ СНЯТА (2026-08-28): теперь 0.
*
* История: сцена была render-bound — кадр рисовался дороже своего
* бюджета, кадровая ветка пейсинга догона не имела, и анимация шла
* ~49 тиков в секунду вместо 60 (замер 2026-08-27). Реплика Джафара
* при этом играет по РЕАЛЬНОМУ времени, поэтому кода приходила
* примерно на секунду раньше молнии, и жест целиком (замах, шаг
* назад, вспышка) сдвигали раньше сценария на подобранные на слух
* 100 тиков.
*
* Причину убрали: отрисовка разложена по кадровым интервалам
* (pv_restore_bg), сцена идёт по единственным часам — кадрам луча, —
* и подкачка трека оплачивается интервалом (pop_music_load_step).
* После этого подгонка стала вредной: молния била больше чем на
* секунду раньше коды (проверка пользователем 2026-08-28). Константу
* оставляем на месте — если запись другого набора (mt32/ogg) снова
* разъедется, крутить надо её, а не тайминги сценария. */
/* ЯКОРЯ ШКАЛЫ. Четыре точки, от которых отсчитывается всё остальное;
* считаются ОДИН РАЗ при входе в сцену из длительностей набора
* (pop_music_info). Держать их статикой, а не тащить структуру времён
* через пять функций: восемь байт против переделки всех сигнатур, а
* сцена всё равно одна на запуск.
*
* Нет индекса или трека — длительность нулевая, и сцена просто проходит
* без пауз на реплики: ждать нечего, звука-то нет. */
static uint16_t pv_m50_end; /* конец «принцесса ждёт» (m50) */
static uint16_t pv_dialog1; /* Джафар входит — начало реплики m53 */
static uint16_t pv_exit; /* конец m53: Джафар уходит */
static uint16_t pv_anim_end; /* конец всей сцены */
/* Ниже — те же выражения, что были в enum: биты сценария отсчитываются от
* своего якоря и от записи НЕ зависят. */
#define PV_M50_END pv_m50_end
#define PV_WAIT_END ((uint16_t)(pv_m50_end + 40))
#define PV_GATE_END ((uint16_t)(pv_m50_end + 88))
#define PV_DOOR_END ((uint16_t)(pv_m50_end + 112))
#define PV_TURN_START PV_DOOR_END
#define PV_WALK1_START ((uint16_t)(pv_m50_end + 152))
#define PV_DIALOG1_START pv_dialog1
#define PV_MUS_4_LOAD ((uint16_t)(pv_dialog1 + 54))
#define PV_WALK2_START ((uint16_t)(pv_dialog1 + 176))
#define PV_DIALOG2_START ((uint16_t)(pv_dialog1 + 416))
#define PV_RAISE_START ((uint16_t)(pv_dialog1 + 696 - PV_MAGIC_LEAD))
#define PV_STEPBACK_START ((uint16_t)(pv_dialog1 + 703 - PV_MAGIC_LEAD))
#define PV_MAGIC_START ((uint16_t)(pv_dialog1 + 822 - PV_MAGIC_LEAD))
#define PV_EXIT_START pv_exit
#define PV_MUS_4_START ((uint16_t)(pv_exit + 42))
#define PV_GLASS_DONE ((uint16_t)(pv_exit + 210))
#define PV_SLUMP_START ((uint16_t)(pv_exit + 273))
#define PV_ANIM_TICKS pv_anim_end
/* Собрать шкалу под НАШ набор записей. 200 — цепочка битов сценария от
* конца m50 до входа Джафара (40+48+24+40+48), 469 — хвост после его
* ухода; оба от записи не зависят. */
static void pv_timing_init(void)
{
pop_mus_info_t m50, m53;
(void)pop_music_info(POP_MUS_STORY_2, &m50);
(void)pop_music_info(POP_MUS_STORY_3, &m53);
pv_m50_end = (uint16_t)(PV_MUS_2_START + m50.ticks);
pv_dialog1 = (uint16_t)(pv_m50_end + 200);
pv_exit = (uint16_t)(pv_dialog1 + m53.ticks);
pv_anim_end = (uint16_t)(pv_exit + 469);
}
/* Возврат долга шкалы (см. цикл сцены): сколько порций насоса отдаём за один
* кадр сцены и сколько их вообще имеет смысл копить. 1 порция — 11,7 мс;
* 128 порций — полторы секунды, дальше догонять уже нечего. */
@@ -870,6 +900,7 @@ static int intro_pv_animated(void)
jaffar_actor.visible = 0;
jaffar_actor.step = 0;
tick = 0;
pv_timing_init(); /* шкала под длительности НАШЕГО набора */
intro_skip_begin(&skip);
while (tick < PV_ANIM_TICKS) {
if (intro_skip_requested(&skip)) {