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 stream
  • playback.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. ๐Ÿ”

  • type is one of ladspa, lv2, builtin, sofa, ebur128 of ffmpeg.
  • name is 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. ๐ŸŽ‚
  • plugin is the type specific plugin name.
    • For LADSPA plugins it will append .so to find the shared object with that name in the LADSPA plugin path.
    • For LV2, this is the plugin URI obtained with lv2ls.
    • For builtin, sofa and ebur128 this is ignored.
    • For ffmpeg this should be filtergraph.
  • label is the type specific filter inside the plugin.
    • For LADSPA this is the label
    • For LV2 this is unused
    • For builtin, sofa and ebur128 this is the name of the filter to use
    • For ffmpeg this is an FFMpeg filtergraph description
  • config contains 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.
  • control contains 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 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 copy builtin filter
  • if multiple graph outputs need to go to one output stream โ†’ use the mixer builtin 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.volumes for the volume of the input of the filter (when for example used as a sink).
  • Use playback.volumes for 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 a config section 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:

  • type is the filter type, pick one from the available biquad labels.
  • freq is the frequency handed to the biquad.
  • gain is the gain handed to the biquad.
  • q is 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 between 64 and 256. 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. The length gets used as the number of coefficients. The default latency is length/2.
    • /dirac โ€” creates a Dirac function that can be used as gain. The default latency is 0.
    • 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 is 0.
    • [ 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 the filename argument 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's 0 by 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. Default 0.0.
  • cpu-percent โ€” the percentage of time to keep the CPU busy. This keeps both the graph and the CPU busy. Default 0.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 between 64 and 256. 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 .sofa extension and contain the HRTF for the various spatial positions.
  • gain โ€” the overall gain to apply to the IR file, default 1.0.
  • latency โ€” the latency introduced by the filter, default 0.
  • normalize โ€” automatically normalize the loudness of the IR, default false.
  • Azimuth โ€” controls the azimuth, this is the direction the sound is coming from in degrees between 0 and 360. 0 is straight ahead. 90 is left, 180 behind, 270 right.
  • Elevation โ€” controls the elevation, this is how high/low the signal is in degrees between -90 and 90. 0 is straight in front, 90 is directly above and -90 directly below.
  • Radius โ€” controls how far away the signal is as a value between 0 and 100. default is 1.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 400ms window and 75% overlap. It works mostly like an R.M.S. meter.
  • "Shortterm LUFS" โ€” contains the shortterm loudness in LUFS over a 3 second window.
  • "Global LUFS" โ€” contains the global integrated loudness in LUFS over the max-history window. "Window LUFS" contains the global integrated loudness in LUFS over the max-window window.
  • "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 to 10.0.
  • max-window โ€” the maximum window to keep in (float) seconds. Default to 0.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 to false.

๐Ÿงš 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 the filter-chain streams 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 last retain samples from the previous block to the input tensor. So the size of that tensor should at least be blocksize + retain samples 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 only rate is 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, iykyk
  • audio.rate โ€” the sample rate
  • audio.channels โ€” how many channels
  • audio.layout โ€” the channel layout
  • audio.position โ€” the channel positions
  • media.name โ€” the media name
  • node.latency โ€” the node latency
  • node.description โ€” the node description
  • node.group โ€” the node group
  • node.link.group โ€” the link group
  • node.virtual โ€” whether the node is virtual
  • node.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 class
  • node.name โ€” if it's not given per stream, the global node.name will be prefixed with 'input.' and 'output.' to generate a capture and playback stream node.name respectively.

๐Ÿ’– 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