Files
Sprinter-SDCC/applications/PoP/roomtest/pop_sfx.c
T
snark13 34ba5f71d5 Музыка: PCM-трек через CBL, первый экран истории озвучен
Путь C из sound_plan.md вместо ранее выбранного A (ноты спикера на AY).
Причина смены: появились готовые записи DOS-версии, и строка «нужен
синтезатор на хосте», из-за которой C считался дорогим, отпала —
осталось `ffmpeg -ac 1 -ar 10937 -f u8`.  Оцифровки музыки в самой игре
нет вовсе: digisnd*.dat содержит только эффекты, музыка лежит MIDI под
Adlib, так что конвертировать ресурс игры всё равно было не из чего.

* toolchain/pop_pack_music.py — запись flac -> сырой u8 на 10 937,5 Гц
  (частота CBL, чтобы её не переключать никогда), нарезка на куски
  MUS\m<id>_<nn>.bin по 16 КБ: bank_load_file читает файл только целиком
  и только в одну страницу.  Каталог -> pop_music_tbl.h; длина трека
  хранится ПОРЦИЯМИ по 128 байт — 169 КБ в uint16 не влезает, 1350
  блоков влезает (32-бит арифметики на Z80 избегаем).
* pop_music.c (банк 9) — загрузка трека в EMM и курсор; pop_sfx_fill
  (резидент) получил третий источник: эффект важнее музыки, музыка
  важнее тишины.  Эффект музыку не сбрасывает — её курсор стоит, пока
  тот доигрывает, и она продолжается с места.
* story_1_absence звучит на первом экране истории, где была тишина.
  Грузим ПОСЛЕ сборки картинки: чтение идёт через ESTEX, и при открытом
  CBL насос не успел бы долить блок.

Проверено записью звука из MAME: корреляция огибающих с эталоном 0,836
при сдвиге 1,8 с, RMS 22,6 против 18,3 (разница — 8-битное квантование).
Эффекты двери в PV-сцене после трека звучат по-прежнему.

Бюджет: резидент +83 Б кода и +25 Б данных, запас W2 151 Б (кучи в
приложении нет, malloc не слинкован); банк 9 занят на 88 %.

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

236 lines
14 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;
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 байтами.
*
* ОКНО W0 БЕРЁМ ВЗАЙМЫ. Фон в этот момент может держать в W0 страницу
* атласа (pop_blit_b делает map ... blit ... unmap), поэтому сохраняем
* порт, подставляем свою страницу и возвращаем как было. Для фона
* подмена невидима: прерывание для него атомарно. Тот же приём, что у
* трамплина с 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;
/* Три источника по убыванию важности: эффект, музыка, тишина. Эффект
* перебивает музыку — как у оригинала, где 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;
}
saved = _io_page_w0;
_io_page_w0 = phys;
cbl_push_otir((const void *)ptr, n);
_io_page_w0 = 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; /* номинант живёт ровно кадр */
if (sfx_left != 0 && /* что-то играет... */
(!snd_interruptible(snd_curr) || snd_prio[id] > snd_prio[snd_curr]))
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();
}