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 port *in_port;
struct port *out_port;
};
{
struct data *data = userdata;
float *in, *out;
if (in == NULL || out == NULL)
return;
memcpy(out, in, n_samples * sizeof(float));
}
.process = on_process,
};
static void do_quit(void *userdata, int signal_number)
{
struct data *data = userdata;
}
int main(int argc, char *argv[])
{
struct data data = { 0, };
uint32_t n_params = 0;
uint8_t buffer[1024];
"audio-filter",
NULL),
&filter_events,
&data);
sizeof(struct port),
NULL),
NULL, 0);
sizeof(struct port),
NULL),
NULL, 0);
));
params, n_params) < 0) {
fprintf(stderr, "can't connect\n");
return -1;
}
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 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
"audio-filter",
NULL),
&filter_events,
&data);
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:1290
#define PW_KEY_MEDIA_TYPE
Media.
Definition keys.h:507
#define PW_KEY_MEDIA_ROLE
Role: Movie, Music, Camera, Screen, Communication, Game, Notification, DSP, Production,...
Definition keys.h:513
#define PW_KEY_MEDIA_CATEGORY
Media Category: Playback, Capture, Duplex, Monitor, Manager.
Definition keys.h:510
struct pw_loop * pw_main_loop_get_loop(struct pw_main_loop *loop)
Get the loop implementation.
Definition main-loop.c:96
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:
sizeof(struct port),
NULL),
NULL, 0);
sizeof(struct port),
NULL),
NULL, 0);
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:1800
@ 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:343
#define PW_KEY_FORMAT_DSP
format related properties
Definition keys.h:550
#define PW_DIRECTION_OUTPUT
Definition port.h:55
#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
));
#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_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
params, n_params) < 0) {
fprintf(stderr, "can't connect\n");
return -1;
}
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:1559
@ 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:
{
struct data *data = userdata;
float *in, *out;
if (in == NULL || out == NULL)
return;
memcpy(out, in, n_samples * sizeof(float));
}
The process function is called for each audio buffer and works as follows:
- Get the number of samples to process from
position->clock.duration
- Get input and output buffer pointers using
pw_filter_get_dsp_buffer()
- Process the audio data (here we just copy input to output)
- 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:
for (uint32_t i = 0; i < n_samples; i++) {
out[i] = in[i] * 0.5f;
}
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];
}
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:
- Start an audio source (e.g.,
pw-play music.wav
)
- Run your filter (
./tutorial7
)
- 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)"
- 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