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:
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* cbltest — полный перебор CBL-звука (Phase 2b, docs/im2_isr_design.md).
|
||||
*
|
||||
* Матрица: 2 насоса (OTIR/ACCEL) × 8 форматов (mono/stereo-L/stereo-R/
|
||||
* stereo-LR, каждый в 8- и 16-бит) × 4 частоты (7.8/10.9/15.6/21.9 кГц)
|
||||
* = 64 комбинации, каждая ~1 с пилообразного тона (период 64 сэмпла/
|
||||
* кадра). Для каждой печатается requests/underruns — 0 недоливов
|
||||
* ожидается всегда. В MAME на слух должно быть отчётливо слышно:
|
||||
* смену тембра насоса (не должно быть разницы OTIR/ACCEL), канал
|
||||
* (левый/правый/оба) и высоту тона (растёт с частотой).
|
||||
*
|
||||
* OTIR+16-бит (16 из 64 комбинаций) ПРОПУСКАЮТСЯ — cbl_open() отдаёт
|
||||
* EINVAL: OTIR-порт данных не умеет собирать 16-бит сэмпл из пары
|
||||
* байт (только акселератор это делает; см. cbl_open.c, сверено с
|
||||
* исходником MAME sprinter.cpp).
|
||||
*
|
||||
* Новая модель без кольца libc (2026-07-07): fill_tone() зовётся ИЗ
|
||||
* ISR, сама пропихивает n байт из уже готового периодического чанка
|
||||
* через cbl_push_otir/accel (по current_pump) — данные никогда не
|
||||
* кончаются, так что CBL_UNDERRUN_APP (без буфера тишины) достаточно.
|
||||
* Main-цикл ничего не поллит — просто ждёт ~1 с через halt().
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
#include <conio.h>
|
||||
#include <errno.h>
|
||||
#include <cbl.h>
|
||||
|
||||
static uint8_t chunk[256];
|
||||
static uint8_t current_pump; /* CBL_PUMP_OTIR/ACCEL для активной комбинации */
|
||||
|
||||
/* Заполнить chunk одним периодом (64 сэмпла/кадра) пилы — раскладка
|
||||
* зависит от bits (8/16) и stereo (моно / чередование L,R). */
|
||||
static void gen_chunk(uint8_t bits, uint8_t stereo, uint8_t want_l, uint8_t want_r)
|
||||
{
|
||||
if (!stereo) {
|
||||
if (bits == 8) {
|
||||
for (uint16_t i = 0; i < sizeof(chunk); i++) {
|
||||
chunk[i] = (uint8_t)(i * 4); /* период 64 (обёртка uint8_t) */
|
||||
}
|
||||
} else {
|
||||
int16_t *p = (int16_t *)chunk;
|
||||
for (uint16_t i = 0; i < sizeof(chunk) / 2; i++) {
|
||||
uint16_t idx = i & 63;
|
||||
p[i] = (int16_t)(uint16_t)(idx * 1024u);
|
||||
}
|
||||
}
|
||||
} else if (bits == 8) {
|
||||
for (uint16_t k = 0; k < sizeof(chunk) / 2; k++) {
|
||||
uint8_t frame = (uint8_t)(k & 63);
|
||||
uint8_t tone = (uint8_t)(frame * 4);
|
||||
chunk[2 * k] = want_l ? tone : 0x80;
|
||||
chunk[2 * k + 1] = want_r ? tone : 0x80;
|
||||
}
|
||||
} else {
|
||||
int16_t *p = (int16_t *)chunk;
|
||||
for (uint16_t k = 0; k < sizeof(chunk) / 4; k++) {
|
||||
uint16_t frame = k & 63;
|
||||
int16_t tone = (int16_t)(uint16_t)(frame * 1024u);
|
||||
p[2 * k] = want_l ? tone : 0;
|
||||
p[2 * k + 1] = want_r ? tone : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Зовётся ИЗ ISR (см. cbl.h: обязано быть быстрым). chunk — готовый
|
||||
* period-64 тон, никогда не "кончается", поэтому всегда успех. */
|
||||
static int fill_tone(uint16_t n)
|
||||
{
|
||||
if (current_pump == CBL_PUMP_ACCEL) cbl_push_accel(chunk, n);
|
||||
else cbl_push_otir(chunk, n);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void play_combo(uint16_t idx, uint16_t total,
|
||||
const char *pump_name, uint8_t pump_mode,
|
||||
const char *fmt_name, uint8_t fmt, uint8_t bits,
|
||||
uint8_t stereo, uint8_t want_l, uint8_t want_r,
|
||||
const char *freq_name, uint8_t freq_code)
|
||||
{
|
||||
gen_chunk(bits, stereo, want_l, want_r);
|
||||
current_pump = pump_mode;
|
||||
|
||||
if (cbl_open(freq_code, fmt, pump_mode, CBL_UNDERRUN_APP, fill_tone) != 0) {
|
||||
printf("[%2u/%u] %s %s %s: cbl_open failed errno=%d\n",
|
||||
idx, total, pump_name, fmt_name, freq_name, errno);
|
||||
return;
|
||||
}
|
||||
|
||||
printf("[%2u/%u] %s %s %s ...\n", idx, total, pump_name, fmt_name, freq_name);
|
||||
|
||||
datetime_t t, t2;
|
||||
getdatetime(&t);
|
||||
getdatetime(&t2);
|
||||
while (t.second == t2.second) {
|
||||
__asm__("halt"); /* ISR сам кормит CBL — просто ждём */
|
||||
getdatetime(&t2);
|
||||
}
|
||||
uint16_t reqs = cbl_requests();
|
||||
uint16_t undr = cbl_underruns();
|
||||
cbl_close();
|
||||
|
||||
if (undr) printf(" !! underruns=%u (reqs=%u)\n", undr, reqs);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static const uint8_t pumps[2] = { CBL_PUMP_OTIR, CBL_PUMP_ACCEL };
|
||||
static const char *const pump_names[2] = { "otir ", "accel" };
|
||||
|
||||
static const uint8_t freq_codes[4] = {
|
||||
CBL_FREQ_7K8, CBL_FREQ_10K9, CBL_FREQ_15K6, CBL_FREQ_21K9
|
||||
};
|
||||
static const char *const freq_names[4] = { "7.8k ", "10.9k", "15.6k", "21.9k" };
|
||||
|
||||
/* формат: биты, fmt-код, stereo?, L?, R?, имя */
|
||||
static const uint8_t fmt_bits[8] = { 8, 8, 8, 8, 16, 16, 16, 16 };
|
||||
static const uint8_t fmt_code[8] = {
|
||||
CBL_FMT_MONO8, CBL_FMT_STEREO8, CBL_FMT_STEREO8, CBL_FMT_STEREO8,
|
||||
CBL_FMT_MONO16, CBL_FMT_STEREO16, CBL_FMT_STEREO16, CBL_FMT_STEREO16
|
||||
};
|
||||
static const uint8_t fmt_stereo[8] = { 0, 1, 1, 1, 0, 1, 1, 1 };
|
||||
static const uint8_t fmt_l[8] = { 1, 1, 0, 1, 1, 1, 0, 1 };
|
||||
static const uint8_t fmt_r[8] = { 1, 0, 1, 1, 1, 0, 1, 1 };
|
||||
static const char *const fmt_names[8] = {
|
||||
"mono8 ", "st8-L ", "st8-R ", "st8-LR ",
|
||||
"mono16 ", "st16-L ", "st16-R ", "st16-LR "
|
||||
};
|
||||
|
||||
uint16_t total = 2u * 8u * 4u;
|
||||
uint16_t idx = 0;
|
||||
|
||||
for (uint8_t p = 0; p < 2; p++) {
|
||||
for (uint8_t f = 0; f < 8; f++) {
|
||||
for (uint8_t q = 0; q < 4; q++) {
|
||||
idx++;
|
||||
/* OTIR принципиально не может собрать 16-бит сэмпл
|
||||
* (см. cbl_open.c) — cbl_open вернёт EINVAL; не тратим
|
||||
* время на заведомо запрещённую комбинацию. */
|
||||
if (pumps[p] == CBL_PUMP_OTIR && fmt_bits[f] == 16) {
|
||||
printf("[%2u/%u] %s %s %s : skip (OTIR can't do 16-bit)\n",
|
||||
idx, total, pump_names[p], fmt_names[f], freq_names[q]);
|
||||
continue;
|
||||
}
|
||||
play_combo(idx, total,
|
||||
pump_names[p], pumps[p],
|
||||
fmt_names[f], fmt_code[f], fmt_bits[f],
|
||||
fmt_stereo[f], fmt_l[f], fmt_r[f],
|
||||
freq_names[q], freq_codes[q]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
puts("press any key (keyboard must work)...");
|
||||
getch();
|
||||
puts("cbltest done.");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user