Путь 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>
52 KiB
Звук в порте PoP — разбор и план
Дата: 2026-08-20, музыка дописана 2026-08-25. Статус: PCM-эффекты реализованы; музыка — путь C (PCM через CBL), первый трек играет.
Задача пользователя: добавить звук. Приоритет — эффекты; музыку, если найдётся способ. Эффекты — обязательно 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)
- Эффекты — WAV через CBL, 8 бит, МОНО, единая частота. Проверено по
convert_digi_sound(seg009.c:2358): один байт на кадр, то есть моно, и байт беззнаковый ((b | b<<8) - 32768), центр 0x80 — ровно формат нашего CBL. Стерео в данных нет вовсе: каналы у оригинала размножаются уже на выходе (digi_audiospec->channels). - Музыка, первый заход — путь A (ноты спикера на AY).
- Заставки и титры — потом WAV. Конфликта с эффектами там нет: одновременно они не звучат.
- Музыка ПО ХОДУ игры (она может совпасть с эффектом) — открыто, два варианта: либо тоже WAV с ГАШЕНИЕМ эффектов на время музыки (музыка важнее — проверить на слух), либо путь B (MIDI -> три голоса AY).
- Все эффекты привести к одной частоте.
- Синтезатор для 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. Что проверить артефактом ДО кодинга
- Порты AY на Sprinter — записать в 0xFFFD/0xBFFD и убедиться, что MAME отдаёт звук (порты идут через DCP-таблицу, «стандартные ZX» — это ожидание, а не факт).
- CBL в режиме huge. Шапка
cbl.hговорит «код/данные в W2 (tiny/big)», но это скорее всего устаревшая оговорка: IM2-трамплин давно переделан на all-modes, и наш кадровый путь в huge работает. Проверитьcbl_openиз roomtest. - Совместное владение портом 0xFE. Бит 5 (луч) у нас держится через
_cbl_port_ref«немым» кодом частоты; когда откроется НАСТОЯЩИЙ CBL, владение переходит к нему. Убедиться, что пейсинг переживаетcbl_open/cbl_close. - Цена 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_swordSDLPoP. Звук 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-эффектов эту задачу не закрывает.
4. МУЗЫКА: решение пересмотрено (2026-08-25) — путь C вместо A
В §1б первым заходом был выбран путь A (ноты PC-спикера на AY), а путь
C (запись -> WAV -> CBL) стоил дорого из-за строки «нужен синтезатор на
хосте». Это обстоятельство отпало: у пользователя есть готовые записи
DOS-версии — applications/PoP/PoP1_DOS_music (flac/mp3/ogg/ogg_MT-32,
22 трека). Синтезировать нечего, остаётся ffmpeg -ac 1 -ar 10937 -f u8.
Что померено по этим записям
| группа | длительность | PCM 10 937,5 Гц |
|---|---|---|
| всё вместе (22 трека) | 339 с | 3 625 КБ = 227 EMM-страниц |
| игровые джинглы (10) | 57 с | 608 КБ |
| заставки и титры | 282 с | 3 017 КБ |
финальный won один |
115 с | 1 233 КБ |
Свободной EMM на старте ~3 440 КБ, так что вся музыка разом в память не влезает и не должна: трек грузится под сцену и освобождается после.
Что сделано
toolchain/pop_pack_music.py -> куски MUS\m<id>_<nn>.bin по 16 КБ
(bank_load_file читает файл только целиком и только в одну страницу) +
каталог pop_music_tbl.h. Длина трека хранится порциями по 128 байт,
а не байтами: 169 КБ в uint16 не влезает, 1350 блоков — легко.
pop_music.c (банк 9) грузит трек в EMM и ставит курсор; насос
pop_sfx_fill (резидент) получил третий источник: эффект важнее
музыки, музыка важнее тишины. Эффект музыку не сбрасывает — её курсор
стоит, пока эффект доигрывает, и она продолжается с места.
Проверено в MAME записью звука: трек story_1_absence на первом экране
истории, корреляция огибающих с эталоном 0,836, RMS 22,6 против 18,3
(разница — 8-битное квантование). Эффекты двери в PV-сцене после него
звучат как прежде, то есть освобождение страниц и возврат к набору
эффектов работают.
Цена и что осталось
- CBL один: пока играет музыка, эффектов нет. Для заставок это не важно (их там не бывает), для игровых джинглов — открытый вопрос §1б.4.
- Резидент вырос на 83 байта (третий источник в насосе) плюс 25 байт
данных под курсор и таблицу страниц; запас W2 — 151 байт. Кучи в
приложении нет (
mallocне слинкован), так что это чистый запас роста. - Банк 9 занят на 88 %. Следующий модуль туда уже не влезет — либо переносить, либо заводить банк 12.
won(77 страниц) в POP_MUS_PAGES=20 не помещается: финал придётся либо резать, либо стримить кусками по ходу.