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
+140
View File
@@ -0,0 +1,140 @@
/*
* cblstream — потоковое чтение с диска ОДНОВРЕМЕННО с воспроизведением
* через CBL. Расчитан на быстрый носитель (HDD) — на floppy
* захлёбывается (см. tests/cblwav, где поэтому весь клип грузится в
* RAM заранее). speech30.pcm — ~39.6 c речи, моно 8-бит unsigned,
* 21.875 кГц.
*
* ЕДИНСТВЕННЫЙ случай из трёх cbl-тестов, где приложению НУЖНО своё
* кольцо (2026-07-07): read() — ESTEX-вызов, а fill()-callback CBL
* зовётся ИЗ ISR, где ESTEX/BIOS под запретом (см. cbl.h). Поэтому
* диск читается ТОЛЬКО в main (в собственное кольцо ring[]), а
* fill_stream() лишь копирует уже готовые байты из ring в стейджинг и
* пропихивает cbl_push_otir() — сам с диском не работает.
*/
#include <stdio.h>
#include <string.h>
#include <conio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <cbl.h>
#include <irq.h>
#define RING_SIZE 2048u
#define READ_CHUNK 1024u
static uint8_t ring[RING_SIZE];
static uint16_t r_rd, r_wr, r_count; /* r_count: main под IRQ_DISABLE, fill() уже в ISR */
static uint8_t staging[128]; /* блок 8-бит моно */
static uint16_t ring_in = 0;
static uint16_t ring_border = 0;
static uint16_t ring_free(void)
{
IRQ_DISABLE();
uint16_t c = r_count;
IRQ_ENABLE();
return (uint16_t)(RING_SIZE - c);
}
/* Вызывать ТОЛЬКО из main. */
static void ring_push(const void *src, uint16_t n)
{
const uint8_t *s = src;
while (n) {
uint16_t tail = (uint16_t)(RING_SIZE - r_wr);
uint16_t chunk = n < tail ? n : tail;
memcpy(ring + r_wr, s, chunk);
r_wr = (uint16_t)((r_wr + chunk) % RING_SIZE);
IRQ_DISABLE();
r_count += chunk;
IRQ_ENABLE();
s += chunk;
n = (uint16_t)(n - chunk);
}
}
/* Зовётся ИЗ ISR — только копия из уже готового ring, никакого ESTEX. */
static int fill_stream(uint16_t n)
{
if (r_count < n) return 0;
uint16_t tail = (uint16_t)(RING_SIZE - r_rd);
if (tail >= n) {
// memcpy(staging, ring + r_rd, n);
cbl_push_otir(ring + r_rd, n);
ring_in++;
} else {
memcpy(staging, ring + r_rd, tail);
memcpy(staging + tail, ring, (uint16_t)(n - tail));
cbl_push_otir(staging, n);
ring_border++;
}
r_rd = (uint16_t)((r_rd + n) % RING_SIZE);
r_count = (uint16_t)(r_count - n); /* уже внутри ISR — IRQ и так замаскированы */
return 1;
}
int main(void)
{
int fd = open("speech30.pcm", O_RDONLY);
if (fd < 0) {
printf("open(speech30.pcm) failed: errno=%d\n", errno);
return 1;
}
static uint8_t readbuf[READ_CHUNK];
uint32_t played = 0;
uint8_t eof = 0;
/* прогреть кольцо реальными данными ДО cbl_open()/puts() — иначе
* пауза тишины на старте (тот же приём, что в cbltest.c/cblwav.c) */
while (!eof && ring_free() >= sizeof(readbuf)) {
int got = read(fd, readbuf, sizeof(readbuf));
if (got <= 0) { eof = 1; break; }
ring_push(readbuf, (uint16_t)got);
played += (uint32_t)got;
}
if (cbl_open(CBL_FREQ_21K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
CBL_UNDERRUN_APP, fill_stream) != 0) {
printf("cbl_open failed: errno=%d\n", errno);
close(fd);
return 1;
}
puts("streaming speech30.pcm from disk (21.875 kHz, mono 8-bit)...");
while (!eof) {
if (ring_free() >= sizeof(readbuf)) {
int got = read(fd, readbuf, sizeof(readbuf));
if (got <= 0) { eof = 1; break; }
ring_push(readbuf, (uint16_t)got);
played += (uint32_t)got;
} else {
__asm__("halt"); /* ждём, пока ISR подъест кольцо */
}
}
close(fd);
printf("streamed %lu bytes from disk\n", played);
/* Ждём "почти полного" опустошения, а не точного RING_SIZE: если
* общая длина потока не кратна 128, необслуженный хвост (<128 Б,
* тот же квирк, что и в старом кольце libc — см. cbl.h) навечно
* оставит r_count>0, и точное ожидание никогда бы не завершилось. */
while (ring_free() < RING_SIZE - 127u) __asm__("halt");
printf("requests: %u, underruns: %u (expect 0 during play)\n",
cbl_requests(), cbl_underruns());
printf("ring: in: %u, border: %u\n",
ring_in, ring_border);
cbl_close();
puts("press any key (keyboard must work)...");
getch();
puts("cblstream done.");
return 0;
}