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
+134
View File
@@ -0,0 +1,134 @@
/*
* cblwav — проигрывание РЕАЛЬНОГО звука через CBL (Phase 2b смоук).
*
* speech.pcm: моно 8-бит unsigned (центр 0x80), 21.875 кГц — сгенерирован
* macOS `say` + `afconvert -d UI8@21875` (см. docs/im2_isr_design.md).
*
* speech.pcm лежит на ДИСКЕТЕ (mc.img на -flop1) — этот привод слишком
* медленный для потокового чтения в реальном времени. Поэтому клип
* целиком грузится в банковую EMM-память ЗАРАНЕЕ (без таймингов),
* крупными блоками через сырой read(), а во время проигрывания диск
* больше не участвует.
*
* Новая модель без кольца libc (2026-07-07): fill_speech() зовётся
* ИЗ ISR за очередным блоком; она делает bank_read() из УЖЕ ЗАГРУЖЕННОЙ
* страницы прямо в маленький стейджинг-буфер и пропихивает его через
* cbl_push_otir() — без промежуточного кольца.
*
* ВАЖНО: fill() не должен звать mem_get_page() (BIOS rst 8) — BIOS сам
* управляет EI/DI вокруг rst 8, а его `ei` при возврате может
* преждевременно снять маску прерываний, пока мы ещё внутри ISR (см.
* cbl.h: "fill() ОБЯЗАН быть быстрым... никаких ESTEX/BIOS-вызовов").
* Поэтому номера физических страниц считаются ОДИН РАЗ на этапе
* загрузки (в main-контексте) и кэшируются в phys_pages[] — fill()
* только индексирует массив. bank_read() — чистый порт-ввод/вывод,
* без BIOS/ESTEX, вызывать из ISR безопасно.
*
* Хвост не кратный размеру блока (78 КБ % 128 = 43) — fill()
* докладывает остаток тишиной (0x80) прямо в стейджинге на последнем
* неполном блоке, никакого отдельного паддинг-прохода не нужно.
*/
#include <stdio.h>
#include <string.h>
#include <conio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <cbl.h>
#include <sprinter_mem.h>
#define PAGE_SIZE 16384u /* размер страницы EMM */
#define LOAD_CHUNK 4096u /* блок разовой загрузки с диска; делит PAGE_SIZE нацело */
#define SPEECH_SIZE 77995ul /* точный размер tests/cblwav/speech.pcm */
#define MAX_PAGES 8
static uint8_t loadbuf[LOAD_CHUNK];
static uint8_t staging[256]; /* под самый большой блок (16-бит = 256) */
static uint8_t phys_pages[MAX_PAGES]; /* кэш физ. страниц — БЕЗ BIOS-вызовов из fill() */
static uint8_t page_idx;
static uint16_t page_off;
static uint32_t played;
static uint32_t loaded;
static volatile uint8_t done;
static int fill_speech(uint16_t n)
{
if (played >= loaded) { done = 1; return 0; }
uint16_t want = n;
if (want > loaded - played) want = (uint16_t)(loaded - played);
bank_read(phys_pages[page_idx], page_off, staging, want);
if (want < n) memset(staging + want, 0x80, n - want); /* хвост короче блока */
cbl_push_otir(staging, n);
page_off += want;
played += want;
if (page_off >= PAGE_SIZE) {
page_off -= PAGE_SIZE;
page_idx++;
}
return 1;
}
int main(void)
{
int fd = open("speech.pcm", O_RDONLY);
if (fd < 0) {
printf("open(speech.pcm) failed: errno=%d\n", errno);
return 1;
}
uint8_t npages = (uint8_t)((SPEECH_SIZE + PAGE_SIZE - 1) / PAGE_SIZE);
uint8_t blk = mem_alloc_pages(npages);
if (!blk) {
printf("mem_alloc_pages(%u) failed: errno=%d\n", npages, errno);
close(fd);
return 1;
}
puts("loading speech.pcm into RAM (slow: floppy)...");
page_idx = 0;
page_off = 0;
phys_pages[0] = mem_get_page(blk, 0);
while (loaded < SPEECH_SIZE) {
int got = read(fd, loadbuf, sizeof(loadbuf));
if (got <= 0) break;
bank_write(phys_pages[page_idx], page_off, loadbuf, (uint16_t)got);
page_off += (uint16_t)got;
loaded += (uint32_t)got;
if (page_off >= PAGE_SIZE) {
page_off -= PAGE_SIZE;
page_idx++;
phys_pages[page_idx] = mem_get_page(blk, page_idx); /* редко: смена страницы */
}
}
close(fd);
printf("loaded %lu bytes into %u page(s)\n", loaded, npages);
page_idx = 0;
page_off = 0;
played = 0;
done = 0;
if (cbl_open(CBL_FREQ_21K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
CBL_UNDERRUN_APP, fill_speech) != 0) {
printf("cbl_open failed: errno=%d\n", errno);
mem_free_block(blk);
return 1;
}
puts("playing speech.pcm (21.875 kHz, mono 8-bit)...");
while (!done) __asm__("halt"); /* ISR сам кормит CBL — просто ждём */
printf("requests: %u, underruns: %u (expect ~1: tail block)\n",
cbl_requests(), cbl_underruns());
cbl_close();
mem_free_block(blk);
puts("press any key (keyboard must work)...");
getch();
puts("cblwav done.");
return 0;
}