SDL 3 has no OSS backend and SchOS has no ALSA kernel side, so sound now goes SDL -> alsa-lib -> alsa-plugins' OSS plugin -> /dev/dsp. That path found two bugs underneath it: - ac97: resetting the PCM-out box also clears the descriptor list address, and nothing set it again. Every stream after the first stop played a "list" at physical address 0 -- silence -- and ALSA resets before each stream. If the engine halts at the end of the queue, the run bit is set again. - /dev/dsp: SETFRAGMENT, GETFMTS, GETCAPS, SETTRIGGER, GETOPTR and GETODELAY for the plugin's ring-and-pointer model, and poll(), writable once a whole fragment fits (ALSA spun at 76% CPU otherwise). speaker-test now plays in real time at 0% CPU, verified in a recording of QEMU's AC'97. - /etc/asound.conf: the default device is a plug to the OSS device at 16-bit stereo 48 kHz, what every SchOS audio driver plays. Also: - getstackpcs: each frame link must go up the same stack by less than KSTACKSIZE. The chain ends at the syscall entry with the user's rbp, which lbreakouthd uses as a register (-1024); the walk dereferenced it under the syscall tracer and the kernel panicked inside acquire(). - fb: FBIOGET_VSCREENINFO reports nominal timings, as Linux's efifb does, instead of zeros, which Xorg's fbdev turned into a 0 Hz RandR mode. - procdump (Ctrl-P) shows where runnable threads are in user space. - World: lbreakouthd instead of lbreakout2 (its stk.c passes vsnprintf a size of 511 plus the prefix length, and Alpine's fortified headers trap on that), plus curl, alsa-lib and alsa-plugins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
121 lines
5.6 KiB
C
121 lines
5.6 KiB
C
#pragma once
|
|
|
|
// The audio class, and the engine above it.
|
|
//
|
|
// A program that wants to make a sound should be able to open a device, write
|
|
// samples to it, and stop -- which is what UNIX audio has always been, and
|
|
// what this is. `/dev/dsp`, PCM in, `write()`, and a handful of ioctls to say
|
|
// what the samples are. No server, no session bus, no protocol.
|
|
//
|
|
// The shape is the display class's, for the same reason: a consumer asks for
|
|
// "the audio device" and gets whatever is bound, so adding a second driver is
|
|
// not an edit to every caller. Found with device_find_class(DEVCLASS_AUDIO).
|
|
//
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// **What the engine is for, and what it is not yet.**
|
|
//
|
|
// AudioEngine owns everything above a driver that is not specific to a chip:
|
|
// what format the stream is in, how many bytes the hardware can take, and what
|
|
// a write() does when it cannot take them all. That is genuinely shared -- a
|
|
// second driver should not re-implement "block until there is room".
|
|
//
|
|
// It is deliberately *one stream*. Mixing several programs into one device is
|
|
// a real feature with a real design behind it, and building it before anything
|
|
// plays a single sound would be the mistake docs/plan/1.0.md warns about: work
|
|
// that exists and is connected to nothing. The second program that wants to
|
|
// play at the same time is what should cause it to be written.
|
|
|
|
struct device;
|
|
|
|
// Interleaved signed 16-bit little-endian, which is what every PC audio chip
|
|
// takes natively and what a WAV file already contains. Others exist and none
|
|
// of them is needed to make a sound; converting in the kernel to satisfy a
|
|
// caller that could have converted first is not an obvious trade.
|
|
#define AUDIO_FMT_S16LE 16
|
|
|
|
struct audio_format {
|
|
unsigned int rate; // frames per second -- 48000, 44100
|
|
unsigned int channels; // 1 mono, 2 stereo
|
|
unsigned int bits; // AUDIO_FMT_S16LE
|
|
};
|
|
|
|
struct audio_ops {
|
|
// Take this format, or report what the hardware will do instead by writing
|
|
// back into `f` and returning 0. Returning negative means it cannot play at
|
|
// all. A driver that only does 48 kHz stereo says so here rather than
|
|
// failing a write() later, which is the difference between a program that
|
|
// adapts and a program that gets silence.
|
|
int (*configure)(struct device *, struct audio_format *f);
|
|
|
|
// Bytes that can be queued right now without waiting. The engine uses this
|
|
// to decide whether a write() has to block, so a driver that returns a
|
|
// hardware-accurate number is a driver whose write() latency is honest.
|
|
int (*space)(struct device *);
|
|
|
|
// Queue PCM. Returns bytes accepted, which may be fewer than offered and
|
|
// may be zero; the engine handles the remainder. Never blocks.
|
|
int (*queue)(struct device *, const void *pcm, int bytes);
|
|
|
|
// Start the stream if it is not running. Called by the engine once there is
|
|
// something to play, rather than at open: a device left running with an
|
|
// empty ring plays whatever was in it, which on real hardware is a loop of
|
|
// the last buffer and sounds exactly as bad as it reads.
|
|
void (*start)(struct device *);
|
|
|
|
// Stop, and discard anything queued.
|
|
void (*stop)(struct device *);
|
|
|
|
// Bytes still to be played. Zero means the hardware has finished. The
|
|
// engine polls this for SNDCTL_DSP_SYNC rather than a driver growing its
|
|
// own wait loop.
|
|
int (*pending)(struct device *);
|
|
};
|
|
|
|
// --- what the kernel uses --------------------------------------------------
|
|
|
|
struct device *audio_get(void);
|
|
int audio_configure(struct audio_format *f); // 0, or negative
|
|
int audio_write(const void *pcm, int bytes); // bytes queued, or negative
|
|
void audio_drain(void);
|
|
void audio_stop(void);
|
|
int dsp_writable(void); // poll(): room for a write
|
|
|
|
// --- what userspace uses ---------------------------------------------------
|
|
//
|
|
// OSS numbering, which is the one every UNIX audio program already knows and
|
|
// the only reason to invent a different set would be to make ports harder.
|
|
// These four are the whole interface a tone generator or a WAV player needs.
|
|
|
|
#define SNDCTL_DSP_SPEED 0xC0045002 // int in/out: sample rate
|
|
#define SNDCTL_DSP_CHANNELS 0xC0045006 // int in/out: 1 or 2
|
|
#define SNDCTL_DSP_SETFMT 0xC0045005 // int in/out: AFMT_S16_LE
|
|
#define SNDCTL_DSP_SYNC 0x00005001 // wait for the queue to drain
|
|
#define SNDCTL_DSP_GETOSPACE 0x8010500C // struct audio_buf_info out
|
|
#define SNDCTL_DSP_RESET 0x00005000 // stop, discard
|
|
// The rest of what a buffer-and-pointer client needs -- ALSA's OSS plugin, which
|
|
// is how SDL and everything else that speaks ALSA reaches this device.
|
|
#define SNDCTL_DSP_SETFRAGMENT 0xC004500A // int in/out: count<<16 | log2(size)
|
|
#define SNDCTL_DSP_GETFMTS 0x8004500B // int out: AFMT_* mask
|
|
#define SNDCTL_DSP_GETCAPS 0x8004500F // int out: DSP_CAP_*
|
|
#define SNDCTL_DSP_SETTRIGGER 0x40045010 // int in: PCM_ENABLE_OUTPUT or 0
|
|
#define SNDCTL_DSP_GETOPTR 0x800C5012 // struct count_info out
|
|
#define SNDCTL_DSP_GETODELAY 0x80045017 // int out: bytes not yet heard
|
|
|
|
#define DSP_CAP_TRIGGER 0x00001000
|
|
#define PCM_ENABLE_OUTPUT 0x00000002
|
|
|
|
struct count_info {
|
|
int bytes; // played since the last reset
|
|
int blocks; // fragments played since the last GETOPTR
|
|
int ptr; // where playback is in a ring of fragsize * fragments
|
|
};
|
|
|
|
#define AFMT_S16_LE 0x00000010
|
|
|
|
struct audio_buf_info {
|
|
int fragments; // how many whole fragments are free
|
|
int fragstotal; // how many there are in total
|
|
int fragsize; // bytes in one
|
|
int bytes; // free bytes, which is the number that matters
|
|
};
|