Last active
November 6, 2025 10:09
-
-
Save Tiiffi/06ca16b368584dbe05abeb452ccffe8c to your computer and use it in GitHub Desktop.
easypw – single-header library for quickly starting with PipeWire
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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