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

PipeWire 1.6.8

Loading...
Searching...
No Matches
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>
#include <spa/pod/builder.h>
#include <spa/param/latency-utils.h>
#include <pipewire/pipewire.h>
#include <pipewire/filter.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 = {
PW_VERSION_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];
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
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), SIGINT, do_quit, &data);
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.
*/
data.filter = pw_filter_new_simple(
pw_main_loop_get_loop(data.loop),
"audio-filter",
pw_properties_new(
PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, "Filter",
PW_KEY_MEDIA_ROLE, "DSP",
NULL),
&filter_events,
&data);
/* make an audio DSP input port */
data.in_port = pw_filter_add_port(data.filter,
PW_DIRECTION_INPUT,
PW_FILTER_PORT_FLAG_MAP_BUFFERS,
sizeof(struct port),
pw_properties_new(
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,
PW_DIRECTION_OUTPUT,
PW_FILTER_PORT_FLAG_MAP_BUFFERS,
sizeof(struct port),
pw_properties_new(
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,
SPA_PARAM_ProcessLatency,
&SPA_PROCESS_LATENCY_INFO_INIT(
.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,
PW_FILTER_FLAG_RT_PROCESS,
params, n_params) < 0) {
fprintf(stderr, "can't connect\n");
return -1;
}
/* and wait while we let things run */
pw_main_loop_run(data.loop);
pw_filter_destroy(data.filter);
pw_main_loop_destroy(data.loop);
pw_deinit();
return 0;
}

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;
};
pw_filter
pw_main_loop
A main loop object.

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(
pw_main_loop_get_loop(data.loop),
"audio-filter",
pw_properties_new(
PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, "Filter",
PW_KEY_MEDIA_ROLE, "DSP",
NULL),
&filter_events,
&data);
pw_filter_new_simple
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:1295
PW_KEY_MEDIA_TYPE
#define PW_KEY_MEDIA_TYPE
Media.
Definition keys.h:512
PW_KEY_MEDIA_ROLE
#define PW_KEY_MEDIA_ROLE
Role: Movie, Music, Camera, Screen, Communication, Game, Notification, DSP, Production,...
Definition keys.h:518
PW_KEY_MEDIA_CATEGORY
#define PW_KEY_MEDIA_CATEGORY
Media Category: Playback, Capture, Duplex, Monitor, Manager.
Definition keys.h:515
pw_main_loop_get_loop
struct pw_loop * pw_main_loop_get_loop(struct pw_main_loop *loop)
Get the loop implementation.
Definition main-loop.c:96
pw_properties_new
struct pw_properties * pw_properties_new(const char *key,...)
Make a new properties object.
Definition properties.c:97

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,
PW_DIRECTION_INPUT,
PW_FILTER_PORT_FLAG_MAP_BUFFERS,
sizeof(struct port),
pw_properties_new(
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,
PW_DIRECTION_OUTPUT,
PW_FILTER_PORT_FLAG_MAP_BUFFERS,
sizeof(struct port),
pw_properties_new(
PW_KEY_FORMAT_DSP, "32 bit float mono audio",
PW_KEY_PORT_NAME, "output",
NULL),
NULL, 0);
pw_filter_add_port
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:1816
PW_FILTER_PORT_FLAG_MAP_BUFFERS
@ PW_FILTER_PORT_FLAG_MAP_BUFFERS
mmap the buffers except DmaBuf that is not explicitly marked as mappable.
Definition filter.h:134
PW_KEY_PORT_NAME
#define PW_KEY_PORT_NAME
port name
Definition keys.h:346
PW_KEY_FORMAT_DSP
#define PW_KEY_FORMAT_DSP
format related properties
Definition keys.h:555
PW_DIRECTION_OUTPUT
#define PW_DIRECTION_OUTPUT
Definition port.h:55
PW_DIRECTION_INPUT
#define PW_DIRECTION_INPUT
Definition port.h:53

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

params[n_params++] = spa_process_latency_build(&b,
SPA_PARAM_ProcessLatency,
&SPA_PROCESS_LATENCY_INFO_INIT(
.ns = 10 * SPA_NSEC_PER_MSEC
));
SPA_PROCESS_LATENCY_INFO_INIT
#define SPA_PROCESS_LATENCY_INFO_INIT(...)
Definition latency.h:92
spa_process_latency_build
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
@ SPA_PARAM_ProcessLatency
processing latency, a SPA_TYPE_OBJECT_ParamProcessLatency
Definition param.h:46
SPA_NSEC_PER_MSEC
#define SPA_NSEC_PER_MSEC
Definition defs.h:255

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,
PW_FILTER_FLAG_RT_PROCESS,
params, n_params) < 0) {
fprintf(stderr, "can't connect\n");
return -1;
}
pw_filter_connect
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:1573
PW_FILTER_FLAG_RT_PROCESS
@ PW_FILTER_FLAG_RT_PROCESS
call process from the realtime thread.
Definition filter.h:112

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