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

582 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Звук в порте PoP — разбор и план
Дата: 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 Б —
довод в пользу давно назревшей реорганизации базовой памяти.