LIBPIPEWIRE-MODULE-FILTER-CHAIN(7)
๐ NAME ๐
Okay bestie, gather round โ this page lives in section 7 of the Miscellaneous Information Manual, and its whole personality is libpipewire-module-filter-chain, aka the Filter-Chain. ๐ธ
๐ฆ DESCRIPTION ๐ฆ
So like, the filter-chain lets you create an arbitrary processing graph out of LADSPA, LV2, sofa, ffmpeg and builtin filters. Any combo. Not me stacking five different plugin ecosystems into one chain and it just works ๐ญ๐
And you can make this filter into a virtual sink/source, or sneak it in between any 2 nodes in the graph. Versatile. Main character energy. ๐
The filter chain is built with 2 streams โ this part matters, bestie:
- a capture stream providing the input to the filter chain ๐
- a playback stream sending out the filtered stream to the next nodes in the graph โจ
And the reason both ends of the filter-chain are built with streams? So the session manager can manage the configuration and connection with the sinks and sources automatically. No manual labour, we don't do that here. ๐
๐ MODULE NAME ๐
libpipewire-module-filter-chain
โจ MODULE OPTIONS โจ
node.descriptionโ a human readable name for the filter chain, so it's not just a mystery blob ๐ซถfilter.graph = []โ a description of the filter graph to run, see below ๐capture.props = {}โ properties to be passed to the input streamplayback.props = {}โ properties to be passed to the output stream
๐ง FILTER GRAPH DESCRIPTION ๐ง
The general structure of the graph description goes like this:
filter.graph = {
nodes = [
{
type = <ladspa | lv2 | builtin | sofa>
name = <name>
plugin = <plugin>
label = <label>
config = {
<configkey> = <value> ...
}
control = {
<controlname|controlindex> = <value> ...
}
}
...
]
links = [
{ output = <portname> input = <portname> }
...
]
inputs = [ <portname> ... ]
outputs = [ <portname> ... ]
capture.volumes = [
{ control = <portname> min = <value> max = <value> scale = <scale> } ...
]
playback.volumes = [
{ control = <portname> min = <value> max = <value> scale = <scale> } ...
]
}
๐ Nodes ๐
Nodes describe the processing filters in the graph. Wanna know what plugins, labels and port names you actually have? Use a tool like lv2ls or listplugins. Very demure, very helpful. ๐
typeis one ofladspa,lv2,builtin,sofa,ebur128offfmpeg.nameis the name for this node โ you might need it later to refer to this node and its ports when setting controls or making links. Remember it like a bestie's birthday. ๐pluginis the type specific plugin name.- For LADSPA plugins it will append
.soto find the shared object with that name in the LADSPA plugin path. - For LV2, this is the plugin URI obtained with
lv2ls. - For
builtin,sofaandebur128this is ignored. - For
ffmpegthis should befiltergraph.
- For LADSPA plugins it will append
labelis the type specific filter inside the plugin.- For LADSPA this is the label
- For LV2 this is unused
- For
builtin,sofaandebur128this is the name of the filter to use - For
ffmpegthis is an FFMpeg filtergraph description
configcontains a filter specific configuration section. Some plugins need this. (convolver,sofa,delay, ...)- For lv2, the config can contain a set of state key/value pairs. If the lv2 plugin supports the
LV2_STATE__interface, these values will be provided for the given keys.
- For lv2, the config can contain a set of state key/value pairs. If the lv2 plugin supports the
controlcontains the initial values for the control ports of the filter. normally these are given with the port name, but it is also possible to give the control index as the key. Sneaky, but valid. ๐
Some examples of ladspa and lv2 plugins, because seeing is believing:
filter.graph = {
nodes = [
{
# an example ladspa plugin
type = ladspa
name = pitch
plugin = "ladspa-rubberband"
label = "rubberband-r3-pitchshifter-mono"
control = {
# controls are using the ladspa port names as seen in analyseplugin
"Semitones" = -3
}
}
{
# an example lv2 plugin
type = lv2
name = pitch
plugin = "http://breakfastquay.com/rdf/lv2-rubberband#mono"
control = {
# controls are using the lv2 symbol as seen with lv2info
"semitones" = -3
}
}
{
# an example lv2 plugin with a state
type = lv2
name = neural
plugin = "http://aidadsp.cc/plugins/aidadsp-bundle/rt-neural-generic"
control = {
# use the port symbols as seen with lv2info
PRESENCE = 1.0
}
config = {
# the config contains state keys and values
"http://aidadsp.cc/plugins/aidadsp-bundle/rt-neural-generic#json" =
"/usr/lib64/lv2/rt-neural-generic.lv2/models/deer ink studios/tw40_blues_solo_deerinkstudios.json"
}
}
]
...
}
๐ Links ๐
Links can be made between ports of nodes, and the portname is given as <node_name>:<port_name>. Colon goes hard, honestly. โจ
You can tee the output of filters to multiple other filters. But if you want the output of multiple filters to go into one filter input port? You need a mixer, bestie. No cheating. ๐ซ
Also: links can be omitted when the graph has just 1 filter. Casual. ๐
๐ช Inputs and Outputs ๐ช
These are the entry and exit ports into the graph definition. Their number defines the number of channels used by the filter-chain. So like, the count is the channel count, periodt. ๐ข
The <portname> can be null when a channel is to be ignored. Ghosted channel. ๐ป
Each input/output in the graph can only be linked to one filter input/output. So:
- if the stream signal needs to be routed to multiple filters โ use the
copybuiltin filter - if multiple graph outputs need to go to one output stream โ use the
mixerbuiltin plugin
inputs and outputs can be omitted, in which case the filter-chain will use all inputs from the first filter and all outputs from the last filter node. The graph will then be duplicated as many times to match the number of input/output channels of the streams. Fan behaviour. ๐ฅ
If the graph has no inputs and the capture channels is set as 0, only the playback stream will be created. Likewise, if there are no outputs and the playback channels is 0, there will be no capture stream created. Balance. โ๏ธ
๐ Volumes ๐
Normally the volume of the sink/source is handled by the stream software volume. But with the capture.volumes and playback.volumes properties, this can be handled by a control port in the graph instead. Upgrade, honestly. ๐
- Use
capture.volumesfor the volume of the input of the filter (when for example used as a sink). - Use
playback.volumesfor the volume of the output of the filter (when for example used as a source).
The min and max values (defaults 0.0 and 1.0) respectively can be used to scale and translate the volume min and max values. โจ
Normally the control values are linear, and it is assumed that the plugin does not perform any scaling to the values. Wanna change that? Use the scale property. By default this is linear, but it can be set to cubic when the control applies a cubic transformation. ๐
๐ BUILTIN FILTERS ๐
There are some genuinely useful builtin filters available, bestie. For these, type should be builtin, and you select the specific builtin filter with the label of the filter node. Easy. ๐
๐ Mixer ๐
Use the mixer plugin if you have multiple input signals that need to be mixed together. It's giving merge, it's giving blend. ๐ซถ
The mixer plugin has up to 8 input ports labeled "In 1" to "In 8", and each one has a gain control labeled "Gain 1" to "Gain 8". There's an output port labeled "Out". Unused input ports will be ignored and won't cause overhead โ so no, you don't have to use all 8, slay. ๐
๐ฆ Copy ๐ฆ
Use the copy plugin if you need to copy a stream input signal to multiple filters. Duplication queen. ๐
It has one input port "In" and one output port "Out". Simple, clean, iconic.
๐ธ Biquads ๐ธ
Biquads can be used to do all kinds of filtering, and they're also what gets used when creating equalizers. The backbone, fr fr. ๐
All biquad filters have an input port "In" and an output port "Out". They have a "Freq", "Q" and "Gain" control โ and their meaning depends on the particular biquad that's being used. The biquads also have "b0", "b1", "b2", "a0", "a1" and "a2" ports that are read-only โ except for the bq_raw biquad, which can configure default values depending on the graph rate and change those at runtime. Special girl. โจ
We refer to https://arachnoid.com/BiQuadDesigner/index.html for an explanation of the controls. ๐
The following labels can be used:
bq_lowpassโ a lowpass filter.bq_highpassโ a highpass filter.bq_bandpassโ a bandpass filter.bq_lowshelfโ a low shelf filter.bq_highshelfโ a high shelf filter.bq_peakingโ a peaking filter.bq_notchโ a notch filter.bq_allpassโ an allpass filter.bq_rawโ a raw biquad filter. You need aconfigsection to specify coefficients per sample rate. The coefficients of the sample rate closest to the graph rate are selected: โ ๏ธ
filter.graph = {
nodes = [
{
type = builtin
name = ...
label = bq_raw
config = {
coefficients = [
{ rate = 44100, b0=.., b1=.., b2=.., a0=.., a1=.., a2=.. },
{ rate = 48000, b0=.., b1=.., b2=.., a0=.., a1=.., a2=.. },
{ rate = 192000, b0=.., b1=.., b2=.., a0=.., a1=.., a2=.. }
]
}
...
}
]
...
}
๐ Parametric EQ ๐
The parametric EQ chains a number of biquads together. It's more efficient than specifying a number of chained biquads, and it can also load configuration from a file. Efficiency and glamour? We love to see it. ๐
The parametric EQ supports multichannel processing and has 8 input and 8 output ports that don't all need to be connected. The ports are named In 1 to In 8 and Out 1 to Out 8. Pick your fighters. ๐
filter.graph = {
nodes = [
{
type = builtin
name = ...
label = param_eq
config = {
filename = "..."
#filename1 = "...", filename2 = "...", ...
filters = [
{ type = ..., freq = ..., gain = ..., q = ... },
{ type = ..., freq = ..., gain = ..., q = ... },
....
]
#filters1 = [ ... ], filters2 = [ ... ], ...
}
...
}
]
...
}
Either a filename or a filters array can be specified. The configuration will be used for all channels. Alternatively, filenameX or filtersX โ where X is the channel number (between 1 and 8) โ can be used to load a channel specific configuration. Per-channel customisation, we're obsessed. ๐โจ
The filename has to point at a parametric EQ config made by the AutoEQ project or Squiglink โ those two are literally the plug for EQ'ing your headphones or your IEMs to some target curve. The classic slay of this genre: EQ'ing to the Harman target curve, or EQ'ing one headphone/IEM to match another ๐
AutoEQ lives at https://github.com/jaakkopasanen/AutoEq and SquigLink is at https://squig.link/ โ go peek, iykyk.
A parametric EQ config from either of them looks like this:
Preamp: -6.8 dB
Filter 1: ON PK Fc 21 Hz Gain 6.7 dB Q 1.100
Filter 2: ON PK Fc 85 Hz Gain 6.9 dB Q 3.000
Filter 3: ON PK Fc 110 Hz Gain -2.6 dB Q 2.700
Filter 4: ON PK Fc 210 Hz Gain 5.9 dB Q 2.100
Filter 5: ON PK Fc 710 Hz Gain -1.0 dB Q 0.600
Filter 6: ON PK Fc 1600 Hz Gain 2.3 dB Q 2.700
So Fc, Gain and Q are the frequency, gain and Q factor, respectively. The fourth column can be PK, LSC or HSC โ that's peaking, low shelf and high shelf respectively. And ngl, most of the time it's peaking filters doing all the work.
The filters โ or the channel-specific filtersX where X is the channel between 1 and 8 โ can hold an array of filter specification objects with these keys, bestie:
typeis the filter type, pick one from the available biquad labels.freqis the frequency handed to the biquad.gainis the gain handed to the biquad.qis the Q handed to the biquad.
Which means you can absolutely run the param eq with no file at all and use every single biquad available. Versatile queen ๐
Convolver
The convolver applies an impulse response to a signal. It's the go-to for reverbs and virtual surround, and it's implemented with a fast FFT implementation (speed, honestly ๐จ).
It's got an input port "In" and an output port "Out". It needs a config section in the node declaration, shaped like this:
filter.graph = {
nodes = [
{
type = builtin
name = ...
label = convolver
config = {
blocksize = ...
tailsize = ...
gain = ...
delay = ...
filename = ...
offset = ...
length = ...
channel = ...
resample_quality = ...
latency = ...
}
...
}
]
...
}
blocksizeโ the size of the blocks used in the FFT. It's a value between64and256. If you don't specify it, it gets computed automatically from the number of samples in the file.tailsizeโ the size of the tail blocks used in the FFT.gainโ the overall gain to apply to the IR file.delayโ the extra delay to add to the IR. A float number is read as seconds, and an integer as samples. Using the delay in seconds is independent of the graph and IR rate, and it's the recommended way. Periodt.filenameโ the IR to load or create. Possible values are:/hilbertโ creates a hilbert function you can use to phase shift the signal by +/-90 degrees. Thelengthgets used as the number of coefficients. The default latency islength/2./diracโ creates a Dirac function that can be used as gain. The default latency is0.- A filename to load as the IR. This has to be a file format supported by
sndfile, or an inline IR written as"/ir:<rate>,<value1>,<value2>". The default latency of file IRs is0. [ filename, ... ]โ an array of filenames. Whichever file has the closest samplerate match with the graph samplerate gets used. (Squad of options, one winner โจ)
offsetโ the sample offset in the file as the start of the IR.lengthโ the number of samples to use as the IR.channelโ the channel to use from the file as the IR.resample_qualityโ the resample quality for when the IR doesn't match the graph samplerate.latencyโ the extra latency in seconds to report. When you leave it unspecified (or< 0.0), the default IR latency from thefilenameargument gets used.
Delay
The delay does exactly what it says โ delays a signal in time ๐ฐ๏ธ. But with the "Feedback" and "Feedforward" controls it can also serve as a comb filter and an allpass filter.
It has an input port "In" and an output port "Out". It also has a "Delay (s)" control port, plus "Feedback" and "Feedforward" ports. It needs a config section in the node declaration shaped like this:
filter.graph = {
nodes = [
{
type = builtin
name = ...
label = delay
config = {
"max-delay" = ...
"latency" = ...
}
control = {
"Delay (s)" = ...
"Feedback" = ...
"Feedforward" = ...
}
...
}
]
...
}
max-delayโ the maximum delay in seconds. The"Delay (s)"parameter gets clamped to this value.latencyโ the latency in seconds. It's0by default, but in some cases the delay can be used to introduce latency with this option.
With the "Feedback" port you can build a comb filter. Set the "Feedback" port and the "Feedforward" port to A and -A respectively, and boom โ an allpass filter. These settings are how you go about crafting custom reverb units ๐ธ
Invert
The invert plugin inverts the phase of the signal. That's the whole job, and it does it flawlessly ๐
It has an input port "In" and an output port "Out".
Clamp
The clamp plugin clamps samples between min and max values. No more, no less ๐
It has an input port "In" and an output port "Out". It also has a "Control" and "Notify" port for the control values.
The final result gets clamped to the "Min" and "Max" control values.
Linear
The linear plugin applies a linear transformation to samples or control values. Math but make it fashion ๐
It has an input port "In" and an output port "Out". It also has a "Control" and "Notify" port for the control values.
The control values "Mult" and "Add" configure the linear transform. Every sample or control value gets calculated as: new = old * Mult + Add.
Reciprocal
The recip plugin calculates the reciprocal (1/x) of samples or control values. Flip the fraction, bestie ๐
It has an input port "In" and an output port "Out". It also has a "Control" and "Notify" port for the control values.
Abs
The abs plugin calculates the absolute value of samples. Negativity? Not on its watch โจ
It has an input port "In" and an output port "Out".
Sqrt
The sqrt plugin calculates the square root of samples. Yes, it's giving math class, but the useful kind ๐งฎ
It has an input port "In" and an output port "Out".
Exp
The exp plugin calculates the exponential (base^x) of samples or control values. Growing exponentially? Iconic behaviour only ๐
It has an input port "In" and an output port "Out". It also has a "Control" and "Notify" port for the control values.
The control value "Base" is used to calculate base ^ x for each sample.
Log
The log plugin calculates the logarithm of samples or control values. The inverse girlie of exp ๐
It has an input port "In" and an output port "Out". It also has a "Control" and "Notify" port for the control values.
The control values "Base", "M1" and "M2" are used to calculate out = M2 * log2f(fabsf(in * M1)) / log2f(Base) for each sample.
Multiply
The mult plugin multiplies samples together. It's literally the group project where everyone contributes โ no free riders here ๐
It has 8 input ports named "In 1" to "In 8" and an output port "Out".
All input port samples get multiplied together into the output. Unused input ports are ignored and cause no overhead at all. Zero waste, we love to see it โป๏ธ
Sine
The sine plugin generates a sine wave, obvi ๐
It has an output port "Out" and also a control output port "notify".
"Freq", "Ampl", "Offset" and "Phase" control the sine wave frequency, amplitude, offset and phase respectively.
Max
Use the max plugin when you need the max value out of a bunch of input ports. Only the best, no crumbs ๐
It has 8 input ports named "In 1" to "In 8" and one output port "Out".
All input port samples get checked to find the maximum value per sample. Unused input ports are ignored and cause no overhead.
dcblock
The dcblock plugin implements a DC blocker. Say bye to that DC offset, fr fr โ๏ธ
It has 8 input ports "In 1" to "In 8" and corresponding output ports "Out 1" to "Out 8". Not all ports need to be connected โ connect however many you actually need.
It also has 1 control input port "R" that controls the DC block R factor.
Ramp
The ramp plugin creates a linear ramp from Start to Stop. Glow up, but gradual ๐โจ
It has 3 input control ports "Start", "Stop" and "Duration (s)". It also has one output port "Out". A linear ramp gets created from "Start" to "Stop" over a duration given by the "Duration (s)" control in (fractional) seconds. The current value is stored in the output notify port "Current".
The ramp output can, for example, be fed into the mult plugin to create a volume ramp up or down. For a more coarse volume ramp, the "Current" value can be used in the linear plugin.
Debug
The debug plugin is your little spy for debugging the audio and control data of other plugins. Nosy in the best way ๐ต๏ธ
It has an "In" input port and an "Out" output data port. The data from "In" gets copied to "Out", and that data gets dumped into the INFO log.
There's also a "Control" input port and a "Notify" output control port. The control from "Control" gets copied to "Notify", and the control value gets dumped into the INFO log.
Zeroramp
The zeroramp plugin detects unnatural silence parts in the audio stream and ramps the volume down or up when entering or leaving the silent area respectively. It's out here preventing the loud pops and clicks that happen when sample values suddenly drop to zero or jump from zero to a large value โ like when a stream pauses, resumes, or errors out ๐ญ. Heads up: it only detects areas where the sample values are absolute zero values, like the ones inserted when you pause a stream.
It has an "In" input port and an "Out" output data port.
There are also "Gap (s)" and "Duration (s)" input control ports. "Gap (s)" determines how long the silence gap is in seconds (default 0.000666) and "Duration (s)" determines how long the fade-in and fade-out should last (default 0.000666).
Noisegate
The noisegate plugin removes low volume noise. Quiet your haters, literally ๐คซ
It has an "In" input port and an "Out" output data port. Normally the input data is passed directly to the output.
The "Level" control port can be used to control the measured volume of the "In" port. When it's not connected, a simple volume algorithm on the "In" port gets used instead.
okay so continued ๐
if the volume drops below the "Close threshold", the noisegate is gonna ramp that volume all the way down to zero, and it takes "Release (s)" seconds to do it. But when the volume climbs above the "Open threshold", it ramps the volume back up to 1, taking "Attack (s)" seconds. And bestie, the gate holds itself open for at least "Hold (s)" seconds before it's even allowed to close again. no rushing it ๐ซถ
๐ Busy
The busy plugin is a whole vibe โ it has no input or output ports, and its entire job is to keep the CPU or the graph busy for a given percent of the time. it's giving "productive but makes no sound" ๐
The node needs a config section with extra configuration:
filter.graph = {
nodes = [
{
type = builtin
name = ...
label = busy
config = {
wait-percent = 0.0
cpu-percent = 50.0
}
...
}
]
...
}
wait-percentโ the percentage of time to wait. This keeps the graph busy but not the CPU. Default0.0.cpu-percentโ the percentage of time to keep the CPU busy. This keeps both the graph and the CPU busy. Default0.0.
๐ธ Null
The null plugin is iconic in its simplicity: it has one data input, "In", and one control input, "Control", and it simply discards the data. that's it. that's the plugin. ate and left no crumbs ๐
๐ฆ SOFA FILTERS
There's an optional sofa type available โ but only when you compile with libmysofa. iykyk โจ
๐ Spatializer
The spatializer is the main character that places the sound in a 3D space. ๐ง
It's got an input port "In" and a stereo pair of output ports called "Out L" and "Out R". And it requires a config section in the node declaration, in this format:
The control can be changed at runtime to move the sounds around in the 3D space. lowkey magical ๐
filter.graph = {
nodes = [
{
type = sofa
name = ...
label = spatializer
config = {
blocksize = ...
tailsize = ...
filename = ...
gain = ...
latency = ...
normalize = ...
}
control = {
"Azimuth" = ...
"Elevation" = ...
"Radius" = ...
}
...
}
]
...
}
blocksizeโ specifies the size of the blocks to use in the FFT. It's a value between64and256. When not specified, this value is computed automatically from the number of samples in the file.tailsizeโ specifies the size of the tail blocks to use in the FFT.filenameโ the SOFA file to load. SOFA files usually end in the.sofaextension and contain the HRTF for the various spatial positions.gainโ the overall gain to apply to the IR file, default1.0.latencyโ the latency introduced by the filter, default0.normalizeโ automatically normalize the loudness of the IR, defaultfalse.Azimuthโ controls the azimuth, this is the direction the sound is coming from in degrees between0and360.0is straight ahead.90is left,180behind,270right.Elevationโ controls the elevation, this is how high/low the signal is in degrees between-90and90.0is straight in front,90is directly above and-90directly below.Radiusโ controls how far away the signal is as a value between0and100. default is1.0.
โญ EBUR128 FILTERS
There is an optional EBU R128 plugin available โ only when compiled with libebur128 โ and you select it with the ebur128 type. Filters inside the plugin get selected with the label field. ๐
๐ ebur128
The ebur128 filter is here to measure the loudness of a signal. periodt.
It has 7 input ports: "In FL", "In FR", "In FC", "In UNUSED", "In SL", "In SR" and "In DUAL MONO" โ these correspond to the different input channels for EBUR128. Not all ports need to be connected for this filter, ngl that's a relief ๐ฎโ๐จ
The input signal gets passed unmodified on the "Out FL", "Out FR", "Out FC", "Out UNUSED", "Out SL", "Out SR" and "Out DUAL MONO" output ports.
There are also 7 output control ports holding the measured loudness info, and you can use them to control the processing of the audio. Some of these ports contain values in LUFS, aka "Loudness Units relative to Full Scale". Heads up: these are negative values, and closer to 0 is louder. You can use the lufs2gain plugin to convert this value into a gain to adjust a volume (see below ๐).
- "Momentary LUFS" โ contains the momentary loudness measurement with a
400mswindow and75%overlap. It works mostly like an R.M.S. meter. - "Shortterm LUFS" โ contains the shortterm loudness in LUFS over a
3second window. - "Global LUFS" โ contains the global integrated loudness in LUFS over the
max-historywindow. "Window LUFS" contains the global integrated loudness in LUFS over themax-windowwindow. - "Range LU" โ contains the loudness range (LRA) in LU units.
- "Peak" โ contains the peak loudness.
- "True Peak" โ contains the true peak loudness oversampling the signal. This can more accurately reflect the peak compared to "Peak".
The node also has an optional config section with extra configuration:
filter.graph = {
nodes = [
{
type = ebur128
name = ...
label = ebur128
config = {
max-history = ...
max-window = ...
use-histogram = ...
}
...
}
]
...
}
max-historyโ the maximum history to keep in (float) seconds. Default to10.0.max-windowโ the maximum window to keep in (float) seconds. Default to0.0. You will need to set this to some value to get "Window LUFS" output control values. not me realizing the default gives you nothing ๐use-histogramโ uses the histogram algorithm to calculate loudness. Defaults tofalse.
๐ง lufs2gain
The lufs2gain filter converts LUFS control values into gain. It needs a target LUFS control input to drive the conversion. no target, no conversion, bestie.
It has 2 input control ports, "LUFS" and "Target LUFS", and it produces 1 output control value: "Gain". This gain can be used as input for the builtin linear filter, for example, to adust the gain. ๐ซถ
๐ฅ FFMPEG
There's an optional FFmpeg filter available โ only when compiled with libavfilter โ and you select it with the ffmpeg type. Use the plugin field to pick which plugin you want. ๐
๐ Filtergraph
The filtergraph FFmpeg plugin is selected with the filtergraph plugin field in the node.
The filtergraph filter lets you specify a whole set of audio filters using the FFmpeg filtergraph syntax (https://ffmpeg.org/ffmpeg-filters.html).
The label field should be used to describe the filtergraph in use.
FFmpeg filtergraph input and output ports can have multiple channels. The filter-chain can split those into individual ports to use as input and output ports. For this, the ports in the filtergraph need a specific name convention, either <port-name>_<channel-name> or <port-name>_<channel-layout>.
When a single channel is specified, the port can be referenced in inputs and outputs sections with <name>:<port-name>_<channel-name>. When a channel-layout is specified, each port name gets a _<number> appended, starting from 0 and counting up for each channel in the layout.
The filtergraph plugin will automatically add format converters when the input port channel-layout, format or graph sample-rates don't match. so thoughtful of it โจ
โ ๏ธ Note that the FFmpeg filtergraph is not Real-time safe because it might do allocations from the processing thread. It is advised to run thefilter-chainstreams in async mode (node.async = true) to avoid interrupting the other RT threads. do NOT skip this one bestie ๐ญ
Some examples:
The stereo ports are split into their channels with the _0 and _1 suffixes.
filter.graph = {
nodes = [
{
type = ffmpeg
plugin = filtergraph
name = filter
label = "[in_stereo]loudnorm=I=-18:TP=-3:LRA=4[out_stereo]"
}
]
inputs = [ "filter:in_stereo_0" "filter:in_stereo_1" ]
outputs = [ "filter:out_stereo_0" "filter:out_stereo_1" ]
...
}
It's possible to have multiple input and output ports for the filtergraphs. In the next example, the ports have a single channel name and so don't have the _0 suffix to identify them. This can be simplified by removing the amerge and channelsplit filters and using the _stereo suffix on port names to let PipeWire do the splitting and merging more efficiently. lowkey a glow-up ๐ธ
filter.graph = {
nodes = [
{
type = ffmpeg
plugin = filtergraph
name = filter
label = "[in_FL][in_FR]amerge,extrastereo,channelsplit[out_FL][out_FR]"
}
]
inputs = [ "filter:in_FL" "filter:in_FR" ]
outputs = [ "filter:out_FL" "filter:out_FR" ]
...
}
Here's a last example of a surround sound upmixer:
filter.graph = {
nodes = [
{
type = ffmpeg
plugin = filtergraph
name = filter
label = "[in_stereo]surround[out_5.1]"
}
]
inputs = [ "filter:in_FL" "filter:in_FR" ]
outputs = [ "filter:out_5.1_0" "filter:out_5.1_1" "filter:out_5.1_2"
"filter:out_5.1_3" "filter:out_5.1_4" "filter:out_5.1_5" ]
...
}
๐ง ONNX FILTERS ๐ง
Okay bestie there's a lil optional filter in here for the machine-learning girlies: an ONNX filter, available only when it's compiled with libonnxruntime, and you pick it with the onnx type. Use the label field to choose the model and to tell it how the tensors map to ports. โจ
filter.graph = {
nodes = [
{
type = onnx
name = onnx
label = {
filename = "..."
blocksize = 512
input-tensors = {
"<name>" = {
dimensions = [ ... ]
#retain = 64
data = "port:..."|"tensor:..."|"param:..."|"control:..."
}
...
}
output-tensors = {
"<name>" = {
dimensions = [ ... ]
#retain = 64
data = "port:..."|"tensor:..."|"param:..."|"control:..."
}
...
}
}
}
]
...
}
The label has to contain an object with all the config for the plugin. ๐ซถ
filenameโ the ONNX model to load. It literally has to point at an onnx file that actually exists, no pretending.blocksizeโ the number of samples you hand over to the model. This depends on the model and on the size of the input/output tensors, so don't just vibes-pick it.input-tensorsโ an object of the model's input tensors and how they should be used. Tensors you don't list here will not be used, periodt.output-tensorsโ an object of the model's output tensors and how they should be used. Same deal: unlisted tensors will not be used. ๐
The input-tensors and output-tensors config must contain an object whose keys are named after the tensors in the model, and each value must be an object with the following keys:
dimensionsโ and array of dimensions of the tensors. (yes bestie that's what it says, an array of dims.)retainโ an optional key for input tensors only. This will prepend the lastretainsamples from the previous block to the input tensor. So the size of that tensor should at least beblocksize + retainsamples large. Do the math, don't cry about it. ๐ญdataโ where the data for the tensor is coming from. There are different options depending on the value of this file, and you select them with a prefix:port:<portname>โ a brand new input/output port gets created on the plugin with the name<portname>, and the data for the tensor will be obtained or copied from/to the port data.tensor:<tensorname>โ the data of this tensor is copied from the given<tensorname>. You can use this to copy output state info into the input state, for example. Very giving. ๐param:<paramname>โ the data of this tensor is obtained from a parameter called<paramname>. Right now onlyrateis a valid paramname, and it holds the value of the filter samplerate.control:<portname>โ a new input/output control port is created and the tensor data will be obtained/copied from/to the control data.
Here's a whole example of the silero VAD model, iconic:
filter.graph = {
nodes = [
{
type = onnx
name = onnx
label = {
filename = "/home/wim/src/silero-vad/src/silero_vad/data/silero_vad.onnx"
blocksize = 512
input-tensors = {
"input" = {
dimensions = [ 1, 576 ]
retain = 64
data = "port:input"
}
"state" = {
dimensions = [ 2, 1, 128 ]
data = "tensor:stateN"
}
"sr" = {
dimensions = [ 1 ]
data = "param:rate"
}
}
output-tensors = {
"output" = {
dimensions = [ 1, 1 ]
data = "control:speech"
}
"stateN" = {
dimensions = [ 2, 1, 128 ]
}
}
}
}
...
]
....
}
๐ GENERAL OPTIONS ๐
Options with well-known behavior, the classics. Most options can be added to the global configuration or to the individual streams, your call bestie:
remote.nameโ the remote name, iykykaudio.rateโ the sample rateaudio.channelsโ how many channelsaudio.layoutโ the channel layoutaudio.positionโ the channel positionsmedia.nameโ the media namenode.latencyโ the node latencynode.descriptionโ the node descriptionnode.groupโ the node groupnode.link.groupโ the link groupnode.virtualโ whether the node is virtualnode.nameโ see the notes below. If it's not specified, it defaults to'filter-chain-PID-MODULEID'.
Stream only properties: โจ these ones are stream-only, no sharing:
media.classโ the media classnode.nameโ if it's not given per stream, the globalnode.namewill be prefixed with'input.'and'output.'to generate a capture and playback streamnode.namerespectively.
๐ EXAMPLE CONFIGURATION OF A VIRTUAL SOURCE ๐
This example uses the rnnoise LADSPA plugin to create a whole new virtual source. Main character energy.
Run it with pipewire -c filter-chain.conf. The configuration can also be put under pipewire.conf.d/ to run it inside the PipeWire server. ๐
# ~/.config/pipewire/filter-chain.conf.d/my-filter-chain-1.conf
context.modules = [
{ name = libpipewire-module-filter-chain
args = {
node.description = "Noise Canceling source"
media.name = "Noise Canceling source"
filter.graph = {
nodes = [
{
type = ladspa
name = rnnoise
plugin = ladspa/librnnoise_ladspa
label = noise_suppressor_stereo
control = {
"VAD Threshold (%)" 50.0
}
}
]
}
capture.props = {
node.name = "capture.rnnoise_source"
node.passive = true
}
playback.props = {
node.name = "rnnoise_source"
media.class = Audio/Source
}
}
}
]
๐ EXAMPLE CONFIGURATION OF A DOLBY SURROUND ENCODER VIRTUAL SINK ๐
This one uses the ladpsa surround encoder to encode a 5.1 signal into a stereo Dolby Surround signal. Ate. ๐ฅ
# ~/.config/pipewire/filter-chain.conf.d/my-filter-chain-2.conf
context.modules = [
{ name = libpipewire-module-filter-chain
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 ]
}
}
}
]
That's the whole page bestie, you made it to the end, I'm so proud of you ๐ฅน๐ซถ This lovely little manual was brought to you by PipeWire version 1.6.11 (the libpipew...ter-chain(7) page), and it's dated to the moment it was generated. Go forth and chain some filters, you absolute legend. ๐
โจ๐ธ
๐ฆ where this page comes from
- fedora 44: pipewire-libs 1.6.11-1.fc44 ยท MIT AND GPL-2.0-or-later AND BSD-2-Clause AND LGPL-2.0-or-later
- fedora 44: pipewire-libs 1.6.11-1.fc44 ยท MIT AND GPL-2.0-or-later AND BSD-2-Clause AND LGPL-2.0-or-later
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms