/* * pop_music.h — музыкальный трек через тот же CBL, что и эффекты. * * ПОЧЕМУ PCM, А НЕ AY. Оцифровки музыки в игре нет вовсе: digisnd*.dat * содержит только эффекты, музыка лежит MIDI под Adlib. Зато есть готовые * записи DOS-версии, и путь «запись -> сырой поток CBL» не требует ни * синтезатора на хосте, ни аранжировки под три канала AY (разбор вариантов * — ../docs/sound_plan.md §1). Плата известна: CBL один, поэтому пока * играет музыка, эффектов нет. Оригинал ведёт себя так же — play_sound * там глушит предыдущий звук. * * ГДЕ УМЕСТНО. Заставки и титры: эффектов там не бывает, картинка * статична, и загрузка сотни-другой килобайт с диска никому не мешает. * Музыка ПО ХОДУ игры (джинглы смерти, зелья) — отдельный вопрос, её * придётся либо гасить эффектами, либо уводить на AY. * * ФОРМАТ на диске — один файл MUS\m.bin на трек, читается порциями по * 16 КБ из одного открытого fd (bank_read_page). Каталог с длинами * генерирует toolchain/pop_pack_music.py в pop_music_tbl.h. */ #ifndef POP_MUSIC_H #define POP_MUSIC_H #include /* Сколько страниц 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 */