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:
@@ -453,3 +453,64 @@ DOS-версией; у нас последовательности развед
|
|||||||
тактов до порога FASTEST, то есть в 60 раз больше цены звука. Проверять
|
тактов до порога FASTEST, то есть в 60 раз больше цены звука. Проверять
|
||||||
эту оговорку заново стоит только если работа кадра подберётся к 430 000 или
|
эту оговорку заново стоит только если работа кадра подберётся к 430 000 или
|
||||||
860 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`. Проверено
|
||||||
|
второй записью — всплеска на выходе больше нет. Механизм тот же, из-за
|
||||||
|
которого звук глушится на время загрузки уровня.
|
||||||
|
|||||||
@@ -962,11 +962,18 @@ int pop_level_switch(void) __banked
|
|||||||
void pop_shutdown(void) __banked
|
void pop_shutdown(void) __banked
|
||||||
{
|
{
|
||||||
gfx_set_idle_hook(0); /* снять хук ДО закрытия raw-канала */
|
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();
|
pop_ctrl_close();
|
||||||
closegraph();
|
closegraph();
|
||||||
pop_bg_free();
|
pop_bg_free();
|
||||||
pop_kid_free();
|
pop_kid_free();
|
||||||
pop_sfx_close();
|
|
||||||
pop_shadow_free();
|
pop_shadow_free();
|
||||||
pop_guard_free();
|
pop_guard_free();
|
||||||
pop_kid_data_free();
|
pop_kid_data_free();
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ extern uint8_t _cbl_port_refs; /* держатели bit7 помимо
|
|||||||
/* Запись в 16-битный порт управления 0x004E. */
|
/* Запись в 16-битный порт управления 0x004E. */
|
||||||
void _cbl_ctrl(uint8_t v);
|
void _cbl_ctrl(uint8_t v);
|
||||||
|
|
||||||
|
/* Залить весь аппаратный буфер (256 слотов) байтом тишины сразу после
|
||||||
|
* включения — иначе первые 23 мс играет то, что лежало в буфере раньше.
|
||||||
|
* Подробный разбор — в шапке _cbl_prime.c. */
|
||||||
|
void _cbl_prime(uint8_t silence);
|
||||||
|
|
||||||
/* ISR-насосы: вызвать fill(), при недоливе — тишина по режиму (один
|
/* ISR-насосы: вызвать fill(), при недоливе — тишина по режиму (один
|
||||||
* регистрируется в _irq_cbl_hook при cbl_open — по выбору CBL_PUMP_*). */
|
* регистрируется в _irq_cbl_hook при cbl_open — по выбору CBL_PUMP_*). */
|
||||||
void _cbl_pump_otir(void);
|
void _cbl_pump_otir(void);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -78,6 +78,12 @@ int cbl_open(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
|
|||||||
IRQ_DISABLE();
|
IRQ_DISABLE();
|
||||||
_irq_cbl_hook = (pump_mode == CBL_PUMP_ACCEL) ? _cbl_pump_accel : _cbl_pump_otir;
|
_irq_cbl_hook = (pump_mode == CBL_PUMP_ACCEL) ? _cbl_pump_accel : _cbl_pump_otir;
|
||||||
_cbl_ctrl((uint8_t)(0x90 | fmt | freq_code)); /* on + int + формат + частота */
|
_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();
|
IRQ_ENABLE();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user