b34997073e
cbl_close() не пишет в порт управления ноль: пока gfx_wait_vsync держит ссылку ради бита луча (в PoP её берёт pop_pace_arm на всю программу), bit7 остаётся включённым. А буфер CBL (256 слотов) при включённом bit7 крутится по кругу и не чистится ничем — железо бесконечно поёт хвост последнего сэмпла, тоном и громкостью по последней мелодии. Пойман пользователем на железе: Ctrl+S во время музыки и пропуск заставки давали ноту до следующего cbl_open. В MAME не воспроизводится: "немым" кодом частоты был reserved-код 2, у которого divs[2]==0 и таймер не заводится вовсе. На железе reserved-коды не определены, ЦАП тактируется — тишина держалась на свойстве эмулятора, а не железа. Теперь тишину даёт СОДЕРЖИМОЕ БУФЕРА: - _cbl_port_sync() после каждой записи в порт управления зовёт _cbl_prime(0x80). Это закрывает и паузу звука, и включение bit7 ради луча на холодном старте (в буфере лежал мусор от прошлой программы), и полное выключение — при bit7=0 те же 256 записей уходят прямо в ЦАП обычного COVOX и центруют его, снимая щелчок; - _CBL_VSYNC_FREQ переведён с reserved-кода 2 на документированный 8 (7,8125 кГц): поведение определено и на железе, и в MAME, прерывания по-прежнему выключены (bit4=0), а бит 7 порта 0xFE трамплин смотрит только при живом хуке насоса; - cbl_close() зовёт sync внутри той же DI-скобки, где снимает хук, иначе насос долил бы буфер уже после заливки. Цена: +2 Б программам со звуком, +11 Б графическим (тянется _cbl_prime следом за gfx_wait_vsync), 256 OUT'ов (~0,3 мс) на редкое событие — эталоны cblstream/cbltest/cblwav/gfx_dbuf обновлены. Рост atlas в size-check к этой правке отношения не имеет (замерен тот же и без неё). Проверено: кодоген _cbl_port.asm; SprPoP пересобран и прогнан в MAME (титры → пропуск заставки → уровень 1, пейсинг по лучу жив). Сам баг проверяется только на железе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
69 lines
5.1 KiB
C
69 lines
5.1 KiB
C
/*
|
||
* _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-бит.
|
||
*
|
||
* ВТОРАЯ РОЛЬ — ГАШЕНИЕ (добавлена 2026-09-02, _cbl_port_sync). Запись
|
||
* в порт управления НЕ чистит буфер и в обратную сторону: пока bit7
|
||
* включён (а его держит gfx_wait_vsync ради бита луча), железо крутит
|
||
* оставшиеся 256 байт по кругу — это ровно "одна нота" из фирменного
|
||
* примера Forum.txt, только незваная. Поэтому после каждой записи в
|
||
* порт управления зовём эту же функцию:
|
||
* - bit7 остался включён — по кругу крутится тишина;
|
||
* - bit7 снят — те же 256 записей идут прямо в ЦАП обычного COVOX
|
||
* (MAME sprinter.cpp, `case 0x88` без cbl_mode()) и оставляют его в
|
||
* нуле 0x80, а не на последнем сэмпле.
|
||
* Гонки с насосом нет и здесь: при гашении хук уже снят, при включении
|
||
* первое прерывание будет только на 128-м слоте.
|
||
*/
|
||
|
||
#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;
|
||
}
|