From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from atuin.qyliss.net (localhost [IPv6:::1]) by atuin.qyliss.net (Postfix) with ESMTP id A776E13F73; Sun, 20 Jul 2025 18:11:33 +0000 (UTC) Received: by atuin.qyliss.net (Postfix, from userid 993) id 7015F13F68; Sun, 20 Jul 2025 18:11:31 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-26) on atuin.qyliss.net X-Spam-Level: * X-Spam-Status: No, score=1.9 required=3.0 tests=DKIM_SIGNED,DKIM_VALID, DKIM_VALID_AU,DMARC_PASS,FREEMAIL_FROM,PDS_OTHER_BAD_TLD, RCVD_IN_DNSWL_NONE,SPF_HELO_NONE autolearn=no autolearn_force=no version=4.0.1 Received: from mail-qv1-xf31.google.com (mail-qv1-xf31.google.com [IPv6:2607:f8b0:4864:20::f31]) by atuin.qyliss.net (Postfix) with ESMTPS id BA49513F67 for ; Sun, 20 Jul 2025 18:11:29 +0000 (UTC) Received: by mail-qv1-xf31.google.com with SMTP id 6a1803df08f44-6fd0a3cd326so41437226d6.1 for ; Sun, 20 Jul 2025 11:11:29 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1753035089; x=1753639889; darn=spectrum-os.org; h=content-transfer-encoding:autocrypt:cc:to:in-reply-to :content-language:references:subject:from:user-agent:mime-version :date:message-id:from:to:cc:subject:date:message-id:reply-to; bh=pI4uFgJpisvyo0djIsvRno1vLISyS0uRPMuM+Euw0wY=; b=EFX0E5RuApo7hW0b2sEd2iJTOWfmumgUpJnxoGPLikQRwlBoctQ/YAkhB9Yy/XMM7A IkEvMhRiwDC9pxHf2ZURTCy95/fN6eEj2BDYzZ1svW/Vd7Mm4r6Z8BPltSSCrFhut7w9 FaoXSLr5PyxfOI/NWxQ8zFeQ8cFKApYhU3h2FWngNl1HoMz9JN71vE07A/QCo1PyzM6U XUVXVBAVSLcW4N4ALPqtYrUvCETIr1EdF/g3rZ51ARfiEPEQYQU5tTbRWdJxTOiiXFe4 6xBpLZ+uYkLK1+qK3iAggDcW2wfg2wt/mGp+HhzpjEDlGlfayHUa581LtSr9BxB2waxX HItw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1753035089; x=1753639889; h=content-transfer-encoding:autocrypt:cc:to:in-reply-to :content-language:references:subject:from:user-agent:mime-version :date:message-id:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=pI4uFgJpisvyo0djIsvRno1vLISyS0uRPMuM+Euw0wY=; b=hiR3DXX1ZcVEHBwCrVerbW2zTe5Pkt7osH97xAMsMCk7vpSQZAukZFD8OF3mk9M2Uz mKpkqFvt2QM7rAg0CWoNU0QnHjNk7b6/HIbIoo4U7FI2S33r2th7b+Che1oi8FK6+MEF UDDAHE3q0yRelm9noN8txSuVvii55KboE1AD2OgvZwjoqverFFS56N3uKqWOrk85PoEa xSJ16/H0BNusko/TOWrYlC8wbUIbpWVxc5xpSoOWT5a+K03nV1ZCXpL/EhKfpzEFoKem 7jdubs5I8JntM/N/1/+gcYrlgc0DumdYVmKyvgF7LjE/C0sbKNcwgeXN5T0G7e7DSNDs xfpg== X-Gm-Message-State: AOJu0YxZ/9LHlB68zf7oOJc3uFeu3dt/eBzan0u6/D2bM/iJ6rPaU+MU hwZudEq7NCYturPx7kdJArCS3rXYCdU3tKtlwa3B3OVS1At4KHZ7DVEmwh11bA== X-Gm-Gg: ASbGncuG3YLFIi7iT+U25gJkGhtXFg67CumGeqFQiVqRXbxrfQBoKbxLxmauhUKkRjB FCcdoh40jnKTJvf01RtDH0WDOjwuCSNVPAwbnWtKwznz0MAVCCn4Mvpc6dWNnr8xIjFEwnnTFka 5aTnNvEQDDhF1LvRW5mgXqSjOk5L61ve3WRLvygqVPSzSDct2u+yPquey4Neu60lxixfi2OG7MK 9MuFVuEAv48r4DnlCQ3k7v/CgSogLaWqCW1dtWCokt+YLlE1BB0MjHE2G1XT2/B4+/QPFitTKGf 8WbiDOkGAWcyct58vmO7W2v3gYwJgH9j5C0H/dUG++otk0Ntsz9qA7Jvj0xKShieTINuCqWpZxD Lf9GMiRPsB5vVfQDWcZ9EnkBCbDY= X-Google-Smtp-Source: AGHT+IHmyKUT8Ru9dt/jiD5VmON3/hlzSg0PyHus28rroEhqr8El95UraO+xwwiOr1micWEWvDOj+Q== X-Received: by 2002:ad4:4ee1:0:b0:702:d0e6:6e6b with SMTP id 6a1803df08f44-704f6bfd432mr299203836d6.45.1753035088276; Sun, 20 Jul 2025 11:11:28 -0700 (PDT) Received: from [10.138.10.6] ([89.187.178.201]) by smtp.gmail.com with ESMTPSA id 6a1803df08f44-7051b9057f4sm30100186d6.34.2025.07.20.11.11.27 (version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128); Sun, 20 Jul 2025 11:11:27 -0700 (PDT) Message-ID: <7f88f09b-5671-4f1e-86d7-df08dc2561eb@gmail.com> Date: Sun, 20 Jul 2025 14:11:25 -0400 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird From: Demi Marie Obenour Subject: [PATCH v5 8/8] img/app: Run PipeWire and WirePlumber in the VMs References: Content-Language: en-US In-Reply-To: To: Spectrum OS Development Autocrypt: addr=demiobenour@gmail.com; keydata= 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 +MYSfkEjBz0E8CLOcAw7JIwAaeBT Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Message-ID-Hash: R4DIGGCCEZW4VCIALVXYLFI4D4745PIR X-Message-ID-Hash: R4DIGGCCEZW4VCIALVXYLFI4D4745PIR X-MailFrom: demiobenour@gmail.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation; header-match-devel.spectrum-os.org-0; header-match-devel.spectrum-os.org-1; header-match-devel.spectrum-os.org-2; header-match-devel.spectrum-os.org-3; header-match-devel.spectrum-os.org-4; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Alyssa Ross X-Mailman-Version: 3.3.9 Precedence: list List-Id: Patches and low-level development discussion Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: WirePlumber is completely overkill as a session manager here, and ideally a trivial session manager would be used instead. I did build a Spectrum OS image and found that PipeWire and WirePlumber both successfully started. PipeWire is configured to listen on the PulseAudio socket, so PulseAudio compatibility works. This does bring a log of unnecessary files into the VMs, which will hopefully be removed later as part of a debloating effort. Signed-off-by: Demi Marie Obenour --- img/app/Makefile | 17 +- img/app/default.nix | 3 + img/app/etc/mdev.conf | 3 + img/app/etc/pipewire/pipewire.conf | 199 +++++++ .../etc/s6-rc/app/dependencies.d/wireplumber | 0 .../s6-rc/pipewire/dependencies.d/directories | 0 .../etc/s6-rc/pipewire/dependencies.d/mdevd | 0 img/app/etc/s6-rc/pipewire/notification-fd | 1 + .../s6-rc/pipewire/notification-fd.license | 2 + img/app/etc/s6-rc/pipewire/run | 25 + img/app/etc/s6-rc/pipewire/type | 1 + img/app/etc/s6-rc/pipewire/type.license | 2 + .../etc/s6-rc/wireplumber/dependencies.d/dbus | 0 .../s6-rc/wireplumber/dependencies.d/pipewire | 0 img/app/etc/s6-rc/wireplumber/run | 4 + img/app/etc/s6-rc/wireplumber/type | 1 + img/app/etc/s6-rc/wireplumber/type.license | 2 + img/app/etc/wireplumber/wireplumber.conf | 536 ++++++++++++++++++ 18 files changed, 794 insertions(+), 2 deletions(-) create mode 100644 img/app/etc/pipewire/pipewire.conf create mode 100644 img/app/etc/s6-rc/app/dependencies.d/wireplumber create mode 100644 img/app/etc/s6-rc/pipewire/dependencies.d/directories create mode 100644 img/app/etc/s6-rc/pipewire/dependencies.d/mdevd create mode 100644 img/app/etc/s6-rc/pipewire/notification-fd create mode 100644 img/app/etc/s6-rc/pipewire/notification-fd.license create mode 100644 img/app/etc/s6-rc/pipewire/run create mode 100644 img/app/etc/s6-rc/pipewire/type create mode 100644 img/app/etc/s6-rc/pipewire/type.license create mode 100644 img/app/etc/s6-rc/wireplumber/dependencies.d/dbus create mode 100644 img/app/etc/s6-rc/wireplumber/dependencies.d/pipewire create mode 100644 img/app/etc/s6-rc/wireplumber/run create mode 100644 img/app/etc/s6-rc/wireplumber/type create mode 100644 img/app/etc/s6-rc/wireplumber/type.license create mode 100644 img/app/etc/wireplumber/wireplumber.conf diff --git a/img/app/Makefile b/img/app/Makefile index e11be09a3c6ca801d9211e49b58e3d05d57e344e..734a76f018dfc1deaa9bf2cfbd4fa0d6885f0546 100644 --- a/img/app/Makefile +++ b/img/app/Makefile @@ -53,7 +53,10 @@ VM_FILES = \ etc/s6-linux-init/scripts/rc.init \ etc/s6-linux-init/scripts/rc.shutdown \ etc/s6-linux-init/scripts/rc.shutdown.final \ - etc/xdg/xdg-desktop-portal/portals.conf + etc/xdg/xdg-desktop-portal/portals.conf \ + etc/pipewire/pipewire.conf \ + etc/wireplumber/wireplumber.conf + VM_DIRS = dev run proc sys tmp \ etc/s6-linux-init/run-image/service VM_FIFOS = etc/s6-linux-init/run-image/service/s6-linux-init-shutdownd/fifo @@ -84,6 +87,7 @@ VM_S6_RC_FILES = \ etc/s6-rc/app/dependencies.d/dbus \ etc/s6-rc/app/dependencies.d/directories \ etc/s6-rc/app/dependencies.d/wayland-proxy-virtwl \ + etc/s6-rc/app/dependencies.d/wireplumber \ etc/s6-rc/app/run \ etc/s6-rc/app/type \ etc/s6-rc/dbus/dependencies.d/directories \ @@ -100,10 +104,18 @@ VM_S6_RC_FILES = \ etc/s6-rc/mdevd/type \ etc/s6-rc/ok-all/contents \ etc/s6-rc/ok-all/type \ + etc/s6-rc/pipewire/dependencies.d/directories \ + etc/s6-rc/pipewire/notification-fd \ + etc/s6-rc/pipewire/run \ + etc/s6-rc/pipewire/type \ etc/s6-rc/wayland-proxy-virtwl/dependencies.d/directories \ etc/s6-rc/wayland-proxy-virtwl/notification-fd \ etc/s6-rc/wayland-proxy-virtwl/run \ - etc/s6-rc/wayland-proxy-virtwl/type + etc/s6-rc/wayland-proxy-virtwl/type \ + etc/s6-rc/wireplumber/dependencies.d/dbus \ + etc/s6-rc/wireplumber/dependencies.d/pipewire \ + etc/s6-rc/wireplumber/run \ + etc/s6-rc/wireplumber/type build/etc/s6-rc: $(VM_S6_RC_FILES) mkdir -p $$(dirname $@) @@ -150,6 +162,7 @@ run-qemu: $(imgdir)/appvm/blk/root.img start-vhost-user-net start-virtiofsd -chardev socket,id=virtiofsd,path=build/virtiofsd.sock \ -device vhost-user-fs-pci,chardev=virtiofsd,tag=virtiofs0 \ -device virtio-gpu-rutabaga-pci,cross-domain=on,hostmem=8G \ + -audio driver=pipewire,model=virtio \ -object memory-backend-memfd,id=mem,size=256M,share=on \ -numa node,memdev=mem \ -device vhost-vsock-pci,guest-cid=3 \ diff --git a/img/app/default.nix b/img/app/default.nix index 740643ac41f6473cdb6f6b0fd1f5f47f4493240d..d3eed1f0accdc8968d1ba5bdec74ab597789082f 100644 --- a/img/app/default.nix +++ b/img/app/default.nix @@ -48,6 +48,9 @@ let pkgs.xwayland pkgs.xdg-desktop-portal pkgs.xdg-desktop-portal-gtk + # Depends on pulseaudio libs + pkgs.pipewire + pkgs.wireplumber ]; })).fhsenv; in diff --git a/img/app/etc/mdev.conf b/img/app/etc/mdev.conf index f2101e1f683c49808358b25520080c59ed2afa8e..80efb77c68cd533b4c46e9d15966807a3ff084dd 100644 --- a/img/app/etc/mdev.conf +++ b/img/app/etc/mdev.conf @@ -5,3 +5,6 @@ $INTERFACE=.* 0:0 660 ! +/etc/mdev/iface $MODALIAS=virtio:d0000001Av.* 0:0 660 ! +/etc/mdev/virtiofs dri/card0 0:0 660 +background { /etc/mdev/listen card0 } +snd/pcmC0D0p 0:0 660 +background { /etc/mdev/listen pcmC0D0p } +snd/pcmC0D0c 0:0 660 +background { /etc/mdev/listen pcmC0D0c } +snd/controlC0 0:0 660 +background { /etc/mdev/listen controlC0 } diff --git a/img/app/etc/pipewire/pipewire.conf b/img/app/etc/pipewire/pipewire.conf new file mode 100644 index 0000000000000000000000000000000000000000..cbea25d2c7ca274db8a3c8772439b0d3a8279f13 --- /dev/null +++ b/img/app/etc/pipewire/pipewire.conf @@ -0,0 +1,199 @@ +# 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. + { 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" + node.suspend-on-idle = true + } + } + { 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" + node.suspend-on-idle = true + } + } +] + +# 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 = [] diff --git a/img/app/etc/s6-rc/app/dependencies.d/wireplumber b/img/app/etc/s6-rc/app/dependencies.d/wireplumber new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/img/app/etc/s6-rc/pipewire/dependencies.d/directories b/img/app/etc/s6-rc/pipewire/dependencies.d/directories new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/img/app/etc/s6-rc/pipewire/dependencies.d/mdevd b/img/app/etc/s6-rc/pipewire/dependencies.d/mdevd new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/img/app/etc/s6-rc/pipewire/notification-fd b/img/app/etc/s6-rc/pipewire/notification-fd new file mode 100644 index 0000000000000000000000000000000000000000..7ed6ff82de6bcc2a78243fc9c54d3ef5ac14da69 --- /dev/null +++ b/img/app/etc/s6-rc/pipewire/notification-fd @@ -0,0 +1 @@ +5 diff --git a/img/app/etc/s6-rc/pipewire/notification-fd.license b/img/app/etc/s6-rc/pipewire/notification-fd.license new file mode 100644 index 0000000000000000000000000000000000000000..c4a0586a407fe14c3e0855749a7524ac3871dda4 --- /dev/null +++ b/img/app/etc/s6-rc/pipewire/notification-fd.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour diff --git a/img/app/etc/s6-rc/pipewire/run b/img/app/etc/s6-rc/pipewire/run new file mode 100644 index 0000000000000000000000000000000000000000..2b3c001fb4f8b00e39aeeefe28ad6a4f7e4760a4 --- /dev/null +++ b/img/app/etc/s6-rc/pipewire/run @@ -0,0 +1,25 @@ +#!/bin/execlineb -P +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2023-2024 Alyssa Ross +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour + +s6-ipcserver-socketbinder -B /run/user/0/pipewire-0 +fdmove -c 3 0 + +s6-ipcserver-socketbinder -B /run/user/0/pipewire-0-manager +fdmove -c 4 0 + +redirfd -r 0 /dev/null + +# Wait for sound devices to be available +if { /etc/mdev/wait pcmC0D0p } +if { /etc/mdev/wait pcmC0D0c } +if { /etc/mdev/wait controlC0 } + +# Notify readiness. +if { fdmove 1 5 echo } +fdclose 5 + +export LISTEN_FDS 2 +getpid LISTEN_PID +pipewire diff --git a/img/app/etc/s6-rc/pipewire/type b/img/app/etc/s6-rc/pipewire/type new file mode 100644 index 0000000000000000000000000000000000000000..5883cff0cd1514b2836f4ffa39fdac769a5213cb --- /dev/null +++ b/img/app/etc/s6-rc/pipewire/type @@ -0,0 +1 @@ +longrun diff --git a/img/app/etc/s6-rc/pipewire/type.license b/img/app/etc/s6-rc/pipewire/type.license new file mode 100644 index 0000000000000000000000000000000000000000..c4a0586a407fe14c3e0855749a7524ac3871dda4 --- /dev/null +++ b/img/app/etc/s6-rc/pipewire/type.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour diff --git a/img/app/etc/s6-rc/wireplumber/dependencies.d/dbus b/img/app/etc/s6-rc/wireplumber/dependencies.d/dbus new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/img/app/etc/s6-rc/wireplumber/dependencies.d/pipewire b/img/app/etc/s6-rc/wireplumber/dependencies.d/pipewire new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/img/app/etc/s6-rc/wireplumber/run b/img/app/etc/s6-rc/wireplumber/run new file mode 100644 index 0000000000000000000000000000000000000000..e721d8df9b97f05812d63520c1b450092986be96 --- /dev/null +++ b/img/app/etc/s6-rc/wireplumber/run @@ -0,0 +1,4 @@ +#!/bin/execlineb -P +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour +wireplumber diff --git a/img/app/etc/s6-rc/wireplumber/type b/img/app/etc/s6-rc/wireplumber/type new file mode 100644 index 0000000000000000000000000000000000000000..5883cff0cd1514b2836f4ffa39fdac769a5213cb --- /dev/null +++ b/img/app/etc/s6-rc/wireplumber/type @@ -0,0 +1 @@ +longrun diff --git a/img/app/etc/s6-rc/wireplumber/type.license b/img/app/etc/s6-rc/wireplumber/type.license new file mode 100644 index 0000000000000000000000000000000000000000..c4a0586a407fe14c3e0855749a7524ac3871dda4 --- /dev/null +++ b/img/app/etc/s6-rc/wireplumber/type.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour diff --git a/img/app/etc/wireplumber/wireplumber.conf b/img/app/etc/wireplumber/wireplumber.conf new file mode 100644 index 0000000000000000000000000000000000000000..68871e93083572976bba29b44f07eb35b86c0275 --- /dev/null +++ b/img/app/etc/wireplumber/wireplumber.conf @@ -0,0 +1,536 @@ +# SPDX-License-Identifier: MIT + +# Copyright © 2019-2021 Collabora Ltd. +# +# 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 WirePlumber. +# 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. +# - Settings for VMs are applied unconditionally. +# - Most comments in the upstream files have been removed. +# - Integration with udev and logind is removed. +# - RT scheduling for the data thread is enabled. +# - The settings are those appropriate for a system-wide instance. + +context.spa-libs = { + api.alsa.* = alsa/libspa-alsa + audio.convert.* = audioconvert/libspa-audioconvert + support.* = support/libspa-support +} + +# Upstream default, with RT scheduling enabled. +context.modules = [ + { + name = libpipewire-module-rt + args = { nice.level = -11, rt.prio = 88 } + } + { name = libpipewire-module-protocol-native } + { name = libpipewire-module-metadata } +] + +wireplumber.profiles = { + main = { + metadata.sm-settings = required + metadata.sm-objects = required + + policy.standard = required + + hardware.audio = required + + # PipeWire Media Session is not running. + # Do not bother to check. + check.no-media-session = disabled + + # No Bluetooth + hardware.bluetooth = disabled + bluetooth.use-persistent-storage = disabled + bluetooth.autoswitch-to-headset-profile = disabled + + # No video capture + hardware.video-capture = disabled + + # There is only one instance of WirePlumber, so + # pretend that this is a system-wide instance. + # See mixin.systemwide-session in the upstream wireplumber.conf. + support.reserve-device = disabled + monitor.alsa.reserve-device = disabled + support.portal-permissionstore = disabled + script.client.access-portal = disabled + support.logind = disabled + monitor.bluez.seat-monitoring = disabled + + # Disable anything that could store state. + # See mixin.stateless in the upstream wireplumber.conf. + hooks.device.profile.state = disabled + hooks.device.routes.state = disabled + hooks.default-nodes.state = disabled + hooks.stream.state = disabled + } +} + +wireplumber.components = [ + { + name = export-core, type = built-in + provides = support.export-core + } + { + name = libpipewire-module-client-node, type = pw-module + provides = pw.client-node + wants = [ support.export-core ] + } + { + name = libpipewire-module-client-device, type = pw-module + provides = pw.client-device + wants = [ support.export-core ] + } + { + name = libpipewire-module-spa-node-factory, type = pw-module + provides = pw.node-factory.spa + requires = [ pw.client-node ] + } + { + name = libpipewire-module-adapter, type = pw-module + provides = pw.node-factory.adapter + requires = [ pw.client-node ] + } + { + name = libwireplumber-module-settings, type = module + arguments = { metadata.name = sm-settings } + provides = metadata.sm-settings + } + { + name = settings-instance, type = built-in + arguments = { metadata.name = sm-settings } + provides = support.settings + after = [ metadata.sm-settings ] + } + { + name = libwireplumber-module-lua-scripting, type = module + provides = support.lua-scripting + } + { + name = libwireplumber-module-standard-event-source, type = module + provides = support.standard-event-source + } + { + name = libwireplumber-module-si-node, type = module + provides = si.node + } + { + name = libwireplumber-module-si-audio-adapter, type = module + provides = si.audio-adapter + } + { + name = libwireplumber-module-si-standard-link, type = module + provides = si.standard-link + } + { + name = libwireplumber-module-default-nodes-api, type = module + provides = api.default-nodes + } + { + name = libwireplumber-module-mixer-api, type = module + provides = api.mixer + } + { + name = metadata.lua, type = script/lua + arguments = { metadata.name = default } + provides = metadata.default + } + { + name = metadata.lua, type = script/lua + arguments = { metadata.name = filters } + provides = metadata.filters + } + { + name = sm-objects.lua, type = script/lua + provides = metadata.sm-objects + } + { + name = session-services.lua, type = script/lua + provides = support.session-services + } + { + name = monitors/alsa.lua, type = script/lua + provides = monitor.alsa + requires = [ support.export-core, pw.client-device ] + } + { + name = client/access-default.lua, type = script/lua + provides = script.client.access-default + } + { + type = virtual, provides = policy.client.access + wants = [ script.client.access-default ] + } + { + name = device/select-profile.lua, type = script/lua + provides = hooks.device.profile.select + } + { + name = device/find-preferred-profile.lua, type = script/lua + provides = hooks.device.profile.find-preferred + } + { + name = device/find-best-profile.lua, type = script/lua + provides = hooks.device.profile.find-best + } + { + name = device/apply-profile.lua, type = script/lua + provides = hooks.device.profile.apply + } + { + type = virtual, provides = policy.device.profile + requires = [ hooks.device.profile.select, + hooks.device.profile.apply ] + wants = [ hooks.device.profile.find-best, hooks.device.profile.find-preferred ] + } + { + name = device/select-routes.lua, type = script/lua + provides = hooks.device.routes.select + } + { + name = device/find-best-routes.lua, type = script/lua + provides = hooks.device.routes.find-best + } + { + name = device/apply-routes.lua, type = script/lua + provides = hooks.device.routes.apply + } + { + type = virtual, provides = policy.device.routes + requires = [ hooks.device.routes.select, + hooks.device.routes.apply ] + wants = [ hooks.device.routes.find-best ] + } + { + name = default-nodes/rescan.lua, type = script/lua + provides = hooks.default-nodes.rescan + } + { + name = default-nodes/find-selected-default-node.lua, type = script/lua + provides = hooks.default-nodes.find-selected + requires = [ metadata.default ] + } + { + name = default-nodes/find-best-default-node.lua, type = script/lua + provides = hooks.default-nodes.find-best + } + { + name = default-nodes/apply-default-node.lua, type = script/lua, + provides = hooks.default-nodes.apply + requires = [ metadata.default ] + } + { + type = virtual, provides = policy.default-nodes + requires = [ hooks.default-nodes.rescan, + hooks.default-nodes.apply ] + wants = [ hooks.default-nodes.find-selected, + hooks.default-nodes.find-best ] + } + { + name = node/create-item.lua, type = script/lua + provides = hooks.node.create-session-item + requires = [ si.audio-adapter, si.node ] + } + { + name = node/suspend-node.lua, type = script/lua + provides = hooks.node.suspend + } + { + name = node/filter-forward-format.lua, type = script/lua + provides = hooks.filter.forward-format + } + { + type = virtual, provides = policy.node + requires = [ hooks.node.create-session-item ] + wants = [ hooks.node.suspend + hooks.filter.forward-format ] + } + { + name = node/audio-group.lua, type = script/lua + provides = node.audio-group + } + + ## Linking hooks + { + name = linking/rescan.lua, type = script/lua + provides = hooks.linking.rescan + } + { + name = linking/find-media-role-target.lua, type = script/lua + provides = hooks.linking.target.find-media-role + } + { + name = linking/find-defined-target.lua, type = script/lua + provides = hooks.linking.target.find-defined + } + { + name = linking/find-audio-group-target.lua, type = script/lua + provides = hooks.linking.target.find-audio-group + requires = [ node.audio-group ] + } + { + name = linking/find-filter-target.lua, type = script/lua + provides = hooks.linking.target.find-filter + requires = [ metadata.filters ] + } + { + name = linking/find-default-target.lua, type = script/lua + provides = hooks.linking.target.find-default + requires = [ api.default-nodes ] + } + { + name = linking/find-best-target.lua, type = script/lua + provides = hooks.linking.target.find-best + requires = [ metadata.filters ] + } + { + name = linking/get-filter-from-target.lua, type = script/lua + provides = hooks.linking.target.get-filter-from + requires = [ metadata.filters ] + } + { + name = linking/prepare-link.lua, type = script/lua + provides = hooks.linking.target.prepare-link + requires = [ api.default-nodes ] + } + { + name = linking/link-target.lua, type = script/lua + provides = hooks.linking.target.link + requires = [ si.standard-link ] + } + { + type = virtual, provides = policy.linking.standard + requires = [ hooks.linking.rescan, + hooks.linking.target.prepare-link, + hooks.linking.target.link ] + wants = [ hooks.linking.target.find-media-role, + hooks.linking.target.find-defined, + hooks.linking.target.find-audio-group, + hooks.linking.target.find-filter, + hooks.linking.target.find-default, + hooks.linking.target.find-best, + hooks.linking.target.get-filter-from ] + } + { + name = linking/rescan-media-role-links.lua, type = script/lua + provides = hooks.linking.role-based.rescan + requires = [ api.mixer ] + } + { + type = virtual, provides = policy.linking.role-based + requires = [ policy.linking.standard, + hooks.linking.role-based.rescan ] + } + { + type = virtual, provides = policy.standard + requires = [ policy.client.access + policy.device.profile + policy.device.routes + policy.default-nodes + policy.linking.standard + policy.linking.role-based + policy.node + support.standard-event-source ] + } + { + type = virtual, provides = hardware.audio + wants = [ monitor.alsa ] + } +] + +wireplumber.components.rules = [ + ## Rules to apply on top of wireplumber.components + { + matches = [ + { + type = "script/lua" + } + ] + actions = { + merge = { + requires = [ support.lua-scripting ] + } + } + } + { + matches = [ + { + provides = "~hooks.*" + name = "!~monitors/.*" + } + ] + actions = { + merge = { + before = [ support.standard-event-source ] + } + } + } + { + matches = [ + { provides = "~monitor.*" } + ] + actions = { + merge = { + after = [ support.standard-event-source ] + } + } + } + # session-services.lua must execute at the very end + { + matches = [ + { name = "!session-services.lua" } + ] + actions = { + merge = { + before = [ support.session-services ] + } + } + } +] + +wireplumber.settings.schema = { + ## Bluetooth + bluetooth.use-persistent-storage = { + description = "Whether to use persistent BT storage or not" + type = "bool" + default = true + } + bluetooth.autoswitch-to-headset-profile = { + description = "Whether to autoswitch to BT headset profile or not" + type = "bool" + default = true + } + + ## Device + device.restore-profile = { + description = "Whether to restore device profile or not" + type = "bool" + default = true + } + device.restore-routes = { + description = "Whether to restore device routes or not" + type = "bool" + default = true + } + device.routes.default-sink-volume = { + description = "The default volume for sink devices" + type = "float" + default = 0.064 + min = 0.0 + max = 1.0 + } + device.routes.default-source-volume = { + description = "The default volume for source devices" + type = "float" + default = 1.0 + min = 0.0 + max = 1.0 + } + + ## Linking + linking.role-based.duck-level = { + description = "The volume level to apply when ducking (= reducing volume for a higher priority stream to be audible) in the role-based linking policy" + type = "float" + default = 0.3 + min = 0.0 + max = 1.0 + } + linking.allow-moving-streams = { + description = "Whether to allow metadata to move streams at runtime or not" + type = "bool" + default = true + } + linking.follow-default-target = { + description = "Whether to allow streams follow the default device or not" + type = "bool" + default = true + } + + ## Monitor + monitor.camera-discovery-timeout = { + description = "The camera discovery timeout in milliseconds" + type = "int" + default = 1000 + min = 0 + max = 60000 + } + + ## Node + node.features.audio.no-dsp = { + description = "Whether to never convert audio to F32 format or not" + type = "bool" + default = false + } + node.features.audio.monitor-ports = { + description = "Whether to enable monitor ports on audio nodes or not" + type = "bool" + default = true + } + node.features.audio.control-port = { + description = "Whether to enable control ports on audio nodes or not" + type = "bool" + default = false + } + node.stream.restore-props = { + description = "Whether to restore properties on stream nodes or not" + type = "bool" + default = true + } + node.stream.restore-target = { + description = "Whether to restore target on stream nodes or not" + type = "bool" + default = true + } + node.stream.default-playback-volume = { + description = "The default volume for playback nodes" + type = "float" + default = 1.0 + min = 0.0 + max = 1.0 + } + node.stream.default-capture-volume = { + description = "The default volume for capture nodes" + type = "float" + default = 1.0 + min = 0.0 + max = 1.0 + } + node.stream.default-media-role = { + description = "A media.role to assign on streams that have none specified" + type = "string" + default = null + } + node.filter.forward-format = { + description = "Whether to forward format on filter nodes or not" + type = "bool" + default = false + } + node.restore-default-targets = { + description = "Whether to restore default targets or not" + type = "bool" + default = true + } +} -- Sincerely, Demi Marie Obenour (she/her/hers)