[ALL] Fixes (non-boot rpi) and tweaks (pipewire/wireplumber)

This commit is contained in:
j1nx 2023-12-27 16:11:15 +00:00
parent 6e49362369
commit adf40880dd
74 changed files with 8481 additions and 15 deletions

@ -1 +1 @@
Subproject commit d16f62919384610b1fb2e6641ed915c9818bef2f
Subproject commit 3419164023f00c1e704d7002f0a8c59c4d164b72

View File

@ -59,7 +59,7 @@ done
# Prepare home data
rm -f ${BINARIES_DIR}/homefs.ext4
truncate --size="6G" ${BINARIES_DIR}/homefs.ext4
truncate --size="6890M" ${BINARIES_DIR}/homefs.ext4
mkfs.ext4 -L "homefs" -E lazy_itable_init=0,lazy_journal_init=0 ${BINARIES_DIR}/homefs.ext4
# Mount home image

View File

@ -28,7 +28,7 @@ BR2_ROOTFS_POST_IMAGE_SCRIPT="$(BR2_EXTERNAL)/board/ovos/ova/post-image.sh"
BR2_ROOTFS_POST_SCRIPT_ARGS="--ova"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.1.68"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.1.69"
BR2_LINUX_KERNEL_DEFCONFIG="x86_64"
BR2_LINUX_KERNEL_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL)/kernel/ovos.config $(BR2_EXTERNAL)/kernel/device-drivers.config $(BR2_EXTERNAL)/kernel/docker.config $(BR2_EXTERNAL)/board/ovos/ova/kernel.config"
BR2_LINUX_KERNEL_LZ4=y

View File

@ -29,7 +29,7 @@ BR2_ROOTFS_POST_IMAGE_SCRIPT="$(BR2_EXTERNAL)/board/ovos/raspberrypi/rpi3/post-i
BR2_ROOTFS_POST_SCRIPT_ARGS="--rpi3"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_TARBALL=y
BR2_LINUX_KERNEL_CUSTOM_TARBALL_LOCATION="$(call github,raspberrypi,linux,e41b05a8fc73f95425aceaf15a68fc25da1d1fe5)/linux-e41b05a8fc73f95425aceaf15a68fc25da1d1fe5.tar.gz"
BR2_LINUX_KERNEL_CUSTOM_TARBALL_LOCATION="$(call github,raspberrypi,linux,342c7ee49e862edc30c893f141f55b9211b7a43b)/linux-342c7ee49e862edc30c893f141f55b9211b7a43b.tar.gz"
BR2_LINUX_KERNEL_DEFCONFIG="bcmrpi3"
BR2_LINUX_KERNEL_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL)/kernel/ovos.config $(BR2_EXTERNAL)/kernel/device-drivers.config $(BR2_EXTERNAL)/kernel/docker.config $(BR2_EXTERNAL)/board/ovos/raspberrypi/kernel.config"
BR2_LINUX_KERNEL_LZ4=y
@ -163,7 +163,6 @@ BR2_PACKAGE_LOCKDEV=y
BR2_PACKAGE_PHYSFS=y
BR2_PACKAGE_FONTCONFIG=y
BR2_PACKAGE_HARFBUZZ=y
BR2_PACKAGE_LIBPNG=y
BR2_PACKAGE_OPENJPEG=y
BR2_PACKAGE_PIXMAN=y
BR2_PACKAGE_WEBP=y
@ -173,11 +172,12 @@ BR2_PACKAGE_LIBGUDEV=y
BR2_PACKAGE_LIBINPUT=y
BR2_PACKAGE_LIBV4L_UTILS=y
BR2_PACKAGE_JSON_GLIB=y
BR2_PACKAGE_LIBCAMERA=y
BR2_PACKAGE_LIBCAMERA_V4L2=y
BR2_PACKAGE_LIBCAMERA_PIPELINE_RASPBERRYPI=y
BR2_PACKAGE_LIBCAMERA_PIPELINE_SIMPLE=y
BR2_PACKAGE_LIBCAMERA_PIPELINE_UVCVIDEO=y
BR2_PACKAGE_LIBCAMERA_PIPELINE_VIMC=y
BR2_PACKAGE_LIBCAMERA_APPS=y
BR2_PACKAGE_LIBCURL=y
BR2_PACKAGE_LIBCURL_CURL=y
BR2_PACKAGE_LIBNDP=y

View File

@ -30,7 +30,7 @@ BR2_ROOTFS_POST_IMAGE_SCRIPT="$(BR2_EXTERNAL)/board/ovos/raspberrypi/rpi4/post-i
BR2_ROOTFS_POST_SCRIPT_ARGS="--rpi4"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_TARBALL=y
BR2_LINUX_KERNEL_CUSTOM_TARBALL_LOCATION="$(call github,raspberrypi,linux,e41b05a8fc73f95425aceaf15a68fc25da1d1fe5)/linux-e41b05a8fc73f95425aceaf15a68fc25da1d1fe5.tar.gz"
BR2_LINUX_KERNEL_CUSTOM_TARBALL_LOCATION="$(call github,raspberrypi,linux,342c7ee49e862edc30c893f141f55b9211b7a43b)/linux-342c7ee49e862edc30c893f141f55b9211b7a43b.tar.gz"
BR2_LINUX_KERNEL_DEFCONFIG="bcm2711"
BR2_LINUX_KERNEL_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL)/kernel/ovos.config $(BR2_EXTERNAL)/kernel/device-drivers.config $(BR2_EXTERNAL)/kernel/docker.config $(BR2_EXTERNAL)/board/ovos/raspberrypi/kernel.config"
BR2_LINUX_KERNEL_LZ4=y
@ -162,7 +162,6 @@ BR2_PACKAGE_LOCKDEV=y
BR2_PACKAGE_PHYSFS=y
BR2_PACKAGE_FONTCONFIG=y
BR2_PACKAGE_HARFBUZZ=y
BR2_PACKAGE_LIBPNG=y
BR2_PACKAGE_OPENJPEG=y
BR2_PACKAGE_PIXMAN=y
BR2_PACKAGE_WEBP=y
@ -172,11 +171,12 @@ BR2_PACKAGE_LIBGUDEV=y
BR2_PACKAGE_LIBINPUT=y
BR2_PACKAGE_LIBV4L_UTILS=y
BR2_PACKAGE_JSON_GLIB=y
BR2_PACKAGE_LIBCAMERA=y
BR2_PACKAGE_LIBCAMERA_V4L2=y
BR2_PACKAGE_LIBCAMERA_PIPELINE_RASPBERRYPI=y
BR2_PACKAGE_LIBCAMERA_PIPELINE_SIMPLE=y
BR2_PACKAGE_LIBCAMERA_PIPELINE_UVCVIDEO=y
BR2_PACKAGE_LIBCAMERA_PIPELINE_VIMC=y
BR2_PACKAGE_LIBCAMERA_APPS=y
BR2_PACKAGE_LIBCURL=y
BR2_PACKAGE_LIBCURL_CURL=y
BR2_PACKAGE_LIBNDP=y

View File

@ -21,8 +21,6 @@ OVOS_CONTAINERS_IMAGES = ovos-messagebus \
ifeq ($(BR2_PACKAGE_OVOS_CONTAINERS_GUI),y)
OVOS_CONTAINERS_IMAGES += ovos-gui-websocket \
ovos-gui-shell
OVOS_CONTAINERS_INSTALL_GUI = YES
endif
define OVOS_CONTAINERS_BUILD_CMDS
@ -41,13 +39,13 @@ define OVOS_CONTAINERS_INSTALL_IMAGES_CMDS
rm -rf $(TARGET_DIR)/home/ovos/.local/share/containers/storage/storage.lock
rm -rf $(TARGET_DIR)/home/ovos/.local/share/containers/storage/userns.lock
rm -rf $(TARGET_DIR)/home/ovos/.local/share/containers/storage/libpod
endef
define OVOS_CONTAINERS_INSTALL_GUI
$(INSTALL) -D -m 644 $(BR2_EXTERNAL_OPENVOICEOS_PATH)/package/ovos-containers/ovos_gui_websocket.container \
$(INSTALL) -D -m 644 $(BR2_EXTERNAL_OPENVOICEOS_PATH)/package/ovos-containers/ovos_gui_websocket.container \
$(TARGET_DIR)/home/ovos/.config/containers/systemd/ovos_gui_websocket.container
$(INSTALL) -D -m 644 $(BR2_EXTERNAL_OPENVOICEOS_PATH)/package/ovos-containers/ovos_gui.container \
$(INSTALL) -D -m 644 $(BR2_EXTERNAL_OPENVOICEOS_PATH)/package/ovos-containers/ovos_gui.container \
$(TARGET_DIR)/home/ovos/.config/containers/systemd/ovos_gui.container
endef
$(eval $(generic-package))

View File

@ -0,0 +1,135 @@
# Real-time Client config file for PipeWire version "0.3.81" #
#
# Copy and edit this file in /etc/pipewire for system-wide changes
# or in ~/.config/pipewire for local changes.
#
# It is also possible to place a file with an updated section in
# /etc/pipewire/client-rt.conf.d/ for system-wide changes or in
# ~/.config/pipewire/client-rt.conf.d/ for local changes.
#
context.properties = {
## Configure properties in the system.
#mem.warn-mlock = false
#mem.allow-mlock = true
#mem.mlock-all = false
log.level = 0
#default.clock.quantum-limit = 8192
}
context.spa-libs = {
#<factory-name regex> = <library-name>
#
# Used to find spa factory names. It maps an spa factory name
# regular expression to a library name that should contain
# that factory.
#
audio.convert.* = audioconvert/libspa-audioconvert
support.* = support/libspa-support
}
context.modules = [
#{ name = <module-name>
# ( args = { <key> = <value> ... } )
# ( flags = [ ( ifexists ) ( nofail ) ] )
# ( condition = [ { <key> = <value> ... } ... ] )
#}
#
# Loads a module with the given parameters.
# If ifexists is given, the module is ignored when it is not found.
# If nofail is given, module initialization failures are ignored.
#
# Uses realtime scheduling to boost the audio thread priorities
{ name = libpipewire-module-rt
args = {
#rt.prio = 88
#rt.time.soft = -1
#rt.time.hard = -1
}
flags = [ ifexists nofail ]
}
# The native communication protocol.
{ name = libpipewire-module-protocol-native }
# Allows creating nodes that run in the context of the
# client. Is used by all clients that want to provide
# data to PipeWire.
{ name = libpipewire-module-client-node }
# Allows creating devices that run in the context of the
# client. Is used by the session manager.
{ name = libpipewire-module-client-device }
# Makes a factory for wrapping nodes in an adapter with a
# converter and resampler.
{ name = libpipewire-module-adapter }
# Allows applications to create metadata objects. It creates
# a factory for Metadata objects.
{ name = libpipewire-module-metadata }
# Provides factories to make session manager objects.
{ name = libpipewire-module-session-manager }
]
filter.properties = {
#node.latency = 1024/48000
}
stream.properties = {
#node.latency = 1024/48000
#node.autoconnect = true
#resample.quality = 4
#channelmix.normalize = false
#channelmix.mix-lfe = true
#channelmix.upmix = true
#channelmix.upmix-method = psd # none, simple
#channelmix.lfe-cutoff = 150
#channelmix.fc-cutoff = 12000
#channelmix.rear-delay = 12.0
#channelmix.stereo-widen = 0.0
#channelmix.hilbert-taps = 0
#dither.noise = 0
}
stream.rules = [
{ matches = [
{
# all keys must match the value. ! negates. ~ starts regex.
#application.name = "pw-cat"
#node.name = "~Google Chrome$"
}
]
actions = {
update-props = {
#node.latency = 512/48000
}
}
}
]
alsa.properties = {
# ALSA params take a single value, an array [] of values
# or a range { min=.. max=... }
#alsa.access = [ MMAP_INTERLEAVED MMAP_NONINTERLEAVED RW_INTERLEAVED RW_NONINTERLEAVED ]
#alsa.format = [ FLOAT S32 S24 S24_3 S16 U8 ]
#alsa.rate = { min=1 max=384000 } # or [ 44100 48000 .. ]
#alsa.channels = { min=1 max=64 } # or [ 2 4 6 .. ]
#alsa.period-bytes = { min=128 max=2097152 } # or [ 128 256 1024 .. ]
#alsa.buffer-bytes = { min=256 max=4194304 } # or [ 256 512 4096 .. ]
#alsa.volume-method = cubic # linear, cubic
}
# client specific properties
alsa.rules = [
{ matches = [ { application.process.binary = "resolve" } ]
actions = {
update-props = {
alsa.buffer-bytes = 131072
}
}
}
]

View File

@ -0,0 +1,63 @@
# Filter-chain config file for PipeWire version "0.3.81" #
#
# This is a base config file for running filters.
#
# Place filter fragments in /etc/pipewire/filter-chain.conf.d/
# for system-wide changes or in ~/.config/pipewire/filter-chain.conf.d/
# for local changes.
#
# Run the filters with pipewire -c filter-chain.conf
#
context.properties = {
## Configure properties in the system.
#mem.warn-mlock = false
#mem.allow-mlock = true
#mem.mlock-all = false
log.level = 0
}
context.spa-libs = {
#<factory-name regex> = <library-name>
#
# Used to find spa factory names. It maps an spa factory name
# regular expression to a library name that should contain
# that factory.
#
audio.convert.* = audioconvert/libspa-audioconvert
support.* = support/libspa-support
}
context.modules = [
#{ name = <module-name>
# ( args = { <key> = <value> ... } )
# ( flags = [ ( ifexists ) ( nofail ) ] )
# ( condition = [ { <key> = <value> ... } ... ] )
#}
#
# Loads a module with the given parameters.
# If ifexists is given, the module is ignored when it is not found.
# If nofail is given, module initialization failures are ignored.
#
# Uses realtime scheduling to boost the audio thread priorities
{ name = libpipewire-module-rt
args = {
#rt.prio = 88
#rt.time.soft = -1
#rt.time.hard = -1
}
flags = [ ifexists nofail ]
}
# The native communication protocol.
{ name = libpipewire-module-protocol-native }
# Allows creating nodes that run in the context of the
# client. Is used by all clients that want to provide
# data to PipeWire.
{ name = libpipewire-module-client-node }
# Makes a factory for wrapping nodes in an adapter with a
# converter and resampler.
{ name = libpipewire-module-adapter }
]

View File

@ -0,0 +1,64 @@
# filter-chain example config file for PipeWire version "0.3.81" #
#
# Copy this file into a conf.d/ directory such as
# ~/.config/pipewire/filter-chain.conf.d/
#
context.modules = [
{ name = libpipewire-module-filter-chain
flags = [ nofail ]
args = {
#audio.format = F32
#audio.rate = 48000
audio.channels = 2
audio.position = [ FL FR ]
node.description = "Demonic example"
media.name = "Demonic example"
filter.graph = {
nodes = [
{
name = rev
type = ladspa
plugin = revdelay_1605
label = revdelay
control = {
"Delay Time (s)" = 2.0
}
}
{
name = pitch
type = ladspa
plugin = am_pitchshift_1433
label = amPitchshift
control = {
"Pitch shift" = 0.6
}
}
{
name = rev2
type = ladspa
plugin = g2reverb
label = G2reverb
control = {
"Reverb tail" = 0.5
"Damping" = 0.9
}
}
]
links = [
{ output = "rev:Output" input = "pitch:Input" }
{ output = "pitch:Output" input = "rev2:In L" }
]
inputs = [ "rev:Input" ]
outputs = [ "rev2:Out L" ]
}
capture.props = {
node.name = "effect_input.filter-chain-demonic"
#media.class = Audio/Sink
}
playback.props = {
node.name = "effect_output.filter-chain-demonic"
#media.class = Audio/Source
}
}
}
]

View File

@ -0,0 +1,47 @@
# Dolby Surround encoder sink
#
# Copy this file into a conf.d/ directory such as
# ~/.config/pipewire/filter-chain.conf.d/
#
context.modules = [
{ name = libpipewire-module-filter-chain
flags = [ nofail ]
args = {
node.description = "Dolby Surround Sink"
media.name = "Dolby Surround Sink"
filter.graph = {
nodes = [
{
type = builtin
name = mixer
label = mixer
control = { "Gain 1" = 0.5 "Gain 2" = 0.5 }
}
{
type = ladspa
name = enc
plugin = surround_encoder_1401
label = surroundEncoder
}
]
links = [
{ output = "mixer:Out" input = "enc:S" }
]
inputs = [ "enc:L" "enc:R" "enc:C" null "mixer:In 1" "mixer:In 2" ]
outputs = [ "enc:Lt" "enc:Rt" ]
}
capture.props = {
node.name = "effect_input.dolby_surround"
media.class = Audio/Sink
audio.channels = 6
audio.position = [ FL FR FC LFE SL SR ]
}
playback.props = {
node.name = "effect_output.dolby_surround"
node.passive = true
audio.channels = 2
audio.position = [ FL FR ]
}
}
}
]

View File

@ -0,0 +1,70 @@
# 6 band sink equalizer
#
# Copy this file into a conf.d/ directory such as
# ~/.config/pipewire/filter-chain.conf.d/
#
context.modules = [
{ name = libpipewire-module-filter-chain
args = {
node.description = "Equalizer Sink"
media.name = "Equalizer Sink"
filter.graph = {
nodes = [
{
type = builtin
name = eq_band_1
label = bq_lowshelf
control = { "Freq" = 100.0 "Q" = 1.0 "Gain" = 0.0 }
}
{
type = builtin
name = eq_band_2
label = bq_peaking
control = { "Freq" = 100.0 "Q" = 1.0 "Gain" = 0.0 }
}
{
type = builtin
name = eq_band_3
label = bq_peaking
control = { "Freq" = 500.0 "Q" = 1.0 "Gain" = 0.0 }
}
{
type = builtin
name = eq_band_4
label = bq_peaking
control = { "Freq" = 2000.0 "Q" = 1.0 "Gain" = 0.0 }
}
{
type = builtin
name = eq_band_5
label = bq_peaking
control = { "Freq" = 5000.0 "Q" = 1.0 "Gain" = 0.0 }
}
{
type = builtin
name = eq_band_6
label = bq_highshelf
control = { "Freq" = 5000.0 "Q" = 1.0 "Gain" = 0.0 }
}
]
links = [
{ output = "eq_band_1:Out" input = "eq_band_2:In" }
{ output = "eq_band_2:Out" input = "eq_band_3:In" }
{ output = "eq_band_3:Out" input = "eq_band_4:In" }
{ output = "eq_band_4:Out" input = "eq_band_5:In" }
{ output = "eq_band_5:Out" input = "eq_band_6:In" }
]
}
audio.channels = 2
audio.position = [ FL FR ]
capture.props = {
node.name = "effect_input.eq6"
media.class = Audio/Sink
}
playback.props = {
node.name = "effect_output.eq6"
node.passive = true
}
}
}
]

View File

@ -0,0 +1,56 @@
# An example filter chain that makes a stereo sink that mixes
# the FL and FR channels to FL, FR, LFE
#
# Copy this file into a conf.d/ directory
#
context.modules = [
{ name = libpipewire-module-filter-chain
args = {
node.description = "LFE example"
media.name = "LFE example"
filter.graph = {
nodes = [
{ name = copyIL type = builtin label = copy }
{ name = copyOL type = builtin label = copy }
{ name = copyIR type = builtin label = copy }
{ name = copyOR type = builtin label = copy }
{
name = mix
type = builtin
label = mixer
control = {
"Gain 1" = 0.5
"Gain 2" = 0.5
}
}
{
type = builtin
name = lpLFE
label = bq_lowpass
control = { "Freq" = 150.0 }
}
]
links = [
{ output = "copyIL:Out" input = "copyOL:In" }
{ output = "copyIR:Out" input = "copyOR:In" }
{ output = "copyIL:Out" input = "mix:In 1" }
{ output = "copyIR:Out" input = "mix:In 2" }
{ output = "mix:Out" input = "lpLFE:In" }
]
inputs = [ "copyIL:In" "copyIR:In" ]
outputs = [ "copyOL:Out" "copyOR:Out" "lpLFE:Out"]
}
capture.props = {
node.name = "input_lfe"
audio.position = [ FL FR ]
media.class = "Audio/Sink"
}
playback.props = {
node.name = "output_lfe"
audio.position = [ FL FR LFE ]
stream.dont-remix = true
node.passive = true
}
}
}
]

View File

@ -0,0 +1,42 @@
# Matrix Spatialiser sink
#
# Copy this file into a conf.d/ directory such as
# ~/.config/pipewire/filter-chain.conf.d/
#
# ( Jean-Philippe Guillemin <hyp3ri0n@sfr.fr> )
#
context.modules = [
{ name = libpipewire-module-filter-chain
flags = [ nofail ]
args = {
node.description = "Matrix Spatialiser"
media.name = "Matrix Spatialiser"
filter.graph = {
nodes = [
{
type = ladspa
name = matrix
plugin = matrix_spatialiser_1422
label = matrixSpatialiser
control = {
"Width" = 80
}
}
]
inputs = [ "matrix:Input L" "matrix:Input R" ]
outputs = [ "matrix:Output L" "matrix:Output R" ]
}
audio.channels = 2
audio.position = [ FL FR ]
capture.props = {
node.name = "effect_input.matrix_spatialiser"
media.class = Audio/Sink
}
playback.props = {
node.name = "effect_output.matrix_spatialiser"
node.passive = true
}
}
}
]

View File

@ -0,0 +1,40 @@
# An example filter chain that makes a stereo sink that mixes
# the FL and FR channels to a single FL channel
#
# Copy this file into a conf.d/ directory such as
# ~/.config/pipewire/filter-chain.conf.d/
#
context.modules = [
{ name = libpipewire-module-filter-chain
args = {
node.description = "Mix example"
media.name = "Mix example"
filter.graph = {
nodes = [
{
name = mix
type = builtin
label = mixer
control = {
"Gain 1" = 0.5
"Gain 2" = 0.5
}
}
]
inputs = [ "mix:In 1" "mix:In 2" ]
outputs = [ "mix:Out" ]
}
capture.props = {
node.name = "mix_input.mix-FL-FR-to-FL"
audio.position = [ FL FR ]
media.class = "Audio/Sink"
}
playback.props = {
node.name = "mix_output.mix-FL-FR-to-FL"
audio.position = [ FL ]
stream.dont-remix = true
node.passive = true
}
}
}
]

View File

@ -0,0 +1,180 @@
# Convolver sink
#
# Copy this file into a conf.d/ directory such as
# ~/.config/pipewire/filter-chain.conf.d/
#
# Adjust the paths to the convolver files to match your system
#
context.modules = [
{ name = libpipewire-module-filter-chain
flags = [ nofail ]
args = {
node.description = "Virtual Surround Sink"
media.name = "Virtual Surround Sink"
filter.graph = {
nodes = [
{
type = builtin
label = convolver
name = convFL_L
config = {
filename = "hrir_kemar/hrir-kemar.wav"
channel = 0
}
}
{
type = builtin
label = convolver
name = convFL_R
config = {
filename = "hrir_kemar/hrir-kemar.wav"
channel = 1
}
}
{
type = builtin
label = convolver
name = convFR_L
config = {
filename = "hrir_kemar/hrir-kemar.wav"
channel = 1
}
}
{
type = builtin
label = convolver
name = convFR_R
config = {
filename = "hrir_kemar/hrir-kemar.wav"
channel = 0
}
}
{
type = builtin
label = convolver
name = convFC
config = {
filename = "hrir_kemar/hrir-kemar.wav"
channel = 2
}
}
{
type = builtin
label = convolver
name = convLFE
config = {
filename = "hrir_kemar/hrir-kemar.wav"
channel = 3
}
}
{
type = builtin
label = convolver
name = convSL_L
config = {
filename = "hrir_kemar/hrir-kemar.wav"
channel = 4
}
}
{
type = builtin
label = convolver
name = convSL_R
config = {
filename = "hrir_kemar/hrir-kemar.wav"
channel = 5
}
}
{
type = builtin
label = convolver
name = convSR_L
config = {
filename = "hrir_kemar/hrir-kemar.wav"
channel = 5
}
}
{
type = builtin
label = convolver
name = convSR_R
config = {
filename = "hrir_kemar/hrir-kemar.wav"
channel = 4
}
}
{
type = builtin
label = mixer
name = mixL
}
{
type = builtin
label = mixer
name = mixR
}
{
type = builtin
label = copy
name = copyFL
}
{
type = builtin
label = copy
name = copyFR
}
{
type = builtin
label = copy
name = copySL
}
{
type = builtin
label = copy
name = copySR
}
]
links = [
{ output = "copyFL:Out" input = "convFL_L:In" }
{ output = "copyFL:Out" input = "convFL_R:In" }
{ output = "copyFR:Out" input = "convFR_R:In" }
{ output = "copyFR:Out" input = "convFR_L:In" }
{ output = "copySL:Out" input = "convSL_L:In" }
{ output = "copySL:Out" input = "convSL_R:In" }
{ output = "copySR:Out" input = "convSR_R:In" }
{ output = "copySR:Out" input = "convSR_L:In" }
{ output = "convFL_L:Out" input = "mixL:In 1" }
{ output = "convFR_L:Out" input = "mixL:In 2" }
{ output = "convFC:Out" input = "mixL:In 3" }
{ output = "convLFE:Out" input = "mixL:In 4" }
{ output = "convSL_L:Out" input = "mixL:In 5" }
{ output = "convSR_L:Out" input = "mixL:In 6" }
{ output = "convFL_R:Out" input = "mixR:In 1" }
{ output = "convFR_R:Out" input = "mixR:In 2" }
{ output = "convFC:Out" input = "mixR:In 3" }
{ output = "convLFE:Out" input = "mixR:In 4" }
{ output = "convSL_R:Out" input = "mixR:In 5" }
{ output = "convSR_R:Out" input = "mixR:In 6" }
]
inputs = [ "copyFL:In" "copyFR:In" "convFC:In" "convLFE:In" "copySL:In" "copySR:In" ]
outputs = [ "mixL:Out" "mixR:Out" ]
}
capture.props = {
node.name = "effect_input.virtual-surround-5.1-kemar"
media.class = Audio/Sink
audio.channels = 6
audio.position = [ FL FR FC LFE SL SR]
}
playback.props = {
node.name = "effect_output.virtual-surround-5.1-kemar"
node.passive = true
audio.channels = 2
audio.position = [ FL FR ]
}
}
}
]

View File

@ -0,0 +1,104 @@
# Convolver sink
#
# Copy this file into a conf.d/ directory such as
# ~/.config/pipewire/filter-chain.conf.d/
#
# Adjust the paths to the convolver files to match your system
#
context.modules = [
{ name = libpipewire-module-filter-chain
flags = [ nofail ]
args = {
node.description = "Virtual Surround Sink"
media.name = "Virtual Surround Sink"
filter.graph = {
nodes = [
# duplicate inputs
{ type = builtin label = copy name = copyFL }
{ type = builtin label = copy name = copyFR }
{ type = builtin label = copy name = copyFC }
{ type = builtin label = copy name = copyRL }
{ type = builtin label = copy name = copyRR }
{ type = builtin label = copy name = copySL }
{ type = builtin label = copy name = copySR }
{ type = builtin label = copy name = copyLFE }
# apply hrir - HeSuVi 14-channel WAV (not the *-.wav variants) (note: */44/* in HeSuVi are the same, but resampled to 44100)
{ type = builtin label = convolver name = convFL_L config = { filename = "hrir_hesuvi/hrir.wav" channel = 0 } }
{ type = builtin label = convolver name = convFL_R config = { filename = "hrir_hesuvi/hrir.wav" channel = 1 } }
{ type = builtin label = convolver name = convSL_L config = { filename = "hrir_hesuvi/hrir.wav" channel = 2 } }
{ type = builtin label = convolver name = convSL_R config = { filename = "hrir_hesuvi/hrir.wav" channel = 3 } }
{ type = builtin label = convolver name = convRL_L config = { filename = "hrir_hesuvi/hrir.wav" channel = 4 } }
{ type = builtin label = convolver name = convRL_R config = { filename = "hrir_hesuvi/hrir.wav" channel = 5 } }
{ type = builtin label = convolver name = convFC_L config = { filename = "hrir_hesuvi/hrir.wav" channel = 6 } }
{ type = builtin label = convolver name = convFR_R config = { filename = "hrir_hesuvi/hrir.wav" channel = 7 } }
{ type = builtin label = convolver name = convFR_L config = { filename = "hrir_hesuvi/hrir.wav" channel = 8 } }
{ type = builtin label = convolver name = convSR_R config = { filename = "hrir_hesuvi/hrir.wav" channel = 9 } }
{ type = builtin label = convolver name = convSR_L config = { filename = "hrir_hesuvi/hrir.wav" channel = 10 } }
{ type = builtin label = convolver name = convRR_R config = { filename = "hrir_hesuvi/hrir.wav" channel = 11 } }
{ type = builtin label = convolver name = convRR_L config = { filename = "hrir_hesuvi/hrir.wav" channel = 12 } }
{ type = builtin label = convolver name = convFC_R config = { filename = "hrir_hesuvi/hrir.wav" channel = 13 } }
# treat LFE as FC
{ type = builtin label = convolver name = convLFE_L config = { filename = "hrir_hesuvi/hrir.wav" channel = 6 } }
{ type = builtin label = convolver name = convLFE_R config = { filename = "hrir_hesuvi/hrir.wav" channel = 13 } }
# stereo output
{ type = builtin label = mixer name = mixL }
{ type = builtin label = mixer name = mixR }
]
links = [
# input
{ output = "copyFL:Out" input="convFL_L:In" }
{ output = "copyFL:Out" input="convFL_R:In" }
{ output = "copySL:Out" input="convSL_L:In" }
{ output = "copySL:Out" input="convSL_R:In" }
{ output = "copyRL:Out" input="convRL_L:In" }
{ output = "copyRL:Out" input="convRL_R:In" }
{ output = "copyFC:Out" input="convFC_L:In" }
{ output = "copyFR:Out" input="convFR_R:In" }
{ output = "copyFR:Out" input="convFR_L:In" }
{ output = "copySR:Out" input="convSR_R:In" }
{ output = "copySR:Out" input="convSR_L:In" }
{ output = "copyRR:Out" input="convRR_R:In" }
{ output = "copyRR:Out" input="convRR_L:In" }
{ output = "copyFC:Out" input="convFC_R:In" }
{ output = "copyLFE:Out" input="convLFE_L:In" }
{ output = "copyLFE:Out" input="convLFE_R:In" }
# output
{ output = "convFL_L:Out" input="mixL:In 1" }
{ output = "convFL_R:Out" input="mixR:In 1" }
{ output = "convSL_L:Out" input="mixL:In 2" }
{ output = "convSL_R:Out" input="mixR:In 2" }
{ output = "convRL_L:Out" input="mixL:In 3" }
{ output = "convRL_R:Out" input="mixR:In 3" }
{ output = "convFC_L:Out" input="mixL:In 4" }
{ output = "convFC_R:Out" input="mixR:In 4" }
{ output = "convFR_R:Out" input="mixR:In 5" }
{ output = "convFR_L:Out" input="mixL:In 5" }
{ output = "convSR_R:Out" input="mixR:In 6" }
{ output = "convSR_L:Out" input="mixL:In 6" }
{ output = "convRR_R:Out" input="mixR:In 7" }
{ output = "convRR_L:Out" input="mixL:In 7" }
{ output = "convLFE_R:Out" input="mixR:In 8" }
{ output = "convLFE_L:Out" input="mixL:In 8" }
]
inputs = [ "copyFL:In" "copyFR:In" "copyFC:In" "copyLFE:In" "copyRL:In" "copyRR:In", "copySL:In", "copySR:In" ]
outputs = [ "mixL:Out" "mixR:Out" ]
}
capture.props = {
node.name = "effect_input.virtual-surround-7.1-hesuvi"
media.class = Audio/Sink
audio.channels = 8
audio.position = [ FL FR FC LFE RL RR SL SR ]
}
playback.props = {
node.name = "effect_output.virtual-surround-7.1-hesuvi"
node.passive = true
audio.channels = 2
audio.position = [ FL FR ]
}
}
}
]

View File

@ -0,0 +1,52 @@
# An example filter chain that makes a source that duplicates the FL channel
# to FL and FR.
#
# Copy this file into a conf.d/ directory such as
# ~/.config/pipewire/filter-chain.conf.d/
#
context.modules = [
{ name = libpipewire-module-filter-chain
args = {
node.description = "Remap example"
media.name = "Remap example"
filter.graph = {
nodes = [
{
name = copyIL
type = builtin
label = copy
}
{
name = copyOL
type = builtin
label = copy
}
{
name = copyOR
type = builtin
label = copy
}
]
links = [
# we can only tee from nodes, not inputs so we need
# to copy the inputs and then tee.
{ output = "copyIL:Out" input = "copyOL:In" }
{ output = "copyIL:Out" input = "copyOR:In" }
]
inputs = [ "copyIL:In" ]
outputs = [ "copyOL:Out" "copyOR:Out" ]
}
capture.props = {
node.name = "remap_input.remap-FL-to-FL-FR"
audio.position = [ FL ]
stream.dont-remix = true
node.passive = true
}
playback.props = {
node.name = "remap_output.remap-FL-to-FL-FR"
audio.position = [ FL FR ]
media.class = "Audio/Source"
}
}
}
]

View File

@ -0,0 +1,44 @@
# Noise canceling source
#
# Copy this file into a conf.d/ directory such as
# ~/.config/pipewire/filter-chain.conf.d/
#
# Adjust the paths to the rnnoise plugin to match your system
#
context.modules = [
{ name = libpipewire-module-filter-chain
flags = [ nofail ]
args = {
node.description = "Noise Canceling source"
media.name = "Noise Canceling source"
filter.graph = {
nodes = [
{
type = ladspa
name = rnnoise
# The path to the plugin. The suffix .so is appended to
# this string and then the file is then located in the directories
# listed in the environment variable LADSPA_PATH or
# /usr/lib64/ladspa, /usr/lib/ladspa or the system library directory
# as a fallback.
# You might want to use an absolute path here to avoid problems.
plugin = "librnnoise_ladspa"
label = noise_suppressor_stereo
control = {
"VAD Threshold (%)" 50.0
}
}
]
}
audio.position = [ FL FR ]
capture.props = {
node.name = "effect_input.rnnoise"
node.passive = true
}
playback.props = {
node.name = "effect_output.rnnoise"
media.class = Audio/Source
}
}
}
]

View File

@ -0,0 +1,133 @@
# JACK client config file for PipeWire version "0.3.81" #
#
# Copy and edit this file in /etc/pipewire for system-wide changes
# or in ~/.config/pipewire for local changes.
#
# It is also possible to place a file with an updated section in
# /etc/pipewire/jack.conf.d/ for system-wide changes or in
# ~/.config/pipewire/jack.conf.d/ for local changes.
#
context.properties = {
## Configure properties in the system.
#mem.warn-mlock = false
#mem.allow-mlock = true
#mem.mlock-all = false
log.level = 0
#default.clock.quantum-limit = 8192
}
context.spa-libs = {
#<factory-name regex> = <library-name>
#
# Used to find spa factory names. It maps an spa factory name
# regular expression to a library name that should contain
# that factory.
#
support.* = support/libspa-support
}
context.modules = [
#{ name = <module-name>
# ( args = { <key> = <value> ... } )
# ( flags = [ ( ifexists ) ( nofail ) ] )
# ( condition = [ { <key> = <value> ... } ... ] )
#}
#
# Loads a module with the given parameters.
# If ifexists is given, the module is ignored when it is not found.
# If nofail is given, module initialization failures are ignored.
#
#
# Boost the data thread priority.
{ name = libpipewire-module-rt
args = {
#rt.prio = 88
#rt.time.soft = -1
#rt.time.hard = -1
}
flags = [ ifexists nofail ]
}
# The native communication protocol.
{ name = libpipewire-module-protocol-native }
# Allows creating nodes that run in the context of the
# client. Is used by all clients that want to provide
# data to PipeWire.
{ name = libpipewire-module-client-node }
# Allows applications to create metadata objects. It creates
# a factory for Metadata objects.
{ name = libpipewire-module-metadata }
]
# global properties for all jack clients
jack.properties = {
#node.latency = 1024/48000
#node.rate = 1/48000
#node.quantum = 1024/48000
#node.lock-quantum = true
#node.force-quantum = 0
#jack.show-monitor = true
#jack.merge-monitor = true
#jack.show-midi = true
#jack.short-name = false
#jack.filter-name = false
#jack.filter-char = " "
#
# allow: Don't restrict self connect requests
# fail-external: Fail self connect requests to external ports only
# ignore-external: Ignore self connect requests to external ports only
# fail-all: Fail all self connect requests
# ignore-all: Ignore all self connect requests
#jack.self-connect-mode = allow
#jack.locked-process = true
#jack.default-as-system = false
#jack.fix-midi-events = true
#jack.global-buffer-size = false
#jack.max-client-ports = 768
#jack.fill-aliases = false
}
# client specific properties
jack.rules = [
{ matches = [
{
# all keys must match the value. ! negates. ~ starts regex.
#client.name = "Carla"
#application.process.binary = "jack_simple_client"
#application.name = "~jack_simple_client.*"
}
]
actions = {
update-props = {
#node.latency = 512/48000
}
}
}
{ matches = [ { application.process.binary = "jack_bufsize" } ]
actions = {
update-props = {
jack.global-buffer-size = true # quantum set globally using metadata
}
}
}
{ matches = [ { application.process.binary = "qsynth" } ]
actions = {
update-props = {
node.always-process = false # makes qsynth idle
node.pause-on-idle = false # makes audio fade out when idle
node.passive = out # makes the sink and qsynth suspend
}
}
}
{ matches = [ { client.name = "Mixxx" } ]
actions = {
update-props = {
jack.merge-monitor = false
}
}
}
]

View File

@ -0,0 +1,363 @@
# Simple daemon config file for PipeWire version "0.3.81" #
#
# Copy and edit this file in /etc/pipewire for system-wide changes
# or in ~/.config/pipewire for local changes.
#
# It is also possible to place a file with an updated section in
# /etc/pipewire/minimal.conf.d/ for system-wide changes or in
# ~/.config/pipewire/minimal.conf.d/ for local changes.
#
context.properties = {
## Configure properties in the system.
#library.name.system = support/libspa-support
#context.data-loop.library.name.system = support/libspa-support
#support.dbus = true
#link.max-buffers = 64
link.max-buffers = 16 # version < 3 clients can't handle more
#mem.warn-mlock = false
#mem.allow-mlock = true
#mem.mlock-all = false
#clock.power-of-two-quantum = true
#log.level = 2
#cpu.zero.denormals = false
core.daemon = true # listening for socket connections
core.name = pipewire-0 # core name and socket name
## Properties for the DSP configuration.
#default.clock.rate = 48000
#default.clock.allowed-rates = [ 48000 ]
#default.clock.quantum = 1024
#default.clock.min-quantum = 32
#default.clock.max-quantum = 2048
#default.clock.quantum-limit = 8192
#default.video.width = 640
#default.video.height = 480
#default.video.rate.num = 25
#default.video.rate.denom = 1
#
settings.check-quantum = true
settings.check-rate = true
#
# These overrides are only applied when running in a vm.
vm.overrides = {
default.clock.min-quantum = 1024
}
}
context.spa-libs = {
#<factory-name regex> = <library-name>
#
# Used to find spa factory names. It maps an spa factory name
# regular expression to a library name that should contain
# that factory.
#
audio.convert.* = audioconvert/libspa-audioconvert
api.alsa.* = alsa/libspa-alsa
support.* = support/libspa-support
}
context.modules = [
#{ name = <module-name>
# ( args = { <key> = <value> ... } )
# ( flags = [ ( ifexists ) ( nofail ) ] )
# ( condition = [ { <key> = <value> ... } ... ] )
#}
#
# Loads a module with the given parameters.
# If ifexists is given, the module is ignored when it is not found.
# If nofail is given, module initialization failures are ignored.
#
# Uses realtime scheduling to boost the audio thread priorities. This uses
# RTKit if the user doesn't have permission to use regular realtime
# scheduling.
{ name = libpipewire-module-rt
args = {
nice.level = -11
#rt.prio = 88
#rt.time.soft = -1
#rt.time.hard = -1
}
flags = [ ifexists nofail ]
}
# The native communication protocol.
{ name = libpipewire-module-protocol-native }
# The profile module. Allows application to access profiler
# and performance data. It provides an interface that is used
# by pw-top and pw-profiler.
{ name = libpipewire-module-profiler }
# Allows applications to create metadata objects. It creates
# a factory for Metadata objects.
{ name = libpipewire-module-metadata }
# Creates a factory for making nodes that run in the
# context of the PipeWire server.
{ name = libpipewire-module-spa-node-factory }
# Allows creating nodes that run in the context of the
# client. Is used by all clients that want to provide
# data to PipeWire.
{ name = libpipewire-module-client-node }
# The access module can perform access checks and block
# new clients.
{ name = libpipewire-module-access
args = {
# access.allowed to list an array of paths of allowed
# apps.
#access.allowed = [
# /usr/bin/pipewire-media-session
#]
# An array of rejected paths.
#access.rejected = [ ]
# An array of paths with restricted access.
#access.restricted = [ ]
# Anything not in the above lists gets assigned the
# access.force permission.
#access.force = flatpak
}
}
# Makes a factory for wrapping nodes in an adapter with a
# converter and resampler.
{ name = libpipewire-module-adapter }
# Makes a factory for creating links between ports.
{ name = libpipewire-module-link-factory }
]
context.objects = [
#{ factory = <factory-name>
# ( args = { <key> = <value> ... } )
# ( flags = [ ( nofail ) ] )
# ( condition = [ { <key> = <value> ... } ... ] )
#}
#
# Creates an object from a PipeWire factory with the given parameters.
# If nofail is given, errors are ignored (and no object is created).
#
#{ factory = spa-node-factory args = { factory.name = videotestsrc node.name = videotestsrc node.description = videotestsrc Spa:Pod:Object:Param:Props:patternType = 1 } }
#{ factory = spa-device-factory args = { factory.name = api.jack.device foo=bar } flags = [ nofail ] }
#{ factory = spa-device-factory args = { factory.name = api.alsa.enum.udev } }
#{ factory = spa-node-factory args = { factory.name = api.alsa.seq.bridge node.name = Internal-MIDI-Bridge } }
#{ factory = adapter args = { factory.name = audiotestsrc node.name = my-test node.description = audiotestsrc } }
#{ factory = spa-node-factory args = { factory.name = api.vulkan.compute.source node.name = my-compute-source } }
# Make a default metadata store
{ factory = metadata
args = {
metadata.name = default
# metadata.values = [
# { key = default.audio.sink value = { name = somesink } }
# { key = default.audio.source value = { name = somesource } }
# ]
}
}
# A default dummy driver. This handles nodes marked with the "node.always-driver"
# property when no other driver is currently active. JACK clients need this.
{ factory = spa-node-factory
args = {
factory.name = support.node.driver
node.name = Dummy-Driver
node.group = pipewire.dummy
priority.driver = 20000
}
}
{ factory = spa-node-factory
args = {
factory.name = support.node.driver
node.name = Freewheel-Driver
priority.driver = 19000
node.group = pipewire.freewheel
node.freewheel = true
}
}
# This creates a single PCM source device for the given
# alsa device path hw:0. You can change source to sink
# to make a sink in the same way.
{ factory = adapter
args = {
factory.name = api.alsa.pcm.source
node.name = "system"
node.description = "system"
media.class = "Audio/Source"
api.alsa.path = "hw:0"
#api.alsa.period-size = 0
#api.alsa.period-num = 0
#api.alsa.headroom = 0
#api.alsa.start-delay = 0
#api.alsa.disable-mmap = false
#api.alsa.disable-batch = false
#api.alsa.use-chmap = false
#api.alsa.multirate = true
#latency.internal.rate = 0
#latency.internal.ns = 0
#clock.name = api.alsa.0
node.suspend-on-idle = true
#audio.format = "S32"
#audio.rate = 48000
#audio.allowed-rates = [ ]
#audio.channels = 4
#audio.position = [ FL FR RL RR ]
#resample.quality = 4
resample.disable = true
#monitor.channel-volumes = false
#channelmix.normalize = false
#channelmix.mix-lfe = true
#channelmix.upmix = true
#channelmix.upmix-method = psd # none, simple
#channelmix.lfe-cutoff = 150
#channelmix.fc-cutoff = 12000
#channelmix.rear-delay = 12.0
#channelmix.stereo-widen = 0.0
#channelmix.hilbert-taps = 0
channelmix.disable = true
#dither.noise = 0
#node.param.Props = {
# params = [
# audio.channels 6
# ]
#}
adapter.auto-port-config = {
mode = dsp
monitor = false
control = false
position = unknown # aux, preserve
}
#node.param.PortConfig = {
# direction = Output
# mode = dsp
# format = {
# mediaType = audio
# mediaSubtype = raw
# format = F32
# rate = 48000
# channels = 4
# position = [ FL FR RL RR ]
# }
#}
}
}
{ factory = adapter
args = {
factory.name = api.alsa.pcm.sink
node.name = "system"
node.description = "system"
media.class = "Audio/Sink"
api.alsa.path = "hw:0"
#api.alsa.period-size = 0
#api.alsa.period-num = 0
#api.alsa.headroom = 0
#api.alsa.start-delay = 0
#api.alsa.disable-mmap = false
#api.alsa.disable-batch = false
#api.alsa.use-chmap = false
#api.alsa.multirate = true
#latency.internal.rate = 0
#latency.internal.ns = 0
#clock.name = api.alsa.0
node.suspend-on-idle = true
#audio.format = "S32"
#audio.rate = 48000
#audio.allowed-rates = [ ]
#audio.channels = 2
#audio.position = "FL,FR"
#resample.quality = 4
resample.disable = true
#channelmix.normalize = false
#channelmix.mix-lfe = true
#channelmix.upmix = true
#channelmix.upmix-method = psd # none, simple
#channelmix.lfe-cutoff = 150
#channelmix.fc-cutoff = 12000
#channelmix.rear-delay = 12.0
#channelmix.stereo-widen = 0.0
#channelmix.hilbert-taps = 0
channelmix.disable = true
#dither.noise = 0
#node.param.Props = {
# params = [
# audio.format S16
# ]
#}
adapter.auto-port-config = {
mode = dsp
monitor = false
control = false
position = unknown # aux, preserve
}
#node.param.PortConfig = {
# direction = Input
# mode = dsp
# monitor = true
# format = {
# mediaType = audio
# mediaSubtype = raw
# format = F32
# rate = 48000
# channels = 4
# }
#}
}
}
# This creates a new Source node. It will have input ports
# that you can link, to provide audio for this source.
#{ factory = adapter
# args = {
# factory.name = support.null-audio-sink
# node.name = "my-mic"
# node.description = "Microphone"
# media.class = "Audio/Source/Virtual"
# audio.position = "FL,FR"
# adapter.auto-port-config = {
# mode = dsp
# monitor = true
# position = preserve # unknown, aux, preserve
# }
# }
#}
# This creates a new link between the source and the virtual
# source ports.
#{ factory = link-factory
# args = {
# link.output.node = system
# link.output.port = capture_1
# link.input.node = my-mic
# link.input.port = input_FL
# }
#}
#{ factory = link-factory
# args = {
# link.output.node = system
# link.output.port = capture_2
# link.input.node = my-mic
# link.input.port = input_FR
# }
#}
]
context.exec = [
#{ path = <program-name>
# ( args = "<arguments>" )
# ( condition = [ { <key> = <value> ... } ... ] )
#}
#
# Execute the given program with arguments.
#
# You can optionally start the pulseaudio-server here as well
# but it is better to start it as a systemd service.
# It can be interesting to start another daemon here that listens
# on another address with the -a option (eg. -a tcp:4713).
#
##{ path = "/usr/bin/pipewire" args = "-c pipewire-pulse.conf" }
]

View File

@ -0,0 +1,129 @@
# AES67 config file for PipeWire version "0.3.81" #
#
# Copy and edit this file in /etc/pipewire for system-wide changes
# or in ~/.config/pipewire for local changes.
#
# It is also possible to place a file with an updated section in
# /etc/pipewire/pipewire-aes67.conf.d/ for system-wide changes or in
# ~/.config/pipewire/pipewire-aes67.conf.d/ for local changes.
#
context.properties = {
## Configure properties in the system.
#mem.warn-mlock = false
#mem.allow-mlock = true
#mem.mlock-all = false
#log.level = 2
#default.clock.quantum-limit = 8192
}
context.spa-libs = {
support.* = support/libspa-support
}
context.objects = [
# An example clock reading from /dev/ptp0. Another option is to sync the
# ptp clock to CLOCK_TAI and then set clock.id = tai.
# If both device and ID are given and available, device takes precedence
{ factory = spa-node-factory
args = {
factory.name = support.node.driver
node.name = PTP0-Driver
node.group = pipewire.ptp0
# This driver should only be used for network nodes marked with group
priority.driver = 0
clock.name = "clock.system.ptp0"
clock.device = "/dev/ptp0"
clock.id = tai
object.export = true
}
}
]
context.modules = [
{ name = libpipewire-module-rt
args = {
nice.level = -11
#rt.prio = 88
#rt.time.soft = -1
#rt.time.hard = -1
}
flags = [ ifexists nofail ]
}
{ name = libpipewire-module-protocol-native }
{ name = libpipewire-module-client-node }
{ name = libpipewire-module-spa-node-factory }
{ name = libpipewire-module-adapter }
{ name = libpipewire-module-rtp-sap
args = {
local.ifname = eth0
sap.ip = 239.255.255.255
sap.port = 9875
net.ttl = 32
net.loop = true
stream.rules = [
{
matches = [
{
rtp.session = "~.*"
}
]
actions = {
create-stream = {
node.virtual = false
media.class = "Audio/Source"
device.api = aes67
sess.latency.msec = 10
node.group = pipewire.ptp0
}
}
},
{
matches = [
{
sess.sap.announce = true
}
]
actions = {
announce-stream = {}
}
}
]
}
},
{ name = libpipewire-module-rtp-sink
args = {
local.ifname = eth0
destination.ip = 239.69.150.243
destination.port = 5004
net.mtu = 1280
net.ttl = 32
net.loop = true
sess.min-ptime = 1
sess.max-ptime = 1
sess.name = "PipeWire RTP stream"
sess.media = "audio"
sess.ts-refclk = "ptp=traceable"
sess.ts-offset = 0
sess.ptime = 1
sess.latency.msec = 1
sess.announce = true
audio.format = "S24BE"
audio.rate = 48000
audio.channels = 2
audio.position = [ FL FR ]
stream.props = {
node.name = "rtp-sink"
media.class = "Audio/Sink"
node.virtual = false
device.api = aes67
sess.sap.announce = true
node.always-process = true
node.group = pipewire.ptp0
}
}
},
]

View File

@ -0,0 +1,73 @@
# PulseAudio config file for PipeWire version "0.3.81" #
#
# Copy and edit this file in /etc/pipewire for system-wide changes
# or in ~/.config/pipewire for local changes.
#
# It is also possible to place a file with an updated section in
# /etc/pipewire/pipewire-pulse.conf.d/ for system-wide changes or in
# ~/.config/pipewire/pipewire-pulse.conf.d/ for local changes.
#
context.properties = {
## Configure properties in the system.
#mem.warn-mlock = false
#mem.allow-mlock = true
#mem.mlock-all = false
#log.level = 2
#default.clock.quantum-limit = 8192
}
context.spa-libs = {
audio.convert.* = audioconvert/libspa-audioconvert
support.* = support/libspa-support
}
context.modules = [
{ name = libpipewire-module-rt
args = {
nice.level = -11
#rt.prio = 88
#rt.time.soft = -1
#rt.time.hard = -1
}
flags = [ ifexists nofail ]
}
{ name = libpipewire-module-protocol-native }
{ name = libpipewire-module-client-node }
{ name = libpipewire-module-adapter }
{ name = libpipewire-module-avb
args = {
# contents of avb.properties can also be placed here
# to have config per server.
}
}
]
# Extra modules can be loaded here. Setup in default.pa can be moved here
context.exec = [
#{ path = "pactl" args = "load-module module-always-sink" }
]
stream.properties = {
#node.latency = 1024/48000
#node.autoconnect = true
#resample.quality = 4
#channelmix.normalize = false
#channelmix.mix-lfe = true
#channelmix.upmix = true
#channelmix.lfe-cutoff = 120
#channelmix.fc-cutoff = 6000
#channelmix.rear-delay = 12.0
#channelmix.stereo-widen = 0.1
#channelmix.hilbert-taps = 0
}
avb.properties = {
# the addresses this server listens on
#ifname = "eth0.2"
ifname = "enp3s0"
# These overrides are only applied when running in a vm.
vm.overrides = {
}
}

View File

@ -0,0 +1,6 @@
context.modules = [
{
name = libpipewire-module-raop-discover
args = { }
}
]

View File

@ -20,10 +20,14 @@ context.modules = [
]
}
capture.props = {
node.name = "openvoiceos_mic.denoised"
node.passive = true
node.name = "capture.rnnoise_source"
}
playback.props = {
node.name = "openvoiceos_denoised_mic"
media.class = Audio/Source
audio.rate = 48000
}
}
}

View File

@ -0,0 +1,91 @@
# WirePlumber daemon context configuration #
context.properties = {
## Properties to configure the PipeWire context and some modules
application.name = "WirePlumber Bluetooth"
log.level = 2
wireplumber.script-engine = lua-scripting
wireplumber.export-core = true
#mem.mlock-all = false
#support.dbus = true
}
context.spa-libs = {
#<factory-name regex> = <library-name>
#
# Used to find spa factory names. It maps an spa factory name
# regular expression to a library name that should contain
# that factory.
#
api.bluez5.* = bluez5/libspa-bluez5
audio.convert.* = audioconvert/libspa-audioconvert
support.* = support/libspa-support
}
context.modules = [
#{ name = <module-name>
# [ args = { <key> = <value> ... } ]
# [ flags = [ [ ifexists ] [ nofail ] ]
#}
#
# PipeWire modules to load.
# If ifexists is given, the module is ignored when it is not found.
# If nofail is given, module initialization failures are ignored.
#
# Uses RTKit to boost the data thread priority. Also allows clamping
# of utilisation when using the Completely Fair Scheduler on Linux.
{ name = libpipewire-module-rt
args = {
nice.level = -11
#rt.prio = 88
#rt.time.soft = -1
#rt.time.hard = -1
#uclamp.min = 0
#uclamp.max = 1024
}
flags = [ ifexists nofail ]
}
# The native communication protocol.
{ name = libpipewire-module-protocol-native }
# Allows creating nodes that run in the context of the
# client. Is used by all clients that want to provide
# data to PipeWire.
{ name = libpipewire-module-client-node }
# Allows creating devices that run in the context of the
# client. Is used by the session manager.
{ name = libpipewire-module-client-device }
# Makes a factory for wrapping nodes in an adapter with a
# converter and resampler.
{ name = libpipewire-module-adapter }
# Allows applications to create metadata objects. It creates
# a factory for Metadata objects.
{ name = libpipewire-module-metadata }
# Provides factories to make session manager objects.
{ name = libpipewire-module-session-manager }
# Provides factories to make SPA node objects.
{ name = libpipewire-module-spa-node-factory }
]
wireplumber.components = [
#{ name = <component-name>, type = <component-type> }
#
# WirePlumber components to load
#
# The lua scripting engine
{ name = libwireplumber-module-lua-scripting, type = module }
# The lua configuration file
# Other components are loaded from there
{ name = bluetooth.lua, type = config/lua }
]

View File

@ -0,0 +1,36 @@
components = {}
function load_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a }
end
end
function load_optional_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a, optional = true }
end
end
function load_pw_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libpipewire-module-" .. m, type = "pw_module", args = a }
end
end
function load_script(s, a)
if not components[s] then
components[s] = { s, type = "script/lua", args = a }
end
end
function load_monitor(s, a)
load_script("monitors/" .. s .. ".lua", a)
end
function load_access(s, a)
load_script("access/access-" .. s .. ".lua", a)
end

View File

@ -0,0 +1,18 @@
bluez_midi_monitor = {}
bluez_midi_monitor.properties = {}
bluez_midi_monitor.rules = {}
function bluez_midi_monitor.enable()
if bluez_midi_monitor.enabled == false then
return
end
load_monitor("bluez-midi", {
properties = bluez_midi_monitor.properties,
rules = bluez_midi_monitor.rules,
})
if bluez_midi_monitor.properties["with-logind"] then
load_optional_module("logind")
end
end

View File

@ -0,0 +1,18 @@
bluez_monitor = {}
bluez_monitor.properties = {}
bluez_monitor.rules = {}
function bluez_monitor.enable()
if bluez_monitor.enabled == false then
return
end
load_monitor("bluez", {
properties = bluez_monitor.properties,
rules = bluez_monitor.rules,
})
if bluez_monitor.properties["with-logind"] then
load_optional_module("logind")
end
end

View File

@ -0,0 +1,140 @@
bluez_monitor.enabled = true
bluez_monitor.properties = {
-- Enabled roles (default: [ a2dp_sink a2dp_source bap_sink bap_source hfp_hf hfp_ag ])
--
-- Currently some headsets (Sony WH-1000XM3) are not working with
-- both hsp_ag and hfp_ag enabled, so by default we enable only HFP.
--
-- Supported roles: hsp_hs (HSP Headset),
-- hsp_ag (HSP Audio Gateway),
-- hfp_hf (HFP Hands-Free),
-- hfp_ag (HFP Audio Gateway)
-- a2dp_sink (A2DP Audio Sink)
-- a2dp_source (A2DP Audio Source)
-- bap_sink (LE Audio Basic Audio Profile Sink)
-- bap_source (LE Audio Basic Audio Profile Source)
--["bluez5.roles"] = "[ a2dp_sink a2dp_source bap_sink bap_source hsp_hs hsp_ag hfp_hf hfp_ag ]",
-- Enabled A2DP codecs (default: all).
--["bluez5.codecs"] = "[ sbc sbc_xq aac ldac aptx aptx_hd aptx_ll aptx_ll_duplex faststream faststream_duplex ]",
-- HFP/HSP backend (default: native).
-- Available values: any, none, hsphfpd, ofono, native
--["bluez5.hfphsp-backend"] = "native",
-- HFP/HSP native backend modem (default: none).
-- Available values: none, any or the modem device string as found in
-- 'Device' property of org.freedesktop.ModemManager1.Modem interface
--["bluez5.hfphsp-backend-native-modem"] = "none",
-- HFP/HSP hardware offload SCO support (default: false).
--["bluez5.hw-offload-sco"] = false,
-- Properties for the A2DP codec configuration
--["bluez5.default.rate"] = 48000,
--["bluez5.default.channels"] = 2,
-- Register dummy AVRCP player, required for AVRCP volume function.
-- Disable if you are running mpris-proxy or equivalent.
--["bluez5.dummy-avrcp-player"] = true,
-- Opus Pro Audio mode settings
--["bluez5.a2dp.opus.pro.channels"] = 3, -- no. channels
--["bluez5.a2dp.opus.pro.coupled-streams"] = 1, -- no. joint stereo pairs, see RFC 7845 Sec. 5.1.1
--["bluez5.a2dp.opus.pro.locations"] = "FL,FR,LFE", -- audio locations
--["bluez5.a2dp.opus.pro.max-bitrate"] = 600000,
--["bluez5.a2dp.opus.pro.frame-dms"] = 50, -- frame duration in 1/10 ms: 25, 50, 100, 200, 400
--["bluez5.a2dp.opus.pro.bidi.channels"] = 1, -- same settings for the return direction
--["bluez5.a2dp.opus.pro.bidi.coupled-streams"] = 0,
--["bluez5.a2dp.opus.pro.bidi.locations"] = "FC",
--["bluez5.a2dp.opus.pro.bidi.max-bitrate"] = 160000,
--["bluez5.a2dp.opus.pro.bidi.frame-dms"] = 400,
-- Enable the logind module, which arbitrates which user will be allowed
-- to have bluetooth audio enabled at any given time (particularly useful
-- if you are using GDM as a display manager, as the gdm user also launches
-- pipewire and wireplumber).
-- This requires access to the D-Bus user session; disable if you are running
-- a system-wide instance of wireplumber.
["with-logind"] = true,
-- The settings below can be used to override feature enabled status. By default
-- all of them are enabled. They may also be disabled via the hardware quirk
-- database, see bluez-hardware.conf
--["bluez5.enable-sbc-xq"] = true,
--["bluez5.enable-msbc"] = true,
--["bluez5.enable-hw-volume"] = true,
}
bluez_monitor.rules = {
-- An array of matches/actions to evaluate.
{
-- Rules for matching a device or node. It is an array of
-- properties that all need to match the regexp. If any of the
-- matches work, the actions are executed for the object.
matches = {
{
-- This matches all cards.
{ "device.name", "matches", "bluez_card.*" },
},
},
-- Apply properties on the matched object.
apply_properties = {
-- Auto-connect device profiles on start up or when only partial
-- profiles have connected. Disabled by default if the property
-- is not specified.
--["bluez5.auto-connect"] = "[ hfp_hf hsp_hs a2dp_sink hfp_ag hsp_ag a2dp_source ]",
-- Hardware volume control (default: [ hfp_ag hsp_ag a2dp_source ])
--["bluez5.hw-volume"] = "[ hfp_hf hsp_hs a2dp_sink hfp_ag hsp_ag a2dp_source ]",
-- LDAC encoding quality
-- Available values: auto (Adaptive Bitrate, default)
-- hq (High Quality, 990/909kbps)
-- sq (Standard Quality, 660/606kbps)
-- mq (Mobile use Quality, 330/303kbps)
--["bluez5.a2dp.ldac.quality"] = "auto",
-- AAC variable bitrate mode
-- Available values: 0 (cbr, default), 1-5 (quality level)
--["bluez5.a2dp.aac.bitratemode"] = 0,
-- Profile connected first
-- Available values: a2dp-sink (default), headset-head-unit
--["device.profile"] = "a2dp-sink",
-- Opus Pro Audio encoding mode: audio, voip, lowdelay
--["bluez5.a2dp.opus.pro.application"] = "audio",
--["bluez5.a2dp.opus.pro.bidi.application"] = "audio",
},
},
{
matches = {
{
-- Matches all sources.
{ "node.name", "matches", "bluez_input.*" },
},
{
-- Matches all sinks.
{ "node.name", "matches", "bluez_output.*" },
},
},
apply_properties = {
--["node.nick"] = "My Node",
--["priority.driver"] = 100,
--["priority.session"] = 100,
--["node.pause-on-idle"] = false,
--["resample.quality"] = 4,
--["channelmix.normalize"] = false,
--["channelmix.mix-lfe"] = false,
--["session.suspend-timeout-seconds"] = 5, -- 0 disables suspend
--["monitor.channel-volumes"] = false,
-- Media source role, "input" or "playback"
-- Defaults to "playback", playing stream to speakers
-- Set to "input" to use as an input for apps
--["bluez5.media-source-role"] = "input",
},
},
}

View File

@ -0,0 +1,42 @@
-- BLE MIDI is currently disabled by default, because it conflicts with
-- the SELinux policy on Fedora 37 and potentially other systems using
-- SELinux. For a workaround, see
-- https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/spa/plugins/bluez5/README-MIDI.md
bluez_midi_monitor.enabled = false
bluez_midi_monitor.properties = {
-- Enable the logind module, which arbitrates which user will be allowed
-- to have bluetooth audio enabled at any given time (particularly useful
-- if you are using GDM as a display manager, as the gdm user also launches
-- pipewire and wireplumber).
-- This requires access to the D-Bus user session; disable if you are running
-- a system-wide instance of wireplumber.
["with-logind"] = true,
-- List of MIDI server node names. Each node name given will create a new instance
-- of a BLE MIDI service. Typical BLE MIDI instruments have on service instance,
-- so adding more than one here may confuse some clients. The node property matching
-- rules below apply also to these servers.
--["servers"] = { "bluez_midi.server" },
}
bluez_midi_monitor.rules = {
-- An array of matches/actions to evaluate.
{
matches = {
{
-- Matches all nodes.
{ "node.name", "matches", "bluez_midi.*" },
},
},
apply_properties = {
--["node.nick"] = "My Node",
--["priority.driver"] = 100,
--["priority.session"] = 100,
--["node.pause-on-idle"] = false,
--["session.suspend-timeout-seconds"] = 5, -- 0 disables suspend
--["monitor.channel-volumes"] = false,
--["node.latency-offset-msec"] = -10, -- delay (<0) input to reduce jitter
},
},
}

View File

@ -0,0 +1,2 @@
bluez_monitor.enable()
bluez_midi_monitor.enable()

View File

@ -0,0 +1,36 @@
components = {}
function load_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a }
end
end
function load_optional_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a, optional = true }
end
end
function load_pw_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libpipewire-module-" .. m, type = "pw_module", args = a }
end
end
function load_script(s, a)
if not components[s] then
components[s] = { s, type = "script/lua", args = a }
end
end
function load_monitor(s, a)
load_script("monitors/" .. s .. ".lua", a)
end
function load_access(s, a)
load_script("access/access-" .. s .. ".lua", a)
end

View File

@ -0,0 +1,74 @@
# WirePlumber daemon context configuration #
context.properties = {
## Properties to configure the PipeWire context and some modules
#application.name = WirePlumber
log.level = 2
wireplumber.script-engine = lua-scripting
#mem.mlock-all = false
#support.dbus = true
}
context.spa-libs = {
#<factory-name regex> = <library-name>
#
# Used to find spa factory names. It maps an spa factory name
# regular expression to a library name that should contain
# that factory.
#
api.alsa.* = alsa/libspa-alsa
api.v4l2.* = v4l2/libspa-v4l2
audio.convert.* = audioconvert/libspa-audioconvert
support.* = support/libspa-support
}
context.modules = [
#{ name = <module-name>
# [ args = { <key> = <value> ... } ]
# [ flags = [ [ ifexists ] [ nofail ] ]
#}
#
# PipeWire modules to load.
# If ifexists is given, the module is ignored when it is not found.
# If nofail is given, module initialization failures are ignored.
#
# The native communication protocol.
{ name = libpipewire-module-protocol-native }
# Allows creating nodes that run in the context of the
# client. Is used by all clients that want to provide
# data to PipeWire.
{ name = libpipewire-module-client-node }
# Allows creating devices that run in the context of the
# client. Is used by the session manager.
{ name = libpipewire-module-client-device }
# Makes a factory for wrapping nodes in an adapter with a
# converter and resampler.
{ name = libpipewire-module-adapter }
# Allows applications to create metadata objects. It creates
# a factory for Metadata objects.
{ name = libpipewire-module-metadata }
# Provides factories to make session manager objects.
{ name = libpipewire-module-session-manager }
]
wireplumber.components = [
#{ name = <component-name>, type = <component-type> }
#
# WirePlumber components to load
#
# The lua scripting engine
{ name = libwireplumber-module-lua-scripting, type = module }
# The lua configuration file
# Other components are loaded from there
{ name = main.lua, type = config/lua }
]

View File

@ -0,0 +1,36 @@
components = {}
function load_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a }
end
end
function load_optional_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a, optional = true }
end
end
function load_pw_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libpipewire-module-" .. m, type = "pw_module", args = a }
end
end
function load_script(s, a)
if not components[s] then
components[s] = { s, type = "script/lua", args = a }
end
end
function load_monitor(s, a)
load_script("monitors/" .. s .. ".lua", a)
end
function load_access(s, a)
load_script("access/access-" .. s .. ".lua", a)
end

View File

@ -0,0 +1,19 @@
default_access = {}
default_access.properties = {}
default_access.rules = {}
function default_access.enable()
if default_access.enabled == false then
return
end
load_access("default", {
rules = default_access.rules
})
if default_access.properties["enable-flatpak-portal"] then
-- Enables portal permissions via org.freedesktop.impl.portal.PermissionStore
load_module("portal-permissionstore")
load_access("portal")
end
end

View File

@ -0,0 +1,29 @@
alsa_monitor = {}
alsa_monitor.properties = {}
alsa_monitor.rules = {}
function alsa_monitor.enable()
if alsa_monitor.enabled == false then
return
end
-- The "reserve-device" module needs to be loaded for reservation to work
if alsa_monitor.properties["alsa.reserve"] then
load_module("reserve-device")
end
load_monitor("alsa", {
properties = alsa_monitor.properties,
rules = alsa_monitor.rules,
})
if alsa_monitor.properties["alsa.midi"] then
load_monitor("alsa-midi", {
properties = alsa_monitor.properties,
})
-- The "file-monitor-api" module needs to be loaded for MIDI device monitoring
if alsa_monitor.properties["alsa.midi.monitoring"] then
load_module("file-monitor-api")
end
end
end

View File

@ -0,0 +1,14 @@
libcamera_monitor = {}
libcamera_monitor.properties = {}
libcamera_monitor.rules = {}
function libcamera_monitor.enable()
if libcamera_monitor.enabled == false then
return
end
load_monitor("libcamera", {
properties = libcamera_monitor.properties,
rules = libcamera_monitor.rules,
})
end

View File

@ -0,0 +1,14 @@
v4l2_monitor = {}
v4l2_monitor.properties = {}
v4l2_monitor.rules = {}
function v4l2_monitor.enable()
if v4l2_monitor.enabled == false then
return
end
load_monitor("v4l2", {
properties = v4l2_monitor.properties,
rules = v4l2_monitor.rules,
})
end

View File

@ -0,0 +1,85 @@
device_defaults = {}
device_defaults.enabled = true
device_defaults.properties = {
-- store preferences to the file system and restore them at startup;
-- when set to false, default nodes and routes are selected based on
-- their priorities and any runtime changes do not persist after restart
["use-persistent-storage"] = true,
-- the default volumes to apply to ACP device nodes, in the linear scale
--["default-volume"] = 0.064,
--["default-input-volume"] = 1.0,
-- Whether to auto-switch to echo cancel sink and source nodes or not
["auto-echo-cancel"] = true,
-- Sets the default echo-cancel-sink node name to automatically switch to
["echo-cancel-sink-name"] = "echo-cancel-sink",
-- Sets the default echo-cancel-source node name to automatically switch to
["echo-cancel-source-name"] = "echo-cancel-source",
}
-- Sets persistent device profiles that should never change when wireplumber is
-- running, even if a new profile with higher priority becomes available
device_defaults.persistent_profiles = {
{
matches = {
{
-- Matches all devices
{ "device.name", "matches", "*" },
},
},
profile_names = {
"off",
"pro-audio"
}
},
}
device_defaults.profile_priorities = {
{
matches = {
{
-- Matches all bluez devices
{ "device.name", "matches", "bluez_card.*" },
},
},
-- lower the index higher the priority
priorities = {
-- "a2dp-sink-sbc",
-- "a2dp-sink-aptx_ll",
-- "a2dp-sink-aptx",
-- "a2dp-sink-aptx_hd",
-- "a2dp-sink-ldac",
-- "a2dp-sink-aac",
-- "a2dp-sink-sbc_xq",
}
},
}
function device_defaults.enable()
if device_defaults.enabled == false then
return
end
-- Selects appropriate default nodes and enables saving and restoring them
load_module("default-nodes", device_defaults.properties)
-- Selects appropriate profile for devices
load_script("policy-device-profile.lua", {
persistent = device_defaults.persistent_profiles,
priorities = device_defaults.profile_priorities
})
-- Selects appropriate device routes ("ports" in pulseaudio terminology)
-- and enables saving and restoring them together with
-- their properties (per-route/port volume levels, channel maps, etc)
load_script("policy-device-routes.lua", device_defaults.properties)
if device_defaults.properties["use-persistent-storage"] then
-- Enables functionality to save and restore default device profiles
load_module("default-profile")
end
end

View File

@ -0,0 +1,42 @@
stream_defaults = {}
stream_defaults.enabled = true
stream_defaults.properties = {
-- whether to restore the last stream properties or not
["restore-props"] = true,
-- whether to restore the last stream target or not
["restore-target"] = true,
-- the default channel volume for new streams whose props were never saved
-- previously. This is only used if "restore-props" is set to true.
["default-channel-volume"] = 1.0,
}
stream_defaults.rules = {
-- Rules to override settings per node
-- {
-- matches = {
-- {
-- { "application.name", "matches", "pw-play" },
-- },
-- },
-- apply_properties = {
-- ["state.restore-props"] = false,
-- ["state.restore-target"] = false,
-- ["state.default-channel-volume"] = 0.5,
-- },
-- },
}
function stream_defaults.enable()
if stream_defaults.enabled == false then
return
end
-- Save and restore stream-specific properties
load_script("restore-stream.lua", {
properties = stream_defaults.properties,
rules = stream_defaults.rules,
})
end

View File

@ -0,0 +1,36 @@
default_access.enabled = true
default_access.properties = {
-- Enable the use of the flatpak portal integration.
-- Disable if you are running a system-wide instance, which
-- doesn't have access to the D-Bus user session
["enable-flatpak-portal"] = true,
}
default_access.rules = {
{
matches = {
{
{ "pipewire.access", "=", "flatpak" },
{ "media.category", "=", "Manager" },
},
},
default_permissions = "all",
},
{
matches = {
{
{ "pipewire.access", "=", "flatpak" },
},
},
default_permissions = "rx",
},
{
matches = {
{
{ "pipewire.access", "=", "restricted" },
},
},
default_permissions = "rx",
},
}

View File

@ -0,0 +1,38 @@
libcamera_monitor.enabled = true
libcamera_monitor.rules = {
-- An array of matches/actions to evaluate.
{
-- Rules for matching a device or node. It is an array of
-- properties that all need to match the regexp. If any of the
-- matches work, the actions are executed for the object.
matches = {
{
-- This matches all cards.
{ "device.name", "matches", "libcamera_device.*" },
},
},
-- Apply properties on the matched object.
apply_properties = {
-- ["device.nick"] = "My Device",
},
},
{
matches = {
{
-- Matches all sources.
{ "node.name", "matches", "libcamera_input.*" },
},
{
-- Matches all sinks.
{ "node.name", "matches", "libcamera_output.*" },
},
},
apply_properties = {
--["node.nick"] = "My Node",
--["priority.driver"] = 100,
--["priority.session"] = 100,
--["node.pause-on-idle"] = false,
},
},
}

View File

@ -0,0 +1,38 @@
v4l2_monitor.enabled = true
v4l2_monitor.rules = {
-- An array of matches/actions to evaluate.
{
-- Rules for matching a device or node. It is an array of
-- properties that all need to match the regexp. If any of the
-- matches work, the actions are executed for the object.
matches = {
{
-- This matches all cards.
{ "device.name", "matches", "v4l2_device.*" },
},
},
-- Apply properties on the matched object.
apply_properties = {
-- ["device.nick"] = "My Device",
},
},
{
matches = {
{
-- Matches all sources.
{ "node.name", "matches", "v4l2_input.*" },
},
{
-- Matches all sinks.
{ "node.name", "matches", "v4l2_output.*" },
},
},
apply_properties = {
--["node.nick"] = "My Node",
--["priority.driver"] = 100,
--["priority.session"] = 100,
--["node.pause-on-idle"] = false,
},
},
}

View File

@ -0,0 +1,26 @@
-- Provide the "default" pw_metadata, which stores
-- dynamic properties of pipewire objects in RAM
load_module("metadata")
-- Default client access policy
default_access.enable()
-- Load devices
alsa_monitor.enable()
v4l2_monitor.enable()
libcamera_monitor.enable()
-- Track/store/restore user choices about devices
device_defaults.enable()
-- Track/store/restore user choices about streams
stream_defaults.enable()
-- Link nodes by stream role and device intended role
load_script("intended-roles.lua")
-- Automatically suspends idle nodes after 3 seconds
load_script("suspend-node.lua")
-- Allows loading objects on demand via metadata
load_script("sm-objects.lua")

View File

@ -0,0 +1,73 @@
# WirePlumber daemon context configuration #
context.properties = {
## Properties to configure the PipeWire context and some modules
application.name = "WirePlumber Policy"
log.level = 2
wireplumber.script-engine = lua-scripting
wireplumber.export-core = false
#mem.mlock-all = false
#support.dbus = true
}
context.spa-libs = {
#<factory-name regex> = <library-name>
#
# Used to find spa factory names. It maps an spa factory name
# regular expression to a library name that should contain
# that factory.
#
audio.convert.* = audioconvert/libspa-audioconvert
support.* = support/libspa-support
}
context.modules = [
#{ name = <module-name>
# [ args = { <key> = <value> ... } ]
# [ flags = [ [ ifexists ] [ nofail ] ]
#}
#
# PipeWire modules to load.
# If ifexists is given, the module is ignored when it is not found.
# If nofail is given, module initialization failures are ignored.
#
# The native communication protocol.
{ name = libpipewire-module-protocol-native }
# Allows creating nodes that run in the context of the
# client. Is used by all clients that want to provide
# data to PipeWire.
{ name = libpipewire-module-client-node }
# Allows creating devices that run in the context of the
# client. Is used by the session manager.
{ name = libpipewire-module-client-device }
# Makes a factory for wrapping nodes in an adapter with a
# converter and resampler.
{ name = libpipewire-module-adapter }
# Allows applications to create metadata objects. It creates
# a factory for Metadata objects.
{ name = libpipewire-module-metadata }
# Provides factories to make session manager objects.
{ name = libpipewire-module-session-manager }
]
wireplumber.components = [
#{ name = <component-name>, type = <component-type> }
#
# WirePlumber components to load
#
# The lua scripting engine
{ name = libwireplumber-module-lua-scripting, type = module }
# The lua configuration file
# Other components are loaded from there
{ name = policy.lua, type = config/lua }
]

View File

@ -0,0 +1,36 @@
components = {}
function load_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a }
end
end
function load_optional_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a, optional = true }
end
end
function load_pw_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libpipewire-module-" .. m, type = "pw_module", args = a }
end
end
function load_script(s, a)
if not components[s] then
components[s] = { s, type = "script/lua", args = a }
end
end
function load_monitor(s, a)
load_script("monitors/" .. s .. ".lua", a)
end
function load_access(s, a)
load_script("access/access-" .. s .. ".lua", a)
end

View File

@ -0,0 +1,98 @@
default_policy = {}
default_policy.enabled = true
default_policy.properties = {}
default_policy.endpoints = {}
default_policy.policy = {
["move"] = true, -- moves session items when metadata target.node changes
["follow"] = true, -- moves session items to the default device when it has changed
-- Whether to forward the ports format of filter stream nodes to their
-- associated filter device nodes. This is needed for application to stream
-- surround audio if echo-cancel is enabled.
["filter.forward-format"] = false,
-- Set to 'true' to disable channel splitting & merging on nodes and enable
-- passthrough of audio in the same format as the format of the device.
-- Note that this breaks JACK support; it is generally not recommended
["audio.no-dsp"] = false,
-- how much to lower the volume of lower priority streams when ducking
-- note that this is a linear volume modifier (not cubic as in pulseaudio)
["duck.level"] = 0.3,
}
bluetooth_policy = {}
bluetooth_policy.policy = {
-- Whether to store state on the filesystem.
["use-persistent-storage"] = true,
-- Whether to use headset profile in the presence of an input stream.
["media-role.use-headset-profile"] = true,
-- Application names correspond to application.name in stream properties.
-- Applications which do not set media.role but which should be considered
-- for role based profile switching can be specified here.
["media-role.applications"] = {
"Firefox", "Chromium input", "Google Chrome input", "Brave input",
"Microsoft Edge input", "Vivaldi input", "ZOOM VoiceEngine",
"Telegram Desktop", "telegram-desktop", "linphone", "Mumble",
"WEBRTC VoiceEngine", "Skype", "Firefox Developer Edition",
},
}
dsp_policy = {}
dsp_policy.policy = {}
dsp_policy.policy.properties = {}
-- An array of matches/filters to apply.
-- `matches` are rules for matching a sink node. It is an array of
-- properties that all need to match the regexp. If any of the
-- matches in an array work, the filters are executed for the sink.
-- `filter_chain` is a JSON string of parameters to filter-chain module
-- `properties` table only has `pro_audio` boolean, which enables Pro Audio mode on the sink when applying DSP
dsp_policy.policy.rules = {}
function default_policy.enable()
if default_policy.enabled == false then
return
end
-- Session item factories, building blocks for the session management graph
-- Do not disable these unless you really know what you are doing
load_module("si-node")
load_module("si-audio-adapter")
load_module("si-standard-link")
load_module("si-audio-endpoint")
-- API to access default nodes from scripts
load_module("default-nodes-api")
-- API to access mixer controls, needed for volume ducking
load_module("mixer-api")
-- Create endpoints statically at startup
load_script("static-endpoints.lua", default_policy.endpoints)
-- Create items for nodes that appear in the graph
load_script("create-item.lua", default_policy.policy)
-- Link nodes to each other to make media flow in the graph
load_script("policy-node.lua", default_policy.policy)
-- Link client nodes with endpoints to make media flow in the graph
load_script("policy-endpoint-client.lua", default_policy.policy)
load_script("policy-endpoint-client-links.lua", default_policy.policy)
-- Link endpoints with device nodes to make media flow in the graph
load_script("policy-endpoint-device.lua", default_policy.policy)
-- Switch bluetooth profile based on media.role
load_script("policy-bluetooth.lua", bluetooth_policy.policy)
-- Load filter chains for hardware requiring DSP
load_script("policy-dsp.lua", dsp_policy.policy)
end

View File

@ -0,0 +1,95 @@
-- uncomment to enable role-based endpoints
-- this is not yet ready for desktop use
--
--[[
default_policy.policy.roles = {
["Capture"] = {
["alias"] = { "Multimedia", "Music", "Voice", "Capture" },
["priority"] = 25,
["action.default"] = "cork",
["action.capture"] = "mix",
["media.class"] = "Audio/Source",
},
["Multimedia"] = {
["alias"] = { "Movie", "Music", "Game" },
["priority"] = 25,
["action.default"] = "cork",
},
["Speech-Low"] = {
["priority"] = 30,
["action.default"] = "cork",
["action.Speech-Low"] = "mix",
},
["Custom-Low"] = {
["priority"] = 35,
["action.default"] = "cork",
["action.Custom-Low"] = "mix",
},
["Navigation"] = {
["priority"] = 50,
["action.default"] = "duck",
["action.Navigation"] = "mix",
},
["Speech-High"] = {
["priority"] = 60,
["action.default"] = "cork",
["action.Speech-High"] = "mix",
},
["Custom-High"] = {
["priority"] = 65,
["action.default"] = "cork",
["action.Custom-High"] = "mix",
},
["Communication"] = {
["priority"] = 75,
["action.default"] = "cork",
["action.Communication"] = "mix",
},
["Emergency"] = {
["alias"] = { "Alert" },
["priority"] = 99,
["action.default"] = "cork",
["action.Emergency"] = "mix",
},
}
default_policy.endpoints = {
["endpoint.capture"] = {
["media.class"] = "Audio/Source",
["role"] = "Capture",
},
["endpoint.multimedia"] = {
["media.class"] = "Audio/Sink",
["role"] = "Multimedia",
},
["endpoint.speech_low"] = {
["media.class"] = "Audio/Sink",
["role"] = "Speech-Low",
},
["endpoint.custom_low"] = {
["media.class"] = "Audio/Sink",
["role"] = "Custom-Low",
},
["endpoint.navigation"] = {
["media.class"] = "Audio/Sink",
["role"] = "Navigation",
},
["endpoint.speech_high"] = {
["media.class"] = "Audio/Sink",
["role"] = "Speech-High",
},
["endpoint.custom_high"] = {
["media.class"] = "Audio/Sink",
["role"] = "Custom-High",
},
["endpoint.communication"] = {
["media.class"] = "Audio/Sink",
["role"] = "Communication",
},
["endpoint.emergency"] = {
["media.class"] = "Audio/Sink",
["role"] = "Emergency",
},
}
]]--

View File

@ -0,0 +1 @@
default_policy.enable()

View File

@ -0,0 +1,53 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
local config = ... or {}
-- preprocess rules and create Interest objects
for _, r in ipairs(config.rules or {}) do
r.interests = {}
for _, i in ipairs(r.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(r.interests, interest)
end
r.matches = nil
end
function rulesGetDefaultPermissions(properties)
for _, r in ipairs(config.rules or {}) do
if r.default_permissions then
for _, interest in ipairs(r.interests) do
if interest:matches(properties) then
return r.default_permissions
end
end
end
end
end
clients_om = ObjectManager {
Interest { type = "client" }
}
clients_om:connect("object-added", function (om, client)
local id = client["bound-id"]
local properties = client["properties"]
local perms = rulesGetDefaultPermissions(properties)
if perms then
Log.info(client, "Granting permissions to client " .. id .. ": " .. perms)
client:update_permissions { ["any"] = perms }
end
end)
clients_om:activate()

View File

@ -0,0 +1,141 @@
MEDIA_ROLE_NONE = 0
MEDIA_ROLE_CAMERA = 1 << 0
function hasPermission (permissions, app_id, lookup)
if permissions then
for key, values in pairs(permissions) do
if key == app_id then
for _, v in pairs(values) do
if v == lookup then
return true
end
end
end
end
end
return false
end
function parseMediaRoles (media_roles_str)
local media_roles = MEDIA_ROLE_NONE
for role in media_roles_str:gmatch('[^,%s]+') do
if role == "Camera" then
media_roles = media_roles | MEDIA_ROLE_CAMERA
end
end
return media_roles
end
function setPermissions (client, allow_client, allow_nodes)
local client_id = client["bound-id"]
Log.info(client, "Granting ALL access to client " .. client_id)
-- Update permissions on client
client:update_permissions { [client_id] = allow_client and "all" or "-" }
-- Update permissions on camera source nodes
for node in nodes_om:iterate() do
local node_id = node["bound-id"]
client:update_permissions { [node_id] = allow_nodes and "all" or "-" }
end
end
function updateClientPermissions (client, permissions)
local client_id = client["bound-id"]
local str_prop = nil
local app_id = nil
local media_roles = nil
local allowed = false
-- Make sure the client is not the portal itself
str_prop = client.properties["pipewire.access.portal.is_portal"]
if str_prop == "yes" then
Log.info (client, "client is the portal itself")
return
end
-- Make sure the client has a portal app Id
str_prop = client.properties["pipewire.access.portal.app_id"]
if str_prop == nil then
Log.info (client, "Portal managed client did not set app_id")
return
end
if str_prop == "" then
Log.info (client, "Ignoring portal check for non-sandboxed client")
setPermissions (client, true, true)
return
end
app_id = str_prop
-- Make sure the client has portal media roles
str_prop = client.properties["pipewire.access.portal.media_roles"]
if str_prop == nil then
Log.info (client, "Portal managed client did not set media_roles")
return
end
media_roles = parseMediaRoles (str_prop)
if (media_roles & MEDIA_ROLE_CAMERA) == 0 then
Log.info (client, "Ignoring portal check for clients without camera role")
return
end
-- Update permissions
allowed = hasPermission (permissions, app_id, "yes")
Log.info (client, "setting permissions: " .. tostring(allowed))
setPermissions (client, allowed, allowed)
end
-- Create portal clients object manager
clients_om = ObjectManager {
Interest {
type = "client",
Constraint { "pipewire.access", "=", "portal" },
}
}
-- Set permissions to portal clients from the permission store if loaded
pps_plugin = Plugin.find("portal-permissionstore")
if pps_plugin then
nodes_om = ObjectManager {
Interest {
type = "node",
Constraint { "media.role", "=", "Camera" },
Constraint { "media.class", "=", "Video/Source" },
}
}
nodes_om:activate()
clients_om:connect("object-added", function (om, client)
local new_perms = pps_plugin:call("lookup", "devices", "camera");
updateClientPermissions (client, new_perms)
end)
nodes_om:connect("object-added", function (om, node)
local new_perms = pps_plugin:call("lookup", "devices", "camera");
for client in clients_om:iterate() do
updateClientPermissions (client, new_perms)
end
end)
pps_plugin:connect("changed", function (p, table, id, deleted, permissions)
if table == "devices" or id == "camera" then
for app_id, _ in pairs(permissions) do
for client in clients_om:iterate {
Constraint { "pipewire.access.portal.app_id", "=", app_id }
} do
updateClientPermissions (client, permissions)
end
end
end
end)
else
-- Otherwise, just set all permissions to all portal clients
clients_om:connect("object-added", function (om, client)
local id = client["bound-id"]
Log.info(client, "Granting ALL access to client " .. id)
client:update_permissions { ["any"] = "all" }
end)
end
clients_om:activate()

View File

@ -0,0 +1,130 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local config = ... or {}
items = {}
function configProperties(node)
local np = node.properties
local properties = {
["item.node"] = node,
["item.plugged.usec"] = GLib.get_monotonic_time(),
["item.features.no-dsp"] = config["audio.no-dsp"],
["item.features.monitor"] = true,
["item.features.control-port"] = false,
["node.id"] = node["bound-id"],
["client.id"] = np["client.id"],
["object.path"] = np["object.path"],
["object.serial"] = np["object.serial"],
["target.object"] = np["target.object"],
["priority.session"] = np["priority.session"],
["device.id"] = np["device.id"],
["card.profile.device"] = np["card.profile.device"],
["target.endpoint"] = np["target.endpoint"],
}
for k, v in pairs(np) do
if k:find("^node") or k:find("^stream") or k:find("^media") then
properties[k] = v
end
end
local media_class = properties["media.class"] or ""
if not properties["media.type"] then
for _, i in ipairs({ "Audio", "Video", "Midi" }) do
if media_class:find(i) then
properties["media.type"] = i
break
end
end
end
properties["item.node.type"] =
media_class:find("^Stream/") and "stream" or "device"
if media_class:find("Sink") or
media_class:find("Input") or
media_class:find("Duplex") then
properties["item.node.direction"] = "input"
elseif media_class:find("Source") or media_class:find("Output") then
properties["item.node.direction"] = "output"
end
return properties
end
function addItem (node, item_type)
local id = node["bound-id"]
local item
-- create item
item = SessionItem ( item_type )
items[id] = item
-- configure item
if not item:configure(configProperties(node)) then
Log.warning(item, "failed to configure item for node " .. tostring(id))
return
end
item:register ()
-- activate item
items[id]:activate (Features.ALL, function (item, e)
if e then
Log.message(item, "failed to activate item: " .. tostring(e));
if item then
item:remove ()
end
else
Log.info(item, "activated item for node " .. tostring(id))
-- Trigger object managers to update status
item:remove ()
if item["active-features"] ~= 0 then
item:register ()
end
end
end)
end
nodes_om = ObjectManager {
Interest {
type = "node",
Constraint { "media.class", "#", "Stream/*", type = "pw-global" },
},
Interest {
type = "node",
Constraint { "media.class", "#", "Video/*", type = "pw-global" },
},
Interest {
type = "node",
Constraint { "media.class", "#", "Audio/*", type = "pw-global" },
Constraint { "wireplumber.is-endpoint", "-", type = "pw" },
},
}
nodes_om:connect("object-added", function (om, node)
local media_class = node.properties['media.class']
if string.find (media_class, "Audio") then
addItem (node, "si-audio-adapter")
else
addItem (node, "si-node")
end
end)
nodes_om:connect("object-removed", function (om, node)
local id = node["bound-id"]
if items[id] then
items[id]:remove ()
items[id] = nil
end
end)
nodes_om:activate()

View File

@ -0,0 +1,93 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author Frédéric Danis <frederic.danis@collabora.com>
--
-- SPDX-License-Identifier: MIT
local sink_ids = {}
local fallback_node = nil
node_om = ObjectManager {
Interest {
type = "node",
Constraint { "media.class", "matches", "Audio/Sink", type = "pw-global" },
-- Do not consider endpoints created by WirePlumber
Constraint { "wireplumber.is-endpoint", "!", true, type = "pw" },
-- or the fallback sink itself
Constraint { "wireplumber.is-fallback", "!", true, type = "pw" },
}
}
function createFallbackSink()
if fallback_node then
return
end
Log.info("Create fallback sink")
local properties = {}
properties["node.name"] = "auto_null"
properties["node.description"] = "Dummy Output"
properties["audio.rate"] = 48000
properties["audio.channels"] = 2
properties["audio.position"] = "FL,FR"
properties["media.class"] = "Audio/Sink"
properties["factory.name"] = "support.null-audio-sink"
properties["node.virtual"] = "true"
properties["monitor.channel-volumes"] = "true"
properties["wireplumber.is-fallback"] = "true"
properties["priority.session"] = 500
fallback_node = LocalNode("adapter", properties)
fallback_node:activate(Feature.Proxy.BOUND)
end
function checkSinks()
local sink_ids_items = 0
for _ in pairs(sink_ids) do sink_ids_items = sink_ids_items + 1 end
if sink_ids_items > 0 then
if fallback_node then
Log.info("Remove fallback sink")
fallback_node = nil
end
elseif not fallback_node then
createFallbackSink()
end
end
function checkSinksAfterTimeout()
if timeout_source then
timeout_source:destroy()
end
timeout_source = Core.timeout_add(1000, function ()
checkSinks()
timeout_source = nil
end)
end
node_om:connect("object-added", function (_, node)
Log.debug("object added: " .. node.properties["object.id"] .. " " ..
tostring(node.properties["node.name"]))
sink_ids[node.properties["object.id"]] = node.properties["node.name"]
checkSinksAfterTimeout()
end)
node_om:connect("object-removed", function (_, node)
Log.debug("object removed: " .. node.properties["object.id"] .. " " ..
tostring(node.properties["node.name"]))
sink_ids[node.properties["object.id"]] = nil
checkSinksAfterTimeout()
end)
node_om:activate()
checkSinksAfterTimeout()

View File

@ -0,0 +1,74 @@
-- WirePlumber
--
-- Copyright © 2021 Asymptotic
-- @author Arun Raghavan <arun@asymptotic.io>
--
-- SPDX-License-Identifier: MIT
--
-- Route streams of a given role (media.role property) to devices that are
-- intended for that role (device.intended-roles property)
metadata_om = ObjectManager {
Interest {
type = "metadata",
Constraint { "metadata.name", "=", "default" },
}
}
devices_om = ObjectManager {
Interest {
type = "node",
Constraint { "media.class", "matches", "Audio/*", type = "pw-global" },
Constraint { "device.intended-roles", "is-present", type = "pw" },
}
}
streams_om = ObjectManager {
Interest {
type = "node",
Constraint { "media.class", "matches", "Stream/*/Audio", type = "pw-global" },
Constraint { "media.role", "is-present", type = "pw-global" }
}
}
local function routeUsingIntendedRole(stream, dev)
local stream_role = stream.properties["media.role"]
local is_input = stream.properties["media.class"]:find("Input") ~= nil
local is_source = dev.properties["media.class"]:find("Source") ~= nil
local dev_roles = dev.properties["device.intended-roles"]
-- Make sure the stream and device direction match
if is_input ~= is_source then
return
end
for role in dev_roles:gmatch("(%a+)") do
if role == stream_role then
Log.info(stream,
string.format("Routing stream '%s' (%d) with role '%s' to '%s' (%d)",
stream.properties["node.name"], stream["bound-id"], stream_role,
dev.properties["node.name"], dev["bound-id"])
)
local metadata = metadata_om:lookup()
metadata:set(stream["bound-id"], "target.node", "Spa:Id", dev["bound-id"])
end
end
end
streams_om:connect("object-added", function (streams_om, stream)
for dev in devices_om:iterate() do
routeUsingIntendedRole(stream, dev)
end
end)
devices_om:connect("object-added", function (devices_om, dev)
for stream in streams_om:iterate() do
routeUsingIntendedRole(stream, dev)
end
end)
metadata_om:activate()
devices_om:activate()
streams_om:activate()

View File

@ -0,0 +1,68 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local config = ... or {}
-- ensure config.properties is not nil
config.properties = config.properties or {}
SND_PATH = "/dev/snd"
SEQ_NAME = "seq"
SND_SEQ_PATH = SND_PATH .. "/" .. SEQ_NAME
midi_node = nil
fm_plugin = nil
function CreateMidiNode ()
-- Midi properties
local props = {}
if type(config.properties["alsa.midi.node-properties"]) == "table" then
props = config.properties["alsa.midi.node-properties"]
end
props["factory.name"] = "api.alsa.seq.bridge"
props["node.name"] = props["node.name"] or "Midi-Bridge"
-- create the midi node
local node = Node("spa-node-factory", props)
node:activate(Feature.Proxy.BOUND, function (n)
Log.info ("activated Midi bridge")
end)
return node;
end
if GLib.access (SND_SEQ_PATH, "rw") then
midi_node = CreateMidiNode ()
elseif config.properties["alsa.midi.monitoring"] then
fm_plugin = Plugin.find("file-monitor-api")
end
-- Only monitor the MIDI device if file does not exist and plugin API is loaded
if midi_node == nil and fm_plugin ~= nil then
-- listen for changed events
fm_plugin:connect ("changed", function (o, file, old, evtype)
-- files attributes changed
if evtype == "attribute-changed" then
if file ~= SND_SEQ_PATH then
return
end
if midi_node == nil and GLib.access (SND_SEQ_PATH, "rw") then
midi_node = CreateMidiNode ()
fm_plugin:call ("remove-watch", SND_PATH)
end
end
-- directory is going to be unmounted
if evtype == "pre-unmount" then
fm_plugin:call ("remove-watch", SND_PATH)
end
end)
-- add watch
fm_plugin:call ("add-watch", SND_PATH, "m")
end

View File

@ -0,0 +1,427 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local config = ... or {}
-- ensure config.properties is not nil
config.properties = config.properties or {}
-- unique device/node name tables
device_names_table = nil
node_names_table = nil
-- preprocess rules and create Interest objects
for _, r in ipairs(config.rules or {}) do
r.interests = {}
for _, i in ipairs(r.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(r.interests, interest)
end
r.matches = nil
end
-- applies properties from config.rules when asked to
function rulesApplyProperties(properties)
for _, r in ipairs(config.rules or {}) do
if r.apply_properties then
for _, interest in ipairs(r.interests) do
if interest:matches(properties) then
for k, v in pairs(r.apply_properties) do
properties[k] = v
end
end
end
end
end
end
function nonempty(str)
return str ~= "" and str or nil
end
function createNode(parent, id, obj_type, factory, properties)
local dev_props = parent.properties
-- set the device id and spa factory name; REQUIRED, do not change
properties["device.id"] = parent["bound-id"]
properties["factory.name"] = factory
-- set the default pause-on-idle setting
properties["node.pause-on-idle"] = false
-- try to negotiate the max ammount of channels
if dev_props["api.alsa.use-acp"] ~= "true" then
properties["audio.channels"] = properties["audio.channels"] or "64"
end
local dev = properties["api.alsa.pcm.device"]
or properties["alsa.device"] or "0"
local subdev = properties["api.alsa.pcm.subdevice"]
or properties["alsa.subdevice"] or "0"
local stream = properties["api.alsa.pcm.stream"] or "unknown"
local profile = properties["device.profile.name"]
or (stream .. "." .. dev .. "." .. subdev)
local profile_desc = properties["device.profile.description"]
-- set priority
if not properties["priority.driver"] then
local priority = (dev == "0") and 1000 or 744
if stream == "capture" then
priority = priority + 1000
end
priority = priority - (tonumber(dev) * 16) - tonumber(subdev)
if profile:find("^pro%-") then
priority = priority + 500
elseif profile:find("^analog%-") then
priority = priority + 9
elseif profile:find("^iec958%-") then
priority = priority + 8
end
properties["priority.driver"] = priority
properties["priority.session"] = priority
end
-- ensure the node has a media class
if not properties["media.class"] then
if stream == "capture" then
properties["media.class"] = "Audio/Source"
else
properties["media.class"] = "Audio/Sink"
end
end
-- ensure the node has a name
if not properties["node.name"] then
local name =
(stream == "capture" and "alsa_input" or "alsa_output")
.. "." ..
(dev_props["device.name"]:gsub("^alsa_card%.(.+)", "%1") or
dev_props["device.name"] or
"unnamed-device")
.. "." ..
profile
-- sanitize name
name = name:gsub("([^%w_%-%.])", "_")
properties["node.name"] = name
-- deduplicate nodes with the same name
for counter = 2, 99, 1 do
if node_names_table[properties["node.name"]] ~= true then
node_names_table[properties["node.name"]] = true
break
end
properties["node.name"] = name .. "." .. counter
end
end
-- and a nick
local nick = nonempty(properties["node.nick"])
or nonempty(properties["api.alsa.pcm.name"])
or nonempty(properties["alsa.name"])
or nonempty(profile_desc)
or dev_props["device.nick"]
if nick == "USB Audio" then
nick = dev_props["device.nick"]
end
-- also sanitize nick, replace ':' with ' '
properties["node.nick"] = nick:gsub("(:)", " ")
-- ensure the node has a description
if not properties["node.description"] then
local desc = nonempty(dev_props["device.description"]) or "unknown"
local name = nonempty(properties["api.alsa.pcm.name"]) or
nonempty(properties["api.alsa.pcm.id"]) or dev
if profile_desc then
desc = desc .. " " .. profile_desc
elseif subdev ~= "0" then
desc = desc .. " (" .. name .. " " .. subdev .. ")"
elseif dev ~= "0" then
desc = desc .. " (" .. name .. ")"
end
-- also sanitize description, replace ':' with ' '
properties["node.description"] = desc:gsub("(:)", " ")
end
-- add api.alsa.card.* properties for rule matching purposes
for k, v in pairs(dev_props) do
if k:find("^api%.alsa%.card%..*") then
properties[k] = v
end
end
-- apply VM overrides
local vm_overrides = config.properties["vm.node.defaults"]
if nonempty(Core.get_vm_type()) and type(vm_overrides) == "table" then
for k, v in pairs(vm_overrides) do
properties[k] = v
end
end
-- apply properties from config.rules
rulesApplyProperties(properties)
if properties["node.disabled"] then
node_names_table [properties ["node.name"]] = nil
return
end
-- create the node
local node = Node("adapter", properties)
node:activate(Feature.Proxy.BOUND)
parent:store_managed_object(id, node)
end
function createDevice(parent, id, factory, properties)
local device = SpaDevice(factory, properties)
if device then
device:connect("create-object", createNode)
device:connect("object-removed", function (parent, id)
local node = parent:get_managed_object(id)
if not node then
return
end
node_names_table[node.properties["node.name"]] = nil
end)
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
parent:store_managed_object(id, device)
else
Log.warning ("Failed to create '" .. factory .. "' device")
end
end
function prepareDevice(parent, id, obj_type, factory, properties)
-- ensure the device has an appropriate name
local name = "alsa_card." ..
(properties["device.name"] or
properties["device.bus-id"] or
properties["device.bus-path"] or
tostring(id)):gsub("([^%w_%-%.])", "_")
properties["device.name"] = name
-- deduplicate devices with the same name
for counter = 2, 99, 1 do
if device_names_table[properties["device.name"]] ~= true then
device_names_table[properties["device.name"]] = true
break
end
properties["device.name"] = name .. "." .. counter
end
-- ensure the device has a description
if not properties["device.description"] then
local d = nil
local f = properties["device.form-factor"]
local c = properties["device.class"]
local n = properties["api.alsa.card.name"]
if n == "Loopback" then
d = I18n.gettext("Loopback")
elseif f == "internal" then
d = I18n.gettext("Built-in Audio")
elseif c == "modem" then
d = I18n.gettext("Modem")
end
d = d or properties["device.product.name"]
or properties["api.alsa.card.name"]
or properties["alsa.card_name"]
or "Unknown device"
properties["device.description"] = d
end
-- ensure the device has a nick
properties["device.nick"] =
properties["device.nick"] or
properties["api.alsa.card.name"] or
properties["alsa.card_name"]
-- set the icon name
if not properties["device.icon-name"] then
local icon = nil
local icon_map = {
-- form factor -> icon
["microphone"] = "audio-input-microphone",
["webcam"] = "camera-web",
["handset"] = "phone",
["portable"] = "multimedia-player",
["tv"] = "video-display",
["headset"] = "audio-headset",
["headphone"] = "audio-headphones",
["speaker"] = "audio-speakers",
["hands-free"] = "audio-handsfree",
}
local f = properties["device.form-factor"]
local c = properties["device.class"]
local b = properties["device.bus"]
icon = icon_map[f] or ((c == "modem") and "modem") or "audio-card"
properties["device.icon-name"] = icon .. "-analog" .. (b and ("-" .. b) or "")
end
-- apply properties from config.rules
rulesApplyProperties(properties)
if properties["device.disabled"] then
device_names_table [properties ["device.name"]] = nil
return
end
-- override the device factory to use ACP
if properties["api.alsa.use-acp"] then
Log.info("Enabling the use of ACP on " .. properties["device.name"])
factory = "api.alsa.acp.device"
end
-- use device reservation, if available
if rd_plugin and properties["api.alsa.card"] then
local rd_name = "Audio" .. properties["api.alsa.card"]
local rd = rd_plugin:call("create-reservation",
rd_name,
config.properties["alsa.reserve.application-name"] or "WirePlumber",
properties["device.name"],
config.properties["alsa.reserve.priority"] or -20);
properties["api.dbus.ReserveDevice1"] = rd_name
-- unlike pipewire-media-session, this logic here keeps the device
-- acquired at all times and destroys it if someone else acquires
rd:connect("notify::state", function (rd, pspec)
local state = rd["state"]
if state == "acquired" then
-- create the device
createDevice(parent, id, factory, properties)
elseif state == "available" then
-- attempt to acquire again
rd:call("acquire")
elseif state == "busy" then
-- destroy the device
parent:store_managed_object(id, nil)
end
end)
rd:connect("release-requested", function (rd)
Log.info("release requested")
parent:store_managed_object(id, nil)
rd:call("release")
end)
if jack_device then
rd:connect("notify::owner-name-changed", function (rd, pspec)
if rd["state"] == "busy" and
rd["owner-application-name"] == "Jack audio server" then
-- TODO enable the jack device
else
-- TODO disable the jack device
end
end)
end
rd:call("acquire")
else
-- create the device
createDevice(parent, id, factory, properties)
end
end
function createMonitor ()
local m = SpaDevice("api.alsa.enum.udev", config.properties)
if m == nil then
Log.message("PipeWire's SPA ALSA udev plugin(\"api.alsa.enum.udev\")"
.. "missing or broken. Sound Cards cannot be enumerated")
return nil
end
-- handle create-object to prepare device
m:connect("create-object", prepareDevice)
-- handle object-removed to destroy device reservations and recycle device name
m:connect("object-removed", function (parent, id)
local device = parent:get_managed_object(id)
if not device then
return
end
if rd_plugin then
local rd_name = device.properties["api.dbus.ReserveDevice1"]
if rd_name then
rd_plugin:call("destroy-reservation", rd_name)
end
end
device_names_table[device.properties["device.name"]] = nil
for managed_node in device:iterate_managed_objects() do
node_names_table[managed_node.properties["node.name"]] = nil
end
end)
-- reset the name tables to make sure names are recycled
device_names_table = {}
node_names_table = {}
-- activate monitor
Log.info("Activating ALSA monitor")
m:activate(Feature.SpaDevice.ENABLED)
return m
end
-- create the JACK device (for PipeWire to act as client to a JACK server)
if config.properties["alsa.jack-device"] then
jack_device = Device("spa-device-factory", {
["factory.name"] = "api.jack.device",
["node.name"] = "JACK-Device",
})
jack_device:activate(Feature.Proxy.BOUND)
end
-- enable device reservation if requested
if config.properties["alsa.reserve"] then
rd_plugin = Plugin.find("reserve-device")
end
-- if the reserve-device plugin is enabled, at the point of script execution
-- it is expected to be connected. if it is not, assume the d-bus connection
-- has failed and continue without it
if rd_plugin and rd_plugin:call("get-dbus")["state"] ~= "connected" then
Log.message("reserve-device plugin is not connected to D-Bus, "
.. "disabling device reservation")
rd_plugin = nil
end
-- handle rd_plugin state changes to destroy and re-create the ALSA monitor in
-- case D-Bus service is restarted
if rd_plugin then
local dbus = rd_plugin:call("get-dbus")
dbus:connect("notify::state", function (b, pspec)
local state = b["state"]
Log.info ("rd-plugin state changed to " .. state)
if state == "connected" then
Log.info ("Creating ALSA monitor")
monitor = createMonitor()
elseif state == "closed" then
Log.info ("Destroying ALSA monitor")
monitor = nil
end
end)
end
-- create the monitor
monitor = createMonitor()

View File

@ -0,0 +1,187 @@
-- WirePlumber
--
-- Copyright © 2022 Pauli Virtanen
-- @author Pauli Virtanen
--
-- SPDX-License-Identifier: MIT
local config = ... or {}
-- unique device/node name tables
node_names_table = nil
id_to_name_table = nil
-- preprocess rules and create Interest objects
for _, r in ipairs(config.rules or {}) do
r.interests = {}
for _, i in ipairs(r.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(r.interests, interest)
end
r.matches = nil
end
-- applies properties from config.rules when asked to
function rulesApplyProperties(properties)
for _, r in ipairs(config.rules or {}) do
if r.apply_properties then
for _, interest in ipairs(r.interests) do
if interest:matches(properties) then
for k, v in pairs(r.apply_properties) do
properties[k] = v
end
end
end
end
end
end
function setLatencyOffset(node, offset_msec)
if not offset_msec then
return
end
local props = { "Spa:Pod:Object:Param:Props", "Props" }
props.latencyOffsetNsec = tonumber(offset_msec) * 1000000
local param = Pod.Object(props)
Log.debug(param, "setting latency offset on " .. tostring(node))
node:set_param("Props", param)
end
function createNode(parent, id, type, factory, properties)
properties["factory.name"] = factory
-- set the node description
local desc = properties["node.description"]
-- sanitize description, replace ':' with ' '
properties["node.description"] = desc:gsub("(:)", " ")
-- set the node name
local name =
"bluez_midi." .. properties["api.bluez5.address"]
-- sanitize name
name = name:gsub("([^%w_%-%.])", "_")
-- deduplicate nodes with the same name
properties["node.name"] = name
for counter = 2, 99, 1 do
if node_names_table[properties["node.name"]] ~= true then
node_names_table[properties["node.name"]] = true
break
end
properties["node.name"] = name .. "." .. counter
end
properties["api.glib.mainloop"] = "true"
-- apply properties from config.rules
rulesApplyProperties(properties)
local latency_offset = properties["node.latency-offset-msec"]
properties["node.latency-offset-msec"] = nil
-- create the node
-- it doesn't necessarily need to be a local node,
-- the other Bluetooth parts run in the local process,
-- so it's consistent to have also this here
local node = LocalNode("spa-node-factory", properties)
node:activate(Feature.Proxy.BOUND)
parent:store_managed_object(id, node)
id_to_name_table[id] = properties["node.name"]
setLatencyOffset(node, latency_offset)
end
function createMonitor()
local monitor_props = {}
for k, v in pairs(config.properties or {}) do
monitor_props[k] = v
end
monitor_props["server"] = nil
monitor_props["api.glib.mainloop"] = "true"
local monitor = SpaDevice("api.bluez5.midi.enum", monitor_props)
if monitor then
monitor:connect("create-object", createNode)
monitor:connect("object-removed", function (parent, id)
node_names_table[id_to_name_table[id]] = nil
id_to_name_table[id] = nil
end)
else
Log.message("PipeWire's BlueZ MIDI SPA missing or broken. Bluetooth not supported.")
return nil
end
-- reset the name tables to make sure names are recycled
node_names_table = {}
id_to_name_table = {}
monitor:activate(Feature.SpaDevice.ENABLED)
return monitor
end
function createServers()
local props = config.properties or {}
if not props["servers"] then
return nil
end
local servers = {}
local i = 1
for k, v in pairs(props["servers"]) do
local node_props = {
["node.name"] = v,
["node.description"] = string.format(I18n.gettext("BLE MIDI %d"), i),
["api.bluez5.role"] = "server",
["factory.name"] = "api.bluez5.midi.node",
["api.glib.mainloop"] = "true",
}
rulesApplyProperties(node_props)
local latency_offset = node_props["node.latency-offset-msec"]
node_props["node.latency-offset-msec"] = nil
local node = LocalNode("spa-node-factory", node_props)
if node then
node:activate(Feature.Proxy.BOUND)
table.insert(servers, node)
setLatencyOffset(node, latency_offset)
else
Log.message("Failed to create BLE MIDI server.")
end
i = i + 1
end
return servers
end
logind_plugin = Plugin.find("logind")
if logind_plugin then
-- if logind support is enabled, activate
-- the monitor only when the seat is active
function startStopMonitor(seat_state)
Log.info(logind_plugin, "Seat state changed: " .. seat_state)
if seat_state == "active" then
monitor = createMonitor()
servers = createServers()
elseif monitor then
monitor:deactivate(Feature.SpaDevice.ENABLED)
monitor = nil
servers = nil
end
end
logind_plugin:connect("state-changed", function(p, s) startStopMonitor(s) end)
startStopMonitor(logind_plugin:call("get-state"))
else
monitor = createMonitor()
servers = createServers()
end

View File

@ -0,0 +1,428 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
local config = ... or {}
local COMBINE_OFFSET = 64
devices_om = ObjectManager {
Interest {
type = "device",
}
}
nodes_om = ObjectManager {
Interest {
type = "node",
Constraint { "node.name", "#", "*.bluez_*put*"},
Constraint { "device.id", "+" },
}
}
-- preprocess rules and create Interest objects
for _, r in ipairs(config.rules or {}) do
r.interests = {}
for _, i in ipairs(r.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(r.interests, interest)
end
r.matches = nil
end
-- applies properties from config.rules when asked to
function rulesApplyProperties(properties)
for _, r in ipairs(config.rules or {}) do
if r.apply_properties then
for _, interest in ipairs(r.interests) do
if interest:matches(properties) then
for k, v in pairs(r.apply_properties) do
properties[k] = v
end
end
end
end
end
end
function setOffloadActive(device, value)
local pod = Pod.Object {
"Spa:Pod:Object:Param:Props", "Props", bluetoothOffloadActive = value
}
device:set_params("Props", pod)
end
nodes_om:connect("object-added", function(_, node)
node:connect("state-changed", function(node, old_state, cur_state)
local interest = Interest {
type = "device",
Constraint { "object.id", "=", node.properties["device.id"]}
}
for d in devices_om:iterate (interest) do
if cur_state == "running" then
setOffloadActive(d, true)
else
setOffloadActive(d, false)
end
end
end)
end)
function createOffloadScoNode(parent, id, type, factory, properties)
local dev_props = parent.properties
local args = {
["audio.channels"] = 1,
["audio.position"] = "[MONO]",
}
local desc =
dev_props["device.description"]
or dev_props["device.name"]
or dev_props["device.nick"]
or dev_props["device.alias"]
or "bluetooth-device"
-- sanitize description, replace ':' with ' '
args["node.description"] = desc:gsub("(:)", " ")
if factory:find("sink") then
local capture_args = {
["device.id"] = parent["bound-id"],
["media.class"] = "Audio/Sink",
["node.pause-on-idle"] = false,
}
for k, v in pairs(properties) do
capture_args[k] = v
end
local name = "bluez_output" .. "." .. (properties["api.bluez5.address"] or dev_props["device.name"]) .. "." .. tostring(id)
args["node.name"] = name:gsub("([^%w_%-%.])", "_")
args["capture.props"] = Json.Object(capture_args)
args["playback.props"] = Json.Object {
["node.passive"] = true,
["node.pause-on-idle"] = false,
}
elseif factory:find("source") then
local playback_args = {
["device.id"] = parent["bound-id"],
["media.class"] = "Audio/Source",
["node.pause-on-idle"] = false,
}
for k, v in pairs(properties) do
playback_args[k] = v
end
local name = "bluez_input" .. "." .. (properties["api.bluez5.address"] or dev_props["device.name"]) .. "." .. tostring(id)
args["node.name"] = name:gsub("([^%w_%-%.])", "_")
args["capture.props"] = Json.Object {
["node.passive"] = true,
["node.pause-on-idle"] = false,
}
args["playback.props"] = Json.Object(playback_args)
else
Log.warning(parent, "Unsupported factory: " .. factory)
return
end
-- Transform 'args' to a json object here
local args_json = Json.Object(args)
-- and get the final JSON as a string from the json object
local args_string = args_json:get_data()
local loopback_properties = {}
local loopback = LocalModule("libpipewire-module-loopback", args_string, loopback_properties)
parent:store_managed_object(id, loopback)
end
device_set_nodes_om = ObjectManager {
Interest {
type = "node",
Constraint { "api.bluez5.set.leader", "+", type = "pw" },
}
}
device_set_nodes_om:connect ("object-added", function(_, node)
-- Connect ObjectConfig events to the right node
if not monitor then
return
end
local interest = Interest {
type = "device",
Constraint { "object.id", "=", node.properties["device.id"] }
}
Log.info("Device set node found: " .. tostring (node["bound-id"]))
for device in devices_om:iterate (interest) do
local device_id = device.properties["api.bluez5.id"]
if not device_id then
goto next_device
end
local spa_device = monitor:get_managed_object (tonumber (device_id))
if not spa_device then
goto next_device
end
local id = node.properties["card.profile.device"]
if id ~= nil then
Log.info(".. assign to device: " .. tostring (device["bound-id"]) .. " node " .. tostring (id))
spa_device:store_managed_object (id, node)
-- set routes again to update volumes etc.
for route in device:iterate_params ("Route") do
device:set_param ("Route", route)
end
end
::next_device::
end
end)
function createSetNode(parent, id, type, factory, properties)
local args = {}
local name
local target_class
local stream_class
local rules = {}
local members_json = Json.Raw (properties["api.bluez5.set.members"])
local channels_json = Json.Raw (properties["api.bluez5.set.channels"])
local members = members_json:parse ()
local channels = channels_json:parse ()
if properties["media.class"] == "Audio/Sink" then
name = "bluez_output"
args["combine.mode"] = "sink"
target_class = "Audio/Sink/Internal"
stream_class = "Stream/Output/Audio/Internal"
else
name = "bluez_input"
args["combine.mode"] = "source"
target_class = "Audio/Source/Internal"
stream_class = "Stream/Input/Audio/Internal"
end
Log.info("Device set: " .. properties["node.name"])
for _, member in pairs(members) do
Log.info("Device set member:" .. member["object.path"])
table.insert(rules,
Json.Object {
["matches"] = Json.Array {
Json.Object {
["object.path"] = member["object.path"],
["media.class"] = target_class,
},
},
["actions"] = Json.Object {
["create-stream"] = Json.Object {
["media.class"] = stream_class,
["audio.position"] = Json.Array (member["channels"]),
}
},
}
)
end
properties["node.virtual"] = false
properties["device.api"] = "bluez5"
properties["api.bluez5.set.members"] = nil
properties["api.bluez5.set.channels"] = nil
properties["api.bluez5.set.leader"] = true
properties["audio.position"] = Json.Array (channels)
args["combine.props"] = Json.Object (properties)
args["stream.props"] = Json.Object {}
args["stream.rules"] = Json.Array (rules)
local args_json = Json.Object(args)
local args_string = args_json:get_data()
local combine_properties = {}
Log.info("Device set node: " .. args_string)
return LocalModule("libpipewire-module-combine-stream", args_string, combine_properties)
end
function createNode(parent, id, type, factory, properties)
local dev_props = parent.properties
if config.properties["bluez5.hw-offload-sco"] and factory:find("sco") then
createOffloadScoNode(parent, id, type, factory, properties)
return
end
-- set the device id and spa factory name; REQUIRED, do not change
properties["device.id"] = parent["bound-id"]
properties["factory.name"] = factory
-- set the default pause-on-idle setting
properties["node.pause-on-idle"] = false
-- set the node description
local desc =
dev_props["device.description"]
or dev_props["device.name"]
or dev_props["device.nick"]
or dev_props["device.alias"]
or "bluetooth-device"
-- sanitize description, replace ':' with ' '
properties["node.description"] = desc:gsub("(:)", " ")
-- set the node name
local name =
((factory:find("sink") and "bluez_output") or
(factory:find("source") and "bluez_input" or factory)) .. "." ..
(properties["api.bluez5.address"] or dev_props["device.name"]) .. "." ..
tostring(id)
-- sanitize name
properties["node.name"] = name:gsub("([^%w_%-%.])", "_")
-- set priority
if not properties["priority.driver"] then
local priority = factory:find("source") and 2010 or 1010
properties["priority.driver"] = priority
properties["priority.session"] = priority
end
-- autoconnect if it's a stream
if properties["api.bluez5.profile"] == "headset-audio-gateway" or
properties["api.bluez5.profile"] == "bap-sink" or
factory:find("a2dp.source") or factory:find("media.source") then
properties["node.autoconnect"] = true
end
-- apply properties from config.rules
rulesApplyProperties(properties)
-- create the node; bluez requires "local" nodes, i.e. ones that run in
-- the same process as the spa device, for several reasons
if properties["api.bluez5.set.leader"] then
local combine = createSetNode(parent, id, type, factory, properties)
parent:store_managed_object(id + COMBINE_OFFSET, combine)
else
local node = LocalNode("adapter", properties)
node:activate(Feature.Proxy.BOUND)
parent:store_managed_object(id, node)
end
end
function removeNode(parent, id)
-- Clear also the device set module, if any
parent:store_managed_object(id + COMBINE_OFFSET, nil)
end
function createDevice(parent, id, type, factory, properties)
local device = parent:get_managed_object(id)
if not device then
-- ensure a proper device name
local name =
(properties["device.name"] or
properties["api.bluez5.address"] or
properties["device.description"] or
tostring(id)):gsub("([^%w_%-%.])", "_")
if not name:find("^bluez_card%.", 1) then
name = "bluez_card." .. name
end
properties["device.name"] = name
-- set the icon name
if not properties["device.icon-name"] then
local icon = nil
local icon_map = {
-- form factor -> icon
["microphone"] = "audio-input-microphone",
["webcam"] = "camera-web",
["handset"] = "phone",
["portable"] = "multimedia-player",
["tv"] = "video-display",
["headset"] = "audio-headset",
["headphone"] = "audio-headphones",
["speaker"] = "audio-speakers",
["hands-free"] = "audio-handsfree",
}
local f = properties["device.form-factor"]
local b = properties["device.bus"]
icon = icon_map[f] or "audio-card"
properties["device.icon-name"] = icon .. (b and ("-" .. b) or "")
end
-- initial profile is to be set by policy-device-profile.lua, not spa-bluez5
properties["bluez5.profile"] = "off"
properties["api.bluez5.id"] = id
-- apply properties from config.rules
rulesApplyProperties(properties)
-- create the device
device = SpaDevice(factory, properties)
if device then
device:connect("create-object", createNode)
device:connect("object-removed", removeNode)
parent:store_managed_object(id, device)
else
Log.warning ("Failed to create '" .. factory .. "' device")
return
end
end
Log.info(parent, string.format("%d, %s (%s): %s",
id, properties["device.description"],
properties["api.bluez5.address"], properties["api.bluez5.connection"]))
-- activate the device after the bluez profiles are connected
if properties["api.bluez5.connection"] == "connected" then
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
else
device:deactivate(Features.ALL)
end
end
function createMonitor()
local monitor_props = config.properties or {}
monitor_props["api.bluez5.connection-info"] = true
local monitor = SpaDevice("api.bluez5.enum.dbus", monitor_props)
if monitor then
monitor:connect("create-object", createDevice)
else
Log.message("PipeWire's BlueZ SPA missing or broken. Bluetooth not supported.")
return nil
end
monitor:activate(Feature.SpaDevice.ENABLED)
return monitor
end
logind_plugin = Plugin.find("logind")
if logind_plugin then
-- if logind support is enabled, activate
-- the monitor only when the seat is active
function startStopMonitor(seat_state)
Log.info(logind_plugin, "Seat state changed: " .. seat_state)
if seat_state == "active" then
monitor = createMonitor()
elseif monitor then
monitor:deactivate(Feature.SpaDevice.ENABLED)
monitor = nil
end
end
logind_plugin:connect("state-changed", function(p, s) startStopMonitor(s) end)
startStopMonitor(logind_plugin:call("get-state"))
else
monitor = createMonitor()
end
nodes_om:activate()
devices_om:activate()
device_set_nodes_om:activate()

View File

@ -0,0 +1,174 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
local config = ... or {}
-- preprocess rules and create Interest objects
for _, r in ipairs(config.rules or {}) do
r.interests = {}
for _, i in ipairs(r.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(r.interests, interest)
end
r.matches = nil
end
-- applies properties from config.rules when asked to
function rulesApplyProperties(properties)
for _, r in ipairs(config.rules or {}) do
if r.apply_properties then
for _, interest in ipairs(r.interests) do
if interest:matches(properties) then
for k, v in pairs(r.apply_properties) do
properties[k] = v
end
end
end
end
end
end
function findDuplicate(parent, id, property, value)
for i = 0, id - 1, 1 do
local obj = parent:get_managed_object(i)
if obj and obj.properties[property] == value then
return true
end
end
return false
end
function createNode(parent, id, type, factory, properties)
local dev_props = parent.properties
local location = properties["api.libcamera.location"]
-- set the device id and spa factory name; REQUIRED, do not change
properties["device.id"] = parent["bound-id"]
properties["factory.name"] = factory
-- set the default pause-on-idle setting
properties["node.pause-on-idle"] = false
-- set the node name
local name =
(factory:find("sink") and "libcamera_output") or
(factory:find("source") and "libcamera_input" or factory)
.. "." ..
(dev_props["device.name"]:gsub("^libcamera_device%.(.+)", "%1") or
dev_props["device.name"] or
dev_props["device.nick"] or
dev_props["device.alias"] or
"libcamera-device")
-- sanitize name
name = name:gsub("([^%w_%-%.])", "_")
properties["node.name"] = name
-- deduplicate nodes with the same name
for counter = 2, 99, 1 do
if findDuplicate(parent, id, "node.name", properties["node.name"]) then
properties["node.name"] = name .. "." .. counter
else
break
end
end
-- set the node description
local desc = dev_props["device.description"] or "libcamera-device"
if location == "front" then
desc = I18n.gettext("Built-in Front Camera")
elseif location == "back" then
desc = I18n.gettext("Built-in Back Camera")
end
-- sanitize description, replace ':' with ' '
properties["node.description"] = desc:gsub("(:)", " ")
-- set the node nick
local nick = properties["node.nick"] or
dev_props["device.product.name"] or
dev_props["device.description"] or
dev_props["device.nick"]
properties["node.nick"] = nick:gsub("(:)", " ")
-- set priority
if not properties["priority.session"] then
local priority = 700
if location == "external" then
priority = priority + 150
elseif location == "front" then
priority = priority + 100
elseif location == "back" then
priority = priority + 50
end
properties["priority.session"] = priority
end
-- apply properties from config.rules
rulesApplyProperties(properties)
if properties ["node.disabled"] then
return
end
-- create the node
local node = Node("spa-node-factory", properties)
node:activate(Feature.Proxy.BOUND)
parent:store_managed_object(id, node)
end
function createDevice(parent, id, type, factory, properties)
-- ensure the device has an appropriate name
local name = "libcamera_device." ..
(properties["device.name"] or
properties["device.bus-id"] or
properties["device.bus-path"] or
tostring(id)):gsub("([^%w_%-%.])", "_")
properties["device.name"] = name
-- deduplicate devices with the same name
for counter = 2, 99, 1 do
if findDuplicate(parent, id, "device.name", properties["device.name"]) then
properties["device.name"] = name .. "." .. counter
else
break
end
end
-- ensure the device has a description
properties["device.description"] =
properties["device.description"]
or properties["device.product.name"]
or "Unknown device"
-- apply properties from config.rules
rulesApplyProperties(properties)
if properties ["device.disabled"] then
return
end
-- create the device
local device = SpaDevice(factory, properties)
if device then
device:connect("create-object", createNode)
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
parent:store_managed_object(id, device)
else
Log.warning ("Failed to create '" .. factory .. "' device")
end
end
monitor = SpaDevice("api.libcamera.enum.manager", config.properties or {})
if monitor then
monitor:connect("create-object", createDevice)
monitor:activate(Feature.SpaDevice.ENABLED)
else
Log.message("PipeWire's libcamera SPA missing or broken. libcamera not supported.")
end

View File

@ -0,0 +1,165 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
local config = ... or {}
-- preprocess rules and create Interest objects
for _, r in ipairs(config.rules or {}) do
r.interests = {}
for _, i in ipairs(r.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(r.interests, interest)
end
r.matches = nil
end
-- applies properties from config.rules when asked to
function rulesApplyProperties(properties)
for _, r in ipairs(config.rules or {}) do
if r.apply_properties then
for _, interest in ipairs(r.interests) do
if interest:matches(properties) then
for k, v in pairs(r.apply_properties) do
properties[k] = v
end
end
end
end
end
end
function findDuplicate(parent, id, property, value)
for i = 0, id - 1, 1 do
local obj = parent:get_managed_object(i)
if obj and obj.properties[property] == value then
return true
end
end
return false
end
function createNode(parent, id, type, factory, properties)
local dev_props = parent.properties
-- set the device id and spa factory name; REQUIRED, do not change
properties["device.id"] = parent["bound-id"]
properties["factory.name"] = factory
-- set the default pause-on-idle setting
properties["node.pause-on-idle"] = false
-- set the node name
local name =
(factory:find("sink") and "v4l2_output") or
(factory:find("source") and "v4l2_input" or factory)
.. "." ..
(dev_props["device.name"]:gsub("^v4l2_device%.(.+)", "%1") or
dev_props["device.name"] or
dev_props["device.nick"] or
dev_props["device.alias"] or
"v4l2-device")
-- sanitize name
name = name:gsub("([^%w_%-%.])", "_")
properties["node.name"] = name
-- deduplicate nodes with the same name
for counter = 2, 99, 1 do
if findDuplicate(parent, id, "node.name", properties["node.name"]) then
properties["node.name"] = name .. "." .. counter
else
break
end
end
-- set the node description
local desc = dev_props["device.description"] or "v4l2-device"
desc = desc .. " (V4L2)"
-- sanitize description, replace ':' with ' '
properties["node.description"] = desc:gsub("(:)", " ")
-- set the node nick
local nick = properties["node.nick"] or
dev_props["device.product.name"] or
dev_props["api.v4l2.cap.card"] or
dev_props["device.description"] or
dev_props["device.nick"]
properties["node.nick"] = nick:gsub("(:)", " ")
-- set priority
if not properties["priority.session"] then
local path = properties["api.v4l2.path"] or "/dev/video100"
local dev = path:gsub("/dev/video(%d+)", "%1")
properties["priority.session"] = 1000 - (tonumber(dev) * 10)
end
-- apply properties from config.rules
rulesApplyProperties(properties)
if properties["node.disabled"] then
return
end
-- create the node
local node = Node("spa-node-factory", properties)
node:activate(Feature.Proxy.BOUND)
parent:store_managed_object(id, node)
end
function createDevice(parent, id, type, factory, properties)
-- ensure the device has an appropriate name
local name = "v4l2_device." ..
(properties["device.name"] or
properties["device.bus-id"] or
properties["device.bus-path"] or
tostring(id)):gsub("([^%w_%-%.])", "_")
properties["device.name"] = name
-- deduplicate devices with the same name
for counter = 2, 99, 1 do
if findDuplicate(parent, id, "device.name", properties["device.name"]) then
properties["device.name"] = name .. "." .. counter
else
break
end
end
-- ensure the device has a description
properties["device.description"] =
properties["device.description"]
or properties["device.product.name"]
or "Unknown device"
-- apply properties from config.rules
rulesApplyProperties(properties)
if properties["device.disabled"] then
return
end
-- create the device
local device = SpaDevice(factory, properties)
if device then
device:connect("create-object", createNode)
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
parent:store_managed_object(id, device)
else
Log.warning ("Failed to create '" .. factory .. "' device")
end
end
monitor = SpaDevice("api.v4l2.enum.udev", config.properties or {})
if monitor then
monitor:connect("create-object", createDevice)
monitor:activate(Feature.SpaDevice.ENABLED)
else
Log.message("PipeWire's V4L SPA missing or broken. Video4Linux not supported.")
end

View File

@ -0,0 +1,398 @@
-- WirePlumber
--
-- Copyright © 2021 Asymptotic Inc.
-- @author Sanchayan Maity <sanchayan@asymptotic.io>
--
-- Based on bt-profile-switch.lua in tests/examples
-- Copyright © 2021 George Kiagiadakis
--
-- Based on bluez-autoswitch in media-session
-- Copyright © 2021 Pauli Virtanen
--
-- SPDX-License-Identifier: MIT
--
-- Checks for the existence of media.role and if present switches the bluetooth
-- profile accordingly. Also see bluez-autoswitch in media-session.
-- The intended logic of the script is as follows.
--
-- When a stream comes in, if it has a Communication or phone role in PulseAudio
-- speak in props, we switch to the highest priority profile that has an Input
-- route available. The reason for this is that we may have microphone enabled
-- non-HFP codecs eg. Faststream.
-- We track the incoming streams with Communication role or the applications
-- specified which do not set the media.role correctly perhaps.
-- When a stream goes away if the list with which we track the streams above
-- is empty, then we revert back to the old profile.
local config = ...
local use_persistent_storage = config["use-persistent-storage"] or false
local applications = {}
local use_headset_profile = config["media-role.use-headset-profile"] or false
local profile_restore_timeout_msec = 2000
local INVALID = -1
local timeout_source = nil
local restore_timeout_source = nil
local state = use_persistent_storage and State("policy-bluetooth") or nil
local headset_profiles = state and state:load() or {}
local last_profiles = {}
local active_streams = {}
local previous_streams = {}
for _, value in ipairs(config["media-role.applications"] or {}) do
applications[value] = true
end
metadata_om = ObjectManager {
Interest {
type = "metadata",
Constraint { "metadata.name", "=", "default" },
}
}
devices_om = ObjectManager {
Interest {
type = "device",
Constraint { "device.api", "=", "bluez5" },
}
}
streams_om = ObjectManager {
Interest {
type = "node",
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
-- Do not consider monitor streams
Constraint { "stream.monitor", "!", "true" }
}
}
local function parseParam(param_to_parse, id)
local param = param_to_parse:parse()
if param.pod_type == "Object" and param.object_id == id then
return param.properties
else
return nil
end
end
local function storeAfterTimeout()
if not use_persistent_storage then
return
end
if timeout_source then
timeout_source:destroy()
end
timeout_source = Core.timeout_add(1000, function ()
local saved, err = state:save(headset_profiles)
if not saved then
Log.warning(err)
end
timeout_source = nil
end)
end
local function saveHeadsetProfile(device, profile_name)
local key = "saved-headset-profile:" .. device.properties["device.name"]
headset_profiles[key] = profile_name
storeAfterTimeout()
end
local function getSavedHeadsetProfile(device)
local key = "saved-headset-profile:" .. device.properties["device.name"]
return headset_profiles[key]
end
local function saveLastProfile(device, profile_name)
last_profiles[device.properties["device.name"]] = profile_name
end
local function getSavedLastProfile(device)
return last_profiles[device.properties["device.name"]]
end
local function isSwitched(device)
return getSavedLastProfile(device) ~= nil
end
local function isBluez5AudioSink(sink_name)
if sink_name and string.find(sink_name, "bluez_output.") ~= nil then
return true
end
return false
end
local function isBluez5DefaultAudioSink()
local metadata = metadata_om:lookup()
local default_audio_sink = metadata:find(0, "default.audio.sink")
return isBluez5AudioSink(default_audio_sink)
end
local function findProfile(device, index, name)
for p in device:iterate_params("EnumProfile") do
local profile = parseParam(p, "EnumProfile")
if not profile then
goto skip_enum_profile
end
Log.debug("Profile name: " .. profile.name .. ", priority: "
.. tostring(profile.priority) .. ", index: " .. tostring(profile.index))
if (index ~= nil and profile.index == index) or
(name ~= nil and profile.name == name) then
return profile.priority, profile.index, profile.name
end
::skip_enum_profile::
end
return INVALID, INVALID, nil
end
local function getCurrentProfile(device)
for p in device:iterate_params("Profile") do
local profile = parseParam(p, "Profile")
if profile then
return profile.name
end
end
return nil
end
local function highestPrioProfileWithInputRoute(device)
local profile_priority = INVALID
local profile_index = INVALID
local profile_name = nil
for p in device:iterate_params("EnumRoute") do
local route = parseParam(p, "EnumRoute")
-- Parse pod
if not route then
goto skip_enum_route
end
if route.direction ~= "Input" then
goto skip_enum_route
end
Log.debug("Route with index: " .. tostring(route.index) .. ", direction: "
.. route.direction .. ", name: " .. route.name .. ", description: "
.. route.description .. ", priority: " .. route.priority)
if route.profiles then
for _, v in pairs(route.profiles) do
local priority, index, name = findProfile(device, v)
if priority ~= INVALID then
if profile_priority < priority then
profile_priority = priority
profile_index = index
profile_name = name
end
end
end
end
::skip_enum_route::
end
return profile_priority, profile_index, profile_name
end
local function hasProfileInputRoute(device, profile_index)
for p in device:iterate_params("EnumRoute") do
local route = parseParam(p, "EnumRoute")
if route and route.direction == "Input" and route.profiles then
for _, v in pairs(route.profiles) do
if v == profile_index then
return true
end
end
end
end
return false
end
local function switchProfile()
local index
local name
if restore_timeout_source then
restore_timeout_source:destroy()
restore_timeout_source = nil
end
for device in devices_om:iterate() do
if isSwitched(device) then
goto skip_device
end
local cur_profile_name = getCurrentProfile(device)
saveLastProfile(device, cur_profile_name)
_, index, name = findProfile(device, nil, cur_profile_name)
if hasProfileInputRoute(device, index) then
Log.info("Current profile has input route, not switching")
goto skip_device
end
local saved_headset_profile = getSavedHeadsetProfile(device)
index = INVALID
if saved_headset_profile then
_, index, name = findProfile(device, nil, saved_headset_profile)
end
if index == INVALID then
_, index, name = highestPrioProfileWithInputRoute(device)
end
if index ~= INVALID then
local pod = Pod.Object {
"Spa:Pod:Object:Param:Profile", "Profile",
index = index
}
Log.info("Setting profile of '"
.. device.properties["device.description"]
.. "' from: " .. cur_profile_name
.. " to: " .. name)
device:set_params("Profile", pod)
else
Log.warning("Got invalid index when switching profile")
end
::skip_device::
end
end
local function restoreProfile()
for device in devices_om:iterate() do
if isSwitched(device) then
local profile_name = getSavedLastProfile(device)
local cur_profile_name = getCurrentProfile(device)
saveLastProfile(device, nil)
if cur_profile_name then
Log.info("Setting saved headset profile to: " .. cur_profile_name)
saveHeadsetProfile(device, cur_profile_name)
end
if profile_name then
local _, index, name = findProfile(device, nil, profile_name)
if index ~= INVALID then
local pod = Pod.Object {
"Spa:Pod:Object:Param:Profile", "Profile",
index = index
}
Log.info("Restoring profile of '"
.. device.properties["device.description"]
.. "' from: " .. cur_profile_name
.. " to: " .. name)
device:set_params("Profile", pod)
else
Log.warning("Failed to restore profile")
end
end
end
end
end
local function triggerRestoreProfile()
if restore_timeout_source then
return
end
if next(active_streams) ~= nil then
return
end
restore_timeout_source = Core.timeout_add(profile_restore_timeout_msec, function ()
restore_timeout_source = nil
restoreProfile()
end)
end
-- We consider a Stream of interest to have role Communication if it has
-- media.role set to Communication in props or it is in our list of
-- applications as these applications do not set media.role correctly or at
-- all.
local function checkStreamStatus(stream)
local app_name = stream.properties["application.name"]
local stream_role = stream.properties["media.role"]
if not (stream_role == "Communication" or applications[app_name]) then
return false
end
if not isBluez5DefaultAudioSink() then
return false
end
-- If a stream we previously saw stops running, we consider it
-- inactive, because some applications (Teams) just cork input
-- streams, but don't close them.
if previous_streams[stream["bound-id"]] and stream.state ~= "running" then
return false
end
return true
end
local function handleStream(stream)
if not use_headset_profile then
return
end
if checkStreamStatus(stream) then
active_streams[stream["bound-id"]] = true
previous_streams[stream["bound-id"]] = true
switchProfile()
else
active_streams[stream["bound-id"]] = nil
triggerRestoreProfile()
end
end
local function handleAllStreams()
for stream in streams_om:iterate {
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
Constraint { "stream.monitor", "!", "true" }
} do
handleStream(stream)
end
end
streams_om:connect("object-added", function (_, stream)
stream:connect("state-changed", function (stream, old_state, cur_state)
handleStream(stream)
end)
stream:connect("params-changed", handleStream)
handleStream(stream)
end)
streams_om:connect("object-removed", function (_, stream)
active_streams[stream["bound-id"]] = nil
previous_streams[stream["bound-id"]] = nil
triggerRestoreProfile()
end)
devices_om:connect("object-added", function (_, device)
-- Devices are unswitched initially
if isSwitched(device) then
saveLastProfile(device, nil)
end
handleAllStreams()
end)
metadata_om:connect("object-added", function (_, metadata)
metadata:connect("changed", function (m, subject, key, t, value)
if (use_headset_profile and subject == 0 and key == "default.audio.sink"
and isBluez5AudioSink(value)) then
-- If bluez sink is set as default, rescan for active input streams
handleAllStreams()
end
end)
end)
metadata_om:activate()
devices_om:activate()
streams_om:activate()

View File

@ -0,0 +1,246 @@
-- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
-- @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT
local self = {}
self.config = ... or {}
self.config.persistent = self.config.persistent or {}
self.config.priorities = self.config.priorities or {}
self.active_profiles = {}
self.default_profile_plugin = Plugin.find("default-profile")
function createIntrestObjects(t)
for _, p in ipairs(t or {}) do
p.interests = {}
for _, i in ipairs(p.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(p.interests, interest)
end
p.matches = nil
end
end
-- Preprocess persistent profiles and create Interest objects
createIntrestObjects(self.config.persistent)
-- Preprocess profile priorities and create Interest objects
createIntrestObjects(self.config.priorities)
-- Checks whether a device profile is persistent or not
function isProfilePersistent(device_props, profile_name)
for _, p in ipairs(self.config.persistent or {}) do
if p.profile_names then
for _, interest in ipairs(p.interests) do
if interest:matches(device_props) then
for _, pn in ipairs(p.profile_names) do
if pn == profile_name then
return true
end
end
end
end
end
end
return false
end
function parseParam(param, id)
local parsed = param:parse()
if parsed.pod_type == "Object" and parsed.object_id == id then
return parsed.properties
else
return nil
end
end
function setDeviceProfile (device, dev_id, dev_name, profile)
if self.active_profiles[dev_id] and
self.active_profiles[dev_id].index == profile.index then
Log.info ("Profile " .. profile.name .. " is already set in " .. dev_name)
return
end
local param = Pod.Object {
"Spa:Pod:Object:Param:Profile", "Profile",
index = profile.index,
}
Log.info ("Setting profile " .. profile.name .. " on " .. dev_name)
device:set_param("Profile", param)
end
function findDefaultProfile (device)
local def_name = nil
if self.default_profile_plugin ~= nil then
def_name = self.default_profile_plugin:call ("get-profile", device)
end
if def_name == nil then
return nil
end
for p in device:iterate_params("EnumProfile") do
local profile = parseParam(p, "EnumProfile")
if profile.name == def_name then
return profile
end
end
return nil
end
-- returns the priorities, if defined
function getDevicePriorities(device_props, profile_name)
for _, p in ipairs(self.config.priorities or {}) do
for _, interest in ipairs(p.interests) do
if interest:matches(device_props) then
return p.priorities
end
end
end
return nil
end
-- find profiles based on user preferences.
function findPreferredProfile(device)
local priority_table = getDevicePriorities(device.properties)
if not priority_table or #priority_table == 0 then
return nil
else
Log.info("priority table found for device " ..
device.properties["device.name"])
end
for _, priority_profile in ipairs(priority_table) do
for p in device:iterate_params("EnumProfile") do
device_profile = parseParam(p, "EnumProfile")
if device_profile.name == priority_profile then
Log.info("Selected user preferred profile " ..
device_profile.name .. " for " .. device.properties["device.name"])
return device_profile
end
end
end
return nil
end
-- find profiles based on inbuilt priorities.
function findBestProfile(device)
-- Takes absolute priority if available or unknown
local profile_prop = device.properties["device.profile"]
local off_profile = nil
local best_profile = nil
local unk_profile = nil
local profile = nil
for p in device:iterate_params("EnumProfile") do
profile = parseParam(p, "EnumProfile")
if profile and profile.name == profile_prop and profile.available ~= "no" then
return profile
elseif profile and profile.name ~= "pro-audio" then
if profile.name == "off" then
off_profile = profile
elseif profile.available == "yes" then
if best_profile == nil or profile.priority > best_profile.priority then
best_profile = profile
end
elseif profile.available ~= "no" then
if unk_profile == nil or profile.priority > unk_profile.priority then
unk_profile = profile
end
end
end
end
if best_profile ~= nil then
profile = best_profile
elseif unk_profile ~= nil then
profile = unk_profile
elseif off_profile ~= nil then
profile = off_profile
end
if profile ~= nil then
Log.info("Found best profile " .. profile.name .. " for " .. device.properties["device.name"])
return profile
else
return nil
end
end
function handleProfiles (device, new_device)
local dev_id = device["bound-id"]
local dev_name = device.properties["device.name"]
local def_profile = findDefaultProfile (device)
-- Do not do anything if active profile is both persistent and default
if not new_device and
self.active_profiles[dev_id] ~= nil and
isProfilePersistent (device.properties, self.active_profiles[dev_id].name) and
def_profile ~= nil and
self.active_profiles[dev_id].name == def_profile.name
then
local active_profile = self.active_profiles[dev_id].name
Log.info ("Device profile " .. active_profile .. " is persistent for " .. dev_name)
return
end
if def_profile ~= nil then
if def_profile.available == "no" then
Log.info ("Default profile " .. def_profile.name .. " unavailable for " .. dev_name)
else
Log.info ("Found default profile " .. def_profile.name .. " for " .. dev_name)
setDeviceProfile (device, dev_id, dev_name, def_profile)
return
end
else
Log.info ("Default profile not found for " .. dev_name)
end
local best_profile = findPreferredProfile(device)
if not best_profile then
best_profile = findBestProfile(device)
end
if best_profile ~= nil then
setDeviceProfile (device, dev_id, dev_name, best_profile)
else
Log.info ("Best profile not found on " .. dev_name)
end
end
function onDeviceParamsChanged (device, param_name)
if param_name == "EnumProfile" then
handleProfiles (device, false)
end
end
self.om = ObjectManager {
Interest {
type = "device",
Constraint { "device.name", "is-present", type = "pw-global" },
}
}
self.om:connect("object-added", function (_, device)
device:connect ("params-changed", onDeviceParamsChanged)
handleProfiles (device, true)
end)
self.om:connect("object-removed", function (_, device)
local dev_id = device["bound-id"]
self.active_profiles[dev_id] = nil
end)
self.om:activate()

View File

@ -0,0 +1,487 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- Based on default-routes.c from pipewire-media-session
-- Copyright © 2020 Wim Taymans
--
-- SPDX-License-Identifier: MIT
local config = ... or {}
-- whether to store state on the file system
use_persistent_storage = config["use-persistent-storage"] or false
-- the default volume to apply
default_volume = tonumber(config["default-volume"] or 0.4^3)
default_input_volume = tonumber(config["default-input-volume"] or 1.0)
-- table of device info
dev_infos = {}
-- the state storage
state = use_persistent_storage and State("default-routes") or nil
state_table = state and state:load() or {}
-- simple serializer {"foo", "bar"} -> "foo;bar;"
function serializeArray(a)
local str = ""
for _, v in ipairs(a) do
str = str .. tostring(v):gsub(";", "\\;") .. ";"
end
return str
end
-- simple deserializer "foo;bar;" -> {"foo", "bar"}
function parseArray(str, convert_value)
local array = {}
local val = ""
local escaped = false
for i = 1, #str do
local c = str:sub(i,i)
if c == '\\' then
escaped = true
elseif c == ';' and not escaped then
val = convert_value and convert_value(val) or val
table.insert(array, val)
val = ""
else
val = val .. tostring(c)
escaped = false
end
end
return array
end
function arrayContains(a, value)
for _, v in ipairs(a) do
if v == value then
return true
end
end
return false
end
function parseParam(param, id)
local route = param:parse()
if route.pod_type == "Object" and route.object_id == id then
return route.properties
else
return nil
end
end
function storeAfterTimeout()
if timeout_source then
timeout_source:destroy()
end
timeout_source = Core.timeout_add(1000, function ()
local saved, err = state:save(state_table)
if not saved then
Log.warning(err)
end
timeout_source = nil
end)
end
function saveProfile(dev_info, profile_name)
if not use_persistent_storage then
return
end
local routes = {}
for idx, ri in pairs(dev_info.route_infos) do
if ri.save then
table.insert(routes, ri.name)
end
end
if #routes > 0 then
local key = dev_info.name .. ":profile:" .. profile_name
state_table[key] = serializeArray(routes)
storeAfterTimeout()
end
end
function saveRouteProps(dev_info, route)
if not use_persistent_storage or not route.props then
return
end
local props = route.props.properties
local key_base = dev_info.name .. ":" ..
route.direction:lower() .. ":" ..
route.name .. ":"
state_table[key_base .. "volume"] =
props.volume and tostring(props.volume) or nil
state_table[key_base .. "mute"] =
props.mute and tostring(props.mute) or nil
state_table[key_base .. "channelVolumes"] =
props.channelVolumes and serializeArray(props.channelVolumes) or nil
state_table[key_base .. "channelMap"] =
props.channelMap and serializeArray(props.channelMap) or nil
state_table[key_base .. "latencyOffsetNsec"] =
props.latencyOffsetNsec and tostring(props.latencyOffsetNsec) or nil
state_table[key_base .. "iec958Codecs"] =
props.iec958Codecs and serializeArray(props.iec958Codecs) or nil
storeAfterTimeout()
end
function restoreRoute(device, dev_info, device_id, route)
-- default props
local props = {
"Spa:Pod:Object:Param:Props", "Route",
mute = false,
}
if route.direction == "Input" then
props.channelVolumes = { default_input_volume }
else
props.channelVolumes = { default_volume }
end
-- restore props from persistent storage
if use_persistent_storage then
local key_base = dev_info.name .. ":" ..
route.direction:lower() .. ":" ..
route.name .. ":"
local str = state_table[key_base .. "volume"]
props.volume = str and tonumber(str) or props.volume
local str = state_table[key_base .. "mute"]
props.mute = str and (str == "true") or false
local str = state_table[key_base .. "channelVolumes"]
props.channelVolumes = str and parseArray(str, tonumber) or props.channelVolumes
local str = state_table[key_base .. "channelMap"]
props.channelMap = str and parseArray(str) or props.channelMap
local str = state_table[key_base .. "latencyOffsetNsec"]
props.latencyOffsetNsec = str and math.tointeger(str) or props.latencyOffsetNsec
local str = state_table[key_base .. "iec958Codecs"]
props.iec958Codecs = str and parseArray(str) or props.iec958Codecs
end
-- convert arrays to Spa Pod
if props.channelVolumes then
table.insert(props.channelVolumes, 1, "Spa:Float")
props.channelVolumes = Pod.Array(props.channelVolumes)
end
if props.channelMap then
table.insert(props.channelMap, 1, "Spa:Enum:AudioChannel")
props.channelMap = Pod.Array(props.channelMap)
end
if props.iec958Codecs then
table.insert(props.iec958Codecs, 1, "Spa:Enum:AudioIEC958Codec")
props.iec958Codecs = Pod.Array(props.iec958Codecs)
end
-- construct Route param
local param = Pod.Object {
"Spa:Pod:Object:Param:Route", "Route",
index = route.index,
device = device_id,
props = Pod.Object(props),
save = route.save,
}
Log.debug(param, "setting route on " .. tostring(device))
device:set_param("Route", param)
route.prev_active = true
route.active = true
end
function findActiveDeviceIDs(profile)
-- parses the classes from the profile and returns the device IDs
----- sample structure, should return { 0, 8 } -----
-- classes:
-- 1: 2
-- 2:
-- 1: Audio/Source
-- 2: 1
-- 3: card.profile.devices
-- 4:
-- 1: 0
-- pod_type: Array
-- value_type: Spa:Int
-- pod_type: Struct
-- 3:
-- 1: Audio/Sink
-- 2: 1
-- 3: card.profile.devices
-- 4:
-- 1: 8
-- pod_type: Array
-- value_type: Spa:Int
-- pod_type: Struct
-- pod_type: Struct
local active_ids = {}
if type(profile.classes) == "table" and profile.classes.pod_type == "Struct" then
for _, p in ipairs(profile.classes) do
if type(p) == "table" and p.pod_type == "Struct" then
local i = 1
while true do
local k, v = p[i], p[i+1]
i = i + 2
if not k or not v then
break
end
if k == "card.profile.devices" and
type(v) == "table" and v.pod_type == "Array" then
for _, dev_id in ipairs(v) do
table.insert(active_ids, dev_id)
end
end
end
end
end
end
return active_ids
end
-- returns an array of the route names that were previously selected
-- for the given device and profile
function getStoredProfileRoutes(dev_name, profile_name)
local key = dev_name .. ":profile:" .. profile_name
local str = state_table[key]
return str and parseArray(str) or {}
end
-- find a route that was previously stored for a device_id
-- spr needs to be the array returned from getStoredProfileRoutes()
function findSavedRoute(dev_info, device_id, spr)
for idx, ri in pairs(dev_info.route_infos) do
if arrayContains(ri.devices, device_id) and
(ri.profiles == nil or arrayContains(ri.profiles, dev_info.active_profile)) and
arrayContains(spr, ri.name) then
return ri
end
end
return nil
end
-- find the best route for a given device_id, based on availability and priority
function findBestRoute(dev_info, device_id)
local best_avail = nil
local best_unk = nil
for idx, ri in pairs(dev_info.route_infos) do
if arrayContains(ri.devices, device_id) and
(ri.profiles == nil or arrayContains(ri.profiles, dev_info.active_profile)) then
if ri.available == "yes" or ri.available == "unknown" then
if ri.direction == "Output" and ri.available ~= ri.prev_available then
best_avail = ri
ri.save = true
break
elseif ri.available == "yes" then
if (best_avail == nil or ri.priority > best_avail.priority) then
best_avail = ri
end
elseif best_unk == nil or ri.priority > best_unk.priority then
best_unk = ri
end
end
end
end
return best_avail or best_unk
end
function restoreProfileRoutes(device, dev_info, profile, profile_changed)
Log.info(device, "restore routes for profile " .. profile.name)
local active_ids = findActiveDeviceIDs(profile)
local spr = getStoredProfileRoutes(dev_info.name, profile.name)
for _, device_id in ipairs(active_ids) do
Log.info(device, "restoring device " .. device_id);
local route = nil
-- restore routes selection for the newly selected profile
-- don't bother if spr is empty, there is no point
if profile_changed and #spr > 0 then
route = findSavedRoute(dev_info, device_id, spr)
if route then
-- we found a saved route
if route.available == "no" then
Log.info(device, "saved route '" .. route.name .. "' not available")
-- not available, try to find next best
route = nil
else
Log.info(device, "found saved route: " .. route.name)
-- make sure we save it again
route.save = true
end
end
end
-- we could not find a saved route, try to find a new best
if not route then
route = findBestRoute(dev_info, device_id)
if not route then
Log.info(device, "can't find best route")
else
Log.info(device, "found best route: " .. route.name)
end
end
-- restore route
if route then
restoreRoute(device, dev_info, device_id, route)
end
end
end
function findRouteInfo(dev_info, route, return_new)
local ri = dev_info.route_infos[route.index]
if not ri and return_new then
ri = {
index = route.index,
name = route.name,
direction = route.direction,
devices = route.devices or {},
profiles = route.profiles,
priority = route.priority or 0,
available = route.available or "unknown",
prev_available = route.available or "unknown",
active = false,
prev_active = false,
save = false,
}
end
return ri
end
function handleDevice(device)
local dev_info = dev_infos[device["bound-id"]]
local new_route_infos = {}
local avail_routes_changed = false
local profile = nil
-- get current profile
for p in device:iterate_params("Profile") do
profile = parseParam(p, "Profile")
end
-- look at all the routes and update/reset cached information
for p in device:iterate_params("EnumRoute") do
-- parse pod
local route = parseParam(p, "EnumRoute")
if not route then
goto skip_enum_route
end
-- find cached route information
local route_info = findRouteInfo(dev_info, route, true)
-- update properties
route_info.prev_available = route_info.available
if route_info.available ~= route.available then
Log.info(device, "route " .. route.name .. " available changed " ..
route_info.available .. " -> " .. route.available)
route_info.available = route.available
if profile and arrayContains(route.profiles, profile.index) then
avail_routes_changed = true
end
end
route_info.prev_active = route_info.active
route_info.active = false
route_info.save = false
-- store
new_route_infos[route.index] = route_info
::skip_enum_route::
end
-- replace old route_infos to lose old routes
-- that no longer exist on the device
dev_info.route_infos = new_route_infos
new_route_infos = nil
-- check for changes in the active routes
for p in device:iterate_params("Route") do
local route = parseParam(p, "Route")
if not route then
goto skip_route
end
-- get cached route info and at the same time
-- ensure that the route is also in EnumRoute
local route_info = findRouteInfo(dev_info, route, false)
if not route_info then
goto skip_route
end
-- update state
route_info.active = true
route_info.save = route.save
if not route_info.prev_active then
-- a new route is now active, restore the volume and
-- make sure we save this as a preferred route
Log.info(device, "new active route found " .. route.name)
restoreRoute(device, dev_info, route.device, route_info)
elseif route.save then
-- just save route properties
Log.info(device, "storing route props for " .. route.name)
saveRouteProps(dev_info, route)
end
::skip_route::
end
-- restore routes for profile
if profile then
local profile_changed = (dev_info.active_profile ~= profile.index)
-- if the profile changed, restore routes for that profile
-- if any of the routes of the current profile changed in availability,
-- then try to select a new "best" route for each device and ignore
-- what was stored
if profile_changed or avail_routes_changed then
dev_info.active_profile = profile.index
restoreProfileRoutes(device, dev_info, profile, profile_changed)
end
saveProfile(dev_info, profile.name)
end
end
om = ObjectManager {
Interest {
type = "device",
Constraint { "device.name", "is-present", type = "pw-global" },
}
}
om:connect("objects-changed", function (om)
local new_dev_infos = {}
for device in om:iterate() do
local dev_info = dev_infos[device["bound-id"]]
-- new device appeared
if not dev_info then
dev_info = {
name = device.properties["device.name"],
active_profile = -1,
route_infos = {},
}
dev_infos[device["bound-id"]] = dev_info
device:connect("params-changed", handleDevice)
handleDevice(device)
end
new_dev_infos[device["bound-id"]] = dev_info
end
-- replace list to get rid of dev_info for devices that no longer exist
dev_infos = new_dev_infos
end)
om:activate()

View File

@ -0,0 +1,86 @@
-- WirePlumber
--
-- Copyright © 2022-2023 The WirePlumber project contributors
-- @author Dmitry Sharshakov <d3dx12.xx@gmail.com>
--
-- SPDX-License-Identifier: MIT
local config = ... or {}
config.rules = config.rules or {}
for _, r in ipairs(config.rules) do
r.interests = {}
for _, i in ipairs(r.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(r.interests, interest)
end
end
-- TODO: only check for hotplug of nodes with known DSP rules
nodes_om = ObjectManager {
Interest { type = "node" },
}
clients_om = ObjectManager {
Interest { type = "client" }
}
filter_chains = {}
hidden_nodes = {}
nodes_om:connect("object-added", function (om, node)
for _, r in ipairs(config.rules or {}) do
for _, interest in ipairs(r.interests) do
if interest:matches(node["global-properties"]) then
local id = node["global-properties"]["object.id"]
if r.filter_chain then
if filter_chains[id] then
Log.warning("Sink " .. id .. " has been plugged now, but has a filter chain loaded. Skipping")
else
filter_chains[id] = LocalModule("libpipewire-module-filter-chain", r.filter_chain, {}, true)
end
end
if r.hide_parent then
Log.debug("Hiding node " .. node["bound-id"] .. " from clients")
for client in clients_om:iterate { type = "client" } do
if not client["properties"]["wireplumber.daemon"] then
client:update_permissions { [node["bound-id"]] = "-" }
end
end
hidden_nodes[node["bound-id"]] = id
end
end
end
end
end)
nodes_om:connect("object-removed", function (om, node)
local id = node["global-properties"]["object.id"]
if filter_chains[id] then
Log.debug("Unloading filter chain associated with sink " .. id)
filter_chains[id] = nil
else
Log.debug("Disconnected sink " .. id .. " does not have any filters to be removed")
end
end)
clients_om:connect("object-added", function (om, client)
for id, _ in pairs(hidden_nodes) do
if not client["properties"]["wireplumber.daemon"] then
client:update_permissions { [id] = "-" }
end
end
end)
nodes_om:activate()
clients_om:activate()

View File

@ -0,0 +1,218 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
local config = ... or {}
config.roles = config.roles or {}
config["duck.level"] = config["duck.level"] or 0.3
function findRole(role)
if role and not config.roles[role] then
for r, p in pairs(config.roles) do
if type(p.alias) == "table" then
for i = 1, #(p.alias), 1 do
if role == p.alias[i] then
return r
end
end
end
end
end
return role
end
function priorityForRole(role)
local r = role and config.roles[role] or nil
return r and r.priority or 0
end
function getAction(dominant_role, other_role)
-- default to "mix" if the role is not configured
if not dominant_role or not config.roles[dominant_role] then
return "mix"
end
local role_config = config.roles[dominant_role]
return role_config["action." .. other_role]
or role_config["action.default"]
or "mix"
end
function restoreVolume(role, media_class)
if not mixer_api then return end
local ep = endpoints_om:lookup {
Constraint { "media.role", "=", role, type = "pw" },
Constraint { "media.class", "=", media_class, type = "pw" },
}
if ep and ep.properties["node.id"] then
Log.debug(ep, "restore role " .. role)
mixer_api:call("set-volume", ep.properties["node.id"], {
monitorVolume = 1.0,
})
end
end
function duck(role, media_class)
if not mixer_api then return end
local ep = endpoints_om:lookup {
Constraint { "media.role", "=", role, type = "pw" },
Constraint { "media.class", "=", media_class, type = "pw" },
}
if ep and ep.properties["node.id"] then
Log.debug(ep, "duck role " .. role)
mixer_api:call("set-volume", ep.properties["node.id"], {
monitorVolume = config["duck.level"],
})
end
end
function getSuspendPlaybackMetadata ()
local suspend = false
local metadata = metadata_om:lookup()
if metadata then
local value = metadata:find(0, "suspend.playback")
if value then
suspend = value == "1" and true or false
end
end
return suspend
end
function rescan()
local links = {
["Audio/Source"] = {},
["Audio/Sink"] = {},
["Video/Source"] = {},
}
Log.info("Rescan endpoint links")
-- deactivate all links if suspend playback metadata is present
local suspend = getSuspendPlaybackMetadata()
for silink in silinks_om:iterate() do
if suspend then
silink:deactivate(Feature.SessionItem.ACTIVE)
end
end
-- gather info about links
for silink in silinks_om:iterate() do
local props = silink.properties
local role = props["media.role"]
local target_class = props["target.media.class"]
local plugged = props["item.plugged.usec"]
local active =
((silink:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0)
if links[target_class] then
table.insert(links[target_class], {
silink = silink,
role = findRole(role),
active = active,
priority = priorityForRole(role),
plugged = plugged and tonumber(plugged) or 0
})
end
end
local function compareLinks(l1, l2)
return (l1.priority > l2.priority) or
((l1.priority == l2.priority) and (l1.plugged > l2.plugged))
end
for media_class, v in pairs(links) do
-- sort on priority and stream creation time
table.sort(v, compareLinks)
-- apply actions
local first_link = v[1]
if first_link then
for i = 2, #v, 1 do
local action = getAction(first_link.role, v[i].role)
if action == "cork" then
if v[i].active then
v[i].silink:deactivate(Feature.SessionItem.ACTIVE)
end
elseif action == "mix" then
if not v[i].active and not suspend then
v[i].silink:activate(Feature.SessionItem.ACTIVE, pendingOperation())
end
restoreVolume(v[i].role, media_class)
elseif action == "duck" then
if not v[i].active and not suspend then
v[i].silink:activate(Feature.SessionItem.ACTIVE, pendingOperation())
end
duck(v[i].role, media_class)
else
Log.warning("Unknown action: " .. action)
end
end
if not first_link.active and not suspend then
first_link.silink:activate(Feature.SessionItem.ACTIVE, pendingOperation())
end
restoreVolume(first_link.role, media_class)
end
end
end
pending_ops = 0
pending_rescan = false
function pendingOperation()
pending_ops = pending_ops + 1
return function()
pending_ops = pending_ops - 1
if pending_ops == 0 and pending_rescan then
pending_rescan = false
rescan()
end
end
end
function maybeRescan()
if pending_ops == 0 then
rescan()
else
pending_rescan = true
end
end
silinks_om = ObjectManager {
Interest {
type = "SiLink",
Constraint { "is.policy.endpoint.client.link", "=", true },
},
}
silinks_om:connect("objects-changed", maybeRescan)
silinks_om:activate()
-- enable ducking if mixer-api is loaded
mixer_api = Plugin.find("mixer-api")
if mixer_api then
endpoints_om = ObjectManager {
Interest { type = "endpoint" },
}
endpoints_om:activate()
end
metadata_om = ObjectManager {
Interest {
type = "metadata",
Constraint { "metadata.name", "=", "default" },
}
}
metadata_om:connect("object-added", function (om, metadata)
metadata:connect("changed", function (m, subject, key, t, value)
if key == "suspend.playback" then
maybeRescan()
end
end)
end)
metadata_om:activate()

View File

@ -0,0 +1,261 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local config = ... or {}
config.roles = config.roles or {}
local self = {}
self.scanning = false
self.pending_rescan = false
function rescan ()
for si in linkables_om:iterate() do
handleLinkable (si)
end
end
function scheduleRescan ()
if self.scanning then
self.pending_rescan = true
return
end
self.scanning = true
rescan ()
self.scanning = false
if self.pending_rescan then
self.pending_rescan = false
Core.sync(function ()
scheduleRescan ()
end)
end
end
function findRole(role, tmc)
if role and not config.roles[role] then
-- find the role with matching alias
for r, p in pairs(config.roles) do
-- default media class can be overridden in the role config data
mc = p["media.class"] or "Audio/Sink"
if (type(p.alias) == "table" and tmc == mc) then
for i = 1, #(p.alias), 1 do
if role == p.alias[i] then
return r
end
end
end
end
-- otherwise get the lowest priority role
local lowest_priority_p = nil
local lowest_priority_r = nil
for r, p in pairs(config.roles) do
mc = p["media.class"] or "Audio/Sink"
if tmc == mc and (lowest_priority_p == nil or
p.priority < lowest_priority_p.priority) then
lowest_priority_p = p
lowest_priority_r = r
end
end
return lowest_priority_r
end
return role
end
function findTargetEndpoint (node, media_class, role)
local target_class_assoc = {
["Stream/Input/Audio"] = "Audio/Source",
["Stream/Output/Audio"] = "Audio/Sink",
["Stream/Input/Video"] = "Video/Source",
}
local media_role = nil
local highest_priority = -1
local target = nil
-- get target media class
local target_media_class = target_class_assoc[media_class]
if not target_media_class then
return nil
end
-- find highest priority endpoint by role
media_role = findRole(role, target_media_class)
for si_target_ep in endpoints_om:iterate {
Constraint { "role", "=", media_role, type = "pw-global" },
Constraint { "media.class", "=", target_media_class, type = "pw-global" },
} do
local priority = tonumber(si_target_ep.properties["priority"])
if priority > highest_priority then
highest_priority = priority
target = si_target_ep
end
end
return target
end
function createLink (si, si_target_ep)
local out_item = nil
local in_item = nil
local si_props = si.properties
local target_ep_props = si_target_ep.properties
if si_props["item.node.direction"] == "output" then
-- playback
out_item = si
in_item = si_target_ep
else
-- capture
out_item = si_target_ep
in_item = si
end
Log.info (string.format("link %s <-> %s",
tostring(si_props["node.name"]),
tostring(target_ep_props["name"])))
-- create and configure link
local si_link = SessionItem ( "si-standard-link" )
if not si_link:configure {
["out.item"] = out_item,
["in.item"] = in_item,
["out.item.port.context"] = "output",
["in.item.port.context"] = "input",
["is.policy.endpoint.client.link"] = true,
["media.role"] = target_ep_props["role"],
["target.media.class"] = target_ep_props["media.class"],
["item.plugged.usec"] = si_props["item.plugged.usec"],
} then
Log.warning (si_link, "failed to configure si-standard-link")
return
end
-- register
si_link:register()
end
function checkLinkable (si)
-- only handle session items that has a node associated proxy
local node = si:get_associated_proxy ("node")
if not node or not node.properties then
return false
end
-- only handle stream session items
local media_class = node.properties["media.class"]
if not media_class or not string.find (media_class, "Stream") then
return false
end
-- Determine if we can handle item by this policy
if endpoints_om:get_n_objects () == 0 then
Log.debug (si, "item won't be handled by this policy")
return false
end
return true
end
function handleLinkable (si)
if not checkLinkable (si) then
return
end
local node = si:get_associated_proxy ("node")
local media_class = node.properties["media.class"] or ""
local media_role = node.properties["media.role"] or "Default"
Log.info (si, "handling item " .. tostring(node.properties["node.name"]) ..
" with role " .. media_role)
-- find proper target endpoint
local si_target_ep = findTargetEndpoint (node, media_class, media_role)
if not si_target_ep then
Log.info (si, "... target endpoint not found")
return
end
-- Check if item is linked to proper target, otherwise re-link
for link in links_om:iterate() do
local out_id = tonumber(link.properties["out.item.id"])
local in_id = tonumber(link.properties["in.item.id"])
if out_id == si.id or in_id == si.id then
local is_out = out_id == si.id and true or false
for peer_ep in endpoints_om:iterate() do
if peer_ep.id == (is_out and in_id or out_id) then
if peer_ep.id == si_target_ep.id then
Log.info (si, "... already linked to proper target endpoint")
return
end
-- remove old link if active, otherwise schedule rescan
if ((link:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0) then
link:remove ()
Log.info (si, "... moving to new target")
else
scheduleRescan ()
Log.info (si, "... scheduled rescan")
return
end
end
end
end
end
-- create new link
createLink (si, si_target_ep)
end
function unhandleLinkable (si)
if not checkLinkable (si) then
return
end
local node = si:get_associated_proxy ("node")
Log.info (si, "unhandling item " .. tostring(node.properties["node.name"]))
-- remove any links associated with this item
for silink in links_om:iterate() do
local out_id = tonumber (silink.properties["out.item.id"])
local in_id = tonumber (silink.properties["in.item.id"])
if out_id == si.id or in_id == si.id then
silink:remove ()
Log.info (silink, "... link removed")
end
end
end
endpoints_om = ObjectManager { Interest { type = "SiEndpoint" }}
linkables_om = ObjectManager { Interest { type = "SiLinkable",
-- only handle si-audio-adapter and si-node
Constraint {
"item.factory.name", "=", "si-audio-adapter", type = "pw-global" },
Constraint {
"active-features", "!", 0, type = "gobject" },
Constraint {
"node.link-group", "-" },
}
}
links_om = ObjectManager { Interest { type = "SiLink",
-- only handle links created by this policy
Constraint { "is.policy.endpoint.client.link", "=", true, type = "pw-global" },
} }
linkables_om:connect("objects-changed", function (om)
scheduleRescan ()
end)
linkables_om:connect("object-removed", function (om, si)
unhandleLinkable (si)
end)
endpoints_om:activate()
linkables_om:activate()
links_om:activate()

View File

@ -0,0 +1,285 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local config = ... or {}
-- ensure config.move and config.follow are not nil
config.move = config.move or false
config.follow = config.follow or false
local self = {}
self.scanning = false
self.pending_rescan = false
function rescan ()
-- check endpoints and register new links
for si_ep in endpoints_om:iterate() do
handleLinkable(si_ep)
end
for filter in streams_om:iterate {
Constraint { "node.link-group", "+" },
} do
handleFilter(filter)
end
end
function scheduleRescan ()
if self.scanning then
self.pending_rescan = true
return
end
self.scanning = true
rescan ()
self.scanning = false
if self.pending_rescan then
self.pending_rescan = false
Core.sync(function ()
scheduleRescan ()
end)
end
end
function findFilterTarget (props)
for si_target in linkables_om:iterate {
-- exclude filter targets
Constraint { "node.link-group", "+" },
} do
local si_props = si_target.properties
if si_props["target.endpoint"] and si_props["target.endpoint"] == props["name"] then
return si_target
end
end
end
function findTargetByDefaultNode (target_media_class)
local def_id = default_nodes:call("get-default-node", target_media_class)
if def_id ~= Id.INVALID then
for si_target in linkables_om:iterate {
-- exclude filter targets
Constraint { "node.link-group", "-" },
} do
local target_node = si_target:get_associated_proxy ("node")
if target_node["bound-id"] == def_id then
return si_target
end
end
end
return nil
end
function findTargetByFirstAvailable (target_media_class)
for si_target in linkables_om:iterate {
-- exclude filter targets
Constraint { "node.link-group", "-" },
} do
local target_node = si_target:get_associated_proxy ("node")
if target_node.properties["media.class"] == target_media_class then
return si_target
end
end
return nil
end
function findUndefinedTarget (si_ep)
local media_class = si_ep.properties["media.class"]
local target_class_assoc = {
["Audio/Source"] = "Audio/Source",
["Stream/Output/Audio"] = "Audio/Sink",
["Audio/Sink"] = "Audio/Sink",
["Video/Source"] = "Video/Source",
}
local target_media_class = target_class_assoc[media_class]
if not target_media_class then
return nil
end
local si_target = findFilterTarget (si_ep.properties)
if not si_target then
si_target = findTargetByDefaultNode (target_media_class)
end
if not si_target then
si_target = findTargetByFirstAvailable (target_media_class)
end
return si_target
end
function createLink (si_ep, si_target)
local out_item = nil
local in_item = nil
local ep_props = si_ep.properties
local target_props = si_target.properties
if target_props["item.node.direction"] == "input" then
-- playback
out_item = si_ep
in_item = si_target
else
-- capture
in_item = si_ep
out_item = si_target
end
local link_string = string.format("link %s <-> %s ",
(is_filter and ep_props["node.name"] or ep_props["name"]),
target_props["node.name"])
Log.info(si_link, link_string)
-- create and configure link
local si_link = SessionItem ( "si-standard-link" )
if not si_link:configure {
["out.item"] = out_item,
["in.item"] = in_item,
["out.item.port.context"] = "output",
["in.item.port.context"] = "input",
["is.policy.endpoint.device.link"] = true,
} then
Log.warning (si_link, "failed to configure si-standard-link")
return
end
-- register
si_link:register ()
Log.info (si_link, " activating link " .. link_string)
-- activate
si_link:activate (Feature.SessionItem.ACTIVE, function (l, e)
if e then
Log.warning (l, "failed to activate link: " .. link_string .. tostring(e))
l:remove ()
else
Log.info (l, "activated link " .. link_string)
end
end)
end
function handleFilter(filter)
handleLinkable(filter)
end
function handleLinkable (si)
local si_props = si.properties
local is_filter = (si_props["node.link-group"] ~= nil)
if is_filter then
Log.info (si, "handling filter " .. si_props["node.name"])
else
Log.info (si, "handling endpoint " .. si_props["name"])
end
-- find proper target item
local si_target = findUndefinedTarget (si)
if not si_target then
Log.info (si, "... target item not found")
return
end
-- Check if item is linked to proper target, otherwise re-link
for link in links_om:iterate() do
local out_id = tonumber(link.properties["out.item.id"])
local in_id = tonumber(link.properties["in.item.id"])
if out_id == si.id or in_id == si.id then
local is_out = out_id == si.id and true or false
for peer in linkables_om:iterate() do
if peer.id == (is_out and in_id or out_id) then
if peer.id == si_target.id then
Log.info (si, "... already linked to proper target")
return
end
-- remove old link if active, otherwise schedule rescan
if ((link:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0) then
link:remove ()
Log.info (si, "... moving to new target")
else
scheduleRescan ()
Log.info (si, "... scheduled rescan")
return
end
end
end
end
end
-- create new link
createLink (si, si_target)
end
function unhandleLinkable (si)
si_props = si.properties
Log.info (si, string.format("unhandling item: %s (%s)",
tostring(si_props["node.name"]), tostring(si_props["node.id"])))
-- remove any links associated with this item
for silink in links_om:iterate() do
local out_id = tonumber (silink.properties["out.item.id"])
local in_id = tonumber (silink.properties["in.item.id"])
if out_id == si.id or in_id == si.id then
silink:remove ()
Log.info (silink, "... link removed")
end
end
end
default_nodes = Plugin.find("default-nodes-api")
endpoints_om = ObjectManager { Interest { type = "SiEndpoint" }}
linkables_om = ObjectManager {
Interest {
type = "SiLinkable",
-- only handle device si-audio-adapter items
Constraint { "item.factory.name", "=", "si-audio-adapter", type = "pw-global" },
Constraint { "item.node.type", "=", "device", type = "pw-global" },
Constraint { "active-features", "!", 0, type = "gobject" },
}
}
streams_om = ObjectManager {
Interest {
type = "SiLinkable",
-- only handle stream si-audio-adapter items
Constraint { "item.factory.name", "=", "si-audio-adapter", type = "pw-global" },
Constraint { "active-features", "!", 0, type = "gobject" },
Constraint { "media.class", "=", "Stream/Output/Audio" },
}
}
links_om = ObjectManager {
Interest {
type = "SiLink",
-- only handle links created by this policy
Constraint { "is.policy.endpoint.device.link", "=", true, type = "pw-global" },
}
}
-- listen for default node changes if config.follow is enabled
if config.follow then
default_nodes:connect("changed", function (p)
scheduleRescan ()
end)
end
linkables_om:connect("objects-changed", function (om)
scheduleRescan ()
end)
endpoints_om:connect("object-added", function (om)
scheduleRescan ()
end)
linkables_om:connect("object-removed", function (om, si)
unhandleLinkable (si)
end)
endpoints_om:activate()
linkables_om:activate()
links_om:activate()
streams_om:activate()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,499 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- Based on restore-stream.c from pipewire-media-session
-- Copyright © 2020 Wim Taymans
--
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local config = ... or {}
config.properties = config.properties or {}
config_restore_props = config.properties["restore-props"] or false
config_restore_target = config.properties["restore-target"] or false
config_default_channel_volume = config.properties["default-channel-volume"] or 1.0
-- preprocess rules and create Interest objects
for _, r in ipairs(config.rules or {}) do
r.interests = {}
for _, i in ipairs(r.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(r.interests, interest)
end
r.matches = nil
end
-- applies properties from config.rules when asked to
function rulesApplyProperties(properties)
for _, r in ipairs(config.rules or {}) do
if r.apply_properties then
for _, interest in ipairs(r.interests) do
if interest:matches(properties) then
for k, v in pairs(r.apply_properties) do
properties[k] = v
end
end
end
end
end
end
-- the state storage
state = State("restore-stream")
state_table = state:load()
-- simple serializer {"foo", "bar"} -> "foo;bar;"
function serializeArray(a)
local str = ""
for _, v in ipairs(a) do
str = str .. tostring(v):gsub(";", "\\;") .. ";"
end
return str
end
-- simple deserializer "foo;bar;" -> {"foo", "bar"}
function parseArray(str, convert_value, with_type)
local array = {}
local val = ""
local escaped = false
for i = 1, #str do
local c = str:sub(i,i)
if c == '\\' then
escaped = true
elseif c == ';' and not escaped then
val = convert_value and convert_value(val) or val
table.insert(array, val)
val = ""
else
val = val .. tostring(c)
escaped = false
end
end
if with_type then
array["pod_type"] = "Array"
end
return array
end
function parseParam(param, id)
local route = param:parse()
if route.pod_type == "Object" and route.object_id == id then
return route.properties
else
return nil
end
end
function storeAfterTimeout()
if timeout_source then
timeout_source:destroy()
end
timeout_source = Core.timeout_add(1000, function ()
local saved, err = state:save(state_table)
if not saved then
Log.warning(err)
end
timeout_source = nil
end)
end
function findSuitableKey(properties)
local keys = {
"media.role",
"application.id",
"application.name",
"media.name",
"node.name",
}
local key = nil
for _, k in ipairs(keys) do
local p = properties[k]
if p then
key = string.format("%s:%s:%s",
properties["media.class"]:gsub("^Stream/", ""), k, p)
break
end
end
return key
end
function saveTarget(subject, target_key, type, value)
if target_key ~= "target.node" and target_key ~= "target.object" then
return
end
local node = streams_om:lookup {
Constraint { "bound-id", "=", subject, type = "gobject" }
}
if not node then
return
end
local stream_props = node.properties
rulesApplyProperties(stream_props)
if stream_props["state.restore-target"] == false then
return
end
local key_base = findSuitableKey(stream_props)
if not key_base then
return
end
local target_value = value
local target_name = nil
if not target_value then
local metadata = metadata_om:lookup()
if metadata then
target_value = metadata:find(node["bound-id"], target_key)
end
end
if target_value and target_value ~= "-1" then
local target_node
if target_key == "target.object" then
target_node = allnodes_om:lookup {
Constraint { "object.serial", "=", target_value, type = "pw-global" }
}
else
target_node = allnodes_om:lookup {
Constraint { "bound-id", "=", target_value, type = "gobject" }
}
end
if target_node then
target_name = target_node.properties["node.name"]
end
end
state_table[key_base .. ":target"] = target_name
Log.info(node, "saving stream target for " ..
tostring(stream_props["node.name"]) ..
" -> " .. tostring(target_name))
storeAfterTimeout()
end
function restoreTarget(node, target_name)
local stream_props = node.properties
local target_in_props = nil
if stream_props ["target.object"] ~= nil or
stream_props ["node.target"] ~= nil then
target_in_props = stream_props ["target.object"] or
stream_props ["node.target"]
Log.debug (string.format ("%s%s%s%s",
"Not restoring the target for ",
stream_props ["node.name"],
" because it is already set to ",
target_in_props))
return
end
local target_node = allnodes_om:lookup {
Constraint { "node.name", "=", target_name, type = "pw" }
}
if target_node then
local metadata = metadata_om:lookup()
if metadata then
metadata:set(node["bound-id"], "target.node", "Spa:Id",
target_node["bound-id"])
end
end
end
function jsonTable(val, name)
local tmp = ""
local count = 0
if name then tmp = tmp .. string.format("%q", name) .. ": " end
if type(val) == "table" then
if val["pod_type"] == "Array" then
tmp = tmp .. "["
for _, v in ipairs(val) do
if count > 0 then tmp = tmp .. "," end
tmp = tmp .. jsonTable(v)
count = count + 1
end
tmp = tmp .. "]"
else
tmp = tmp .. "{"
for k, v in pairs(val) do
if count > 0 then tmp = tmp .. "," end
tmp = tmp .. jsonTable(v, k)
count = count + 1
end
tmp = tmp .. "}"
end
elseif type(val) == "number" then
tmp = tmp .. tostring(val)
elseif type(val) == "string" then
tmp = tmp .. string.format("%q", val)
elseif type(val) == "boolean" then
tmp = tmp .. (val and "true" or "false")
else
tmp = tmp .. "\"[type:" .. type(val) .. "]\""
end
return tmp
end
function moveToMetadata(key_base, metadata)
local route_table = { }
local count = 0
key = "restore.stream." .. key_base
key = string.gsub(key, ":", ".", 1);
local str = state_table[key_base .. ":volume"]
if str then
route_table["volume"] = tonumber(str)
count = count + 1;
end
local str = state_table[key_base .. ":mute"]
if str then
route_table["mute"] = str == "true"
count = count + 1;
end
local str = state_table[key_base .. ":channelVolumes"]
if str then
route_table["volumes"] = parseArray(str, tonumber, true)
count = count + 1;
end
local str = state_table[key_base .. ":channelMap"]
if str then
route_table["channels"] = parseArray(str, nil, true)
count = count + 1;
end
if count > 0 then
metadata:set(0, key, "Spa:String:JSON", jsonTable(route_table));
end
end
function saveStream(node)
local stream_props = node.properties
rulesApplyProperties(stream_props)
if config_restore_props and stream_props["state.restore-props"] ~= false then
local key_base = findSuitableKey(stream_props)
if not key_base then
return
end
Log.info(node, "saving stream props for " ..
tostring(stream_props["node.name"]))
for p in node:iterate_params("Props") do
local props = parseParam(p, "Props")
if not props then
goto skip_prop
end
if props.volume then
state_table[key_base .. ":volume"] = tostring(props.volume)
end
if props.mute ~= nil then
state_table[key_base .. ":mute"] = tostring(props.mute)
end
if props.channelVolumes then
state_table[key_base .. ":channelVolumes"] = serializeArray(props.channelVolumes)
end
if props.channelMap then
state_table[key_base .. ":channelMap"] = serializeArray(props.channelMap)
end
::skip_prop::
end
storeAfterTimeout()
end
end
function build_default_channel_volumes (node)
local def_vol = config_default_channel_volume
local channels = 2
local res = {}
local str = node.properties["state.default-channel-volume"]
if str ~= nil then
def_vol = tonumber (str)
end
for pod in node:iterate_params("Format") do
local pod_parsed = pod:parse()
if pod_parsed ~= nil then
channels = pod_parsed.properties.channels
break
end
end
while (#res < channels) do
table.insert(res, def_vol)
end
return res;
end
function restoreStream(node)
local stream_props = node.properties
rulesApplyProperties(stream_props)
local key_base = findSuitableKey(stream_props)
if not key_base then
return
end
if config_restore_props and stream_props["state.restore-props"] ~= false then
local props = { "Spa:Pod:Object:Param:Props", "Props" }
local str = state_table[key_base .. ":volume"]
props.volume = str and tonumber(str) or nil
local str = state_table[key_base .. ":mute"]
props.mute = str and (str == "true") or nil
local str = state_table[key_base .. ":channelVolumes"]
props.channelVolumes = str and parseArray(str, tonumber) or
build_default_channel_volumes (node)
local str = state_table[key_base .. ":channelMap"]
props.channelMap = str and parseArray(str) or nil
-- convert arrays to Spa Pod
if props.channelVolumes then
table.insert(props.channelVolumes, 1, "Spa:Float")
props.channelVolumes = Pod.Array(props.channelVolumes)
end
if props.channelMap then
table.insert(props.channelMap, 1, "Spa:Enum:AudioChannel")
props.channelMap = Pod.Array(props.channelMap)
end
Log.info(node, "restore values from " .. key_base)
local param = Pod.Object(props)
Log.debug(param, "setting props on " .. tostring(node))
node:set_param("Props", param)
end
if config_restore_target and stream_props["state.restore-target"] ~= false then
local str = state_table[key_base .. ":target"]
if str then
restoreTarget(node, str)
end
end
end
if config_restore_target then
metadata_om = ObjectManager {
Interest {
type = "metadata",
Constraint { "metadata.name", "=", "default" },
}
}
metadata_om:connect("object-added", function (om, metadata)
-- process existing metadata
for s, k, t, v in metadata:iterate(Id.ANY) do
saveTarget(s, k, t, v)
end
-- and watch for changes
metadata:connect("changed", function (m, subject, key, type, value)
saveTarget(subject, key, type, value)
end)
end)
metadata_om:activate()
end
function handleRouteSettings(subject, key, type, value)
if type ~= "Spa:String:JSON" then
return
end
if string.find(key, "^restore.stream.") == nil then
return
end
if value == nil then
return
end
local json = Json.Raw (value);
if json == nil or not json:is_object () then
return
end
local vparsed = json:parse()
local key_base = string.sub(key, string.len("restore.stream.") + 1)
local str;
key_base = string.gsub(key_base, "%.", ":", 1);
if vparsed.volume ~= nil then
state_table[key_base .. ":volume"] = tostring (vparsed.volume)
end
if vparsed.mute ~= nil then
state_table[key_base .. ":mute"] = tostring (vparsed.mute)
end
if vparsed.channels ~= nil then
state_table[key_base .. ":channelMap"] = serializeArray (vparsed.channels)
end
if vparsed.volumes ~= nil then
state_table[key_base .. ":channelVolumes"] = serializeArray (vparsed.volumes)
end
storeAfterTimeout()
end
rs_metadata = ImplMetadata("route-settings")
rs_metadata:activate(Features.ALL, function (m, e)
if e then
Log.warning("failed to activate route-settings metadata: " .. tostring(e))
return
end
-- copy state into the metadata
moveToMetadata("Output/Audio:media.role:Notification", m)
-- watch for changes
m:connect("changed", function (m, subject, key, type, value)
handleRouteSettings(subject, key, type, value)
end)
end)
allnodes_om = ObjectManager { Interest { type = "node" } }
allnodes_om:activate()
streams_om = ObjectManager {
-- match stream nodes
Interest {
type = "node",
Constraint { "media.class", "matches", "Stream/*", type = "pw-global" },
},
-- and device nodes that are not associated with any routes
Interest {
type = "node",
Constraint { "media.class", "matches", "Audio/*", type = "pw-global" },
Constraint { "device.routes", "is-absent", type = "pw" },
},
Interest {
type = "node",
Constraint { "media.class", "matches", "Audio/*", type = "pw-global" },
Constraint { "device.routes", "equals", "0", type = "pw" },
},
}
streams_om:connect("object-added", function (streams_om, node)
node:connect("params-changed", saveStream)
restoreStream(node)
end)
streams_om:activate()

View File

@ -0,0 +1,103 @@
-- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
--
-- The script exposes a metadata object named "sm-objects" that clients can
-- use to load objects into the WirePlumber daemon process. The objects are
-- loaded as soon as the metadata is set and are destroyed when the metadata
-- is cleared.
--
-- To load an object, a client needs to set a metadata entry with:
--
-- * subject:
-- The ID of the owner of the object; you can use 0 here, but the
-- idea is to be able to restrict which clients can change and/or
-- delete these objects by using IDs of other objects appropriately
--
-- * key: "<UNIQUE-OBJECT-NAME>"
-- This is the name that will be used to identify the object.
-- If an object with the same name already exists, it will be destroyed.
-- Note that the keys are unique per subject, so you can have multiple
-- objects with the same name as long as they are owned by different subjects.
--
-- * type: "Spa:String:JSON"
--
-- * value: "{ type = <object-type>,
-- name = <object-name>,
-- args = { ...object arguments... } }"
-- The object type can be one of the following:
-- - "pw-module": loads a pipewire module: `name` and `args` are interpreted
-- just like a module entry in pipewire.conf
-- - "metadata": loads a metadata object with `metadata.name` = `name`
-- and any additional properties provided in `args`
--
on_demand_objects = {}
object_constructors = {
["pw-module"] = LocalModule,
["metadata"] = function (name, args)
local m = ImplMetadata (name, args)
m:activate (Features.ALL, function (m, e)
if e then
Log.warning ("failed to activate on-demand metadata `" .. name .. "`: " .. tostring (e))
end
end)
return m
end
}
function handle_metadata_changed (m, subject, key, type, value)
-- destroy all objects when metadata is cleared
if not key then
on_demand_objects = {}
return
end
local object_id = key .. "@" .. tostring(subject)
-- destroy existing object instance, if needed
if on_demand_objects[object_id] then
Log.debug("destroy on-demand object: " .. object_id)
on_demand_objects[object_id] = nil
end
if value then
local json = Json.Raw(value)
if not json:is_object() then
Log.warning("loading '".. object_id .. "' failed: expected JSON object, got: '" .. value .. "'")
return
end
local obj = json:parse(1)
if not obj.type then
Log.warning("loading '".. object_id .. "' failed: no object type specified")
return
end
if not obj.name then
Log.warning("loading '".. object_id .. "' failed: no object name specified")
return
end
local constructor = object_constructors[obj.type]
if not constructor then
Log.warning("loading '".. object_id .. "' failed: unknown object type: " .. obj.type)
return
end
Log.info("load on-demand object: " .. object_id .. " -> " .. obj.name)
on_demand_objects[object_id] = constructor(obj.name, obj.args)
end
end
objects_metadata = ImplMetadata ("sm-objects")
objects_metadata:activate (Features.ALL, function (m, e)
if e then
Log.warning ("failed to activate the sm-objects metadata: " .. tostring (e))
else
m:connect("changed", handle_metadata_changed)
end
end)

View File

@ -0,0 +1,36 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local endpoints_config = ...
function createEndpoint (factory_name, properties)
-- create endpoint
local ep = SessionItem ( factory_name )
if not ep then
Log.warning (ep, "could not create endpoint of type " .. factory_name)
return
end
-- configure endpoint
if not ep:configure(properties) then
Log.warning(ep, "failed to configure endpoint " .. properties.name)
return
end
-- activate and register endpoint
ep:activate (Features.ALL, function (item)
item:register ()
Log.info(item, "registered endpoint " .. properties.name)
end)
end
for name, properties in pairs(endpoints_config) do
properties["name"] = name
createEndpoint ("si-audio-endpoint", properties)
end

View File

@ -0,0 +1,59 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
om = ObjectManager {
Interest { type = "node",
Constraint { "media.class", "matches", "Audio/*" }
},
Interest { type = "node",
Constraint { "media.class", "matches", "Video/*" }
},
}
sources = {}
om:connect("object-added", function (om, node)
node:connect("state-changed", function (node, old_state, cur_state)
-- Always clear the current source if any
local id = node["bound-id"]
if sources[id] then
sources[id]:destroy()
sources[id] = nil
end
-- Add a timeout source if idle for at least 5 seconds
if cur_state == "idle" or cur_state == "error" then
-- honor "session.suspend-timeout-seconds" if specified
local timeout =
tonumber(node.properties["session.suspend-timeout-seconds"]) or 5
if timeout == 0 then
return
end
-- add idle timeout; multiply by 1000, timeout_add() expects ms
sources[id] = Core.timeout_add(timeout * 1000, function()
-- Suspend the node
-- but check first if the node still exists
if (node:get_active_features() & Feature.Proxy.BOUND) ~= 0 then
Log.info(node, "was idle for a while; suspending ...")
node:send_command("Suspend")
end
-- Unref the source
sources[id] = nil
-- false (== G_SOURCE_REMOVE) destroys the source so that this
-- function does not get fired again after 5 seconds
return false
end)
end
end)
end)
om:activate()

View File

@ -0,0 +1,96 @@
# WirePlumber daemon context configuration #
context.properties = {
## Properties to configure the PipeWire context and some modules
#application.name = WirePlumber
log.level = 2
wireplumber.script-engine = lua-scripting
#wireplumber.export-core = true
#mem.mlock-all = false
#support.dbus = true
}
context.spa-libs = {
#<factory-name regex> = <library-name>
#
# Used to find spa factory names. It maps an spa factory name
# regular expression to a library name that should contain
# that factory.
#
api.alsa.* = alsa/libspa-alsa
api.bluez5.* = bluez5/libspa-bluez5
api.v4l2.* = v4l2/libspa-v4l2
api.libcamera.* = libcamera/libspa-libcamera
audio.convert.* = audioconvert/libspa-audioconvert
support.* = support/libspa-support
}
context.modules = [
#{ name = <module-name>
# [ args = { <key> = <value> ... } ]
# [ flags = [ [ ifexists ] [ nofail ] ]
#}
#
# PipeWire modules to load.
# If ifexists is given, the module is ignored when it is not found.
# If nofail is given, module initialization failures are ignored.
#
# Uses RTKit to boost the data thread priority. Also allows clamping
# of utilisation when using the Completely Fair Scheduler on Linux.
{ name = libpipewire-module-rt
args = {
nice.level = -19
rt.prio = 95
#rt.time.soft = -1
#rt.time.hard = -1
#uclamp.min = 0
#uclamp.max = 1024
}
flags = [ ifexists nofail ]
}
# The native communication protocol.
{ name = libpipewire-module-protocol-native }
# Allows creating nodes that run in the context of the
# client. Is used by all clients that want to provide
# data to PipeWire.
{ name = libpipewire-module-client-node }
# Allows creating devices that run in the context of the
# client. Is used by the session manager.
{ name = libpipewire-module-client-device }
# Makes a factory for wrapping nodes in an adapter with a
# converter and resampler.
{ name = libpipewire-module-adapter }
# Allows applications to create metadata objects. It creates
# a factory for Metadata objects.
{ name = libpipewire-module-metadata }
# Provides factories to make session manager objects.
{ name = libpipewire-module-session-manager }
# Provides factories to make SPA node objects.
{ name = libpipewire-module-spa-node-factory }
]
wireplumber.components = [
#{ name = <component-name>, type = <component-type> }
#
# WirePlumber components to load
#
# The lua scripting engine
{ name = libwireplumber-module-lua-scripting, type = module }
# The lua configuration file(s)
# Other components are loaded from there
{ name = main.lua, type = config/lua }
{ name = policy.lua, type = config/lua }
{ name = bluetooth.lua, type = config/lua }
]