diff --git a/CMakeLists.txt b/CMakeLists.txt index fd7e5a4..605d6e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,7 @@ if(UNIX AND NOT APPLE) pkg_check_modules(VAAPI libva libva-drm) pkg_check_modules(ALSA REQUIRED alsa) pkg_check_modules(PULSEAUDIO libpulse-simple libpulse) + pkg_check_modules(PIPEWIRE libpipewire-0.3) if(NOT HEADLESS) pkg_check_modules(GTK3 REQUIRED gtk+-3.0) @@ -98,6 +99,10 @@ if(UNIX AND NOT APPLE) if(PULSEAUDIO_FOUND) add_compile_definitions(HAVE_PULSEAUDIO) endif() + + if(PIPEWIRE_FOUND) + add_compile_definitions(HAVE_PIPEWIRE) + endif() endif() if(WIN32) @@ -151,6 +156,8 @@ set(LINUX_SOURCES src/audio_playback.c src/audio_capture_pulse.c src/audio_playback_pulse.c + src/audio_capture_pipewire.c + src/audio_playback_pipewire.c src/audio_capture_dummy.c src/audio_playback_dummy.c src/input.c @@ -233,6 +240,11 @@ if(UNIX AND NOT APPLE) target_include_directories(rootstream PRIVATE ${PULSEAUDIO_INCLUDE_DIRS}) endif() + if(PIPEWIRE_FOUND) + target_link_libraries(rootstream PRIVATE ${PIPEWIRE_LIBRARIES}) + target_include_directories(rootstream PRIVATE ${PIPEWIRE_INCLUDE_DIRS}) + endif() + if(NOT HEADLESS AND GTK3_FOUND) target_link_libraries(rootstream PRIVATE ${GTK3_LIBRARIES}) target_include_directories(rootstream PRIVATE ${GTK3_INCLUDE_DIRS}) @@ -277,6 +289,7 @@ if(UNIX AND NOT APPLE) src/opus_codec.c src/audio_playback.c src/audio_playback_pulse.c + src/audio_playback_pipewire.c src/audio_playback_dummy.c src/latency.c ${PLATFORM_SOURCES} @@ -301,6 +314,11 @@ if(UNIX AND NOT APPLE) target_include_directories(rstr-player PRIVATE ${PULSEAUDIO_INCLUDE_DIRS}) endif() + if(PIPEWIRE_FOUND) + target_link_libraries(rstr-player PRIVATE ${PIPEWIRE_LIBRARIES}) + target_include_directories(rstr-player PRIVATE ${PIPEWIRE_INCLUDE_DIRS}) + endif() + if(Opus_FOUND) target_link_libraries(rstr-player PRIVATE Opus::opus) else() @@ -406,6 +424,7 @@ message(STATUS " Headless: ${HEADLESS}") if(UNIX) message(STATUS " VA-API: ${VAAPI_FOUND}") message(STATUS " PulseAudio: ${PULSEAUDIO_FOUND}") + message(STATUS " PipeWire: ${PIPEWIRE_FOUND}") message(STATUS " Avahi: ${AVAHI_FOUND}") endif() message(STATUS "") diff --git a/include/rootstream.h b/include/rootstream.h index 3931e85..5b73ca0 100644 --- a/include/rootstream.h +++ b/include/rootstream.h @@ -697,6 +697,19 @@ int audio_playback_write_pulse(rootstream_ctx_t *ctx, int16_t *samples, size_t num_samples); void audio_playback_cleanup_pulse(rootstream_ctx_t *ctx); +/* Audio backends - PipeWire */ +bool audio_capture_pipewire_available(void); +int audio_capture_init_pipewire(rootstream_ctx_t *ctx); +int audio_capture_frame_pipewire(rootstream_ctx_t *ctx, int16_t *samples, + size_t *num_samples); +void audio_capture_cleanup_pipewire(rootstream_ctx_t *ctx); + +bool audio_playback_pipewire_available(void); +int audio_playback_init_pipewire(rootstream_ctx_t *ctx); +int audio_playback_write_pipewire(rootstream_ctx_t *ctx, const int16_t *samples, + size_t num_samples); +void audio_playback_cleanup_pipewire(rootstream_ctx_t *ctx); + /* Audio backends - Dummy (silent/discard) */ int audio_capture_init_dummy(rootstream_ctx_t *ctx); int audio_capture_frame_dummy(rootstream_ctx_t *ctx, int16_t *samples, diff --git a/src/audio_capture_pipewire.c b/src/audio_capture_pipewire.c new file mode 100644 index 0000000..1ea0112 --- /dev/null +++ b/src/audio_capture_pipewire.c @@ -0,0 +1,317 @@ +/* + * audio_capture_pipewire.c - PipeWire audio capture fallback + * + * Works on modern Linux distributions where PipeWire is the default audio server. + * Fedora 40+, Ubuntu 24.04+, Arch, etc. + * + * Uses pw_stream for simple, non-blocking audio capture. + */ + +#include "../include/rootstream.h" +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_PIPEWIRE +#include +#include +#include +#include + +typedef struct { + struct pw_loop *loop; + struct pw_stream *stream; + struct pw_core *core; + struct pw_context *context; + + int16_t *buffer; + size_t buffer_size; + size_t read_pos; + + int sample_rate; + int channels; + int frame_size; +} pipewire_capture_ctx_t; + +/* Stream events callback */ +static void on_process(void *userdata) { + pipewire_capture_ctx_t *pw = (pipewire_capture_ctx_t *)userdata; + struct pw_buffer *b; + struct spa_buffer *buf; + + if ((b = pw_stream_dequeue_buffer(pw->stream)) == NULL) { + return; + } + + buf = b->buffer; + + /* Get audio data from buffer */ + for (uint32_t i = 0; i < buf->n_datas; i++) { + struct spa_data *d = &buf->datas[i]; + + if (d->data == NULL) continue; + + uint32_t n_samples = d->chunk->size / sizeof(int16_t); + int16_t *samples = (int16_t *)d->data; + + /* Copy to our buffer */ + if (pw->read_pos + n_samples <= pw->buffer_size) { + memcpy(pw->buffer + pw->read_pos, samples, + n_samples * sizeof(int16_t)); + pw->read_pos += n_samples; + } + } + + pw_stream_queue_buffer(pw->stream, b); +} + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .process = on_process, +}; + +/* + * Initialize PipeWire audio capture + */ +int audio_capture_init_pipewire(rootstream_ctx_t *ctx) { + pipewire_capture_ctx_t *pw = calloc(1, sizeof(pipewire_capture_ctx_t)); + if (!pw) { + fprintf(stderr, "ERROR: Memory allocation failed\n"); + return -1; + } + + pw->sample_rate = 48000; + pw->channels = 2; + pw->frame_size = 240; /* 5ms at 48kHz */ + pw->buffer_size = pw->frame_size * pw->channels * 4; /* 4 frames buffer */ + + pw->buffer = calloc(pw->buffer_size, sizeof(int16_t)); + if (!pw->buffer) { + free(pw); + return -1; + } + + pw->read_pos = 0; + + /* Initialize PipeWire (will be cleaned up in cleanup function) */ + pw_init(NULL, NULL); + + /* Create main loop */ + pw->loop = pw_loop_new(NULL); + if (!pw->loop) { + fprintf(stderr, "ERROR: Cannot create PipeWire main loop\n"); + pw_deinit(); + free(pw->buffer); + free(pw); + return -1; + } + + /* Create context */ + pw->context = pw_context_new(pw->loop, NULL, 0); + if (!pw->context) { + fprintf(stderr, "ERROR: Cannot create PipeWire context\n"); + pw_loop_destroy(pw->loop); + pw_deinit(); + free(pw->buffer); + free(pw); + return -1; + } + + /* Create core and connect */ + pw->core = pw_context_connect(pw->context, NULL, 0); + if (!pw->core) { + fprintf(stderr, "ERROR: Cannot connect to PipeWire core\n"); + pw_context_destroy(pw->context); + pw_loop_destroy(pw->loop); + pw_deinit(); + free(pw->buffer); + free(pw); + return -1; + } + + /* Create stream */ + pw->stream = pw_stream_new_simple( + pw->loop, + "RootStream Capture", + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_AUDIO_FORMAT, "S16LE", + NULL + ), + &stream_events, + pw + ); + + if (!pw->stream) { + fprintf(stderr, "ERROR: Cannot create PipeWire stream\n"); + pw_core_disconnect(pw->core); + pw_context_destroy(pw->context); + pw_loop_destroy(pw->loop); + pw_deinit(); + free(pw->buffer); + free(pw); + return -1; + } + + /* Build stream parameters */ + uint8_t params_buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); + + const struct spa_pod *params[1]; + struct spa_audio_info_raw info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_S16, + .channels = pw->channels, + .rate = pw->sample_rate + ); + + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info); + + /* Connect stream for capture */ + if (pw_stream_connect( + pw->stream, + PW_DIRECTION_INPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS, + params, + 1 + ) < 0) { + fprintf(stderr, "ERROR: Cannot connect PipeWire stream\n"); + pw_stream_destroy(pw->stream); + pw_core_disconnect(pw->core); + pw_context_destroy(pw->context); + pw_loop_destroy(pw->loop); + pw_deinit(); + free(pw->buffer); + free(pw); + return -1; + } + + ctx->audio_capture_priv = pw; + printf("✓ PipeWire audio capture initialized (48kHz, stereo)\n"); + return 0; +} + +/* + * Capture a frame via PipeWire + */ +int audio_capture_frame_pipewire(rootstream_ctx_t *ctx, int16_t *samples, + size_t *num_samples) { + if (!ctx || !samples || !num_samples) return -1; + + pipewire_capture_ctx_t *pw = (pipewire_capture_ctx_t *)ctx->audio_capture_priv; + if (!pw || !pw->stream) return -1; + + /* Run main loop to process events */ + pw_loop_iterate(pw->loop, 0); + + /* Check if we have enough samples */ + if (pw->read_pos < (size_t)(pw->frame_size * pw->channels)) { +#ifdef DEBUG + /* Debug logging only - this is expected during initial buffering */ + static int log_count = 0; + if (log_count++ < 3) { + fprintf(stderr, "DEBUG: PipeWire capture waiting for data (%zu/%d samples)\n", + pw->read_pos, pw->frame_size * pw->channels); + } +#endif + return -1; /* Not enough data yet */ + } + + /* Copy samples */ + size_t bytes_to_copy = pw->frame_size * pw->channels * sizeof(int16_t); + memcpy(samples, pw->buffer, bytes_to_copy); + *num_samples = pw->frame_size * pw->channels; + + /* Shift remaining data */ + if (pw->read_pos > (size_t)(pw->frame_size * pw->channels)) { + memmove(pw->buffer, + pw->buffer + pw->frame_size * pw->channels, + (pw->read_pos - pw->frame_size * pw->channels) * sizeof(int16_t)); + pw->read_pos -= pw->frame_size * pw->channels; + } else { + pw->read_pos = 0; + } + + return 0; +} + +/* + * Cleanup PipeWire capture + */ +void audio_capture_cleanup_pipewire(rootstream_ctx_t *ctx) { + if (!ctx || !ctx->audio_capture_priv) return; + + pipewire_capture_ctx_t *pw = (pipewire_capture_ctx_t *)ctx->audio_capture_priv; + + if (pw->stream) pw_stream_destroy(pw->stream); + if (pw->core) pw_core_disconnect(pw->core); + if (pw->context) pw_context_destroy(pw->context); + if (pw->loop) pw_loop_destroy(pw->loop); + if (pw->buffer) free(pw->buffer); + + pw_deinit(); + + free(pw); + ctx->audio_capture_priv = NULL; +} + +/* + * Check if PipeWire is available + */ +bool audio_capture_pipewire_available(void) { + /* Try to connect to PipeWire daemon */ + pw_init(NULL, NULL); + + struct pw_loop *loop = pw_loop_new(NULL); + if (!loop) { + pw_deinit(); + return false; + } + + struct pw_context *context = pw_context_new(loop, NULL, 0); + if (!context) { + pw_loop_destroy(loop); + pw_deinit(); + return false; + } + + struct pw_core *core = pw_context_connect(context, NULL, 0); + bool available = (core != NULL); + + if (core) pw_core_disconnect(core); + pw_context_destroy(context); + pw_loop_destroy(loop); + pw_deinit(); + + return available; +} + +#else + +/* Stub for NO_PIPEWIRE builds */ +int audio_capture_init_pipewire(rootstream_ctx_t *ctx) { + (void)ctx; + return -1; +} + +int audio_capture_frame_pipewire(rootstream_ctx_t *ctx, int16_t *samples, + size_t *num_samples) { + (void)ctx; + (void)samples; + (void)num_samples; + return -1; +} + +void audio_capture_cleanup_pipewire(rootstream_ctx_t *ctx) { + (void)ctx; +} + +bool audio_capture_pipewire_available(void) { + return false; +} + +#endif diff --git a/src/audio_playback_pipewire.c b/src/audio_playback_pipewire.c new file mode 100644 index 0000000..6ff588a --- /dev/null +++ b/src/audio_playback_pipewire.c @@ -0,0 +1,248 @@ +/* + * audio_playback_pipewire.c - PipeWire audio playback fallback + * + * Works on modern Linux distributions where PipeWire is the default. + */ + +#include "../include/rootstream.h" +#include +#include +#include +#include +#include + +#ifdef HAVE_PIPEWIRE +#include +#include +#include + +typedef struct { + struct pw_loop *loop; + struct pw_stream *stream; + struct pw_core *core; + struct pw_context *context; + + int sample_rate; + int channels; +} pipewire_playback_ctx_t; + +/* Stream events */ +static void on_playback_process(void *userdata) { + pipewire_playback_ctx_t *pw = (pipewire_playback_ctx_t *)userdata; + struct pw_buffer *b; + + if ((b = pw_stream_dequeue_buffer(pw->stream)) == NULL) + return; + + pw_stream_queue_buffer(pw->stream, b); +} + +static const struct pw_stream_events playback_stream_events = { + PW_VERSION_STREAM_EVENTS, + .process = on_playback_process, +}; + +/* + * Initialize PipeWire audio playback + */ +int audio_playback_init_pipewire(rootstream_ctx_t *ctx) { + pipewire_playback_ctx_t *pw = calloc(1, sizeof(pipewire_playback_ctx_t)); + if (!pw) return -1; + + pw->sample_rate = 48000; + pw->channels = 2; + + /* Initialize PipeWire (will be cleaned up in cleanup function) */ + pw_init(NULL, NULL); + + /* Create main loop */ + pw->loop = pw_loop_new(NULL); + if (!pw->loop) { + pw_deinit(); + free(pw); + return -1; + } + + /* Create context */ + pw->context = pw_context_new(pw->loop, NULL, 0); + if (!pw->context) { + pw_loop_destroy(pw->loop); + pw_deinit(); + free(pw); + return -1; + } + + /* Connect to core */ + pw->core = pw_context_connect(pw->context, NULL, 0); + if (!pw->core) { + pw_context_destroy(pw->context); + pw_loop_destroy(pw->loop); + pw_deinit(); + free(pw); + return -1; + } + + /* Create playback stream */ + pw->stream = pw_stream_new_simple( + pw->loop, + "RootStream Playback", + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_AUDIO_FORMAT, "S16LE", + NULL + ), + &playback_stream_events, + pw + ); + + if (!pw->stream) { + pw_core_disconnect(pw->core); + pw_context_destroy(pw->context); + pw_loop_destroy(pw->loop); + pw_deinit(); + free(pw); + return -1; + } + + /* Build stream parameters */ + uint8_t params_buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); + + const struct spa_pod *params[1]; + struct spa_audio_info_raw info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_S16, + .channels = pw->channels, + .rate = pw->sample_rate + ); + + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info); + + /* Connect stream for playback */ + if (pw_stream_connect( + pw->stream, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS, + params, + 1 + ) < 0) { + pw_stream_destroy(pw->stream); + pw_core_disconnect(pw->core); + pw_context_destroy(pw->context); + pw_loop_destroy(pw->loop); + pw_deinit(); + free(pw); + return -1; + } + + ctx->audio_playback_priv = pw; + printf("✓ PipeWire audio playback initialized\n"); + return 0; +} + +/* + * Write audio samples via PipeWire + */ +int audio_playback_write_pipewire(rootstream_ctx_t *ctx, const int16_t *samples, + size_t num_samples) { + if (!ctx || !samples || num_samples == 0) return 0; + + pipewire_playback_ctx_t *pw = (pipewire_playback_ctx_t *)ctx->audio_playback_priv; + if (!pw || !pw->stream) return -1; + + struct pw_buffer *b = pw_stream_dequeue_buffer(pw->stream); + if (!b) return -1; + + struct spa_buffer *buf = b->buffer; + + /* Copy samples to buffer */ + for (uint32_t i = 0; i < buf->n_datas; i++) { + struct spa_data *d = &buf->datas[i]; + size_t size = num_samples * sizeof(int16_t); + + if (d->maxsize >= size) { + memcpy(d->data, samples, size); + d->chunk->size = size; + d->chunk->offset = 0; + d->chunk->stride = sizeof(int16_t); + } + } + + pw_stream_queue_buffer(pw->stream, b); + return 0; +} + +/* + * Cleanup PipeWire playback + */ +void audio_playback_cleanup_pipewire(rootstream_ctx_t *ctx) { + if (!ctx || !ctx->audio_playback_priv) return; + + pipewire_playback_ctx_t *pw = (pipewire_playback_ctx_t *)ctx->audio_playback_priv; + + if (pw->stream) pw_stream_destroy(pw->stream); + if (pw->core) pw_core_disconnect(pw->core); + if (pw->context) pw_context_destroy(pw->context); + if (pw->loop) pw_loop_destroy(pw->loop); + + pw_deinit(); + + free(pw); + ctx->audio_playback_priv = NULL; +} + +/* + * Check if PipeWire is available + */ +bool audio_playback_pipewire_available(void) { + pw_init(NULL, NULL); + + struct pw_loop *loop = pw_loop_new(NULL); + if (!loop) { + pw_deinit(); + return false; + } + + struct pw_context *context = pw_context_new(loop, NULL, 0); + if (!context) { + pw_loop_destroy(loop); + pw_deinit(); + return false; + } + + struct pw_core *core = pw_context_connect(context, NULL, 0); + bool available = (core != NULL); + + if (core) pw_core_disconnect(core); + pw_context_destroy(context); + pw_loop_destroy(loop); + pw_deinit(); + + return available; +} + +#else + +int audio_playback_init_pipewire(rootstream_ctx_t *ctx) { + (void)ctx; + return -1; +} + +int audio_playback_write_pipewire(rootstream_ctx_t *ctx, const int16_t *samples, + size_t num_samples) { + (void)ctx; + (void)samples; + (void)num_samples; + return -1; +} + +void audio_playback_cleanup_pipewire(rootstream_ctx_t *ctx) { + (void)ctx; +} + +bool audio_playback_pipewire_available(void) { + return false; +} + +#endif diff --git a/src/diagnostics.c b/src/diagnostics.c index a879787..5d1d299 100644 --- a/src/diagnostics.c +++ b/src/diagnostics.c @@ -101,13 +101,19 @@ void diagnostics_print_available_backends(rootstream_ctx_t *ctx) { printf("\n Audio Capture:\n"); printf(" Primary (ALSA): ✓ Compiled in\n"); - printf(" Fallback (PulseAudio): "); -#ifdef HAVE_PULSE + printf(" Fallback 1 (PulseAudio): "); +#ifdef HAVE_PULSEAUDIO printf("✓ Compiled in\n"); #else printf("✗ Not compiled\n"); #endif - printf(" Fallback (Dummy): ✓ Always available\n"); + printf(" Fallback 2 (PipeWire): "); +#ifdef HAVE_PIPEWIRE + printf("✓ Compiled in\n"); +#else + printf("✗ Not compiled\n"); +#endif + printf(" Fallback 3 (Dummy): ✓ Always available\n"); printf("\n Input Injection:\n"); printf(" Primary (uinput): %s\n", @@ -184,7 +190,7 @@ void diagnostics_print_recommendations(rootstream_ctx_t *ctx) { recommendations++; #endif -#ifndef HAVE_PULSE +#ifndef HAVE_PULSEAUDIO printf(" • Install PulseAudio support: apt-get install libpulse-dev\n"); recommendations++; #endif diff --git a/src/service.c b/src/service.c index 5693e74..32ffa86 100644 --- a/src/service.c +++ b/src/service.c @@ -365,6 +365,13 @@ int service_run_host(rootstream_ctx_t *ctx) { .cleanup_fn = audio_capture_cleanup_pulse, .is_available_fn = audio_capture_pulse_available, }, + { + .name = "PipeWire", + .init_fn = audio_capture_init_pipewire, + .capture_fn = audio_capture_frame_pipewire, + .cleanup_fn = audio_capture_cleanup_pipewire, + .is_available_fn = audio_capture_pipewire_available, + }, { .name = "Dummy (Silent)", .init_fn = audio_capture_init_dummy, @@ -627,6 +634,13 @@ int service_run_client(rootstream_ctx_t *ctx) { .cleanup_fn = audio_playback_cleanup_pulse, .is_available_fn = audio_playback_pulse_available, }, + { + .name = "PipeWire", + .init_fn = audio_playback_init_pipewire, + .playback_fn = audio_playback_write_pipewire, + .cleanup_fn = audio_playback_cleanup_pipewire, + .is_available_fn = audio_playback_pipewire_available, + }, { .name = "Dummy (Silent)", .init_fn = audio_playback_init_dummy,