CBL: заливка буфера тишиной при открытии + щелчок на выходе из PoP

Две разные болячки, обе разобраны записью звука MAME в WAV.

libc: буфер CBL железо не чистит, а запись в порт управления сразу пускает
воспроизведение с нулевого слота — первые 256 сэмплов (23,4 мс) уходит то,
что лежало раньше.  Своими данными звук идёт лишь с третьей половины:
прерывание приходит на 128-м слоте и ставит указатель на противоположную
половину.  _cbl_prime заливает буфер тишиной сразу после включения (раньше
нельзя — запись проходит только при поднятом bit7).  В MAME это немо
(эмулируемый буфер стартует нулями при двухдополнительном ЦАП), на железе
это ровно тот мусор, что ловился на тестовых примерах CBL.

PoP: на выходе по ESC звучало ровно 11 мс шума на полной громкости — один
пропущенный блок (128 сэмплов).  Причина: pop_shutdown освобождал атласы и
графику через ESTEX при открытом звуке, насос не успевал долить.  Звук
гасим первым действием.  Проверено записью — всплеска больше нет.

Заодно записан разбор стартового всплеска (sound_plan.md §10): это не
мусор, а gate_closing_fast из левой комнаты, обрываемый soft_land.  Обрыв
одноголосьем — поведение оригинала (play_digi_sound начинается с
stop_digi, seg009.c:2402).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 16:23:06 +03:00
parent e0c86a96ef
commit f159aa47e1
5 changed files with 134 additions and 1 deletions
+61
View File
@@ -453,3 +453,64 @@ DOS-версией; у нас последовательности развед
тактов до порога 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`. Проверено
второй записью — всплеска на выходе больше нет. Механизм тот же, из-за
которого звук глушится на время загрузки уровня.
+8 -1
View File
@@ -962,11 +962,18 @@ int pop_level_switch(void) __banked
void pop_shutdown(void) __banked
{
gfx_set_idle_hook(0); /* снять хук ДО закрытия raw-канала */
/* ЗВУК ГАСИМ ПЕРВЫМ, до closegraph и освобождения страниц. Дальше идут
* ESTEX-вызовы (mem_free_block на каждый атлас), а они уходят надолго —
* насос не успевает долить очередной блок, и железо доигрывает по кругу
* несвежую половину буфера. Замер записи MAME: на выходе по ESC ровно
* 11 мс шума на полной громкости, то есть ОДИН пропущенный блок (128
* сэмплов = 11,7 мс). Та же причина, по которой звук глушится на время
* загрузки уровня (pop_sfx.h). */
pop_sfx_close();
pop_ctrl_close();
closegraph();
pop_bg_free();
pop_kid_free();
pop_sfx_close();
pop_shadow_free();
pop_guard_free();
pop_kid_data_free();
+5
View File
@@ -55,6 +55,11 @@ extern uint8_t _cbl_port_refs; /* держатели bit7 помимо
/* Запись в 16-битный порт управления 0x004E. */
void _cbl_ctrl(uint8_t v);
/* Залить весь аппаратный буфер (256 слотов) байтом тишины сразу после
* включения — иначе первые 23 мс играет то, что лежало в буфере раньше.
* Подробный разбор — в шапке _cbl_prime.c. */
void _cbl_prime(uint8_t silence);
/* ISR-насосы: вызвать fill(), при недоливе — тишина по режиму (один
* регистрируется в _irq_cbl_hook при cbl_open — по выбору CBL_PUMP_*). */
void _cbl_pump_otir(void);
+54
View File
@@ -0,0 +1,54 @@
/*
* _cbl_prime — залить аппаратный буфер CBL тишиной СРАЗУ после включения.
*
* ЗАЧЕМ. Буфер CBL (256 слотов, две половины по 128) железо НЕ чистит ни
* при сбросе, ни при записи в порт управления — там остаётся то, что лежало
* раньше: хвост прошлой сессии, а на холодном старте вообще неинициализи-
* рованное содержимое. Между тем запись в порт управления взводит счётчик
* воспроизведения в 0 и тут же пускает таймер, поэтому первое, что уходит в
* ЦАП, — эти самые 256 слотов. На слух это кусок мусора в момент включения
* звука (поймано пользователем на старте PoP, 2026-08-20).
*
* Разбор по MAME (src/mame/sinclair/sprinter.cpp, единственная доступная нам
* модель железа):
* - case 0x89 (порт управления): `m_cbl_cnt = 0; m_cbl_wa = 0;` и завод
* таймера — буфер при этом не трогается;
* - cbl_tick: играет `m_cbl_data[m_cbl_cnt++]`, а прерывание «долей
* половину» поднимает только на `!(m_cbl_cnt & 0x7f)`, ставя указатель
* записи на ПРОТИВОПОЛОЖНУЮ половину (`m_cbl_wa = m_cbl_cnt ^ 0x80`).
* Отсюда ровно: первое прерывание приходит, когда сыграна половина 0..127,
* и приложение заполняет её же, пока играет половина 128..255. Своими
* данными звук пойдёт только с третьей половины, то есть НЕЗАПОЛНЕННЫМИ
* уходят все 256 слотов — 23,4 мс на 10 937,5 Гц.
*
* ПОЧЕМУ ИМЕННО ТАК ЛЕЧИТСЯ. Заранее, до включения, залить нельзя: запись
* в порт данных попадает в буфер только при уже поднятом bit7 (`case 0x88:
* if (cbl_mode())`). Значит заливаем сразу ПОСЛЕ включения — за 256 OUT'ов
* (~6 400 тактов, 0,3 мс) таймер успевает продвинуться на два-три слота, и
* наружу проскакивает пара сэмплов вместо 23 мс. Гонки с насосом нет:
* первое прерывание будет только на 128-м слоте, а зовут нас под DI.
*
* Слотов ровно 256 НЕЗАВИСИМО от формата (в 16-бит слот держит целый
* сэмпл), поэтому и записей всегда 256 — байт тишины разный: 0x80 для
* 8-бит беззнакового, 0x00 для 16-бит.
*/
#include "_cbl.h"
void _cbl_prime(uint8_t silence) __naked
{
(void)silence;
__asm
;; __sdcccall(1): uint8_t-аргумент уже в A.
;; C = порт данных, B = счётчик (B=0 значит 256 итераций). Пишем
;; через `out (c),a`, а не `out (n),a`: у OTIR в этот порт старший
;; байт адреса тоже задаёт B и меняется по ходу повторяем ровно
;; ту же картину на шине, чтобы не зависеть от декодирования.
ld c, #_CBL_DATA_PORT
ld b, #0
00001$:
out (c), a
djnz 00001$
ret
__endasm;
}
+6
View File
@@ -78,6 +78,12 @@ int cbl_open(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
IRQ_DISABLE();
_irq_cbl_hook = (pump_mode == CBL_PUMP_ACCEL) ? _cbl_pump_accel : _cbl_pump_otir;
_cbl_ctrl((uint8_t)(0x90 | fmt | freq_code)); /* on + int + формат + частота */
/* ...и НЕМЕДЛЕННО забить буфер тишиной: железо пускает воспроизведение
* с нулевого слота той же записью в порт управления, а буфер хранит
* старое содержимое — без этого первые 23 мс уходит мусор. Заливка
* обязана быть внутри той же DI-скобки и сразу за включением (разбор —
* _cbl_prime.c). */
_cbl_prime((uint8_t)((fmt & CBL_FMT_MONO16) ? 0x00 : 0x80));
IRQ_ENABLE();
return 0;
}