std.pw
PipeWire audio control via libpipewire-0.3 — enumerate sinks, sources and
application streams, read and write volume / mute, change the session default
device, and watch a device for live changes. Available on an optional
build probe: if libpipewire-0.3 was not present at build time the whole
module throws UNSUPPORTED when a function is called (see
Availability).
Every function returns or accepts a compact device object:
{
id: 62, # PipeWire global node id
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 classifies the node by its media.class: sink (output device),
source (capture device), playback (an application’s output stream),
record (an application’s capture stream), other.
The device object, field by field
id— the PipeWire global node id (any numeric type:62,62L,62.0
all work). Stable for the session; pass it back as a numeric spec.role— one of the five strings above. Sinks/sources are hardware;
playback/record are per-application streams (theirdescriptionis
usually the binary name).name— the node name (alsa_output.…). Stable across reconnects —
prefer it overidin saved config, since ids 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.pw.list_sinks() -> array of device objects
std.pw.list_sources() -> array of device objects
std.pw.playback() -> array of device objects
std.pw.dump() -> array of all audio nodes
std.pw.default_sink() -> device object (or empty)
std.pw.default_source() -> device object (or empty)
std.pw.set_default_sink(spec) -> bool
std.pw.set_default_source(spec) -> bool
std.pw.volume(spec) -> double (0.0 .. 1.0)
std.pw.mute(spec) -> bool
std.pw.set_volume(spec, value) -> double (new volume)
std.pw.set_mute(spec, on) -> bool (new mute state)
std.pw.subscribe(spec, fn) -> unsub function
The spec argument
Functions that address a device accept three forms:
| Spec | Meaning |
|---|---|
number (62) |
the PipeWire global node id |
string ("alsa_output.usb-…") |
the node 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.pw.list_sinks()
var first = sinks[0]
std.pw.set_volume(first.id, 0.5) # by id — pins this device
std.pw.set_volume(first.name, 50) # by name — same request, survives reconnects
# std.pw.set_volume(first, 0.5) # error: a device object is not a spec
Unknown ids/names throw INVALID_ARGUMENT (“no such audio node for spec”).
"default-sink" is resolved at call time, so std.pw.volume("default-sink")
always reads whatever the session default currently is.
Listing devices
var sinks = std.pw.list_sinks()
for (s: sinks) {
var pct = s.volume * 100
print("[" + s.id + "] " + s.description + " " + pct + "%")
}
print(len(std.pw.list_sources()) + " sources")
print(len(std.pw.playback()) + " playback streams") # running apps
std.pw.dump() # everything at once: sinks + sources + streams
Defaults
var def = std.pw.default_sink()
if (len(def) > 0) {
print("default sink:", def.name, "at", def.volume)
} else {
print("no default sink set")
}
std.pw.set_default_sink(def.id) # by id …
std.pw.set_default_sink(def.name) # … or by name
The default is written through the session manager’s "default" metadata
(default.audio.sink / default.audio.source on the core subject), which
WirePlumber applies. Returns true when the write was accepted.
If no default is configured, default_sink() / default_source() return an
empty object. len() works on objects (it counts keys), so test with
len(def) > 0 as above — reading name/id on the empty object throws.
Volume and mute
Volume is linear: 0.0 is silent, 1.0 is 100% (the same percentage
wpctl / pactl print). Values above 1.0 passed as 0..100 style integers
are also accepted and clamped — 50 and 0.5 are the same request.
std.pw.set_volume("default-sink", 0.5) # 50%
std.pw.set_volume(62, 80) # 80%, by id
std.pw.set_mute("default-sink", true)
print(std.pw.mute("default-sink")) # true
print(std.pw.volume("default-sink")) # 0.5
On a WirePlumber session the sink volume lives on the card device’s active
route; std.pw writes it there (the same place the session manager does),
so changes stick and are visible to wpctl / pactl immediately. Nodes
without a card (application playback streams) are written through node Props.
Subscribe
std.pw.subscribe(spec, fn) watches one device and invokes fn with a fresh
device object whenever its volume, mute or properties change. It returns an
unsubscribe function.
The special specs follow the default: subscribe("default-sink", fn)
re-targets whenever the default sink changes and reports the newly-default
device instead.
var unsub = std.pw.subscribe("default-sink", fn(dev) {
print("default sink now:", dev.description, "vol", dev.volume)
})
# Plug in a headset and switch defaults — the callback keeps firing
# with whatever device is currently the default.
std.async.sleep(10000) # keep the script alive: exiting would drop the subscription with it
unsub()
Subscriptions run on their own private PipeWire connection and event loop, so
they keep receiving events independently of the query functions. Script
exceptions thrown inside the callback are contained; they never crash the
dispatch loop. An initial report is delivered as soon as the target resolves.
Availability
std.pw is built in whenever libpipewire-0.3 is detected at configure time
(probed through pkg-config). The pw member of std is always present,
so scripts that reference it still parse and load on a build without PipeWire;
each function throws UNSUPPORTED on the first call instead:
RUNTIME_ERROR: volume() requires libpipewire-0.3, which was not available at build time