diff --git a/applications/PoP/docs/sound_plan.md b/applications/PoP/docs/sound_plan.md index 62d8248..e24717d 100644 --- a/applications/PoP/docs/sound_plan.md +++ b/applications/PoP/docs/sound_plan.md @@ -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`. Проверено +второй записью — всплеска на выходе больше нет. Механизм тот же, из-за +которого звук глушится на время загрузки уровня. diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c index cf404ad..53404c1 100644 --- a/applications/PoP/roomtest/roomtest_cold.c +++ b/applications/PoP/roomtest/roomtest_cold.c @@ -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(); diff --git a/libc/cbl/_cbl.h b/libc/cbl/_cbl.h index a57b039..20075ac 100644 --- a/libc/cbl/_cbl.h +++ b/libc/cbl/_cbl.h @@ -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); diff --git a/libc/cbl/_cbl_prime.c b/libc/cbl/_cbl_prime.c new file mode 100644 index 0000000..2e39120 --- /dev/null +++ b/libc/cbl/_cbl_prime.c @@ -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; +} diff --git a/libc/cbl/cbl_open.c b/libc/cbl/cbl_open.c index 62affbc..d58e71c 100644 --- a/libc/cbl/cbl_open.c +++ b/libc/cbl/cbl_open.c @@ -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; }