PipeWire  1.6.4
Tutorial - Part 7: Creating an Audio DSP Filter

Tutorial - Part 6: Binding Objects | Index

In this tutorial we show how to use pw_filter to create a real-time audio processing filter. This is useful for implementing audio effects, equalizers, analyzers, and other DSP applications.

Let's take a look at the code before we break it down:

#include <stdio.h>
#include <errno.h>
#include <math.h>
#include <signal.h>
struct data;
struct port {
struct data *data;
};
struct data {
struct pw_main_loop *loop;
struct pw_filter *filter;
struct port *in_port;
struct port *out_port;
};
/* [on_process] */
static void on_process(void *userdata, struct spa_io_position *position)
{
struct data *data = userdata;
float *in, *out;
uint32_t n_samples = position->clock.duration;
pw_log_trace("do process %d", n_samples);
in = pw_filter_get_dsp_buffer(data->in_port, n_samples);
out = pw_filter_get_dsp_buffer(data->out_port, n_samples);
if (in == NULL || out == NULL)
return;
/* Simple passthrough - copy input to output.
* Here you could implement any audio processing:
* - Filters (lowpass, highpass, bandpass)
* - Effects (reverb, delay, distortion)
* - Dynamic processing (compressor, limiter)
* - Equalization
* - etc.
*/
memcpy(out, in, n_samples * sizeof(float));
}
/* [on_process] */
static const struct pw_filter_events filter_events = {
.process = on_process,
};
static void do_quit(void *userdata, int signal_number)
{
struct data *data = userdata;
pw_main_loop_quit(data->loop);
}
int main(int argc, char *argv[])
{
struct data data = { 0, };
const struct spa_pod *params[1];
uint32_t n_params = 0;
uint8_t buffer[1024];
pw_init(&argc, &argv);
/* make a main loop. If you already have another main loop, you can add
* the fd of this pipewire mainloop to it. */
data.loop = pw_main_loop_new(NULL);
pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data);
/* Create a simple filter, the simple filter manages the core and remote
* objects for you if you don't need to deal with them.
*
* Pass your events and a user_data pointer as the last arguments. This
* will inform you about the filter state. The most important event
* you need to listen to is the process event where you need to process
* the data.
*/
"audio-filter",
NULL),
&filter_events,
&data);
/* make an audio DSP input port */
data.in_port = pw_filter_add_port(data.filter,
sizeof(struct port),
PW_KEY_FORMAT_DSP, "32 bit float mono audio",
PW_KEY_PORT_NAME, "input",
NULL),
NULL, 0);
/* make an audio DSP output port */
data.out_port = pw_filter_add_port(data.filter,
sizeof(struct port),
PW_KEY_FORMAT_DSP, "32 bit float mono audio",
PW_KEY_PORT_NAME, "output",
NULL),
NULL, 0);
/* Set processing latency information */
params[n_params++] = spa_process_latency_build(&b,
.ns = 10 * SPA_NSEC_PER_MSEC
));
/* Now connect this filter. We ask that our process function is
* called in a realtime thread. */
if (pw_filter_connect(data.filter,
params, n_params) < 0) {
fprintf(stderr, "can't connect\n");
return -1;
}
/* and wait while we let things run */
return 0;
}
spa/pod/builder.h
buffer[1023]
Definition: core.h:437
int pw_filter_connect(struct pw_filter *filter, enum pw_filter_flags flags, const struct spa_pod **params, uint32_t n_params)
Connect a filter for processing.
Definition: filter.c:1570
void * pw_filter_add_port(struct pw_filter *filter, enum pw_direction direction, enum pw_filter_port_flags flags, size_t port_data_size, struct pw_properties *props, const struct spa_pod **params, uint32_t n_params)
add a port to the filter, returns user data of port_data_size.
Definition: filter.c:1813
struct pw_filter * pw_filter_new_simple(struct pw_loop *loop, const char *name, struct pw_properties *props, const struct pw_filter_events *events, void *data)
Definition: filter.c:1292
void * pw_filter_get_dsp_buffer(void *port_data, uint32_t n_samples)
Get a data pointer to the buffer data.
Definition: filter.c:2054
#define PW_VERSION_FILTER_EVENTS
Definition: filter.h:67
void pw_filter_destroy(struct pw_filter *filter)
Destroy a filter
Definition: filter.c:1418
@ PW_FILTER_FLAG_RT_PROCESS
call process from the realtime thread.
Definition: filter.h:112
@ PW_FILTER_PORT_FLAG_MAP_BUFFERS
mmap the buffers except DmaBuf that is not explicitly marked as mappable.
Definition: filter.h:134
#define PW_KEY_PORT_NAME
port name
Definition: keys.h:346
#define PW_KEY_MEDIA_TYPE
Media.
Definition: keys.h:512
#define PW_KEY_MEDIA_ROLE
Role: Movie, Music, Camera, Screen, Communication, Game, Notification, DSP, Production,...
Definition: keys.h:518
#define PW_KEY_MEDIA_CATEGORY
Media Category: Playback, Capture, Duplex, Monitor, Manager.
Definition: keys.h:515
#define PW_KEY_FORMAT_DSP
format related properties
Definition: keys.h:555
#define pw_log_trace(...)
Definition: log.h:182
PW_API_LOOP_IMPL struct spa_source * pw_loop_add_signal(struct pw_loop *object, int signal_number, spa_source_signal_func_t func, void *data)
Definition: loop.h:177
struct pw_main_loop * pw_main_loop_new(const struct spa_dict *props)
Create a new main loop.
Definition: main-loop.c:63
int pw_main_loop_quit(struct pw_main_loop *loop)
Quit a main loop.
Definition: main-loop.c:108
void pw_main_loop_destroy(struct pw_main_loop *loop)
Destroy a loop.
Definition: main-loop.c:73
int pw_main_loop_run(struct pw_main_loop *loop)
Run a main loop.
Definition: main-loop.c:122
struct pw_loop * pw_main_loop_get_loop(struct pw_main_loop *loop)
Get the loop implementation.
Definition: main-loop.c:96
void pw_init(int *argc, char **argv[])
Initialize PipeWire.
Definition: pipewire.c:499
void pw_deinit(void)
Deinitialize PipeWire.
Definition: pipewire.c:614
#define PW_DIRECTION_OUTPUT
Definition: port.h:55
#define PW_DIRECTION_INPUT
Definition: port.h:53
struct pw_properties * pw_properties_new(const char *key,...)
Make a new properties object.
Definition: properties.c:97
#define SPA_PROCESS_LATENCY_INFO_INIT(...)
Definition: latency.h:92
SPA_API_LATENCY_UTILS struct spa_pod * spa_process_latency_build(struct spa_pod_builder *builder, uint32_t id, const struct spa_process_latency_info *info)
Definition: latency-utils.h:143
@ SPA_PARAM_ProcessLatency
processing latency, a SPA_TYPE_OBJECT_ParamProcessLatency
Definition: param.h:46
#define SPA_POD_BUILDER_INIT(buffer, size)
Definition: builder.h:72
#define SPA_NSEC_PER_MSEC
Definition: defs.h:255
pipewire/pipewire.h
pipewire/filter.h
Events for a filter.
Definition: filter.h:65
A main loop object.
uint64_t duration
Duration of current cycle, in samples @ rate.
Definition: io.h:193
The position information adds extra meaning to the raw clock times.
Definition: io.h:353
struct spa_io_clock clock
clock position of driver, always valid and read only
Definition: io.h:354
Definition: builder.h:63
void * data
Definition: builder.h:64
Definition: pod.h:57

Save as tutorial7.c and compile with:

gcc -Wall tutorial7.c -o tutorial7 -lm $(pkg-config --cflags --libs libpipewire-0.3)

Overview

Unlike pw_stream which is designed for applications that produce or consume audio data, pw_filter is designed for applications that process existing audio streams. Filters have both input and output ports and operate in the DSP domain using 32-bit floating point samples.

Setting up the Filter

We start with the usual boilerplate and define our data structure:

struct data {
struct pw_main_loop *loop;
struct pw_filter *filter;
struct port *in_port;
struct port *out_port;
};

The filter object manages both input and output ports. Each port represents an audio channel that can be connected to other applications.

Creating the Filter

data.filter = pw_filter_new_simple(
"audio-filter",
NULL),
&filter_events,
&data);

We use pw_filter_new_simple() which automatically manages the core connection for us. The properties are important:

  • PW_KEY_MEDIA_TYPE: "Audio" indicates this is an audio filter
  • PW_KEY_MEDIA_CATEGORY: "Filter" tells the session manager this processes audio
  • PW_KEY_MEDIA_ROLE: "DSP" indicates this is for audio processing

Adding Ports

Next we add input and output ports:

data.in_port = pw_filter_add_port(data.filter,
sizeof(struct port),
PW_KEY_FORMAT_DSP, "32 bit float mono audio",
PW_KEY_PORT_NAME, "input",
NULL),
NULL, 0);
data.out_port = pw_filter_add_port(data.filter,
sizeof(struct port),
PW_KEY_FORMAT_DSP, "32 bit float mono audio",
PW_KEY_PORT_NAME, "output",
NULL),
NULL, 0);

Key points about filter ports:

  • PW_DIRECTION_INPUT and PW_DIRECTION_OUTPUT specify the port direction
  • PW_FILTER_PORT_FLAG_MAP_BUFFERS allows direct memory access to buffers
  • PW_KEY_FORMAT_DSP indicates this uses 32-bit float DSP format
  • DSP ports work with normalized floating-point samples (typically -1.0 to 1.0)

Setting Process Latency

This tells PipeWire that our filter adds 10 milliseconds of processing latency. This information helps the audio system maintain proper timing and latency compensation throughout the audio graph.

Connecting the Filter

if (pw_filter_connect(data.filter,
params, n_params) < 0) {
fprintf(stderr, "can't connect\n");
return -1;
}

The PW_FILTER_FLAG_RT_PROCESS flag ensures our process callback runs in the real-time audio thread. This is crucial for low-latency audio processing but means our process function must be real-time safe (no allocations, file I/O, or blocking operations).

The Process Callback

The heart of the filter is the process callback:

static void on_process(void *userdata, struct spa_io_position *position)
{
struct data *data = userdata;
float *in, *out;
uint32_t n_samples = position->clock.duration;
pw_log_trace("do process %d", n_samples);
in = pw_filter_get_dsp_buffer(data->in_port, n_samples);
out = pw_filter_get_dsp_buffer(data->out_port, n_samples);
if (in == NULL || out == NULL)
return;
/* Simple passthrough - copy input to output.
* Here you could implement any audio processing:
* - Filters (lowpass, highpass, bandpass)
* - Effects (reverb, delay, distortion)
* - Dynamic processing (compressor, limiter)
* - Equalization
* - etc.
*/
memcpy(out, in, n_samples * sizeof(float));
}

The process function is called for each audio buffer and works as follows:

  1. Get the number of samples to process from position->clock.duration
  2. Get input and output buffer pointers using pw_filter_get_dsp_buffer()
  3. Process the audio data (here we just copy input to output)
  4. The framework handles queueing the processed buffers

Key Points about DSP Processing:

  • Float Format: DSP buffers use 32-bit float samples, typically normalized to [-1.0, 1.0]
  • Real-time Safe: The process function runs in the audio thread and must be real-time safe
  • Buffer Management: pw_filter_get_dsp_buffer() handles the buffer lifecycle automatically
  • Sample-accurate: Processing happens at the audio sample rate with precise timing

Advanced Usage

This example shows a simple passthrough, but you can implement any audio processing:

/* Example: Simple volume control */
for (uint32_t i = 0; i < n_samples; i++) {
out[i] = in[i] * 0.5f; // Reduce volume by half
}
/* Example: Simple high-pass filter */
static float last_sample = 0.0f;
float alpha = 0.99f;
for (uint32_t i = 0; i < n_samples; i++) {
out[i] = alpha * (out[i] + in[i] - last_sample);
last_sample = in[i];
}

Comparison with pw_stream

Feature pw_stream pw_filter
Use case Audio playback/recording Audio processing/effects
Data format Various (S16, S32, etc.) 32-bit float DSP
Ports Single direction Input and output
Buffer management Manual queue/dequeue Automatic via get_dsp_buffer
Typical apps Media players, recorders Equalizers, effects, analyzers

Connecting and Linking the Filter

Manual Linking Options

Filters require manual connection by design. You can connect them using:

Using pw-link command line:

# List output ports (sources)
pw-link -o
# List input ports (sinks)
pw-link -i
# List existing connections
pw-link -l
# Connect a source to filter input
pw-link "source_app:output_FL" "audio-filter:input"
# Connect filter output to sink
pw-link "audio-filter:output" "sink_app:input_FL"

Understanding Filter Auto-Connection Behavior

Important**: Unlike audio sources and sinks, filters are not automatically connected by WirePlumber. This is by design because filters are meant to be explicitly inserted into audio chains where needed.

Why filters don't auto-connect**:

  • Filters process existing audio streams rather than generate/consume them
  • Auto-connecting filters could create unwanted audio processing
  • Filters typically require specific placement in the audio graph
  • Manual connection gives users control over when/where effects are applied

Testing the Filter

The filter requires manual connection to test. Here's the recommended workflow:

  1. Start an audio source (e.g., pw-play music.wav)
  2. Run your filter (./tutorial7)
  3. Check available ports:
    # List output ports
    pw-link -o | grep -E "(pw-play|audio-filter)"
    # List input ports
    pw-link -i | grep -E "(audio-filter|playback)"
  4. Connect the audio chain manually:
    # Connect source -> filter -> sink
    pw-link "pw-play:output_FL" "audio-filter:input"
    pw-link "audio-filter:output" "alsa_output.pci-0000_00_1f.3.analog-stereo:playback_FL"

You should hear the audio pass through your filter. Modify the process function to add effects like volume changes, filtering, or other audio processing.

Alternative: Use a patchbay tool**

  • Helvum: flatpak install flathub org.pipewire.Helvum
  • qpwgraph: Available in most Linux distributions
  • Carla: Full-featured audio plugin host

These tools provide graphical interfaces for connecting PipeWire nodes and are ideal for experimenting with filter placement.

Tutorial - Part 6: Binding Objects | Index