/* * 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 #include #include #include #include #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