5086c47f0f
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>
135 lines
5.5 KiB
C
135 lines
5.5 KiB
C
/*
|
|
* 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;
|
|
}
|