Files
Sprinter-SDCC/applications/SprPoP/src/pop_sfx.c
T
snark13 7377bf6a2c SprPoP: заставка и музыка сразу после запуска, а не через несколько секунд
От запуска до проявления титула проходило ~3 с чёрного экрана в тишине:
pop_boot читал ВСЕ ресурсы игры до первого пикселя, а pop_title_show перед
fade_in вычитывал целиком трек заставки (248 КБ).

Старт разбит на три фазы (sprpop_cold.c):
- pop_boot() — настройки, POP.CFG, графика, чёрный экран, FONT.ATL;
- pop_boot_sound() — snd.idx + ОДНА страница snd.arc, индекс музыки, CBL;
- pop_boot_rest() — остальные страницы звука, kid.ani, Тень, атласы Кида;
  идемпотентно, между кусками доливает трек.

pop_title_show теперь: fade_in -> pop_boot_sound -> pop_music_stream(54) ->
pop_boot_rest под стоящим титулом.  Порядок «сначала fade_in, потом тема»
сохранён как у show_title (seg000.c:1981).  Первый такт заставки укорочен
на фактически потраченное время (часы насоса 85,4 Гц -> кадры луча
сдвигами), иначе сцена уехала бы относительно музыки.  title_wait доливает
трек по полстраницы за кадр.

Музыка играет с ПЕРВОЙ страницы: pop_music_stream уже был (игровые
джинглы), кольцо won не понадобилось — m54 это 16 страниц из 20 доступных.

Звук поднимается в два приёма (pop_sfx_init_begin/finish): насосу для
тишины нужен ровно один блок, и упаковщик обязан класть его первым блоком
страницы 0 (tools/pop_idx.py) — значит для открытия CBL хватает ОДНОЙ
страницы (33 мс вместо 440).  Пока набор неполон, pop_sfx_play пропускает
эффекты; заявки на музыку проходят.

Четыре загрузки из boot УБРАНЫ, а не отложены: атлас фона, страж, уровень 1
и pop_pal_game_load — их и так делает pop_level_switch на входе в любой
уровень (а pop_new_game_load ещё и палитру перед ним), и оба маршрута в
игру, LEVEL_LOAD и DEMO, упираются в него.  Это ~0,77 с на ровном месте.
Так же устроен оригинал: init_game_main до заставки читает только меч,
пламя, звуки и палитры, а chtab_5/6/7 грузит load_lev_spr на старте уровня.
Страховка на случай пропуска заставки в первую секунду — идемпотентный
pop_boot_rest() в начале pop_new_game_load и pop_level_switch.

FONT.ATL остаётся в минимальной фазе, и это не про шрифт: снимок палитры
физически лежит в хвосте его страницы (pop_ui.c), а вся машинерия яркости
начинается с `if (!font_ready) return`.  С отложенным шрифтом заставка
возникала разом на полной яркости — поймано пользователем.

Проверено в MAME: титул виден сразу и проявляется полосами, pop_mus_left
убывает (трек стримится), pop_snd_pages=10 (набор дочитался), цепочка
титул -> интро -> пропуск -> уровень 1 работает.  Банк 8: 95,5 % (732 Б).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
2026-09-02 11:30:53 +03:00

316 lines
20 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_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_music.c). */
uint8_t pop_mus_ring;
/* Заявка на музыкальный трек: номер в нумерации оригинала, 0 — нет заявки
* (нулевого трека не бывает). Ставит pop_sfx_play, снимает главный цикл. */
uint8_t pop_mus_req;
/* ХОЧЕТ ЛИ ПОЛЬЗОВАТЕЛЬ МУЗЫКУ (Settings / Ctrl+M) — отдельно от звука.
* Оригинал такого разделения не знал (там один тумблер на всё), но у нас
* музыка это ПОТОК С ДИСКА: кому мешает подкачка или просто не нужен
* саундтрек, гасит её, не теряя эффектов. */
uint8_t pop_mus_want;
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;
/* КОЛЬЦО. Финальная тема — 1,2 МБ, в память целиком не влезает
* никак, поэтому она играется из нескольких страниц по кругу, а
* загрузчик дочитывает в те, что мы уже прошли (pop_music.c).
* Конец трека определяет left, а не номер страницы. */
if (pop_mus_ring) {
if ((uint8_t)(pop_mus_pg + 1) < pop_mus_ring) pop_mus_pg++;
else pop_mus_pg = 0;
} else 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) {
/* Заявку принимаем ТОЛЬКО в музыкальном диапазоне оригинала
* (24..43 — Adlib-треки игры). Пустых слотов в наборе больше нет,
* но случайный мусорный id обязан молчать, а не запускать трек на
* минуту: ровно так проявился sdcc_z80_cmp_store_a_bug в звуке
* тряски — разность двух номеров попала в 27 и заиграла музыка. */
if (pop_mus_want && id >= 24 && id <= 43) pop_mus_req = id;
return;
}
/* НАБОР ЕЩЁ ДОЧИТЫВАЕТСЯ. На старте программы звук открывается после
* ОДНОЙ страницы (в ней блок тишины), чтобы музыка заставки заиграла
* почти сразу; остальные приезжают позже (pop_sfx_init_begin/finish).
* Страницы под них уже выделены, но не прочитаны — эффект из такой
* страницы был бы шумом из чужих данных. Заявку на МУЗЫКУ выше это
* не трогает: её данные лежат отдельно. */
if (!pop_snd_pages) 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();
}