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>
286 lines
18 KiB
C
286 lines
18 KiB
C
/*
|
||
* 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();
|
||
}
|