2349481b86
Часть 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
582 lines
34 KiB
Markdown
582 lines
34 KiB
Markdown
# Аудио без перелинковки: runtime-индексы музыки и эффектов
|
||
|
||
> Статус: **часть 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. Цель
|
||
|
||
Один и тот же `sprpop.exe` должен корректно работать с любым штатным набором
|
||
музыки (`flac`, `mp3`, `ogg`, `mt32`) без перекомпиляции и перелинковки.
|
||
Замена набора должна сводиться к замене файлов в `MUS/` вместе с описывающим
|
||
их индексом.
|
||
|
||
Сейчас это невозможно: упаковщик печатает зависящие от набора значения в
|
||
`gen/pop_music_tbl.h` и `gen/pop_music_ticks.h`, после чего они становятся
|
||
частью EXE. Если заменить только `MUS/mNN.bin`, загрузчик продолжает верить
|
||
размерам старого набора, а сценарии — его длительностям.
|
||
|
||
## 2. Подтверждённое текущее состояние
|
||
|
||
После преобразования в PCM наборы `flac`, `mp3` и `ogg` имеют одинаковые
|
||
длины. Исполнение MT-32 отличается. Значимые примеры:
|
||
|
||
| трек | FLAC/MP3/OGG | MT-32 |
|
||
|---|---:|---:|
|
||
| m32, конец уровня 4 | 438 тиков | 449 тиков |
|
||
| m41, обычный конец уровня | 732 | 685 |
|
||
| m50, принцесса ждёт | 831 | 867 |
|
||
| m53, реплика Джафара | 985 | 1044 |
|
||
| m56, финальная тема | 9865 блоков / 78 страниц | 10462 / 82 |
|
||
|
||
Все обычные треки каждого из четырёх наборов по-прежнему помещаются в
|
||
`POP_MUS_PAGES == 20`. Только m56 требует кольцевого проигрывателя.
|
||
`uint16_t` достаточно и для самого длинного MT-32-трека: 10462 блока.
|
||
|
||
Compile-time длительности используются не повсюду:
|
||
|
||
- `pop_music.c` читает из `pop_music_tbl.h` наличие, число страниц и блоков;
|
||
- `pop_intro.c` использует длительности m50 и m53 для шкалы PV-сцены;
|
||
- `pop_kid.c` использует длительности m32 и m41 для паузы конца уровня,
|
||
включая режим с выключенной музыкой;
|
||
- остальные ожидания уже опираются на `pop_music_busy()` и автоматически
|
||
заработают с правильным runtime-числом блоков.
|
||
|
||
## 3. Что должно остаться константами программы
|
||
|
||
В индекс переносятся только свойства конкретного набора. Инварианты формата
|
||
остаются в коде, а заголовок IDX лишь подтверждает их:
|
||
|
||
- PCM unsigned 8-bit mono;
|
||
- частота `CBL_FREQ_10K9`;
|
||
- один блок насоса — 128 байт;
|
||
- пространство оригинальных sound id — 0..56;
|
||
- `POP_MUS_PAGES == 20` для обычного загрузчика;
|
||
- размер кольца потокового проигрывателя.
|
||
|
||
Это важно: IDX не должен обещать программе другой формат PCM, который насос
|
||
физически не умеет воспроизводить.
|
||
|
||
## 4. Предлагаемый файл `MUS/MUSIC.IDX`
|
||
|
||
Предпочтителен отдельный индекс набора, а не заголовок в каждом треке:
|
||
|
||
- все сценарные длительности доступны сразу после старта;
|
||
- `mNN.bin` остаются простыми сырыми PCM-потоками;
|
||
- загрузчик трека меняется минимально;
|
||
- один индекс легко заменить вместе с набором;
|
||
- формат помещается в один 512-байтовый сектор DSS.
|
||
|
||
Предлагаемая версия `PMI1`:
|
||
|
||
```text
|
||
0..3 "PMI1" magic и версия
|
||
4 57 число плотных записей (id 0..56)
|
||
5 4 размер записи
|
||
6 7 log2 размера блока: 1 << 7 = 128
|
||
7 1 формат PCM: u8 mono, CBL_FREQ_10K9
|
||
8..15 0 резерв будущих версий
|
||
|
||
16.. 57 записей по 4 байта:
|
||
+0..1 uint16 blocks длина PCM в блоках по 128 байт
|
||
+2..3 uint16 ticks60 длительность ожидания в тиках оригинала
|
||
|
||
244..511 нулевой резерв до одного сектора
|
||
```
|
||
|
||
Нулевые `blocks` и `ticks60` означают, что трека с таким id в наборе нет.
|
||
|
||
`pages` в IDX не хранится: это производная величина, и её дублирование может
|
||
разойтись с `blocks`:
|
||
|
||
```c
|
||
pages = blocks / 128 + ((blocks & 127) != 0);
|
||
```
|
||
|
||
По умолчанию упаковщик вычисляет тики той же формулой, что сейчас:
|
||
|
||
```text
|
||
ticks60 = round(blocks * 40 / 57)
|
||
```
|
||
|
||
Хранить `ticks60` отдельно всё же полезно: в будущем момент окончания
|
||
сценарной реплики можно будет уточнить независимо от технического хвоста PCM.
|
||
Генератор обязан печатать предупреждение, если явно заданные тики заметно
|
||
отличаются от длительности файла.
|
||
|
||
## 5. Размещение индекса в памяти
|
||
|
||
Загружать таблицу в обычный изменяемый `static` нельзя. Данные банковых
|
||
модулей сейчас попадают в общий `_DATA`/W2, а свободная куча составляет около
|
||
238 байт. Даже таблица из 228 байт практически уничтожит этот запас.
|
||
|
||
Индекс следует держать в одной EMM-странице:
|
||
|
||
1. `pop_music_init()` выделяет блок из одной страницы;
|
||
2. загружает туда первые 512 байт `MUS\MUSIC.IDX`;
|
||
3. проверяет magic, версию, размеры полей и формат PCM;
|
||
4. готовит страницу для безопасного временного отображения в W0;
|
||
5. сохраняет в W2 только номер блока/страницы и флаг готовности.
|
||
|
||
Потеря 16 КБ EMM ради маленькой таблицы допустима: EMM у игры с запасом, а
|
||
W1/W2 — самый дефицитный ресурс. Не следует ради экономии страницы прятать
|
||
индекс в хвост `kid.ani` или звукового набора: это создаст ненужную связь
|
||
между независимыми ресурсами.
|
||
|
||
Доступ к записи предоставляет банковая функция наподобие:
|
||
|
||
```c
|
||
int8_t pop_music_info(uint8_t id, pop_music_info_t *out) __banked;
|
||
```
|
||
|
||
Она на короткое время отображает страницу IDX в W0, копирует четыре байта в
|
||
буфер вызывающего и сразу восстанавливает окно. Наружу указатель на EMM не
|
||
выдаётся.
|
||
|
||
Индекс живёт до выхода из программы. `pop_music_free()`, который вызывается
|
||
между сценами, освобождать его не должен; для полного завершения нужен
|
||
отдельный shutdown либо освобождение в общем маршруте выхода.
|
||
|
||
## 6. Изменения загрузчика музыки
|
||
|
||
`pop_music_load_begin(id)` должен получать из runtime-индекса:
|
||
|
||
- наличие трека;
|
||
- число блоков;
|
||
- вычисленное число EMM-страниц.
|
||
|
||
Далее существующая архитектура почти не меняется:
|
||
|
||
- `slot_blocks[]` уже хранит runtime-длину загруженного трека;
|
||
- `pop_mus_left` получает её при `play`/`stream`;
|
||
- насос сам останавливается на правильном блоке;
|
||
- `pop_music_busy()` автоматически отражает фактический конец;
|
||
- кольцевой проигрыватель получает правильные 82 страницы MT-32 m56 вместо
|
||
78 страниц FLAC.
|
||
|
||
При каждом открытии `mNN.bin` надо без дополнительного `open` проверить
|
||
фактический размер:
|
||
|
||
1. `lseek(fd, 0, SEEK_END)`;
|
||
2. убедиться, что размер положительный и кратен 128;
|
||
3. сравнить его с `blocks * 128` из IDX;
|
||
4. вернуть позицию через `lseek(fd, 0, SEEK_SET)`.
|
||
|
||
`lseek(SEEK_END)` уже реализован в libc поверх DSS `MOVE_FP $15` и
|
||
используется самой игрой для `POP.CFG`.
|
||
|
||
Рекомендуемая политика несовпадения:
|
||
|
||
- вывести диагностическое сообщение;
|
||
- считать фактический размер файла главным;
|
||
- пересчитать `blocks/pages/ticks60` в загруженной EMM-копии индекса;
|
||
- продолжить работу, если размер проходит ограничения.
|
||
|
||
Так случайно забытый старый IDX не приведёт к чтению чужой EMM-страницы или
|
||
обрыву трека. При этом штатная поставка обязана всегда включать согласованные
|
||
IDX и PCM.
|
||
|
||
## 7. Runtime-тики без 32-битного переполнения
|
||
|
||
Если тики приходится восстанавливать из фактического размера, прямое
|
||
`blocks * 40` может переполнить `uint16_t`. Та же формула считается только
|
||
16-битной арифметикой:
|
||
|
||
```c
|
||
ticks = (blocks / 57) * 40
|
||
+ ((blocks % 57) * 40 + 28) / 57;
|
||
```
|
||
|
||
Даже при `blocks == 65535` промежуточные значения остаются в `uint16_t`.
|
||
Это холодный путь, поэтому небольшая цена деления допустима.
|
||
|
||
## 8. Перевод сценариев с compile-time на runtime
|
||
|
||
### PV-сцена (`pop_intro.c`)
|
||
|
||
Текущий большой `enum` смешивает два класса величин:
|
||
|
||
- неизменные интервалы сценария;
|
||
- абсолютные точки, сдвигаемые длительностями m50 и m53.
|
||
|
||
Надо сохранить относительные сценарные константы, а при входе в PV-сцену
|
||
один раз собрать локальную структуру `pv_timing_t`:
|
||
|
||
```text
|
||
m50_end = MUS_2_START + ticks(m50)
|
||
wait_end = m50_end + 40
|
||
...
|
||
dialog1_start = предыдущая фиксированная цепочка
|
||
exit_start = dialog1_start + ticks(m53)
|
||
anim_end = exit_start + 469
|
||
```
|
||
|
||
Структуру лучше держать на стеке `intro_pv_animated()` и передавать нужным
|
||
helper-функциям указателем. File-scope изменяемая таблица снова попала бы в
|
||
W2.
|
||
|
||
m50 уже загружен до начала PV. m53 начинает подгружаться задолго до своего
|
||
старта, поэтому метаданные обоих треков к моменту использования доступны.
|
||
|
||
### Конец уровня (`pop_kid.c`)
|
||
|
||
Вместо `POP_MUS_TICKS_32/41` редкое событие окончания уровня вызывает
|
||
банковый accessor IDX и вычисляет `pop_endmus_left` из runtime-тиков.
|
||
|
||
Индекс используется даже при выключенной музыке: оригинал выдерживает эту
|
||
паузу молча. Поэтому нельзя заменять её одним `pop_music_busy()`.
|
||
|
||
## 9. Генератор и сборка
|
||
|
||
`tools/pop_pack_music.py` должен:
|
||
|
||
1. как сейчас преобразовать выбранный набор в `mNN.bin`;
|
||
2. проверить кратность каждого результата 128 байтам;
|
||
3. собрать плотные записи 0..56;
|
||
4. записать `MUS/MUSIC.IDX` в формате `PMI1`;
|
||
5. больше не генерировать `pop_music_tbl.h` и `pop_music_ticks.h`.
|
||
|
||
Makefile должен:
|
||
|
||
- добавить `MUS/MUSIC.IDX` в `DISK` и staging;
|
||
- убрать музыкальные generated-header'ы из `GEN_H`;
|
||
- убрать их из зависимостей EXE;
|
||
- оставить `MUSIC_FMT` зависимостью только музыкальных ресурсов;
|
||
- гарантировать, что `make music-flac/mp3/ogg/mt32` заменяет и PCM, и IDX,
|
||
но не перелинковывает EXE.
|
||
|
||
Критерий архитектуры: SHA/дата `sprpop.exe` не меняется при переключении
|
||
между четырьмя музыкальными целями.
|
||
|
||
## 10. Ошибки и совместимость
|
||
|
||
Игра не должна падать из-за необязательной музыки:
|
||
|
||
- нет IDX — музыка отключена, игра продолжает работать;
|
||
- неверная magic/версия/формат — музыка отключена с диагностикой;
|
||
- записи нет — конкретный трек считается отсутствующим;
|
||
- файла нет — запрос трека завершается молча/с диагностикой, как сейчас;
|
||
- обычный трек требует больше 20 страниц — не загружать обычным путём;
|
||
- потоковый трек имеет больше 255 страниц — отвергнуть, потому что текущие
|
||
счётчики страниц восьмибитные;
|
||
- IDX и файл расходятся — применить политику §6;
|
||
- все файловые операции выполнять через `POP_PATH_CALL`, чтобы сохранить
|
||
работу на старых DSS с повреждением текущего каталога.
|
||
|
||
Для отсутствующего/повреждённого IDX остаётся выбрать поведение немой паузы
|
||
конца уровня: нулевая пауза либо небольшой канонический fallback. Это не
|
||
мешает основной архитектуре, но решение надо принять до реализации.
|
||
|
||
## 11. Этапы реализации
|
||
|
||
### MI0 — формат и host-тест
|
||
|
||
- вынести writer/reader `PMI1` в тестируемый код упаковщика;
|
||
- проверить magic, размеры, LE-поля, нулевые записи и padding;
|
||
- для всех четырёх наборов сверить `blocks/ticks` с фактическими PCM;
|
||
- зафиксировать тестом значения MT-32 m50/m53/m56 как отличающиеся от FLAC.
|
||
|
||
### MI1 — runtime-загрузка IDX
|
||
|
||
- `pop_music_init()`/shutdown;
|
||
- одна EMM-страница, загрузка и валидация;
|
||
- accessor одной записи;
|
||
- отказ без порчи W0/W3, EMM и файловых дескрипторов.
|
||
|
||
### MI2 — loader/pump
|
||
|
||
- заменить `pop_mus_tbl[]` runtime-записью;
|
||
- проверять реальный размер файла;
|
||
- обычный, немедленный и кольцевой пути должны использовать одну метаинформацию;
|
||
- удалить `pop_music_tbl.h`.
|
||
|
||
### MI3 — runtime-шкала сцен
|
||
|
||
- перевести m50/m53 в `pop_intro.c` на локальную runtime-шкалу;
|
||
- перевести m32/m41 в `pop_kid.c`;
|
||
- удалить `pop_music_ticks.h`.
|
||
|
||
### MI4 — сборка и образы
|
||
|
||
- добавить IDX в каждый музыкальный набор;
|
||
- исключить музыку из зависимостей линковки;
|
||
- собрать четыре HDD-варианта с одним EXE.
|
||
|
||
### MI5 — приёмка в MAME/на железе
|
||
|
||
- FLAC: title -> story -> PV, обычный конец уровня, конец уровня 4, ending;
|
||
- MT-32: те же маршруты, особенно m50/m53 и полный m56;
|
||
- музыка выключена: пауза конца уровня берётся из IDX;
|
||
- замена набора без пересборки EXE;
|
||
- повреждённый IDX, отсутствующий трек, несовпадающий размер;
|
||
- контроль, что не запущено более одного MAME и предыдущий экземпляр закрыт
|
||
перед новым прогоном.
|
||
|
||
## 12. Критерии готовности
|
||
|
||
- один бинарник запускается со всеми четырьмя наборами;
|
||
- ни один музыкальный generated-header не входит в сборку C;
|
||
- начало следующей сцены/уровня соответствует фактической записи;
|
||
- MT-32 m56 проигрывает все 82 страницы и не обрывается как FLAC-вариант;
|
||
- обычные треки не читают за пределами выделенного EMM-блока;
|
||
- IDX не расходует сотни байт W2;
|
||
- ошибочный набор отключает музыку безопасно;
|
||
- переключение `music-*` не меняет EXE;
|
||
- старые DSS продолжают работать через восстановление каталога приложения.
|
||
|
||
---
|
||
|
||
# Часть II. Звуковые эффекты без перелинковки
|
||
|
||
## 13. Цель и отличие от музыки
|
||
|
||
Один и тот же `sprpop.exe` должен работать с разными наборами PCM-эффектов,
|
||
если в них сохранена исходная нумерация sound id 0..56. Замена набора должна
|
||
сводиться к замене согласованной пары `SND/snd.idx` + `SND/snd.arc`, без
|
||
генерации C-заголовка и перелинковки EXE.
|
||
|
||
Набор состоит из двух согласованных файлов:
|
||
|
||
- `SND/snd.arc` — обычный архив PBA1 с PCM-страницами;
|
||
- `SND/snd.idx` — описание раскладки эффектов внутри этих страниц.
|
||
|
||
Отдельный IDX предпочтительнее расширения заголовка `snd.arc`: PBA1 остаётся
|
||
универсальным и не получает специального варианта только для звука, а
|
||
формат индекса можно независимо версионировать и проверять тем же способом,
|
||
что будущий `MUS/MUSIC.IDX`.
|
||
|
||
Существующий формат записи менять не требуется:
|
||
|
||
```c
|
||
typedef struct {
|
||
uint8_t page;
|
||
uint16_t off;
|
||
uint16_t len;
|
||
} pop_snd_ent_t;
|
||
```
|
||
|
||
Это ровно необходимые проигрывателю номер логической страницы, смещение в
|
||
ней и длина PCM. В текущем ABI SDCC/z80 запись занимает 5 байт; дисковый
|
||
формат обязан описывать эти пять байт явно (`uint16` little-endian), а код
|
||
должен проверять `sizeof(pop_snd_ent_t) == 5` на этапе сборки.
|
||
|
||
## 14. Подтверждённое текущее состояние
|
||
|
||
Сейчас `tools/pop_pack_sound.py` печатает раскладку в
|
||
`gen/pop_sound_tbl.h`, а `pop_sfx.c` включает её как
|
||
`static const pop_snd_tbl[57]`. Поэтому конкретные `page/off/len` становятся
|
||
частью EXE.
|
||
|
||
В текущей сборке таблица занимает 285 байт в `_CODE`, по адресам
|
||
`0x52D8..0x53F4`. Это общий резидентный диапазон игры: при huge-модели
|
||
`_CODE`, `_DATA`, heap и stack совместно используют плоские 32 КБ
|
||
`0x4000..0xBFFF`; граница W1/W2 отдельного бюджета здесь не создаёт.
|
||
|
||
Проверены оба имеющихся исходных набора DIGISND:
|
||
|
||
| набор | PCM-эффектов | страниц после упаковки | отличия раскладки |
|
||
|---|---:|---:|---:|
|
||
| MSDOS 1.3/1.4 | 31 из 57 | 10 | эталон текущей сборки |
|
||
| SDLPoP 1.0/1.1 | 31 из 57 | 9 | отличаются 8 id |
|
||
|
||
Максимальный эффект занимает 17 664 байта, или 138 блоков по 128 байт.
|
||
У SDLPoP эффект 48 содержит только 7 исходных сэмплов; это свойство самого
|
||
набора, а не ошибка runtime-индекса.
|
||
|
||
## 15. Файл `SND/snd.idx`
|
||
|
||
IDX занимает один 512-байтовый сектор и после короткого заголовка является
|
||
точным дисковым дампом 57 записей `pop_snd_tbl`:
|
||
|
||
```text
|
||
0..3 "PSI1" magic и версия индекса эффектов
|
||
4 57 число плотных записей, id 0..56
|
||
5 5 размер записи
|
||
6 7 log2 размера блока: 1 << 7 = 128
|
||
7 1 формат: unsigned 8-bit mono, CBL_FREQ_10K9
|
||
8 pages фактическое число PCM-страниц в snd.arc
|
||
9..15 0 резерв
|
||
|
||
16..300 57 записей по 5 байт:
|
||
+0 uint8 page
|
||
+1..2 uint16 off, little-endian
|
||
+3..4 uint16 len, little-endian
|
||
|
||
301..511 0 резерв до одного сектора
|
||
```
|
||
|
||
`pages` — фактический размер конкретного набора, например 10 для MSDOS или
|
||
9 для SDLPoP. Его не следует называть или трактовать как
|
||
`POP_SND_MAX_PAGES`: максимум 16 является compile-time-вместимостью
|
||
загрузчика, а IDX сообщает число реально нужных страниц и обязан укладываться
|
||
в этот предел.
|
||
|
||
Загрузчик сверяет `pages` с числом элементов внешнего PBA1 и не доверяет
|
||
расходящимся файлам. `off` и `len` обязаны быть кратны 128; нулевая длина
|
||
означает отсутствие PCM для данного id.
|
||
|
||
IDX загружается прямо в резидентный `pop_snd_tbl`, поэтому отдельной страницы
|
||
EMM для него не требуется. Цена решения — одно дополнительное открытие и
|
||
чтение 512 байт при старте звука; на фоне загрузки 9–10 страниц PCM это
|
||
приемлемый холодный расход.
|
||
|
||
## 16. Размещение runtime-таблицы в памяти
|
||
|
||
`pop_snd_tbl` становится обычным изменяемым внутренним объектом:
|
||
|
||
```c
|
||
pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT];
|
||
```
|
||
|
||
Его следует определить в отдельном internal data-модуле и объявить через
|
||
`extern` в `_pop_sfx.h`, потому что таблицу заполняет холодный загрузчик из
|
||
банка 8, а читает резидентный `pop_sfx.c`.
|
||
|
||
После снятия `const` SDCC перенесёт эти 285 байт из `_CODE` в `_DATA`. Это
|
||
не добавляет 285 байт к общему резидентному расходу: `_CODE` одновременно
|
||
уменьшается на тот же размер, а `_CODE` и `_DATA` последовательно лежат в
|
||
одном диапазоне `0x4000..0xBFFF`. Возможна лишь небольшая разница из-за
|
||
выравнивания, которую надо проверить итоговой map-картой и size-check.
|
||
|
||
Банк 8 может читать индекс напрямую в этот буфер: весь диапазон
|
||
`0x4000..0xBFFF` остаётся доступен, пока банковый код исполняется в W3.
|
||
Горячие обращения `pop_snd_tbl[id].page/off/len` и ISR не требуют новых
|
||
маппингов или accessor-функций.
|
||
|
||
## 17. Фактическое число страниц
|
||
|
||
Инвариантами программы остаются:
|
||
|
||
```c
|
||
#define POP_SND_MAX_PAGES 16
|
||
#define POP_SND_COUNT 57
|
||
#define POP_SND_BLOCK 128
|
||
```
|
||
|
||
`POP_SND_PAGES == 10` больше не должен означать размер конкретного набора.
|
||
Вместо него появляется runtime-состояние:
|
||
|
||
```c
|
||
uint8_t pop_snd_pages;
|
||
uint8_t pop_snd_page[POP_SND_MAX_PAGES];
|
||
```
|
||
|
||
Загрузчик выделяет `mem_alloc_pages(pop_snd_pages)` и заполняет только
|
||
фактическое число элементов массива. MSDOS-набор займёт 10 страниц,
|
||
SDLPoP-набор — 9. Предел 16 оставляет запас будущим наборам без
|
||
перекомпиляции и увеличивает постоянный массив лишь на 6 байт относительно
|
||
текущего; ещё один байт занимает `pop_snd_pages`.
|
||
|
||
Насос при переходе длинного эффекта через границу страницы сравнивает
|
||
`sfx_pg + 1` с `pop_snd_pages`, а не с compile-time-константой.
|
||
|
||
## 18. Загрузка и валидация
|
||
|
||
`pop_sfx_init()` должен выполнять следующую последовательность:
|
||
|
||
1. открыть `SND/snd.idx` через существующий путь `POP_PATH_CALL`;
|
||
2. прочитать 16-байтовый заголовок, затем 285 байт записей прямо в
|
||
`pop_snd_tbl`, и проверить полный размер IDX;
|
||
3. проверить magic, версию, число/размер записей, формат и размер блока;
|
||
4. получить из IDX фактическое число страниц и проверить диапазон 1..16;
|
||
5. открыть `SND/snd.arc` через `POP_PATH_CALL`;
|
||
6. прочитать обычную таблицу PBA1 с ёмкостью `POP_SND_MAX_PAGES`;
|
||
7. сверить `PSI1.pages` с числом элементов PBA1;
|
||
8. проверить каждую ненулевую запись;
|
||
9. только после этого выделить фактическое число EMM-страниц и загрузить их.
|
||
|
||
Для каждой записи проверяются:
|
||
|
||
- `page < pop_snd_pages`;
|
||
- `off < 16384` и `off % POP_SND_BLOCK == 0`;
|
||
- `len % POP_SND_BLOCK == 0`;
|
||
- диапазон `page/off/len` не выходит за загруженный набор;
|
||
- переход длинного эффекта на следующие страницы не превышает
|
||
`pop_snd_pages`.
|
||
|
||
При любой ошибке блок EMM и файловый дескриптор освобождаются, таблица не
|
||
используется, эффекты остаются выключенными. Набор без `snd.idx` считается
|
||
неполным; хранить в EXE старую таблицу как fallback не следует, иначе
|
||
зависимость бинарника от конкретного набора останется.
|
||
|
||
## 19. Генератор и сборка
|
||
|
||
`tools/pop_pack_sound.py` должен:
|
||
|
||
1. как сейчас разобрать DIGISND, привести PCM к CBL_FREQ_10K9 и выровнять
|
||
начала/длины на 128 байт;
|
||
2. вычислить фактическое число страниц и отвергнуть набор больше 16;
|
||
3. сформировать плотные 57 записей `page/off/len`;
|
||
4. записать `SND/snd.idx` в формате `PSI1`, включая фактическое число
|
||
страниц;
|
||
5. собрать обычный `SND/snd.arc` из полученных страниц;
|
||
6. больше не генерировать `gen/pop_sound_tbl.h`.
|
||
|
||
Чтобы число страниц не было захардкожено списком `s0.bin`..`s9.bin` в
|
||
Makefile, упаковщик звука предпочтительно должен сразу формировать
|
||
`assets/packed/SND/snd.arc` либо передавать упаковщику архивов динамический
|
||
список результатов без сохранения устаревшей десятой страницы.
|
||
|
||
Стабильные тип и константы переносятся в обычный internal-заголовок. EXE не
|
||
должен зависеть от результата упаковки звука: цель выбора набора меняет
|
||
согласованную пару `snd.idx` + `snd.arc`.
|
||
|
||
## 20. Этапы реализации эффектов
|
||
|
||
### SI0 — формат и host-тест
|
||
|
||
- writer/reader отдельного `snd.idx` в формате `PSI1`;
|
||
- проверка точного пятибайтового LE-формата записи;
|
||
- сборка и разбор обоих имеющихся DIGISND-наборов;
|
||
- проверки 10 страниц MSDOS, 9 страниц SDLPoP и отличающихся записей.
|
||
|
||
### SI1 — runtime-таблица
|
||
|
||
- изменяемый `pop_snd_tbl` в общем резидентном CODE/DATA-диапазоне;
|
||
- загрузка и полная валидация `PSI1`;
|
||
- удаление generated `pop_sound_tbl.h`;
|
||
- проверка map-карты: перенос CODE -> DATA не должен съесть heap.
|
||
|
||
### SI2 — гибкое выделение страниц
|
||
|
||
- `POP_SND_MAX_PAGES == 16` и runtime `pop_snd_pages`;
|
||
- фактический размер стека таблицы PBA1;
|
||
- выделение/освобождение 9, 10 и тестовых 16 страниц;
|
||
- runtime-гард перехода длинного эффекта между страницами.
|
||
|
||
### SI3 — сборка без перелинковки
|
||
|
||
- убрать фиксированный `SND_ATL` из десяти имён;
|
||
- сделать `snd.idx` и `snd.arc` согласованными результатами выбора набора;
|
||
- проверить неизменность SHA/даты `sprpop.exe` при смене набора.
|
||
|
||
### SI4 — приёмка
|
||
|
||
- проиграть короткий, обычный и переходящий страницу эффекты;
|
||
- проверить приоритеты и перебивание звуков;
|
||
- проверить музыку поверх общего CBL после runtime-загрузки эффектов;
|
||
- отсутствующий IDX и повреждённые magic/count/pages/page/off/len должны
|
||
безопасно отключать эффекты;
|
||
- перед каждым MAME-прогоном завершать предыдущий экземпляр и никогда не
|
||
запускать две копии одновременно.
|
||
|
||
## 21. Критерии готовности эффектов
|
||
|
||
- один `sprpop.exe` работает с MSDOS- и SDLPoP-наборами;
|
||
- замена согласованной пары `SND/snd.idx` + `SND/snd.arc` не требует
|
||
компиляции C и не меняет EXE;
|
||
- `pop_snd_tbl` больше не генерируется как C-код;
|
||
- загружается фактическое число страниц в диапазоне 1..16;
|
||
- таблица не расходует дополнительную страницу EMM и не увеличивает
|
||
суммарный резидентный CODE+DATA на свои 285 байт;
|
||
- горячий путь и ISR используют прежние прямые `page/off/len`;
|
||
- повреждённый индекс не приводит к чтению за пределами EMM-блока;
|
||
- файловые операции сохраняют совместимость со старыми DSS через
|
||
восстановление каталога приложения.
|