Skip to content

Instantly share code, notes, and snippets.

@tomfun
Created May 11, 2025 07:20
Show Gist options
  • Save tomfun/88711b25266bdd7575e698fc5cc3888e to your computer and use it in GitHub Desktop.
Save tomfun/88711b25266bdd7575e698fc5cc3888e to your computer and use it in GitHub Desktop.
Human like compressor
context.modules = [
{
name = libpipewire-module-filter-chain
args = {
node.name = "eq_from_fly_plate"
node.description = "EQ for FlyPlate LFE"
media.name = "EQ for FlyPlate"
audio.format = "S32BE"
audio.rate = 48000
audio.channels = 2
audio.position = [ FL FR ]
capture.props = { "media.class": "Audio/Sink" }
filter.graph = {
nodes = [
{
type = builtin
label = param_eq
name = eq_from_fly_plate_lfe
config = { filename = "/home/tomfun/prj/tomfun/infrastructure/pulse_audio/eq_from_fly_plate.txt" }
}
]
}
}
}
{
name = libpipewire-module-filter-chain
flags=[ "nofail" ]
args = {
node.name = "eq_hl_comp"
node.description = "Human-like compressor chain"
media.name = "Compressor"
audio.format = "S32BE"
audio.rate = 48000
audio.channels = 2
audio.position = [ FL FR ]
filter.graph = {
nodes = [
{
type = builtin
name = eq_before_comp
label = param_eq
config = {
filename = "/home/tomfun/prj/tomfun/infrastructure/pulse_audio/eq_comp.txt"
}
}
{
type = ladspa
name = compressor
plugin = sc4_1882
label = sc4
control = {
"0" = 0.2
"1" = 20
"2" = 500
"3" = -30
"4" = 8
"5" = 5
}
}
{
type = builtin
name = eq_after_comp
label = param_eq
config = {
filename = "/home/tomfun/prj/tomfun/infrastructure/pulse_audio/eq_after_comp.txt"
}
}
]
}
links = [
{ output = "eq_before_comp:Out" input = "compressor:In" }
{ output = "compressor:Out" input = "eq_after_comp:In" }
]
inputs = [ "eq_before_comp:In 1" "eq_before_comp:In 2" ]
outputs = [ "eq_after_comp:Out 1" "eq_after_comp:Out 2" ]
capture.props = {
media.class = "Audio/Sink"
audio.format = "S32BE"
audio.rate = 48000
audio.channels = 2
audio.position = [ FL FR ]
}
playback.props = {
audio.channels = 2
audio.position = [ FL FR ]
}
}
}
]

❓ PipeWire filter-chain works for 1 node, but fails with 2+ node chain (EQ → Compressor → EQ)

Hi all 👋 I'm using PipeWire master (1.5.0) and WirePlumber 0.5.8 on LinuxMint. I'm building a modular DSP setup using libpipewire-module-filter-chain from config files.


✅ What works

The following simple 1-node filter-chain works fine:

{
  name = libpipewire-module-filter-chain
  args = {
    node.name = "eq_from_fly_plate"
    filter.graph = {
      nodes = [
        {
          type = builtin
          label = param_eq
          name  = eq_from_fly_plate_lfe
          config = { filename = "/.../eq_from_fly_plate.txt" }
        }
      ]
    }
  }
}

This node shows up properly and I can manually patch it to HDMI using Helvum or pw-link, and it passes audio.


❌ What doesn’t work

When I try to load a more complex filter-chain with 3 nodes:

  • EQ → LADSPA (sc4) → EQ

...the node loads, but no audio goes through. It seems to be silently broken. Nothing shows up in Helvum/patchbay output for this sink.

{
  name = libpipewire-module-filter-chain
  args = {
    node.name = "eq_hl_comp"
    ...
    filter.graph = {
      nodes = [
        { type = builtin, name = eq_before_comp, ... }
        { type = ladspa,  name = compressor, plugin = sc4_1882, label = sc4, control = { "0"=..., ... } }
        { type = builtin, name = eq_after_comp, ... }
      ]
    }
    links = [
      { output = "eq_before_comp:Out" input = "compressor:In" }
      { output = "compressor:Out"     input = "eq_after_comp:In" }
    ]
    inputs  = [ "eq_before_comp:In 1", "eq_before_comp:In 2" ]
    outputs = [ "eq_after_comp:Out 1", "eq_after_comp:Out 2" ]
  }
}

I verified that control = { "0" = ..., ... } is required format. Still no audio or output appears.

Attempts

I've verified that the graph loads and nodes are created by PipeWire — they appear in pw-cli and can be manually routed to HDMI via Helvum. However, even when simplified to a two-node chain (e.g. param_eq → param_eq, skipping the LADSPA compressor entirely), the result is the same: audio flows through the graph virtually, but no actual DSP is applied. The output signal is either silence or an unmodified passthrough, even though all links appear visually correct.

I also ensured that input/output port names, channel layouts, and formats are correctly defined and that filter.graph.inputs/outputs match the final node. I restarted PipeWire and WirePlumber with clean state, tested multiple EQ config files, and even moved the failing part into a working structure. Still, any multi-node graph (even very basic ones) silently fails to apply DSP, unlike the one-node version which works reliably.


🔧 My setup

$ wireplumber --version
WirePlumber 0.5.8
# master

$ pipewire --version
PipeWire 1.5.0 (compiled and linked)
# master

$ systemctl --user restart wireplumber.service pipewire.{service,socket} pipewire-pulse.{service,socket}

Config is loaded via:

$ pw-config
{
  "config.path": "/usr/share/pipewire/pipewire.conf",
  "override.3.0.config.path": "/home/tomfun/.config/pipewire/pipewire.conf.d/01-pipewire.conf"
}

Does anyone know why a multi-node filter-chain graph would silently fail, while a 1-node version works? Thanks a lot 🙏

#!/usr/bin/env bash
set -e
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
export script_dir
sudo cp custom.pa /etc/pulse/default.pa.d/custom.pa
sudo systemctl daemon-reload
sudo apt install libasound2-dev libbluetooth-dev libsbc-dev libudev-dev doxygen
sudo apt install \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-ugly \
gstreamer1.0-libav \
gstreamer1.0-tools \
gir1.2-gstreamer-1.0 \
python3-gi
#sudo apt install gstreamer1.0-plugins-bad gstreamer1.0-plugins-good gstreamer1.0-plugins-base gstreamer1.0-plugins-ugly
rm -f ~/.config/systemd/user/stick_hard_eq.service
mkdir -p ~/.config/pipewire/pipewire.conf.d/
mkdir -p ~/.config/pipewire/pipewire-pulse.conf.d/
mkdir -p ~/.config/wireplumber/wireplumber.conf.d/
rm -f ~/.config/pipewire/pipewire.conf.d/pipewire.conf \
~/.config/pipewire/pipewire.conf.d/* \
~/.config/pipewire/pipewire-pulse.conf.d/pipewire-pulseaudio.conf \
~/.config/wireplumber/wireplumber.conf.d/01-wireplumber.conf
ln -sv "$script_dir/stick_hard_eq.service" ~/.config/systemd/user/
ln -sv "$script_dir/pipewire-pulseaudio.conf" ~/.config/pipewire/pipewire-pulse.conf.d/
ln -sv "$script_dir/01-pipewire.conf" ~/.config/pipewire/pipewire.conf.d/
ln -sv "$script_dir/01-wireplumber.conf" ~/.config/wireplumber/wireplumber.conf.d/
cd pipewire
git pull
rm -rf builddir
meson setup builddir \
--prefix=/usr \
--libdir=/usr/lib/x86_64-linux-gnu \
-Dalsa=enabled \
-Dspa-plugins=enabled \
-Dsystemd=enabled \
-Dgstreamer=enabled \
-Dbluez5=enabled \
-Dudev=enabled \
-Dpipewire-alsa=enabled \
-Dsession-managers=wireplumber \
-Dman=enabled \
--buildtype=release
ninja -C builddir
sudo ninja -C builddir install
sudo ldconfig
sudo apt-mark hold pipewire pipewire-bin pipewire-audio-client-libraries pipewire-pulse wireplumber pulseaudio
sudo rm -f /etc/systemd/user/pipewire.{service,socket}
sudo rm -f /etc/systemd/user/pipewire-pulse.{service,socket}
sudo rm -f \
/usr/local/lib/x86_64-linux-gnu/libpipewire-0.3.so* \
/usr/local/lib/x86_64-linux-gnu/libwireplumber-0.5.so*
systemctl --user daemon-reload
systemctl --user --now enable wireplumber.service pipewire.{service,socket} pipewire-pulse.{service,socket}
#systemctl --user enable stick_hard_eq.service
#systemctl --user restart pipewire.service stick_hard_eq.service
@tomfun
Copy link
Author

tomfun commented May 11, 2025

@tomfun
Copy link
Author

tomfun commented May 11, 2025

The same thing with pulse audio: https://github.com/tomfun/eq-pulse-audio

@tomfun
Copy link
Author

tomfun commented May 11, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment