Files
Sprinter-SDCC/applications/PoP/docs/sound_plan.md
T
snark13 a630568a8b Звук PoP: приоритеты и перебиваемость вместо «всегда перебивать»
Пользователь услышал расхождение с SDLPoP: у нас решётка обрывалась
приземлением Кида, в оригинале доигрывает до конца, а приземления не
слышно.  Оказалось, упущен целый механизм.

play_sound (seg000:12C5) НЕ играет, а только номинирует кандидата на
кадр — из нескольких выживает важнейший (меньше prio = важнее, при
равенстве последний).  play_next_sound (seg000:1304) раз в кадр решает,
запускать ли: можно, только если ничего не играет ЛИБО текущий помечен
перебиваемым и новый не менее важен.  Иначе номинант выбрасывается —
очереди в оригинале нет.

Отсюда всё, что слышно: gate_closing_fast неперебиваем и доигрывает
целиком; челюсти (prio 0x10) всегда важнее решётки (0x32), поэтому
решётка звучит только в паузах между укусами.

Таблицы из SDLPoP с учётом fix_sound_priorities (в его config.h он
определён безусловно).  Створка двери уровня — единственная запись,
правимая на ходу, вынесена в отдельный байт.  Добавлен пропущенный
stop_sounds на завершении открытия двери (seg007:455).

Проверено записью MAME: старт уровня 1 был 135+210 мс (решётка, обрезанная
на 80 мс), стал один всплеск 455 мс с корреляцией огибающей +0,889 со
звуком 6.

Заведён BUG-SND-FIRSTRUN: искажение первого эффекта при первом запуске
после загрузки системы — вероятно, лечится _cbl_prime, но проверить можно
только на железе.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:46:04 +03:00

41 KiB
Raw Blame History

Звук в порте PoP — разбор и план

Дата: 2026-08-20. Статус: разбор, кода нет.

Задача пользователя: добавить звук. Приоритет — эффекты; музыку, если найдётся способ. Эффекты — обязательно WAV, а не PC-спикер (уточнение 2026-08-20; см. §1а — оказалось, что они и так все в WAV). Ниже — что реально лежит в ассетах, что умеет железо, и почему получившийся план вышел проще, чем ожидалось.

1. Главный вывод

Ни MIDI разбирать, ни ноты сочинять не придётся, и ресэмплировать тоже.

  • Эффекты уже лежат 8-битным беззнаковым PCM на 11 000 Гц, а у CBL есть режим 10 937,5 Гц — расхождение 0,6 %, на слух неразличимо. Формат сэмпла совпадает с нашим CBL байт в байт (cbl.h: 8 бит, беззнаковый, центр 0x80). То есть данные играются как есть, без конверсии.
  • Музыка есть в виде списков нот PC-спикера — 7 КБ на всю игру, а нота там задана прямо в ГЕРЦАХ. Пересчёт в делитель AY — одно деление.
  • AY и COVOX на Sp2000 сведены в один ЦАП TDA1543 (док Ивана Мака, §5), значит музыка на AY и эффекты через CBL звучат ОДНОВРЕМЕННО, и смешивать их программно не надо.

1а. Уточнение после разбора ВСЕХ наборов MS-DOS версии (2026-08-20)

Пользователь попросил, чтобы эффекты были не PC-спикером, а WAV, и заодно посмотреть mt32snd[1-2].dat. Разобрал все восемь .dat из MSDOS/. Ответ короткий: эффекты И ТАК все до одного есть в WAV, а вот у музыки WAV нет ни в одном наборе.

набор формат какие звуки сколько
digisnd1..3 WAV, 8 бит PCM эффекты 0..23, 44..49, 51 31
mt32snd1..2 MIDI для Roland MT-32 ТЕ ЖЕ эффекты 0..23, 44..51 31
midisnd1..2 MIDI (AdLib/GM) музыка 24..43, 50, 52..56 22
ibm_snd1..2 ноты PC-спикера всё подряд, 0..56 57

Здесь пряталась ловушка: mt32snd по имени похож на «музыку получше», а на деле это набор ЭФФЕКТОВ для владельцев MT-32 — те же id, что у digisnd. Музыки в нём нет вовсе.

Частоты WAV: 28 звуков на 11 000 Гц, по одному на 8 200, 14 000 и 2 750. Итого 112 922 сэмпла = 11,4 с, ~110 КБ ≈ 6,7 EMM-страниц.

Только PC-спикером, без альтернатив, остаются четыре id: 31, 34, 42 (пустые) и 38 blink — четыре ноты. То есть на весь звук игры спикер нужен ровно для одного писка.

Музыка: WAV нет, есть три пути

путь данные что получится цена
A. Ноты PC-спикера на AY 7 КБ один квадратный голос — ровно то, что слышали на IBM PC 1989 секвенсор на полсотни строк
B. MIDI -> AY, три голоса 27 КБ исходника богаче: бас + мелодия + арпеджио разбор MIDI + раскладка по каналам
C. MIDI -> WAV на хосте, стрим через CBL см. ниже настоящее звучание, любое нужен синтезатор на хосте + место

Про объём для пути C (замерено по длительностям треков):

группа треков длительность WAV 11 кГц
звучат ПО ХОДУ игры (гимн уровня, смерть, зелья, перо, победа) 12 74,7 с 803 КБ = 50 EMM-страниц
заставки и титры 10 248,1 с 2 665 КБ = 167 страниц

Игровая половина в EMM влезает (при ~215 свободных страницах), а заставочная — нет, её пришлось бы стримить с диска. Но заставки идут тогда, когда игра ничего не рисует, так что стрим там как раз уместен.

Предложение: начинать с A (7 КБ, работает сразу, ноль рисков), а C держать как отдельную фазу — она ортогональна: проигрыватель WAV для музыки это тот же cbl_push, что и для эффектов, только длиннее буфер. B имеет смысл только если C окажется неподъёмным по месту.

1б. РЕШЕНИЯ (пользователь, 2026-08-20)

  1. Эффекты — WAV через CBL, 8 бит, МОНО, единая частота. Проверено по convert_digi_sound (seg009.c:2358): один байт на кадр, то есть моно, и байт беззнаковый ((b | b<<8) - 32768), центр 0x80 — ровно формат нашего CBL. Стерео в данных нет вовсе: каналы у оригинала размножаются уже на выходе (digi_audiospec->channels).
  2. Музыка, первый заход — путь A (ноты спикера на AY).
  3. Заставки и титры — потом WAV. Конфликта с эффектами там нет: одновременно они не звучат.
  4. Музыка ПО ХОДУ игры (она может совпасть с эффектом) — открыто, два варианта: либо тоже WAV с ГАШЕНИЕМ эффектов на время музыки (музыка важнее — проверить на слух), либо путь B (MIDI -> три голоса AY).
  5. Все эффекты привести к одной частоте.
  6. Синтезатор для MIDI -> WAV — решать ближе к делу; годятся и онлайн- конвертеры, хоть вручную, если fluidsynth/timidity не поставится.

Про единую частоту (замер)

Приводим не к 11 000, а ровно к 10 937,5 Гц — частоте CBL (CBL_FREQ_10K9). Тогда тон точен, а не «на 0,6 % ниже»: проигрывание 11 000 Гц данных на 10 937,5 даёт сдвиг 9,9 цента, что на коротком эффекте не слышно, но бесплатно избавиться от него всё равно приятно — пересчитывать три файла всё равно придётся.

id звук было станет дельта
15 leveldoor_sliding 2 750 Гц, 4 436 сэмплов 17 643 +13 207 Б
23 footstep 8 200 Гц, 996 1 329 +333 Б
51 princess_door_opening 14 000 Гц, 6 188 4 834 1 354 Б
остальные 28 (11 000 Гц) ×0,9943 588 Б

Итог: 112 922 -> 124 531 Б, 6,9 -> 7,6 EMM-страниц. Рост целиком от leveldoor_sliding: источник у него 2 750 Гц, вчетверо реже целевой, и апсэмплинг не улучшит звучание — только уравняет формат. Платим 0,8 страницы за то, что CBL открывается ОДИН раз и частоту менять не надо никогда — ни между эффектами, ни при переходе на музыку-WAV.

(Альтернатива для него — хранить как есть и повторять каждый сэмпл четырежды в рантайме: 2 750 × 4 = 11 000 ровно. Это код в fill() ради 13 КБ; не стоит того, но если место когда-нибудь прижмёт — вариант есть.)

1в. Бюджет памяти EMM (живой замер 2026-08-20)

Замерено mem_info из работающей программы (уровень 1), а не посчитано на бумаге: инструментовка ставилась временно и откатана.

страниц КБ
всего в машине 256 4 096
система (DSS) + сам exe: база + 8 банков кода 43 688
наши ассеты 81 1 296
занято 124 1 984
свободно 132 2 112 (2,06 МБ)

Разбивка 81 страницы ассетов (сходится точно):

набор страниц
Тень (sk* 28 + sf* 4) 32
Кид (kid0..27) 28
фон тайлсета (env 10 + wall 1 + fore 1) 12
страж 5
зелья (chtab_1), меч, kid_data.bin, страница уровня по 1

Самый крупный потребитель теперь — набор Тени, 32 страницы, больше самого Кида. Если место когда-нибудь прижмёт, там есть очевидный резерв (кадры смерти и позы, в которых Тень не бывает), но при 132 свободных страницах трогать незачем.

Что из этого следует для звука

статья страниц останется свободно
эффекты WAV, все 31, 10 937,5 Гц 8 124
музыка ПО ХОДУ игры в WAV (путь C, 12 треков) 50 74
заставки и титры в WAV (10 треков, 248 с) 167 не влезает

То есть эффекты — капля, игровая музыка в WAV тоже поместится, а заставочную придётся стримить с диска в любом случае (что и планировалось: во время заставок игра ничего не рисует).

Оговорка: 132 свободных страницы — это на уровне 1. На уровне 9 добавятся зеркальные наборы (pop_vflip_load_all: 28 Кид + 5 страж + меч = 34 страницы), останется ~98. Проверять запас надо ИМЕННО ТАМ.

1г. MSDOS против SDLPoP: чем отличаются наборы (сверено 2026-08-20)

У нас лежат ДВЕ копии звука — оригинальные .dat в MSDOS/ (версия 1.3/1.4) и распакованные ассеты SDLPoP/data/ (версия 1.0/1.1). Разница есть, и она влияет на выбор источника.

Оцифровка: берём MSDOS

Заголовок разный (digi_new_type против digi_type), но 28 звуков из 31 совпадают побайтно. Различаются три, и все не в пользу SDLPoP:

id звук MSDOS SDLPoP
10 sword_vs_sword 5 020 сэмплов 3 504
11 sword_moving 1 172 1 172, но другие байты
48 spiked 5 069 7 — то есть звука нет

spiked в наборе SDLPoP фактически пустой. Поэтому упаковщик читает MSDOS/digisnd*.dat, а не распакованные ассеты — в отличие от графики, где источник наоборот SDLPoP.

MIDI: если дойдём до музыки — брать SDLPoP

Здесь всё наоборот. Содержимое музыкально то же (деление 480, те же каналы 0..7 плюс ударные), но:

MSDOS SDLPoP
формат MIDI 0 — всё слито в ОДНУ дорожку 1 — 8-9 дорожек
размер (звук 24) 327 Б 448 Б
размер (звук 56) 13 587 Б 12 773 Б

Формат 1 с отдельной дорожкой на инструмент — это готовое разделение голосов. Для пути B (MIDI -> три канала AY) оно решает половину задачи: дорожки можно выбирать напрямую (бас / мелодия / гармония), а не разбирать слитый поток и догадываться, что чем было.

Итог: эффекты из MSDOS, музыка (когда дойдёт) из SDLPoP.

2. Что лежит в ассетах (замерено, а не по памяти)

Звук в PoP адресуется как ресурс 10000 + N, N = 0..56 — 57 звуков (load_sound, seg009.c:2289). Наборов три, и они ПАРАЛЛЕЛЬНЫЕ: один и тот же звук есть в нескольких видах.

набор что это объём покрытие
DIGISND1..3.DAT оцифровка, 8 бит PCM 103 941 Б 31 звук (эффекты)
MIDISND1..2.DAT MIDI-музыка 27 776 Б музыка
IBM_SND1..2 (распакованы) ноты PC-спикера 7 КБ все 57

2.1 Оцифровка (эффекты)

Разбор контейнера: индекс по 8 байт на запись (id, offset, size), первый байт ресурса — контрольная сумма, тело за ней (спецификация POP-DAT-FormatSpecifications, §3.1.2 — на этом я сначала споткнулся и читал мусор). Тело — digi_type: word rate, word count, word unk, byte size, дальше сэмплы.

  • 31 звук, все 8-битные;
  • частоты: 28 звуков на 11 000 Гц, по одному на 8 200 и 14 000;
  • 103 701 сэмпл = 9,4 секунды, 103 941 Б ≈ 6,3 EMM-страницы.

2.2 Ноты PC-спикера (и эффекты, и музыка)

Формат: byte type(=0), word tempo, дальше тройки word frequency, byte length; frequency <= 1 — пауза, 0x12 — конец. Ключевое, что пришлось смотреть в play_speaker_sound/speaker_callback (seg009.c):

  • frequency — это ГЕРЦЫ напрямую (generate_square_wave(stream, (float)note->frequency, ...)), а не делитель PIT, как кажется по маленьким числам;
  • длительность ноты = length / tempo СЕКУНД.

Замеры по всем 57 звукам: 2212 нот, частоты 16..65507 Гц, длительности 1,74..2571 мс.

Из них 23 звука — те, у которых оцифровки НЕТ, то есть вся музыка: заставки, гимны уровней, смерть, победа, титры (id 24..43, 50..56). 1469 нот, и вот их длительности:

длительность ноты нот доля
< 3 мс 0 0 %
3..12 мс 2 0,1 %
12..25 мс 140 9,5 %
> 25 мс 1327 90,3 %

Это число решает вопрос про таймер — см. §4.

3. Что умеет железо (док Ивана Мака §5 + MAME)

  • AY-3-8910/8912 в ПЛМ, «программируется по стандартным описаниям» — то есть ZX-порты; в MAME он заведён как AY8910(config, "ay8912", X_SP/24), то есть тактовая 1,75 МГц. Период канала = 109375 / частота(Гц), 12 бит (макс 4095) → снизу берутся частоты от ~27 Гц. Точные Z80-адреса портов идут через таблицу DCP, а не напрямую — проверить артефактом до кодинга (ожидаем ZX-стандарт 0xFFFD/0xBFFD).
  • CBL — COVOX с буфером 256 Б, две половины по 128; бит 7 порта 0xFE показывает играющую половину, порт управления 0x4E. Прерывание — когда половина сменилась.
  • Бипер (бит 5 порта 0xFE) — туда же в ЦАП. Нам не нужен.
  • Всё это сведено в один ЦАП, поэтому AY и CBL звучат вместе.

У нас уже есть готовая обвязка CBL (libc/include/cbl.h): callback fill(n), выдача блока через cbl_push_otir() (порт 0x4F) или через акселератор, коды частот, счётчик недоливов. Своего кольца библиотека не держит — данные пропихиваются прямо из наших EMM-страниц.

4. Предлагаемая архитектура

           эффекты (31 шт, 8 бит 11 кГц)          музыка (23 шт, ноты)
                     │                                    │
              EMM-страницы (6,3)                    7 КБ нот в банке
                     │                                    │
        cbl_push_otir из fill()                  запись 3 регистров AY
                     │                                    │
                 CBL (10,9 кГц) ──────┐      ┌────────── AY (1,75 МГц)
                                      ▼      ▼
                                   TDA1543 (аппаратное смешивание)

Один источник прерываний — CBL. Его callback приходит каждые 128 сэмплов = 11,7 мс при 10,9 кГц, и он же двигает секвенсор музыки. Отдельный таймер (CTC) НЕ нужен: по таблице из §2.2 короче 12 мс всего 2 ноты из 1469 — они растянутся на один тик, чего не слышно.

Почему это важно: irq_ctc_install сейчас требует кода в W2 (tiny/big), а roomtest — huge, и попадёт ли туда CTC-трамплин, зависит от раскладки. Обойтись без него — значит не открывать этот фронт вовсе.

Пейсинг кадра при этом не страдает. Наш темп считается ПО ЛУЧУ (pop_pace.h), а не по кадровым прерываниям, поэтому то, что CBL-ветка трамплина делает приватный RETI и съедает кадровые прерывания, нам безразлично. Если бы пейсинг остался на прерываниях — звук бы его сломал.

5. Объём работ

фаза что оценка
З1 распаковщик pop_pack_sound.py: DAT → .snd-страницы EMM (эффекты) + .not (ноты музыки) формат уже разобран
З2 pop_sfx.c: cbl_open + fill(), таблица «звук → страница/смещение/длина», pop_sfx_play(id) ядро
З3 развесить вызовы: 66 мест play_sound в оригинале; у нас часть уже помечена TODO (pop_ctrl.c:408, guards.c:992, pop_map.c:3035/3107, …). Плюс бесплатный кусок: опкод SEQ_SOUND в seqtbl уже разбирается нашим play_seq (pop_kid.c:326) — шаги, приземления и прочее поедут сами механическая
З4 pop_music.c: секвенсор нот на AY (путь A), тик из CBL-callback небольшая
З6 заставки и титры — WAV-музыка потоком (эффекты в это время не звучат) после З1-З4
З7 музыка по ходу игры: WAV с гашением эффектов ЛИБО путь B — решать по итогам З6 открыто
З5 приоритеты и вытеснение: у оригинала play_sound глушит предыдущий (stop_sounds), музыка и эффект — разные каналы правила из seg009

6. Что проверить артефактом ДО кодинга

  1. Порты AY на Sprinter — записать в 0xFFFD/0xBFFD и убедиться, что MAME отдаёт звук (порты идут через DCP-таблицу, «стандартные ZX» — это ожидание, а не факт).
  2. CBL в режиме huge. Шапка cbl.h говорит «код/данные в W2 (tiny/big)», но это скорее всего устаревшая оговорка: IM2-трамплин давно переделан на all-modes, и наш кадровый путь в huge работает. Проверить cbl_open из roomtest.
  3. Совместное владение портом 0xFE. Бит 5 (луч) у нас держится через _cbl_port_ref «немым» кодом частоты; когда откроется НАСТОЯЩИЙ CBL, владение переходит к нему. Убедиться, что пейсинг переживает cbl_open/cbl_close.
  4. Цена fill() в кадре. 128 байт через cbl_push_otir раз в 11,7 мс — замерить тем же способом, что и остальное (брейкпоинт + totalcycles).

7. Про MIDI — почему не он

Музыка в MIDISND — настоящие MThd/MTrk чанки, 27 КБ. Чтобы играть их на AY, нужен разбор MIDI, раскладка каналов на три голоса и таблица инструментов — это отдельный проект, и звучать он будет НЕ так, как оригинал на PC. А набор PC-спикера — это ровно то, что слышал игрок на IBM PC 1989 года: один квадратный голос. Он у нас есть целиком, весит 7 КБ и ложится на AY напрямую.

Если позже захочется богаче — материал уже будет разобран, и можно разложить те же мелодии на три канала AY (бас/мелодия/арпеджио), не трогая ни данные, ни секвенсор.


8. Разводка вызовов по коду (сделано 2026-08-20)

Портированы ВСЕ места play_sound() SDLPoP, у которых есть оцифровка (id 0..23, 44..49, 51 — остальные id это музыка, у них в pop_sound_tbl.h длина 0, и вызов просто глушит текущий эффект).

id что где у нас оригинал
0 разбился насмерть pop_map.c land seg005
1 крик падения pop_map.c do_fall seg005:39
2 плита рухнула pop_room.c (обе ветки посадки) seg007
3 кнопка нажата pop_trob.c seg007
4/5/6/7 ворота: закрываются / открываются / рухнули / стоп pop_trob.c seg007
8 удар о стену pop_map.c bumped_fall/bumped_floor + seqtbl seg004/seg006
9 зацеп за карниз pop_map.c check_grab seg006
11 свист клинка мимо guards.c check_hurting seg002:0DAE
12/13 ранен соперник / Кид guards.c hurt_by_sword seg002:0C1F
13 Кид ранен зельем pop_map.c ветка «злого» зелья seg006:1894
14/15 дверь уровня: закрывается / едет pop_trob.c seg007
16 средняя посадка; толчок о стража pop_map.c land / bump_into_opponent seg005/seg003:0654
17 мягкая посадка pop_map.c land seg005
18 пьёт seqtbl (SEQ_SOUND) seg006
19 вынул меч pop_ctrl.c seg005:945
20/21/22 дрожит плита pop_map.c loose_shake seg007:0E55
23 шаг seqtbl (SEQ_SOUND) seg006
44 скелет оживает guards.c pop_check_skel seg002:106D
45 прыжок в зеркало pop_map.c jump_through_mirror seg003:0617
46 сожрал чомпер pop_map.c seg004
47 чомпер щёлкнул pop_trob.c (кадр 2) seg007
48 напоролся на пики pop_map.c seg005
49 пики пошли pop_map.c start_anim_spike seg007:08F6

Что осталось не разведено — только МУЗЫКА (25 презентация, 26 объятия, 27/35/40 заставки, 29 встреча Джафара, 30/33 зелья, 32/41 конец уровня, 36 время вышло, 37 победа, 43 смерть Джафара, 50/52/53 сюжетные вставки) и 51 (дверь принцессы, тоже из заставки). Их черёд — фаза «музыка».

Квирк, за которым следить. Звук ворот у оригинала звучит не всегда, а по условию видимости (play_door_sound_if_visible, seg007:1250): либо ворота в комнате слева и стоят в 9-й колонке, либо ворота в НАРИСОВАННОЙ комнате и колонка не 9-я. У нас это параметр audible у animate_door.

Тряска плиты — свой домен prandom. Оригинал берёт номер сэмпла (20/21/22) из общего генератора и вдобавок «сжигает» один бросок ради совместимости с DOS-версией; у нас последовательности разведены по доменам (impl_diff.md), поэтому у тряски свой сид, а холостой бросок не делаем — на розыгрыши физики и кладки это не влияет.

9. Цена звука в тактах (замер 2026-08-20, MAME)

Вопрос был поставлен так: звук идёт по прерываниям, значит размазан по всем фазам кадра — и если фазы укладываются, всё хорошо? Да, но проверять это надо не по фазам, а по двум числам, потому что нагрузка от звука постоянная и от сцены не зависит вовсе.

9.1 Одно прерывание CBL

Зонды: bpset на входе трамплина (_irq_tramp) и на reti ветки CBL, разница totalcycles. 398 замеров в сцене 11/15.

величина значение
цена одного прерывания 7 825 тактов ровно, с разбросом до 8 299 (среднее 8 001)
период между прерываниями 245 759 тактов (= 128 сэмплов на 10 937,5 Гц)
доля процессорного времени 8 001 / 245 759 = 3,26 %

Цена постоянная, потому что работа фиксированная: OTIR ровно 128 байт плюс скобка сохранения контекста. Ветвлений по данным в насосе нет.

9.2 Дрожание обслуживания — риск для ЗВУКА, не для кадра

Период плавает 228 804 … 262 734, то есть прерывание опаздывает максимум на ~17 000 тактов = 0,8 мс. Это самая длинная DI-скобка в коде (акселератор режется по 16 строк, memory sprinter_wait_states_2x). Буфер CBL — 128 сэмплов = 11,7 мс, запас 14×. Недолива быть не может; счётчик cbl_underruns() это подтверждает косвенно (наш fill всегда возвращает 1, поэтому он ловит только отсутствие данных, не опоздание).

9.3 A/B в одном прогоне (Ctrl+S), сцена 11/15

Один и тот же кадр, звук выключается на ходу — сравнение чистое.

работа min работа max работа avg прерываний CBL на кадр
звук ВКЛ 453 132 572 532 498 064 1,40
звук ВЫКЛ 444 348 559 434 487 372 0,00
разница +8 784 +13 098 +10 692 (+2,2 %)

Разница на лёгком кадре (+8 784) — ровно одно прерывание, сходится с §9.1. CBL/кадр = 0 при выключенном звуке подтверждает, что Ctrl+S реально ЗАКРЫВАЕТ CBL, а не глушит сэмпл: иначе насос продолжал бы отдавать блоки тишины и платить те же 3,26 %.

9.4 Укладываемся ли

Логический кадр (pop_pace.h): NORMAL = 4 растра вне боя = 1 720 000 тактов, FASTEST = 3 растра = 1 290 000.

работа доля NORMAL доля FASTEST
11/15, обычная позиция 498 064 29 % 39 %
11/15, тяжёлая позиция (Кид на 7 px правее) 750 066 макс 44 % 58 %

Период кадра за все прогоны: 1 719 936 … 1 720 752 — ровно 4 растра, ни одного проскока. Звук занимает 1,1 % бюджета NORMAL и 1,5 % FASTEST.

9.5 Где 3,26 % МОГЛИ БЫ стоить дорого

Ответ «всё размазано, если фазы влезли — ок» верен с одной оговоркой. Пейсинг квантован растром: работа 1,00 растра и 1,02 растра дают РАЗНЫЙ период кадра (3 против 4 интервалов), то есть скачок сразу на 20 мс. Значит звук опасен ровно в одной ситуации — когда сцена стоит в пределах ~8 000 тактов НИЖЕ кратного растру порога. Сейчас ближайший запас — 540 000 тактов до порога FASTEST, то есть в 60 раз больше цены звука. Проверять эту оговорку заново стоит только если работа кадра подберётся к 430 000 или 860 000 вплотную.

10. Мусор при включении и щелчок на выходе (разбор 2026-08-20)

Жалоба: «при старте, когда разрешается звук, проходит кусок мусора». Разобрано записью выхода MAME в WAV (-wavwrite) — по огибающей и автокорреляции, а не на слух.

10.1 На старте мусора НЕТ; это настоящие звуки

От cbl_open до первого эффекта в записи точная цифровая тишина (размах 1 при разрешении 16 бит). Дальше — два штатных звука:

что когда длительность
gate_closing_fast (6) — решётка в комнате СЛЕВА +0,30 с после cbl_open обрывается на 80 мс
soft_land (17) — Кид приземляется +0,38 с 383 мс

Опознаны корреляцией огибающих с оригинальными сэмплами digisnd: звук 6 даёт +0,72 с начала записи, звук 17 — +0,32 со сдвигом 80 мс. Приземление на старте КОРРЕКТНО: start_pos уровня 1 — тайл (0,0), а он space, то есть Кид падает на ряд ниже, на площадку с факелами.

Обрыв первого звука вторым — тоже поведение оригинала, а не наш дефект: play_digi_sound (seg009.c:2402) начинается с stop_digi(), голос ОДИН. Ощущение «мусора» даёт именно 80-мс огрызок скрежещущей решётки.

Звук кнопки (3) при этом не слышен: pop_sfx_play(3) случается ДО cbl_open и глохнет. С SDLPoP совпадает (там на старте тоже только решётка), но держится это на порядке вызовов — если поднимать звук раньше pop_start_level, щелчок кнопки станет слышен.

10.2 Незалитый буфер CBL — дефект есть, но в MAME он немой

Буфер CBL (256 слотов) железо не чистит ни сбросом, ни записью в порт управления, а эта запись сразу пускает воспроизведение с нулевого слота. Значит первые 256 сэмплов (23,4 мс) — то, что лежало раньше. Разбор по sprinter.cpp: case 0x89 делает m_cbl_cnt = 0; m_cbl_wa = 0, а прерывание «долей половину» приходит только на 128-м слоте и ставит указатель на ПРОТИВОПОЛОЖНУЮ половину — своими данными звук идёт лишь с третьей половины.

В MAME это не слышно: эмулируемый буфер стартует нулями, а ЦАП двухдополнительный, то есть 0 = середина шкалы. На ЖЕЛЕЗЕ там неинициализированное ОЗУ — ровно тот мусор, который ловился ещё на тестовых примерах CBL. Лечение — _cbl_prime в cbl_open: сразу после включения 256 записей байта тишины в порт данных (заранее нельзя, запись проходит только при поднятом bit7). Стоит 6 400 тактов один раз за открытие; за это время таймер уходит на два-три слота.

10.3 Щелчок на выходе — ГОЛОДАНИЕ насоса, вылечено

На выходе по ESC в записи было ровно 11 мс шума на полной громкости (размах 33 671 — громче всего в прогоне), потом мгновенная тишина. 11 мс = один блок CBL (128 сэмплов = 11,7 мс), то есть один пропущенный долив: pop_shutdown звал closegraph/pop_bg_free/pop_kid_free (а это ESTEX на каждый атлас) ПРИ ОТКРЫТОМ звуке, насос не успевал, и железо доигрывало несвежую половину.

Лечение: pop_sfx_close() первым действием pop_shutdown. Проверено второй записью — всплеска на выходе больше нет. Механизм тот же, из-за которого звук глушится на время загрузки уровня.

11. Приоритеты и перебиваемость: звук у оригинала НЕ «всегда перебивать»

Пользователь услышал расхождение: у нас решётка обрывалась приземлением Кида, в SDLPoP — доигрывала до звонкого конца, а приземления не было слышно вовсе. Разбор исходника показал, что мы упустили ЦЕЛЫЙ МЕХАНИЗМ.

11.1 Модель оригинала

play_sound(id)          seg000:12C5 — только НОМИНИРУЕТ кандидата на кадр:
    if next < 0 || prio[id] <= prio[next]:  next = id

play_next_sound()       seg000:1304 — раз в кадр решает, запускать ли:
    if next >= 0:
        if !играет_что_то ||
           (перебиваем[текущий] && prio[next] <= prio[текущий]):
              текущий = next;  запустить
    next = -1               // НЕ запустили -> номинант ВЫБРОШЕН, очереди нет

Три следствия, каждое слышно:

  • Неперебиваемый звук доигрывает целиком. У gate_closing_fast (6) interruptible = 0, поэтому приземление Кида (17) в этот момент пропадает совсем — не откладывается, а именно теряется.
  • Внутри кадра выживает важнейший. Меньше prio — важнее; при равенстве побеждает ПОСЛЕДНИЙ (сравнение <=).
  • Два источника не «чередуются как получится». Челюсти (47, prio 0x10) всегда важнее решётки (4, prio 0x32): решётка не может перебить укус, а укус решётку — может. Отсюда и картина на ур. 9 к. 9, где решётка звучит только в паузах между укусами.

11.2 Что сделано у нас

pop_sfx_play теперь только номинирует; запуск — в pop_sfx_tick, который зовётся раз в кадр в конце отрисовки (там же, где оригинал зовёт play_next_sound, seg000:954). Таблицы snd_prio (57 байт) и битовая карта snd_intr (8 байт) — в резиденте, значения из SDLPoP С УЧЁТОМ fix_sound_priorities(): в config.h SDLPoP FIX_SOUND_PRIORITIES определён безусловно, значит сравниваемся мы с исправленным вариантом (звук 10 → 0x0D, 48 → 0x15, 49 перебиваем).

Створка двери уровня (15) — единственная запись, которую оригинал правит на ходу: перебиваема при закрытии, нет при открытии (seg007:442/464). Держим отдельным байтом pop_sfx_slide_intr, чтобы таблица осталась в ПЗУ. Там же добавлен пропущенный stop_sounds() на завершении открытия двери (seg007:455) — без него неперебиваемый съезд (1,6 с) блокировал бы очередь.

Звуки без оцифровки (музыка, длина 0) не номинируются вовсе — порт проверки if (NULL == sound_pointers[id]) return;. Раньше такой id глушил живой эффект.

11.3 Проверка

Записью MAME, старт уровня 1:

всплески что это
до 135 мс + 210 мс решётка, обрезанная приземлением на 80 мс
после один, 455 мс решётка целиком, корреляция огибающей со звуком 6 +0,889

Цена: резидент +~250 Б (таблицы + логика), куча ужалась с 347 до 134 Б — довод в пользу давно назревшей реорганизации базовой памяти.