# SPDX-License-Identifier: MIT # Copyright © 2018 Wim Taymans # Copyright © 2025 Demi Marie Obenour # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), # to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, # and/or sell copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice (including the next # paragraph) shall be included in all copies or substantial portions of the # Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. # This file is based on the upstream default configuration. This can be # found in upstream GitLab or in any distro with a recent version of PipeWire. # The following changes have been made: # # - Conditions that have known values in Spectrum VMs are omitted. # - Modules for hardware devices Spectrum VMs don't have are not loaded. # - The PulseAudio emulation server is loaded. # - Settings for VMs are applied unconditionally. # - Most comments in the upstream files have been removed. # - Device nodes for virtio-sound devices have been added. # - Integration with udev and logind is removed. context.properties = { # Upstream defaults. link.max-buffers = 16 core.daemon = true core.name = pipewire-0 # Account for running in a VM default.clock.min-quantum = 1024 } # Upstream defaults, with support for AVB, V4L2, libcamera # bluez, Vulkan, JACK, and video conversion omitted. context.spa-libs = { audio.convert.* = audioconvert/libspa-audioconvert api.alsa.* = alsa/libspa-alsa support.* = support/libspa-support } context.modules = [ # Upstream defaults { name = libpipewire-module-rt args = { nice.level = -11, rt.prio = 88 } } { name = libpipewire-module-protocol-native } { name = libpipewire-module-metadata } { name = libpipewire-module-spa-device-factory } { name = libpipewire-module-spa-node-factory } { name = libpipewire-module-client-node } { name = libpipewire-module-access } { name = libpipewire-module-client-device } { name = libpipewire-module-portal } { name = libpipewire-module-adapter } { name = libpipewire-module-link-factory } { name = libpipewire-module-session-manager } # Load the PulseAudio server into PipeWire. # This avoids needing a separate pipewire-pulse # process. The args are those used when running # in a VM. { name = libpipewire-module-protocol-pulse args = { server.address = [ "unix:native" ] pulse.min.quantum = 1024/48000 } } ] context.objects = [ # Upstream defaults { factory = spa-node-factory args = { factory.name = support.node.driver node.name = Dummy-Driver node.group = pipewire.dummy node.sync-group = sync.dummy priority.driver = 200000 } } { factory = spa-node-factory args = { factory.name = support.node.driver node.name = Freewheel-Driver priority.driver = 190000 node.group = pipewire.freewheel node.sync-group = sync.dummy node.freewheel = true } } # Spectrum doesn't use udev, so device nodes must be created statically. # Creating them with pw-cli works as long as pw-cli is running, but # the nodes are destroyed when pw-cli exits. # # PipeWire chooses the node with the highest priority.driver value as # graph driver, which is the node that decides when the processing graph # is going to run. If both the capture node and playback node are in # the same graph, the capture node should be chosen as the driver. This # is because the driver gets to choose the rate of the graph and so is # much less likely to xrun. Since capture xruns result in corrupted # audio recordings, while playback xruns just result in a glitch, it # is more important to avoid capture xruns. # # When there are multiple sources or sinks that could be used, # WirePlumber links application nodes to the one with the highest # priority.session value. In the configuration created here, # there are two valid audio sources: the virtio sound card's # capture stream and the monitor of its playback stream. The # capture stream is the correct choice, so its priority.session # should be higher. # # The recommendation to give the capture device higher values # for priority.driver and priority.session comes from George # Kiagiadakis of Collabora, who also provided the values # used (2000 and 1000) and why they must be different. # See . { factory = adapter args = { alsa.card = 0, alsa.card_name = "VirtIO SoundCard" alsa.device = 0 alsa.driver_name = "virtio_snd" alsa.id = "SoundCard" alsa.long_card_name = "VirtIO SoundCard at pci/0000:00:01.0/virtio0" alsa.name = "VirtIO SoundCard" alsa.subdevice = 0 alsa.subdevice_name = "subdevice #0" api.alsa.card.longname = "VirtIO SoundCard at pci/0000:00:01.0/virtio0" api.alsa.card.name = "VirtIO SoundCard" api.alsa.headroom = 0 api.alsa.path = "hw:0,0,0" api.alsa.pcm.card = 0, api.alsa.pcm.stream = "playback" audio.allowed-rates = [ ] audio.channels = 2 audio.format = "S32" audio.position = "FL,FR" audio.rate = 48000 factory.name = "api.alsa.pcm.sink" media.class = "Audio/Sink" node.name = "alsa_output.pci-0000_00_01.0.analog-stereo" node.suspend-on-idle = true priority.driver = 1000 priority.session = 1000 } } { factory = adapter args = { alsa.card = 0, alsa.card_name = "VirtIO SoundCard" alsa.device = 0 alsa.driver_name = "virtio_snd" alsa.id = "SoundCard" alsa.long_card_name = "VirtIO SoundCard at pci/0000:00:01.0/virtio0" alsa.name = "VirtIO SoundCard" alsa.subdevice = 0 alsa.subdevice_name = "subdevice #0" api.alsa.card.longname = "VirtIO SoundCard at pci/0000:00:01.0/virtio0" api.alsa.card.name = "VirtIO SoundCard" api.alsa.headroom = 0 api.alsa.path = "hw:0,0,0" api.alsa.pcm.card = 0, api.alsa.pcm.stream = "capture" audio.allowed-rates = [ ] audio.channels = 2 audio.format = "S32" audio.position = "FL,FR" audio.rate = 48000 factory.name = "api.alsa.pcm.source" media.class = "Audio/Source" node.name = "alsa_input.pci-0000_00_01.0.analog-stereo" node.suspend-on-idle = true priority.driver = 2000 priority.session = 2000 } } ] # Load the modules that are in the default config *except* # for ones whose job is to maintain state. pulse.cmd = [ { cmd = "load-module" args = "module-always-sink" flags = [ ] } { cmd = "load-module" args = "module-device-manager" flags = [ ] } ] # More default stuff. pulse.rules = [ { matches = [ { application.process.binary = "teams" } { application.process.binary = "teams-insiders" } { application.process.binary = "teams-for-linux" } { application.process.binary = "skypeforlinux" } ] actions = { quirks = [ force-s16-info ] } } { matches = [ { application.process.binary = "firefox" } ] actions = { quirks = [ remove-capture-dont-move ] } } { matches = [ { application.name = "~speech-dispatcher.*" } ] actions = { update-props = { pulse.min.req = 512/48000 pulse.min.quantum = 512/48000 pulse.idle.timeout = 5 } } } ] context.exec = []