SprPoP: звуковые эффекты без перелинковки — раскладка уехала на диск

Часть II плана music_runtime_index_plan.md (SI0..SI4).  gen/pop_sound_tbl.h
БОЛЬШЕ НЕ ГЕНЕРИРУЕТСЯ: раскладка набора читается из SND/snd.idx (формат
PSI1, писатель и разборщик — tools/pop_idx.py, 22 теста в make test-tools).
Один и тот же sprpop.exe работает с набором SDLPoP (9 страниц) и MSDOS
(10) — sha256 бинарника при смене набора не меняется.

Заодно умолчание источника эффектов переведено на SDLPoP (SND_SRC=sdlpop):
сборка обязана работать без оригинального дистрибутива DOS.  У кого он
есть, включает лучший набор явно — make SND_SRC=msdos (там полнее
оцифровка: в SDLPoP звук 48 spiked пустой).

Устройство: pop_snd_tbl/pop_snd_page/pop_snd_pages — резидентные данные
(pop_snd_data.c), тип и инварианты — рукописный pop_snd_tbl.h.  Записи
читаются ОДНИМ read прямо в таблицу, поэтому sizeof(pop_snd_ent_t) == 5
стало частью дискового контракта: проверяется статически и полем размера
записи в заголовке.  POP_SND_PAGES как compile-time размер набора исчез —
вместо него POP_SND_MAX_PAGES (вместимость, 16) и runtime pop_snd_pages.

Цена: таблица переехала из _CODE в _DATA, суммарный резидент почти не
изменился (куча 239 -> 229 Б); банк 8 +601 Б на чтение и валидацию.

Валидация не доверяет файлу: заголовок целиком плюс каждая запись
(страница, смещение, кратность блоку, непересечение с блоком тишины,
выход за последнюю страницу).  Последнее считается В БЛОКАХ — байтовый
адрес конца не влезает в uint16, а 32-битная арифметика на Z80 дорога.

НЕТ ИНДЕКСА — ЭФФЕКТОВ НЕТ, НО МУЗЫКА ИГРАЕТ.  Первая версия просто
возвращала ошибку, и игра становилась непроходимой: тишину льёт первый
блок набора, без набора CBL не открывался, а с ним вставала музыка (её
блоки считает тот же насос) — заставка ждала конца трека вечно.  Теперь
поднимается пустой набор с блоком тишины.  Заливается ровно 128 байт и
под DI: gfx_w0_page_prepare ставит в страницу IRQ-стабы, и заливка всей
страницы затирала их — первое же прерывание давало чёрный экран.

Грабли сборки: смена SND_SRC тихо давала неверный результат
(sdlpop -> msdos -> sdlpop оставлял чужой набор в assets/packed).  Причина
не в логике, а в секундной гранулярности mtime.  Лечение убирает время из
решения: смена варианта сносит stamp'ы своего семейства, а упаковка,
сборка архива и копия индекса делаются одним рецептом.  То же получила и
музыка (MUSIC_FMT).

Проверено в MAME: таблица в памяти совпадает с файлом из образа побайтово;
один EXE поднимает оба набора; отладочный --order reverse (30 из 31
записей отличаются от штатных) звучит правильно; битый индекс выключает
эффекты, не роняя игру; без индекса PV-сцена проходит с музыкой; Ctrl+S
работает в обоих режимах.  Разбор — docs/sound_plan.md §10.

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 16:14:53 +03:00
parent 589894c50d
commit 2349481b86
19 changed files with 1077 additions and 207 deletions
+43 -3
View File
@@ -5,9 +5,10 @@ assets/, gen/ и build/.
Раскладка:
assets/orig/ ОРИГИНАЛЬНЫЕ данные (SDLPoP, MSDOS, записи музыки).
В репозиторий не идут — копирайт; откуда их взять,
написано в assets/orig/README.md.
assets/orig/ ОРИГИНАЛЬНЫЕ данные (SDLPoP, записи музыки и,
необязательно, MSDOS). В репозиторий не идут —
копирайт; выкачивает их tools/fetch_orig.py
(`make fetch`), см. assets/orig/README.md.
build/atl/<set>/ промежуточный выхлоп упаковщиков: отдельные атласы,
из которых потом склеиваются архивы. Пересоздаётся,
в репозиторий не идёт.
@@ -31,6 +32,7 @@ SDLPOP = ORIG / "SDLPoP"
SDLPOP_DATA = SDLPOP / "data"
SDLPOP_SRC = SDLPOP / "src"
MSDOS = ORIG / "MSDOS"
MUSIC_ROOT = ORIG / "PoP1_DOS_music"
# Записи DOS-версии лежат в четырёх вариантах. Имена треков во всех наборах
# одинаковые, различаются каталог и расширение; ogg_MT-32 — та же музыка в
@@ -52,6 +54,44 @@ def music_src(fmt="flac"):
sub, ext = MUSIC_FORMATS[fmt]
return MUSIC_ROOT / sub, ext
# --- источники звуковых эффектов ------------------------------------------
# Контейнеры digisnd1..3 есть в ДВУХ поставках, и они не одинаковы: в SDLPoP
# лежит версия из репозитория (её и качает fetch_orig.py), в MSDOS/ — из
# оригинального дистрибутива 1.0, если пользователь его себе положил.
# ПО УМОЛЧАНИЮ БЕРЁМ SDLPoP: сборка обязана работать без MSDOS вообще.
# Ключ = значение SND_SRC в Makefile; регистр имён в поставках разный.
SND_SOURCES = {
"sdlpop": (SDLPOP_DATA, "DIGISND%d.DAT"),
"msdos": (MSDOS, "digisnd%d.dat"),
}
def snd_src(name="sdlpop"):
"""(каталог, шаблон имени) контейнеров оцифровки для источника."""
if name not in SND_SOURCES:
raise SystemExit(
"неизвестный источник звука %r; есть: %s"
% (name, ", ".join(sorted(SND_SOURCES))))
return SND_SOURCES[name]
def find_ci(directory, name):
"""Путь к файлу без оглядки на регистр (FAT-имена приезжают по-разному).
Возвращает None, если файла нет. Точное совпадение проверяется первым,
чтобы на case-insensitive ФС не читать каталог зря.
"""
p = directory / name
if p.exists():
return p
if not directory.is_dir():
return None
low = name.lower()
for e in directory.iterdir():
if e.name.lower() == low:
return e
return None
# --- выход ----------------------------------------------------------------
ATL = APP / "build" / "atl" # промежуточные атласы, по набору в папке
PACKED = APP / "assets" / "packed" # раскладка диска игры