diff --git a/CMakeLists.txt b/CMakeLists.txt index 523fda15..c4dc5f28 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,7 @@ option(MINIAUDIO_NO_OPENSL "Disable the OpenSL|ES backend" option(MINIAUDIO_NO_WEBAUDIO "Disable the Web Audio backend" OFF) option(MINIAUDIO_NO_CUSTOM "Disable support for custom backends" OFF) option(MINIAUDIO_NO_NULL "Disable the null backend" OFF) +option(MINIAUDIO_NO_PIPEWIRE "Disable the PipeWire backend" OFF) option(MINIAUDIO_ENABLE_ONLY_SPECIFIC_BACKENDS "Only enable specific backends. Backends can be enabled with MINIAUDIO_ENABLE_[BACKEND]." OFF) option(MINIAUDIO_ENABLE_WASAPI "Enable the WASAPI backend" OFF) option(MINIAUDIO_ENABLE_DSOUND "Enable the DirectSound backend" OFF) @@ -155,6 +156,9 @@ endif() if(MINIAUDIO_NO_NULL) list(APPEND COMPILE_DEFINES MA_NO_NULL) endif() +if(MINIAUDIO_NO_PIPEWIRE) + list(APPEND COMPILE_DEFINES MA_NO_PIPEWIRE) +endif() if(MINIAUDIO_ENABLE_ONLY_SPECIFIC_BACKENDS) if(MINIAUDIO_ENABLE_WASAPI) list(APPEND COMPILE_DEFINES MA_ENABLE_WASAPI) @@ -442,6 +446,20 @@ else() message(STATUS "SteamAudio not found. miniaudio_engine_steamaudio will be excluded.") endif() +# PipeWire. For this we care only about SPA. +if(NOT MINIAUDIO_NO_PIPEWIRE) + find_path(SPA_INCLUDE_DIR NAMES spa/param/audio/format-utils.h HINTS + /usr/include/spa-0.2 + ) + + if(SPA_INCLUDE_DIR) + message(STATUS "Found SPA include directory: ${SPA_INCLUDE_DIR}") + else() + message(STATUS "SPA include directory not found. PipeWire will be excluded.") + set(MINIAUDIO_NO_PIPEWIRE ON) + list(APPEND COMPILE_DEFINES MA_NO_PIPEWIRE) + endif() +endif() # Link libraries set(COMMON_LINK_LIBRARIES) @@ -472,6 +490,24 @@ target_compile_options (miniaudio PRIVATE ${COMPILE_OPTIONS}) target_compile_definitions(miniaudio PRIVATE ${COMPILE_DEFINES}) +# Extra Backends +if(NOT MINIAUDIO_NO_PIPEWIRE) + add_library(miniaudio_pipewire STATIC + extras/backends/pipewire/miniaudio_pipewire.c + extras/backends/pipewire/miniaudio_pipewire.h + ) + + list(APPEND LIBS_TO_INSTALL miniaudio_pipewire) + install(FILES extras/backends/pipewire/miniaudio_pipewire.h DESTINATION include/miniaudio/extras/backends/pipewire) + + target_include_directories(miniaudio_pipewire PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/extras/backends/pipewire) + target_compile_options (miniaudio_pipewire PRIVATE ${COMPILE_OPTIONS}) + target_compile_definitions(miniaudio_pipewire PRIVATE ${COMPILE_DEFINES}) + target_include_directories(miniaudio_pipewire PRIVATE ${SPA_INCLUDE_DIR}) +endif() + + +# Extra Decoders add_library(libvorbis_interface INTERFACE) if(HAS_LIBVORBIS) if(TARGET vorbisfile) @@ -590,6 +626,15 @@ if(MINIAUDIO_BUILD_TESTS) add_miniaudio_test(miniaudio_generation generation/generation.c) add_test(NAME miniaudio_generation COMMAND miniaudio_generation) + + if(NOT MINIAUDIO_NO_PIPEWIRE) + add_executable (miniaudio_pipewire_test extras/backends/pipewire/miniaudio_pipewire_test.c) + target_compile_options (miniaudio_pipewire_test PRIVATE ${COMPILE_OPTIONS}) + target_compile_definitions(miniaudio_pipewire_test PRIVATE ${COMPILE_DEFINES}) + target_include_directories(miniaudio_pipewire_test PRIVATE ${SPA_INCLUDE_DIR}) + target_link_libraries (miniaudio_pipewire_test PRIVATE ${COMMON_LINK_LIBRARIES}) + add_test(NAME miniaudio_pipewire_test COMMAND miniaudio_pipewire_test --auto) + endif() endif() # Examples diff --git a/extras/backends/pipewire/miniaudio_pipewire.c b/extras/backends/pipewire/miniaudio_pipewire.c new file mode 100644 index 00000000..36f52940 --- /dev/null +++ b/extras/backends/pipewire/miniaudio_pipewire.c @@ -0,0 +1,878 @@ +/* +Cannot use SPA_AUDIO_INFO_RAW_INIT because it's a define with varargs. +*/ +#ifndef miniaudio_backend_pipewire_c +#define miniaudio_backend_pipewire_c + +#include "miniaudio_pipewire.h" + +#include /* memset() */ +#include /* assert() */ + +#ifndef MA_PIPEWIRE_ASSERT +#define MA_PIPEWIRE_ASSERT(x) assert(x) +#endif + +#ifndef MA_PIPEWIRE_ZERO_OBJECT +#define MA_PIPEWIRE_ZERO_OBJECT(x) memset((x), 0, sizeof(*(x))) +#endif + + +/* +NOTES FOR NEW BACKEND SYSTEM + +There are currently a few problems with the existing backend system: + + - Backends depend on knowledge of `ma_device` and `ma_context`. It would be better if they had a clearer separation of concerns. + + - For backends that require explicit handling of automatic stream routing, things can get annoyingly awkward when the backend + updates it's internal state and needs to tell the device about it's new properties, such as the new sample rate, etc. + + - There is a non-trivial burden put onto backends when it comes to duplex mode. It would be good if there was a way where the + backend could just concern itself with playback or capture, and then have miniaudio deal with the duplex part of it. Backends + should still be able to support duplex mode themselves in case they can do something more optimal. + + - Due to the combination of tight coupling with `ma_device`, and the application being in control of when the `ma_device` object + is destroyed, things can get a bit messy because the backend may be in the middle of something, such as a device reroute, when + the application decides to destroy the device. By decoupling the backend state from the `ma_device` object, it could give the + backend more flexibility to handle this gracefully. + + + +Some random thoughts on how to improve things follow. + +One idea is to introduce the concept of a "backend state". The backend will concern itself only with this state. It will have no +knowledge of `ma_device` or `ma_context`. It will only care or know about it's own state. + +When a backend initializes a context or device, it'll allocate a state object containing only backend-specific information. This state +object can then be installed into the `ma_context` or `ma_device` object. When creating the state object, miniaudio will provide the +necessary information via a configuration structure. When installing the state object, the backend will provide the necessary information +back to miniaudio via a descriptor structure. + +There can be two different state objects for a device: one for playback and one for capture. When a device is initialized as duplex, +but is not supported by the backend, the backend can return `MA_DEVICE_TYPE_NOT_SUPPORTED`. When this happens, miniaudio can fall back +to creating a separate playback and capture device and then deal with the duplex part itself through the use of a proxy data callback. + +Decoupling the backend state from the `ma_device` object will allow the backend to keep the state object alive while something like a +device reroute is still in progress. This is important because applications can destroy the `ma_device` object at any time. + +When a backend state is installed, it is always accompanied by a `ma_device_descriptor` object. In device initialization, these are +passed into the initialization routine as both an input and output parameter. For installing a device state after the device has +been initialized, such as when a device reroute occurs, a function called something like `ma_device_install_backend_state()` can be +used: + + ma_result ma_device_install_backend_state(ma_device* pDevice, ma_device_type deviceType, const ma_device_descriptor* pDescriptor, void* pBackendState); + +This function would replace `ma_device_post_init()`. + +Need a way for backends to process audio data easily. Maybe pass in a `ma_device_data_callback` object to the initialization function +which can then be stored in the backend state? + + ma_device_data_callback_process(pDeviceDataCallback, pFramesOut, pFramesIn, frameCount); + + +*/ + + + +#if defined(MA_LINUX) + #define MA_SUPPORT_PIPEWIRE +#endif + +#if defined(MA_SUPPORT_PIPEWIRE) && !defined(MA_NO_PIPEWIRE) && (!defined(MA_ENABLE_ONLY_SPECIFIC_BACKENDS) || defined(MA_ENABLE_PIPEWIRE)) + #define MA_HAS_PIPEWIRE +#endif + +#if defined(MA_HAS_PIPEWIRE) +#if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wc99-extensions" + #pragma GCC diagnostic ignored "-Wgnu-statement-expression-from-macro-expansion" + #pragma GCC diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" +#endif +/*#include */ +#include /* For spa_format_audio_raw_build() */ +#include +#include +#include +#include + +#define MA_PW_KEY_MEDIA_TYPE "media.type" +#define MA_PW_KEY_MEDIA_CATEGORY "media.category" +#define MA_PW_KEY_MEDIA_ROLE "media.role" + +#define MA_PW_ID_ANY 0xFFFFFFFF + +struct ma_pw_thread_loop; +struct ma_pw_loop; +struct ma_pw_context; +struct ma_pw_core; +struct ma_pw_properties; +struct ma_pw_stream; +struct ma_pw_stream_control; + +enum ma_pw_stream_state +{ + MA_PW_STREAM_STATE_ERROR = -1, + MA_PW_STREAM_STATE_UNCONNECTED = 0, + MA_PW_STREAM_STATE_CONNECTING = 1, + MA_PW_STREAM_STATE_PAUSED = 2, + MA_PW_STREAM_STATE_STREAMING = 3 +}; + +enum ma_pw_stream_flags +{ + MA_PW_STREAM_FLAG_NONE = 0, + MA_PW_STREAM_FLAG_AUTOCONNECT = (1 << 0), + MA_PW_STREAM_FLAG_INACTIVE = (1 << 1), + MA_PW_STREAM_FLAG_MAP_BUFFERS = (1 << 2), + MA_PW_STREAM_FLAG_DRIVER = (1 << 3), + MA_PW_STREAM_FLAG_RT_PROCESS = (1 << 4), + MA_PW_STREAM_FLAG_NO_CONVERT = (1 << 5), + MA_PW_STREAM_FLAG_EXCLUSIVE = (1 << 6), + MA_PW_STREAM_FLAG_DONT_RECONNECT = (1 << 7), + MA_PW_STREAM_FLAG_ALLOC_BUFFERS = (1 << 8), + MA_PW_STREAM_FLAG_TRIGGER = (1 << 9), + MA_PW_STREAM_FLAG_ASYNC = (1 << 10) +}; + +struct ma_pw_buffer +{ + struct spa_buffer* buffer; + void* user_data; + ma_uint64 size; + ma_uint64 requested; +}; + + +#define MA_PW_VERSION_STREAM_EVENTS 2 + +struct ma_pw_stream_events +{ + ma_uint32 version; + void (* destroy )(void* data); + void (* state_changed)(void* data, enum ma_pw_stream_state oldState, enum ma_pw_stream_state newState, const char* error); + void (* control_info )(void* data, ma_uint32 id, const struct ma_pw_stream_control* control); + void (* io_changed )(void* data, ma_uint32 id, void* area, ma_uint32 size); + void (* param_changed)(void* data, ma_uint32 id, const struct spa_pod* param); + void (* add_buffer )(void* data, struct ma_pw_buffer* buffer); + void (* remove_buffer)(void* data, struct ma_pw_buffer* buffer); + void (* process )(void* data); + void (* drained )(void* data); + void (* command )(void* data, const struct spa_command* command); + void (* trigger_done )(void* data); +}; + +typedef void (* ma_pw_init_proc )(int* argc, char*** argv); +typedef void (* ma_pw_deinit_proc )(void); +typedef struct ma_pw_thread_loop* (* ma_pw_thread_loop_new_proc )(const char* name, const struct spa_dict* props); +typedef void (* ma_pw_thread_loop_destroy_proc )(struct ma_pw_thread_loop* loop); +typedef struct ma_pw_loop* (* ma_pw_thread_loop_get_loop_proc )(struct ma_pw_thread_loop* loop); +typedef int (* ma_pw_thread_loop_start_proc )(struct ma_pw_thread_loop* loop); +typedef void (* ma_pw_thread_loop_lock_proc )(struct ma_pw_thread_loop* loop); +typedef void (* ma_pw_thread_loop_unlock_proc )(struct ma_pw_thread_loop* loop); +typedef struct ma_pw_context* (* ma_pw_context_new_proc )(struct ma_pw_loop* loop, const char* name, const struct spa_dict* props); +typedef void (* ma_pw_context_destroy_proc )(struct ma_pw_context* context); +typedef struct ma_pw_core* (* ma_pw_context_connect_proc )(struct ma_pw_context* context, struct ma_pw_properties* properties, size_t user_data_size); +typedef void (* ma_pw_core_disconnect_proc )(struct ma_pw_core* core); +typedef struct ma_pw_properties* (* ma_pw_properties_new_proc )(const char* key, ...); +typedef void (* ma_pw_properties_free_proc )(struct ma_pw_properties* properties); +typedef struct ma_pw_stream* (* ma_pw_stream_new_proc )(struct ma_pw_core* core, const char* name, struct ma_pw_properties* props); +typedef void (* ma_pw_stream_destroy_proc )(struct ma_pw_stream* stream); +typedef void (* ma_pw_stream_add_listener_proc )(struct ma_pw_stream* stream, struct spa_hook* listener, const struct ma_pw_stream_events* events, void* data); +typedef int (* ma_pw_stream_connect_proc )(struct ma_pw_stream* stream, enum spa_direction direction, ma_uint32 target_id, enum ma_pw_stream_flags flags, const struct spa_pod** params, ma_uint32 paramCount); +typedef int (* ma_pw_stream_set_active_proc )(struct ma_pw_stream* stream, bool active); +typedef struct ma_pw_buffer* (* ma_pw_stream_dequeue_buffer_proc)(struct ma_pw_stream* stream); +typedef int (* ma_pw_stream_queue_buffer_proc )(struct ma_pw_stream* stream, struct ma_pw_buffer* buffer); +typedef int (* ma_pw_stream_update_params_proc )(struct ma_pw_stream* stream, const struct spa_pod** params, ma_uint32 paramCount); + +typedef struct +{ + ma_handle hPipeWire; + ma_pw_init_proc pw_init; + ma_pw_deinit_proc pw_deinit; + ma_pw_thread_loop_new_proc pw_thread_loop_new; + ma_pw_thread_loop_destroy_proc pw_thread_loop_destroy; + ma_pw_thread_loop_get_loop_proc pw_thread_loop_get_loop; + ma_pw_thread_loop_start_proc pw_thread_loop_start; + ma_pw_thread_loop_lock_proc pw_thread_loop_lock; + ma_pw_thread_loop_unlock_proc pw_thread_loop_unlock; + ma_pw_context_new_proc pw_context_new; + ma_pw_context_destroy_proc pw_context_destroy; + ma_pw_context_connect_proc pw_context_connect; + ma_pw_core_disconnect_proc pw_core_disconnect; + ma_pw_properties_new_proc pw_properties_new; + ma_pw_properties_free_proc pw_properties_free; + ma_pw_stream_new_proc pw_stream_new; + ma_pw_stream_destroy_proc pw_stream_destroy; + ma_pw_stream_add_listener_proc pw_stream_add_listener; + ma_pw_stream_connect_proc pw_stream_connect; + ma_pw_stream_set_active_proc pw_stream_set_active; + ma_pw_stream_dequeue_buffer_proc pw_stream_dequeue_buffer; + ma_pw_stream_queue_buffer_proc pw_stream_queue_buffer; + ma_pw_stream_update_params_proc pw_stream_update_params; +} ma_context_state_pipewire; + +typedef struct +{ + struct ma_pw_thread_loop* pThreadLoop; + struct ma_pw_context* pContext; + struct ma_pw_core* pCore; + struct ma_pw_stream* pStreamPlayback; + struct ma_pw_stream* pStreamCapture; + struct spa_hook eventListener; + ma_format format; + ma_uint32 channels; + ma_uint32 sampleRate; + ma_bool32 isInitialized; + ma_bool32 isInternalFormatFinalised; + struct + { + ma_device_descriptor* pDescriptor; /* This is only used for setting up internal format. It's needed here because it looks like the only way to get the internal format is via a stupid callback. */ + struct ma_pw_stream* pStream; /* The stream that's being configured. One of either pStreamPlayback or pStreamCapture. Used for the stupid param_changed callback. TODO: I think this can be removed. */ + } paramChangedCallbackData; +} ma_device_state_pipewire; + + +static enum spa_audio_format ma_format_to_pipewire(ma_format format) +{ + switch (format) + { + case ma_format_u8: return SPA_AUDIO_FORMAT_U8; + case ma_format_s16: return SPA_AUDIO_FORMAT_S16; + case ma_format_s24: return SPA_AUDIO_FORMAT_S24; + case ma_format_s32: return SPA_AUDIO_FORMAT_S32; + case ma_format_f32: return SPA_AUDIO_FORMAT_F32; + default: return SPA_AUDIO_FORMAT_UNKNOWN; + } +} + +static ma_format ma_format_from_pipewire(enum spa_audio_format format) +{ + switch (format) + { + case SPA_AUDIO_FORMAT_U8: return ma_format_u8; + case SPA_AUDIO_FORMAT_S16: return ma_format_s16; + case SPA_AUDIO_FORMAT_S24: return ma_format_s24; + case SPA_AUDIO_FORMAT_S32: return ma_format_s32; + case SPA_AUDIO_FORMAT_F32: return ma_format_f32; + default: return ma_format_unknown; + } +} + + +static ma_context_state_pipewire* ma_context_get_backend_state__pipewire(ma_context* pContext) +{ + return (ma_context_state_pipewire*)ma_context_get_backend_state(pContext); +} + +static ma_device_state_pipewire* ma_device_get_backend_state__pipewire(ma_device* pDevice) +{ + return (ma_device_state_pipewire*)ma_device_get_backend_state(pDevice); +} + + +static void ma_backend_info__pipewire(ma_device_backend_info* pBackendInfo) +{ + MA_PIPEWIRE_ASSERT(pBackendInfo != NULL); + pBackendInfo->pName = "PipeWire"; +} + +static ma_result ma_context_init__pipewire(ma_context* pContext, const void* pContextBackendConfig, void** ppContextState) +{ + /* We'll use a list of possible shared object names for easier extensibility. */ + ma_context_state_pipewire* pContextStatePipeWire; + ma_context_config_pipewire* pContextConfigPipeWire = (ma_context_config_pipewire*)pContextBackendConfig; + ma_log* pLog = ma_context_get_log(pContext); + ma_handle hPipeWire; + size_t iName; + const char* pSONames[] = { + "libpipewire-0.3.so.0", + "libpipewire.so" + }; + + (void)pContextConfigPipeWire; + + pContextStatePipeWire = (ma_context_state_pipewire*)ma_calloc(sizeof(*pContextStatePipeWire), ma_context_get_allocation_callbacks(pContext)); + if (pContextStatePipeWire == NULL) { + return MA_OUT_OF_MEMORY; + } + + + /* Check if we have a PipeWire SO. If we can't find this we need to abort. */ + for (iName = 0; iName < ma_countof(pSONames); iName += 1) { + hPipeWire = ma_dlopen(pLog, pSONames[iName]); + if (hPipeWire != NULL) { + break; + } + } + + if (hPipeWire == NULL) { + ma_free(pContextStatePipeWire, ma_context_get_allocation_callbacks(pContext)); + return MA_NO_BACKEND; /* PipeWire could not be loaded. */ + } + + /* Now that we have the handle to the shared object we can go ahead and load some function pointers. */ + pContextStatePipeWire->hPipeWire = hPipeWire; + pContextStatePipeWire->pw_init = (ma_pw_init_proc )ma_dlsym(pLog, hPipeWire, "pw_init"); + pContextStatePipeWire->pw_deinit = (ma_pw_deinit_proc )ma_dlsym(pLog, hPipeWire, "pw_deinit"); + pContextStatePipeWire->pw_thread_loop_new = (ma_pw_thread_loop_new_proc )ma_dlsym(pLog, hPipeWire, "pw_thread_loop_new"); + pContextStatePipeWire->pw_thread_loop_destroy = (ma_pw_thread_loop_destroy_proc )ma_dlsym(pLog, hPipeWire, "pw_thread_loop_destroy"); + pContextStatePipeWire->pw_thread_loop_get_loop = (ma_pw_thread_loop_get_loop_proc )ma_dlsym(pLog, hPipeWire, "pw_thread_loop_get_loop"); + pContextStatePipeWire->pw_thread_loop_start = (ma_pw_thread_loop_start_proc )ma_dlsym(pLog, hPipeWire, "pw_thread_loop_start"); + pContextStatePipeWire->pw_thread_loop_lock = (ma_pw_thread_loop_lock_proc )ma_dlsym(pLog, hPipeWire, "pw_thread_loop_lock"); + pContextStatePipeWire->pw_thread_loop_unlock = (ma_pw_thread_loop_unlock_proc )ma_dlsym(pLog, hPipeWire, "pw_thread_loop_unlock"); + pContextStatePipeWire->pw_context_new = (ma_pw_context_new_proc )ma_dlsym(pLog, hPipeWire, "pw_context_new"); + pContextStatePipeWire->pw_context_destroy = (ma_pw_context_destroy_proc )ma_dlsym(pLog, hPipeWire, "pw_context_destroy"); + pContextStatePipeWire->pw_context_connect = (ma_pw_context_connect_proc )ma_dlsym(pLog, hPipeWire, "pw_context_connect"); + pContextStatePipeWire->pw_core_disconnect = (ma_pw_core_disconnect_proc )ma_dlsym(pLog, hPipeWire, "pw_core_disconnect"); + pContextStatePipeWire->pw_properties_new = (ma_pw_properties_new_proc )ma_dlsym(pLog, hPipeWire, "pw_properties_new"); + pContextStatePipeWire->pw_properties_free = (ma_pw_properties_free_proc )ma_dlsym(pLog, hPipeWire, "pw_properties_free"); + pContextStatePipeWire->pw_stream_new = (ma_pw_stream_new_proc )ma_dlsym(pLog, hPipeWire, "pw_stream_new"); + pContextStatePipeWire->pw_stream_destroy = (ma_pw_stream_destroy_proc )ma_dlsym(pLog, hPipeWire, "pw_stream_destroy"); + pContextStatePipeWire->pw_stream_add_listener = (ma_pw_stream_add_listener_proc )ma_dlsym(pLog, hPipeWire, "pw_stream_add_listener"); + pContextStatePipeWire->pw_stream_connect = (ma_pw_stream_connect_proc )ma_dlsym(pLog, hPipeWire, "pw_stream_connect"); + pContextStatePipeWire->pw_stream_set_active = (ma_pw_stream_set_active_proc )ma_dlsym(pLog, hPipeWire, "pw_stream_set_active"); + pContextStatePipeWire->pw_stream_dequeue_buffer = (ma_pw_stream_dequeue_buffer_proc)ma_dlsym(pLog, hPipeWire, "pw_stream_dequeue_buffer"); + pContextStatePipeWire->pw_stream_queue_buffer = (ma_pw_stream_queue_buffer_proc )ma_dlsym(pLog, hPipeWire, "pw_stream_queue_buffer"); + pContextStatePipeWire->pw_stream_update_params = (ma_pw_stream_update_params_proc )ma_dlsym(pLog, hPipeWire, "pw_stream_update_params"); + + if (pContextStatePipeWire->pw_init != NULL) { + pContextStatePipeWire->pw_init(NULL, NULL); + } + + *ppContextState = pContextStatePipeWire; + + return MA_SUCCESS; +} + +static void ma_context_uninit__pipewire(ma_context* pContext) +{ + ma_context_state_pipewire* pContextStatePipeWire = ma_context_get_backend_state__pipewire(pContext); + + MA_PIPEWIRE_ASSERT(pContextStatePipeWire != NULL); + + if (pContextStatePipeWire->pw_deinit != NULL) { + pContextStatePipeWire->pw_deinit(); + } + + /* Close the handle to the PipeWire shared object last. */ + ma_dlclose(ma_context_get_log(pContext), pContextStatePipeWire->hPipeWire); + pContextStatePipeWire->hPipeWire = NULL; + + ma_free(pContextStatePipeWire, ma_context_get_allocation_callbacks(pContext)); +} + +static ma_result ma_context_enumerate_devices__pipewire(ma_context* pContext, ma_enum_devices_callback_proc callback, void* pCallbackUserData) +{ + ma_context_state_pipewire* pContextStatePipeWire = ma_context_get_backend_state__pipewire(pContext); + ma_bool32 cbResult = MA_TRUE; + + MA_PIPEWIRE_ASSERT(pContextStatePipeWire != NULL); + MA_PIPEWIRE_ASSERT(callback != NULL); + + /* TODO: Proper device enumeration. */ + + /* Playback. */ + if (cbResult) { + ma_device_info deviceInfo; + MA_PIPEWIRE_ZERO_OBJECT(&deviceInfo); + deviceInfo.id.custom.i = 0; + ma_strncpy_s(deviceInfo.name, sizeof(deviceInfo.name), "Default Playback Device", (size_t)-1); + + cbResult = callback(ma_device_type_playback, &deviceInfo, pCallbackUserData); + } + + /* Capture. */ + if (cbResult) { + ma_device_info deviceInfo; + MA_PIPEWIRE_ZERO_OBJECT(&deviceInfo); + deviceInfo.id.custom.i = 0; + ma_strncpy_s(deviceInfo.name, sizeof(deviceInfo.name), "Default Capture Device", (size_t)-1); + + cbResult = callback(ma_device_type_capture, &deviceInfo, pCallbackUserData); + } + + return MA_SUCCESS; +} + +static ma_result ma_context_get_device_info__pipewire(ma_context* pContext, ma_device_type deviceType, const ma_device_id* pDeviceID, ma_device_info* pDeviceInfo) +{ + /* TODO: Implement this properly. */ + + (void)pContext; + (void)pDeviceID; + + MA_PIPEWIRE_ZERO_OBJECT(pDeviceInfo); + + /* ID */ + pDeviceInfo->id.custom.i = 0; + + /* Name */ + if (deviceType == ma_device_type_playback) { + ma_strncpy_s(pDeviceInfo->name, sizeof(pDeviceInfo->name), "Default Playback Device", (size_t)-1); + } else { + ma_strncpy_s(pDeviceInfo->name, sizeof(pDeviceInfo->name), "Default Capture Device", (size_t)-1); + } + + pDeviceInfo->nativeDataFormats[pDeviceInfo->nativeDataFormatCount].format = ma_format_unknown; + pDeviceInfo->nativeDataFormats[pDeviceInfo->nativeDataFormatCount].channels = 0; + pDeviceInfo->nativeDataFormats[pDeviceInfo->nativeDataFormatCount].sampleRate = 0; + pDeviceInfo->nativeDataFormats[pDeviceInfo->nativeDataFormatCount].flags = 0; + pDeviceInfo->nativeDataFormatCount += 1; + + return MA_SUCCESS; +} + + +static void ma_stream_event_param_changed__pipewire(void* pUserData, ma_uint32 id, const struct spa_pod* pParam) +{ + ma_device* pDevice = (ma_device*)pUserData; + ma_device_state_pipewire* pDeviceStatePipeWire = ma_device_get_backend_state__pipewire(pDevice); + ma_context_state_pipewire* pContextStatePipeWire = ma_context_get_backend_state__pipewire(ma_device_get_context(pDevice)); + + if (pParam != NULL && id == SPA_PARAM_Format) { + struct spa_audio_info_raw audioInfo; + char podBuilderBuffer[1024]; + struct spa_pod_builder podBuilder; + const struct spa_pod* pBufferParameters[1]; + ma_uint32 bufferSizeInFrames; + ma_uint32 bytesPerFrame; + + if (pDeviceStatePipeWire->isInternalFormatFinalised) { + ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_WARNING, "PipeWire format parameter changed after device has been initialized."); + return; + } + + printf("Param Changed: %d\n", id); + + /* + We can now determine the format/channels/rate which will let us configure the size of the buffer and set the + internal format of the device. + */ + spa_format_audio_raw_parse(pParam, &audioInfo); + + printf("Format: %d\n", audioInfo.format); + printf("Channels: %d\n", audioInfo.channels); + printf("Rate: %d\n", audioInfo.rate); + printf("Channel Map: {"); + for (ma_uint32 iChannel = 0; iChannel < audioInfo.channels; iChannel += 1) { + printf("%d", audioInfo.position[iChannel]); + if (iChannel < audioInfo.channels - 1) { + printf(", "); + } + } + printf("}\n"); + + + /* Now that we definitely know the sample rate, we can reliable configure the size of the buffer. */ + bufferSizeInFrames = pDeviceStatePipeWire->paramChangedCallbackData.pDescriptor->periodSizeInFrames; + if (bufferSizeInFrames == 0) { + bufferSizeInFrames = ma_calculate_buffer_size_in_frames_from_descriptor(pDeviceStatePipeWire->paramChangedCallbackData.pDescriptor, (ma_uint32)audioInfo.rate, ma_performance_profile_low_latency); + } + + /* Update the descriptor. This is where the internal format/channels/rate is set. */ + pDeviceStatePipeWire->paramChangedCallbackData.pDescriptor->format = ma_format_from_pipewire(audioInfo.format); + pDeviceStatePipeWire->paramChangedCallbackData.pDescriptor->channels = audioInfo.channels; + pDeviceStatePipeWire->paramChangedCallbackData.pDescriptor->sampleRate = audioInfo.rate; + pDeviceStatePipeWire->paramChangedCallbackData.pDescriptor->periodSizeInFrames = bufferSizeInFrames; + pDeviceStatePipeWire->paramChangedCallbackData.pDescriptor->periodCount = 2; + + + bytesPerFrame = ma_get_bytes_per_frame(pDeviceStatePipeWire->paramChangedCallbackData.pDescriptor->format, pDeviceStatePipeWire->paramChangedCallbackData.pDescriptor->channels); + + /* Now update the PipeWire buffer properties. */ + podBuilder = SPA_POD_BUILDER_INIT(podBuilderBuffer, sizeof(podBuilderBuffer)); + + pBufferParameters[0] = (const spa_pod*)spa_pod_builder_add_object(&podBuilder, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(pDeviceStatePipeWire->paramChangedCallbackData.pDescriptor->periodCount, 2, 8), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(bytesPerFrame), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(bytesPerFrame * bufferSizeInFrames)); + + pContextStatePipeWire->pw_stream_update_params(pDeviceStatePipeWire->paramChangedCallbackData.pStream, pBufferParameters, sizeof(pBufferParameters) / sizeof(pBufferParameters[0])); + + pDeviceStatePipeWire->isInternalFormatFinalised = MA_TRUE; + } +} + +static void ma_stream_event_process__pipewire(void* pUserData) +{ + ma_device* pDevice = (ma_device*)pUserData; + ma_device_state_pipewire* pDeviceStatePipeWire = ma_device_get_backend_state__pipewire(pDevice); + ma_context_state_pipewire* pContextStatePipeWire = ma_context_get_backend_state__pipewire(ma_device_get_context(pDevice)); + struct ma_pw_stream* pStream; + struct ma_pw_buffer* pBuffer; + ma_uint32 bytesPerFrame; + ma_uint32 frameCount; + ma_device_type deviceType; + + if (!pDeviceStatePipeWire->isInitialized) { + printf("Not initialized\n"); + return; + } + + deviceType = ma_device_get_type(pDevice); + + if (deviceType == ma_device_type_playback || deviceType == ma_device_type_duplex) { + pStream = pDeviceStatePipeWire->pStreamPlayback; + bytesPerFrame = ma_get_bytes_per_frame(pDeviceStatePipeWire->format, pDeviceStatePipeWire->channels); + } else { + pStream = pDeviceStatePipeWire->pStreamCapture; + bytesPerFrame = ma_get_bytes_per_frame(pDeviceStatePipeWire->format, pDeviceStatePipeWire->channels); + } + + for (;;) { + pBuffer = pContextStatePipeWire->pw_stream_dequeue_buffer(pStream); + if (pBuffer == NULL) { + return; + } + + if (pBuffer->buffer->datas[0].data == NULL) { + return; + } + + //frameCount = (ma_uint32)ma_min(pBuffer->requested, pBuffer->buffer->datas[0].maxsize / bytesPerFrame); + frameCount = (ma_uint32)(pBuffer->buffer->datas[0].maxsize / bytesPerFrame); + if (frameCount > 0) { + if (pStream == pDeviceStatePipeWire->pStreamPlayback) { + printf("(Playback) Processing %d frames... %d %d\n", (int)frameCount, (int)pBuffer->requested, pBuffer->buffer->datas[0].maxsize / bytesPerFrame); + ma_device_handle_backend_data_callback(pDevice, pBuffer->buffer->datas[0].data, NULL, frameCount); + } else { + printf("(Capture) Processing %d frames...\n", (int)frameCount); + ma_device_handle_backend_data_callback(pDevice, NULL, pBuffer->buffer->datas[0].data, frameCount); + } + + //printf("Done...\n"); + } else { + ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_WARNING, "(PipeWire) No frames to process.\n"); + } + + pBuffer->buffer->datas[0].chunk->offset = 0; + pBuffer->buffer->datas[0].chunk->stride = bytesPerFrame; + pBuffer->buffer->datas[0].chunk->size = frameCount * bytesPerFrame; + + pContextStatePipeWire->pw_stream_queue_buffer(pStream, pBuffer); + } +} + +static const struct ma_pw_stream_events ma_gStreamEventsPipeWire = +{ + MA_PW_VERSION_STREAM_EVENTS, + NULL, /* destroy */ + NULL, /* state_changed */ + NULL, /* control_info */ + NULL, /* io_changed */ + ma_stream_event_param_changed__pipewire, + NULL, /* add_buffer */ + NULL, /* remove_buffer */ + ma_stream_event_process__pipewire, + NULL, /* drained */ + NULL, /* command */ + NULL, /* trigger_done */ +}; + +static ma_result ma_device_init_internal__pipewire(ma_device* pDevice, ma_context_state_pipewire* pContextStatePipeWire, ma_device_state_pipewire* pDeviceStatePipeWire, const ma_device_config_pipewire* pDeviceConfigPipeWire, ma_device_type deviceType, ma_device_descriptor* pDescriptor) +{ + struct spa_audio_info_raw audioInfo; + struct ma_pw_stream* pStream; + struct ma_pw_properties* pProperties; + char podBuilderBuffer[1024]; /* A random buffer for use by the POD builder. I have no idea what the purpose of this is and what an appropriate size it should be set to. Why is this even a thing? */ + struct spa_pod_builder podBuilder; + const struct spa_pod* pConnectionParameters[1]; + enum ma_pw_stream_flags streamFlags; + int connectResult; + //ma_uint32 bufferSizeInFrames; + + /* This function can only be called for playback or capture sides. */ + if (deviceType != ma_device_type_playback && deviceType != ma_device_type_capture) { + return MA_INVALID_ARGS; + } + + pDeviceStatePipeWire->paramChangedCallbackData.pDescriptor = pDescriptor; /* <-- This is only for the param_changed callback. We'll clear this to NULL when we're done initializing. */ + + + pProperties = pContextStatePipeWire->pw_properties_new( + MA_PW_KEY_MEDIA_TYPE, "Audio", + MA_PW_KEY_MEDIA_CATEGORY, "Playback", + MA_PW_KEY_MEDIA_ROLE, (pDeviceConfigPipeWire->pMediaRole != NULL) ? pDeviceConfigPipeWire->pMediaRole : "Game", + NULL); + + pContextStatePipeWire->pw_thread_loop_lock(pDeviceStatePipeWire->pThreadLoop); + { + pStream = pContextStatePipeWire->pw_stream_new(pDeviceStatePipeWire->pCore, (pDeviceConfigPipeWire->pStreamName != NULL) ? pDeviceConfigPipeWire->pStreamName : "miniaudio", pProperties); + if (pStream == NULL) { + ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR, "Failed to create PipeWire stream."); + pContextStatePipeWire->pw_thread_loop_unlock(pDeviceStatePipeWire->pThreadLoop); + return MA_ERROR; + } + + /* This installs callbacks for process and param_changed. "process" is for queuing audio data, and "param_changed" is for getting the internal format/channels/rate. */ + pContextStatePipeWire->pw_stream_add_listener(pStream, &pDeviceStatePipeWire->eventListener, &ma_gStreamEventsPipeWire, pDevice); + + /* Set the stream in the device data so that we can use it in the param_changed callback. This will be cleared later. */ + pDeviceStatePipeWire->paramChangedCallbackData.pStream = pStream; + + + + podBuilder = SPA_POD_BUILDER_INIT(podBuilderBuffer, sizeof(podBuilderBuffer)); + + memset(&audioInfo, 0, sizeof(audioInfo)); + audioInfo.format = ma_format_to_pipewire(pDescriptor->format); + audioInfo.channels = pDescriptor->channels; + audioInfo.rate = pDescriptor->sampleRate; + /* We're going to leave the channel map alone and just do a conversion ourselves if it differs from the native map. */ + /* TODO: It looks like the params_changed callback does not have a filled out channel map? We might need to do a miniaudio-to-PipeWire channel map conversion and do it that way. Was hoping to just use the native channel map. */ + + pConnectionParameters[0] = spa_format_audio_raw_build(&podBuilder, SPA_PARAM_EnumFormat, &audioInfo); + + /* + pConnectionParameters[1] = spa_pod_builder_add_object(&podBuilder, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(pDescriptor->periodCount, 2, 8), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(bufferSizeInFrames * ma_get_bytes_per_frame(pDescriptor->format, pDescriptor->channels))); + */ + + /* + I'm just using MAP_BUFFERS because it's what the PipeWire examples do. I don't know what this does. Also, what's the + point in the AUTOCONNECT flag? Is that not what we're already doing by calling a function called "connect"?! + + We also can't use INACTIVE because without it, the param_changed callback will not get called, but we depend on that + so we can get access to the internal format/channels/rate. + */ + streamFlags = (ma_pw_stream_flags)(MA_PW_STREAM_FLAG_AUTOCONNECT | /*MA_PW_STREAM_FLAG_INACTIVE |*/ MA_PW_STREAM_FLAG_MAP_BUFFERS); + + connectResult = pContextStatePipeWire->pw_stream_connect(pStream, (deviceType == ma_device_type_playback) ? SPA_DIRECTION_OUTPUT : SPA_DIRECTION_INPUT, MA_PW_ID_ANY, streamFlags, pConnectionParameters, ma_countof(pConnectionParameters)); + if (connectResult < 0) { + ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR, "Failed to connect PipeWire stream."); + pContextStatePipeWire->pw_stream_destroy(pStream); + pContextStatePipeWire->pw_thread_loop_unlock(pDeviceStatePipeWire->pThreadLoop); + return MA_ERROR; + } + + if (deviceType == ma_device_type_playback) { + pDeviceStatePipeWire->pStreamPlayback = pStream; + } else { + pDeviceStatePipeWire->pStreamCapture = pStream; + } + } + pContextStatePipeWire->pw_thread_loop_unlock(pDeviceStatePipeWire->pThreadLoop); + + /* TODO: Wait on a mutex or spinlock. */ + while (pDeviceStatePipeWire->isInternalFormatFinalised == MA_FALSE) { + //ma_sleep(10); + } + + /* Devices are in a stopped state by default in miniaudio. */ + pContextStatePipeWire->pw_thread_loop_lock(pDeviceStatePipeWire->pThreadLoop); + { + pContextStatePipeWire->pw_stream_set_active(pStream, MA_FALSE); + } + pContextStatePipeWire->pw_thread_loop_unlock(pDeviceStatePipeWire->pThreadLoop); + + + printf("STREAM INIT DONE\n"); + pDeviceStatePipeWire->paramChangedCallbackData.pDescriptor = NULL; + pDeviceStatePipeWire->paramChangedCallbackData.pStream = NULL; + pDeviceStatePipeWire->isInitialized = MA_TRUE; + return MA_SUCCESS; +} + +static ma_result ma_device_init__pipewire(ma_device* pDevice, const void* pDeviceBackendConfig, ma_device_descriptor* pDescriptorPlayback, ma_device_descriptor* pDescriptorCapture, void** ppDeviceState) +{ + ma_result result; + struct ma_pw_thread_loop* pThreadLoop; + struct ma_pw_context* pPipeWireContext; + struct ma_pw_core* pCore; + ma_context_state_pipewire* pContextStatePipeWire; + ma_device_state_pipewire* pDeviceStatePipeWire; + const ma_device_config_pipewire* pDeviceConfigPipeWire; + ma_device_config_pipewire defaultDeviceConfigPipeWire; + ma_device_type deviceType; + + pContextStatePipeWire = ma_context_get_backend_state__pipewire(ma_device_get_context(pDevice)); + MA_PIPEWIRE_ASSERT(pContextStatePipeWire != NULL); + + /* Grab the config. This can be null in which case we'll use a default. */ + pDeviceConfigPipeWire = (const ma_device_config_pipewire*)pDeviceBackendConfig; + if (pDeviceConfigPipeWire == NULL) { + defaultDeviceConfigPipeWire = ma_device_config_pipewire_init(); + pDeviceConfigPipeWire = &defaultDeviceConfigPipeWire; + } + + deviceType = ma_device_get_type(pDevice); + + /* Not sure how to do loopback with PipeWire, but it feels like something PipeWire would support. Look into this. */ + if (deviceType == ma_device_type_loopback) { + return MA_DEVICE_TYPE_NOT_SUPPORTED; + } + + pThreadLoop = pContextStatePipeWire->pw_thread_loop_new((pDeviceConfigPipeWire->pThreadName != NULL) ? pDeviceConfigPipeWire->pThreadName : "miniaudio (PipeWire)", NULL); + if (pThreadLoop == NULL) { + ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR, "Failed to create PipeWire thread loop."); + return MA_ERROR; + } + + pPipeWireContext = pContextStatePipeWire->pw_context_new(pContextStatePipeWire->pw_thread_loop_get_loop(pThreadLoop), NULL, 0); + if (pPipeWireContext == NULL) { + ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR, "Failed to create PipeWire context."); + pContextStatePipeWire->pw_thread_loop_destroy(pThreadLoop); + return MA_ERROR; + } + + pCore = pContextStatePipeWire->pw_context_connect(pPipeWireContext, NULL, 0); + if (pCore == NULL) { + ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR, "Failed to connect PipeWire context."); + pContextStatePipeWire->pw_context_destroy(pPipeWireContext); + pContextStatePipeWire->pw_thread_loop_destroy(pThreadLoop); + return MA_ERROR; + } + + /* We can now allocate our per-device PipeWire-specific data. */ + pDeviceStatePipeWire = (ma_device_state_pipewire*)ma_calloc(sizeof(*pDeviceStatePipeWire), ma_device_get_allocation_callbacks(pDevice)); + if (pDeviceStatePipeWire == NULL) { + pContextStatePipeWire->pw_thread_loop_destroy(pThreadLoop); + return MA_OUT_OF_MEMORY; + } + + pDeviceStatePipeWire->pThreadLoop = pThreadLoop; + pDeviceStatePipeWire->pContext = pPipeWireContext; + pDeviceStatePipeWire->pCore = pCore; + + /* If I start the loop right after creating it, I get errors from PipeWire. */ + pContextStatePipeWire->pw_thread_loop_start(pThreadLoop); + + if (deviceType == ma_device_type_capture || deviceType == ma_device_type_duplex) { + result = ma_device_init_internal__pipewire(pDevice, pContextStatePipeWire, pDeviceStatePipeWire, pDeviceConfigPipeWire, ma_device_type_capture, pDescriptorCapture); + } + if (deviceType == ma_device_type_playback || deviceType == ma_device_type_duplex) { + result = ma_device_init_internal__pipewire(pDevice, pContextStatePipeWire, pDeviceStatePipeWire, pDeviceConfigPipeWire, ma_device_type_playback, pDescriptorPlayback); + } + + if (result != MA_SUCCESS) { + ma_free(pDeviceStatePipeWire, ma_device_get_allocation_callbacks(pDevice)); + return result; + } + + pDeviceStatePipeWire->format = pDescriptorPlayback->format; + pDeviceStatePipeWire->channels = pDescriptorPlayback->channels; + pDeviceStatePipeWire->sampleRate = pDescriptorPlayback->sampleRate; + + *ppDeviceState = pDeviceStatePipeWire; + + return MA_SUCCESS; +} + + + +static void ma_device_uninit__pipewire(ma_device* pDevice) +{ + ma_device_state_pipewire* pDeviceStatePipeWire = ma_device_get_backend_state__pipewire(pDevice); + ma_context_state_pipewire* pContextStatePipeWire = ma_context_get_backend_state__pipewire(ma_device_get_context(pDevice)); + + pContextStatePipeWire->pw_thread_loop_lock(pDeviceStatePipeWire->pThreadLoop); + { + if (pDeviceStatePipeWire->pStreamCapture != NULL) { + pContextStatePipeWire->pw_stream_destroy(pDeviceStatePipeWire->pStreamCapture); + pDeviceStatePipeWire->pStreamCapture = NULL; + } + + if (pDeviceStatePipeWire->pStreamPlayback != NULL) { + pContextStatePipeWire->pw_stream_destroy(pDeviceStatePipeWire->pStreamPlayback); + pDeviceStatePipeWire->pStreamPlayback = NULL; + } + } + pContextStatePipeWire->pw_thread_loop_unlock(pDeviceStatePipeWire->pThreadLoop); + + ma_free(pDeviceStatePipeWire, ma_device_get_allocation_callbacks(pDevice)); +} + + + +static ma_result ma_device_start__pipewire(ma_device* pDevice) +{ + ma_device_state_pipewire* pDeviceStatePipeWire = ma_device_get_backend_state__pipewire(pDevice); + ma_context_state_pipewire* pContextStatePipeWire = ma_context_get_backend_state__pipewire(ma_device_get_context(pDevice)); + + pContextStatePipeWire->pw_thread_loop_lock(pDeviceStatePipeWire->pThreadLoop); + { + if (pDeviceStatePipeWire->pStreamCapture != NULL) { + pContextStatePipeWire->pw_stream_set_active(pDeviceStatePipeWire->pStreamCapture, MA_TRUE); + } + + if (pDeviceStatePipeWire->pStreamPlayback != NULL) { + pContextStatePipeWire->pw_stream_set_active(pDeviceStatePipeWire->pStreamPlayback, MA_TRUE); + } + } + pContextStatePipeWire->pw_thread_loop_unlock(pDeviceStatePipeWire->pThreadLoop); + + return MA_SUCCESS; +} + + +static ma_result ma_device_stop__pipewire(ma_device* pDevice) +{ + ma_device_state_pipewire* pDeviceStatePipeWire = ma_device_get_backend_state__pipewire(pDevice); + ma_context_state_pipewire* pContextStatePipeWire = ma_context_get_backend_state__pipewire(ma_device_get_context(pDevice)); + + pContextStatePipeWire->pw_thread_loop_lock(pDeviceStatePipeWire->pThreadLoop); + { + if (pDeviceStatePipeWire->pStreamCapture != NULL) { + pContextStatePipeWire->pw_stream_set_active(pDeviceStatePipeWire->pStreamCapture, MA_FALSE); + } + + if (pDeviceStatePipeWire->pStreamPlayback != NULL) { + pContextStatePipeWire->pw_stream_set_active(pDeviceStatePipeWire->pStreamPlayback, MA_FALSE); + } + } + pContextStatePipeWire->pw_thread_loop_unlock(pDeviceStatePipeWire->pThreadLoop); + + return MA_SUCCESS; +} + +static ma_device_backend_vtable ma_gDeviceBackendVTable_PipeWire = +{ + ma_backend_info__pipewire, + ma_context_init__pipewire, + ma_context_uninit__pipewire, + ma_context_enumerate_devices__pipewire, + ma_context_get_device_info__pipewire, + ma_device_init__pipewire, + ma_device_uninit__pipewire, + ma_device_start__pipewire, + ma_device_stop__pipewire, + NULL, /* onDeviceRead */ + NULL, /* onDeviceWrite */ + NULL, /* onDeviceLoop */ + NULL /* onDeviceWakeup */ +}; + +ma_device_backend_vtable* ma_device_backend_pipewire = &ma_gDeviceBackendVTable_PipeWire; + +#if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) + #pragma GCC diagnostic pop +#endif +#else +ma_device_backend_vtable* ma_device_backend_pipewire = NULL; +#endif /* MA_HAS_PIPEWIRE */ + +MA_API ma_context_config_pipewire ma_context_config_pipewire_init(void) +{ + ma_context_config_pipewire config; + + memset(&config, 0, sizeof(config)); + + return config; +} + +MA_API ma_device_config_pipewire ma_device_config_pipewire_init(void) +{ + ma_device_config_pipewire config; + + memset(&config, 0, sizeof(config)); + + return config; +} +#endif /* miniaudio_backend_pipewire_c */ \ No newline at end of file diff --git a/extras/backends/pipewire/miniaudio_pipewire.h b/extras/backends/pipewire/miniaudio_pipewire.h new file mode 100644 index 00000000..36ac7bf8 --- /dev/null +++ b/extras/backends/pipewire/miniaudio_pipewire.h @@ -0,0 +1,49 @@ +/* +The PipeWire backend depends on the libspa development packages. On Debian-based distributions, +this can be installed with: + + sudo apt install libspa-0.2-dev + +If using Ubuntu, this may install it in a "spa-0.2" subfolder. In this case, you might need +to add the following to your build command: + + -I/usr/include/spa-0.2 + +Unfortunately PipeWire has a hard dependency on the above package, and because it's made up +entirely of non-trivial inlined code, it's not possible to avoid this dependency. It's for +this reason the PipeWire backend cannot be included in miniaudio.h since it has a requirement +that it does not depend on external development packages. +*/ +#ifndef miniaudio_backend_pipewire_h +#define miniaudio_backend_pipewire_h + +#include "../../../miniaudio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +extern ma_device_backend_vtable* ma_device_backend_pipewire; + + +typedef struct +{ + int _unused; +} ma_context_config_pipewire; + +MA_API ma_context_config_pipewire ma_context_config_pipewire_init(void); + + +typedef struct +{ + const char* pThreadName; /* If NULL, defaults to "miniaudio-pipewire". */ + const char* pStreamName; /* If NULL, defaults to "miniaudio" */ + const char* pMediaRole; /* If NULL, defaults to "Game". */ +} ma_device_config_pipewire; + +MA_API ma_device_config_pipewire ma_device_config_pipewire_init(void); + +#ifdef __cplusplus +} +#endif +#endif /* miniaudio_backend_pipewire_h */ \ No newline at end of file diff --git a/extras/backends/pipewire/miniaudio_pipewire_test.c b/extras/backends/pipewire/miniaudio_pipewire_test.c new file mode 100644 index 00000000..34ab3e48 --- /dev/null +++ b/extras/backends/pipewire/miniaudio_pipewire_test.c @@ -0,0 +1,121 @@ +/* +Compile with the following: + + gcc miniaudio_pipewire_test.c -lm -I/usr/include/spa-0.2 +*/ + +#define MA_DEBUG_OUTPUT +#include "../../../miniaudio.c" +#include "miniaudio_pipewire.c" + + +/* +Main program starts here. +*/ +#define DEVICE_FORMAT ma_format_f32 +#define DEVICE_CHANNELS 2 +#define DEVICE_SAMPLE_RATE 48000 + +void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) +{ + MA_ASSERT(pDevice->playback.channels == DEVICE_CHANNELS); + + if (pDevice->type == ma_device_type_playback) { + ma_waveform* pSineWave; + + pSineWave = (ma_waveform*)pDevice->pUserData; + MA_ASSERT(pSineWave != NULL); + + //printf("WAVEFORM: %d\n", (int)frameCount); + ma_waveform_read_pcm_frames(pSineWave, pOutput, frameCount, NULL); + } + + if (pDevice->type == ma_device_type_duplex) { + ma_copy_pcm_frames(pOutput, pInput, frameCount, pDevice->playback.format, pDevice->playback.channels); + } +} + +int main(int argc, char** argv) +{ + ma_result result; + ma_context_config contextConfig; + ma_context context; + ma_device_config deviceConfig; + ma_device device; + ma_waveform_config sineWaveConfig; + ma_waveform sineWave; + ma_context_config_pipewire pipewireContextConfig; + ma_device_config_pipewire pipewireDeviceConfig; + char name[256]; + + + /* Plug in our vtable pointers. Add any custom backends to this list. */ + pipewireContextConfig = ma_context_config_pipewire_init(); + + ma_device_backend_config pBackends[] = + { + { ma_device_backend_pipewire, &pipewireContextConfig } + }; + + contextConfig = ma_context_config_init(); + + result = ma_context_init(pBackends, sizeof(pBackends)/sizeof(pBackends[0]), &contextConfig, (ma_context*)&context); + if (result != MA_SUCCESS) { + printf("Failed to initialize context.\n"); + return -1; + } + + + /* In playback mode we're just going to play a sine wave. */ + sineWaveConfig = ma_waveform_config_init(DEVICE_FORMAT, DEVICE_CHANNELS, DEVICE_SAMPLE_RATE, ma_waveform_type_sine, 0.2, 220); + ma_waveform_init(&sineWaveConfig, &sineWave); + + + /* The device is created exactly as per normal. */ + pipewireDeviceConfig = ma_device_config_pipewire_init(); + + ma_device_backend_config pBackendDeviceConfigs[] = + { + { ma_device_backend_pipewire, &pipewireDeviceConfig } + }; + + deviceConfig = ma_device_config_init(ma_device_type_playback); + deviceConfig.playback.format = DEVICE_FORMAT; + deviceConfig.playback.channels = DEVICE_CHANNELS; + deviceConfig.capture.format = DEVICE_FORMAT; + deviceConfig.capture.channels = DEVICE_CHANNELS; + deviceConfig.sampleRate = DEVICE_SAMPLE_RATE; + deviceConfig.dataCallback = data_callback; + deviceConfig.pUserData = &sineWave; + deviceConfig.pBackendConfigs = pBackendDeviceConfigs; + deviceConfig.backendConfigCount = (sizeof(pBackendDeviceConfigs) / sizeof(pBackendDeviceConfigs[0])); + deviceConfig.periodSizeInMilliseconds = 20; + + result = ma_device_init((ma_context*)&context, &deviceConfig, (ma_device*)&device); + if (result != MA_SUCCESS) { + printf("Failed to initialize device.\n"); + ma_context_uninit((ma_context*)&context); + return -1; + } + + + ma_device_get_name((ma_device*)&device, ma_device_type_playback, name, sizeof(name), NULL); + printf("Device Name: %s\n", name); + + if (ma_device_start((ma_device*)&device) != MA_SUCCESS) { + ma_device_uninit((ma_device*)&device); + ma_context_uninit((ma_context*)&context); + return -5; + } + + printf("Press Enter to quit...\n"); + getchar(); + + ma_device_uninit((ma_device*)&device); + ma_context_uninit((ma_context*)&context); + + (void)argc; + (void)argv; + + return 0; +}