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
+123
View File
@@ -0,0 +1,123 @@
/*
* cbl.h — COVOX-Blaster: потоковый вывод звука через прерывания.
*
* CBL — COVOX с буферным ОЗУ (256 Б, аппаратно разбито на ДВЕ половины
* по 128 Б — двойная буферизация целиком на стороне железа, см.
* официальную документацию "5.3 COVOX-Blaster"): бит 7 порта 0xFE
* отражает, какая половина СЕЙЧАС играет; когда она флипает, вектор
* 0xFF поднимает прерывание — пора долить НЕАКТИВНУЮ половину.
*
* Библиотека НЕ держит собственного кольца — оно было бы лишним
* вторым буфером поверх аппаратного. Вместо этого при cbl_open()
* регистрируется callback fill(): ISR вызывает его напрямую, а
* callback САМ пропихивает n байт из данных приложения (откуда бы они
* ни жили — статический массив, банковая EMM-страница, файл) прямо в
* CBL через cbl_push_otir()/cbl_push_accel() — без промежуточной
* копии в libc.
*
* fill(n) ОБЯЗАН быть быстрым (то же ограничение, что у irq_install()
* — никаких ESTEX/BIOS/gfx-вызовов, только память и один push) и
* вернуть ненулевое значение, если реально пропихнул n байт; 0 — если
* данных сейчас нет (недолив/underrun).
*
* Формат данных (bits 6/5 порта 0x4E; см. Forum.txt "Звук через
* COVOX-Blaster"):
* 8-бит: unsigned, центр 0x80 (моно — один байт/сэмпл;
* стерео — чередование L/R байт)
* 16-бит: signed, центр 0x0000, little-endian (моно — int16/сэмпл;
* стерео — чередование L/R int16)
* Блок запроса — 128 байт для 8-бит, 256 байт для 16-бит (не зависит
* от моно/стерео — это ПОЛНЫЙ байтовый блок, раскладка каналов внутри
* него на размер не влияет); n, приходящий в fill(), равен этому блоку.
*
* Способ выдачи блока — CBL_PUMP_* (см. cbl_open):
* OTIR — cbl_push_otir(), блочный вывод в порт 0x4F; НЕ умеет
* 16-бит (см. ниже) — cbl_open(..., 16-бит формат,
* CBL_PUMP_OTIR, ...) вернёт EINVAL.
* ACCEL — cbl_push_accel(), запись в спец-страницу EMM 0xFD
* (маппится в W3 на 0xC000) через акселератор (см.
* docs/converted/accel_r.txt); единственный способ получить
* настоящий 16-бит сэмпл.
*
* OTIR + 16-бит запрещён не просто "хуже", а физически не работает:
* по исходнику эмулятора (MAME sprinter.cpp) порт данных 0x4F ВСЕГДА
* кладёт байт как есть в один слот, не собирая пару байт в 16-бит
* значение — это умеет только акселераторный путь (страница 0xFD).
*
* Поведение при недоливе — CBL_UNDERRUN_* (4-й параметр cbl_open):
* CBL_UNDERRUN_APP (по умолчанию, 0) — недолив не наша забота;
* в CBL доигрывает то, что уже лежит в его буфере (никакого
* буфера тишины не аллоцируется).
* CBL_UNDERRUN_SILENCE (1) — насос сам пропихивает тишину при
* недоливе; буфер тишины (128/256 Б по формату) аллоцируется
* malloc'ом ВНУТРИ cbl_open(), только в этом режиме.
* cbl_underruns() считает недоливы в ОБОИХ режимах — это диагностика,
* поведение не меняет.
*
* cbl_close() обязателен (atexit подстрахует) — иначе CBL продолжит
* прерывать шелл после выхода; заодно освобождает буфер тишины, если
* он был аллоцирован.
*
* Требования как у <irq.h>: код/данные в W2 (tiny/big).
*/
#ifndef CBL_H
#define CBL_H
#include <stdint.h>
/* Коды частоты дискретизации (bits 3..0 порта 0x4E; из форума
* Sprinter Team; коды 0/1 — legacy, не использовать). */
#define CBL_FREQ_7K8 0x8 /* 7.8125 кГц */
#define CBL_FREQ_10K9 0x9 /* 10.9375 кГц */
#define CBL_FREQ_15K6 0xA /* 15.625 кГц */
#define CBL_FREQ_21K9 0xB /* 21.875 кГц */
#define CBL_FREQ_31K3 0xC /* 31.25 кГц */
#define CBL_FREQ_43K8 0xD /* 43.75 кГц */
#define CBL_FREQ_54K7 0xE /* 54.6875 кГц */
#define CBL_FREQ_109K 0xF /* 109.375 кГц */
/* Формат: биты 5(16-бит)/6(stereo) порта 0x4E — значения готовы для
* прямого OR с freq_code внутри cbl_open(). */
#define CBL_FMT_MONO8 0x00 /* 8-бит, моно — блок 128 Б */
#define CBL_FMT_MONO16 0x20 /* 16-бит, моно — блок 256 Б */
#define CBL_FMT_STEREO8 0x40 /* 8-бит, стерео — блок 128 Б */
#define CBL_FMT_STEREO16 0x60 /* 16-бит, стерео — блок 256 Б */
/* Способ выдачи блока. */
#define CBL_PUMP_OTIR 0 /* cbl_push_otir() — базовый, не умеет 16-бит */
#define CBL_PUMP_ACCEL 1 /* cbl_push_accel() — единственный путь для 16-бит */
/* Поведение при недоливе (fill() вернул 0 или не задан). */
#define CBL_UNDERRUN_APP 0 /* по умолчанию: не наша забота, буфер тишины не аллоцируется */
#define CBL_UNDERRUN_SILENCE 1 /* насос сам шлёт тишину; буфер аллоцируется в cbl_open() */
/* fill(n): пропихнуть n байт (через cbl_push_otir/accel) прямо из
* данных приложения. Вызывается ИЗ ISR — обязан быть быстрым (без
* ESTEX/BIOS/gfx/malloc). Вернуть ненулевое при успехе, 0 — нет
* данных сейчас (недолив). */
typedef int (*cbl_fill_fn)(uint16_t n);
/* Включить CBL с частотой CBL_FREQ_*, форматом CBL_FMT_*, способом
* выдачи CBL_PUMP_* и поведением при недоливе CBL_UNDERRUN_*; fill —
* callback, зовущийся из ISR за новым блоком (может быть NULL, тогда
* недолив — на каждом запросе). 0 или -1 + errno (EBUSY — уже
* открыт; EINVAL — плохой код частоты/формат/pump/underrun_mode, или
* не-W2 режим; ENOMEM — не хватило памяти под буфер тишины). */
int cbl_open(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
uint8_t underrun_mode, cbl_fill_fn fill);
/* Выключить CBL, отпустить IM2-таблицу, освободить буфер тишины (если
* был). Идемпотентно. */
void cbl_close(void);
/* Пропихнуть n байт из src прямо в CBL (звать ИЗ fill()). */
void cbl_push_otir (const void *src, uint16_t n);
void cbl_push_accel(const void *src, uint16_t n);
/* Счётчики диагностики: блоки-запросы и недоливы (fill() вернул 0
* или не задан). */
uint16_t cbl_requests(void);
uint16_t cbl_underruns(void);
#endif