Files
Sprinter-SDCC/applications/PoP/docs/sound_plan.md
T
snark13 47a4b084c4 Меню, статус-строка и оболочка игры: title/intro/cutscene/HoF, POP.CFG, палитры
Эта сессия (меню + текст в служебных полосах):

* Меню: рамка выделения считается от силуэта текста (SEL_PAD сверху и
  снизу), а не «на глаз»; все экраны центрируются в игровых 200 строках
  (UI_CENTER_TOP/UI_CENTER_FIELD) и в них помещаются; Enter и Space —
  равноправные клавиши действия (ui_action_down).
* Settings: убраны SHOW SPRINTER SCREEN и BACK, добавлены DEBUG BAR и
  ABOUT.  About показывает тот же текст, что стартовый Sprinter screen,
  минус строка про клавиши — общий about_text(), чтобы экраны не
  разъехались.  CONTROLS собирается таблицей и центрируется по
  фактическому числу строк.
* GAME PAUSED переехала в нижнюю статус-строку, как в оригинале
  (SDLPoP rect_bottom_text = {193,70,202,250}): это состояние программы,
  а не пункт меню.  POP_HP_Y вынесен в pop_cdraw.h — полосу делят два
  модуля.
* pop_status.c (банк 9) — текст в обеих служебных полосах.  Нижняя:
  порт display_text_bottom + таймера (QUICKSAVE/QUICKLOAD/SOUND ON/OFF,
  24 тика).  Верхняя отладочная переведена с палочек на текст
  «Level ##, Room ##, Speed: …, Sound: …, Immortal #» малым шрифтом, с
  своим форматированием чисел (без printf и без деления).
  Заявка сообщения — запись одного байта pop_status_msg: резидент W1/W2
  не растёт, весь рендер в банке.  Бюджет после правок не изменился
  (_CODE 23981, куча 267 Б).
* Цена вывода: блит глифа ~4,6 тыс. тактов независимо от размера, поэтому
  всё change-driven, отладочная строка перерисовывается ПО ПОЛЯМ, пробелы
  не блитятся вовсе, а вход в комнату заливает только игровое поле
  (pop_screen_fill_field) — борта от комнаты к комнате не меняются.
* Интро: в PV-сцене зазвучали пропавшие эффекты оригинала — закрытие
  ворот (4) и открытие двери покоев (51), из которой входит Джафар.
* docs/status_line_text.md — полная инвентаризация ВСЕХ текстов SDLPoP в
  статус-строке: геометрия, семантика text_time_total как идентификатора
  сообщения, мигание, рестарт по истечении 36/288.

Вместе с этим выкладывается накопленная работа по оболочке полной игры:
автомат состояний (pop_app), title, intro/PV и cutscene, attract-demo,
Hall of Fame, глобальный таймер, настройки и POP.CFG, модуль палитр и
fade, звуковой набор, host-тесты на новые швы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:58:36 +03:00

49 KiB
Raw Blame History

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

Дата: 2026-08-20. Статус: PCM-эффекты реализованы; музыка/AY не реализованы.

Задача пользователя: добавить звук. Приоритет — эффекты; музыку, если найдётся способ. Эффекты — обязательно 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
10 клинок о клинок roomtest.c после check_sword_hurt, если один из бойцов в кадре 167 seg000:1353
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

Что осталось не разведено — только МУЗЫКА (24/28 смерть, 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 Б — довод в пользу давно назревшей реорганизации базовой памяти.

12. Ворота: гейт слышимости и «решётка встала» (2026-08-20)

Проверка на сцене, которую предложил пользователь — уровень 9, комната 9: кнопка (1,8), челюсти (1,2), а ворота в комнате 4, тайл (1,9), то есть в комнате СЛЕВА. Нашлись три расхождения сразу.

12.1 Гейт слышимости был неверный

У нас стояло audible = (room == cur_room). У оригинала (play_door_sound_if_visible, seg007:1239) правило другое:

  • ворота в комнате СЛЕВА и в колонке 9 — СЛЫШНЫ (створка видна в шве);
  • ворота в отрисованной комнате и НЕ в колонке 9 — слышны;
  • особый случай: уровень 3, комната 2 — слышны всегда.

Сцена 9/9 попадает ровно в первый пункт, поэтому спуск решётки у нас молчал. Подъём при этом совпадал с оригиналом — потому что звук открытия (5) идёт БЕЗ гейта (seg007:386, прямой play_sound). Эта асимметрия и была подсказкой.

Взят вариант под FIX_GATE_SOUNDS (условия через ИЛИ): в config.h SDLPoP он определён безусловно.

12.2 Потерян звук «решётка встала» (7)

gate_stop() (seg007:05E3) зовётся из ТРЁХ мест animate_door и каждый раз играет звук 7 через гейт слышимости: конец закрытия, открытие насовсем и ветка «уже 0xFF». У нас во всех трёх стояло только *type = -1 без звука. Добавлено. Лязг после ОБЫЧНОГО открытия (seg007:395) остаётся без гейта — там оригинал зовёт play_sound напрямую.

12.3 Кнопка: у оригинала есть параметр playsound

trigger_button(playsound, ...) — в трёх местах он нулевой: вход на уровень (seg003:170), выход Джаффара (seg002:520) и зелье «открыть» (seg006:1890, у нас не портировано). Мы играли щелчок всегда. Добавлен параметр snd.

12.4 Почему щелчок кнопки слышно через раз — это НЕ баг

Бюджет сцены 9/9 (длительности после пересчёта на 10 937,5 Гц):

звук длительность prio
челюсти (47) 465 мс 0x10
решётка вниз (4) 97 мс 0x32
решётка вверх (5) 123 мс 0x37
решётка встала (7) 75 мс 0x30
кнопка (3) 106 мс 0x66

Цикл челюстей — 15 кадров = 1229 мс (замерено брейкпоинтом на номинации: 25 805 000 тактов между укусами). Значит укус занимает 465 мс, пауза 764 мс. Кнопка (prio 0x66) перебить челюсти не может (0x66 > 0x10), поэтому слышна только если нажатие попало в паузу — примерно в 6 случаях из 10. Подтверждено пользователем на живой сцене.

12.5 Грабли сцены

Если игра стартует ПРЯМО в комнате с челюстями, они не заводятся сами: нужно сходить Кидом на левую кнопку и вернуться. Это поведение оригинала (trob челюстей создаётся событием), а не наш дефект — учитывать при постановке автотестов.

13. Повторный аудит игрового звука (2026-08-24)

Проверены три независимых слоя: содержимое атласов, места вызова и живой тракт play -> tick -> CBL.

  • В восьми SND*.ATL есть все 31 оцифрованных ресурса: 0..23, 44..49 и 51; ненулевая страница/длина есть у каждой записи таблицы.
  • Для всех 30 PCM-эффектов, которые могут возникать непосредственно в игре, есть место вызова. Последним пропуском был звук 10 при столкновении клинков; условие перенесено буквально из check_sword_vs_sword SDLPoP. Звук 51 относится к сцене с принцессой, а не к игровому циклу.
  • Звук 19 «Кид достал меч» проверен в MAME брейкпоинтами. В момент вызова предыдущий PCM уже закончился (sfx_left=0), номинация дошла до pop_sfx_tick, после чего курсор получил id 19, страницу 5, смещение 0x1500 и длину 2816 байт. В этом прогоне его не подавляли решётка, плиты, шаги или приоритеты. Если он всё ещё субъективно не слышен, искать надо после выбора эффекта — в непрерывности CBL/громкости самого сэмпла.
  • После продолжительного прогона title/intro/demo/menu CBL обслужил 3303 блока и сообщил 0 программных недоливов (cbl_requests=0x0CE7, cbl_underruns=0). Это исключает возврат fill=0, но само по себе не измеряет запоздание прерывания внутри слишком длинной секции DI.
  • Регрессия full-game на входе в Level 1 оказалась именно запозданием CBL: pop_level_switch открывал его ДО блокирующего BIOS fade-in. Demo был чистым, потому что включал палитру без fade. Теперь загрузчик оставляет CBL закрытым, а caller открывает его после окончательной палитры/QuickLoad; скрежет на Level 1 исчез в MAME. Однократный щелчок самого первого cbl_open за всю MAME-сессию остаётся отдельной низкоприоритетной задачей.
  • Открытие pause menu у SDLPoP беззвучно; движение играет 21, вход/выход из подменю — 22, изменение настройки — 10. Эти вызовы перенесены. На время полного копирования страницы и файловых операций CBL закрывается, после flip открывается снова: аппаратная половина не должна повторять старые данные и давать «скрежет».

Отдельно остаётся игровая музыка и сигнальные мелодии без PCM: смерть (24/28), начало/появление Shadow (25), встреча Jaffar (29), большое и малое зелья (30/33), Shadow (32), победа/меч (37), перо (39), конец уровня (41) и победа над Jaffar (43). Вызовы и AY-проигрыватель для них ещё не реализованы; наличие всех PCM-эффектов эту задачу не закрывает.