/* * 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 #include #include #include #include #include /* 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; 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; /* Нет оцифровки (музыка) — не номинируем вовсе: порт проверки * `if (NULL == sound_pointers[id]) return;`. Иначе музыкальный id * выигрывал бы конкурс и глушил живой эффект. */ if (pop_snd_tbl[id].len == 0) 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(); }