[All] Revert certain pipewire tweaks (let's start over)
This commit is contained in:
parent
9116521d1e
commit
5fb25536c7
|
@ -1,4 +1,4 @@
|
|||
# Real-time Client config file for PipeWire version "0.3.81" #
|
||||
# Real-time Client config file for PipeWire version "1.0.0" #
|
||||
#
|
||||
# Copy and edit this file in /etc/pipewire for system-wide changes
|
||||
# or in ~/.config/pipewire for local changes.
|
||||
|
@ -75,11 +75,11 @@ context.modules = [
|
|||
]
|
||||
|
||||
filter.properties = {
|
||||
node.latency = 256/48000
|
||||
#node.latency = 1024/48000
|
||||
}
|
||||
|
||||
stream.properties = {
|
||||
node.latency = 256/48000
|
||||
#node.latency = 1024/48000
|
||||
#node.autoconnect = true
|
||||
#resample.quality = 4
|
||||
#channelmix.normalize = false
|
||||
|
@ -111,6 +111,7 @@ stream.rules = [
|
|||
]
|
||||
|
||||
alsa.properties = {
|
||||
#alsa.deny = false
|
||||
# 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 ]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Client config file for PipeWire version "0.3.78" #
|
||||
# Client config file for PipeWire version "1.0.0" #
|
||||
#
|
||||
# Copy and edit this file in /etc/pipewire for system-wide changes
|
||||
# or in ~/.config/pipewire for local changes.
|
||||
|
@ -66,11 +66,11 @@ context.modules = [
|
|||
]
|
||||
|
||||
filter.properties = {
|
||||
node.latency = 256/48000
|
||||
#node.latency = 1024/48000
|
||||
}
|
||||
|
||||
stream.properties = {
|
||||
node.latency = 256/48000
|
||||
#node.latency = 1024/48000
|
||||
#node.autoconnect = true
|
||||
#resample.quality = 4
|
||||
#channelmix.normalize = false
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
# 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 }
|
||||
]
|
|
@ -1,64 +0,0 @@
|
|||
# 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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -1,47 +0,0 @@
|
|||
# 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 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -1,70 +0,0 @@
|
|||
# 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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -1,56 +0,0 @@
|
|||
# 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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -1,42 +0,0 @@
|
|||
# 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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -1,40 +0,0 @@
|
|||
# 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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -1,180 +0,0 @@
|
|||
# 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 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -1,104 +0,0 @@
|
|||
# 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 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -1,52 +0,0 @@
|
|||
# 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -1,44 +0,0 @@
|
|||
# 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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -1,133 +0,0 @@
|
|||
# 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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -1,363 +0,0 @@
|
|||
# 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 = [ 16000 22050 44100 48000 88200 96000 ]
|
||||
#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" }
|
||||
]
|
|
@ -1,129 +0,0 @@
|
|||
# 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
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
|
@ -1,73 +0,0 @@
|
|||
# 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 = {
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
# PulseAudio config file for PipeWire version "0.3.78" #
|
||||
# PulseAudio config file for PipeWire version "1.0.0" #
|
||||
#
|
||||
# Copy and edit this file in /etc/pipewire for system-wide changes
|
||||
# or in ~/.config/pipewire for local changes.
|
||||
|
@ -27,9 +27,12 @@ context.modules = [
|
|||
{ name = libpipewire-module-rt
|
||||
args = {
|
||||
nice.level = -19
|
||||
#rt.prio = 88
|
||||
rt.prio = 95
|
||||
#rt.time.soft = -1
|
||||
#rt.time.hard = -1
|
||||
#uclamp.min = 0
|
||||
#uclamp.max = 1024
|
||||
}
|
||||
flags = [ ifexists nofail ]
|
||||
}
|
||||
|
@ -42,16 +45,6 @@ context.modules = [
|
|||
args = {
|
||||
# contents of pulse.properties can also be placed here
|
||||
# to have config per server.
|
||||
pulse.min.req = "256/48000"
|
||||
pulse.default.req = "256/48000"
|
||||
pulse.max.req = "256/48000"
|
||||
pulse.min.quantum = "256/48000"
|
||||
pulse.max.quantum = "256/48000"
|
||||
pulse.min.req = "256/48000"
|
||||
pulse.default.req = "256/48000"
|
||||
pulse.max.req = "256/48000"
|
||||
pulse.min.quantum = "256/48000"
|
||||
pulse.max.quantum = "256/48000"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -75,7 +68,7 @@ pulse.cmd = [
|
|||
]
|
||||
|
||||
stream.properties = {
|
||||
node.latency = 256/48000
|
||||
#node.latency = 1024/48000
|
||||
#node.autoconnect = true
|
||||
#resample.quality = 4
|
||||
#channelmix.normalize = false
|
||||
|
@ -126,7 +119,7 @@ pulse.rules = [
|
|||
{
|
||||
matches = [
|
||||
{
|
||||
# all keys must match the value. ~ starts regex.
|
||||
# all keys must match the value. ! negates. ~ starts regex.
|
||||
#client.name = "Firefox"
|
||||
#application.process.binary = "teams"
|
||||
#application.name = "~speech-dispatcher.*"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Daemon config file for PipeWire version "0.3.78" #
|
||||
# Daemon config file for PipeWire version "1.0.0" #
|
||||
#
|
||||
# Copy and edit this file in /etc/pipewire for system-wide changes
|
||||
# or in ~/.config/pipewire for local changes.
|
||||
|
@ -26,11 +26,11 @@ context.properties = {
|
|||
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 = 256
|
||||
default.clock.min-quantum = 64
|
||||
default.clock.max-quantum = 512
|
||||
#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
|
||||
|
@ -50,6 +50,8 @@ context.properties = {
|
|||
# enables autoloading of access module, when disabled an alternative
|
||||
# access module needs to be loaded.
|
||||
module.access = true
|
||||
# enables autoloading of module-jackdbus-detect
|
||||
module.jackdbus-detect = true
|
||||
}
|
||||
|
||||
context.spa-libs = {
|
||||
|
@ -88,19 +90,27 @@ context.modules = [
|
|||
|
||||
# Uses realtime scheduling to boost the audio thread priorities. This uses
|
||||
# RTKit if the user doesn't have permission to use regular realtime
|
||||
# scheduling.
|
||||
# scheduling. You can also clamp utilisation values to improve scheduling
|
||||
# on embedded and heterogeneous systems, e.g. Arm big.LITTLE devices.
|
||||
{ name = libpipewire-module-rt
|
||||
args = {
|
||||
nice.level = -19
|
||||
rt.prio = 95
|
||||
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 }
|
||||
{ name = libpipewire-module-protocol-native
|
||||
args = {
|
||||
# List of server Unix sockets, and optionally permissions
|
||||
#sockets = [ { name = "pipewire-0" }, { name = "pipewire-0-manager" } ]
|
||||
}
|
||||
}
|
||||
|
||||
# The profile module. Allows application to access profiler
|
||||
# and performance data. It provides an interface that is used
|
||||
|
@ -139,21 +149,12 @@ context.modules = [
|
|||
# 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
|
||||
#]
|
||||
# Socket-specific access permissions
|
||||
#access.socket = { pipewire-0 = "default", pipewire-0-manager = "unrestricted" }
|
||||
|
||||
# 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
|
||||
# Deprecated legacy mode (not socket-based),
|
||||
# for now enabled by default if access.socket is not specified
|
||||
#access.legacy = true
|
||||
}
|
||||
condition = [ { module.access = true } ]
|
||||
}
|
||||
|
@ -179,6 +180,29 @@ context.modules = [
|
|||
flags = [ ifexists nofail ]
|
||||
condition = [ { module.x11.bell = true } ]
|
||||
}
|
||||
{ name = libpipewire-module-jackdbus-detect
|
||||
args = {
|
||||
#jack.library = libjack.so.0
|
||||
#jack.server = null
|
||||
#jack.client-name = PipeWire
|
||||
#jack.connect = true
|
||||
#tunnel.mode = duplex # source|sink|duplex
|
||||
source.props = {
|
||||
#audio.channels = 2
|
||||
#midi.ports = 1
|
||||
#audio.position = [ FL FR ]
|
||||
# extra sink properties
|
||||
}
|
||||
sink.props = {
|
||||
#audio.channels = 2
|
||||
#midi.ports = 1
|
||||
#audio.position = [ FL FR ]
|
||||
# extra sink properties
|
||||
}
|
||||
}
|
||||
flags = [ ifexists nofail ]
|
||||
condition = [ { module.jackdbus-detect = true } ]
|
||||
}
|
||||
]
|
||||
|
||||
context.objects = [
|
||||
|
@ -193,11 +217,11 @@ context.objects = [
|
|||
# If condition is given, the object is created only when the context properties
|
||||
# all match the match rules.
|
||||
#
|
||||
#{ factory = spa-node-factory args = { factory.name = videotestsrc node.name = videotestsrc Spa:Pod:Object:Param:Props:patternType = 1 } }
|
||||
#{ 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 } }
|
||||
#{ 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 } }
|
||||
|
||||
# A default dummy driver. This handles nodes marked with the "node.always-driver"
|
||||
|
|
|
@ -10,7 +10,7 @@ context.modules = [
|
|||
type = ladspa
|
||||
name = rnnoise
|
||||
plugin = /usr/lib/ladspa/librnnoise_ladspa.so
|
||||
label = noise_suppressor_mono
|
||||
label = noise_suppressor_stereo
|
||||
control = {
|
||||
"VAD Threshold (%)" 85.0
|
||||
"VAD Grace Period (ms)" = 200
|
||||
|
@ -27,7 +27,6 @@ context.modules = [
|
|||
playback.props = {
|
||||
node.name = "openvoiceos_denoised_mic"
|
||||
media.class = Audio/Source
|
||||
audio.rate = 48000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,91 +0,0 @@
|
|||
# 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 }
|
||||
]
|
|
@ -1,36 +0,0 @@
|
|||
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
|
|
@ -1,18 +0,0 @@
|
|||
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
|
|
@ -1,18 +0,0 @@
|
|||
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
|
|
@ -1,2 +0,0 @@
|
|||
bluez_monitor.enable()
|
||||
bluez_midi_monitor.enable()
|
|
@ -1,36 +0,0 @@
|
|||
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
|
|
@ -1,74 +0,0 @@
|
|||
# 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 }
|
||||
]
|
|
@ -1,36 +0,0 @@
|
|||
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
|
|
@ -1,19 +0,0 @@
|
|||
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
|
|
@ -1,29 +0,0 @@
|
|||
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
|
|
@ -1,14 +0,0 @@
|
|||
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
|
|
@ -1,14 +0,0 @@
|
|||
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
|
|
@ -1,85 +0,0 @@
|
|||
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
|
|
@ -1,42 +0,0 @@
|
|||
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
|
|
@ -140,8 +140,8 @@ alsa_monitor.rules = {
|
|||
--["audio.rate"] = 44100,
|
||||
--["audio.allowed-rates"] = "32000,96000",
|
||||
--["audio.position"] = "FL,FR",
|
||||
["api.alsa.period-size"] = 256,
|
||||
["api.alsa.period-num"] = 3,
|
||||
--["api.alsa.period-size"] = 256,
|
||||
--["api.alsa.period-num"] = 3,
|
||||
--["api.alsa.headroom"] = 0,
|
||||
--["api.alsa.start-delay"] = 0,
|
||||
--["api.alsa.disable-mmap"] = false,
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
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",
|
||||
},
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
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,
|
||||
},
|
||||
},
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
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,
|
||||
},
|
||||
},
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
-- 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")
|
|
@ -1,73 +0,0 @@
|
|||
# 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 }
|
||||
]
|
|
@ -1,36 +0,0 @@
|
|||
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
|
|
@ -1,98 +0,0 @@
|
|||
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
|
|
@ -1,95 +0,0 @@
|
|||
-- 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",
|
||||
},
|
||||
}
|
||||
]]--
|
|
@ -1 +0,0 @@
|
|||
default_policy.enable()
|
|
@ -1,53 +0,0 @@
|
|||
-- 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()
|
|
@ -1,141 +0,0 @@
|
|||
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()
|
|
@ -1,130 +0,0 @@
|
|||
-- 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()
|
|
@ -1,93 +0,0 @@
|
|||
-- 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()
|
|
@ -1,74 +0,0 @@
|
|||
-- 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()
|
|
@ -1,68 +0,0 @@
|
|||
-- 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
|
|
@ -1,427 +0,0 @@
|
|||
-- 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()
|
|
@ -1,187 +0,0 @@
|
|||
-- 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
|
|
@ -1,428 +0,0 @@
|
|||
-- 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()
|
|
@ -1,174 +0,0 @@
|
|||
-- 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
|
|
@ -1,165 +0,0 @@
|
|||
-- 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
|
|
@ -1,398 +0,0 @@
|
|||
-- 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()
|
|
@ -1,246 +0,0 @@
|
|||
-- 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()
|
|
@ -1,487 +0,0 @@
|
|||
-- 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()
|
|
@ -1,86 +0,0 @@
|
|||
-- 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()
|
|
@ -1,218 +0,0 @@
|
|||
-- 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()
|
|
@ -1,261 +0,0 @@
|
|||
-- 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()
|
|
@ -1,285 +0,0 @@
|
|||
-- 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
|
@ -1,499 +0,0 @@
|
|||
-- 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()
|
|
@ -1,103 +0,0 @@
|
|||
-- 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)
|
|
@ -1,36 +0,0 @@
|
|||
-- 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
|
|
@ -1,59 +0,0 @@
|
|||
-- 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()
|
Loading…
Reference in New Issue