diff --git a/CMakeLists.txt b/CMakeLists.txt index 26befb96..fae9d789 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,6 +88,7 @@ option ( enable-winmidi "compile Windows MIDI support (if it is available)" on ) option ( enable-sdl2 "compile SDL2 audio support (if it is available)" on ) option ( enable-pkgconfig "use pkg-config to locate fluidsynth's (mostly optional) dependencies" on ) option ( enable-pulseaudio "compile PulseAudio support (if it is available)" on ) +option ( enable-pipewire "compile PipeWire support (if it is available)" on ) option ( enable-readline "compile readline lib line editing (if it is available)" on ) option ( enable-threads "enable multi-threading support (such as parallel voice synthesis)" on ) option ( enable-openmp "enable OpenMP support (parallelization of soundfont decoding, vectorization of voice mixing, etc.)" on ) @@ -595,6 +596,14 @@ else(NOT enable-pkgconfig) unset_pkg_config ( JACK ) endif ( enable-jack ) + unset ( PIPEWIRE_SUPPORT CACHE ) + if ( enable-pipewire ) + pkg_check_modules ( PIPEWIRE libpipewire-0.3 ) + set ( PIPEWIRE_SUPPORT ${PIPEWIRE_FOUND} ) + else ( enable-pipewire ) + unset_pkg_config ( PIPEWIRE ) + endif ( enable-pipewire ) + unset ( LASH_SUPPORT CACHE ) if ( enable-lash ) pkg_check_modules ( LASH lash-1.0>=0.3 ) @@ -830,6 +839,7 @@ link_directories ( ${GLIB_LIBRARY_DIRS} ${LASH_LIBRARY_DIRS} ${JACK_LIBRARY_DIRS} + ${PIPEWIRE_LIBRARY_DIRS} ${ALSA_LIBRARY_DIRS} ${PULSE_LIBRARY_DIRS} ${PORTAUDIO_LIBRARY_DIRS} diff --git a/cmake_admin/report.cmake b/cmake_admin/report.cmake index 588cf81b..ac4fe95a 100644 --- a/cmake_admin/report.cmake +++ b/cmake_admin/report.cmake @@ -31,6 +31,12 @@ else ( JACK_SUPPORT ) set ( AUDIO_MIDI_REPORT "${AUDIO_MIDI_REPORT} JACK: no\n" ) endif ( JACK_SUPPORT ) +if ( PIPEWIRE_SUPPORT ) + set ( AUDIO_MIDI_REPORT "${AUDIO_MIDI_REPORT} PipeWire: yes\n" ) +else ( PIPEWIRE_SUPPORT ) + set ( AUDIO_MIDI_REPORT "${AUDIO_MIDI_REPORT} PipeWire: no\n" ) +endif ( PIPEWIRE_SUPPORT ) + if ( MIDISHARE_SUPPORT ) set ( AUDIO_MIDI_REPORT "${AUDIO_MIDI_REPORT} MidiShare: yes\n" ) else ( MIDISHARE_SUPPORT ) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4f3ef86a..7f6df7d2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -75,6 +75,11 @@ if ( JACK_SUPPORT ) include_directories ( ${JACK_INCLUDE_DIRS} ) endif ( JACK_SUPPORT ) +if ( PIPEWIRE_SUPPORT ) + set ( fluid_pipewire_SOURCES drivers/fluid_pipewire.c ) + include_directories ( SYSTEM ${PIPEWIRE_INCLUDE_DIRS} ) +endif ( PIPEWIRE_SUPPORT ) + if ( PORTAUDIO_SUPPORT ) set ( fluid_portaudio_SOURCES drivers/fluid_portaudio.c ) include_directories ( ${PORTAUDIO_INCLUDE_DIRS} ) @@ -277,6 +282,7 @@ add_library ( libfluidsynth-OBJ OBJECT ${fluid_dart_SOURCES} ${fluid_dbus_SOURCES} ${fluid_jack_SOURCES} + ${fluid_pipewire_SOURCES} ${fluid_lash_SOURCES} ${fluid_midishare_SOURCES} ${fluid_opensles_SOURCES} @@ -353,6 +359,7 @@ target_link_libraries ( libfluidsynth ${GMODULE_LIBRARIES} ${LASH_LIBRARIES} ${JACK_LIBRARIES} + ${PIPEWIRE_LIBRARIES} ${ALSA_LIBRARIES} ${PULSE_LIBRARIES} ${PORTAUDIO_LIBRARIES} diff --git a/src/config.cmake b/src/config.cmake index d3aaf701..240bc262 100644 --- a/src/config.cmake +++ b/src/config.cmake @@ -130,6 +130,9 @@ /* Define to enable JACK driver */ #cmakedefine JACK_SUPPORT @JACK_SUPPORT@ +/* Define to enable PipeWire driver */ +#cmakedefine PIPEWIRE_SUPPORT @PIPEWIRE_SUPPORT@ + /* Include the LADSPA Fx unit */ #cmakedefine LADSPA @LADSPA_SUPPORT@ diff --git a/src/drivers/fluid_adriver.c b/src/drivers/fluid_adriver.c index 9e8898af..a704fa08 100644 --- a/src/drivers/fluid_adriver.c +++ b/src/drivers/fluid_adriver.c @@ -70,6 +70,16 @@ static const fluid_audriver_definition_t fluid_audio_drivers[] = }, #endif +#if PIPEWIRE_SUPPORT + { + "pipewire", + new_fluid_pipewire_audio_driver, + new_fluid_pipewire_audio_driver2, + delete_fluid_pipewire_audio_driver, + fluid_pipewire_audio_driver_settings + }, +#endif + #if OSS_SUPPORT { "oss", diff --git a/src/drivers/fluid_adriver.h b/src/drivers/fluid_adriver.h index 9b214d58..8720fbfc 100644 --- a/src/drivers/fluid_adriver.h +++ b/src/drivers/fluid_adriver.h @@ -138,6 +138,14 @@ void fluid_jack_audio_driver_settings(fluid_settings_t *settings); int fluid_jack_obtain_synth(fluid_settings_t *settings, fluid_synth_t **synth); #endif +#if PIPEWIRE_SUPPORT +fluid_audio_driver_t *new_fluid_pipewire_audio_driver(fluid_settings_t *settings, fluid_synth_t *synth); +fluid_audio_driver_t *new_fluid_pipewire_audio_driver2(fluid_settings_t *settings, + fluid_audio_func_t func, void *data); +void delete_fluid_pipewire_audio_driver(fluid_audio_driver_t *p); +void fluid_pipewire_audio_driver_settings(fluid_settings_t *settings); +#endif + #if SNDMAN_SUPPORT fluid_audio_driver_t *new_fluid_sndmgr_audio_driver(fluid_settings_t *settings, fluid_synth_t *synth); diff --git a/src/drivers/fluid_pipewire.c b/src/drivers/fluid_pipewire.c new file mode 100644 index 00000000..bc17c3c4 --- /dev/null +++ b/src/drivers/fluid_pipewire.c @@ -0,0 +1,307 @@ +/* FluidSynth - A Software Synthesizer + * + * Copyright (C) 2003 Peter Hanappe and others. + * Copyright (C) 2021 E. "sykhro" Melucci + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA + */ + +/* fluid_pipewire.c + * + * Audio driver for PipeWire. + * + */ + +#include "fluid_synth.h" +#include "fluid_adriver.h" +#include "fluid_settings.h" + +#if PIPEWIRE_SUPPORT + +#include +#include + +/* At the moment, only stereo is supported */ +#define NUM_CHANNELS 2 +static const int stride = sizeof(float) * NUM_CHANNELS; + +typedef struct +{ + fluid_audio_driver_t driver; + fluid_audio_func_t user_callback; + void *data; + + /* Used only with the user-provided callback */ + float *lbuf, *rbuf; + + int buffer_period; + struct spa_pod_builder *builder; + + struct pw_thread_loop *pw_loop; + struct pw_stream *pw_stream; + struct pw_stream_events *events; + +} fluid_pipewire_audio_driver_t; + + +/* Fast-path rendering routine with no user processing callbacks */ +static void fluid_pipewire_event_process(void *data) +{ + fluid_pipewire_audio_driver_t *drv = data; + struct pw_buffer *pwb; + struct spa_buffer *buf; + float *dest; + + pwb = pw_stream_dequeue_buffer(drv->pw_stream); + if (!pwb) + { + FLUID_LOG(FLUID_WARN, "No buffers!"); + return; + } + + buf = pwb->buffer; + dest = buf->datas[0].data; + if (!dest) + { + return; + } + + fluid_synth_write_float(drv->data, drv->buffer_period, dest, 0, 2, dest, 1, 2); + buf->datas[0].chunk->offset = 0; + buf->datas[0].chunk->stride = stride; + buf->datas[0].chunk->size = drv->buffer_period * stride; + + pw_stream_queue_buffer(drv->pw_stream, pwb); +} + +/* Rendering routine with support for user-defined audio manipulation */ +static void fluid_pipewire_event_process2(void *data) +{ + fluid_pipewire_audio_driver_t *drv = data; + struct pw_buffer *pwb; + struct spa_buffer *buf; + float *dest; + float *channels[NUM_CHANNELS] = { drv->lbuf, drv->rbuf }; + int i; + + pwb = pw_stream_dequeue_buffer(drv->pw_stream); + if (!pwb) + { + FLUID_LOG(FLUID_WARN, "No buffers!"); + return; + } + + buf = pwb->buffer; + dest = buf->datas[0].data; + if (!dest) + { + return; + } + + FLUID_MEMSET(drv->lbuf, 0, drv->buffer_period * sizeof(float)); + FLUID_MEMSET(drv->rbuf, 0, drv->buffer_period * sizeof(float)); + + (*drv->user_callback)(drv->data, drv->buffer_period, 0, NULL, NUM_CHANNELS, channels); + + /* Interleave the floating point data */ + for (i = 0; i < drv->buffer_period; i++) + { + dest[i * 2] = drv->lbuf[i]; + dest[i * 2 + 1] = drv->rbuf[i]; + } + + buf->datas[0].chunk->offset = 0; + buf->datas[0].chunk->stride = stride; + buf->datas[0].chunk->size = drv->buffer_period * stride; + + pw_stream_queue_buffer(drv->pw_stream, pwb); +} + +fluid_audio_driver_t *new_fluid_pipewire_audio_driver(fluid_settings_t *settings, fluid_synth_t *synth) +{ + return new_fluid_pipewire_audio_driver2(settings, NULL, synth); +} + +fluid_audio_driver_t * +new_fluid_pipewire_audio_driver2(fluid_settings_t *settings, fluid_audio_func_t func, void *data) +{ + fluid_pipewire_audio_driver_t *drv; + int period_size; + int buffer_length; + int res; + int pw_flags; + int realtime_prio = 0; + double sample_rate; + char *media_role = NULL; + char *media_type = NULL; + char *media_category = NULL; + float *buffer = NULL; + const struct spa_pod *params[1]; + + drv = FLUID_NEW(fluid_pipewire_audio_driver_t); + if (!drv) + { + FLUID_LOG(FLUID_ERR, "Out of memory"); + return NULL; + } + FLUID_MEMSET(drv, 0, sizeof(*drv)); + + fluid_settings_getint(settings, "audio.period-size", &period_size); + fluid_settings_getint(settings, "audio.realtime-prio", &realtime_prio); + fluid_settings_getnum(settings, "synth.sample-rate", &sample_rate); + fluid_settings_dupstr(settings, "audio.pipewire.media-role", &media_role); + fluid_settings_dupstr(settings, "audio.pipewire.media-type", &media_type); + fluid_settings_dupstr(settings, "audio.pipewire.media-category", &media_category); + + drv->data = data; + drv->user_callback = func; + drv->buffer_period = period_size; + + drv->events = FLUID_NEW(struct pw_stream_events); + if (!drv->events) + { + FLUID_LOG(FLUID_ERR, "Out of memory"); + goto driver cleanup; + } + FLUID_MEMSET(drv->events, 0, sizeof(*drv->events)); + drv->events->version = PW_VERSION_STREAM_EVENTS; + drv->events->process = func ? fluid_pipewire_event_process2 : fluid_pipewire_event_process; + + drv->pw_loop = pw_thread_loop_new("fluid_pipewire", NULL); + if (!drv->pw_loop) + { + FLUID_LOG(FLUID_ERR, "Failed to allocate PipeWire loop"); + goto driver_cleanup; + } + + drv->pw_stream = pw_stream_new_simple( + pw_thread_loop_get_loop(drv->pw_loop), + "FluidSynth", + pw_properties_new(PW_KEY_MEDIA_TYPE, media_type, PW_KEY_MEDIA_CATEGORY, media_category, PW_KEY_MEDIA_ROLE, media_role, NULL), + drv->events, + drv); + if (!drv->pw_stream) + { + FLUID_LOG(FLUID_ERR, "Failed to allocate PipeWire stream"); + goto driver_cleanup; + } + + buffer = FLUID_ARRAY(float, NUM_CHANNELS *period_size); + if (!buffer) + { + FLUID_LOG(FLUID_ERR, "Out of memory"); + goto driver_cleanup; + } + buffer_length = period_size * sizeof(float) * NUM_CHANNELS; + + drv->builder = FLUID_NEW(struct spa_pod_builder); + if (!drv->builder) + { + FLUID_LOG(FLUID_ERR, "Out of memory"); + goto driver_cleanup; + } + FLUID_MEMSET(drv->builder, 0, sizeof(*drv->builder)); + drv->builder->data = buffer; + drv->builder->size = buffer_length; + + if (func) + { + drv->lbuf = FLUID_ARRAY(float, period_size); + drv->rbuf = FLUID_ARRAY(float, period_size); + + if (!drv->lbuf || !drv->rbuf) + { + FLUID_LOG(FLUID_ERR, "Out of memory"); + goto driver_cleanup; + } + } + + params[0] = spa_format_audio_raw_build(drv->builder, + SPA_PARAM_EnumFormat, + &SPA_AUDIO_INFO_RAW_INIT(.format = SPA_AUDIO_FORMAT_F32, + .channels = 2, + .rate = sample_rate)); + + pw_flags = PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS; + pw_flags |= realtime_prio ? PW_STREAM_FLAG_RT_PROCESS : 0; + res = pw_stream_connect(drv->pw_stream, PW_DIRECTION_OUTPUT, PW_ID_ANY, pw_flags, params, 1); + if (res < 0) + { + FLUID_LOG(FLUID_ERR, "PipeWire stream connection failed"); + goto driver_cleanup; + } + + res = pw_thread_loop_start(drv->pw_loop); + if (res != 0) + { + FLUID_LOG(FLUID_ERR, "Failed starting PipeWire loop"); + goto driver_cleanup; + } + + FLUID_LOG(FLUID_INFO, "Using PipeWire audio driver"); + + free(media_role); + free(media_type); + free(media_category); + + return (fluid_audio_driver_t *)drv; + +driver_cleanup: + free(media_role); + free(media_type); + free(media_category); + + delete_fluid_pipewire_audio_driver((fluid_audio_driver_t *)drv); + return NULL; +} + +void delete_fluid_pipewire_audio_driver(fluid_audio_driver_t *p) +{ + fluid_pipewire_audio_driver_t *drv = (fluid_pipewire_audio_driver_t *)p; + fluid_return_if_fail(drv); + + if (drv->pw_stream) + { + pw_stream_destroy(drv->pw_stream); + } + + if (drv->pw_loop) + { + pw_thread_loop_destroy(drv->pw_loop); + } + + FLUID_FREE(drv->lbuf); + FLUID_FREE(drv->rbuf); + + if (drv->builder) + { + FLUID_FREE(drv->builder->data); + } + FLUID_FREE(drv->builder); + + FLUID_FREE(drv->events); + + FLUID_FREE(drv); +} + +void fluid_pipewire_audio_driver_settings(fluid_settings_t *settings) +{ + fluid_settings_register_str(settings, "audio.pipewire.media-role", "Music", 0); + fluid_settings_register_str(settings, "audio.pipewire.media-type", "Audio", 0); + fluid_settings_register_str(settings, "audio.pipewire.media-category", "Playback", 0); +} + +#endif