Добавить общий каталог назначения для HDD и удалить локальную копию упаковщика.\n\nУбрать устаревшие generated-имена ресурсов, выводить число страниц Kid из kid.arc и ограничить звуковую таблицу горячим модулем.\n\nЗафиксировать планы runtime-индексов музыки и PCM-эффектов.
33 KiB
Аудио без перелинковки: runtime-индексы музыки и эффектов
Статус: план будущего улучшения, 2026-08-30. Код пока не менялся.
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:
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:
pages = blocks / 128 + ((blocks & 127) != 0);
По умолчанию упаковщик вычисляет тики той же формулой, что сейчас:
ticks60 = round(blocks * 40 / 57)
Хранить ticks60 отдельно всё же полезно: в будущем момент окончания
сценарной реплики можно будет уточнить независимо от технического хвоста PCM.
Генератор обязан печатать предупреждение, если явно заданные тики заметно
отличаются от длительности файла.
5. Размещение индекса в памяти
Загружать таблицу в обычный изменяемый static нельзя. Данные банковых
модулей сейчас попадают в общий _DATA/W2, а свободная куча составляет около
238 байт. Даже таблица из 228 байт практически уничтожит этот запас.
Индекс следует держать в одной EMM-странице:
pop_music_init()выделяет блок из одной страницы;- загружает туда первые 512 байт
MUS\MUSIC.IDX; - проверяет magic, версию, размеры полей и формат PCM;
- готовит страницу для безопасного временного отображения в W0;
- сохраняет в W2 только номер блока/страницы и флаг готовности.
Потеря 16 КБ EMM ради маленькой таблицы допустима: EMM у игры с запасом, а
W1/W2 — самый дефицитный ресурс. Не следует ради экономии страницы прятать
индекс в хвост kid.ani или звукового набора: это создаст ненужную связь
между независимыми ресурсами.
Доступ к записи предоставляет банковая функция наподобие:
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 проверить
фактический размер:
lseek(fd, 0, SEEK_END);- убедиться, что размер положительный и кратен 128;
- сравнить его с
blocks * 128из IDX; - вернуть позицию через
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-битной арифметикой:
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:
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 должен:
- как сейчас преобразовать выбранный набор в
mNN.bin; - проверить кратность каждого результата 128 байтам;
- собрать плотные записи 0..56;
- записать
MUS/MUSIC.IDXв форматеPMI1; - больше не генерировать
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/SOUND.IDX + SND/SND.ARC, без
генерации C-заголовка и перелинковки EXE.
Набор состоит из двух согласованных файлов:
SND/SND.ARC— обычный архив PBA1 с PCM-страницами;SND/SOUND.IDX— описание раскладки эффектов внутри этих страниц.
Отдельный IDX предпочтительнее расширения заголовка snd.arc: PBA1 остаётся
универсальным и не получает специального варианта только для звука, а
формат индекса можно независимо версионировать и проверять тем же способом,
что будущий MUS/MUSIC.IDX.
Существующий формат записи менять не требуется:
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/SOUND.IDX
IDX занимает один 512-байтовый сектор и после короткого заголовка является
точным дисковым дампом 57 записей pop_snd_tbl:
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 становится обычным изменяемым внутренним объектом:
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. Фактическое число страниц
Инвариантами программы остаются:
#define POP_SND_MAX_PAGES 16
#define POP_SND_COUNT 57
#define POP_SND_BLOCK 128
POP_SND_PAGES == 10 больше не должен означать размер конкретного набора.
Вместо него появляется runtime-состояние:
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() должен выполнять следующую последовательность:
- открыть
SND/SOUND.IDXчерез существующий путьPOP_PATH_CALL; - прочитать 16-байтовый заголовок, затем 285 байт записей прямо в
pop_snd_tbl, и проверить полный размер IDX; - проверить magic, версию, число/размер записей, формат и размер блока;
- получить из IDX фактическое число страниц и проверить диапазон 1..16;
- открыть
SND/SND.ARCчерезPOP_PATH_CALL; - прочитать обычную таблицу PBA1 с ёмкостью
POP_SND_MAX_PAGES; - сверить
PSI1.pagesс числом элементов PBA1; - проверить каждую ненулевую запись;
- только после этого выделить фактическое число EMM-страниц и загрузить их.
Для каждой записи проверяются:
page < pop_snd_pages;off < 16384иoff % POP_SND_BLOCK == 0;len % POP_SND_BLOCK == 0;- диапазон
page/off/lenне выходит за загруженный набор; - переход длинного эффекта на следующие страницы не превышает
pop_snd_pages.
При любой ошибке блок EMM и файловый дескриптор освобождаются, таблица не
используется, эффекты остаются выключенными. Набор без SOUND.IDX считается
неполным; хранить в EXE старую таблицу как fallback не следует, иначе
зависимость бинарника от конкретного набора останется.
19. Генератор и сборка
tools/pop_pack_sound.py должен:
- как сейчас разобрать DIGISND, привести PCM к CBL_FREQ_10K9 и выровнять начала/длины на 128 байт;
- вычислить фактическое число страниц и отвергнуть набор больше 16;
- сформировать плотные 57 записей
page/off/len; - записать
SND/SOUND.IDXв форматеPSI1, включая фактическое число страниц; - собрать обычный
SND/SND.ARCиз полученных страниц; - больше не генерировать
gen/pop_sound_tbl.h.
Чтобы число страниц не было захардкожено списком s0.bin..s9.bin в
Makefile, упаковщик звука предпочтительно должен сразу формировать
assets/packed/SND/snd.arc либо передавать упаковщику архивов динамический
список результатов без сохранения устаревшей десятой страницы.
Стабильные тип и константы переносятся в обычный internal-заголовок. EXE не
должен зависеть от результата упаковки звука: цель выбора набора меняет
согласованную пару SOUND.IDX + SND.ARC.
20. Этапы реализации эффектов
SI0 — формат и host-тест
- writer/reader отдельного
SOUND.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и runtimepop_snd_pages;- фактический размер стека таблицы PBA1;
- выделение/освобождение 9, 10 и тестовых 16 страниц;
- runtime-гард перехода длинного эффекта между страницами.
SI3 — сборка без перелинковки
- убрать фиксированный
SND_ATLиз десяти имён; - сделать
SOUND.IDXиSND.ARCсогласованными результатами выбора набора; - проверить неизменность SHA/даты
sprpop.exeпри смене набора.
SI4 — приёмка
- проиграть короткий, обычный и переходящий страницу эффекты;
- проверить приоритеты и перебивание звуков;
- проверить музыку поверх общего CBL после runtime-загрузки эффектов;
- отсутствующий IDX и повреждённые magic/count/pages/page/off/len должны безопасно отключать эффекты;
- перед каждым MAME-прогоном завершать предыдущий экземпляр и никогда не запускать две копии одновременно.
21. Критерии готовности эффектов
- один
sprpop.exeработает с MSDOS- и SDLPoP-наборами; - замена согласованной пары
SND/SOUND.IDX+SND/SND.ARCне требует компиляции C и не меняет EXE; pop_snd_tblбольше не генерируется как C-код;- загружается фактическое число страниц в диапазоне 1..16;
- таблица не расходует дополнительную страницу EMM и не увеличивает суммарный резидентный CODE+DATA на свои 285 байт;
- горячий путь и ISR используют прежние прямые
page/off/len; - повреждённый индекс не приводит к чтению за пределами EMM-блока;
- файловые операции сохраняют совместимость со старыми DSS через восстановление каталога приложения.