PipeWire 1.6.2
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>
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];
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), 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:1566
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:1807
#define PW_VERSION_FILTER_EVENTS
Definition filter.h:67
void pw_filter_destroy(struct pw_filter *filter)
Destroy a filter
Definition filter.c:1414
void * pw_filter_get_dsp_buffer(void *port_data, uint32_t n_samples)
Get a data pointer to the buffer data.
Definition filter.c:2045
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:1288
@ 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
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_main_loop * pw_main_loop_new(const struct spa_dict *props)
Create a new main loop.
Definition main-loop.c:63
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:488
void pw_deinit(void)
Deinitialize PipeWire.
Definition pipewire.c:603
#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
void(* process)(void *data, struct spa_io_position *position)
do processing.
Definition filter.h:93
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
uint32_t type
Definition pod.h:59

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