std.pa
PulseAudio audio control via libpulse — enumerate sinks, sources and
application streams, read and write volume / mute, change the default device,
and watch a device for live changes. Available on an optional build probe:
if libpulse was not present at build time the whole module throws
UNSUPPORTED when a function is called (see Availability).
The script surface mirrors std.pw function-for-function.
On modern desktops PipeWire serves PulseAudio clients through
pipewire-pulse, so std.pa and std.pw usually talk to the same daemon
over different protocols — std.pa is the right choice when you want
guaranteed PulseAudio compatibility (true PulseAudio daemons, older systems,
or containers where only the PA socket is forwarded).
Every function returns or accepts a compact device object:
{
id: 62, # PulseAudio index
role: "sink", # "sink" | "source" | "playback" | "record" | "other"
name: "alsa_output.usb-….analog-stereo",
description: "HyperX Cloud Alpha Wireless Analog Stereo",
volume: 0.30, # linear 0.0 .. 1.0 (30%)
muted: false,
channels: 2
}
role semantics: sink (output device), source (capture device, including
.monitor sources), playback (a running application’s output stream, its
description is the binary name), record (a running application’s capture
stream).
The device object, field by field
id— the PulseAudio index (any numeric type:62,62L,62.0all
work). Stable for the session; pass it back as a numeric spec.role— one of the five strings above.name— the device/stream name (alsa_output.…). Stable across
reconnects — prefer it overidin saved config, since indexes can shift
between sessions.description— human label for display. Never usable as a spec.volume— linear0.0…1.0float as read.set_volumeaccepts the
same range, or0…100-style integers (50and0.5are the same
request).muted— boolean.channels— channel count of the device/stream.
Reference
std.pa.list_sinks() -> array of device objects
std.pa.list_sources() -> array of device objects
std.pa.playback() -> array of device objects
std.pa.dump() -> array of all devices and streams
std.pa.default_sink() -> device object (or empty)
std.pa.default_source() -> device object (or empty)
std.pa.set_default_sink(spec) -> bool
std.pa.set_default_source(spec) -> bool
std.pa.volume(spec) -> double (0.0 .. 1.0)
std.pa.mute(spec) -> bool
std.pa.set_volume(spec, value) -> double (new volume)
std.pa.set_mute(spec, on) -> bool (new mute state)
std.pa.subscribe(spec, fn) -> unsub function
The spec argument
| Spec | Meaning |
|---|---|
number (62) |
the PulseAudio index |
string ("alsa_output.usb-…") |
the device name |
"default-sink" / "default-source" |
the current default; re-resolved on every call |
Round-trip pattern: list once, then address by the returned fields — never
by description, and never by passing the whole object back (a device
object is not itself a spec):
var sinks = std.pa.list_sinks()
var first = sinks[0]
std.pa.set_volume(first.id, 0.5) # by index — pins this device
std.pa.set_volume(first.name, 50) # by name — same request, survives reconnects
# std.pa.set_volume(first, 0.5) # error: a device object is not a spec
Unknown indexes/names throw INVALID_ARGUMENT (“no such audio device for
spec”). "default-sink" is resolved at call time, so
std.pa.volume("default-sink") always reads whatever the session default
currently is.
Listing devices
for (s: std.pa.list_sinks()) {
print("[" + s.id + "] " + s.description + " " + (s.volume * 100) + "%")
}
for (p: std.pa.playback()) {
print(p.description, "is playing at", p.volume) # per-app volume control
}
Defaults
var def = std.pa.default_sink()
if (len(def) > 0) {
print("default sink:", def.name)
} else {
print("no default sink set")
}
std.pa.set_default_sink("alsa_output.usb-….analog-stereo")
# equivalently: std.pa.set_default_sink(def.id)
Default changes are ordinary server operations (set-default-sink /
set-default-source), identical to what pactl issues. Returns true when
the server accepted the change.
Volume and mute
Volume is linear: 0.0 silent, 1.0 = 100% (PA_VOLUME_NORM). Values in
the 0..100 style are also accepted (50 ≡ 0.5); anything above 1.0 is
divided by 100 and clamped.
std.pa.set_volume("default-sink", 0.5)
std.pa.set_mute("default-sink", true)
print(std.pa.mute("default-sink")) # true
Volume, mute and default writes are server operations, so they always
stick — there is no session-manager policy that can override them.
Per-application volume works too:
for (p: std.pa.playback()) {
std.pa.set_volume(p.id, 0.2) # turn every app down
}
Subscribe
std.pa.subscribe(spec, fn) watches one device and invokes fn with a fresh
device object on every change (volume, mute, new/remove). It returns an
unsubscribe function.
The special specs follow the default: subscribe("default-sink", fn)
re-targets whenever the server’s default sink changes and reports the
newly-default device.
var unsub = std.pa.subscribe("default-sink", fn(dev) {
print("default sink now:", dev.description, "vol", dev.volume)
})
std.async.sleep(10000) # keep the script alive: exiting would drop the subscription with it
unsub()
Subscriptions run on their own private connection and event thread, so they
keep receiving events independently of the query functions. Script exceptions
thrown inside the callback are contained. An initial report is delivered as
soon as the target resolves.
Differences from std.pw
- Device ids are PulseAudio indices, not PipeWire global node ids — don’t
mix them between modules. list_sources()includes.monitorsources (loopback captures of sinks);
PulseAudio treats them as real sources. Filter with
std.string.contains(s.name, ".monitor")if you don’t want them.playback()streams report the process binary asdescription.- Volume/mute/default writes are plain server ops and always apply.
Availability
std.pa is built in whenever libpulse is detected at configure time (probed
through pkg-config). The pa member of std is always present, so
scripts that reference it still parse and load on a build without PulseAudio;
each function throws UNSUPPORTED on the first call instead:
RUNTIME_ERROR: volume() requires libpulse, which was not available at build time