df5071a967
Часть I плана music_runtime_index_plan.md (MI0..MI5). gen/pop_music_tbl.h и gen/pop_music_ticks.h УДАЛЕНЫ: длины треков и длительности реплик читаются из MUS/mus.idx (формат PMI1, tools/pop_idx.py, тесты в make test-tools). Один и тот же sprpop.exe работает с любым из четырёх наборов записей — sha256 бинарника при смене MUSIC_FMT не меняется. ГДЕ ЖИВЁТ ИНДЕКС. 228 байт таблицы в W2 не положить (свободной кучи там порядка двух сотен), поэтому индекс лежит в одной странице EMM, а в резиденте от него два байта. Данные в странице — со смещения 0x100: gfx_w0_page_prepare пишет в неё стабы прерываний (0x38 и 0x66), и с нуля они попали бы прямо в записи id 10 и 21. Со смещением работает штатная защита, а не запрет прерываний (тот же приём, что CFG_BASE в pop_config.c). Число страниц в индексе не хранится — считается из blocks, чтобы не разъехалось. ПАУЗА КОНЦА УРОВНЯ — СОСТОЯНИЕМ, А НЕ СЧЁТЧИКОМ. pop_endmus_left и POP_MUS_TICKS_32/41 удалены; главный цикл ждёт pop_music_active() — «заявка лежит, идёт загрузка или трек звучит». Одного busy мало: между заявкой и первой нотой 190-230 мс (замер в sound_plan §9). Прежний счётчик закрывал эту щель ценой зависимости EXE от набора и жёсткого делителя /4, который врал в режимах FAST/FASTEST (там логический кадр 3 кадра луча, а не 4). Побочно исправилось расхождение с SDLPoP: при выключенном звуке заявка не кладётся, и уровень меняется сразу, как в оригинале (seg006:651 + seg003:387) — раньше игра держала пройденный уровень лишние 12 секунд в тишине. PV-СЦЕНА — на четырёх якорях (8 байт статики), которые считаются из индекса при входе в сцену; прежние выражения шкалы не изменились. План предлагал протащить структуру времён через пять функций — для сцены, которая идёт раз за запуск, это того не стоит. ПАМЯТЬ. За обе фазы резидент не вырос, а освободился: _CODE 23865 -> 23544, куча 239 -> 256 Б. Банк 9 похудел на 118 Б (ушла pop_mus_tbl из rodata), банк 11 — на длительности реплик. ПРОВЕРЕНО В MAME: exe побайтово одинаков для flac и mt32; все 22 трека в индексах различаются, и контрольные значения совпали с предсказанными планом (m41 732->685, m50 831->867, m53 985->1044, m56 9865->10462 блоков, 78->82 страницы); на mt32 PV-сцена проходит целиком по его длительностям; без mus.idx музыки нет, эффекты работают, игра проходима. НЕ ПРОВЕРЕНО: потоковый m56 на 82 страницах — до финала надо дойти в игре. Единственный оставшийся пункт приёмки, отмечен в sound_plan §11.5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
151 lines
11 KiB
C
151 lines
11 KiB
C
/*
|
||
* pop_music.h — музыкальный трек через тот же CBL, что и эффекты.
|
||
*
|
||
* ПОЧЕМУ PCM, А НЕ AY. Оцифровки музыки в игре нет вовсе: digisnd*.dat
|
||
* содержит только эффекты, музыка лежит MIDI под Adlib. Зато есть готовые
|
||
* записи DOS-версии, и путь «запись -> сырой поток CBL» не требует ни
|
||
* синтезатора на хосте, ни аранжировки под три канала AY (разбор вариантов
|
||
* — ../docs/sound_plan.md §1). Плата известна: CBL один, поэтому пока
|
||
* играет музыка, эффектов нет. Оригинал ведёт себя так же — play_sound
|
||
* там глушит предыдущий звук.
|
||
*
|
||
* ГДЕ УМЕСТНО. Заставки и титры: эффектов там не бывает, картинка
|
||
* статична, и загрузка сотни-другой килобайт с диска никому не мешает.
|
||
* Музыка ПО ХОДУ игры (джинглы смерти, зелья) — отдельный вопрос, её
|
||
* придётся либо гасить эффектами, либо уводить на AY.
|
||
*
|
||
* ФОРМАТ на диске — один файл MUS\m<id>.bin на трек, читается порциями по
|
||
* 16 КБ из одного открытого fd (bank_read_page). Каталог с длинами
|
||
* генерирует toolchain/pop_pack_music.py в pop_music_tbl.h.
|
||
*/
|
||
#ifndef POP_MUSIC_H
|
||
#define POP_MUSIC_H
|
||
|
||
#include <stdint.h>
|
||
|
||
/* Сколько страниц EMM держим под трек. Двадцать = 320 КБ, это все
|
||
* заставочные треки, кроме финального «won» (115 с, 1,2 МБ): тот придётся
|
||
* либо резать, либо стримить, и это отдельная задача. */
|
||
#define POP_MUS_PAGES 20
|
||
|
||
/* --- ИНДЕКС НАБОРА (MUS/mus.idx, формат PMI1) ------------------------- *
|
||
*
|
||
* Длины треков и длительности реплик приезжают С ДИСКА, а не из EXE: так
|
||
* набор записей (flac/mp3/ogg/mt32) меняется без перекомпиляции игры.
|
||
* Формат пишет tools/pop_idx.py, он же покрыт make test-tools.
|
||
*
|
||
* Индекс живёт в ОДНОЙ странице EMM, а не в W2: таблица на 57 записей —
|
||
* 228 байт, а свободной кучи в резиденте порядка двух сотен. Наружу
|
||
* указатель на EMM не выдаётся: pop_music_info копирует запись в буфер
|
||
* вызывающего, на короткое время подключая страницу в W0. */
|
||
#define POP_MUS_COUNT 57 /* пространство id оригинала, 0..56 */
|
||
#define POP_MUS_IDX_REC 4 /* blocks (u16) + ticks60 (u16) */
|
||
#define POP_MUS_IDX_HDR 16
|
||
#define POP_MUS_IDX_SIZE 512 /* сектор DSS */
|
||
/* Индекс кладётся в страницу СО СМЕЩЕНИЯ 0x100, а не с нуля. Причина в
|
||
* gfx_w0_page_prepare: он делает страницу безопасной в W0, записывая в
|
||
* неё переход на IRQ-восстановитель (0x38) и RETN для NMI (0x66). С
|
||
* нуля эти байты легли бы прямо в записи (id 10 и 21) и испортили
|
||
* индекс; со смещением 0x100 стабы и данные не пересекаются, и работает
|
||
* штатная защита от прерываний, а не запрет их. Тот же приём и по той
|
||
* же причине — в pop_config.c (CFG_BASE). */
|
||
#define POP_MUS_IDX_BASE 0x100
|
||
|
||
typedef struct {
|
||
uint16_t blocks; /* длина трека в порциях насоса по 128 байт */
|
||
uint16_t ticks; /* та же длина в тиках сцены (60 Гц оригинала) */
|
||
} pop_mus_info_t;
|
||
|
||
/* Поднять индекс (один раз за запуск). 0 — ок, -1 — нет файла либо он
|
||
* не той версии; во втором случае музыки не будет, но игра работает. */
|
||
int8_t pop_music_index_load(void) __banked;
|
||
|
||
/* Запись трека в буфер вызывающего. -1 — индекса нет, id вне диапазона
|
||
* или трека нет в наборе (тогда поля нулевые). */
|
||
int8_t pop_music_info(uint8_t id, pop_mus_info_t *out) __banked;
|
||
|
||
/* Сколько EMM-страниц занимает трек такой длины. Считается, а НЕ хранится
|
||
* в индексе: дублирование однажды разъедется с blocks. */
|
||
#define pop_music_pages(blocks) \
|
||
(uint16_t)(((blocks) >> 7) + (((blocks) & 127) != 0))
|
||
|
||
/* Номера треков — нумерация оригинала (SDLPoP data/music/names.txt). */
|
||
#define POP_MUS_INTRO 54 /* тема заставки, звучит на титрах */
|
||
#define POP_MUS_STORY_1 55 /* «In the absence of the Sultan...» */
|
||
#define POP_MUS_STORY_2 50 /* принцесса ждёт */
|
||
#define POP_MUS_STORY_3 53 /* входит Джафар */
|
||
#define POP_MUS_STORY_4 52 /* Джафар уходит */
|
||
#define POP_MUS_WON 56 /* победа: 115 с, только потоком */
|
||
|
||
/* Курсор проигрывания — читает НАСОС (pop_sfx.c, резидент), пишет
|
||
* загрузчик. Отдельный от эффектов: они звучат поверх музыки, а не
|
||
* вместо неё, и у каждого свой источник. */
|
||
extern uint8_t pop_mus_page[POP_MUS_PAGES]; /* физические страницы EMM */
|
||
extern volatile uint8_t pop_mus_pg; /* индекс текущей страницы */
|
||
extern volatile uint16_t pop_mus_ptr; /* адрес в окне W0 */
|
||
extern volatile uint16_t pop_mus_left; /* порций до конца, 0 — молчим */
|
||
/* Номер играющего трека (нумерация оригинала). Нужен менеджеру звука:
|
||
* музыка участвует в таблицах приоритета наравне с эффектами. */
|
||
extern uint8_t pop_mus_id;
|
||
/* Размер кольца страниц для потокового трека; 0 — обычный режим. */
|
||
extern uint8_t pop_mus_ring;
|
||
|
||
/* ДВА СЛОТА. Заставочные сцены идут цепочкой треков (титры -> история ->
|
||
* PV из трёх реплик), и загрузка каждого — это полсекунды-секунда диска.
|
||
* Прятать её в паузу между репликами негде: пауз в оригинале нет, там
|
||
* следующая реплика начинается сразу. Поэтому слотов два: пока звучит
|
||
* один, следующий читается во второй, и переключение стоит копирования
|
||
* двадцати байт таблицы страниц.
|
||
*
|
||
* Загрузить трек в свободный слот (НЕ запускает; играющий трек не трогает).
|
||
* 0 — готово, -1 — нет трека/памяти/файла. */
|
||
int8_t pop_music_load(uint8_t id) __banked;
|
||
|
||
/* ТА ЖЕ ЗАГРУЗКА ПО ОДНОЙ СТРАНИЦЕ ЗА ВЫЗОВ — для тех, кому нельзя
|
||
* замереть на секунду: страница стоит 33 мс, то есть четверть кадра
|
||
* заставочной сцены, и подкачка следующей реплики не видна.
|
||
* begin — открыть файл и занять страницы (0 / -1);
|
||
* step — прочитать очередную: 0 — ещё есть, 1 — трек готов к play,
|
||
* -1 — ошибка (слот освобождён);
|
||
* loading — идёт ли чтение (звать step имеет смысл, пока 1).
|
||
* Оборвать начатое чтение можно pop_music_free. */
|
||
int8_t pop_music_load_begin(uint8_t id) __banked;
|
||
int8_t pop_music_load_step(void) __banked;
|
||
uint8_t pop_music_loading(void) __banked;
|
||
|
||
/* Отпустить ОБА слота. Останавливает проигрывание, если оно шло. */
|
||
void pop_music_free(void) __banked;
|
||
|
||
/* Пустить трек СРАЗУ, дочитывая его по ходу звучания: первая страница
|
||
* читается здесь (33 мс), остальные — вызывающим, по одной за кадр, пока
|
||
* pop_music_loading(). Для игровых джинглов, где паузы на диск быть не
|
||
* может. 0 — играет, -1 — трека нет либо не хватило памяти. */
|
||
int8_t pop_music_stream(uint8_t id) __banked;
|
||
|
||
/* Пустить последний загруженный трек с начала; слот предыдущего при этом
|
||
* освобождается. Без парного load ничего не делает. */
|
||
void pop_music_play(void) __banked;
|
||
void pop_music_stop(void) __banked;
|
||
|
||
/* ОБСЛУЖИВАНИЕ МУЗЫКИ — один вызов на кадр из любого цикла (игра, интро,
|
||
* катсцена): разбирает заявку pop_mus_req и доливает страницы начатого
|
||
* стрима. Всё чтение с диска живёт здесь. */
|
||
void pop_music_service(void) __banked;
|
||
|
||
/* ПОТОКОВЫЙ ТРЕК ИЗ КОЛЬЦА СТРАНИЦ — для того, что в память не влезает.
|
||
* Финал `won` весит 1,2 МБ (78 страниц); играется из шести по кругу, а
|
||
* дочитывает его pop_music_service, пока трек звучит.
|
||
* 0 — играет, -1 — нет файла/памяти. Останавливает pop_music_ring_stop
|
||
* (её же зовёт pop_music_free). */
|
||
int8_t pop_music_ring_start(uint8_t id) __banked;
|
||
void pop_music_ring_stop(void) __banked;
|
||
|
||
/* Трек ещё звучит? 0 — доиграл или не запускался. */
|
||
uint8_t pop_music_busy(void) __banked;
|
||
|
||
/* «Трек в работе»: заявка/загрузка/звучание. Для мест, где ждут КОНЦА
|
||
* мелодии, — busy там ложен в окне запуска (см. pop_music.c). */
|
||
uint8_t pop_music_active(void) __banked;
|
||
|
||
#endif /* POP_MUSIC_H */
|