Звуковые эффекты PoP: оцифровка оригинала через CBL

Набор digisnd1..3.dat приведён упаковщиком к 10 937,5 Гц (частота
железа), склеен в 8 EMM-страниц с выравниванием каждого звука на 128 —
размер блока запроса CBL, поэтому ни один блок не пересекает границу
страницы и проигрыватель не знает слова «стык».

Насос (pop_sfx.c) резидентный: его зовут из прерывания CBL, из горячих
мест физики и из play_seq.  Тишину льём свою (первый блок набора), а не
через CBL_UNDERRUN_SILENCE с его malloc — куча в резиденте W2 тесная.
Открытие CBL разведено с загрузкой (pop_sfx_start отдельно от
pop_sfx_init): пока ESTEX читает файлы, насос не успевает долить блок и
железо крутит хвост буфера — на слух скрежет.

Разведены все места play_sound() SDLPoP, у которых есть оцифровка
(id 0..23, 44..49): посадки, падение, удары о стену, зацеп, тряска и
провал плит, ворота, дверь уровня, пики, чомпер, кнопки, боёвка, меч,
зеркало, скелет, зелье.  Таблица соответствий — docs/sound_plan.md §8.
Музыкальные id остаются с нулевой длиной до фазы музыки.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 15:16:43 +03:00
parent 24ced6058f
commit 8ea4c32e51
25 changed files with 561 additions and 137 deletions
+15 -2
View File
@@ -117,7 +117,12 @@ def main():
print(f" звук {sid}: {rate} Гц -> {RATE:g} Гц, {len(samples)} -> {len(pcm)} сэмплов")
# Раскладка: начало каждого звука кратно BLOCK, страницы по PAGE.
blob = bytearray()
# ПЕРВЫЙ БЛОК — ТИШИНА. Он нужен насосу: когда эффект кончился, в CBL
# всё равно надо что-то отдавать, иначе железо доигрывает по кругу хвост
# своего буфера (жужжание). Держим тишину СВОЮ, а не просим у libc
# (CBL_UNDERRUN_SILENCE делает malloc), потому что куча в резиденте W2
# тесная и её отказ ронял бы весь звук.
blob = bytearray([0x80] * BLOCK)
table = {}
for sid in sorted(sounds):
pad = (-len(blob)) % BLOCK
@@ -158,6 +163,9 @@ def main():
"#define POP_SND_PAGES %d" % npages,
"#define POP_SND_COUNT %d" % NSOUND,
"#define POP_SND_BLOCK %d" % BLOCK,
"/* Первый блок страницы 0 — тишина для насоса (см. упаковщик). */",
"#define POP_SND_SILENCE_PAGE 0",
"#define POP_SND_SILENCE_OFF 0x0000",
"",
"typedef struct { uint8_t page; uint16_t off; uint16_t len; } pop_snd_ent_t;",
"",
@@ -170,10 +178,15 @@ def main():
lines += [
"};",
"",
"/* Имена файлов видит ТОЛЬКО тот, кто попросил POP_SND_FILES (холодная",
" * половина): иначе копия строк уезжает в каждый включивший заголовок",
" * модуль, а в резиденте W1/W2 каждая сотня байт на счету. Тот же",
" * приём, что у POP_BG_ATLAS_NAMES в pop_bg_atlas.h. */",
"#ifdef POP_SND_FILES",
"static const char *const pop_snd_files[POP_SND_PAGES] = {",
]
lines += [' "SND\\\\s%d.bin",' % i for i in range(npages)]
lines += ["};", "", "#endif", ""]
lines += ["};", "#endif", "", "#endif", ""]
with open(HDR, "w") as f:
f.write("\n".join(lines))