libc: IM2 Phase 2b — звук CBL/COVOX через callback fill(), без кольца libc

CBL уже имеет аппаратный буфер 256 Б (2×128, двойная буферизация на
стороне железа) — держать поверх него ещё одно кольцо в libc было бы
лишней копией. cbl_open(freq, fmt, pump_mode, underrun_mode, fill)
регистрирует callback, вызываемый из ISR за очередным блоком; он сам
пропихивает данные приложения (откуда угодно) через cbl_push_otir()/
cbl_push_accel() — без промежуточного буфера.

- два насоса: OTIR (порт 0x4F) и ACCEL (акселератор, спец-страница
  EMM 0xFD@0xC000); OTIR+16-бит запрещён (EINVAL) — по исходнику MAME
  порт данных физически не может собрать 16-бит сэмпл из пары байт;
- форматы CBL_FMT_MONO8/16/STEREO8/16, частоты 7.8..109к;
- CBL_UNDERRUN_APP (по умолчанию, недолив не наша забота) /
  CBL_UNDERRUN_SILENCE (буфер тишины malloc'ится только в этом режиме);
- tests/cbltest: матрица 64 комбинации (2 насоса × 8 форматов × 4
  частоты); tests/cblwav: banked-стрим речи с дискеты (физстраницы
  кэшированы заранее — mem_get_page нельзя звать из fill()/ISR);
  tests/cblstream: единственный случай с собственным кольцом уровня
  приложения (диск нельзя читать из fill()).

Verified в MAME 2026-07-07 — все три теста работают.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 21:21:54 +03:00
parent 8a952b99eb
commit 5086c47f0f
28 changed files with 1208 additions and 22 deletions
+56
View File
@@ -168,6 +168,62 @@ font[row*256+char]).
Handler'у нельзя: ESTEX/BIOS-вызовы, gfx_*/своп окон, акселератор,
banked-функции; только volatile-глобалы и быстрая работа (<1 мс).
## <cbl.h> — потоковый звук CBL/COVOX (вектор 0xFF, свой ISR)
Без собственного кольца (2026-07-07): у CBL уже есть аппаратный буфер
256 Б (2×128, двойная буферизация на стороне железа — см. официальную
доку "5.3 COVOX-Blaster"); библиотека просто зовёт `fill()` приложения
ИЗ ISR, а оно само пропихивает данные (откуда угодно) через
`cbl_push_otir/accel` — без промежуточной копии.
| Сигнатура | Описание |
|---|---|
| `int cbl_open(freq_code, fmt, pump_mode, underrun_mode, fill)` | включить CBL (`0x90\|fmt\|freq`), зарегистрировать callback; 0 / -1+errno (EBUSY повтор, EINVAL — freq/fmt/pump_mode/underrun_mode плохие или OTIR+16-бит, ENOMEM — буфер тишины) |
| `void cbl_close(void)` | выключить CBL, снять хук, освободить буфер тишины (если был); висит на atexit |
| `void cbl_push_otir(const void *src, uint16_t n)` | пропихнуть n байт через `otir` в порт 0x4F; звать ИЗ fill() |
| `void cbl_push_accel(const void *src, uint16_t n)` | то же акселератором (страница EMM 0xFD@0xC000); единственный путь для 16-бит |
| `uint16_t cbl_requests(void)` | счётчик запросов блока ISR (~fs/(сэмплов в блоке) в секунду) |
| `uint16_t cbl_underruns(void)` | счётчик недоливов (`fill` вернул 0 или не задан) |
| `CBL_FREQ_7K8 .. CBL_FREQ_109K` | коды частоты (биты 3..0 control-порта) |
| `CBL_FMT_MONO8/MONO16/STEREO8/STEREO16` | формат (биты 5/6 control-порта); блок 128 Б (8-бит) или 256 Б (16-бит), не зависит от моно/стерео; тишина 0x80 (8-бит) / 0x0000 (16-бит, знаковый) |
| `CBL_PUMP_OTIR / CBL_PUMP_ACCEL` | способ выдачи — см. ниже |
| `CBL_UNDERRUN_APP / CBL_UNDERRUN_SILENCE` | поведение при недоливе — см. ниже |
`typedef int (*cbl_fill_fn)(uint16_t n);` — callback, зовётся ИЗ ISR за
очередным блоком (n = `_cbl_block`, 128/256). **Обязан быть быстрым**
— никаких ESTEX/BIOS/gfx-вызовов (тот же констрейнт, что у
`irq_install()`-хендлера); вернуть ненулевое, если реально пропихнул n
байт. Диск (read()) читать из fill() НЕЛЬЗЯ — см. tests/cblstream,
где под это заведено кольцо уровня приложения.
Два насоса (3-й параметр `cbl_open`):
- **OTIR** (`_cbl_pump_otir`) — `cbl_push_otir()` в порт 0x4F; базовый.
**НЕ умеет 16-бит**`cbl_open(..., MONO16/STEREO16, CBL_PUMP_OTIR,
...)` вернёт EINVAL: по исходнику MAME (sprinter.cpp) порт данных
ВСЕГДА кладёт байт как есть в один слот, не собирая пару байт в
16-бит значение и не сверяясь с 16-бит флагом вообще.
- **ACCEL** (`_cbl_pump_accel`) — `cbl_push_accel()` через акселератор
в спец-страницу EMM 0xFD, замапленную в окно W3 на 0xC000 (см.
docs/converted/accel_r.txt, Forum.txt); размер блока патчится SMC
(`LD D,D` + immediate `LD A,n` + `LD L,L` — тот же паттерн, что и в
libc/gfx/_gfx_hfill256.c); единственный путь для 16-бит.
**Verified в MAME 2026-07-07** (tests/cbltest, вся accel-половина
матрицы прошла без ошибок/underrun).
Поведение при недоливе (4-й параметр `cbl_open`):
- **CBL_UNDERRUN_APP** (по умолчанию, 0) — не забота библиотеки, буфер
тишины НЕ аллоцируется, в CBL доигрывает то, что уже лежало в его
аппаратном буфере.
- **CBL_UNDERRUN_SILENCE** (1) — насос сам пропихивает тишину; буфер
(128/256 Б по формату) аллоцируется malloc'ом ВНУТРИ `cbl_open()`
только в этом режиме.
`cbl_underruns()` считает недоливы в обоих режимах — диагностика,
поведение не меняет.
Приватное прерывание, к DSS не чейнится; бит 7 порта 0xFE (запрос
блока) читается только пока `cbl_open` не закрыт (при выключенном CBL
бит всегда 1 — MAME-квирк).
## <palette.h> — низкий уровень (BIOS $A4/$A6)
`pal_load pal_get pal_set_color pal_get_color` (страница 0..7,