Set endpoint volume and controls
This guide shows how to change an endpoint’s volume, mute an output,
close a microphone, and set a sound card’s controls with kubectl.
These changes do not need a claim. They do not interrupt a pod that
is playing or recording. You need the operator
installed
on your
liken
cluster.
Every output and input that the operator publishes has its own
resource: a Sink
for playback and a
Source
for capture. You write the
settings you want in spec, and the operator writes the hardware’s
facts and its latest readings in status.
The operator applies a volume and mute from spec when you change
them and when the endpoint appears. It applies a control or a codec
from spec again whenever the hardware drifts from it. None of this
needs the speaker to be free: a pod can claim it and play while the
operator applies the settings. The endpoint volume is separate from
the volume of the pod’s own stream.
While a claim holds a Bluetooth speaker, the codec in the claim’s
parameters applies. The codec in the Sink’s spec takes effect when
that claim ends.
1. See what is there
kubectl get sinks
kubectl get sources
Each row is one endpoint: the node it is on, how it connects, the level and mute it was last read at, which claim holds it, and whether it is connected and ready. To see everything the operator reports about one, read the whole resource:
kubectl get sink kitchen-pci-0000-00-1f-3-hdmi-0 -o yaml
Read two parts of status. capabilities lists the controls the
sound card offers for this endpoint, including the range or choices
for each control. observed is the last value the operator read for
each setting. The operator updates it when the hardware reports a
change.
Turn a knob on a USB DAC, press the volume button on a Bluetooth
speaker, or let a client change the graph, and the new value shows
here within about a second. An endpoint that nothing is playing
through has no level of its own to read. observed then shows the
level the operator last wrote to it, from your declaration or from a
volume ask, which is the level it will start at. Until the operator
writes one, it shows no level at all.
2. Set the volume
kubectl patch sink a0-ab-51-33-b7-12 --type merge \
-p '{"spec":{"volume":{"level":40}}}'
volume.level is a percentage, where 100 is full level with no gain
applied. For an output on the sound card, this is the software
level PipeWire applies. For a Bluetooth speaker, it is the
speaker’s own volume: the operator sends it over AVRCP when the
speaker supports absolute volume, so the number on the speaker’s
display moves too.
The change takes effect at once, even while a pod is playing through the speaker. The pod’s own stream volume is a separate control on top of this one, so the two never conflict.
The declared level is where the endpoint starts. The operator writes
it when you change it and each time the endpoint appears: a speaker
that reconnects, or a node that PipeWire builds again. Between those
moments the level can move. A press of the speaker’s own button, a
client that changes the graph, and a remote’s volume key all change
it, and the operator reports the new level in observed and does not
write your value back. An operator restart writes nothing, so the
level a person chose before the restart stays. If you never declare a
level, a node that PipeWire builds while the operator runs starts at
100.
A remote’s volume key reaches a Sink through the media operator. It
writes each press as an ask in status.session.volumeAsk, and the
operator applies each new ask once. volume.max is the highest level
an ask sets, 100 unless you declare it, and volume.step is how far
one press moves the level, 5 unless you declare it:
kubectl patch sink a0-ab-51-33-b7-12 --type merge \
-p '{"spec":{"volume":{"level":40,"max":80,"step":2}}}'
A person at the speaker can still turn it above max.
3. Mute an output, or close a microphone
kubectl patch sink kitchen-pci-0000-00-1f-3-hdmi-0 --type merge \
-p '{"spec":{"mute":true}}'
The television goes silent. A player that holds it keeps running,
and it plays into silence until you set mute back to false.
The same field on a Source closes a microphone:
kubectl patch source kitchen-usb-0573-1573-a34004801402-usb-audio-capture \
--type merge -p '{"spec":{"mute":true}}'
To close every microphone in the house at once, run that patch over
the list from kubectl get sources.
4. Set a control on the sound card itself
Some cards have their own hardware controls: a USB DAC’s volume, a
laptop codec’s headphone switch, an input selector on a card with
several jacks. status.capabilities lists them under the names the
kernel uses, and you set them in spec.controls under the same
names. An integer control takes a number in its range, a switch
takes on or off, and a selector takes one of its listed values:
kubectl patch sink kitchen-usb-0573-1573-a34004801402-usb-audio --type merge \
-p '{"spec":{"controls":{"PCM Playback Volume":"96"}}}'
The operator checks the name and the value against the capability before it writes. A name the card does not have, or a value out of range, is skipped and logged rather than written:
kubectl -n liken-system logs ds/audio-operator | grep controls
Not every endpoint has controls. An HDMI output has only its
IEC958 Playback Switch, because an HDMI PCM has no volume control
of its own. Use volume.level for its level. A Bluetooth speaker has
none.
5. Remove a declared setting
Remove the field, and the operator stops applying it. The hardware
keeps whatever value it has at that moment, because the operator
never makes up a value on its own. Removing volume also removes
max and step, and a remote’s volume key then steps by the
defaults:
kubectl patch sink a0-ab-51-33-b7-12 --type json \
-p '[{"op":"remove","path":"/spec/volume"}]'
6. Set the channel layout
A sink of a sound card plays a multichannel stream on its own channels only when PipeWire knows the position of each one. The operator reads the positions from an HDMI monitor’s ELD and from a USB device’s channel map. The analog jack reports no speakers, so its streams play in stereo until you state the layout. See which layout each sink has:
kubectl get sinks
To play 5.1 through the analog outputs, list the positions in the order of the card’s PCM slots:
kubectl patch sink node-1-pci-0000-00-1f-3-alc257-analog --type merge \
-p '{"spec":{"layout":["FL","FR","RL","RR","FC","LFE"]}}'
The same field overrides a monitor or a device that reports its
layout wrong. A new layout restarts PipeWire, and the restart ends
every stream on the machine. So the operator waits until nothing
plays. While it waits, the Sink’s LayoutApplied condition is
False with the reason AwaitingIdle. Remove the field to return
the sink to the layout its hardware reports:
kubectl patch sink node-1-pci-0000-00-1f-3-alc257-analog --type json \
-p '[{"op":"remove","path":"/spec/layout"}]'
The operator sends PCM and does not write the kernel’s channel map.
So a height speaker plays only what a receiver makes from the other
channels, and Dolby Atmos and DTS:X do not reach the receiver. The
Sink reference
gives
the layouts the operator selects and the reasons.
Grant volume control
Because the resources are ordinary Kubernetes objects, RBAC decides
who may change them. A role that can patch sinks but not
sources fits a wall remote or a home automation rule that sets
volume and must never touch a microphone:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: volume-remote
rules:
- apiGroups: [audio.liken.sh]
resources: [sinks]
verbs: [get, list, watch, patch]
If two writers share one resource, server-side apply keeps them
apart. A remote that applies only spec.volume.level under its own field
manager and a person who sets spec.controls never overwrite each
other. If they do collide on one field, the API server reports a
conflict instead of silently taking the last write.