Skip to content

Instantly share code, notes, and snippets.

@tomfun
Last active January 20, 2026 11:05
Show Gist options
  • Select an option

  • Save tomfun/88711b25266bdd7575e698fc5cc3888e to your computer and use it in GitHub Desktop.

Select an option

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_before_comp"
node.description = "Human-like compressor chain (entry eq_before_comp)"
media.name = "HCL out"
audio.format = "S32BE"
audio.rate = 48000
# ? del:
target.object = "hcl_compressor"
capture.props = {
media.class = "Audio/Sink"
node.name = "eq_before_comp"
audio.channels = 6
audio.position = [ FL FR FC LFE SL SR ]
}
# And sends *stereo out* to your actual HDMI device
playback.props = {
node.target = "hcl_compressor"
audio.channels = 2
audio.position = [ FL FR ]
}
# Main filter graph
filter.graph = {
nodes = [
# Duplicate the C
{
type = builtin
name = dupeC
label = copy
}
# Duplicate the LFE
{
type = builtin
name = dupeLfe
label = copy
}
# Build LEFT output from FL + C + LFE + SL
{
type = builtin
label = mixer
name = mixL
control = {
"Gain 1" = 1.0 # FL
"Gain 2" = 0.0 # FR
"Gain 3" = 0.7 # FC (center into left)
"Gain 4" = 0.5 # LFE
"Gain 5" = 0.3 # SL
"Gain 6" = 0.0 # SR
}
}
# Build RIGHT output from FR + C + LFE + SR
{
type = builtin
label = mixer
name = mixR
control = {
"Gain 1" = 0.0 # FL
"Gain 2" = 1.0 # FR
"Gain 3" = 0.7 # FC (center into right)
"Gain 4" = 0.5 # LFE
"Gain 5" = 0.0 # SL
"Gain 6" = 0.3 # SR
}
}
{
type = builtin
label = param_eq
name = eq_before
config = { filename = "/home/tomfun/prj/tomfun/infrastructure/pulse_audio/eq_comp.txt" }
}
{
type = ladspa
name = compressor
plugin = sc4_1882
label = sc4
# https://github.com/swh/ladspa/blob/master/sc4_1882.xml#L116
control = {
"0" = 0.5
"1" = 20
"2" = 250
"3" = -30
"4" = 6
"5" = 4
"6" = 13
}
}
{
type = builtin
label = param_eq
name = eq_after
config = { filename = "/home/tomfun/prj/tomfun/infrastructure/pulse_audio/eq_after_comp.txt" }
}
]
inputs = [
"mixL:In 1" # FL
"mixR:In 2" # FR
"dupeC:In" # FC
"dupeLfe:In" # LFE
"mixL:In 5" # SL
"mixR:In 6" # SR
]
# Wire EQ outputs into the L/R mixers
links = [
{ output = "dupeC:Out" input = "mixL:In 3" }
{ output = "dupeC:Out" input = "mixR:In 3" }
{ output = "dupeLfe:Out" input = "mixL:In 4" }
{ output = "dupeLfe:Out" input = "mixR:In 4" }
# FL -> L mixer
{ output = "mixL:Out" input = "eq_before:In 1" }
# FR -> R mixer
{ output = "mixR:Out" input = "eq_before:In 2" }
{ output="eq_before:Out 1" input="compressor:Left input" }
{ output="eq_before:Out 2" input="compressor:Right input" }
# compressor -> EQ(after)
{ output="compressor:Left output" input="eq_after:In 1" }
{ output="compressor:Right output" input="eq_after:In 2" }
]
# Final graph outputs: stereo
outputs = [
"eq_after:Out 1" # Left
"eq_after:Out 2" # Right
]
}
}
}
{
name = libpipewire-module-filter-chain
args = {
node.name = "eq_sven"
node.description = "EQ Sven + LFE"
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_sven_lfe
config = { filename = "/home/tomfun/prj/tomfun/infrastructure/pulse_audio/eq_sven.txt" }
}
]
}
}
}
{ name = libpipewire-module-zeroconf-discover flags=[ "nofail" ] }
{
# https://docs.pipewire.org/devel/page_module_roc_source.html
name = libpipewire-module-roc-source
args = {
fec.code = rs8m
local.ip = 10.50.10.8
# These must match whatever ports you configure on the desktop's roc-source
remote.source.port = 10001
remote.repair.port = 10002
remote.control.port = 10003
#roc.resampler.profile = high
sess.latency.msec = 120
source.name = "ROC-Source"
#sink.props = {
# node.name = "input.eq_before_comp"
#}
}
}
#{
# name = libpipewire-module-rtp-source
# args = {
# #local.ifname = eth0
# local.ifname = enp3s0
# #local.ifname = wg0
# #source.ip = 224.0.0.56
# #source.ip = 10.50.10.6
# #source.ip = 192.168.0.108
# source.ip = 0.0.0.0
# source.port = 26864
# sess.latency.msec = 200
# sess.ignore-ssrc = true
# #sess.ignore-ssrc = false
# node.always-process = true
# sess.ts-direct = true
# sess.media = "audio"
# #audio.format = "S16LE"
# audio.format = "S16BE"
# audio.rate = 48000
# audio.channels = 2
# audio.position = [ FL FR ]
# stream.props = {
# media.class = "Audio/Source"
# node.name = "RTP_from_Laptop"
# node.description = "RTP audio from laptop mic"
# }
# }
#}
#{
# name = libpipewire-module-rtp-sap
# args = {
# #local.ifname = "enp3s0"
# sap.ip = "10.50.10.6"
# sap.port = 26864
# #sap.cleanup.sec = 5
# source.ip = "0.0.0.0"
# #net.ttl = 1
# #net.loop = false
# stream.rules = [
# { matches = [
# # any of the items in matches needs to match, if one does,
# # actions are emitted.
# { # all keys must match the value. ! negates. ~ starts regex.
# #rtp.origin = "wim 3883629975 0 IN IP4 0.0.0.0"
# #rtp.payload = "127"
# #rtp.fmt = "L16/48000/2"
# #rtp.session = "PipeWire RTP Stream on fedora"
# #rtp.ts-offset = 0
# #rtp.ts-refclk = "private"
# sess.sap.announce = true
# }
# ]
# actions = {
# announce-stream = {
# }
# }
# }
# { matches = [
# { # all keys must match the value. ! negates. ~ starts regex.
# #rtp.origin = "wim 3883629975 0 IN IP4 0.0.0.0"
# #rtp.payload = "127"
# #rtp.fmt = "L16/48000/2"
# #rtp.session = "PipeWire RTP Stream on fedora"
# #rtp.ts-offset = 0
# #rtp.ts-refclk = "private"
# rtp.session = "~.*"
# }
# ]
# actions = {
# create-stream = {
# #sess.latency.msec = 100
# #sess.ts-direct = false
# #target.object = ""
# }
# }
# }
# ]
# }
#}
{ name = libpipewire-module-pulse-tunnel
args = {
tunnel.mode = source
# Set the remote address to tunnel to
pulse.server.address = "tcp:10.50.10.6"
#pulse.latency = 200
reconnect.interval.ms = 1000
audio.rate=48000
audio.channels=2
audio.position=[ FL FR ]
target.object=alsa_input.pci-0000_00_1f.3.analog-stereo
stream.props = {
media.class = "Audio/Source"
node.name = "pa_from_Laptop"
node.description = "audio from laptop mic"
}
}
}
]

❓ 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