Skip to main content
Topic: Anyone tried using libpipewire-module-filter-chain at all?  (Read 675 times) previous topic - next topic
0 Members and 1 Guest are viewing this topic.

Anyone tried using libpipewire-module-filter-chain at all?

I've been poking around a bit trying to get this module working with limited success.  Got a version of the builtin equalizer working, and also I think I got a lv2 compressor working, though had some trouble implementing both.

I use easyeffects to apply a few effects because my headphones are unusually horrible without anything (Sennheiser  HD 350BT) and it's definitely an awesome solution to give better sound quality.  I know a lot of people user easyeffects for laptop speakers as well.

Just trying to find out if anyone has experience mucking around with this feature because it looks like a nice "eliminate extra processes" type solution although probably won't reduce system resource consumption by too much since the plugins are the same other than the handful of ones builtin for pipewire.

This is what I've had some success with, a slightly modified version of the included example eq that pipewire includes in /usr/share/pipewire/filter-chain/.  It's been modified based on the settings I've been using in easyeffects (made it an 8 band eq, and changed to the frequency and gain for each band to match what easyeffects had been doing).

~/.config/pipewire/pipewire.conf.d/sink-eq6.conf

Code: [Select]
# 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_peaking
                        control = { "Freq" = 44.0 "Q" = 0.3 "Gain" = -7.5 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_2
                        label = bq_lowshelf
                        control = { "Freq" = 105.0 "Q" = 0.71 "Gain" = 5.5 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_3
                        label = bq_peaking
                        control = { "Freq" = 350.0 "Q" = 1.4 "Gain" = 2.0 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_4
                        label = bq_peaking
                        control = { "Freq" = 2100.0 "Q" = 0.9 "Gain" = -3.8 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_5
                        label = bq_peaking
                        control = { "Freq" = 4800.0 "Q" = 0.7 "Gain" = 6.0 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_6
                        label = bq_peaking
                        control = { "Freq" = 5200.0 "Q" = 6.0 "Gain" = -1.4 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_7
                        label = bq_peaking
                        control = { "Freq" = 8000.0 "Q" = 1.4 "Gain" = -1.0 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_8
                        label = bq_highshelf
                        control = { "Freq" = 11000.0 "Q" = 0.71 "Gain" = 2.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" }
                    { output = "eq_band_6:Out" input = "eq_band_7:In" }
                    { output = "eq_band_7:Out" input = "eq_band_8: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
            }
        }
    }
]

This is what I haven't had success with (system memory usage climbs to approaching 100%, pipewire fails to start).  Tried to combine both the working eq setup, and what I believe was a working compressor lv2 setup (on it's own).  I believe it was working because pipewire started, and the compressor sink was visible in pavucontrol.  I must be doing something wrong with the combination of the two filters.

Code: [Select]
context.modules = [
    { name = libpipewire-module-filter-chain
        args = {
            "node.description": "Compressor",
            "media.name": "Compressor",
            "filter.graph": {
                "nodes": [
                    {
                        type  = builtin,
                        name  = eq_band_1,
                        label = bq_peaking,
                        control = { "Freq" = 44.0 "Q" = 0.3 "Gain" = -7.5 },
                    }
                    {
                        type  = builtin,
                        name  = eq_band_2,
                        label = bq_lowshelf,
                        control = { "Freq" = 105.0 "Q" = 0.71 "Gain" = 5.5 },
                    }
                    {
                        type  = builtin,
                        name  = eq_band_3,
                        label = bq_peaking,
                        control = { "Freq" = 350.0 "Q" = 1.4 "Gain" = 2.0 },
                    }
                    {
                        type  = builtin,
                        name  = eq_band_4,
                        label = bq_peaking,
                        control = { "Freq" = 2100.0 "Q" = 0.9 "Gain" = -3.8 },
                    }
                    {
                        type  = builtin,
                        name  = eq_band_5,
                        label = bq_peaking,
                        control = { "Freq" = 4800.0 "Q" = 0.7 "Gain" = 6.0 },
                    }
                    {
                        type  = builtin,
                        name  = eq_band_6,
                        label = bq_peaking,
                        control = { "Freq" = 5200.0 "Q" = 6.0 "Gain" = -1.4 },
                    }
                    {
                        type  = builtin,
                        name  = eq_band_7,
                        label = bq_peaking,
                        control = { "Freq" = 8000.0 "Q" = 1.4 "Gain" = -1.0 },
                    }
                    {
                        type  = builtin,
                        name  = eq_band_8,
                        label = bq_highshelf,
                        control = { "Freq" = 11000.0 "Q" = 0.71 "Gain" = 2.0 },
                    }
                    {
                        "type": "lv2",
                        "plugin": "http://lsp-plug.in/plugins/lv2/mb_compressor_stereo",
                        "name": "stereo_comp_x8",
                        "control": {
                            "mode": 1,
                        }
                    }
                ]
            }
            playback.props = {
                node.name         = "compressor_output",
                audio.position    = [ FL FR ],
audio.channels    = 2,
            }
        }
    }
]

Update: It seems to be working with two seperate conf files, one for each effect:

Code: [Select]
sink-eq6.conf:

# 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_peaking
                        control = { "Freq" = 44.0 "Q" = 0.3 "Gain" = -7.5 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_2
                        label = bq_lowshelf
                        control = { "Freq" = 105.0 "Q" = 0.71 "Gain" = 5.5 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_3
                        label = bq_peaking
                        control = { "Freq" = 350.0 "Q" = 1.4 "Gain" = 2.0 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_4
                        label = bq_peaking
                        control = { "Freq" = 2100.0 "Q" = 0.9 "Gain" = -3.8 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_5
                        label = bq_peaking
                        control = { "Freq" = 4800.0 "Q" = 0.7 "Gain" = 6.0 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_6
                        label = bq_peaking
                        control = { "Freq" = 5200.0 "Q" = 6.0 "Gain" = -1.4 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_7
                        label = bq_peaking
                        control = { "Freq" = 8000.0 "Q" = 1.4 "Gain" = -1.0 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_8
                        label = bq_highshelf
                        control = { "Freq" = 11000.0 "Q" = 0.71 "Gain" = 2.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" }
                    { output = "eq_band_6:Out" input = "eq_band_7:In" }
                    { output = "eq_band_7:Out" input = "eq_band_8: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
            }
        }
    }
]

sink-comp.conf:

context.modules = [
    { name = libpipewire-module-filter-chain
        args = {
            "node.description": "Compressor Sink"
            "media.name": "Compressor Sink"
            "filter.graph": {
                "nodes": [
                    {
                        "type": "lv2"
                        "plugin": "http://lsp-plug.in/plugins/lv2/compressor_lr"
                        "name": "LSP_Compressor_LeftRight"
                        "control": {
                        }
                    }
                ]
            }
            audio.channels = 2
            audio.position = [ FL FR ]
capture.props = {
node.name = "effect_input.comp"
media.class = Audio/Sink
}
            playback.props = {
                node.name         = "effect_output.comp"
node.passive = true
            }
       
        }
    }
]

But then I realized to actually run the audio through the effects I had to connect things, clearly this was also the case when I was trying with just a single effect as well I just didn't realize.  I used helvum to do that:



But I feel like there must be a way to just route all audio through the filter-chain by default.  I'm missing something for sure.  Tried digging through the documentation, but seems pretty sparse.  Also tried to find somebody somewhere who has really dove into this stuff but I couldn't find much of anything anywhere.  Lots of people giving tutorials on how to use easyeffects but it's almost like nobody knows this filter-chain module is even a thing.  Anybody here got any expertise they could share?

Re: Anyone tried using libpipewire-module-filter-chain at all?

Reply #1
Maybe it's this what you're looking for in regards to connecting a filter by default https://pipewire.pages.freedesktop.org/wireplumber/policies/software_dsp.html

This crossed my mind too but with my usual lack the time and energy i didn't try it, either with one of these filter chains or with something like this https://bennett.dev/auto-link-pipewire-ports-wireplumber/ to route the jack ports of a dsp program, now that you broke the ice with this i'll return with some news of my own soon :)

Re: Anyone tried using libpipewire-module-filter-chain at all?

Reply #2
Hmm yes that looks promising.  Having some trouble getting it to work at the moment though.  I followed the example, finding the node.name for my headphones sink by using "wpctl status", which returned this:
Code: [Select]
PipeWire 'pipewire-0' [1.0.4, user@B650-Plus, cookie:3368478153]
 └─ Clients:
        28. FireDragon                          [1.0.4, user@B650-Plus, pid:2650]
        32. xdg-desktop-portal-hyprland         [1.0.4, user@B650-Plus, pid:2040]
        33. WirePlumber                         [1.0.4, user@B650-Plus, pid:2047]
        34. pipewire                            [1.0.4, user@B650-Plus, pid:2083]
        35. waybar                              [1.0.4, user@B650-Plus, pid:1901]
        36. Blueman                             [1.0.4, user@B650-Plus, pid:1856]
        49. WirePlumber [export]                [1.0.4, user@B650-Plus, pid:2047]
        61. Blueman                             [1.0.4, user@B650-Plus, pid:2268]
        63. WirePlumber                         [1.0.4, user@B650-Plus, pid:2047]
        74. FireDragon                          [1.0.4, user@B650-Plus, pid:2698]
        75. FireDragon                          [1.0.4, user@B650-Plus, pid:2482]
        76. FireDragon                          [1.0.4, user@B650-Plus, pid:2776]
        77. FireDragon                          [1.0.4, user@B650-Plus, pid:2779]
        78. FireDragon                          [1.0.4, user@B650-Plus, pid:2891]
        79. FireDragon                          [1.0.4, user@B650-Plus, pid:2894]
        80. FireDragon                          [1.0.4, user@B650-Plus, pid:2897]
        86. FireDragon                          [1.0.4, user@B650-Plus, pid:5415]
        87. wpctl                               [1.0.4, user@B650-Plus, pid:6375]
        88. FireDragon                          [1.0.4, user@B650-Plus, pid:3820]
        89. FireDragon                          [1.0.4, user@B650-Plus, pid:5379]
        90. FireDragon                          [1.0.4, user@B650-Plus, pid:5726]
        91. FireDragon                          [1.0.4, user@B650-Plus, pid:5456]

Audio
 ├─ Devices:
 │      50. Navi 21/23 HDMI/DP Audio Controller [alsa]
 │      51. Family 17h/19h HD Audio Controller  [alsa]
 │      62. HD 350BT                            [bluez5]
 │ 
 ├─ Sinks:
 │      56. Family 17h/19h HD Audio Controller Analog Stereo [vol: 1.00]
 │  *   66. HD 350BT                            [vol: 0.53]
 │ 
 ├─ Sources:
 │      57. Family 17h/19h HD Audio Controller Analog Stereo [vol: 1.00]
 │ 
 ├─ Filters:
 │    - loopback-2047-17                                           
 │  *   64. bluez_input.80:C3:BA:39:3D:FF                                [Audio/Source]
 │      65. bluez_capture_internal.80:C3:BA:39:3D:FF                     [Stream/Input/Audio/Internal]
 │ 
 └─ Streams:
        81. FireDragon                                                 
             82. output_FL       > HD 350BT:playback_FL [active]
             83. output_FR       > HD 350BT:playback_FR [active]

Video
 ├─ Devices:
 │ 
 ├─ Sinks:
 │ 
 ├─ Sources:
 │ 
 ├─ Filters:
 │ 
 └─ Streams:

Settings
 └─ Default Configured Devices:
         0. Audio/Sink    bluez_output.80_C3_BA_39_3D_FF.1
         1. Audio/Source  bluez_input.80:C3:BA:39:3D:FF

and then "wpctl inspect 66", which returned this:
Code: [Select]
id 66, type PipeWire:Interface:Node
    api.bluez5.address = "80:C3:BA:39:3D:FF"
    api.bluez5.codec = "aptx"
    api.bluez5.profile = "a2dp-sink"
    api.bluez5.transport = ""
    audio.adapt.follower = ""
    bluez5.loopback = "false"
    card.profile.device = "1"
  * client.id = "49"
    clock.quantum-limit = "8192"
    device.api = "bluez5"
  * device.id = "62"
    device.routes = "1"
  * factory.id = "11"
    factory.mode = "merge"
    factory.name = "api.bluez5.a2dp.sink"
    library.name = "audioconvert/libspa-audioconvert"
  * media.class = "Audio/Sink"
    media.name = "HD 350BT"
  * node.description = "HD 350BT"
    node.driver = "true"
  * node.name = "bluez_output.80_C3_BA_39_3D_FF.1"
    node.pause-on-idle = "false"
  * object.serial = "72"
  * priority.driver = "1010"
  * priority.session = "1010"

So then I created ~/.config/wireplumber/wireplumber.conf.d/99-my-dsp.conf like this:
Code: [Select]
node.software-dsp.rules = [
  {
    matches = [
      { "node.name" = "bluez_output.80_C3_BA_39_3D_FF.1" }
      ##  { "alsa.id" = "~WeirdHardware*" } # Wildcard match
    ]

    actions = {
      create-filter = {
        filter-graph = {
            "nodes": [
                    {
                        "type": "lv2",
                        "plugin": "http://lsp-plug.in/plugins/lv2/slap_delay_stereo",
                        "name": "slap_delay_stereo",
                        "control": {
                        "pred": "200",
                        "wet": "10",
                        "dm0": "1",
                        "dt0": "200,"
                        "dd0": "200",
                        "df0": "1",
                        },
                        "target.object": "bluez_output.80_C3_BA_39_3D_FF.1"
                    }.
                    {
                    "type": "lv2",
                    "plugin": "http://lsp-plug.in/plugins/lv2/noise_generator_x4",
                    "name": "noise_generator_x4",
                    "control": {
                    "na_1": "50",
                    },
                    "target.object": "bluez_output.80_C3_BA_39_3D_FF.1"
                    }
                ]
        } # Virtual node goes here
        hide-parent = true
      }
    }
  }
]

And I put the
Code: [Select]
wireplumber.profiles = [
  main = {
    node.software-dsp = required
  }
]
part into the relevant part of /usr/share/wireplumber/wireplumber.conf because for some reason with it in ~/.config/wireplumber/wireplumber.conf.d/99-my-dsp.conf wireplumber failed to start and gave a whole whack of errors in the logs.  Kinda confusing, but I guess maybe the .conf.d entry of "main = " overwrote the whole "main = " from wireplumber.conf rather than just adding the additional node.software-dsp = required line, which makes sense I suppose.  Could have copied the default settings from wireplumber.conf into the .conf.d file rather than the other way around.  Probably will try that in a moment.

But anyways, no filters/effects are present in my audio as of now.  I tried to activate the most obvious "yeah it's working" effects so it was easy to tell (hence the slap delay and noise generator plugins being used).  At first I tried just the slap delay and that wasn't working on it's own so it's not a multiples issue.

One thing the example/tutorial wasn't clear on was this "Note" section:
Code: [Select]
Note

The target.object property of the virtual node should be configured statically to point to the node matched by the rule.
Because nowhere in the example is there anything like target.object, so I tried just adding that in myself but I'm not sure where it is supposed to go or what the syntax should be looking like.  I've since tried putting it within different sections like this:
Code: [Select]
        } # Virtual node goes here
        hide-parent = true
                "target.object" = "bluez_output.80_C3_BA_39_3D_FF.1"

      }
                "target.object" = "bluez_output.80_C3_BA_39_3D_FF.1"

    }
                "target.object" = "bluez_output.80_C3_BA_39_3D_FF.1"

  }
                "target.object" = "bluez_output.80_C3_BA_39_3D_FF.1"

]

And tried with "target.object": rather than "target.object" = , but not getting any effects on the audio.

Also, I'm a little confused by the final statement after the example "This will match any sinks with the UCM HiFi Speaker profile set that are associated with cards containing the string “WeirdHardware” at the start of their name." since this seems to put a lot of importance on the "alsa.id" value, but not the "node.name".

This would be an amazing feature to have a gui for (similar to EasyEffects, but just for setting .conf.d file contents).

Re: Anyone tried using libpipewire-module-filter-chain at all?

Reply #3
Okay I found something interesting: https://pipewire.pages.freedesktop.org/wireplumber/policies/smart_filters.html

"The smart filters policy allows automatically linking filters together, in a chain, and tied to a specific target node."

This is the way.  Check it out:



Code: [Select]
~/.config/pipewire/pipewire.conf.d/sink-eq6.conf

# 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.name": "eq-sink"
            "node.description": "Equalizer Sink"
            "media.name": "Equalizer Sink"
            "filter.graph": {
                "nodes": [
                    {
                        type  = builtin
                        name  = eq_band_1
                        label = bq_peaking
                        control = { "Freq" = 44.0 "Q" = 0.3 "Gain" = -7.5 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_2
                        label = bq_lowshelf
                        control = { "Freq" = 105.0 "Q" = 0.71 "Gain" = 5.5 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_3
                        label = bq_peaking
                        control = { "Freq" = 350.0 "Q" = 1.4 "Gain" = 2.0 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_4
                        label = bq_peaking
                        control = { "Freq" = 2100.0 "Q" = 0.9 "Gain" = -3.8 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_5
                        label = bq_peaking
                        control = { "Freq" = 4800.0 "Q" = 0.7 "Gain" = 6.0 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_6
                        label = bq_peaking
                        control = { "Freq" = 5200.0 "Q" = 6.0 "Gain" = -1.4 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_7
                        label = bq_peaking
                        control = { "Freq" = 8000.0 "Q" = 1.4 "Gain" = -1.0 }
                    }
                    {
                        type  = builtin
                        name  = eq_band_8
                        label = bq_highshelf
                        control = { "Freq" = 11000.0 "Q" = 0.71 "Gain" = 2.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" }
                    { output = "eq_band_6:Out" input = "eq_band_7:In" }
                    { output = "eq_band_7:Out" input = "eq_band_8:In" }
                ]
            }
    audio.channels = 2
            capture.props = {
            audio.position = [ FL FR ]
            filter.smart = true
            filter.smart.name = eq
            filter.smart.after = [ compressor ]
                media.class = Audio/Sink
            }
            playback.props = {
            audio.position = [ FL FR ]
                node.passive = true
                node.dont-remix = true
            }
        }
    }
]


~/.config/pipewire/pipewire.conf.d/sink-comp.conf
context.modules = [
    { name = libpipewire-module-filter-chain
        args = {
        "node.name": "compressor-sink"
            "node.description": "Compressor Sink"
            "media.name": "Compressor Sink"
            "filter.graph": {
                "nodes": [
                    {
                        "type": "lv2"
                        "plugin": "http://lsp-plug.in/plugins/lv2/compressor_lr"
                        "name": "LSP_Compressor_LeftRight"
                        "control": {
                        }
                    }
                ]
            }
            audio.channels = 2
capture.props = {
audio.position= [ FL FR ]
media.class = Audio/Sink
filter.smart = true
filter.smart.name = compressor
}
            playback.props = {
            audio.position = [ FL FR ]
node.passive = true
node.dont-remix = true
            }
       
        }
    }
]


Seems to be working fine although the audio quality is nowhere near what I get with the preset I've been using with EasyEffects.  Digging through the preset file has offered limited help in reproducing the filter-chain since the file doesn't use the proper names for the filters, I guess easyeffects has some internal database for what Loudness#2 equates to in terms of "http://lsp-plug.in/plugins/lv2/loudness" or whatever.  All of the settings are saved in the preset, but until I can find out which exact plugin is being used, the presets are somewhat useless for producing a 1-to-1 reproduction of the filter-chain.  Still this is looking very promising.