AnotherOS

Documentation

Audio — Garsas

The sound stack: one server, per-application volume, and a client API that keeps working when there is no sound card.

Garsas is the audio stack. It has two halves that are often confused, so this page names them separately before anything else.

What it is
The kernel multiplexer Picks the highest-priority sound driver (HDA, AC'97, ES1370, SB16) and backs /dev/dsp and /dev/mixer with it.
garsasd, the sound server The policy and mixing layer: application identity, per-application volume, routing, events, and the native client API.

All audio goes through Garsas. Nothing opens the card directly. An application that opens /dev/dsp itself is a bug, not a shortcut.

Why there is a server at all

/dev/dsp carries per-descriptor format state and sums its writers, which makes concurrent playback correct. It does not make it manageable: the kernel sees file descriptors, so it can sum two streams and can never say which application is making a noise, or let you turn just that one down.

  • garsasd owns application identity, per-application volume and mute remembered across runs, routing, events, and capture consent.
  • The kernel owns the hardware, the ring, the codec path, the poll-driven refill, per-descriptor format state and summing, and the master gain.

garsasd hands the kernel exactly one stream, at its own mix format. It does not re-implement the kernel's resampler; it uses it, once, for the whole system.

Writing a client

#include <garsas.h>

garsas_conn *c = garsas_connect("lt.anotheros.MyApp", "My Application");

struct garsas_format f = { 48000, 2, GARSAS_FMT_F32 };
garsas_stream *s = garsas_stream_create(c, GARSAS_PLAYBACK, &f, "media");
garsas_stream_start(s);

for (;;) {
    int room = garsas_stream_writable(s);   /* frames, in YOUR format */
    if (room <= 0) { garsas_pump(c, 5); continue; }
    long n = garsas_stream_enqueue(s, buffer, filled);
    garsas_pump(c, 0);
}

garsas_stream_drain(s, 5000);               /* bounded; there is no untimed form */
garsas_stream_destroy(s);
garsas_disconnect(c);

Link with -lgarsas.

Five things a port will get wrong if it does not read them:

  • The application id is the policy key, and it must be stable across runs. A bundle id — not a process id, and not argv[0] with a version in it. An empty one still gets sound but never gets a remembered volume, because there is nothing to remember it against.
  • Volume is a linear amplitude scalar between 0 and 1, the same meaning CoreAudio and the Win32 endpoint volume both document. Not decibels, not a percentage. The effective gain is the stream's volume times the application's.
  • Latency is reported in frames at your stream's own rate, not in milliseconds. Divide by your rate at the call site, where the rate is in view.
  • A connection is not thread-safe and is not meant to be. One connection, one thread, like a CoreAudio IOProc. The library takes no locks, starts no threads and installs no signal handlers.
  • There is no render callback. garsasd tells you how many frames it consumed, how much the device still holds and how much you underran, and you pace on that. A callback contract would promise a timeliness nothing underneath can deliver: there is no stream-completion interrupt on this hardware, and the refill is poll-driven.

The fallback, which is load-bearing

If garsasd is not running, garsas_connect() still succeeds, and the library mixes your process's own streams and writes /dev/dsp itself. garsas_transport() tells you which of three you got:

GARSAS_VIA_DAEMON The normal one: policy, per-application volume, routing, events.
GARSAS_VIA_OSS No daemon: sound, but no policy.
GARSAS_VIA_NONE No daemon and no device: streams are clock-paced, nothing is heard.

GARSAS_VIA_NONE is a state, not an error. A machine with no sound card must behave the same as one with, and an application must not have to care.

The mix format

A client never converts to the device's format. It converts to the mix format — garsasd's own choice, announced once and immutable for the life of a connection.

That is what keeps routing and hotplug from making the conversion target mutable at runtime inside every client process. Plugging in a headset changes nothing in any client.

When the mix format genuinely must change, garsasd opens a numbered mix epoch and sends a format event. Frames already enqueued are played out in the format they were made for — nothing in flight is reinterpreted. The library re-aims its converters, acknowledges the epoch, and produces at the new rate from there.

Per-application volume

Per-application policy lives in garsas.conf in the configuration directory, written only by garsasd:

app lt.anotheros.Firefox 0.4000 0 Firefox

sound.conf is a different file: master volume, balance, input and the alert sound live there and belong to the Settings Sound pane. Master volume is applied to the hardware mixer, and garsasd is not in its path at all.

Reading or setting another application's volume needs a control connection (garsas_connect_control()). An ordinary client cannot enumerate or alter anything but its own streams — per-application volume is the owner's control, not an API any program may reach for.

Capture

There is one ADC, so the policy is deliberate.

  • garsasd captures once and delivers a copy to every subscribed stream. Refusing to fan out would break a call plus a recorder.
  • The first capture client fixes the format; later ones are converted in the library. The ADC is never reprogrammed under a live capture.
  • The microphone is not held open when nothing is recording. An idle held microphone is a privacy defect, a power defect, and it makes any recording indicator a lie.
  • Exclusive capture is refused while anything else is capturing. There is no pre-emption.

Below garsasd the kernel's capture is a fan-out too: every open descriptor that reads gets the whole signal, in the format it negotiated. A reader that starts late hears what is said after it started; one that falls far behind loses the oldest audio, and the kernel counts that where you can see it.