Skip to content

Instantly share code, notes, and snippets.

@Tiiffi
Last active November 6, 2025 10:09
Show Gist options
  • Save Tiiffi/06ca16b368584dbe05abeb452ccffe8c to your computer and use it in GitHub Desktop.
Save Tiiffi/06ca16b368584dbe05abeb452ccffe8c to your computer and use it in GitHub Desktop.
easypw – single-header library for quickly starting with PipeWire
/*
* compilation: cc prog.c -o prog $(pkg-config --cflags --libs libpipewire-0.3)
*
* TODO:
* - Error handling!
* - Support for planar formats
* - Allow custom allocator, default to malloc/free
* - Volume adjust?
*
* Adapted by Tiiffi — based on the example at https://docs.pipewire.org/audio-src-ring2_8c-example.html
* Original code by: Wim Taymans
* License: MIT
*
*/
#ifndef EASYPW_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdlib.h>
#include <string.h>
#include <spa/param/audio/format-utils.h>
#include <spa/utils/ringbuffer.h>
#include <pipewire/pipewire.h>
#define EASYPW_DEFAULT_FORMAT SPA_AUDIO_FORMAT_S16
#define EASYPW_DEFAULT_RATE 48000
#define EASYPW_DEFAULT_CHANNELS 2
#define EASYPW_DEFAULT_VOLUME 0.7f // unused
#define EASYPW_BUFFER_SIZE (4 * 1024) // adjust if needed
#define EASYPW_MIN_SIZE 256
#define EASYPW_MAX_SIZE EASYPW_BUFFER_SIZE
struct easypw_ctx {
struct pw_thread_loop *thread_loop;
struct pw_loop *loop;
struct pw_stream *stream;
struct spa_ringbuffer ring;
//int16_t buffer[EASYPW_BUFFER_SIZE * EASYPW_DEFAULT_CHANNELS];
uint8_t *buffer;
int eventfd;
bool running;
enum spa_audio_format format;
uint32_t sample_size;
uint32_t channels;
uint32_t rate;
const char *source_name;
};
// NOTE: number of frames = number of samples / channels
// "easypw_push_samples" macro calculates frames for user
#define easypw_push_samples(ctx, samples, n_samples) \
easypw_push_frames((ctx), (samples), (n_samples) / ((ctx)->channels));
extern void easypw_push_frames(struct easypw_ctx *ctx, void *samples, uint32_t n_frames);
//extern void easypw_push_samples(struct easypw_ctx *ctx, void *samples, uint32_t n_samples);
extern void easypw_init(struct easypw_ctx *data, int32_t channels, int32_t rate,
enum spa_audio_format audio_format, const char *audio_source);
extern void easypw_destroy(struct easypw_ctx *data);
#ifdef __cplusplus
}
#endif
#endif // EASYPW_H
//#define EASYPW_IMPLEMENTATION
#ifdef EASYPW_IMPLEMENTATION
/* this can be called from any thread with a block of samples to write into
* the ringbuffer. It will block until all data has been written */
void easypw_push_frames(struct easypw_ctx *ctx, void *samples, uint32_t n_frames)
{
int32_t filled;
uint32_t index;
uint32_t avail;
uint32_t stride = ctx->sample_size * ctx->channels;
uint64_t count;
uint8_t *s = (uint8_t *) samples;
while (n_frames > 0) {
while (true) {
filled = spa_ringbuffer_get_write_index(&ctx->ring, &index);
/* we xrun, this can not happen because we never read more
* than what there is in the ringbuffer and we never write more than
* what is left */
spa_assert(filled >= 0);
spa_assert(filled <= EASYPW_BUFFER_SIZE);
/* this is how much samples we can write */
avail = EASYPW_BUFFER_SIZE - filled;
if (avail > 0)
break;
/* no space.. block and wait for free space */
spa_system_eventfd_read(ctx->loop->system, ctx->eventfd, &count);
}
if (avail > n_frames)
avail = n_frames;
spa_ringbuffer_write_data(&ctx->ring,
ctx->buffer, EASYPW_BUFFER_SIZE * stride,
(index % EASYPW_BUFFER_SIZE) * stride,
s, avail * stride);
s += avail * ctx->channels;
n_frames -= avail;
/* and advance the ringbuffer */
spa_ringbuffer_write_update(&ctx->ring, index + avail);
}
}
/* NOTE: Using macro version instead of function for now
void easypw_push_samples(struct easypw_ctx *ctx, void *samples, uint32_t n_samples)
{
easypw_push_frames(ctx, samples, n_samples / ctx->sample_size / ctx->channels);
}
*/
/*
* our data processing function is in general:
*
* struct pw_buffer *b;
* b = pw_stream_dequeue_buffer(stream);
*
* .. generate stuff in the buffer ...
* In this case we read samples from a ringbuffer. The ringbuffer is
* filled up by another thread.
*
* pw_stream_queue_buffer(stream, b);
*/
static void easypw_on_process(void *userdata)
{
struct easypw_ctx *data = (struct easypw_ctx *) userdata;
struct pw_buffer *b;
struct spa_buffer *buf;
uint8_t *p;
uint32_t index;
uint32_t to_read;
uint32_t to_silence;
int32_t avail;
int32_t n_frames;
uint32_t stride;
if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) {
pw_log_warn("out of buffers: %m");
return;
}
buf = b->buffer;
if ((p = (uint8_t *) buf->datas[0].data) == NULL)
return;
/* the amount of space in the ringbuffer and the read index */
avail = spa_ringbuffer_get_read_index(&data->ring, &index);
stride = data->sample_size * data->channels;
n_frames = buf->datas[0].maxsize / stride;
if (b->requested)
n_frames = SPA_MIN((int32_t)b->requested, n_frames);
/* we can read if there is something available */
to_read = avail > 0 ? SPA_MIN(avail, n_frames) : 0;
/* and fill the remainder with silence */
to_silence = n_frames - to_read;
if (to_read > 0) {
/* read data into the buffer */
spa_ringbuffer_read_data(&data->ring,
data->buffer, EASYPW_BUFFER_SIZE * stride,
(index % EASYPW_BUFFER_SIZE) * stride,
p, to_read * stride);
/* update the read pointer */
spa_ringbuffer_read_update(&data->ring, index + to_read);
}
if (to_silence > 0)
/* set the rest of the buffer to silence */
memset(SPA_PTROFF(p, to_read * stride, void), 0, to_silence * stride);
buf->datas[0].chunk->offset = 0;
buf->datas[0].chunk->stride = stride;
buf->datas[0].chunk->size = n_frames * stride;
pw_stream_queue_buffer(data->stream, b);
/* signal the main thread to fill the ringbuffer, we can only do this, for
* example when the available ringbuffer space falls below a certain
* level. */
spa_system_eventfd_write(data->loop->system, data->eventfd, 1);
}
#if 1
static uint32_t easypw_set_audio_format_bit_length(enum spa_audio_format audio_format)
{
/* NOTE: I think Pipewire already handles these internally
if (audio_format == SPA_AUDIO_FORMAT_S16_LE) audio_format = SPA_AUDIO_FORMAT_S16;
else if (audio_format == SPA_AUDIO_FORMAT_U16_LE) audio_format = SPA_AUDIO_FORMAT_U16;
else if (audio_format == SPA_AUDIO_FORMAT_S32_LE) audio_format = SPA_AUDIO_FORMAT_S32;
else if (audio_format == SPA_AUDIO_FORMAT_U32_LE) audio_format = SPA_AUDIO_FORMAT_U32;
else if (audio_format == SPA_AUDIO_FORMAT_F32_LE) audio_format = SPA_AUDIO_FORMAT_F32;
else if (audio_format == SPA_AUDIO_FORMAT_F64_LE) audio_format = SPA_AUDIO_FORMAT_F64;
*/
uint32_t sample_size = (uint32_t) sizeof(int16_t);
switch (audio_format)
{
case SPA_AUDIO_FORMAT_S8:
case SPA_AUDIO_FORMAT_U8:
sample_size = (uint32_t) sizeof(int8_t);
break;
case SPA_AUDIO_FORMAT_S16:
case SPA_AUDIO_FORMAT_S16_BE:
case SPA_AUDIO_FORMAT_U16:
case SPA_AUDIO_FORMAT_U16_BE:
sample_size = (uint32_t) sizeof(int16_t);
break;
case SPA_AUDIO_FORMAT_S32:
case SPA_AUDIO_FORMAT_S32_BE:
case SPA_AUDIO_FORMAT_U32:
case SPA_AUDIO_FORMAT_U32_BE:
case SPA_AUDIO_FORMAT_F32:
case SPA_AUDIO_FORMAT_F32_BE:
sample_size = (uint32_t) sizeof(int32_t);
break;
case SPA_AUDIO_FORMAT_F64:
case SPA_AUDIO_FORMAT_F64_BE:
sample_size = (uint32_t) sizeof(int64_t);
break;
default:
sample_size = (uint32_t) sizeof(int16_t);
break;
/* TODO: Check how these formats work
case SPA_AUDIO_FORMAT_ULAW:
case SPA_AUDIO_FORMAT_ALAW:*
case SPA_AUDIO_FORMAT_S24_32_LE:
case SPA_AUDIO_FORMAT_S24_32_BE:
case SPA_AUDIO_FORMAT_U24_32_LE:
case SPA_AUDIO_FORMAT_U24_32_BE:
case SPA_AUDIO_FORMAT_S24_LE:
case SPA_AUDIO_FORMAT_S24_BE:
case SPA_AUDIO_FORMAT_U24_LE:
case SPA_AUDIO_FORMAT_U24_BE:
case SPA_AUDIO_FORMAT_S24:
*/
}
return sample_size;
}
#endif
static const struct pw_stream_events stream_events = {
PW_VERSION_STREAM_EVENTS,
.process = easypw_on_process,
};
static void easypw_do_quit(void *userdata, int signal_number)
{
(void) signal_number;
struct easypw_ctx *data = (struct easypw_ctx *) userdata;
data->running = false;
}
void easypw_init(struct easypw_ctx *data, int32_t channels, int32_t rate,
enum spa_audio_format audio_format, const char *source_name)
{
memset(data, 0, sizeof(*data));
/* Set audio format and audio format sample size */
data->format = audio_format;
data->sample_size = easypw_set_audio_format_bit_length(audio_format);
data->channels = channels;
data->rate = rate;
/* Allocate buffer */
data->buffer = (uint8_t *) malloc(EASYPW_BUFFER_SIZE * data->sample_size * data->channels);
if (data->buffer == 0)
abort(); // TODO: Handle this better
const struct spa_pod *params[1];
uint8_t buffer[1024];
struct pw_properties *props;
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
// NOTE: Command line parsing disabled
pw_init(0, NULL);
data->thread_loop = pw_thread_loop_new("audio-src", NULL);
data->loop = pw_thread_loop_get_loop(data->thread_loop);
data->running = true;
pw_thread_loop_lock(data->thread_loop);
// NOTE: These should probably be separate functions for explicit control
pw_loop_add_signal(data->loop, SIGINT, easypw_do_quit, data);
pw_loop_add_signal(data->loop, SIGTERM, easypw_do_quit, data);
spa_ringbuffer_init(&data->ring);
if ((data->eventfd = spa_system_eventfd_create(data->loop->system, SPA_FD_CLOEXEC)) < 0) {
// return data->eventfd;
// TODO: Handle this properly!
}
pw_thread_loop_start(data->thread_loop);
props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, "Playback",
PW_KEY_MEDIA_ROLE, "Music",
NULL);
#if 0 // NOTE: Disabled for now
if (argc > 1)
/* Set stream target if given on command line */
pw_properties_set(props, PW_KEY_TARGET_OBJECT, argv[1]);
#endif
data->source_name = (!source_name) ? "easypw-audio-src" : source_name;
data->stream = pw_stream_new_simple(
data->loop,
data->source_name,
props,
&stream_events,
data);
/* Make one parameter with the supported formats. The SPA_PARAM_EnumFormat
* id means that this is a format enumeration (of 1 value). */
struct spa_audio_info_raw spa_audio_info = {
.format = audio_format,
.flags = 0,
.rate = data->rate,
.channels = data->channels,
.position = {0}
};
params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &spa_audio_info);
/* Now connect this stream. We ask that our process function is
* called in a realtime thread. */
pw_stream_connect(data->stream,
PW_DIRECTION_OUTPUT,
PW_ID_ANY,
(enum pw_stream_flags)
(PW_STREAM_FLAG_AUTOCONNECT |
PW_STREAM_FLAG_MAP_BUFFERS |
PW_STREAM_FLAG_RT_PROCESS),
params, 1);
pw_thread_loop_start(data->thread_loop);
pw_thread_loop_unlock(data->thread_loop);
}
void easypw_destroy(struct easypw_ctx *data)
{
free(data->buffer);
pw_thread_loop_lock(data->thread_loop);
pw_stream_destroy(data->stream);
pw_thread_loop_unlock(data->thread_loop);
pw_thread_loop_destroy(data->thread_loop);
close(data->eventfd);
pw_deinit();
memset(data, 0xCD, sizeof(*data));
}
#endif // EASYPW_IMPLEMENTATION
// compilation: cc easypw_example.c -o easypw_example $(pkg-config --cflags --libs libpipewire-0.3)
#include <stdio.h>
#define EASYPW_IMPLEMENTATION
#include "easypw.h"
#define NUM_SAMPLES 512
int main(void)
{
// initialize easypw
// 2 channels, 48000Hz sampling rate and signed 16-bit samples
struct easypw_ctx pw;
easypw_init(&pw, 2, 48000, SPA_AUDIO_FORMAT_S16, NULL);
// buffer for samples
uint16_t pcm[NUM_SAMPLES];
// read raw PCM data from stdin and push samples to Pipewire
// expects 2 channels, signed 16bit samples and 48000Hz rate
size_t bytes_read;
while ((bytes_read = fread(pcm, sizeof(pcm), 1, stdin)) > 0)
{
// break from loop if not running
if (pw.running == false)
break;
// last argument is number of frames (= number of samples / channels)
//easypw_push_frames(&pw, pcm, NUM_SAMPLES / 2);
// this is simpler to use, number of frames calculation is made for user
easypw_push_samples(&pw, pcm, NUM_SAMPLES);
}
// wait for playback thread to finnish
sleep(1);
// cleanup
easypw_destroy(&pw);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment