Files
Sprinter-SDCC/applications/SprPoP/src/pop_sfx_cold.c
T
snark13 2349481b86 SprPoP: звуковые эффекты без перелинковки — раскладка уехала на диск
Часть II плана music_runtime_index_plan.md (SI0..SI4).  gen/pop_sound_tbl.h
БОЛЬШЕ НЕ ГЕНЕРИРУЕТСЯ: раскладка набора читается из SND/snd.idx (формат
PSI1, писатель и разборщик — tools/pop_idx.py, 22 теста в make test-tools).
Один и тот же sprpop.exe работает с набором SDLPoP (9 страниц) и MSDOS
(10) — sha256 бинарника при смене набора не меняется.

Заодно умолчание источника эффектов переведено на SDLPoP (SND_SRC=sdlpop):
сборка обязана работать без оригинального дистрибутива DOS.  У кого он
есть, включает лучший набор явно — make SND_SRC=msdos (там полнее
оцифровка: в SDLPoP звук 48 spiked пустой).

Устройство: pop_snd_tbl/pop_snd_page/pop_snd_pages — резидентные данные
(pop_snd_data.c), тип и инварианты — рукописный pop_snd_tbl.h.  Записи
читаются ОДНИМ read прямо в таблицу, поэтому sizeof(pop_snd_ent_t) == 5
стало частью дискового контракта: проверяется статически и полем размера
записи в заголовке.  POP_SND_PAGES как compile-time размер набора исчез —
вместо него POP_SND_MAX_PAGES (вместимость, 16) и runtime pop_snd_pages.

Цена: таблица переехала из _CODE в _DATA, суммарный резидент почти не
изменился (куча 239 -> 229 Б); банк 8 +601 Б на чтение и валидацию.

Валидация не доверяет файлу: заголовок целиком плюс каждая запись
(страница, смещение, кратность блоку, непересечение с блоком тишины,
выход за последнюю страницу).  Последнее считается В БЛОКАХ — байтовый
адрес конца не влезает в uint16, а 32-битная арифметика на Z80 дорога.

НЕТ ИНДЕКСА — ЭФФЕКТОВ НЕТ, НО МУЗЫКА ИГРАЕТ.  Первая версия просто
возвращала ошибку, и игра становилась непроходимой: тишину льёт первый
блок набора, без набора CBL не открывался, а с ним вставала музыка (её
блоки считает тот же насос) — заставка ждала конца трека вечно.  Теперь
поднимается пустой набор с блоком тишины.  Заливается ровно 128 байт и
под DI: gfx_w0_page_prepare ставит в страницу IRQ-стабы, и заливка всей
страницы затирала их — первое же прерывание давало чёрный экран.

Грабли сборки: смена SND_SRC тихо давала неверный результат
(sdlpop -> msdos -> sdlpop оставлял чужой набор в assets/packed).  Причина
не в логике, а в секундной гранулярности mtime.  Лечение убирает время из
решения: смена варианта сносит stamp'ы своего семейства, а упаковка,
сборка архива и копия индекса делаются одним рецептом.  То же получила и
музыка (MUSIC_FMT).

Проверено в MAME: таблица в памяти совпадает с файлом из образа побайтово;
один EXE поднимает оба набора; отладочный --order reverse (30 из 31
записей отличаются от штатных) звучит правильно; битый индекс выключает
эффекты, не роняя игру; без индекса PV-сцена проходит с музыкой; Ctrl+S
работает в обоих режимах.  Разбор — docs/sound_plan.md §10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
2026-08-31 16:14:53 +03:00

263 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_cold.c — ХОЛОДНАЯ половина звука: поднять набор и открыть CBL.
* Отрабатывает один раз за запуск, поэтому живёт в банке 8; горячая
* половина (насос, запуск эффекта) — в резиденте, см. pop_sfx.c.
*/
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sprinter_mem.h>
#include <cbl.h>
#include "pop_sfx.h"
#include "pop_arc.h"
#include "_pop_sfx.h"
#include "_pop_file.h" /* POP_PATH_CALL — старые DSS и пути */
#include <irq.h> /* IRQ_DISABLE вокруг записи в W0 */
#include <sprite.h> /* gfx_w0_map — залить блок тишины */
static uint8_t snd_blk; /* блок EMM под весь набор */
/* --- ИНДЕКС НАБОРА (SND/snd.idx, формат PSI1) ----------------------- *
*
* Раскладка эффектов приезжает с диска, а не из EXE: так пара
* snd.idx + snd.arc заменяется целиком (SDLPoP <-> MSDOS) без
* перекомпиляции игры. Писатель формата — tools/pop_idx.py, он же
* покрыт тестом; проверки ниже повторяют его один в один, потому что
* доверять содержимому файла нельзя: битый индекс увёл бы насос за
* пределы выделенного блока EMM.
*
* Заголовок: 16 байт, дальше POP_SND_COUNT записей по 5 байт подряд —
* ровно в том виде, в каком лежит pop_snd_tbl[], поэтому читаются они
* ОДНИМ read'ом прямо туда, без разбора по полям. */
#define IDX_HDR_SIZE 16
#define IDX_BLOCK_LOG2 7 /* 1 << 7 == POP_SND_BLOCK */
#define IDX_PCM_FMT 1 /* u8 mono, CBL_FREQ_10K9 */
#define IDX_TBL_SIZE (POP_SND_COUNT * (int)sizeof(pop_snd_ent_t))
/* Имя знает сам модуль — литерал лежит в rodata банка 8, и open() читает
* его, пока в W3 стоит этот же банк (тот же приём, что в pop_arc.c). */
static const char idx_path[] = "SND\\snd.idx";
/* Одна запись: ровно те проверки, что делает упаковщик.
*
* СЧИТАЕМ В БЛОКАХ, А НЕ В БАЙТАХ. Байтовый адрес конца эффекта
* (page * 16384 + off + len) не влезает в uint16, а 32-битная арифметика
* на Z80 дорога (memory avoid_32bit_arith_z80). В блоках по 128 весь
* набор — не больше POP_SND_MAX_PAGES * 128 = 2048, и всё считается
* шестнадцатью битами. */
static int8_t idx_entry_ok(const pop_snd_ent_t *e, uint8_t pages)
{
uint16_t start, end;
if (e->len == 0) /* нет оцифровки — это музыка */
return (e->page == 0 && e->off == 0) ? 0 : -1;
if (e->page >= pages) return -1;
if (e->off >= 0x4000) return -1;
if ((e->off | e->len) & (POP_SND_BLOCK - 1)) return -1;
/* Блок тишины неприкосновенен: насос льёт его, когда играть нечего. */
if (e->page == POP_SND_SILENCE_PAGE && e->off < POP_SND_BLOCK) return -1;
start = (uint16_t)((uint16_t)e->page << 7) + (uint16_t)(e->off >> 7);
end = (uint16_t)(start + (uint16_t)(e->len >> 7));
/* Длинный эффект переливается на следующие страницы — они обязаны
* быть загружены, иначе насос уйдёт в чужую память. */
if (end > (uint16_t)((uint16_t)pages << 7)) return -1;
return 0;
}
/* Прочитать и проверить индекс. Возвращает число страниц набора либо 0. */
static uint8_t idx_load(void)
{
uint8_t head[IDX_HDR_SIZE];
uint8_t pages, i;
int fd;
POP_PATH_CALL(fd, open(idx_path, O_RDONLY));
if (fd < 0) return 0;
if (read(fd, head, IDX_HDR_SIZE) != IDX_HDR_SIZE ||
memcmp(head, "PSI1", 4) != 0 ||
head[4] != POP_SND_COUNT ||
head[5] != (uint8_t)sizeof(pop_snd_ent_t) ||
head[6] != IDX_BLOCK_LOG2 ||
head[7] != IDX_PCM_FMT) {
close(fd);
return 0;
}
pages = head[8];
if (pages == 0 || pages > POP_SND_MAX_PAGES) { close(fd); return 0; }
/* Записи ложатся прямо в резидентную таблицу: дисковый формат и
* структура совпадают побайтово (проверено статически в
* pop_snd_data.c и полем head[5] выше). */
if (read(fd, pop_snd_tbl, IDX_TBL_SIZE) != IDX_TBL_SIZE) {
close(fd);
return 0;
}
close(fd);
for (i = 0; i < POP_SND_COUNT; i++)
if (idx_entry_ok(&pop_snd_tbl[i], pages) != 0) return 0;
return pages;
}
/* НАБОРА НЕТ — НО НАСОС ВСЁ РАВНО ОБЯЗАН РАБОТАТЬ.
*
* Тишину в паузах насос льёт из ПЕРВОГО БЛОКА НАБОРА, поэтому без набора
* CBL не открывался вовсе. А вместе с ним вставала и МУЗЫКА: её блоки
* отсчитывает тот же насос (pop_mus_left убывает только в pop_sfx_fill).
* Заставка при этом висла намертво — ждала конца трека, который не
* начинал играть, и pop_music_busy() не гас никогда (поймано
* пользователем 2026-08-31 на образе без snd.idx).
*
* Поэтому при отказе индекса поднимаем ПУСТОЙ набор: одна страница EMM с
* блоком тишины. Эффектов нет — все len в таблице нулевые, и
* pop_sfx_play уходит по ветке «нет оцифровки», — а музыка играет.
*
* ЗАЛИВАЕМ ТОЛЬКО ПЕРВЫЙ БЛОК, и это не экономия: gfx_w0_page_prepare
* ставит в страницу IRQ/NMI-стабы, чтобы прерывание при замапленном W0
* нашло валидный код. Заливка ВСЕЙ страницы затирает их, и первое же
* прерывание уводит процессор в мусор — чёрный экран на старте (поймано
* при первой попытке этого фикса). Насосу больше 128 байт и не нужно:
* он берёт ровно блок тишины, да и то через W3, а не через W0.
* Запись — под DI: 128 байт накрывают стаб RST38, и до unmap прерываний
* быть не должно. */
static int8_t snd_silence_only(void)
{
uint8_t blk, page;
uint8_t *w0 = (uint8_t *)0x0000; /* окно 0 — сюда ляжет страница */
memset(pop_snd_tbl, 0, sizeof(pop_snd_tbl));
blk = mem_alloc_pages(1);
if (!blk) return -1;
page = mem_get_page(blk, 0);
if (!page) { mem_free_block(blk); return -1; }
gfx_w0_page_prepare(page);
IRQ_DISABLE();
gfx_w0_map(page);
memset(w0, 0x80, POP_SND_BLOCK); /* 0x80 — тишина в 8-бит без знака */
gfx_w0_unmap();
IRQ_ENABLE();
pop_snd_page[POP_SND_SILENCE_PAGE] = page;
snd_blk = blk;
pop_snd_pages = 1;
return 0;
}
int pop_sfx_init(void) __banked
{
uint8_t i, blk, pages;
if (pop_snd_ok) return 0;
/* СНАЧАЛА ИНДЕКС, ПОТОМ ДАННЫЕ: сколько страниц читать и куда какой
* эффект лёг — знает только он. Набора без индекса не бывает:
* держать в EXE запасную таблицу нельзя, иначе привязка бинарника к
* конкретному набору вернётся через чёрный ход. */
pages = idx_load();
if (!pages) {
/* Набор эффектов не поднялся — но звук как таковой поднять надо,
* иначе встанет музыка (см. snd_silence_only). Возвращаем -1:
* эффектов действительно нет, и вызывающий вправе об этом знать. */
if (snd_silence_only() != 0) return -1;
pop_snd_want = 1;
return -1;
}
/* Одним блоком: страницы блока идут подряд по логическому индексу, и
* таблица звуков адресует их именно так (page = смещение >> 14).
*
* Из АРХИВА, а не десятью файлами: `open` стоит 51,4 мс против 32,6 мс
* на чтение 16 КБ (замер 2026-08-25), то есть девять лишних открытий —
* это почти полсекунды на ровном месте. Таблица живёт на стеке, см.
* pop_arc.h. */
{
pop_arc_t arc;
uint8_t tbl[POP_SND_MAX_PAGES * POP_ARC_REC];
/* Архив обязан содержать РОВНО столько страниц, сколько обещал
* индекс: расходящейся паре доверять нельзя — она означает, что
* snd.idx и snd.arc из разных наборов. */
if (pop_arc_open_id(&arc, POP_ARC_SND, tbl, POP_SND_MAX_PAGES) !=
(int8_t)pages)
return -1;
blk = mem_alloc_pages(pages);
if (!blk) { pop_arc_close(&arc); return -1; }
for (i = 0; i < pages; i++) {
pop_snd_page[i] = mem_get_page(blk, i);
if (!pop_snd_page[i] ||
pop_arc_read(&arc, tbl, i, 0, pop_snd_page[i], 0, 0) != 16384) {
pop_arc_close(&arc);
mem_free_block(blk);
return -1;
}
}
pop_arc_close(&arc);
}
snd_blk = blk;
/* Число страниц ставим ПОСЛЕДНИМ: до этого момента насос обязан
* видеть 0 и не пытаться ходить по полузаполненной таблице. */
pop_snd_pages = pages;
pop_snd_want = 1; /* набор есть — звук по умолчанию включён */
return 0;
}
/* ЗАПУСТИТЬ ВЫВОД — ОТДЕЛЬНО ОТ ЗАГРУЗКИ, и это не косметика.
*
* Пока идёт чтение файлов, CBL держать открытым НЕЛЬЗЯ: ESTEX уходит в
* диск надолго, насос не успевает долить очередные 128 байт, и железо
* крутит по кругу хвост своего 256-байтового буфера. На слух это
* сплошной скрежет (поймано пользователем на старте, 2026-08-20).
* Поэтому загрузка страниц и открытие CBL разведены: грузим молча,
* включаем звук последним действием, а на время загрузки уровня глушим
* (pop_sfx_pause).
*
* Параметры: 10 937,5 Гц, 8 бит моно — формат данных один в один
* (sound_plan.md). OTIR, а не акселератор: тот нужен только для 16 бит.
* cbl_open, а не cbl_open_silence: тишину насос льёт СВОЮ (первый блок
* набора), и просить у libc буфер через malloc не надо — куча в резиденте
* W2 тесная, и её отказ ронял бы весь звук. Заодно так malloc вообще не
* приезжает в резидент (275 Б, см. libc/cbl/_cbl_open_raw.c). */
int pop_sfx_start(void) __banked
{
if (pop_snd_ok) return 0;
if (!pop_snd_want) return 0; /* выключено пользователем (Ctrl+S) */
if (!snd_blk) return -1; /* набор не загружен */
if (cbl_open(CBL_FREQ_10K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
pop_sfx_fill) != 0)
return -1;
pop_snd_ok = 1;
return 0;
}
void pop_sfx_pause(void) __banked
{
if (!pop_snd_ok) return;
pop_sfx_stop();
cbl_close();
pop_snd_ok = 0;
}
void pop_sfx_close(void) __banked
{
pop_sfx_pause();
if (snd_blk) { mem_free_block(snd_blk); snd_blk = 0; }
}
/* Ctrl+S — порт `turn_sound_on_off((!is_sound_on) * 15)` из SDLPoP
* (seg000:657). Выключение реально ЗАКРЫВАЕТ CBL, а не глушит сэмпл:
* иначе насос продолжал бы отдавать блоки тишины 85 раз в секунду, а это
* 3,3 % процессорного времени ни за что (замер: 8 001 такт на прерывание
* при периоде 245 759). Открытие обратно дешёвое — cbl_open только
* настраивает железо, страницы набора никуда не девались. */
void pop_sfx_toggle(void) __banked
{
if (pop_snd_want) {
pop_sfx_pause(); /* pause сам не трогает want... */
pop_snd_want = 0; /* ...поэтому гасим его следом */
} else {
pop_snd_want = 1;
/* Набора может не быть вовсе (файлы не прочитались, страницы не
* дались) — тогда откатываем флаг, чтобы индикация не врала. */
if (pop_sfx_start() != 0) pop_snd_want = 0;
}
}