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:
@@ -17,8 +17,9 @@ sprinter-cc / libc / libbgi. Действуют правила корневог
|
|||||||
## Главное правило: SDLPoP — источник истины. Сначала читай, потом кодь
|
## Главное правило: SDLPoP — источник истины. Сначала читай, потом кодь
|
||||||
|
|
||||||
**`assets/orig/SDLPoP/src/` (github.com/NagyD/SDLPoP, GPLv3) — ЕДИНСТВЕННЫЙ
|
**`assets/orig/SDLPoP/src/` (github.com/NagyD/SDLPoP, GPLv3) — ЕДИНСТВЕННЫЙ
|
||||||
авторитетный источник того, как оригинальный движок это делает.** Правило
|
авторитетный источник того, как оригинальный движок это делает.** В
|
||||||
без исключений:
|
репозитории его нет: заполняется `make fetch` (`make fetch-check` — что уже
|
||||||
|
на месте). Правило без исключений:
|
||||||
|
|
||||||
1. **Перед реализацией ЛЮБОЙ функции** (движение, коллизия, окклюзия,
|
1. **Перед реализацией ЛЮБОЙ функции** (движение, коллизия, окклюзия,
|
||||||
падение, loose-полы, стражники, отрисовка, тайминги, любые числовые
|
падение, loose-полы, стражники, отрисовка, тайминги, любые числовые
|
||||||
@@ -69,6 +70,8 @@ make собрать build/sprpop.exe и разложить build
|
|||||||
make hdd + образ build/hdd/sprpop.chd
|
make hdd + образ build/hdd/sprpop.chd
|
||||||
make mame-link однократно: подставить образ в MAME (см. ниже)
|
make mame-link однократно: подставить образ в MAME (см. ниже)
|
||||||
make -C tests/host модульные тесты движка под ucsim_z80 (секунды, без MAME)
|
make -C tests/host модульные тесты движка под ucsim_z80 (секунды, без MAME)
|
||||||
|
make test-tools тесты упаковщиков на хосте (форматы, которые читает Z80)
|
||||||
|
make fetch скачать внешние данные в assets/orig/ (SDLPoP + музыка)
|
||||||
make resources перегенерировать ресурсы из assets/orig/
|
make resources перегенерировать ресурсы из assets/orig/
|
||||||
make music-mp3 музыка из другого набора (flac|mp3|ogg|mt32)
|
make music-mp3 музыка из другого набора (flac|mp3|ogg|mt32)
|
||||||
make clean снести build/ (ассеты не трогает)
|
make clean снести build/ (ассеты не трогает)
|
||||||
@@ -93,13 +96,19 @@ make distclean clean + снести assets/packed/ (вернуть — mak
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `src/` | рукописный C: главный цикл `sprpop.c`, холодная половина `sprpop_cold.c`, движок `pop_*.c` |
|
| `src/` | рукописный C: главный цикл `sprpop.c`, холодная половина `sprpop_cold.c`, движок `pop_*.c` |
|
||||||
| `gen/` | генерируемые заголовки (индексы архивов, таблицы кадров, шрифт, палитры). **Руками не править** — их печатают упаковщики; лежат в репозитории, потому что без них `src/` не собрать |
|
| `gen/` | генерируемые заголовки (индексы архивов, таблицы кадров, шрифт, палитры). **Руками не править** — их печатают упаковщики; лежат в репозитории, потому что без них `src/` не собрать |
|
||||||
| `assets/orig/` | оригинальные данные (SDLPoP, MSDOS, записи музыки). Вне репозитория — копирайт; что и откуда взять, написано в `assets/orig/README.md` |
|
| `assets/orig/` | оригинальные данные (SDLPoP, записи музыки; MSDOS — необязательно). Вне репозитория — копирайт; вместо них в репозитории лежит `tools/fetch_orig.py`, который их качает (`make fetch`). Правила — в `assets/orig/README.md` |
|
||||||
| `assets/packed/` | то, что ложится на диск игры, уже в раскладке диска (`BG/`, `KID/`, …). В репозитории — иначе из чистого клона не собрать |
|
| `assets/packed/` | то, что ложится на диск игры, уже в раскладке диска (`BG/`, `KID/`, …). В репозитории — иначе из чистого клона не собрать |
|
||||||
| `tools/` | конверторы ресурсов. Раскладку путей знает ОДИН файл — `tools/paths.py`; менять пути нужно там, а не в отдельных упаковщиках |
|
| `tools/` | конверторы ресурсов. Раскладку путей знает ОДИН файл — `tools/paths.py`; менять пути нужно там, а не в отдельных упаковщиках. Адреса ВНЕШНИХ источников — так же в одном: `tools/fetch_orig.py` |
|
||||||
| `docs/` | планы, доски, справочники; `docs/PoP/` — форматы ресурсов оригинала |
|
| `docs/` | планы, доски, справочники; `docs/PoP/` — форматы ресурсов оригинала |
|
||||||
| `tests/host/` | модульные тесты движка под ucsim_z80 |
|
| `tests/host/` | модульные тесты движка под ucsim_z80 |
|
||||||
| `build/` | выход: `sprpop.exe`, каталоги ресурсов, `hdd/`, промежуточные `atl/` |
|
| `build/` | выход: `sprpop.exe`, каталоги ресурсов, `hdd/`, промежуточные `atl/` |
|
||||||
|
|
||||||
|
Звуковые ЭФФЕКТЫ по умолчанию берутся из SDLPoP (`SND_SRC=sdlpop`) — сборка
|
||||||
|
обязана работать без оригинального дистрибутива DOS. У кого лежит
|
||||||
|
`assets/orig/MSDOS/`, включает его набор явно (`make SND_SRC=msdos`): там
|
||||||
|
оцифровка полнее — в SDLPoP пуст звук 48 `spiked` (насаживание на пики).
|
||||||
|
Разбор — `docs/sound_plan.md`.
|
||||||
|
|
||||||
Музыка собирается из одного из четырёх наборов записей
|
Музыка собирается из одного из четырёх наборов записей
|
||||||
(`assets/orig/PoP1_DOS_music/`): `flac` по умолчанию, плюс `mp3`, `ogg` и
|
(`assets/orig/PoP1_DOS_music/`): `flac` по умолчанию, плюс `mp3`, `ogg` и
|
||||||
`mt32` (исполнение Roland MT-32 — звучит иначе, длина треков другая). Набор
|
`mt32` (исполнение Roland MT-32 — звучит иначе, длина треков другая). Набор
|
||||||
@@ -153,6 +162,8 @@ Makefile склеивает архивы прямо в `assets/packed/<КАТА
|
|||||||
| `pop_kid.c/.h` | Анимация/движение: интерпретатор seqtbl `play_seq` + frame_table (`seg006`), окна `Char` (loadkid/loadshad), атласы Кида. |
|
| `pop_kid.c/.h` | Анимация/движение: интерпретатор seqtbl `play_seq` + frame_table (`seg006`), окна `Char` (loadkid/loadshad), атласы Кида. |
|
||||||
| `pop_cdraw.c/.h` | ОТРИСОВКА персонажей — одна на всех Char (порт `add_kid_to_objtable`/`add_guard_to_objtable`): спрайт, clip_char, брызги, клинок, heal; полоса HP и палитры соперника. Там же ПРОПУСК неизменившегося кадра (`pop_char_skip_mask`). |
|
| `pop_cdraw.c/.h` | ОТРИСОВКА персонажей — одна на всех Char (порт `add_kid_to_objtable`/`add_guard_to_objtable`): спрайт, clip_char, брызги, клинок, heal; полоса HP и палитры соперника. Там же ПРОПУСК неизменившегося кадра (`pop_char_skip_mask`). |
|
||||||
| `pop_ctrl.c/.h` | Управление: диспетчер `control()` (`seg005`) + ввод `read_user_control` (`seg006`) поверх `<kbd_raw.h>`. |
|
| `pop_ctrl.c/.h` | Управление: диспетчер `control()` (`seg005`) + ввод `read_user_control` (`seg006`) поверх `<kbd_raw.h>`. |
|
||||||
|
| `pop_sfx.c` + `pop_sfx_cold.c` | Звук: насос CBL в резиденте + холодная загрузка набора. Раскладка эффектов НЕ компилируется в EXE — читается с диска (`SND/snd.idx`, формат `PSI1`); см. `docs/sound_plan.md` §10. |
|
||||||
|
| `pop_snd_tbl.h` + `pop_snd_data.c` | Тип записи набора и инварианты (рукописный заголовок) + резидентные `pop_snd_tbl`/`pop_snd_page`/`pop_snd_pages`. Размер записи 5 байт — часть дискового контракта, проверяется статически. |
|
||||||
| `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). |
|
| `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). |
|
||||||
|
|
||||||
## Банки: две мины, которых компилятор не видит
|
## Банки: две мины, которых компилятор не видит
|
||||||
|
|||||||
+105
-13
@@ -100,6 +100,7 @@ BANK_FLAGS := $(foreach n,$(BANK_NUMS),\
|
|||||||
|
|
||||||
# Модули резидента (кроме sprpop.c — его подставляет app.mk).
|
# Модули резидента (кроме sprpop.c — его подставляет app.mk).
|
||||||
EXTRA_SRCS := $(addprefix $(SRC_DIR)/,pop_vflip.c pop_pace.c pop_sfx.c pop_state.c \
|
EXTRA_SRCS := $(addprefix $(SRC_DIR)/,pop_vflip.c pop_pace.c pop_sfx.c pop_state.c \
|
||||||
|
pop_snd_data.c \
|
||||||
pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c \
|
pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c \
|
||||||
pop_qsave_io.c)
|
pop_qsave_io.c)
|
||||||
|
|
||||||
@@ -166,7 +167,10 @@ SHADOW_ATL := $(foreach n,$(KID_N),$(ATL)/shadow/sk$(n).atl) \
|
|||||||
# Звук: оригинальная оцифровка на частоте CBL (10 937,5 Гц, 8 бит моно).
|
# Звук: оригинальная оцифровка на частоте CBL (10 937,5 Гц, 8 бит моно).
|
||||||
# Число файлов задаёт упаковщик (POP_SND_PAGES в gen/pop_sound_tbl.h) —
|
# Число файлов задаёт упаковщик (POP_SND_PAGES в gen/pop_sound_tbl.h) —
|
||||||
# список ОБЯЗАН совпадать: недостающий файл это молча пропавшие эффекты.
|
# список ОБЯЗАН совпадать: недостающий файл это молча пропавшие эффекты.
|
||||||
SND_ATL := $(foreach n,0 1 2 3 4 5 6 7 8 9,$(ATL)/sound/s$(n).bin)
|
# Страниц звука СТАТИЧЕСКИ НЕ ПОСЧИТАТЬ: их число зависит от источника
|
||||||
|
# (SDLPoP даёт 9, MSDOS — 10, см. SND_SRC). Поэтому списка s0..sN здесь нет,
|
||||||
|
# а архив зависит от stamp'а упаковщика и берёт страницы из каталога уже
|
||||||
|
# после того, как тот отработал (см. правило SND/snd.arc ниже).
|
||||||
TITLE_ATL := $(foreach s,0 1 2 3,$(foreach p,0 1 2 3 4,$(ATL)/title/t$(s)_$(p).atl))
|
TITLE_ATL := $(foreach s,0 1 2 3,$(foreach p,0 1 2 3 4,$(ATL)/title/t$(s)_$(p).atl))
|
||||||
# PV/INTRO: story и стадии режутся на полосы; у настоящего PV intro есть ещё
|
# PV/INTRO: story и стадии режутся на полосы; у настоящего PV intro есть ещё
|
||||||
# чистый фон b0_* и шесть <=16K actor-атласов — они остаются в EMM на время
|
# чистый фон b0_* и шесть <=16K actor-атласов — они остаются в EMM на время
|
||||||
@@ -189,6 +193,15 @@ MUS_IDS := 24 25 26 27 28 29 30 32 33 35 36 37 39 40 41 43 50 52 53 54 55 56
|
|||||||
# Формат входит в имя stamp'а, поэтому смена MUSIC_FMT сама по себе делает
|
# Формат входит в имя stamp'а, поэтому смена MUSIC_FMT сама по себе делает
|
||||||
# музыку устаревшей — принудительная пересборка не нужна.
|
# музыку устаревшей — принудительная пересборка не нужна.
|
||||||
MUSIC_FMT ?= flac
|
MUSIC_FMT ?= flac
|
||||||
|
# Источник оцифрованных ЭФФЕКТОВ (digisnd1..3): sdlpop (умолчание) или msdos.
|
||||||
|
# Умолчание — SDLPoP, потому что он выкачивается автоматически (`make fetch`)
|
||||||
|
# и сборка обязана работать вообще без оригинального дистрибутива DOS.
|
||||||
|
# Наборы НЕ одинаковы: в MSDOS оцифровка полнее (в SDLPoP `spiked` (48)
|
||||||
|
# фактически пустой, `sword_vs_sword` (10) короче — разбор в
|
||||||
|
# docs/sound_plan.md). У кого дистрибутив есть — `make SND_SRC=msdos`.
|
||||||
|
# Источник входит в имя stamp'а, поэтому смена SND_SRC сама делает звук
|
||||||
|
# устаревшим, как и смена MUSIC_FMT.
|
||||||
|
SND_SRC ?= sdlpop
|
||||||
# res2000 — attract-demo; res2001..res2014 — штатная игра. res2015 (copy
|
# res2000 — attract-demo; res2001..res2014 — штатная игра. res2015 (copy
|
||||||
# protection) намеренно не пакуется и не существует в маршруте загрузчика.
|
# protection) намеренно не пакуется и не существует в маршруте загрузчика.
|
||||||
LVL_NUMS := 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14
|
LVL_NUMS := 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14
|
||||||
@@ -209,7 +222,7 @@ DISK := README_E.TXT README_R.TXT \
|
|||||||
BG/bg.arc BG/pop_tile.pal BG/pal_tile.pal \
|
BG/bg.arc BG/pop_tile.pal BG/pal_tile.pal \
|
||||||
KID/kid.arc KID/kid.pal KID/kid.ani \
|
KID/kid.arc KID/kid.pal KID/kid.ani \
|
||||||
GUARD/guard.arc SKEL/skel.arc VIZIER/vizier.arc SHADOW/shadow.arc \
|
GUARD/guard.arc SKEL/skel.arc VIZIER/vizier.arc SHADOW/shadow.arc \
|
||||||
SND/snd.arc \
|
SND/snd.arc SND/snd.idx \
|
||||||
$(foreach i,$(MUS_IDS),MUS/m$(i).bin) \
|
$(foreach i,$(MUS_IDS),MUS/m$(i).bin) \
|
||||||
FONT/font.atl \
|
FONT/font.atl \
|
||||||
TITLE/title.arc TITLE/title.pal \
|
TITLE/title.arc TITLE/title.pal \
|
||||||
@@ -220,8 +233,11 @@ DISK := README_E.TXT README_R.TXT \
|
|||||||
# список — это и зависимость exe, и то, что умеет чинить себя само.
|
# список — это и зависимость exe, и то, что умеет чинить себя само.
|
||||||
GEN_ARC_H := bg_arc.h kid_arc.h guard_arc.h skel_arc.h vizier_arc.h \
|
GEN_ARC_H := bg_arc.h kid_arc.h guard_arc.h skel_arc.h vizier_arc.h \
|
||||||
shadow_arc.h title_arc.h pv_arc.h
|
shadow_arc.h title_arc.h pv_arc.h
|
||||||
|
# pop_sound_tbl.h здесь БОЛЬШЕ НЕТ: раскладка эффектов уехала на диск
|
||||||
|
# (SND/snd.idx), и EXE от звукового набора не зависит — пару
|
||||||
|
# snd.idx + snd.arc можно менять без перекомпиляции.
|
||||||
GEN_H := $(addprefix gen/,$(GEN_ARC_H) pop_bg_atlas.h kid_data.h \
|
GEN_H := $(addprefix gen/,$(GEN_ARC_H) pop_bg_atlas.h kid_data.h \
|
||||||
pop_guard_pal.h pop_shadow_atlas.h pop_sound_tbl.h \
|
pop_guard_pal.h pop_shadow_atlas.h \
|
||||||
pop_music_tbl.h pop_music_ticks.h pop_font.h)
|
pop_music_tbl.h pop_music_ticks.h pop_font.h)
|
||||||
|
|
||||||
PACKED_FILES := $(addprefix $(PACKED)/,$(DISK))
|
PACKED_FILES := $(addprefix $(PACKED)/,$(DISK))
|
||||||
@@ -343,6 +359,28 @@ $(STAMP)/$(1): $(2) | $(STAMP) check-orig
|
|||||||
@touch $$@
|
@touch $$@
|
||||||
endef
|
endef
|
||||||
|
|
||||||
|
# ВАРИАНТЫ РЕСУРСА (источник звука SND_SRC, набор записей MUSIC_FMT). Оба
|
||||||
|
# варианта пишут в ОДИН выход — build/atl/sound, build/atl/music, — поэтому
|
||||||
|
# одного stamp'а на вариант мало: вернувшись к прежнему варианту, make видит
|
||||||
|
# его stamp свежим и не делает НИЧЕГО, а в выходе лежит чужое (поймано на
|
||||||
|
# sdlpop -> msdos -> sdlpop: заголовок остался с 10 страницами вместо 9).
|
||||||
|
# Лечится файлом с именем текущего варианта: он переписывается только при
|
||||||
|
# смене (cmp), а его свежесть делает stamp устаревшим.
|
||||||
|
.PHONY: variant-check
|
||||||
|
variant-check: ;
|
||||||
|
# СМЕНА ВАРИАНТА СНОСИТ STAMP'Ы СВОЕГО СЕМЕЙСТВА, а не полагается на то,
|
||||||
|
# что make заметит свежесть variant-файла. Причина — секундная
|
||||||
|
# гранулярность mtime: упаковка укладывается в одну секунду с записью
|
||||||
|
# variant, и stamp того же имени считается актуальным. Возврат к прежнему
|
||||||
|
# варианту (`sdlpop -> msdos -> sdlpop`) тогда ТИХО оставлял чужой набор.
|
||||||
|
# Нет файла — правило обязано выполниться, и время тут ни при чём.
|
||||||
|
define variant_rule
|
||||||
|
$(STAMP)/$(1).variant: variant-check | $(STAMP)
|
||||||
|
@echo "$(2)" | cmp -s - $$@ || { echo "$(2)" > $$@; rm -f $(STAMP)/$(1)-*; }
|
||||||
|
endef
|
||||||
|
$(eval $(call variant_rule,sound,$(SND_SRC)))
|
||||||
|
$(eval $(call variant_rule,music,$(MUSIC_FMT)))
|
||||||
|
|
||||||
$(eval $(call stamp_rule,bg,$(TOOLS)/pop_pack_bg.py $(TOOLS)/render_room.py $(TOOLS)/paths.py,pop_pack_bg.py dungeon && $(PY) pop_pack_bg.py palace))
|
$(eval $(call stamp_rule,bg,$(TOOLS)/pop_pack_bg.py $(TOOLS)/render_room.py $(TOOLS)/paths.py,pop_pack_bg.py dungeon && $(PY) pop_pack_bg.py palace))
|
||||||
$(eval $(call stamp_rule,kid,$(TOOLS)/pop_pack_kid.py $(TOOLS)/pop_pack_bg.py $(TOOLS)/paths.py,pop_pack_kid.py))
|
$(eval $(call stamp_rule,kid,$(TOOLS)/pop_pack_kid.py $(TOOLS)/pop_pack_bg.py $(TOOLS)/paths.py,pop_pack_kid.py))
|
||||||
$(eval $(call stamp_rule,kid-bin,$(TOOLS)/pop_extract_kid_data.py $(TOOLS)/paths.py,pop_extract_kid_data.py))
|
$(eval $(call stamp_rule,kid-bin,$(TOOLS)/pop_extract_kid_data.py $(TOOLS)/paths.py,pop_extract_kid_data.py))
|
||||||
@@ -350,8 +388,25 @@ $(eval $(call stamp_rule,guard,$(TOOLS)/pop_pack_guard.py $(TOOLS)/paths.py,pop_
|
|||||||
$(eval $(call stamp_rule,skel,$(TOOLS)/pop_pack_guard.py $(TOOLS)/paths.py,pop_pack_guard.py SKEL))
|
$(eval $(call stamp_rule,skel,$(TOOLS)/pop_pack_guard.py $(TOOLS)/paths.py,pop_pack_guard.py SKEL))
|
||||||
$(eval $(call stamp_rule,vizier,$(TOOLS)/pop_pack_guard.py $(TOOLS)/paths.py,pop_pack_guard.py VIZIER))
|
$(eval $(call stamp_rule,vizier,$(TOOLS)/pop_pack_guard.py $(TOOLS)/paths.py,pop_pack_guard.py VIZIER))
|
||||||
$(eval $(call stamp_rule,shadow,$(TOOLS)/pop_pack_shadow.py $(TOOLS)/pop_pack_bg.py $(TOOLS)/pop_pack_kid.py $(TOOLS)/paths.py,pop_pack_shadow.py))
|
$(eval $(call stamp_rule,shadow,$(TOOLS)/pop_pack_shadow.py $(TOOLS)/pop_pack_bg.py $(TOOLS)/pop_pack_kid.py $(TOOLS)/paths.py,pop_pack_shadow.py))
|
||||||
$(eval $(call stamp_rule,sound,$(TOOLS)/pop_pack_sound.py $(TOOLS)/paths.py,pop_pack_sound.py))
|
# ЗВУК — ОСОБЫЙ СЛУЧАЙ: упаковка, сборка архива и копия индекса делаются
|
||||||
$(eval $(call stamp_rule,music-$(MUSIC_FMT),$(TOOLS)/pop_pack_music.py $(TOOLS)/paths.py,pop_pack_music.py --format $(MUSIC_FMT) $(MUS_IDS)))
|
# ОДНИМ рецептом, а не тремя правилами через таймстемпы.
|
||||||
|
#
|
||||||
|
# Почему. Упаковщик и копирование укладываются в одну секунду, а make
|
||||||
|
# сравнивает mtime с секундной гранулярностью: цель, сделанная в ту же
|
||||||
|
# секунду, что и её зависимость, считается актуальной. При смене SND_SRC
|
||||||
|
# это давало ТИХО НЕВЕРНЫЙ результат — `sdlpop -> msdos -> sdlpop`
|
||||||
|
# перепаковывал build/atl, но в assets/packed оставался прежний набор
|
||||||
|
# (10 страниц вместо 9; поймано 2026-08-31, с паузой в секунду не
|
||||||
|
# воспроизводится). Внутри одного рецепта сравнивать нечего.
|
||||||
|
$(STAMP)/sound-$(SND_SRC): $(TOOLS)/pop_pack_sound.py $(TOOLS)/pop_idx.py \
|
||||||
|
$(TOOLS)/paths.py $(TOOLS)/pop_pack_arc.py \
|
||||||
|
$(STAMP)/sound.variant | $(STAMP) check-orig
|
||||||
|
cd $(TOOLS) && $(PY) pop_pack_sound.py --source $(SND_SRC)
|
||||||
|
@mkdir -p $(PACKED)/SND
|
||||||
|
$(ARC) $(PACKED)/SND/snd.arc $$(ls $(ATL)/sound/s*.bin | sort -V)
|
||||||
|
@cp $(ATL)/sound/snd.idx $(PACKED)/SND/snd.idx
|
||||||
|
@touch $@
|
||||||
|
$(eval $(call stamp_rule,music-$(MUSIC_FMT),$(TOOLS)/pop_pack_music.py $(TOOLS)/paths.py $(STAMP)/music.variant,pop_pack_music.py --format $(MUSIC_FMT) $(MUS_IDS)))
|
||||||
$(eval $(call stamp_rule,font,$(TOOLS)/pop_extract_font.py $(TOOLS)/paths.py,pop_extract_font.py))
|
$(eval $(call stamp_rule,font,$(TOOLS)/pop_extract_font.py $(TOOLS)/paths.py,pop_extract_font.py))
|
||||||
$(eval $(call stamp_rule,title,$(TOOLS)/pop_pack_title.py $(TOOLS)/paths.py,pop_pack_title.py))
|
$(eval $(call stamp_rule,title,$(TOOLS)/pop_pack_title.py $(TOOLS)/paths.py,pop_pack_title.py))
|
||||||
$(eval $(call stamp_rule,pv,$(TOOLS)/pop_pack_intro.py $(TOOLS)/paths.py,pop_pack_intro.py))
|
$(eval $(call stamp_rule,pv,$(TOOLS)/pop_pack_intro.py $(TOOLS)/paths.py,pop_pack_intro.py))
|
||||||
@@ -371,7 +426,6 @@ $(eval $(call from_stamp,$(GUARD_ATL) gen/pop_guard_pal.h,guard))
|
|||||||
$(eval $(call from_stamp,$(SKEL_ATL),skel))
|
$(eval $(call from_stamp,$(SKEL_ATL),skel))
|
||||||
$(eval $(call from_stamp,$(VIZIER_ATL),vizier))
|
$(eval $(call from_stamp,$(VIZIER_ATL),vizier))
|
||||||
$(eval $(call from_stamp,$(SHADOW_ATL) gen/pop_shadow_atlas.h,shadow))
|
$(eval $(call from_stamp,$(SHADOW_ATL) gen/pop_shadow_atlas.h,shadow))
|
||||||
$(eval $(call from_stamp,$(SND_ATL) gen/pop_sound_tbl.h,sound))
|
|
||||||
$(eval $(call from_stamp,$(foreach i,$(MUS_IDS),$(ATL)/music/m$(i).bin) gen/pop_music_tbl.h gen/pop_music_ticks.h,music-$(MUSIC_FMT)))
|
$(eval $(call from_stamp,$(foreach i,$(MUS_IDS),$(ATL)/music/m$(i).bin) gen/pop_music_tbl.h gen/pop_music_ticks.h,music-$(MUSIC_FMT)))
|
||||||
$(eval $(call from_stamp,$(ATL)/font/font.atl gen/pop_font.h,font))
|
$(eval $(call from_stamp,$(ATL)/font/font.atl gen/pop_font.h,font))
|
||||||
$(eval $(call from_stamp,$(TITLE_ATL) $(ATL)/title/title.pal,title))
|
$(eval $(call from_stamp,$(TITLE_ATL) $(ATL)/title/title.pal,title))
|
||||||
@@ -396,9 +450,15 @@ $(eval $(call arc_rule,TITLE,title,title,$(TITLE_ATL),TITLE))
|
|||||||
$(eval $(call arc_rule,PV,pv,pv,$(PV_ATL),PV))
|
$(eval $(call arc_rule,PV,pv,pv,$(PV_ATL),PV))
|
||||||
# У звука своего *_arc.h нет: содержимое адресуется таблицей pop_sound_tbl.h,
|
# У звука своего *_arc.h нет: содержимое адресуется таблицей pop_sound_tbl.h,
|
||||||
# которую печатает сам упаковщик звука.
|
# которую печатает сам упаковщик звука.
|
||||||
$(PACKED)/SND/snd.arc: $(SND_ATL) $(TOOLS)/pop_pack_arc.py
|
# Зависимость на stamp, а не на список страниц: сколько их будет, известно
|
||||||
@mkdir -p $(dir $@)
|
# только после запуска упаковщика (источник задаёт число). Порядок страниц
|
||||||
$(ARC) $@ $(SND_ATL)
|
# в архиве обязан быть ЧИСЛОВЫМ — pop_snd_tbl адресует их индексом, а голый
|
||||||
|
# ls даёт s10 перед s2. sort -V, а не `-t s -k n`: буква s есть и в пути.
|
||||||
|
# Пара snd.arc + snd.idx согласована по построению: её делает ОДИН рецепт
|
||||||
|
# stamp'а выше (см. там же, почему не раздельными правилами). Здесь
|
||||||
|
# только восстановление, если файл снесли руками.
|
||||||
|
$(PACKED)/SND/snd.arc $(PACKED)/SND/snd.idx: $(STAMP)/sound-$(SND_SRC)
|
||||||
|
@test -f $@ || { $(MAKE) GENERATE=1 -B $(STAMP)/sound-$(SND_SRC); test -f $@; }
|
||||||
|
|
||||||
# Файлы, которые ложатся на диск как есть, — простое копирование из staging.
|
# Файлы, которые ложатся на диск как есть, — простое копирование из staging.
|
||||||
define copy_rule
|
define copy_rule
|
||||||
@@ -419,6 +479,7 @@ $(foreach i,$(MUS_IDS),$(eval $(call copy_rule,MUS/m$(i).bin,$(ATL)/music/m$(i).
|
|||||||
# читает как есть, упаковщика для них нет. check-orig здесь тоже order-only —
|
# читает как есть, упаковщика для них нет. check-orig здесь тоже order-only —
|
||||||
# без оригиналов взять уровни неоткуда, и сказать об этом надо внятно.
|
# без оригиналов взять уровни неоткуда, и сказать об этом надо внятно.
|
||||||
LVL_ORIG := assets/orig/SDLPoP/data/LEVELS
|
LVL_ORIG := assets/orig/SDLPoP/data/LEVELS
|
||||||
|
SND_ORIG_MSDOS := assets/orig/MSDOS
|
||||||
define lvl_rule
|
define lvl_rule
|
||||||
$(PACKED)/LEVELS/res20$(1).bin: $(LVL_ORIG)/res20$(1).bin | check-orig
|
$(PACKED)/LEVELS/res20$(1).bin: $(LVL_ORIG)/res20$(1).bin | check-orig
|
||||||
@mkdir -p $$(dir $$@)
|
@mkdir -p $$(dir $$@)
|
||||||
@@ -447,7 +508,7 @@ resources-bg: $(PACKED)/BG/bg.arc $(PACKED)/BG/pop_tile.pal $(PACKED)/BG/pal
|
|||||||
resources-kid: $(PACKED)/KID/kid.arc $(PACKED)/KID/kid.pal $(PACKED)/KID/kid.ani
|
resources-kid: $(PACKED)/KID/kid.arc $(PACKED)/KID/kid.pal $(PACKED)/KID/kid.ani
|
||||||
resources-actors: $(PACKED)/GUARD/guard.arc $(PACKED)/SKEL/skel.arc \
|
resources-actors: $(PACKED)/GUARD/guard.arc $(PACKED)/SKEL/skel.arc \
|
||||||
$(PACKED)/VIZIER/vizier.arc $(PACKED)/SHADOW/shadow.arc
|
$(PACKED)/VIZIER/vizier.arc $(PACKED)/SHADOW/shadow.arc
|
||||||
resources-sound: $(PACKED)/SND/snd.arc
|
resources-sound: $(PACKED)/SND/snd.arc $(PACKED)/SND/snd.idx
|
||||||
resources-music: $(foreach i,$(MUS_IDS),$(PACKED)/MUS/m$(i).bin)
|
resources-music: $(foreach i,$(MUS_IDS),$(PACKED)/MUS/m$(i).bin)
|
||||||
resources-font: $(PACKED)/FONT/font.atl
|
resources-font: $(PACKED)/FONT/font.atl
|
||||||
resources-title: $(PACKED)/TITLE/title.arc $(PACKED)/TITLE/title.pal
|
resources-title: $(PACKED)/TITLE/title.arc $(PACKED)/TITLE/title.pal
|
||||||
@@ -459,8 +520,12 @@ resources: resources-bg resources-kid resources-actors resources-sound \
|
|||||||
|
|
||||||
check-orig:
|
check-orig:
|
||||||
@test -d $(LVL_ORIG) || { \
|
@test -d $(LVL_ORIG) || { \
|
||||||
echo "SprPoP: нет assets/orig/ — генерировать ресурсы не из чего." >&2; \
|
echo "SprPoP: нет assets/orig/SDLPoP — генерировать ресурсы не из чего." >&2; \
|
||||||
echo " Что и откуда положить, написано в assets/orig/README.md" >&2; false; }
|
echo " Скачать: make fetch (подробности — assets/orig/README.md)" >&2; false; }
|
||||||
|
@test "$(SND_SRC)" != msdos || test -d $(SND_ORIG_MSDOS) || { \
|
||||||
|
echo "SprPoP: SND_SRC=msdos, но нет $(SND_ORIG_MSDOS)." >&2; \
|
||||||
|
echo " Дистрибутив DOS не качается — положить руками, либо собирать" >&2; \
|
||||||
|
echo " со звуком из SDLPoP: make SND_SRC=sdlpop" >&2; false; }
|
||||||
|
|
||||||
else
|
else
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
@@ -510,6 +575,33 @@ $(MUSIC_TARGETS):
|
|||||||
@$(MAKE) MUSIC_FMT=$(patsubst music-%,%,$@) resources-music
|
@$(MAKE) MUSIC_FMT=$(patsubst music-%,%,$@) resources-music
|
||||||
@$(MAKE) MUSIC_FMT=$(patsubst music-%,%,$@) stage
|
@$(MAKE) MUSIC_FMT=$(patsubst music-%,%,$@) stage
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Внешние исходные данные (assets/orig/)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# В репозитории чужих данных нет — есть tools/fetch_orig.py, который знает,
|
||||||
|
# откуда их взять, и таблица источников внутри него. Качаем только то,
|
||||||
|
# чего нет; `FETCH_ARGS=--force` перекачивает заново, `FETCH_ARGS=--ref v1.23`
|
||||||
|
# берёт конкретный тег SDLPoP.
|
||||||
|
# make fetch SDLPoP + записи музыки
|
||||||
|
# make fetch-sdlpop только SDLPoP (data/ + src/-эталон)
|
||||||
|
# make fetch-music только записи саундтрека (43 МБ)
|
||||||
|
# make fetch-check что уже на месте
|
||||||
|
# make fetch-list таблица источников
|
||||||
|
# MSDOS/ не качается — это копирайтный дистрибутив; он НЕОБЯЗАТЕЛЕН, всё
|
||||||
|
# нужное (уровни, оцифровка) есть в SDLPoP. См. SND_SRC выше.
|
||||||
|
FETCH := $(PY) $(TOOLS)/fetch_orig.py
|
||||||
|
fetch: ; $(FETCH) all $(FETCH_ARGS)
|
||||||
|
fetch-sdlpop: ; $(FETCH) sdlpop $(FETCH_ARGS)
|
||||||
|
fetch-music: ; $(FETCH) music $(FETCH_ARGS)
|
||||||
|
fetch-check: ; @$(FETCH) --check
|
||||||
|
fetch-list: ; @$(FETCH) --list
|
||||||
|
FETCH_TARGETS := fetch fetch-sdlpop fetch-music fetch-check fetch-list
|
||||||
|
|
||||||
|
# Тесты инструментов (хост, питон): форматы, которые читает Z80. Секунды,
|
||||||
|
# ни эмулятора, ни оригиналов не нужно.
|
||||||
|
test-tools:
|
||||||
|
@cd $(CURDIR) && $(PY) -m unittest discover -s tools/tests
|
||||||
|
|
||||||
# Перелинковка приложения без пересборки библиотек и ресурсов.
|
# Перелинковка приложения без пересборки библиотек и ресурсов.
|
||||||
relink:
|
relink:
|
||||||
$(MAKE) -W Makefile $(EXE)
|
$(MAKE) -W Makefile $(EXE)
|
||||||
@@ -529,4 +621,4 @@ distclean: clean
|
|||||||
.PHONY: stage check-orig resources resources-rebuild resources-bg resources-kid \
|
.PHONY: stage check-orig resources resources-rebuild resources-bg resources-kid \
|
||||||
resources-actors resources-sound resources-music resources-font \
|
resources-actors resources-sound resources-music resources-font \
|
||||||
resources-title resources-pv resources-levels $(MUSIC_TARGETS) \
|
resources-title resources-pv resources-levels $(MUSIC_TARGETS) \
|
||||||
relink distclean mame-link
|
$(FETCH_TARGETS) test-tools relink distclean mame-link
|
||||||
|
|||||||
@@ -13,12 +13,19 @@ make # build/sprpop.exe + ресурсы
|
|||||||
make hdd # + образ build/hdd/sprpop.chd
|
make hdd # + образ build/hdd/sprpop.chd
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Чужих данных в репозитории нет — есть адреса, откуда их взять. Чтобы
|
||||||
|
перегенерировать ресурсы (или получить эталон механики), сначала:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make fetch # SDLPoP + записи саундтрека DOS-версии в assets/orig/
|
||||||
|
```
|
||||||
|
|
||||||
## Что где
|
## Что где
|
||||||
|
|
||||||
```
|
```
|
||||||
src/ рукописный C: главный цикл sprpop.c + движок pop_*.c
|
src/ рукописный C: главный цикл sprpop.c + движок pop_*.c
|
||||||
gen/ генерируемые заголовки — руками не править
|
gen/ генерируемые заголовки — руками не править
|
||||||
assets/orig/ оригинальные данные игры (вне репозитория, см. там README)
|
assets/orig/ оригинальные данные игры (вне репозитория, качает `make fetch`)
|
||||||
assets/packed/ то, что ложится на диск игры, в раскладке диска
|
assets/packed/ то, что ложится на диск игры, в раскладке диска
|
||||||
tools/ конверторы ресурсов; все пути — в tools/paths.py
|
tools/ конверторы ресурсов; все пути — в tools/paths.py
|
||||||
docs/ планы, доски задач и багов, справочники (начинать с docs/README.md)
|
docs/ планы, доски задач и багов, справочники (начинать с docs/README.md)
|
||||||
@@ -33,9 +40,12 @@ build/ выход: sprpop.exe, каталоги ресурсов, hdd/,
|
|||||||
| `make` | собрать `build/sprpop.exe` и разложить ресурсы в `build/` |
|
| `make` | собрать `build/sprpop.exe` и разложить ресурсы в `build/` |
|
||||||
| `make hdd` | + образ жёсткого диска `build/hdd/sprpop.chd` |
|
| `make hdd` | + образ жёсткого диска `build/hdd/sprpop.chd` |
|
||||||
| `make mame-link` | однократно подставить образ в MAME (символьная ссылка на `IMG/test_hdd.chd`) |
|
| `make mame-link` | однократно подставить образ в MAME (символьная ссылка на `IMG/test_hdd.chd`) |
|
||||||
|
| `make fetch` | скачать внешние исходные данные в `assets/orig/`: `fetch-sdlpop`, `fetch-music`, `fetch-check`, `fetch-list` |
|
||||||
| `make resources` | перегенерировать ресурсы из `assets/orig/` |
|
| `make resources` | перегенерировать ресурсы из `assets/orig/` |
|
||||||
| `make music-mp3` | музыка из другого набора записей: `music-flac` (умолчание), `music-mp3`, `music-ogg`, `music-mt32` |
|
| `make music-mp3` | музыка из другого набора записей: `music-flac` (умолчание), `music-mp3`, `music-ogg`, `music-mt32` |
|
||||||
|
| `make SND_SRC=msdos` | оцифровка эффектов из дистрибутива DOS вместо SDLPoP (нужен свой `assets/orig/MSDOS/`) |
|
||||||
| `make -C tests/host` | модульные тесты движка (секунды, без эмулятора) |
|
| `make -C tests/host` | модульные тесты движка (секунды, без эмулятора) |
|
||||||
|
| `make test-tools` | тесты упаковщиков на хосте: форматы файлов, которые читает Z80 |
|
||||||
| `make clean` | снести `build/` (ассеты не трогает) |
|
| `make clean` | снести `build/` (ассеты не трогает) |
|
||||||
| `make distclean` | `clean` + снести упакованные ассеты `assets/packed/` |
|
| `make distclean` | `clean` + снести упакованные ассеты `assets/packed/` |
|
||||||
|
|
||||||
@@ -54,7 +64,14 @@ build/ выход: sprpop.exe, каталоги ресурсов, hdd/,
|
|||||||
|
|
||||||
`assets/orig/` нужен только чтобы ресурсы **перегенерировать** (`make
|
`assets/orig/` нужен только чтобы ресурсы **перегенерировать** (`make
|
||||||
resources`) и как **эталон механики**: любая правка поведения сверяется с
|
resources`) и как **эталон механики**: любая правка поведения сверяется с
|
||||||
`assets/orig/SDLPoP/src/` до кодинга, см. `CLAUDE.md`.
|
`assets/orig/SDLPoP/src/` до кодинга, см. `CLAUDE.md`. Заполняется он одной
|
||||||
|
командой `make fetch` — качается SDLPoP (ресурсы + исходники-эталон) и записи
|
||||||
|
саундтрека; адрес архива музыки скрипт берёт из самого SDLPoP.
|
||||||
|
|
||||||
|
Оригинальный дистрибутив DOS (`assets/orig/MSDOS/`) **не нужен**: и уровни, и
|
||||||
|
оцифровка звука берутся из SDLPoP. Он даёт ровно одно — более полный набор
|
||||||
|
эффектов (в SDLPoP пуст звук `spiked`, 48); у кого дистрибутив есть, включает
|
||||||
|
его явно: `make SND_SRC=msdos`. Подробности — `assets/orig/README.md`.
|
||||||
|
|
||||||
## Происхождение
|
## Происхождение
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -13,6 +13,7 @@
|
|||||||
| [`impl_diff.md`](impl_diff.md) | **Осознанные расхождения с SDLPoP**: где сделано не дословно и почему. Новое расхождение — записью сюда, а не только комментарием в коде |
|
| [`impl_diff.md`](impl_diff.md) | **Осознанные расхождения с SDLPoP**: где сделано не дословно и почему. Новое расхождение — записью сюда, а не только комментарием в коде |
|
||||||
| [`keys.txt`](keys.txt) | **Целевая раскладка управления**, к которой подгоняем SprPoP |
|
| [`keys.txt`](keys.txt) | **Целевая раскладка управления**, к которой подгоняем SprPoP |
|
||||||
| [`keys_plan.md`](keys_plan.md) | План перевода на неё: что совпадает, что переназначить, каких функций нет вовсе |
|
| [`keys_plan.md`](keys_plan.md) | План перевода на неё: что совпадает, что переназначить, каких функций нет вовсе |
|
||||||
|
| [`../assets/orig/README.md`](../assets/orig/README.md) | **Откуда берутся исходные данные**: `make fetch` (SDLPoP + музыка), зачем нужен и почему необязателен `MSDOS/`, `SND_SRC` |
|
||||||
|
|
||||||
## Производительность
|
## Производительность
|
||||||
|
|
||||||
|
|||||||
@@ -133,3 +133,51 @@ raw-клавиатура (`<kbd_raw.h>`), которую мы и так заби
|
|||||||
и трогать его ради экономии секунды стоит только на спокойную голову.
|
и трогать его ради экономии секунды стоит только на спокойную голову.
|
||||||
|
|
||||||
**Выигрыш.** Одна-две секунды один раз на уровень.
|
**Выигрыш.** Одна-две секунды один раз на уровень.
|
||||||
|
|
||||||
|
## Музыка одним постоянно открытым архивом (замер 2026-08-31, НЕ сейчас)
|
||||||
|
|
||||||
|
**Идея.** Сейчас каждый трек — отдельный файл `MUS/mNN.bin`, и
|
||||||
|
`pop_music_load_begin` открывает свой на каждый запуск. Свести треки в
|
||||||
|
один файл и держать его `fd` открытым: вместо `chdir`+`open`+`chdir`
|
||||||
|
останется `lseek` к смещению трека.
|
||||||
|
|
||||||
|
**Почему это стоит внимания — разложение окна старта музыки** (полный
|
||||||
|
замер и метод — `sound_plan.md` §9):
|
||||||
|
|
||||||
|
| шаг | цена | уйдёт? |
|
||||||
|
|---|---:|---|
|
||||||
|
| ожидание `pop_music_service` в кадре | 22,6 мс | нет |
|
||||||
|
| `chdir` #1 | 46,1 мс | **да** |
|
||||||
|
| `open` файла трека | 34,5 мс | **да** |
|
||||||
|
| `chdir` #2 | 59,0 мс | **да** |
|
||||||
|
| чтение первых 8 КБ | 34,0 мс | нет |
|
||||||
|
| **итого** | **196,7 мс** | |
|
||||||
|
|
||||||
|
Уходит **139,6 мс** — но это ВЕРХНЯЯ граница: появляется `lseek` к
|
||||||
|
смещению трека, которого сейчас нет вовсе (файл читается последовательно
|
||||||
|
с нуля), и его цена НЕ ИЗМЕРЕНА. Реальный выигрыш = 139,6 минус `lseek`;
|
||||||
|
померить можно тем же способом, `lseek` уже используется в
|
||||||
|
`pop_arc.c:89`. Остаётся ≈ 57 мс, то есть старт трека ускоряется
|
||||||
|
примерно вчетверо.
|
||||||
|
|
||||||
|
**Где заметно.** Реплики PV-сцены идут встык (три трека подряд), плюс
|
||||||
|
каждый игровой джингл — смерть, зелье, подобранный меч.
|
||||||
|
|
||||||
|
**Чем осложнено.**
|
||||||
|
|
||||||
|
1. **`PBA1` под это не годится.** Размер элемента там `uint16` (≤ 64 КБ),
|
||||||
|
а трек — до 1,2 МБ (m56, 78 страниц); резать по страницам нельзя,
|
||||||
|
элементов вышло бы ~240 при `POP_ARC_MAX = 126`. Нужен свой индекс со
|
||||||
|
смещениями — то есть эта задача СМЫКАЕТСЯ с `MUS/MUSIC.IDX` из
|
||||||
|
`music_runtime_index_plan.md`: одно изменение раскладки, а не два.
|
||||||
|
2. **Постоянно занятый файловый манипулятор** — один из восьми (memory
|
||||||
|
`dss_fd_limit`, девятый `open` вешает DSS). Рядом свои открывают
|
||||||
|
`POP.CFG`, quicksave и загрузка уровня.
|
||||||
|
3. **`chdir` убрать нельзя** — он и есть половина выигрыша, но нужен
|
||||||
|
старым DSS (`POP_PATH_CALL`); экономия берётся не его удалением, а тем,
|
||||||
|
что открытие вообще перестаёт выполняться на каждый трек.
|
||||||
|
|
||||||
|
**Оговорка.** Задержка старта музыки сама по себе НЕ является дефектом:
|
||||||
|
трек начинается на границе события, на слух это не сбой. У эффектов
|
||||||
|
такой задержки нет вовсе — они целиком в EMM (см. `sound_plan.md` §9.1).
|
||||||
|
Так что это оптимизация, а не исправление.
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
# Аудио без перелинковки: runtime-индексы музыки и эффектов
|
# Аудио без перелинковки: runtime-индексы музыки и эффектов
|
||||||
|
|
||||||
> Статус: план будущего улучшения, 2026-08-30. Код пока не менялся.
|
> Статус: **часть II (эффекты, SI0..SI4) СДЕЛАНА 2026-08-31** — разбор и
|
||||||
|
> результаты в `sound_plan.md` §10. Часть I (музыка, MI0..MI5) — план.
|
||||||
|
>
|
||||||
|
> Что в реализации разошлось с планом ниже: файл назван `SND/snd.idx`
|
||||||
|
> (единый basename с `snd.arc`, оба в нижнем регистре), а при отсутствии
|
||||||
|
> индекса поднимается ПУСТОЙ набор с блоком тишины — иначе встаёт музыка
|
||||||
|
> и игра непроходима (§10.5 sound_plan). Про `ticks60` в `PMI1` решено:
|
||||||
|
> поле в формате оставить даже если сценарии перейдут на ожидание звука —
|
||||||
|
> загрузчик волен не тащить его в память.
|
||||||
|
|
||||||
## 1. Цель
|
## 1. Цель
|
||||||
|
|
||||||
@@ -330,13 +338,13 @@ Makefile должен:
|
|||||||
|
|
||||||
Один и тот же `sprpop.exe` должен работать с разными наборами PCM-эффектов,
|
Один и тот же `sprpop.exe` должен работать с разными наборами PCM-эффектов,
|
||||||
если в них сохранена исходная нумерация sound id 0..56. Замена набора должна
|
если в них сохранена исходная нумерация sound id 0..56. Замена набора должна
|
||||||
сводиться к замене согласованной пары `SND/SOUND.IDX` + `SND/SND.ARC`, без
|
сводиться к замене согласованной пары `SND/snd.idx` + `SND/snd.arc`, без
|
||||||
генерации C-заголовка и перелинковки EXE.
|
генерации C-заголовка и перелинковки EXE.
|
||||||
|
|
||||||
Набор состоит из двух согласованных файлов:
|
Набор состоит из двух согласованных файлов:
|
||||||
|
|
||||||
- `SND/SND.ARC` — обычный архив PBA1 с PCM-страницами;
|
- `SND/snd.arc` — обычный архив PBA1 с PCM-страницами;
|
||||||
- `SND/SOUND.IDX` — описание раскладки эффектов внутри этих страниц.
|
- `SND/snd.idx` — описание раскладки эффектов внутри этих страниц.
|
||||||
|
|
||||||
Отдельный IDX предпочтительнее расширения заголовка `snd.arc`: PBA1 остаётся
|
Отдельный IDX предпочтительнее расширения заголовка `snd.arc`: PBA1 остаётся
|
||||||
универсальным и не получает специального варианта только для звука, а
|
универсальным и не получает специального варианта только для звука, а
|
||||||
@@ -381,7 +389,7 @@ typedef struct {
|
|||||||
У SDLPoP эффект 48 содержит только 7 исходных сэмплов; это свойство самого
|
У SDLPoP эффект 48 содержит только 7 исходных сэмплов; это свойство самого
|
||||||
набора, а не ошибка runtime-индекса.
|
набора, а не ошибка runtime-индекса.
|
||||||
|
|
||||||
## 15. Файл `SND/SOUND.IDX`
|
## 15. Файл `SND/snd.idx`
|
||||||
|
|
||||||
IDX занимает один 512-байтовый сектор и после короткого заголовка является
|
IDX занимает один 512-байтовый сектор и после короткого заголовка является
|
||||||
точным дисковым дампом 57 записей `pop_snd_tbl`:
|
точным дисковым дампом 57 записей `pop_snd_tbl`:
|
||||||
@@ -472,12 +480,12 @@ SDLPoP-набор — 9. Предел 16 оставляет запас буду
|
|||||||
|
|
||||||
`pop_sfx_init()` должен выполнять следующую последовательность:
|
`pop_sfx_init()` должен выполнять следующую последовательность:
|
||||||
|
|
||||||
1. открыть `SND/SOUND.IDX` через существующий путь `POP_PATH_CALL`;
|
1. открыть `SND/snd.idx` через существующий путь `POP_PATH_CALL`;
|
||||||
2. прочитать 16-байтовый заголовок, затем 285 байт записей прямо в
|
2. прочитать 16-байтовый заголовок, затем 285 байт записей прямо в
|
||||||
`pop_snd_tbl`, и проверить полный размер IDX;
|
`pop_snd_tbl`, и проверить полный размер IDX;
|
||||||
3. проверить magic, версию, число/размер записей, формат и размер блока;
|
3. проверить magic, версию, число/размер записей, формат и размер блока;
|
||||||
4. получить из IDX фактическое число страниц и проверить диапазон 1..16;
|
4. получить из IDX фактическое число страниц и проверить диапазон 1..16;
|
||||||
5. открыть `SND/SND.ARC` через `POP_PATH_CALL`;
|
5. открыть `SND/snd.arc` через `POP_PATH_CALL`;
|
||||||
6. прочитать обычную таблицу PBA1 с ёмкостью `POP_SND_MAX_PAGES`;
|
6. прочитать обычную таблицу PBA1 с ёмкостью `POP_SND_MAX_PAGES`;
|
||||||
7. сверить `PSI1.pages` с числом элементов PBA1;
|
7. сверить `PSI1.pages` с числом элементов PBA1;
|
||||||
8. проверить каждую ненулевую запись;
|
8. проверить каждую ненулевую запись;
|
||||||
@@ -493,7 +501,7 @@ SDLPoP-набор — 9. Предел 16 оставляет запас буду
|
|||||||
`pop_snd_pages`.
|
`pop_snd_pages`.
|
||||||
|
|
||||||
При любой ошибке блок EMM и файловый дескриптор освобождаются, таблица не
|
При любой ошибке блок EMM и файловый дескриптор освобождаются, таблица не
|
||||||
используется, эффекты остаются выключенными. Набор без `SOUND.IDX` считается
|
используется, эффекты остаются выключенными. Набор без `snd.idx` считается
|
||||||
неполным; хранить в EXE старую таблицу как fallback не следует, иначе
|
неполным; хранить в EXE старую таблицу как fallback не следует, иначе
|
||||||
зависимость бинарника от конкретного набора останется.
|
зависимость бинарника от конкретного набора останется.
|
||||||
|
|
||||||
@@ -505,9 +513,9 @@ SDLPoP-набор — 9. Предел 16 оставляет запас буду
|
|||||||
начала/длины на 128 байт;
|
начала/длины на 128 байт;
|
||||||
2. вычислить фактическое число страниц и отвергнуть набор больше 16;
|
2. вычислить фактическое число страниц и отвергнуть набор больше 16;
|
||||||
3. сформировать плотные 57 записей `page/off/len`;
|
3. сформировать плотные 57 записей `page/off/len`;
|
||||||
4. записать `SND/SOUND.IDX` в формате `PSI1`, включая фактическое число
|
4. записать `SND/snd.idx` в формате `PSI1`, включая фактическое число
|
||||||
страниц;
|
страниц;
|
||||||
5. собрать обычный `SND/SND.ARC` из полученных страниц;
|
5. собрать обычный `SND/snd.arc` из полученных страниц;
|
||||||
6. больше не генерировать `gen/pop_sound_tbl.h`.
|
6. больше не генерировать `gen/pop_sound_tbl.h`.
|
||||||
|
|
||||||
Чтобы число страниц не было захардкожено списком `s0.bin`..`s9.bin` в
|
Чтобы число страниц не было захардкожено списком `s0.bin`..`s9.bin` в
|
||||||
@@ -517,13 +525,13 @@ Makefile, упаковщик звука предпочтительно долж
|
|||||||
|
|
||||||
Стабильные тип и константы переносятся в обычный internal-заголовок. EXE не
|
Стабильные тип и константы переносятся в обычный internal-заголовок. EXE не
|
||||||
должен зависеть от результата упаковки звука: цель выбора набора меняет
|
должен зависеть от результата упаковки звука: цель выбора набора меняет
|
||||||
согласованную пару `SOUND.IDX` + `SND.ARC`.
|
согласованную пару `snd.idx` + `snd.arc`.
|
||||||
|
|
||||||
## 20. Этапы реализации эффектов
|
## 20. Этапы реализации эффектов
|
||||||
|
|
||||||
### SI0 — формат и host-тест
|
### SI0 — формат и host-тест
|
||||||
|
|
||||||
- writer/reader отдельного `SOUND.IDX` в формате `PSI1`;
|
- writer/reader отдельного `snd.idx` в формате `PSI1`;
|
||||||
- проверка точного пятибайтового LE-формата записи;
|
- проверка точного пятибайтового LE-формата записи;
|
||||||
- сборка и разбор обоих имеющихся DIGISND-наборов;
|
- сборка и разбор обоих имеющихся DIGISND-наборов;
|
||||||
- проверки 10 страниц MSDOS, 9 страниц SDLPoP и отличающихся записей.
|
- проверки 10 страниц MSDOS, 9 страниц SDLPoP и отличающихся записей.
|
||||||
@@ -545,7 +553,7 @@ Makefile, упаковщик звука предпочтительно долж
|
|||||||
### SI3 — сборка без перелинковки
|
### SI3 — сборка без перелинковки
|
||||||
|
|
||||||
- убрать фиксированный `SND_ATL` из десяти имён;
|
- убрать фиксированный `SND_ATL` из десяти имён;
|
||||||
- сделать `SOUND.IDX` и `SND.ARC` согласованными результатами выбора набора;
|
- сделать `snd.idx` и `snd.arc` согласованными результатами выбора набора;
|
||||||
- проверить неизменность SHA/даты `sprpop.exe` при смене набора.
|
- проверить неизменность SHA/даты `sprpop.exe` при смене набора.
|
||||||
|
|
||||||
### SI4 — приёмка
|
### SI4 — приёмка
|
||||||
@@ -561,7 +569,7 @@ Makefile, упаковщик звука предпочтительно долж
|
|||||||
## 21. Критерии готовности эффектов
|
## 21. Критерии готовности эффектов
|
||||||
|
|
||||||
- один `sprpop.exe` работает с MSDOS- и SDLPoP-наборами;
|
- один `sprpop.exe` работает с MSDOS- и SDLPoP-наборами;
|
||||||
- замена согласованной пары `SND/SOUND.IDX` + `SND/SND.ARC` не требует
|
- замена согласованной пары `SND/snd.idx` + `SND/snd.arc` не требует
|
||||||
компиляции C и не меняет EXE;
|
компиляции C и не меняет EXE;
|
||||||
- `pop_snd_tbl` больше не генерируется как C-код;
|
- `pop_snd_tbl` больше не генерируется как C-код;
|
||||||
- загружается фактическое число страниц в диапазоне 1..16;
|
- загружается фактическое число страниц в диапазоне 1..16;
|
||||||
|
|||||||
@@ -163,10 +163,16 @@ B имеет смысл только если C окажется неподъё
|
|||||||
1.3/1.4) и распакованные ассеты `SDLPoP/data/` (версия 1.0/1.1). Разница
|
1.3/1.4) и распакованные ассеты `SDLPoP/data/` (версия 1.0/1.1). Разница
|
||||||
есть, и она влияет на выбор источника.
|
есть, и она влияет на выбор источника.
|
||||||
|
|
||||||
### Оцифровка: берём MSDOS
|
### Оцифровка: по умолчанию SDLPoP, MSDOS — опция
|
||||||
|
|
||||||
Заголовок разный (`digi_new_type` против `digi_type`), но **28 звуков из
|
**С 2026-08-31 умолчание — SDLPoP** (`SND_SRC=sdlpop`): он выкачивается
|
||||||
31 совпадают побайтно**. Различаются три, и все не в пользу SDLPoP:
|
автоматически (`make fetch`), а оригинального дистрибутива DOS у сборки
|
||||||
|
может не быть вовсе — и она обязана работать без него. У кого дистрибутив
|
||||||
|
есть, включает лучший набор явно: `make SND_SRC=msdos`.
|
||||||
|
|
||||||
|
Платим за это ровно тем, что разобрано ниже. Заголовок в поставках разный
|
||||||
|
(`digi_new_type` против `digi_type`), но **28 звуков из 31 совпадают
|
||||||
|
побайтно**. Различаются три, и все не в пользу SDLPoP:
|
||||||
|
|
||||||
| id | звук | MSDOS | SDLPoP |
|
| id | звук | MSDOS | SDLPoP |
|
||||||
|---|---|---:|---:|
|
|---|---|---:|---:|
|
||||||
@@ -174,9 +180,13 @@ B имеет смысл только если C окажется неподъё
|
|||||||
| 11 | `sword_moving` | 1 172 | 1 172, но **другие байты** |
|
| 11 | `sword_moving` | 1 172 | 1 172, но **другие байты** |
|
||||||
| 48 | `spiked` | 5 069 | **7** — то есть звука нет |
|
| 48 | `spiked` | 5 069 | **7** — то есть звука нет |
|
||||||
|
|
||||||
`spiked` в наборе SDLPoP фактически пустой. Поэтому упаковщик читает
|
`spiked` в наборе SDLPoP фактически пустой: это звук насаживания на пики
|
||||||
`MSDOS/digisnd*.dat`, а не распакованные ассеты — в отличие от графики,
|
(`spiked()`, seg005.c:245, кадр 177), и с умолчанием он не звучит — от пик
|
||||||
где источник наоборот SDLPoP.
|
остаётся только шипение выдвижения (49 `spikes`). Звон клинков (10) на
|
||||||
|
секунду короче, а 11 просто другая запись.
|
||||||
|
|
||||||
|
Всё остальное — включая уровни `LEVELS/res20xx.bin` — берётся из SDLPoP
|
||||||
|
всегда: `MSDOS/` нужен ровно для этих трёх звуков и ни для чего больше.
|
||||||
|
|
||||||
### MIDI: если дойдём до музыки — брать SDLPoP
|
### MIDI: если дойдём до музыки — брать SDLPoP
|
||||||
|
|
||||||
@@ -961,3 +971,226 @@ load_intro у оригинала и так начинается с ожидан
|
|||||||
Проверено в MAME на сборке `LEVEL=14`: после встречи с принцессой звучит
|
Проверено в MAME на сборке `LEVEL=14`: после встречи с принцессой звучит
|
||||||
тема победы (`pop_mus_id` = 56, `pop_mus_ring` = 6), курсор уходит далеко
|
тема победы (`pop_mus_id` = 56, `pop_mus_ring` = 6), курсор уходит далеко
|
||||||
за размер кольца — то есть подкачка успевает.
|
за размер кольца — то есть подкачка успевает.
|
||||||
|
|
||||||
|
## 9. ЗАДЕРЖКА СТАРТА ЗВУКА: ЗАМЕР (2026-08-31)
|
||||||
|
|
||||||
|
Замерено в MAME на живой сборке: точка отсчёта — подстановка заявки
|
||||||
|
отладчиком при ОСТАНОВЛЕННОЙ машине, конец — условный watchpoint
|
||||||
|
`wpset 19FF6,1,w,{wpdata != 0}` на старший байт `pop_mus_left`, то есть
|
||||||
|
момент, когда трек реально зазвучал. Масштаб тактов откалиброван не по
|
||||||
|
памяти, а по аппаратным часам: период тика насоса CBL — ровно
|
||||||
|
`128/10937,5 = 11,7029` мс, измерено 245 748 и 245 778 тактов, отсюда
|
||||||
|
**21,00 МГц** (кадр луча — 429 973 такта, развёртка 48,8 Гц, не 50).
|
||||||
|
|
||||||
|
### 9.1 Эффекты стартуют без задержки, музыка — через ~200 мс
|
||||||
|
|
||||||
|
Разница принципиальная и вытекает из устройства:
|
||||||
|
|
||||||
|
* **Оцифрованные эффекты диска не касаются вовсе.** `pop_sfx_init()`
|
||||||
|
вызывается ОДИН раз за запуск (`sprpop_cold.c`), вычитывает весь
|
||||||
|
`SND/snd.arc` в EMM и закрывает файл. Дальше `pop_sfx_play` только
|
||||||
|
номинирует, а `pop_sfx_tick` берёт `page/off/len` из таблицы в памяти.
|
||||||
|
Задержка = остаток текущего кадра + ближайший запрос CBL (≤ 11,7 мс).
|
||||||
|
Держать архив открытым НЕЧЕГО: после старта он не нужен.
|
||||||
|
* **Музыка читается с диска на каждый трек** — `mNN.bin` лежат
|
||||||
|
отдельными файлами, и `pop_music_load_begin` открывает свой.
|
||||||
|
|
||||||
|
### 9.2 Разложение окна музыки
|
||||||
|
|
||||||
|
От заявки (`pop_mus_req`) до звучания (`pop_mus_left != 0`), одна
|
||||||
|
загрузка по шагам:
|
||||||
|
|
||||||
|
| шаг | цена |
|
||||||
|
|---|---:|
|
||||||
|
| ожидание `pop_music_service` в кадре | 22,6 мс |
|
||||||
|
| `chdir` #1 (вход в `POP_PATH_CALL`) | **46,1 мс** |
|
||||||
|
| `open` файла трека | 34,5 мс |
|
||||||
|
| `chdir` #2 (восстановление каталога) | **59,0 мс** |
|
||||||
|
| чтение первых 8 КБ (`bank_read_page`) | 34,0 мс |
|
||||||
|
| **итого** | **196,7 мс** |
|
||||||
|
|
||||||
|
**Главное здесь: два `chdir` стоят 105 мс — больше половины окна, и
|
||||||
|
каждый дороже самого `open`.** Убрать их нельзя: старые DSS оставляют
|
||||||
|
рабочий каталог внутри последнего подкаталога, и без восстановления
|
||||||
|
ломаются относительные пути (ради этого `POP_PATH_CALL` и заведён).
|
||||||
|
Теоретический запас — снять ОДИН из двух (≈ 50 мс), но это риск для
|
||||||
|
других мест, которые полагаются на восстановленный каталог.
|
||||||
|
|
||||||
|
Для музыки задержка непринципиальна: трек и так начинается на границе
|
||||||
|
события. Критична она была бы для эффектов — там её нет по построению.
|
||||||
|
|
||||||
|
### 9.3 Разброс и режимы скорости
|
||||||
|
|
||||||
|
Семь прогонов: **189,3 … 232,8 мс**, среднее 200,1. Величина
|
||||||
|
ФИЗИЧЕСКАЯ и от режима скорости не зависит, меняется только её пересчёт
|
||||||
|
в логические кадры:
|
||||||
|
|
||||||
|
| режим | логический кадр (замерено) | окно в кадрах |
|
||||||
|
|---|---:|---:|
|
||||||
|
| NORMAL вне боя | 81,93 мс = 4,00 кадра луча | 2,31 … 2,84 |
|
||||||
|
| FAST / FASTEST вне боя | 61,4 мс = 3,00 кадра луча | 3,09 … 3,42 |
|
||||||
|
|
||||||
|
Худший случай (232,8 мс в коротком кадре) — 3,79 логического кадра.
|
||||||
|
Пейсер при этом работает ровно как задумано: `pop_pace_n` заказывает 4
|
||||||
|
кадра луча вне боя при NORMAL и 3 при FAST/FASTEST, замер это
|
||||||
|
подтвердил. Подкачка трека кадр НЕ просаживает.
|
||||||
|
|
||||||
|
### 9.4 Следствие для паузы конца уровня
|
||||||
|
|
||||||
|
Ждать окончания мелодии по одному `pop_music_busy()` НЕЛЬЗЯ: в этом окне
|
||||||
|
он ещё ложен, и уровень сменился бы до первой ноты. Правильный признак
|
||||||
|
— состояние «аудио-событие в работе», которое выводится из уже
|
||||||
|
существующих переменных без новых сущностей:
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint8_t pop_music_active(void) __banked /* банк 9 — там ld_busy */
|
||||||
|
{
|
||||||
|
return (uint8_t)(pop_mus_req || ld_busy || pop_mus_left);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Переход «запрошен → грузится → звучит» снаружи непрерывен: `service`
|
||||||
|
обнуляет `pop_mus_req` и тут же в `load_begin` взводит `ld_busy`, а
|
||||||
|
главный цикл между этими операторами не выполняется.
|
||||||
|
|
||||||
|
Это лучше фиксированной задержки «на раскачку»: не нужна подобранная
|
||||||
|
константа, нет зависимости от режима скорости, а при провале загрузки
|
||||||
|
(нет файла, нет памяти) все три признака ложны и уровень меняется сразу,
|
||||||
|
вместо слепого ожидания. Заодно чинится расхождение с SDLPoP при
|
||||||
|
ВЫКЛЮЧЕННОМ звуке: `pop_sfx_play` возвращается рано при `!pop_snd_ok`,
|
||||||
|
заявка не кладётся, паузы нет — как в оригинале (`seg006:651` ставит
|
||||||
|
трек только `if (is_sound_on)`, а `seg003:387` ждёт
|
||||||
|
`check_sound_playing()`). Сейчас `pop_endmus_left` взводится
|
||||||
|
безусловно, и при выключенном звуке игра держит пройденный уровень
|
||||||
|
лишние 12 с молча.
|
||||||
|
|
||||||
|
## 10. ЭФФЕКТЫ БЕЗ ПЕРЕЛИНКОВКИ: РАСКЛАДКА УЕХАЛА НА ДИСК (2026-08-31)
|
||||||
|
|
||||||
|
Сделана часть II плана `music_runtime_index_plan.md` (этапы SI0..SI4).
|
||||||
|
Раскладка звукового набора больше не компилируется в EXE: пара
|
||||||
|
`SND/snd.arc` + `SND/snd.idx` меняется целиком, бинарник остаётся прежним.
|
||||||
|
|
||||||
|
### 10.1 Что именно изменилось
|
||||||
|
|
||||||
|
`gen/pop_sound_tbl.h` УДАЛЁН — упаковщик его не печатает. Вместо него:
|
||||||
|
|
||||||
|
* `src/pop_snd_tbl.h` — рукописный: тип записи и инварианты программы
|
||||||
|
(`POP_SND_COUNT` 57, `POP_SND_BLOCK` 128, `POP_SND_MAX_PAGES` 16,
|
||||||
|
константы блока тишины). От набора не зависит НИЧЕГО из этого;
|
||||||
|
* `src/pop_snd_data.c` — резидентные данные: `pop_snd_tbl[57]`,
|
||||||
|
`pop_snd_page[16]`, `pop_snd_pages`;
|
||||||
|
* `SND/snd.idx` — дисковый индекс, формат `PSI1` (писатель и разборщик —
|
||||||
|
`tools/pop_idx.py`, покрыт `make test-tools`, 22 теста);
|
||||||
|
* `pop_sfx_cold.c` — чтение и полная валидация индекса.
|
||||||
|
|
||||||
|
`POP_SND_PAGES` как compile-time размер набора БОЛЬШЕ НЕ СУЩЕСТВУЕТ. Его
|
||||||
|
заменили две разные величины: `POP_SND_MAX_PAGES` — вместимость
|
||||||
|
загрузчика (16, запас под чужие наборы), и runtime `pop_snd_pages` —
|
||||||
|
сколько страниц у ЭТОГО набора. Насос сверяет переход длинного эффекта
|
||||||
|
через границу страницы со вторым, а не с первым.
|
||||||
|
|
||||||
|
### 10.2 Формат `PSI1` и почему запись ровно 5 байт
|
||||||
|
|
||||||
|
Один сектор DSS: 16 байт заголовка (магия, число записей, размер записи,
|
||||||
|
log2 блока, формат PCM, число страниц) и 57 записей по 5 байт —
|
||||||
|
`page` (u8), `off` (u16 LE), `len` (u16 LE), — то есть побайтовый образ
|
||||||
|
`pop_snd_tbl[]`. Записи читаются ОДНИМ `read` прямо в резидентную
|
||||||
|
таблицу, без разбора по полям.
|
||||||
|
|
||||||
|
Поэтому `sizeof(pop_snd_ent_t) == 5` в ABI SDCC z80 — часть ДИСКОВОГО
|
||||||
|
контракта, а не деталь компилятора. Ловится с двух сторон: статически
|
||||||
|
(`pop_snd_data.c`) и полем размера записи в заголовке, которое загрузчик
|
||||||
|
сверяет со своим `sizeof`.
|
||||||
|
|
||||||
|
### 10.3 Цена в памяти
|
||||||
|
|
||||||
|
Таблица была `static const` в `_CODE`, стала изменяемой в `_DATA`.
|
||||||
|
Замер до/после:
|
||||||
|
|
||||||
|
| | было | стало | Δ |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| `_CODE` | 23865 | 23582 | −283 |
|
||||||
|
| данные | 7128 | 7421 | +293 |
|
||||||
|
| куча резидента | 239 Б | 229 Б | −10 |
|
||||||
|
| банк 8 | 14824 | 15425 | +601 |
|
||||||
|
|
||||||
|
Суммарный резидентный расход почти не изменился — в huge-модели `_CODE`
|
||||||
|
и данные лежат подряд в одном диапазоне `0x4000..0xBFFF`. Реальные
|
||||||
|
10 байт — это расширение `pop_snd_page` с размера набора до
|
||||||
|
`POP_SND_MAX_PAGES` плюс байт счётчика.
|
||||||
|
|
||||||
|
### 10.4 Валидация: чему именно нельзя доверять
|
||||||
|
|
||||||
|
Индекс — файл на диске, и битый индекс увёл бы насос за пределы
|
||||||
|
выделенного блока EMM. Проверяются заголовок целиком и КАЖДАЯ запись:
|
||||||
|
`page < pop_snd_pages`, `off` внутри страницы, кратность `off`/`len`
|
||||||
|
блоку 128, непересечение с блоком тишины и то, что длинный эффект не
|
||||||
|
выходит за последнюю страницу набора.
|
||||||
|
|
||||||
|
Последняя проверка считается В БЛОКАХ, а не в байтах: байтовый адрес
|
||||||
|
конца (`page * 16384 + off + len`) не влезает в `uint16`, а 32-битная
|
||||||
|
арифметика на Z80 дорога. В блоках весь набор — не больше
|
||||||
|
`16 * 128 = 2048`, и всё считается шестнадцатью битами.
|
||||||
|
|
||||||
|
Число страниц из индекса дополнительно сверяется с числом элементов в
|
||||||
|
`snd.arc`: расходящаяся пара означает, что файлы из разных наборов.
|
||||||
|
|
||||||
|
### 10.5 Нет индекса — эффектов нет, но МУЗЫКА ОСТАЁТСЯ
|
||||||
|
|
||||||
|
Первая версия просто возвращала ошибку, и это оказалось непроходимой
|
||||||
|
игрой: тишину в паузах насос льёт из первого блока набора, поэтому без
|
||||||
|
набора CBL не открывался вовсе — а вместе с ним вставала музыка, чьи
|
||||||
|
блоки отсчитывает тот же насос. Заставка ждала конца трека, который не
|
||||||
|
начинал играть, и `pop_music_busy()` не гас никогда.
|
||||||
|
|
||||||
|
Теперь при отказе индекса поднимается ПУСТОЙ набор: одна страница EMM с
|
||||||
|
блоком тишины. Все `len` нулевые, поэтому `pop_sfx_play` уходит по ветке
|
||||||
|
«нет оцифровки» — эффекты молчат, музыка играет, игра проходима.
|
||||||
|
|
||||||
|
Две тонкости этой заливки, обе выяснены на живой машине:
|
||||||
|
|
||||||
|
1. **Заливается только первый блок (128 байт), а не вся страница.**
|
||||||
|
`gfx_w0_page_prepare` ставит в страницу IRQ/NMI-стабы, чтобы
|
||||||
|
прерывание при замапленном W0 нашло валидный код. Заливка всей
|
||||||
|
страницы затирает их, и первое же прерывание уводит процессор в
|
||||||
|
мусор — чёрный экран на старте. Насосу больше и не нужно: он берёт
|
||||||
|
ровно блок тишины, да и то через W3.
|
||||||
|
2. **Запись под `IRQ_DISABLE`** — 128 байт накрывают стаб RST38, и до
|
||||||
|
`gfx_w0_unmap` прерываний быть не должно.
|
||||||
|
|
||||||
|
### 10.6 Грабли сборки: секундная гранулярность mtime
|
||||||
|
|
||||||
|
Смена `SND_SRC` тихо давала НЕВЕРНЫЙ результат: `sdlpop -> msdos ->
|
||||||
|
sdlpop` перепаковывал `build/atl`, а в `assets/packed` оставался прежний
|
||||||
|
набор. Причина не в логике, а во времени: упаковка укладывается в одну
|
||||||
|
секунду с записью stamp'а, а `make` считает цель, сделанную в ту же
|
||||||
|
секунду, что и зависимость, актуальной. С паузой в секунду не
|
||||||
|
воспроизводится вовсе.
|
||||||
|
|
||||||
|
Лечится двумя приёмами, и оба убирают время из решения:
|
||||||
|
|
||||||
|
* смена варианта СНОСИТ stamp'ы своего семейства (`rm -f
|
||||||
|
$(STAMP)/sound-*`) — нет файла, значит правило обязано выполниться;
|
||||||
|
* упаковка, сборка архива и копия индекса делаются ОДНИМ рецептом:
|
||||||
|
внутри рецепта сравнивать нечего.
|
||||||
|
|
||||||
|
То же лечение получила и музыка (`MUSIC_FMT`) — механизм общий.
|
||||||
|
|
||||||
|
### 10.7 Что проверено на живой машине
|
||||||
|
|
||||||
|
* таблица в памяти совпадает с файлом из образа ПОБАЙТОВО (285 байт);
|
||||||
|
* один и тот же EXE (sha256 не меняется) поднимает SDLPoP (9 страниц) и
|
||||||
|
MSDOS (10 страниц);
|
||||||
|
* СТРОГАЯ проверка: у двух реальных наборов совпадают 23 записи из 31,
|
||||||
|
поэтому сделан отладочный ключ `pop_pack_sound.py --order reverse` —
|
||||||
|
набор с обратным порядком укладки, где от штатного отличаются 30 из 31
|
||||||
|
записей. Игра на нём звучит правильно, значит раскладка берётся с
|
||||||
|
диска, а не из памяти;
|
||||||
|
* битый индекс (испорченная магия) — эффекты выключаются, игра жива;
|
||||||
|
* отсутствующий индекс — музыка играет, эффектов нет, PV-сцена проходит;
|
||||||
|
* Ctrl+S в обоих режимах работает штатно.
|
||||||
|
|
||||||
|
Маркер для ручной проверки: звук 51 `princess_door_opening` (дверь в
|
||||||
|
сцене с принцессой) лежит у наборов по РАЗНЫМ страницам — 8 у SDLPoP,
|
||||||
|
9 у MSDOS. А вот 4 `gate_closing` из той же сцены совпадает побайтно и
|
||||||
|
для проверки подмены не годится.
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
/* pop_sound_tbl.h — раскладка оцифрованных эффектов PoP.
|
|
||||||
* СГЕНЕРЕНО tools/pop_pack_sound.py — НЕ править вручную.
|
|
||||||
*
|
|
||||||
* Данные: 8 бит беззнаковые, МОНО, 10937.5 Гц (CBL_FREQ_10K9) — формат
|
|
||||||
* CBL один в один, конвертировать в рантайме нечего.
|
|
||||||
*
|
|
||||||
* Начало каждого звука кратно 128 — размеру блока запроса CBL, а
|
|
||||||
* страница (16384) кратна ему же. Поэтому НИ ОДИН блок не пересекает
|
|
||||||
* границу страницы. Больше того: с 2026-08-25 каждый звук лежит
|
|
||||||
* ЦЕЛИКОМ в одной странице — не влезает в остаток, значит начинаем
|
|
||||||
* новую (страницы EMM у нас с запасом). Единственное исключение —
|
|
||||||
* эффект, который сам ДЛИННЕЕ страницы: для него насос переключает
|
|
||||||
* страницу на ходу, и эта ветка в pop_sfx_fill остаётся ради него.
|
|
||||||
*
|
|
||||||
* Смещение хранится РАЗОБРАННЫМ на страницу и адрес в окне: на Z80
|
|
||||||
* 32-битная арифметика дорога, а так всё влезает в 8/16 бит.
|
|
||||||
* Длина округлена вверх до блока — неполных блоков не бывает.
|
|
||||||
*/
|
|
||||||
#ifndef POP_SOUND_TBL_H
|
|
||||||
#define POP_SOUND_TBL_H
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#define POP_SND_PAGES 10
|
|
||||||
#define POP_SND_COUNT 57
|
|
||||||
#define POP_SND_BLOCK 128
|
|
||||||
/* Первый блок страницы 0 — тишина для насоса (см. упаковщик). */
|
|
||||||
#define POP_SND_SILENCE_PAGE 0
|
|
||||||
#define POP_SND_SILENCE_OFF 0x0000
|
|
||||||
|
|
||||||
/* Полная таблица нужна только горячей половине звука. */
|
|
||||||
#ifdef POP_SND_TABLE
|
|
||||||
typedef struct { uint8_t page; uint16_t off; uint16_t len; } pop_snd_ent_t;
|
|
||||||
|
|
||||||
/* len == 0 — у звука нет оцифровки (это музыка, см. sound_plan.md). */
|
|
||||||
static const pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT] = {
|
|
||||||
{ 0, 0x0080, 1664 }, /* 0 */
|
|
||||||
{ 0, 0x0700, 12032 }, /* 1 */
|
|
||||||
{ 1, 0x0000, 6272 }, /* 2 */
|
|
||||||
{ 1, 0x1880, 1280 }, /* 3 */
|
|
||||||
{ 1, 0x1D80, 1152 }, /* 4 */
|
|
||||||
{ 1, 0x2200, 1408 }, /* 5 */
|
|
||||||
{ 1, 0x2780, 5504 }, /* 6 */
|
|
||||||
{ 2, 0x0000, 896 }, /* 7 */
|
|
||||||
{ 2, 0x0380, 1664 }, /* 8 */
|
|
||||||
{ 2, 0x0A00, 2816 }, /* 9 */
|
|
||||||
{ 2, 0x1500, 4992 }, /* 10 */
|
|
||||||
{ 2, 0x2880, 1280 }, /* 11 */
|
|
||||||
{ 2, 0x2D80, 1280 }, /* 12 */
|
|
||||||
{ 3, 0x0000, 3840 }, /* 13 */
|
|
||||||
{ 3, 0x0F00, 7552 }, /* 14 */
|
|
||||||
{ 4, 0x0000, 17664 }, /* 15 */
|
|
||||||
{ 5, 0x0500, 4736 }, /* 16 */
|
|
||||||
{ 5, 0x1780, 4224 }, /* 17 */
|
|
||||||
{ 6, 0x0000, 6912 }, /* 18 */
|
|
||||||
{ 6, 0x1B00, 2816 }, /* 19 */
|
|
||||||
{ 6, 0x2600, 1408 }, /* 20 */
|
|
||||||
{ 6, 0x2B80, 1152 }, /* 21 */
|
|
||||||
{ 6, 0x3000, 1280 }, /* 22 */
|
|
||||||
{ 6, 0x3500, 1408 }, /* 23 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 24 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 25 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 26 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 27 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 28 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 29 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 30 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 31 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 32 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 33 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 34 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 35 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 36 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 37 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 38 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 39 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 40 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 41 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 42 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 43 */
|
|
||||||
{ 7, 0x0000, 2688 }, /* 44 */
|
|
||||||
{ 7, 0x0A80, 4224 }, /* 45 */
|
|
||||||
{ 7, 0x1B00, 4608 }, /* 46 */
|
|
||||||
{ 8, 0x0000, 5120 }, /* 47 */
|
|
||||||
{ 8, 0x1400, 5120 }, /* 48 */
|
|
||||||
{ 8, 0x2800, 4480 }, /* 49 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 50 */
|
|
||||||
{ 9, 0x0000, 4864 }, /* 51 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 52 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 53 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 54 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 55 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 56 */
|
|
||||||
};
|
|
||||||
#endif /* POP_SND_TABLE */
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -8,10 +8,15 @@
|
|||||||
#define _POP_SFX_INTERNAL_H
|
#define _POP_SFX_INTERNAL_H
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include "pop_sound_tbl.h" /* POP_SND_* и, по запросу, таблица эффектов */
|
#include "pop_snd_tbl.h" /* тип записи и инварианты (НЕ генерируется) */
|
||||||
|
|
||||||
/* Физические страницы набора, по индексу из pop_snd_tbl[].page. */
|
/* РАСКЛАДКА НАБОРА и его страницы — общие данные обеих половин, живут в
|
||||||
extern uint8_t pop_snd_page[POP_SND_PAGES];
|
* pop_snd_data.c. Значения приезжают с диска (SND/snd.idx): холодная
|
||||||
|
* половина их читает и проверяет, горячая только пользуется. */
|
||||||
|
extern pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT];
|
||||||
|
extern uint8_t pop_snd_page[POP_SND_MAX_PAGES];
|
||||||
|
/* Сколько страниц у загруженного набора; 0 — набор не поднят. */
|
||||||
|
extern uint8_t pop_snd_pages;
|
||||||
/* 1 — набор поднят и CBL открыт; 0 — играем молча. */
|
/* 1 — набор поднят и CBL открыт; 0 — играем молча. */
|
||||||
extern uint8_t pop_snd_ok;
|
extern uint8_t pop_snd_ok;
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,8 @@
|
|||||||
#include <irq.h> /* IRQ_DISABLE/IRQ_ENABLE вокруг курсора */
|
#include <irq.h> /* IRQ_DISABLE/IRQ_ENABLE вокруг курсора */
|
||||||
#include "pop_sfx.h"
|
#include "pop_sfx.h"
|
||||||
#include "pop_music.h"
|
#include "pop_music.h"
|
||||||
#define POP_SND_TABLE /* таблица нужна только горячей половине */
|
|
||||||
#include "_pop_sfx.h"
|
#include "_pop_sfx.h"
|
||||||
|
|
||||||
/* Физические страницы набора; заполняет холодная половина. */
|
|
||||||
uint8_t pop_snd_page[POP_SND_PAGES];
|
|
||||||
uint8_t pop_snd_ok;
|
uint8_t pop_snd_ok;
|
||||||
|
|
||||||
/* ХОЧЕТ ЛИ ЗВУКА ПОЛЬЗОВАТЕЛЬ (Ctrl+S) — отдельно от pop_snd_ok.
|
/* ХОЧЕТ ЛИ ЗВУКА ПОЛЬЗОВАТЕЛЬ (Ctrl+S) — отдельно от pop_snd_ok.
|
||||||
@@ -170,7 +167,7 @@ int pop_sfx_fill(uint16_t n)
|
|||||||
* целиком в одну). Гард обязателен: без него ошибка в длине увела
|
* целиком в одну). Гард обязателен: без него ошибка в длине увела
|
||||||
* бы насос за pop_snd_page[] и он читал бы чужую память — на слух
|
* бы насос за pop_snd_page[] и он читал бы чужую память — на слух
|
||||||
* это «играет не тот звук», причём каждый раз другой. */
|
* это «играет не тот звук», причём каждый раз другой. */
|
||||||
if ((uint8_t)(sfx_pg + 1) < POP_SND_PAGES) sfx_pg++;
|
if ((uint8_t)(sfx_pg + 1) < pop_snd_pages) sfx_pg++;
|
||||||
else sfx_left = n; /* обрываем */
|
else sfx_left = n; /* обрываем */
|
||||||
}
|
}
|
||||||
sfx_left -= n;
|
sfx_left -= n;
|
||||||
|
|||||||
@@ -5,20 +5,163 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
#include <sprinter_mem.h>
|
#include <sprinter_mem.h>
|
||||||
#include <cbl.h>
|
#include <cbl.h>
|
||||||
#include "pop_sfx.h"
|
#include "pop_sfx.h"
|
||||||
#include "pop_arc.h"
|
#include "pop_arc.h"
|
||||||
#include "_pop_sfx.h"
|
#include "_pop_sfx.h"
|
||||||
|
#include "_pop_file.h" /* POP_PATH_CALL — старые DSS и пути */
|
||||||
|
#include <irq.h> /* IRQ_DISABLE вокруг записи в W0 */
|
||||||
|
#include <sprite.h> /* gfx_w0_map — залить блок тишины */
|
||||||
|
|
||||||
static uint8_t snd_blk; /* блок EMM под весь набор */
|
static uint8_t snd_blk; /* блок EMM под весь набор */
|
||||||
|
|
||||||
|
/* --- ИНДЕКС НАБОРА (SND/snd.idx, формат PSI1) ----------------------- *
|
||||||
|
*
|
||||||
|
* Раскладка эффектов приезжает с диска, а не из EXE: так пара
|
||||||
|
* snd.idx + snd.arc заменяется целиком (SDLPoP <-> MSDOS) без
|
||||||
|
* перекомпиляции игры. Писатель формата — tools/pop_idx.py, он же
|
||||||
|
* покрыт тестом; проверки ниже повторяют его один в один, потому что
|
||||||
|
* доверять содержимому файла нельзя: битый индекс увёл бы насос за
|
||||||
|
* пределы выделенного блока EMM.
|
||||||
|
*
|
||||||
|
* Заголовок: 16 байт, дальше POP_SND_COUNT записей по 5 байт подряд —
|
||||||
|
* ровно в том виде, в каком лежит pop_snd_tbl[], поэтому читаются они
|
||||||
|
* ОДНИМ read'ом прямо туда, без разбора по полям. */
|
||||||
|
#define IDX_HDR_SIZE 16
|
||||||
|
#define IDX_BLOCK_LOG2 7 /* 1 << 7 == POP_SND_BLOCK */
|
||||||
|
#define IDX_PCM_FMT 1 /* u8 mono, CBL_FREQ_10K9 */
|
||||||
|
#define IDX_TBL_SIZE (POP_SND_COUNT * (int)sizeof(pop_snd_ent_t))
|
||||||
|
|
||||||
|
/* Имя знает сам модуль — литерал лежит в rodata банка 8, и open() читает
|
||||||
|
* его, пока в W3 стоит этот же банк (тот же приём, что в pop_arc.c). */
|
||||||
|
static const char idx_path[] = "SND\\snd.idx";
|
||||||
|
|
||||||
|
/* Одна запись: ровно те проверки, что делает упаковщик.
|
||||||
|
*
|
||||||
|
* СЧИТАЕМ В БЛОКАХ, А НЕ В БАЙТАХ. Байтовый адрес конца эффекта
|
||||||
|
* (page * 16384 + off + len) не влезает в uint16, а 32-битная арифметика
|
||||||
|
* на Z80 дорога (memory avoid_32bit_arith_z80). В блоках по 128 весь
|
||||||
|
* набор — не больше POP_SND_MAX_PAGES * 128 = 2048, и всё считается
|
||||||
|
* шестнадцатью битами. */
|
||||||
|
static int8_t idx_entry_ok(const pop_snd_ent_t *e, uint8_t pages)
|
||||||
|
{
|
||||||
|
uint16_t start, end;
|
||||||
|
|
||||||
|
if (e->len == 0) /* нет оцифровки — это музыка */
|
||||||
|
return (e->page == 0 && e->off == 0) ? 0 : -1;
|
||||||
|
if (e->page >= pages) return -1;
|
||||||
|
if (e->off >= 0x4000) return -1;
|
||||||
|
if ((e->off | e->len) & (POP_SND_BLOCK - 1)) return -1;
|
||||||
|
/* Блок тишины неприкосновенен: насос льёт его, когда играть нечего. */
|
||||||
|
if (e->page == POP_SND_SILENCE_PAGE && e->off < POP_SND_BLOCK) return -1;
|
||||||
|
start = (uint16_t)((uint16_t)e->page << 7) + (uint16_t)(e->off >> 7);
|
||||||
|
end = (uint16_t)(start + (uint16_t)(e->len >> 7));
|
||||||
|
/* Длинный эффект переливается на следующие страницы — они обязаны
|
||||||
|
* быть загружены, иначе насос уйдёт в чужую память. */
|
||||||
|
if (end > (uint16_t)((uint16_t)pages << 7)) return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Прочитать и проверить индекс. Возвращает число страниц набора либо 0. */
|
||||||
|
static uint8_t idx_load(void)
|
||||||
|
{
|
||||||
|
uint8_t head[IDX_HDR_SIZE];
|
||||||
|
uint8_t pages, i;
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
POP_PATH_CALL(fd, open(idx_path, O_RDONLY));
|
||||||
|
if (fd < 0) return 0;
|
||||||
|
if (read(fd, head, IDX_HDR_SIZE) != IDX_HDR_SIZE ||
|
||||||
|
memcmp(head, "PSI1", 4) != 0 ||
|
||||||
|
head[4] != POP_SND_COUNT ||
|
||||||
|
head[5] != (uint8_t)sizeof(pop_snd_ent_t) ||
|
||||||
|
head[6] != IDX_BLOCK_LOG2 ||
|
||||||
|
head[7] != IDX_PCM_FMT) {
|
||||||
|
close(fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
pages = head[8];
|
||||||
|
if (pages == 0 || pages > POP_SND_MAX_PAGES) { close(fd); return 0; }
|
||||||
|
/* Записи ложатся прямо в резидентную таблицу: дисковый формат и
|
||||||
|
* структура совпадают побайтово (проверено статически в
|
||||||
|
* pop_snd_data.c и полем head[5] выше). */
|
||||||
|
if (read(fd, pop_snd_tbl, IDX_TBL_SIZE) != IDX_TBL_SIZE) {
|
||||||
|
close(fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
for (i = 0; i < POP_SND_COUNT; i++)
|
||||||
|
if (idx_entry_ok(&pop_snd_tbl[i], pages) != 0) return 0;
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* НАБОРА НЕТ — НО НАСОС ВСЁ РАВНО ОБЯЗАН РАБОТАТЬ.
|
||||||
|
*
|
||||||
|
* Тишину в паузах насос льёт из ПЕРВОГО БЛОКА НАБОРА, поэтому без набора
|
||||||
|
* CBL не открывался вовсе. А вместе с ним вставала и МУЗЫКА: её блоки
|
||||||
|
* отсчитывает тот же насос (pop_mus_left убывает только в pop_sfx_fill).
|
||||||
|
* Заставка при этом висла намертво — ждала конца трека, который не
|
||||||
|
* начинал играть, и pop_music_busy() не гас никогда (поймано
|
||||||
|
* пользователем 2026-08-31 на образе без snd.idx).
|
||||||
|
*
|
||||||
|
* Поэтому при отказе индекса поднимаем ПУСТОЙ набор: одна страница EMM с
|
||||||
|
* блоком тишины. Эффектов нет — все len в таблице нулевые, и
|
||||||
|
* pop_sfx_play уходит по ветке «нет оцифровки», — а музыка играет.
|
||||||
|
*
|
||||||
|
* ЗАЛИВАЕМ ТОЛЬКО ПЕРВЫЙ БЛОК, и это не экономия: gfx_w0_page_prepare
|
||||||
|
* ставит в страницу IRQ/NMI-стабы, чтобы прерывание при замапленном W0
|
||||||
|
* нашло валидный код. Заливка ВСЕЙ страницы затирает их, и первое же
|
||||||
|
* прерывание уводит процессор в мусор — чёрный экран на старте (поймано
|
||||||
|
* при первой попытке этого фикса). Насосу больше 128 байт и не нужно:
|
||||||
|
* он берёт ровно блок тишины, да и то через W3, а не через W0.
|
||||||
|
* Запись — под DI: 128 байт накрывают стаб RST38, и до unmap прерываний
|
||||||
|
* быть не должно. */
|
||||||
|
static int8_t snd_silence_only(void)
|
||||||
|
{
|
||||||
|
uint8_t blk, page;
|
||||||
|
uint8_t *w0 = (uint8_t *)0x0000; /* окно 0 — сюда ляжет страница */
|
||||||
|
|
||||||
|
memset(pop_snd_tbl, 0, sizeof(pop_snd_tbl));
|
||||||
|
blk = mem_alloc_pages(1);
|
||||||
|
if (!blk) return -1;
|
||||||
|
page = mem_get_page(blk, 0);
|
||||||
|
if (!page) { mem_free_block(blk); return -1; }
|
||||||
|
gfx_w0_page_prepare(page);
|
||||||
|
IRQ_DISABLE();
|
||||||
|
gfx_w0_map(page);
|
||||||
|
memset(w0, 0x80, POP_SND_BLOCK); /* 0x80 — тишина в 8-бит без знака */
|
||||||
|
gfx_w0_unmap();
|
||||||
|
IRQ_ENABLE();
|
||||||
|
pop_snd_page[POP_SND_SILENCE_PAGE] = page;
|
||||||
|
snd_blk = blk;
|
||||||
|
pop_snd_pages = 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int pop_sfx_init(void) __banked
|
int pop_sfx_init(void) __banked
|
||||||
{
|
{
|
||||||
uint8_t i, blk;
|
uint8_t i, blk, pages;
|
||||||
|
|
||||||
if (pop_snd_ok) return 0;
|
if (pop_snd_ok) return 0;
|
||||||
|
|
||||||
|
/* СНАЧАЛА ИНДЕКС, ПОТОМ ДАННЫЕ: сколько страниц читать и куда какой
|
||||||
|
* эффект лёг — знает только он. Набора без индекса не бывает:
|
||||||
|
* держать в EXE запасную таблицу нельзя, иначе привязка бинарника к
|
||||||
|
* конкретному набору вернётся через чёрный ход. */
|
||||||
|
pages = idx_load();
|
||||||
|
if (!pages) {
|
||||||
|
/* Набор эффектов не поднялся — но звук как таковой поднять надо,
|
||||||
|
* иначе встанет музыка (см. snd_silence_only). Возвращаем -1:
|
||||||
|
* эффектов действительно нет, и вызывающий вправе об этом знать. */
|
||||||
|
if (snd_silence_only() != 0) return -1;
|
||||||
|
pop_snd_want = 1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
/* Одним блоком: страницы блока идут подряд по логическому индексу, и
|
/* Одним блоком: страницы блока идут подряд по логическому индексу, и
|
||||||
* таблица звуков адресует их именно так (page = смещение >> 14).
|
* таблица звуков адресует их именно так (page = смещение >> 14).
|
||||||
*
|
*
|
||||||
@@ -28,14 +171,17 @@ int pop_sfx_init(void) __banked
|
|||||||
* pop_arc.h. */
|
* pop_arc.h. */
|
||||||
{
|
{
|
||||||
pop_arc_t arc;
|
pop_arc_t arc;
|
||||||
uint8_t tbl[POP_SND_PAGES * POP_ARC_REC];
|
uint8_t tbl[POP_SND_MAX_PAGES * POP_ARC_REC];
|
||||||
|
|
||||||
if (pop_arc_open_id(&arc, POP_ARC_SND, tbl, POP_SND_PAGES) !=
|
/* Архив обязан содержать РОВНО столько страниц, сколько обещал
|
||||||
POP_SND_PAGES)
|
* индекс: расходящейся паре доверять нельзя — она означает, что
|
||||||
|
* snd.idx и snd.arc из разных наборов. */
|
||||||
|
if (pop_arc_open_id(&arc, POP_ARC_SND, tbl, POP_SND_MAX_PAGES) !=
|
||||||
|
(int8_t)pages)
|
||||||
return -1;
|
return -1;
|
||||||
blk = mem_alloc_pages(POP_SND_PAGES);
|
blk = mem_alloc_pages(pages);
|
||||||
if (!blk) { pop_arc_close(&arc); return -1; }
|
if (!blk) { pop_arc_close(&arc); return -1; }
|
||||||
for (i = 0; i < POP_SND_PAGES; i++) {
|
for (i = 0; i < pages; i++) {
|
||||||
pop_snd_page[i] = mem_get_page(blk, i);
|
pop_snd_page[i] = mem_get_page(blk, i);
|
||||||
if (!pop_snd_page[i] ||
|
if (!pop_snd_page[i] ||
|
||||||
pop_arc_read(&arc, tbl, i, 0, pop_snd_page[i], 0, 0) != 16384) {
|
pop_arc_read(&arc, tbl, i, 0, pop_snd_page[i], 0, 0) != 16384) {
|
||||||
@@ -47,6 +193,9 @@ int pop_sfx_init(void) __banked
|
|||||||
pop_arc_close(&arc);
|
pop_arc_close(&arc);
|
||||||
}
|
}
|
||||||
snd_blk = blk;
|
snd_blk = blk;
|
||||||
|
/* Число страниц ставим ПОСЛЕДНИМ: до этого момента насос обязан
|
||||||
|
* видеть 0 и не пытаться ходить по полузаполненной таблице. */
|
||||||
|
pop_snd_pages = pages;
|
||||||
pop_snd_want = 1; /* набор есть — звук по умолчанию включён */
|
pop_snd_want = 1; /* набор есть — звук по умолчанию включён */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* pop_snd_data.c — РЕЗИДЕНТНЫЕ ДАННЫЕ звукового набора.
|
||||||
|
*
|
||||||
|
* Отдельным модулем, потому что стороны у этих данных разные: заполняет
|
||||||
|
* их ХОЛОДНЫЙ загрузчик из банка 8 (pop_sfx_cold.c), а читает ГОРЯЧИЙ
|
||||||
|
* насос из резидента (pop_sfx.c) — в том числе из прерывания. Держать
|
||||||
|
* их в любой из половин значило бы тянуть чужую в резидент.
|
||||||
|
*
|
||||||
|
* Инициализаторов нет намеренно: crt0 зануляет _DATA (memory
|
||||||
|
* sdcc_static_storage_gotcha), а «набор не загружен» — это и есть нули.
|
||||||
|
*
|
||||||
|
* ЦЕНА ПЕРЕЕЗДА. Таблица была `static const` в _CODE (285 Б); сняв
|
||||||
|
* const, мы переносим её в _DATA — суммарный резидентный расход не
|
||||||
|
* меняется, потому что в huge-модели _CODE и данные лежат подряд в одном
|
||||||
|
* диапазоне 0x4000..0xBFFF. Реально добавилось 8 байт: pop_snd_page
|
||||||
|
* вырос с размера набора до POP_SND_MAX_PAGES (запас под чужие наборы)
|
||||||
|
* плюс байт самого счётчика.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "pop_snd_tbl.h"
|
||||||
|
|
||||||
|
/* РАСКЛАДКА НАБОРА — приезжает из SND/snd.idx целиком, одним read'ом
|
||||||
|
* прямо сюда. Поэтому размер записи обязан совпадать с дисковым: если
|
||||||
|
* ABI когда-нибудь добавит выравнивание, сборка упадёт здесь, а не
|
||||||
|
* рассыплется звуком в рантайме. */
|
||||||
|
pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT];
|
||||||
|
typedef char pop_snd_ent_is_5_bytes[(sizeof(pop_snd_ent_t) == 5) ? 1 : -1];
|
||||||
|
|
||||||
|
/* Физические страницы EMM, по индексу из pop_snd_tbl[].page. */
|
||||||
|
uint8_t pop_snd_page[POP_SND_MAX_PAGES];
|
||||||
|
|
||||||
|
/* Сколько страниц у ЭТОГО набора (1..POP_SND_MAX_PAGES). Насос сверяет
|
||||||
|
* с ним переход длинного эффекта через границу страницы. */
|
||||||
|
uint8_t pop_snd_pages;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* pop_snd_tbl.h — РАСКЛАДКА ОЦИФРОВАННЫХ ЭФФЕКТОВ: тип и инварианты.
|
||||||
|
*
|
||||||
|
* Раньше этот файл ПЕЧАТАЛ упаковщик (gen/pop_sound_tbl.h), и конкретные
|
||||||
|
* page/off/len становились частью EXE — из-за чего замена звукового набора
|
||||||
|
* (SDLPoP <-> MSDOS) требовала перекомпиляции игры. Теперь значения
|
||||||
|
* приезжают с диска (SND/snd.idx, формат PSI1 — tools/pop_idx.py), а
|
||||||
|
* здесь остаётся только то, что от набора НЕ зависит: устройство записи и
|
||||||
|
* пределы, на которые рассчитан загрузчик.
|
||||||
|
*
|
||||||
|
* ЗАПИСЬ ЧИТАЕТСЯ С ДИСКА ПОБАЙТОВО, прямо в pop_snd_tbl[]. Значит
|
||||||
|
* раскладка структуры в ABI SDCC z80 (uint8 + uint16 + uint16 подряд, без
|
||||||
|
* выравнивания = 5 байт) — часть дискового контракта, а не деталь
|
||||||
|
* компилятора. Расхождение молча разъехало бы весь набор, поэтому оно
|
||||||
|
* ловится статически (pop_snd_data.c) и проверяется загрузчиком по полю
|
||||||
|
* размера записи в заголовке индекса.
|
||||||
|
*/
|
||||||
|
#ifndef POP_SND_TBL_H
|
||||||
|
#define POP_SND_TBL_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* Пространство id оригинала: 0..56. От набора не зависит. */
|
||||||
|
#define POP_SND_COUNT 57
|
||||||
|
/* Блок запроса CBL при 8 бит; страница EMM кратна ему, поэтому ни один
|
||||||
|
* блок не пересекает границу страницы (см. docs/sound_plan.md). */
|
||||||
|
#define POP_SND_BLOCK 128
|
||||||
|
/* СКОЛЬКО СТРАНИЦ ГОТОВ ПРИНЯТЬ ЗАГРУЗЧИК — вместимость, а НЕ размер
|
||||||
|
* конкретного набора: тот приходит в индексе (pop_snd_pages). Запас
|
||||||
|
* ради будущих наборов без перекомпиляции; цена — 16 байт таблицы
|
||||||
|
* страниц вместо 9-10. */
|
||||||
|
#define POP_SND_MAX_PAGES 16
|
||||||
|
|
||||||
|
/* ПЕРВЫЙ БЛОК СТРАНИЦЫ 0 — ТИШИНА. Насос льёт её, когда играть нечего:
|
||||||
|
* без этого железо доигрывает по кругу хвост своего буфера (жужжание).
|
||||||
|
* Это инвариант ПРОГРАММЫ — упаковщик обязан оставить блок свободным, а
|
||||||
|
* загрузчик проверяет, что ни одна запись в него не залезла. */
|
||||||
|
#define POP_SND_SILENCE_PAGE 0
|
||||||
|
#define POP_SND_SILENCE_OFF 0x0000
|
||||||
|
|
||||||
|
/* page — индекс в pop_snd_page[] (физические страницы EMM), off — начало
|
||||||
|
* внутри страницы, len — длина PCM. Оба кратны POP_SND_BLOCK; len == 0
|
||||||
|
* значит «у этого id нет оцифровки» (это музыка, см. sound_plan.md). */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t page;
|
||||||
|
uint16_t off;
|
||||||
|
uint16_t len;
|
||||||
|
} pop_snd_ent_t;
|
||||||
|
|
||||||
|
#endif /* POP_SND_TBL_H */
|
||||||
@@ -5,9 +5,10 @@ assets/, gen/ и build/.
|
|||||||
|
|
||||||
Раскладка:
|
Раскладка:
|
||||||
|
|
||||||
assets/orig/ ОРИГИНАЛЬНЫЕ данные (SDLPoP, MSDOS, записи музыки).
|
assets/orig/ ОРИГИНАЛЬНЫЕ данные (SDLPoP, записи музыки и,
|
||||||
В репозиторий не идут — копирайт; откуда их взять,
|
необязательно, MSDOS). В репозиторий не идут —
|
||||||
написано в assets/orig/README.md.
|
копирайт; выкачивает их tools/fetch_orig.py
|
||||||
|
(`make fetch`), см. assets/orig/README.md.
|
||||||
build/atl/<set>/ промежуточный выхлоп упаковщиков: отдельные атласы,
|
build/atl/<set>/ промежуточный выхлоп упаковщиков: отдельные атласы,
|
||||||
из которых потом склеиваются архивы. Пересоздаётся,
|
из которых потом склеиваются архивы. Пересоздаётся,
|
||||||
в репозиторий не идёт.
|
в репозиторий не идёт.
|
||||||
@@ -31,6 +32,7 @@ SDLPOP = ORIG / "SDLPoP"
|
|||||||
SDLPOP_DATA = SDLPOP / "data"
|
SDLPOP_DATA = SDLPOP / "data"
|
||||||
SDLPOP_SRC = SDLPOP / "src"
|
SDLPOP_SRC = SDLPOP / "src"
|
||||||
MSDOS = ORIG / "MSDOS"
|
MSDOS = ORIG / "MSDOS"
|
||||||
|
|
||||||
MUSIC_ROOT = ORIG / "PoP1_DOS_music"
|
MUSIC_ROOT = ORIG / "PoP1_DOS_music"
|
||||||
# Записи DOS-версии лежат в четырёх вариантах. Имена треков во всех наборах
|
# Записи DOS-версии лежат в четырёх вариантах. Имена треков во всех наборах
|
||||||
# одинаковые, различаются каталог и расширение; ogg_MT-32 — та же музыка в
|
# одинаковые, различаются каталог и расширение; ogg_MT-32 — та же музыка в
|
||||||
@@ -52,6 +54,44 @@ def music_src(fmt="flac"):
|
|||||||
sub, ext = MUSIC_FORMATS[fmt]
|
sub, ext = MUSIC_FORMATS[fmt]
|
||||||
return MUSIC_ROOT / sub, ext
|
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" # промежуточные атласы, по набору в папке
|
ATL = APP / "build" / "atl" # промежуточные атласы, по набору в папке
|
||||||
PACKED = APP / "assets" / "packed" # раскладка диска игры
|
PACKED = APP / "assets" / "packed" # раскладка диска игры
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
"""pop_idx.py — ДИСКОВЫЕ ИНДЕКСЫ аудио-наборов SprPoP.
|
||||||
|
|
||||||
|
Зачем. Раскладка звукового набора (какой эффект в какой странице, по
|
||||||
|
какому смещению и какой длины) сегодня печатается упаковщиком в
|
||||||
|
gen/pop_sound_tbl.h и становится частью EXE. Из-за этого замена набора
|
||||||
|
(SDLPoP <-> MSDOS) требует перекомпиляции игры. Индекс переносит эту
|
||||||
|
раскладку НА ДИСК: пара `SND/snd.idx` + `SND/snd.arc` меняется целиком,
|
||||||
|
а бинарник остаётся прежним.
|
||||||
|
|
||||||
|
Формат читают ДВЕ стороны — этот упаковщик и загрузчик на Z80, — поэтому
|
||||||
|
писатель и разборщик живут в одном месте и покрыты тестом
|
||||||
|
(tools/tests/test_pop_idx.py). Числа little-endian: у Z80 это родной
|
||||||
|
порядок, и разбор на нём — просто чтение байтов подряд.
|
||||||
|
|
||||||
|
PSI1 — индекс ЭФФЕКТОВ, ровно один сектор DSS (512 Б):
|
||||||
|
|
||||||
|
0..3 'PSI1' магия и версия
|
||||||
|
4 57 число плотных записей, id 0..56
|
||||||
|
5 5 размер записи
|
||||||
|
6 7 log2 размера блока: 1 << 7 = 128
|
||||||
|
7 1 формат PCM: 8 бит без знака, моно, CBL_FREQ_10K9
|
||||||
|
8 pages сколько PCM-страниц в снд-архиве (1..16)
|
||||||
|
9..15 0 резерв
|
||||||
|
16..300 57 записей по 5 байт: page (u8), off (u16), len (u16)
|
||||||
|
301..511 0 добивка до сектора
|
||||||
|
|
||||||
|
Размер записи 5 — это НЕ произвольное число, а sizeof(pop_snd_ent_t) в
|
||||||
|
ABI SDCC z80 (uint8 + uint16 + uint16 без выравнивания). Загрузчик
|
||||||
|
читает записи ПРЯМО в резидентную таблицу, поэтому расхождение здесь
|
||||||
|
молча разъедет всю раскладку; на стороне C это проверяется статически.
|
||||||
|
|
||||||
|
Нулевая `len` означает, что у этого id нет оцифровки (это музыка).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import struct
|
||||||
|
|
||||||
|
SECTOR = 512
|
||||||
|
|
||||||
|
# --- PSI1: эффекты ---------------------------------------------------------
|
||||||
|
SND_MAGIC = b"PSI1"
|
||||||
|
SND_COUNT = 57 # пространство id оригинала: 0..56
|
||||||
|
SND_REC = 5 # sizeof(pop_snd_ent_t) в ABI SDCC z80
|
||||||
|
SND_HDR = 16
|
||||||
|
SND_MAX_PAGES = 16 # compile-time вместимость загрузчика
|
||||||
|
BLOCK = 128 # блок запроса CBL при 8 бит
|
||||||
|
BLOCK_LOG2 = 7
|
||||||
|
PCM_U8_MONO_10K9 = 1 # единственный формат, который умеет наш насос
|
||||||
|
PAGE = 16384
|
||||||
|
|
||||||
|
|
||||||
|
class IdxError(Exception):
|
||||||
|
"""Индекс не разобрался: битый, чужой версии или противоречивый."""
|
||||||
|
|
||||||
|
|
||||||
|
def snd_pack(entries, pages):
|
||||||
|
"""Собрать PSI1 из 57 записей (page, off, len) и числа страниц."""
|
||||||
|
if len(entries) != SND_COUNT:
|
||||||
|
raise IdxError("записей должно быть %d, а не %d"
|
||||||
|
% (SND_COUNT, len(entries)))
|
||||||
|
if not 1 <= pages <= SND_MAX_PAGES:
|
||||||
|
raise IdxError("страниц %d — вне 1..%d" % (pages, SND_MAX_PAGES))
|
||||||
|
|
||||||
|
out = bytearray(SECTOR)
|
||||||
|
out[0:4] = SND_MAGIC
|
||||||
|
out[4] = SND_COUNT
|
||||||
|
out[5] = SND_REC
|
||||||
|
out[6] = BLOCK_LOG2
|
||||||
|
out[7] = PCM_U8_MONO_10K9
|
||||||
|
out[8] = pages
|
||||||
|
for i, (page, off, ln) in enumerate(entries):
|
||||||
|
_check_entry(i, page, off, ln, pages)
|
||||||
|
struct.pack_into("<BHH", out, SND_HDR + i * SND_REC, page, off, ln)
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
def snd_unpack(data):
|
||||||
|
"""Разобрать PSI1 -> (записи, pages). Проверки те же, что у Z80."""
|
||||||
|
if len(data) != SECTOR:
|
||||||
|
raise IdxError("размер %d, а должен быть сектор %d" % (len(data), SECTOR))
|
||||||
|
if data[0:4] != SND_MAGIC:
|
||||||
|
raise IdxError("не PSI1: %r" % (bytes(data[0:4]),))
|
||||||
|
if data[4] != SND_COUNT:
|
||||||
|
raise IdxError("записей %d, ожидалось %d" % (data[4], SND_COUNT))
|
||||||
|
if data[5] != SND_REC:
|
||||||
|
raise IdxError("размер записи %d, ожидался %d" % (data[5], SND_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))
|
||||||
|
pages = data[8]
|
||||||
|
if not 1 <= pages <= SND_MAX_PAGES:
|
||||||
|
raise IdxError("страниц %d — вне 1..%d" % (pages, SND_MAX_PAGES))
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
for i in range(SND_COUNT):
|
||||||
|
page, off, ln = struct.unpack_from("<BHH", data, SND_HDR + i * SND_REC)
|
||||||
|
_check_entry(i, page, off, ln, pages)
|
||||||
|
entries.append((page, off, ln))
|
||||||
|
return entries, pages
|
||||||
|
|
||||||
|
|
||||||
|
def _check_entry(i, page, off, ln, pages):
|
||||||
|
"""Одна запись: то же, что обязан проверить загрузчик перед EMM.
|
||||||
|
|
||||||
|
ПЕРВЫЙ БЛОК СТРАНИЦЫ 0 — ТИШИНА. Насос льёт её, когда играть нечего
|
||||||
|
(POP_SND_SILENCE_PAGE/OFF), поэтому набор не имеет права положить туда
|
||||||
|
эффект: иначе вместо паузы будет обрывок звука по кругу.
|
||||||
|
"""
|
||||||
|
if ln == 0:
|
||||||
|
if (page, off) != (0, 0):
|
||||||
|
raise IdxError("запись %d пуста, но page/off не нулевые" % i)
|
||||||
|
return
|
||||||
|
if page >= pages:
|
||||||
|
raise IdxError("запись %d: page %d >= pages %d" % (i, page, pages))
|
||||||
|
if off >= PAGE:
|
||||||
|
raise IdxError("запись %d: off %d вне страницы" % (i, off))
|
||||||
|
if off % BLOCK or ln % BLOCK:
|
||||||
|
raise IdxError("запись %d: off/len не кратны блоку %d" % (i, BLOCK))
|
||||||
|
if page == 0 and off < BLOCK:
|
||||||
|
raise IdxError("запись %d залезает в блок тишины (стр.0, off 0)" % i)
|
||||||
|
# Длинный эффект переливается на следующие страницы — они обязаны быть.
|
||||||
|
last = (page * PAGE + off + ln - 1) // PAGE
|
||||||
|
if last >= pages:
|
||||||
|
raise IdxError("запись %d выходит за набор: кончается в стр.%d из %d"
|
||||||
|
% (i, last, pages))
|
||||||
@@ -2,7 +2,11 @@
|
|||||||
"""
|
"""
|
||||||
pop_pack_sound.py — упаковщик ЗВУКОВЫХ ЭФФЕКТОВ (оцифровка, chtab нет).
|
pop_pack_sound.py — упаковщик ЗВУКОВЫХ ЭФФЕКТОВ (оцифровка, chtab нет).
|
||||||
|
|
||||||
ИСТОЧНИК. `MSDOS/digisnd1..3.dat` — оригинальные контейнеры. Разбор:
|
ИСТОЧНИК. Контейнеры `digisnd1..3` — по умолчанию из `SDLPoP/data/`
|
||||||
|
(`--source sdlpop`), потому что SDLPoP скачивается автоматически и без него
|
||||||
|
сборки всё равно нет. У кого лежит оригинальный дистрибутив DOS, может
|
||||||
|
взять оцифровку оттуда: `--source msdos` (= `make SND_SRC=msdos`); наборы
|
||||||
|
НЕ одинаковы — версии данных разные. Разбор:
|
||||||
индекс по 8 байт (id, offset, size), **первый байт ресурса — контрольная
|
индекс по 8 байт (id, offset, size), **первый байт ресурса — контрольная
|
||||||
сумма**, тело за ней (спецификация `docs/POP-DAT-FormatSpecifications`,
|
сумма**, тело за ней (спецификация `docs/POP-DAT-FormatSpecifications`,
|
||||||
§3.1.2). Тело — один из двух вариантов заголовка:
|
§3.1.2). Тело — один из двух вариантов заголовка:
|
||||||
@@ -29,8 +33,12 @@ CBL — конвертировать нечего.
|
|||||||
знает слова «стык»: замапил страницу, отдал 128 байт, сдвинул указатель.
|
знает слова «стык»: замапил страницу, отдал 128 байт, сдвинул указатель.
|
||||||
Плата — до 127 байт на звук, всего ~2 КБ.
|
Плата — до 127 байт на звук, всего ~2 КБ.
|
||||||
|
|
||||||
Выход: build/atl/sound/s0..sN.bin (по 16 384 Б) + gen/pop_sound_tbl.h
|
ВЫХОД: build/atl/sound/s0..sN.bin (страницы по 16 384 Б) + snd.idx —
|
||||||
|
дисковая раскладка набора (формат PSI1, tools/pop_idx.py). C-заголовка
|
||||||
|
БОЛЬШЕ НЕТ: раскладка уехала на диск, чтобы замена набора не требовала
|
||||||
|
перекомпиляции игры.
|
||||||
"""
|
"""
|
||||||
|
import argparse
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
@@ -38,11 +46,11 @@ import sys
|
|||||||
import sys as _sys, os as _os
|
import sys as _sys, os as _os
|
||||||
_sys.path.insert(0, _os.path.dirname(_os.path.abspath(__file__)))
|
_sys.path.insert(0, _os.path.dirname(_os.path.abspath(__file__)))
|
||||||
import paths as P # раскладка каталогов SprPoP (tools/paths.py)
|
import paths as P # раскладка каталогов SprPoP (tools/paths.py)
|
||||||
|
import pop_idx # формат дискового индекса PSI1 (tools/pop_idx.py)
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
DAT_DIR = str(P.MSDOS)
|
|
||||||
OUT_DIR = str(P.atl("sound"))
|
OUT_DIR = str(P.atl("sound"))
|
||||||
HDR = str(P.GEN / "pop_sound_tbl.h")
|
IDX = "snd.idx" # дисковый индекс набора, рядом с snd.arc
|
||||||
|
|
||||||
PAGE = 16384
|
PAGE = 16384
|
||||||
BLOCK = 128 # блок запроса CBL при 8 бит (см. cbl.h)
|
BLOCK = 128 # блок запроса CBL при 8 бит (см. cbl.h)
|
||||||
@@ -101,12 +109,35 @@ def resample(src, src_rate, dst_rate):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[1])
|
||||||
|
ap.add_argument("--source", default="sdlpop", choices=sorted(P.SND_SOURCES),
|
||||||
|
help="откуда брать digisnd1..3 (по умолчанию sdlpop)")
|
||||||
|
ap.add_argument("--order", default="id", choices=("id", "reverse"),
|
||||||
|
help="порядок укладки эффектов в страницах: id (штатный) "
|
||||||
|
"или reverse — ОТЛАДОЧНЫЙ, для проверки того, что "
|
||||||
|
"игра берёт раскладку из snd.idx, а не из своей "
|
||||||
|
"памяти: смещения расходятся у ВСЕХ эффектов")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
# Имена контейнеров в поставках отличаются регистром, и на Linux это не
|
||||||
|
# мелочь: ищем без оглядки на него (P.find_ci).
|
||||||
|
dat_dir, pattern = P.snd_src(args.source)
|
||||||
res = {}
|
res = {}
|
||||||
for i in (1, 2, 3):
|
for i in (1, 2, 3):
|
||||||
p = os.path.join(DAT_DIR, f"digisnd{i}.dat")
|
p = P.find_ci(dat_dir, pattern % i)
|
||||||
if not os.path.exists(p):
|
if p is None:
|
||||||
sys.exit(f"нет файла {p}")
|
sys.exit(f"нет файла {dat_dir / (pattern % i)} "
|
||||||
|
f"(источник {args.source}); см. assets/orig/README.md")
|
||||||
res.update(dat_resources(p))
|
res.update(dat_resources(p))
|
||||||
|
print(f" источник оцифровки: {args.source} ({dat_dir})")
|
||||||
|
# У набора SDLPoP оцифровка беднее: `spiked` (48) там длиной 7 сэмплов,
|
||||||
|
# то есть звука пик фактически нет (разбор — docs/sound_plan.md). Если
|
||||||
|
# у пользователя лежит дистрибутив DOS, сказать об этом стоит вслух —
|
||||||
|
# молча отдавать тишину вместо эффекта нельзя.
|
||||||
|
if args.source == "sdlpop" and P.MSDOS.is_dir():
|
||||||
|
print(" ВНИМАНИЕ: у вас есть assets/orig/MSDOS — там оцифровка полнее"
|
||||||
|
" (в наборе SDLPoP звук 48 `spiked` пустой).")
|
||||||
|
print(" собрать с ней: make SND_SRC=msdos")
|
||||||
|
|
||||||
sounds = {}
|
sounds = {}
|
||||||
for sid, body in sorted(res.items()):
|
for sid, body in sorted(res.items()):
|
||||||
@@ -128,7 +159,15 @@ def main():
|
|||||||
# тесная и её отказ ронял бы весь звук.
|
# тесная и её отказ ронял бы весь звук.
|
||||||
blob = bytearray([0x80] * BLOCK)
|
blob = bytearray([0x80] * BLOCK)
|
||||||
table = {}
|
table = {}
|
||||||
for sid in sorted(sounds):
|
# ПОРЯДОК УКЛАДКИ. Штатно — по возрастанию id; `--order reverse` кладёт
|
||||||
|
# те же эффекты задом наперёд. Данные и таблица при этом остаются
|
||||||
|
# согласованными (индекс печатается из table), но НИ ОДНО смещение не
|
||||||
|
# совпадает со штатным набором — только так и проверяется, что игра
|
||||||
|
# читает раскладку с диска, а не помнит её с компиляции: у двух
|
||||||
|
# реальных наборов совпадают 23 записи из 31, и подмена на них видна
|
||||||
|
# слабо.
|
||||||
|
order = sorted(sounds, reverse=(args.order == "reverse"))
|
||||||
|
for sid in order:
|
||||||
pad = (-len(blob)) % BLOCK
|
pad = (-len(blob)) % BLOCK
|
||||||
blob += bytes([0x80]) * pad # 0x80 — тишина в беззнаковом 8-бит
|
blob += bytes([0x80]) * pad # 0x80 — тишина в беззнаковом 8-бит
|
||||||
# Длину округляем ВВЕРХ до блока: хвост последнего блока — это
|
# Длину округляем ВВЕРХ до блока: хвост последнего блока — это
|
||||||
@@ -148,61 +187,34 @@ def main():
|
|||||||
blob += bytes([0x80]) * ((-len(blob)) % PAGE)
|
blob += bytes([0x80]) * ((-len(blob)) % PAGE)
|
||||||
|
|
||||||
os.makedirs(OUT_DIR, exist_ok=True)
|
os.makedirs(OUT_DIR, exist_ok=True)
|
||||||
|
# Старые страницы СНОСИМ: у разных источников их разное число (SDLPoP 9,
|
||||||
|
# MSDOS 10), и лишняя s9.bin от прошлого прогона уехала бы в архив
|
||||||
|
# хвостом, которого нет в таблице.
|
||||||
|
for old in os.listdir(OUT_DIR):
|
||||||
|
if old.startswith("s") and old.endswith(".bin"):
|
||||||
|
os.remove(os.path.join(OUT_DIR, old))
|
||||||
npages = len(blob) // PAGE
|
npages = len(blob) // PAGE
|
||||||
for i in range(npages):
|
for i in range(npages):
|
||||||
with open(os.path.join(OUT_DIR, f"s{i}.bin"), "wb") as f:
|
with open(os.path.join(OUT_DIR, f"s{i}.bin"), "wb") as f:
|
||||||
f.write(blob[i * PAGE:(i + 1) * PAGE])
|
f.write(blob[i * PAGE:(i + 1) * PAGE])
|
||||||
|
|
||||||
lines = [
|
# ДИСКОВЫЙ ИНДЕКС. Та же раскладка, что и в заголовке ниже, но на
|
||||||
"/* pop_sound_tbl.h — раскладка оцифрованных эффектов PoP.",
|
# диске: с ним пара snd.idx + snd.arc заменяется целиком, без
|
||||||
" * СГЕНЕРЕНО tools/pop_pack_sound.py — НЕ править вручную.",
|
# перекомпиляции игры (docs/music_runtime_index_plan.md, часть II).
|
||||||
" *",
|
# Проверки внутри snd_pack — ровно те, что делает загрузчик на Z80:
|
||||||
" * Данные: 8 бит беззнаковые, МОНО, %g Гц (CBL_FREQ_10K9) — формат" % RATE,
|
# набор, который не пройдёт здесь, не пройдёт и там.
|
||||||
" * CBL один в один, конвертировать в рантайме нечего.",
|
if npages > pop_idx.SND_MAX_PAGES:
|
||||||
" *",
|
sys.exit("набор занял %d страниц, а загрузчик берёт не больше %d"
|
||||||
" * Начало каждого звука кратно %d — размеру блока запроса CBL, а" % BLOCK,
|
% (npages, pop_idx.SND_MAX_PAGES))
|
||||||
" * страница (%d) кратна ему же. Поэтому НИ ОДИН блок не пересекает" % PAGE,
|
entries = [table.get(sid, (0, 0)) for sid in range(NSOUND)]
|
||||||
" * границу страницы. Больше того: с 2026-08-25 каждый звук лежит\n * ЦЕЛИКОМ в одной странице — не влезает в остаток, значит начинаем\n * новую (страницы EMM у нас с запасом). Единственное исключение —\n * эффект, который сам ДЛИННЕЕ страницы: для него насос переключает\n * страницу на ходу, и эта ветка в pop_sfx_fill остаётся ради него.",
|
entries = [(off >> 14, off & 0x3FFF, ln) for off, ln in entries]
|
||||||
" *",
|
with open(os.path.join(OUT_DIR, IDX), "wb") as f:
|
||||||
" * Смещение хранится РАЗОБРАННЫМ на страницу и адрес в окне: на Z80",
|
f.write(pop_idx.snd_pack(entries, npages))
|
||||||
" * 32-битная арифметика дорога, а так всё влезает в 8/16 бит.",
|
print(" индекс -> %s (%d страниц)" % (os.path.join(OUT_DIR, IDX),
|
||||||
" * Длина округлена вверх до блока — неполных блоков не бывает.",
|
npages))
|
||||||
" */",
|
|
||||||
"#ifndef POP_SOUND_TBL_H",
|
|
||||||
"#define POP_SOUND_TBL_H",
|
|
||||||
"",
|
|
||||||
"#include <stdint.h>",
|
|
||||||
"",
|
|
||||||
"#define POP_SND_PAGES %d" % npages,
|
|
||||||
"#define POP_SND_COUNT %d" % NSOUND,
|
|
||||||
"#define POP_SND_BLOCK %d" % BLOCK,
|
|
||||||
"/* Первый блок страницы 0 — тишина для насоса (см. упаковщик). */",
|
|
||||||
"#define POP_SND_SILENCE_PAGE 0",
|
|
||||||
"#define POP_SND_SILENCE_OFF 0x0000",
|
|
||||||
"",
|
|
||||||
"/* Полная таблица нужна только горячей половине звука. */",
|
|
||||||
"#ifdef POP_SND_TABLE",
|
|
||||||
"typedef struct { uint8_t page; uint16_t off; uint16_t len; } pop_snd_ent_t;",
|
|
||||||
"",
|
|
||||||
"/* len == 0 — у звука нет оцифровки (это музыка, см. sound_plan.md). */",
|
|
||||||
"static const pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT] = {",
|
|
||||||
]
|
|
||||||
for sid in range(NSOUND):
|
|
||||||
off, ln = table.get(sid, (0, 0))
|
|
||||||
lines.append(" { %d, 0x%04X, %5d }, /* %2d */" % (off >> 14, off & 0x3FFF, ln, sid))
|
|
||||||
lines += [
|
|
||||||
"};",
|
|
||||||
"#endif /* POP_SND_TABLE */",
|
|
||||||
"",
|
|
||||||
"#endif",
|
|
||||||
"",
|
|
||||||
]
|
|
||||||
with open(HDR, "w") as f:
|
|
||||||
f.write("\n".join(lines))
|
|
||||||
|
|
||||||
have = sum(1 for s in sounds.values() if s)
|
have = sum(1 for s in sounds.values() if s)
|
||||||
print(f"ИТОГО: {have} эффектов, {len(blob)} Б = {npages} EMM-страниц -> {OUT_DIR}")
|
print(f"ИТОГО: {have} эффектов, {len(blob)} Б = {npages} EMM-страниц -> {OUT_DIR}")
|
||||||
print(f" заголовок -> {HDR}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
"""test_pop_idx — формат дисковых индексов аудио (tools/pop_idx.py).
|
||||||
|
|
||||||
|
Индекс читают ДВЕ стороны: упаковщик на хосте и загрузчик на Z80. Тест
|
||||||
|
закрепляет ровно то, на что рассчитывает вторая сторона: точные смещения
|
||||||
|
полей, little-endian, размер сектора и НАБОР ОТКАЗОВ — потому что битый
|
||||||
|
индекс обязан выключить звук, а не увести насос в чужую память.
|
||||||
|
|
||||||
|
Запуск: make test-tools (либо python3 -m unittest discover tools/tests)
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import pop_idx as X
|
||||||
|
|
||||||
|
|
||||||
|
def empty():
|
||||||
|
return [(0, 0, 0)] * X.SND_COUNT
|
||||||
|
|
||||||
|
|
||||||
|
class SndFormat(unittest.TestCase):
|
||||||
|
"""PSI1: раскладка байтов, на которую смотрит Z80."""
|
||||||
|
|
||||||
|
def test_size_is_one_sector(self):
|
||||||
|
self.assertEqual(len(X.snd_pack(empty(), 1)), X.SECTOR)
|
||||||
|
|
||||||
|
def test_header_fields(self):
|
||||||
|
d = X.snd_pack(empty(), 9)
|
||||||
|
self.assertEqual(d[0:4], b"PSI1")
|
||||||
|
self.assertEqual(d[4], 57) # число записей
|
||||||
|
self.assertEqual(d[5], 5) # sizeof(pop_snd_ent_t) в SDCC z80
|
||||||
|
self.assertEqual(d[6], 7) # log2(128)
|
||||||
|
self.assertEqual(d[7], 1) # u8 mono 10937.5
|
||||||
|
self.assertEqual(d[8], 9) # фактических страниц
|
||||||
|
self.assertEqual(d[9:16], b"\0" * 7)
|
||||||
|
|
||||||
|
def test_entry_is_little_endian_at_fixed_offset(self):
|
||||||
|
"""Смещение записи и порядок байтов — контракт с загрузчиком."""
|
||||||
|
e = empty()
|
||||||
|
e[3] = (2, 0x1500, 0x0480)
|
||||||
|
d = X.snd_pack(e, 9)
|
||||||
|
at = X.SND_HDR + 3 * X.SND_REC
|
||||||
|
self.assertEqual(d[at], 2)
|
||||||
|
self.assertEqual(d[at + 1:at + 3], b"\x00\x15") # off LE
|
||||||
|
self.assertEqual(d[at + 3:at + 5], b"\x80\x04") # len LE
|
||||||
|
self.assertEqual(struct.unpack_from("<BHH", d, at), (2, 0x1500, 0x0480))
|
||||||
|
|
||||||
|
def test_tail_is_zero_padded(self):
|
||||||
|
d = X.snd_pack(empty(), 1)
|
||||||
|
self.assertEqual(d[X.SND_HDR + X.SND_COUNT * X.SND_REC:], b"\0" * 211)
|
||||||
|
|
||||||
|
def test_roundtrip(self):
|
||||||
|
e = empty()
|
||||||
|
e[10] = (0, 128, 3584)
|
||||||
|
e[48] = (8, 0x1400, 128)
|
||||||
|
e[56] = (8, 0x2600, 4864)
|
||||||
|
d = X.snd_pack(e, 9)
|
||||||
|
back, pages = X.snd_unpack(d)
|
||||||
|
self.assertEqual(pages, 9)
|
||||||
|
self.assertEqual(back, e)
|
||||||
|
|
||||||
|
|
||||||
|
class SndRejects(unittest.TestCase):
|
||||||
|
"""Битый индекс обязан отвергаться, а не молча разъезжать раскладку."""
|
||||||
|
|
||||||
|
def bad(self, mutate):
|
||||||
|
e = empty()
|
||||||
|
e[1] = (0, 128, 256)
|
||||||
|
d = bytearray(X.snd_pack(e, 4))
|
||||||
|
mutate(d)
|
||||||
|
with self.assertRaises(X.IdxError):
|
||||||
|
X.snd_unpack(bytes(d))
|
||||||
|
|
||||||
|
def test_wrong_magic(self):
|
||||||
|
self.bad(lambda d: d.__setitem__(slice(0, 4), b"PSI2"))
|
||||||
|
|
||||||
|
def test_wrong_count(self):
|
||||||
|
self.bad(lambda d: d.__setitem__(4, 56))
|
||||||
|
|
||||||
|
def test_wrong_record_size(self):
|
||||||
|
self.bad(lambda d: d.__setitem__(5, 4))
|
||||||
|
|
||||||
|
def test_wrong_block_log2(self):
|
||||||
|
self.bad(lambda d: d.__setitem__(6, 8))
|
||||||
|
|
||||||
|
def test_unknown_pcm_format(self):
|
||||||
|
self.bad(lambda d: d.__setitem__(7, 2))
|
||||||
|
|
||||||
|
def test_pages_zero(self):
|
||||||
|
self.bad(lambda d: d.__setitem__(8, 0))
|
||||||
|
|
||||||
|
def test_pages_over_capacity(self):
|
||||||
|
self.bad(lambda d: d.__setitem__(8, X.SND_MAX_PAGES + 1))
|
||||||
|
|
||||||
|
def test_truncated(self):
|
||||||
|
e = empty()
|
||||||
|
d = X.snd_pack(e, 1)
|
||||||
|
with self.assertRaises(X.IdxError):
|
||||||
|
X.snd_unpack(d[:-1])
|
||||||
|
|
||||||
|
def test_page_beyond_set(self):
|
||||||
|
self.bad(lambda d: struct.pack_into("<BHH", d, X.SND_HDR + 5, 9, 0, 128))
|
||||||
|
|
||||||
|
def test_offset_not_block_aligned(self):
|
||||||
|
self.bad(lambda d: struct.pack_into("<BHH", d, X.SND_HDR + 5, 1, 100, 128))
|
||||||
|
|
||||||
|
def test_length_not_block_aligned(self):
|
||||||
|
self.bad(lambda d: struct.pack_into("<BHH", d, X.SND_HDR + 5, 1, 128, 100))
|
||||||
|
|
||||||
|
def test_entry_runs_past_last_page(self):
|
||||||
|
"""Эффект длиннее остатка набора увёл бы насос в чужую память."""
|
||||||
|
self.bad(lambda d: struct.pack_into("<BHH", d, X.SND_HDR + 5,
|
||||||
|
3, 0x3F80, 16384))
|
||||||
|
|
||||||
|
def test_entry_in_silence_block(self):
|
||||||
|
"""Первый блок страницы 0 занят тишиной насоса — он неприкосновенен."""
|
||||||
|
self.bad(lambda d: struct.pack_into("<BHH", d, X.SND_HDR + 5, 0, 0, 128))
|
||||||
|
|
||||||
|
def test_empty_entry_must_be_all_zero(self):
|
||||||
|
self.bad(lambda d: struct.pack_into("<BHH", d, X.SND_HDR + 5, 2, 256, 0))
|
||||||
|
|
||||||
|
|
||||||
|
class SndPackGuards(unittest.TestCase):
|
||||||
|
"""Упаковщик не имеет права выпустить набор, который отвергнет Z80."""
|
||||||
|
|
||||||
|
def test_rejects_too_many_pages(self):
|
||||||
|
with self.assertRaises(X.IdxError):
|
||||||
|
X.snd_pack(empty(), X.SND_MAX_PAGES + 1)
|
||||||
|
|
||||||
|
def test_rejects_wrong_entry_count(self):
|
||||||
|
with self.assertRaises(X.IdxError):
|
||||||
|
X.snd_pack([(0, 0, 0)] * 56, 1)
|
||||||
|
|
||||||
|
def test_rejects_entry_in_silence_block(self):
|
||||||
|
e = empty()
|
||||||
|
e[0] = (0, 0, 128)
|
||||||
|
with self.assertRaises(X.IdxError):
|
||||||
|
X.snd_pack(e, 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user