SchOS/include/dev/audio.h
Nexoniarz c77841ce3d Sound through ALSA, a 2D game, and a stack walk that cannot panic
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
2026-10-25 05:39:16 +01:00

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
};