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:
@@ -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);
|
||||
|
||||
@@ -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_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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user