Files
Sprinter-SDCC/applications/PoP/roomtest/pop_sfx.c
T
snark13 7e6b38cca6 Музыка по ходу игры: смерть, зелья, меч, конец уровня, катсцены
Звуки 24..43 в наборе оцифровки пустые — в оригинале это Adlib-музыка, и
в digisnd её нет. На этом и построено подключение: pop_sfx_play для звука
с нулевой длиной кладёт номер в pop_mus_req, а разбирает заявку
pop_music_service() — один вызов на кадр из любого цикла (игра, интро,
катсцена). Всё чтение с диска живёт там.

Ждать полной загрузки джингла нельзя — это фриз посреди игры. Поэтому
pop_music_stream читает первую страницу (33 мс) и сразу пускает трек: она
звучит 1,5 с, а следующая читается те же 33 мс. Остальные доливаются по
одной за кадр.

Расставлено по местам оригинала: смерть (24/28/32), вступление первого
уровня и тень шестого (25), сцены перед уровнями (27/35/40), встреча с
Джафаром (29), зелья (30/33/39), время вышло (36), меч и смерть стража
(37), смерть Джафара (43), конец уровня (41/32), встреча с принцессой
(26). Темы «раз за уровень» сбрасывает pop_music_level_start.

Упакован 21 трек (2,4 МБ). Не озвучен только финал: won — 115 с и 78
страниц EMM, ему нужен кольцевой стриминг (задача MUS-WON на доске).

Проверено в MAME: вступительная тема первого уровня отыгрывается на
старте (курсор дошёл до конца, id 25). Host-тесты: 15 наборов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 21:52:37 +03:00

286 lines
18 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* pop_sfx.c — ГОРЯЧАЯ часть звуковых эффектов: насос в прерывании CBL,
* запуск/останов и таблица звуков. Зачем всё это и в каком виде лежат
* данные — в шапке pop_sfx.h и ../docs/sound_plan.md.
*
* ПОЧЕМУ МОДУЛЬ РЕЗИДЕНТНЫЙ (в EXTRA_SRCS, а не в банке). Три причины,
* и каждой хватило бы одной:
* - callback насоса зовётся ИЗ ПРЕРЫВАНИЯ; трамплин перед вызовом
* ставит базовую страницу W1, значит хендлер обязан быть в базе;
* - pop_sfx_play дёргается из горячих мест (play_seq на каждый шаг
* Кида, физика, боёвка) — трамплин там был бы чистой потерей;
* Файловое чтение страниц выполняет общий резидентный bank_load_file().
* Холодная половина (аллокация страниц, cbl_open) — в pop_sfx_cold.c.
*/
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <cbl.h>
#include <irq.h> /* IRQ_DISABLE/IRQ_ENABLE вокруг курсора */
#include "pop_sfx.h"
#include "pop_music.h"
#include "_pop_sfx.h"
/* Физические страницы набора; заполняет холодная половина. */
uint8_t pop_snd_page[POP_SND_PAGES];
uint8_t pop_snd_ok;
/* ХОЧЕТ ЛИ ЗВУКА ПОЛЬЗОВАТЕЛЬ (Ctrl+S) — отдельно от pop_snd_ok.
*
* Разделять обязательно: pop_snd_ok значит «CBL сейчас открыт», и его
* гасит служебная пауза на время загрузки уровня. Если бы Ctrl+S правил
* только его, то первая же смена уровня позвала бы pop_sfx_start() и
* вернула звук, который пользователь выключил. Поэтому start уважает
* этот флаг, а pause его не трогает.
*
* Живёт в резиденте, а не в банке: метку в борте рисует отладочный ярлык
* каждый кадр, и тащить ради одного байта трамплин незачем. */
uint8_t pop_snd_want;
/* Курсор проигрывания. volatile: пишет главный цикл, читает прерывание.
* Всё в 8/16 бит — 32-битной арифметики на Z80 избегаем (см. memory
* avoid_32bit_arith_z80); поэтому и в таблице смещение уже разобрано на
* страницу и адрес в окне. */
/* Курсор МУЗЫКИ. Лежит здесь, а не в pop_music.c, по той же причине, что
* и курсор эффектов: его читает насос из прерывания, то есть резидент. */
volatile uint8_t pop_mus_pg;
volatile uint16_t pop_mus_ptr;
volatile uint16_t pop_mus_left;
/* Номер играющего трека в нумерации ОРИГИНАЛА: музыка участвует в тех же
* таблицах приоритета и перебиваемости, что и эффекты (см. pop_sfx_tick). */
uint8_t pop_mus_id;
/* Тик насоса: +1 на каждый отданный CBL блок, то есть 85,4 Гц. */
volatile uint8_t pop_snd_tick;
/* Заявка на музыкальный трек: номер в нумерации оригинала, 0 — нет заявки
* (нулевого трека не бывает). Ставит pop_sfx_play, снимает главный цикл. */
uint8_t pop_mus_req;
static volatile uint8_t sfx_pg; /* индекс в pop_snd_page[] */
static volatile uint16_t sfx_ptr; /* адрес в окне W0: 0x0000..0x3FFF */
static volatile uint16_t sfx_left; /* сколько байт осталось (кратно 128) */
/* ---- насос ---------------------------------------------------------- *
* Зовётся из ISR CBL раз в ~11,7 мс за очередными 128 байтами.
*
* ОКНО БЕРЁМ ВЗАЙМЫ У W3, А НЕ У W0 — и это принципиально.
*
* Раньше насос занимал W0. Оказалось, что так звук рвёт ЛЮБОЙ вызов
* BIOS: `rst 8` — это `out ($7C),a`, который включает системное ПЗУ и
* перестраивает окно 0 (в MAME это update_memory: m_pages[0] +
* m_bank_view0.select). ПЗУ ложится ПОВЕРХ страничного регистра, поэтому
* запись в порт 0x82 из прерывания ничего не даёт — OTIR честно
* вычитывает ПЗУ и отдаёт его в CBL. На слух это скрежет; поймано на
* затемнении в PoP (2026-08-25), причём одинаково при чтении палитры,
* записи палитры и вообще любой функции BIOS.
*
* W3 таким свойством не обладает: он управляется ТОЛЬКО портом 0xE2, и
* подмену из прерывания никто не перекрывает. BIOS тоже двигает W3
* (LP_OPEN_PG, PIC_SET_PAL), но во время нашего ISR он не исполняется —
* мы возвращаем окно до выхода, и для него подмена невидима. Ровно так
* же незаметна она и для фона с accel-блитом: их DI-банды прерывание не
* рвёт, а между бандами W3 всегда восстановлен.
*
* Идея пользователя, 2026-08-25. Тот же приём заимствования окна, что у
* трамплина с W1 (_irq_tramp.c).
*
* Заплатки ISR-стаба в 0x38/0x66 нашим страницам НЕ нужны (в отличие от
* страницы уровня, pop_level_read_file): пока страница замаплена, мы
* внутри прерывания и IFF сброшен — прерваться некому.
*
* ТИШИНУ ЛЬЁМ СВОЮ. Когда эффект кончился, отдавать в CBL всё равно надо:
* иначе железо крутит по кругу хвост своего буфера и это слышно как
* жужжание. Готовый блок 0x80 лежит первым в наборе (упаковщик), поэтому
* libc-шный CBL_UNDERRUN_SILENCE с его malloc нам не нужен — а куча в
* тесном резиденте W2 отказала бы и уронила весь звук. */
int pop_sfx_fill(uint16_t n)
{
uint8_t saved, phys;
uint16_t ptr;
/* ЧАСЫ СЦЕН. Насос зовётся строго по расходу буфера CBL — 85,4 раза в
* секунду, что бы ни делал главный цикл. Это единственный источник времени,
* которому не мешают ни долгие блиты, ни чтение с диска: кадровые
* прерывания теряются в di-окнах акселератора (pop_pace.h), а счёт
* ожиданий vsync считает НАШИ ожидания, а не прошедшее время.
* Катсцены пейсятся по нему — иначе картинка уезжает от музыки. */
pop_snd_tick++;
/* Три источника по убыванию важности: эффект, музыка, тишина. Эффект
* перебивает музыку — как у оригинала, где play_sound глушит всё, что
* звучало; голос-то один. Музыка при этом НЕ теряет позицию: её
* курсор стоит, пока эффект доигрывает, и она продолжается с места. */
if (sfx_left) {
phys = pop_snd_page[sfx_pg]; ptr = sfx_ptr;
} else if (pop_mus_left) {
phys = pop_mus_page[pop_mus_pg]; ptr = pop_mus_ptr;
} else {
phys = pop_snd_page[POP_SND_SILENCE_PAGE];
ptr = POP_SND_SILENCE_OFF;
}
/* ОКНО БЕРЁМ ВЗАЙМЫ У W3, А НЕ У W0 — см. шапку файла. */
saved = _io_page_w3;
_io_page_w3 = phys;
cbl_push_otir((const void *)(0xC000u + ptr), n);
_io_page_w3 = saved;
if (!sfx_left) {
if (!pop_mus_left) return 1; /* тишина — курсор не двигаем */
/* Длину трека считаем ПОРЦИЯМИ насоса, а не байтами: 169 КБ в
* uint16 не влезает, а 1350 блоков — легко (32-бит арифметики на
* Z80 избегаем, тем более в прерывании). */
pop_mus_left--;
pop_mus_ptr += n;
if (pop_mus_ptr >= 0x4000) {
pop_mus_ptr = 0;
if ((uint8_t)(pop_mus_pg + 1) < POP_MUS_PAGES) pop_mus_pg++;
else pop_mus_left = 0;
}
return 1;
}
sfx_ptr += n;
if (sfx_ptr >= 0x4000) { /* граница страницы — ровно по блоку */
sfx_ptr = 0;
/* Переход на следующую страницу нужен ровно одному эффекту — тому,
* что сам ДЛИННЕЕ страницы (упаковщик с 2026-08-25 кладёт остальные
* целиком в одну). Гард обязателен: без него ошибка в длине увела
* бы насос за pop_snd_page[] и он читал бы чужую память — на слух
* это «играет не тот звук», причём каждый раз другой. */
if ((uint8_t)(sfx_pg + 1) < POP_SND_PAGES) sfx_pg++;
else sfx_left = n; /* обрываем */
}
sfx_left -= n;
return 1;
}
/* ---- ОЧЕРЕДЬ И ПРИОРИТЕТЫ (порт seg000:12C5 + seg000:1304) ------------ *
*
* Оригинал НЕ перебивает играющий звук чем попало, и это слышно. Модель
* ровно такая:
*
* play_sound(id) — только НОМИНИРУЕТ кандидата на кадр; из нескольких
* за кадр остаётся ВАЖНЕЙШИЙ (меньше prio = важнее,
* при равенстве побеждает последний — сравнение <=);
* play_next_sound() — раз в кадр решает, запускать ли: можно, если ничего
* не играет, ЛИБО текущий звук помечен «перебиваемым»
* И новый не менее важен. Иначе номинант ВЫБРАСЫВАЕТСЯ
* (next_sound = -1 безусловно), а не встаёт в очередь.
*
* Отсюда то, что слышно в игре: `gate_closing_fast` (6) неперебиваем и
* доигрывает до звонкого конца, а приземление Кида в этот момент пропадает
* совсем; челюсти (47, prio 0x10) всегда важнее решётки (4, prio 0x32), и
* решётка звучит только в паузах между укусами.
*
* Таблицы — из SDLPoP (sound_prio_table seg000:1528, sound_interruptible
* data.h:433) с правками fix_sound_priorities(): в config.h SDLPoP
* FIX_SOUND_PRIORITIES определён безусловно, то есть сравниваем мы себя
* именно с исправленным вариантом (10 -> 0x0D, 48 -> 0x15, 49 перебиваем).
*/
/* Приоритет: МЕНЬШЕ значит ВАЖНЕЕ. Музыкальные id тут тоже есть — они
* никогда не номинируются (нет оцифровки), но индексация должна совпадать
* с оригиналом, иначе таблицу не сверить глазами. */
static const uint8_t snd_prio[POP_SND_COUNT] = {
0x14, 0x1E, 0x23, 0x66, 0x32, 0x37, 0x30, 0x30, 0x4B, 0x50, /* 0.. 9 */
0x0D, 0x12, 0x0C, 0x0B, 0x69, 0x6E, 0x73, 0x78, 0x7D, 0x82, /* 10..19 */
0x91, 0x96, 0x9B, 0xA0, 0x01, 0x01, 0x01, 0x01, 0x01, 0x13, /* 20..29 */
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, /* 30..39 */
0x01, 0x01, 0x01, 0x01, 0x87, 0x8C, 0x0F, 0x10, 0x15, 0x16, /* 40..49 */
0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01 /* 50..56 */
};
/* «Играющий звук можно перебить» — битовая карта (бит id&7 байта id>>3):
* 57 флагов уложены в 8 байт, потому что в резиденте W1/W2 каждая сотня
* байт на счету, а читаем мы их раз в кадр. */
static const uint8_t snd_intr[8] = {
/* 0.. 7 */ (uint8_t)(0<<0 | 1<<1 | 1<<2 | 1<<3 | 1<<4 | 1<<5 | 0<<6 | 1<<7),
/* 8..15 */ (uint8_t)(1<<0 | 1<<1 | 1<<2 | 1<<3 | 1<<4 | 1<<5 | 0<<6 | 0<<7),
/*16..23 */ (uint8_t)(1<<0 | 1<<1 | 0<<2 | 1<<3 | 1<<4 | 1<<5 | 1<<6 | 1<<7),
/*24..31 */ (uint8_t)(0<<0 | 0<<1 | 0<<2 | 0<<3 | 0<<4 | 1<<5 | 0<<6 | 0<<7),
/*32..39 */ 0,
/*40..47 */ (uint8_t)(0<<0 | 0<<1 | 0<<2 | 0<<3 | 0<<4 | 0<<5 | 0<<6 | 1<<7),
/*48..55 */ 0,
/*56.. */ 0
};
/* Створка двери уровня (15) — единственная запись, которую оригинал правит
* НА ХОДУ (seg007:442/464): при закрытии её перебивать можно, при открытии
* нельзя (иначе съезд створки обрывается и «конца» не слышно). Держим
* отдельным байтом, чтобы таблица осталась в ПЗУ. */
uint8_t pop_sfx_slide_intr;
/* Номинант кадра, ЗАПИСАННЫЙ КАК id+1: ноль значит «никого». Так сделано
* ради crt0 — он зануляет _DATA, а «нет номинанта» у оригинала это -1;
* со смещением на единицу начальное состояние получается даром. */
static uint8_t snd_next1;
static uint8_t snd_curr; /* что играет — для prio/intr */
static uint8_t snd_interruptible(uint8_t id)
{
if (id == 15) return pop_sfx_slide_intr;
return (uint8_t)(snd_intr[id >> 3] & (uint8_t)(1 << (id & 7)));
}
/* ---- запуск / останов ------------------------------------------------ */
void pop_sfx_play(uint8_t id)
{
if (!pop_snd_ok || id >= POP_SND_COUNT) return;
/* НЕТ ОЦИФРОВКИ — ЗНАЧИТ ЭТО МУЗЫКА. В оригинале звуки 24..43 играет
* Adlib по MIDI, и в digisnd их нет вовсе; у нас на их месте записи
* DOS-версии, которые надо читать с диска. В прерывании и в горячем
* коде этого делать нельзя, поэтому здесь только ЗАЯВКА одним байтом —
* её разбирает главный цикл (pop_music_stream, банк 9).
* В конкурсе эффектов такой id не участвует: он не глушит живой звук
* (порт проверки `if (NULL == sound_pointers[id]) return;`). */
if (pop_snd_tbl[id].len == 0) {
pop_mus_req = id;
return;
}
if (!snd_next1 || snd_prio[id] <= snd_prio[snd_next1 - 1])
snd_next1 = (uint8_t)(id + 1);
}
void pop_sfx_tick(void)
{
uint8_t id, pg;
uint16_t off, len;
if (!snd_next1) return;
id = (uint8_t)(snd_next1 - 1);
snd_next1 = 0; /* номинант живёт ровно кадр */
/* ЧТО СЕЙЧАС ЗВУЧИТ — эффект либо музыка. У оригинала это один и тот
* же вопрос: check_sound_playing() отвечает «играет хоть что-то», без
* различия типа, а музыкальные id лежат в тех же таблицах приоритета и
* перебиваемости (seg000:1672 + data.h:433). Почти вся музыка помечена
* НЕперебиваемой, поэтому пока она играет, эффект просто не стартует —
* ровно это слышно в оригинале. Исключение одно: 29 meet_Jaffar. */
{
uint8_t cur = sfx_left ? snd_curr : pop_mus_id;
if ((sfx_left != 0 || pop_mus_left != 0) &&
(!snd_interruptible(cur) || snd_prio[id] > snd_prio[cur]))
return; /* ...и уступать не обязано */
}
snd_curr = id;
pg = pop_snd_tbl[id].page;
off = pop_snd_tbl[id].off;
len = pop_snd_tbl[id].len;
/* Курсор из трёх полей, а читает его прерывание — обновляем под DI,
* иначе насос может застать наполовину обновлённое состояние и уехать
* в чужие данные. */
IRQ_DISABLE();
sfx_pg = pg;
sfx_ptr = off;
sfx_left = len;
IRQ_ENABLE();
}
void pop_sfx_stop(void)
{
IRQ_DISABLE();
sfx_left = 0;
IRQ_ENABLE();
}