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:
@@ -30,6 +30,30 @@ ABI SDCC z80 (uint8 + uint16 + uint16 без выравнивания). Заг
|
||||
молча разъедет всю раскладку; на стороне C это проверяется статически.
|
||||
|
||||
Нулевая `len` означает, что у этого id нет оцифровки (это музыка).
|
||||
|
||||
PMI1 — индекс МУЗЫКИ, тоже один сектор:
|
||||
|
||||
0..3 'PMI1' магия и версия
|
||||
4 57 число плотных записей, id 0..56
|
||||
5 4 размер записи
|
||||
6 7 log2 размера блока
|
||||
7 1 формат PCM
|
||||
8..15 0 резерв
|
||||
16..243 57 записей по 4 байта: blocks (u16), ticks60 (u16)
|
||||
244..511 0 добивка до сектора
|
||||
|
||||
Числа страниц здесь НЕТ намеренно: это производная от `blocks`, и
|
||||
дублирование однажды разъедется с ним. Загрузчик считает сам:
|
||||
|
||||
pages = blocks / 128 + ((blocks & 127) != 0)
|
||||
|
||||
`ticks60` — длительность в тиках сцены (60 Гц оригинала), нужная там, где
|
||||
сценарий ждёт конца реплики. Считается из blocks отношением 40/57 —
|
||||
обратным к тому, которым шкала сцены переводит тики в порции насоса.
|
||||
Хранится ОТДЕЛЬНО, а не выводится в рантайме: так момент окончания
|
||||
реплики можно будет уточнить независимо от технического хвоста PCM.
|
||||
|
||||
Нулевой `blocks` означает, что трека с таким id в наборе нет.
|
||||
"""
|
||||
|
||||
import struct
|
||||
@@ -124,3 +148,83 @@ def _check_entry(i, page, off, ln, pages):
|
||||
if last >= pages:
|
||||
raise IdxError("запись %d выходит за набор: кончается в стр.%d из %d"
|
||||
% (i, last, pages))
|
||||
|
||||
|
||||
# --- PMI1: музыка ----------------------------------------------------------
|
||||
MUS_MAGIC = b"PMI1"
|
||||
MUS_COUNT = 57 # то же пространство id оригинала
|
||||
MUS_REC = 4
|
||||
MUS_HDR = 16
|
||||
# Потоковый трек (финальная тема) заведомо длиннее обычных, и предел ему
|
||||
# ставят не страницы загрузчика, а восьмибитные счётчики страниц в коде.
|
||||
MUS_MAX_PAGES = 255
|
||||
|
||||
|
||||
def mus_ticks(blocks):
|
||||
"""Длительность в тиках сцены (60 Гц) по длине в блоках.
|
||||
|
||||
Отношение 40/57 — обратное к тому, которым шкала сцены переводит тики
|
||||
в порции насоса (57/40, pop_intro.c). Берём именно его, а не точное
|
||||
10937,5/128/60: длина реплики и ход шкалы обязаны считаться ОДНОЙ
|
||||
арифметикой, иначе они разъедутся на длинных треках. Округление к
|
||||
ближайшему: отбрасывание вниз теряло по тику на реплику.
|
||||
"""
|
||||
return (blocks * 40 + 28) // 57
|
||||
|
||||
|
||||
def mus_pack(entries):
|
||||
"""Собрать PMI1 из 57 записей (blocks, ticks60)."""
|
||||
if len(entries) != MUS_COUNT:
|
||||
raise IdxError("записей должно быть %d, а не %d"
|
||||
% (MUS_COUNT, len(entries)))
|
||||
out = bytearray(SECTOR)
|
||||
out[0:4] = MUS_MAGIC
|
||||
out[4] = MUS_COUNT
|
||||
out[5] = MUS_REC
|
||||
out[6] = BLOCK_LOG2
|
||||
out[7] = PCM_U8_MONO_10K9
|
||||
for i, (blocks, ticks) in enumerate(entries):
|
||||
_check_mus(i, blocks, ticks)
|
||||
struct.pack_into("<HH", out, MUS_HDR + i * MUS_REC, blocks, ticks)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def mus_unpack(data):
|
||||
"""Разобрать PMI1 -> записи. Проверки те же, что у Z80."""
|
||||
if len(data) != SECTOR:
|
||||
raise IdxError("размер %d, а должен быть сектор %d" % (len(data), SECTOR))
|
||||
if data[0:4] != MUS_MAGIC:
|
||||
raise IdxError("не PMI1: %r" % (bytes(data[0:4]),))
|
||||
if data[4] != MUS_COUNT:
|
||||
raise IdxError("записей %d, ожидалось %d" % (data[4], MUS_COUNT))
|
||||
if data[5] != MUS_REC:
|
||||
raise IdxError("размер записи %d, ожидался %d" % (data[5], MUS_REC))
|
||||
if data[6] != BLOCK_LOG2:
|
||||
raise IdxError("log2 блока %d, ожидался %d" % (data[6], BLOCK_LOG2))
|
||||
if data[7] != PCM_U8_MONO_10K9:
|
||||
raise IdxError("формат PCM %d — насос умеет только %d"
|
||||
% (data[7], PCM_U8_MONO_10K9))
|
||||
entries = []
|
||||
for i in range(MUS_COUNT):
|
||||
blocks, ticks = struct.unpack_from("<HH", data, MUS_HDR + i * MUS_REC)
|
||||
_check_mus(i, blocks, ticks)
|
||||
entries.append((blocks, ticks))
|
||||
return entries
|
||||
|
||||
|
||||
def mus_pages(blocks):
|
||||
"""Сколько EMM-страниц занимает трек такой длины."""
|
||||
return (blocks + 127) // 128
|
||||
|
||||
|
||||
def _check_mus(i, blocks, ticks):
|
||||
if blocks == 0:
|
||||
if ticks:
|
||||
raise IdxError("запись %d: трека нет, но тики ненулевые" % i)
|
||||
return
|
||||
if mus_pages(blocks) > MUS_MAX_PAGES:
|
||||
raise IdxError("запись %d: %d блоков — больше %d страниц, счётчики "
|
||||
"страниц в коде восьмибитные"
|
||||
% (i, blocks, MUS_MAX_PAGES))
|
||||
if ticks == 0:
|
||||
raise IdxError("запись %d: трек есть, а длительность нулевая" % i)
|
||||
|
||||
Reference in New Issue
Block a user