Skip to content

pipewire: Audio sinks where only large volume changes are "significant" #279

@real-or-random

Description

@real-or-random

My audio sink is a SteelSeries Arctis Pro Wireless. This sink appear to be special. Only "very" significant volume changes are applied and result in an update from pipewire.

As a result, small volume changes make the volume stuck as quickshell waits for an update:

// Successful volume change (but the resulting volume is quite different from what we've asked for)
INFO quickshell.service.pipewire.node: Changing volumes of PwNode(0x7f2641b01280, id=64/bound) to QList(0.34, 0.34) via device
INFO quickshell.service.pipewire.device: Changing volumes of PwDevice(0x7f2641b185c0, id=62/bound) on route device 1 to QList(0.34, 0.34)
INFO quickshell.service.pipewire.node: Got updated volumes of PwNode(0x7f2641b01280, id=64/bound) - QList(0.3125, 0.3125)

// Another successful one: We ask for 0.35 and get 0.375
INFO quickshell.service.pipewire.node: Changing volumes of PwNode(0x7f2641b01280, id=64/bound) to QList(0.35, 0.35) via device
INFO quickshell.service.pipewire.device: Changing volumes of PwDevice(0x7f2641b185c0, id=62/bound) on route device 1 to QList(0.35, 0.35)
INFO quickshell.service.pipewire.node: Got updated volumes of PwNode(0x7f2641b01280, id=64/bound) - QList(0.375, 0.375)

// Now we ask for 0.36, which most likely also translates to 0.375. So we see no actual change and no update. 
INFO quickshell.service.pipewire.node: Changing volumes of PwNode(0x7f2641b01280, id=64/bound) to QList(0.36, 0.36) via device
INFO quickshell.service.pipewire.device: Changing volumes of PwDevice(0x7f2641b185c0, id=62/bound) on route device 1 to QList(0.36, 0.36)

// We'll wait forever before issuing another change.
INFO quickshell.service.pipewire.node: Waiting to change volumes of PwNode(0x7f2641b01280, id=64/bound) to QList(0.77, 0.77) via device

I've seen this logic here:

auto significantChange = this->mServerVolumes.isEmpty();
for (auto i = 0; i < this->mServerVolumes.length(); i++) {
auto serverVolume = this->mServerVolumes.value(i);
auto targetVolume = realVolumes.value(i);
if (targetVolume == 0 || abs(targetVolume - serverVolume) >= 0.0001) {
significantChange = true;
break;
}
}
if (significantChange) {
qCInfo(logNode) << "Changing volumes of" << this->node << "to" << realVolumes
<< "via device";
if (!this->node->device->setVolumes(this->node->routeDevice, realVolumes)) {
return;
}
this->mDeviceVolumes = realVolumes;
this->node->device->waitForDevice();
} else {
// Insignificant changes won't cause an info event on the device, leaving qs hung in the
// "waiting for acknowledgement" state forever.
qCInfo(logNode) << "Ignoring volume change for" << this->node << "to" << realVolumes
<< "from" << this->mServerVolumes
<< "as it is a device node and the change is too small.";
}

I believe it's the wrong approach for my device. The threshold would have to be set to something like 0.02 or 0.03, which feels too large (?).

Perhaps the right thing to do, which hopefully works for all devices, is to use time-based debouncing? That could be easily implemented by changing the implementation of waitForDevice() and waitingForDevice().

Or, simpler, is it really necessary to have some kind of rate limiting? I've skimmed the source code of pavucontrol, and it doesn't seem to do anything like this.

Further information

quickshell works for other sinks on my system. I don't think there's any special property that could tell quickshell that the threshold should be high here. pw-cli tells me this:

	id: 64
	permissions: rwxm-
	type: PipeWire:Interface:Node/3
*	input ports: 2/65
*	output ports: 2/0
*	state: "suspended"
*	properties:
*		alsa.card = "1"
*		alsa.card_name = "Arctis Pro Wireless"
*		alsa.class = "generic"
*		alsa.components = "USB1038:1294"
*		alsa.device = "1"
*		alsa.driver_name = "snd_usb_audio"
*		alsa.id = "USB Audio"
*		alsa.long_card_name = "SteelSeries Arctis Pro Wireless at usb-0000:00:14.0-5.1.1.2.3, full speed"
*		alsa.mixer_name = "USB Mixer"
*		alsa.name = "USB Audio #1"
*		alsa.resolution_bits = "16"
*		alsa.subclass = "generic-mix"
*		alsa.subdevice = "0"
*		alsa.subdevice_name = "subdevice #0"
*		alsa.sync.id = "00000000:00000000:00000000:00000000"
*		api.alsa.card.longname = "SteelSeries Arctis Pro Wireless at usb-0000:00:14.0-5.1.1.2.3, full speed"
*		api.alsa.card.name = "Arctis Pro Wireless"
*		api.alsa.path = "hw:1,1,0"
*		api.alsa.pcm.card = "1"
*		api.alsa.pcm.stream = "playback"
*		audio.channels = "2"
*		audio.position = "FL,FR"
*		card.profile.device = "1"
*		device.api = "alsa"
*		device.bus = "usb"
*		device.class = "sound"
*		device.icon-name = "audio-card-analog"
*		device.id = "62"
*		device.profile.description = "Game"
*		device.profile.name = "stereo-game"
*		device.routes = "1"
*		factory.name = "api.alsa.pcm.sink"
*		media.class = "Audio/Sink"
*		node.description = "Arctis Pro Wireless Game"
*		node.name = "alsa_output.usb-SteelSeries_Arctis_Pro_Wireless-00.stereo-game"
*		node.nick = "USB Audio #1"
*		node.pause-on-idle = "false"
*		object.path = "alsa:acp:Wireless:1:playback"
*		port.group = "playback"
*		priority.driver = "828"
*		priority.session = "828"
*		factory.id = "19"
*		clock.quantum-limit = "8192"
*		client.id = "41"
*		node.driver = "true"
*		node.loop.name = "data-loop.0"
*		library.name = "audioconvert/libspa-audioconvert"
*		object.id = "64"
*		object.serial = "64"
*	params: (9)
*	 3 (Spa:Enum:ParamId:EnumFormat) r-
*	 1 (Spa:Enum:ParamId:PropInfo) r-
*	 2 (Spa:Enum:ParamId:Props) rw
*	 4 (Spa:Enum:ParamId:Format) -w
*	 10 (Spa:Enum:ParamId:EnumPortConfig) r-
*	 11 (Spa:Enum:ParamId:PortConfig) rw
*	 15 (Spa:Enum:ParamId:Latency) rw
*	 16 (Spa:Enum:ParamId:ProcessLatency) rw
*	 17 (Spa:Enum:ParamId:Tag) rw

I use Dank Material Shell.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions