diff --git a/CMakeLists.txt b/CMakeLists.txt index 98a56457..392c6929 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -186,9 +186,17 @@ if(UNIX) endif() if(${CMAKE_SYSTEM_NAME} MATCHES "Linux") + find_package(PkgConfig) find_package(ALSA) find_package(JACK) + if(PkgConfig_FOUND) + pkg_check_modules(PIPEWIRE libpipewire-0.3) + if (PIPEWIRE_FOUND) + message(STATUS "Enabled Pipewire support") + endif() + endif() + # Linux MIDI support requires ALSA and RtMidi if(ALSA_FOUND) find_package(RTMIDI 2.1) # 2.1.0 and compatible versions diff --git a/INSTALL.md b/INSTALL.md index 17c77571..11c6c3df 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -19,6 +19,7 @@ following development libraries installed on your system: - RtMidi/ALSA (optional, for Linux MIDI support) - JACK (optional) +- Pipewire (optional and requires pkg-config to discover library in build script) - SDL2 For all **non-Windows/macOS ports**, the decompression libs (optional): diff --git a/docs/readme_unix b/docs/readme_unix index 8dd0ac32..adca2a34 100644 --- a/docs/readme_unix +++ b/docs/readme_unix @@ -44,6 +44,11 @@ Two new audio drivers have been added; ALSA and JACK. JACK by default does not attempt to guess which output device to connect to, the connection should be made manually. +* Pipewire support (experimental) * + +An experimental audio driver for Pipewire has been implemented. The output +stream is set to auto-connect to an output device via Pipewire's usual +mechanisms. Video Driver ============ diff --git a/src/milkyplay/AudioDriverManager.cpp b/src/milkyplay/AudioDriverManager.cpp index 1c3afef1..0c0759d4 100644 --- a/src/milkyplay/AudioDriverManager.cpp +++ b/src/milkyplay/AudioDriverManager.cpp @@ -153,6 +153,9 @@ AudioDriverManager::AudioDriverManager() : #ifdef HAVE_JACK_JACK_H #include "AudioDriver_JACK.h" #endif +#ifdef HAVE_PIPEWIRE +#include "AudioDriver_Pipewire.h" +#endif AudioDriverManager::AudioDriverManager() : defaultDriverIndex(0) @@ -163,6 +166,9 @@ AudioDriverManager::AudioDriverManager() : #endif #ifdef HAVE_JACK_JACK_H count++; +#endif +#ifdef HAVE_PIPEWIRE + count++; #endif ALLOC_DRIVERLIST(count); count = 0; @@ -173,6 +179,9 @@ AudioDriverManager::AudioDriverManager() : #if HAVE_JACK_JACK_H driverList[count++] = new AudioDriver_JACK(); #endif +#if HAVE_PIPEWIRE + driverList[count++] = new AudioDriver_Pipewire(); +#endif } #elif defined(DRIVER_SDL) diff --git a/src/milkyplay/CMakeLists.txt b/src/milkyplay/CMakeLists.txt index 9c3f886e..c74defe3 100644 --- a/src/milkyplay/CMakeLists.txt +++ b/src/milkyplay/CMakeLists.txt @@ -255,6 +255,23 @@ else() message(STATUS "Enabled JACK support") endif() + if(PIPEWIRE_FOUND) + target_sources(milkyplay PRIVATE + # Source + drivers/pipewire/AudioDriver_Pipewire.cpp + + # Header + drivers/pipewire/AudioDriver_Pipewire.h + ) + target_compile_definitions(milkyplay PRIVATE -DHAVE_PIPEWIRE) + target_include_directories(milkyplay PRIVATE + drivers/pipewire + ${PIPEWIRE_INCLUDE_DIRS} + ) + target_link_libraries(milkyplay PUBLIC ${PIPEWIRE_LIBRARIES}) + message(STATUS "Enabled Pipewire support") + endif() + if(SDL2_FOUND) target_sources(milkyplay PRIVATE # Sources diff --git a/src/milkyplay/drivers/pipewire/AudioDriver_Pipewire.cpp b/src/milkyplay/drivers/pipewire/AudioDriver_Pipewire.cpp new file mode 100644 index 00000000..f44bc25d --- /dev/null +++ b/src/milkyplay/drivers/pipewire/AudioDriver_Pipewire.cpp @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2025, The MilkyTracker Team. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * - Neither the name of the nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * AudioDriver_Pipewire.cpp + * Pipewire Audio driver for MilkyTracker + * (it's not that good) + * + * Created by Evan Walter on 27/1/2025 + */ + +#ifdef HAVE_PIPEWIRE + +#include "pipewire/stream.h" +#include +#include "AudioDriver_Pipewire.h" +#include +#include +void AudioDriver_Pipewire::on_stream_param_change(void *userdata, uint32_t id, const struct spa_pod *param) { + AudioDriver_Pipewire *driver = (AudioDriver_Pipewire *) userdata; + struct pw_stream *stream = driver->stream; + uint8_t params_pod_buf[1024]; + struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(params_pod_buf, sizeof(params_pod_buf)); + const struct spa_pod *params[2]; + if (param == NULL || id != SPA_PARAM_Format) + return; + + printf("Pipewire: negotiating buffer format\n"); + + params[0] = (struct spa_pod *) spa_pod_builder_add_object(&builder, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_buffers, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(2 * sizeof(int16_t) * driver->period), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(2 * sizeof(int16_t)), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(1 << SPA_DATA_MemPtr), + SPA_PARAM_BUFFERS_metaType, SPA_POD_Int(0), + SPA_PARAM_BUFFERS_align, SPA_POD_Int(sizeof(int))); + + pw_stream_update_params(stream, params, 1); + printf("Pipewire: stream parameters updated\n"); +} + +void AudioDriver_Pipewire::on_process(void *userdata) +{ + struct pw_buffer *b; + struct spa_buffer *buf_spa; + int16_t *dst; + + AudioDriver_Pipewire *driver = (AudioDriver_Pipewire *) userdata; + const size_t frame_size = sizeof(int16_t) * 2; + + if ((b = pw_stream_dequeue_buffer(driver->stream)) == NULL) + { + return; + } + + // SPA info for the buffer we're to fill this time + buf_spa = b->buffer; + int max_buf_size = buf_spa->datas[0].maxsize; + if ((dst = (int16_t *) buf_spa->datas[0].data) == NULL) + return; + if (max_buf_size != frame_size * driver->period) + return; + + struct spa_chunk *chunk = buf_spa->datas[0].chunk; + chunk->offset = 0; + chunk->stride = frame_size; + chunk->size = max_buf_size; + + driver->fillAudioWithCompensation((char *) dst, max_buf_size); + + pw_stream_queue_buffer(driver->stream, b); +} + +AudioDriver_Pipewire::AudioDriver_Pipewire() : + AudioDriver_COMPENSATE() +{ +} + +AudioDriver_Pipewire::~AudioDriver_Pipewire() +{ +} + +mp_sint32 AudioDriver_Pipewire::initDevice(mp_sint32 periodSizeAsSamples, const mp_uint32 mixFrequency, MasterMixer* mixer) +{ + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder b; + events = new pw_stream_events(); + events->version = PW_VERSION_STREAM_EVENTS; + events->process = AudioDriver_Pipewire::on_process; + events->param_changed = AudioDriver_Pipewire::on_stream_param_change; + period = periodSizeAsSamples; + + printf("Pipewire: Beginning device init\n"); + + b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + pw_init(NULL, NULL); + + printf("Pipewire: Initialized Pipewire\n"); + + pw_loop *loop = pw_loop_new(NULL); + thread_loop = pw_thread_loop_new_full(loop, "MilkyTracker", NULL); + + printf("Pipewire: Created thread loop\n"); + + pw_thread_loop_lock(thread_loop); + pw_loop *inner_loop = pw_thread_loop_get_loop(thread_loop); + pw_thread_loop_unlock(thread_loop); + + stream = pw_stream_new_simple( + inner_loop, + "audio-src", + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + NULL), + events, + this); + + printf("Pipewire: Created stream\n"); + + // We need to spell out the whole thing ourselves: since C++ + // is really picky about struct designator syntax, the macro from PW + // doesn't work + spa_audio_info_raw audio_init_params; + audio_init_params.format = SPA_AUDIO_FORMAT_S16; + audio_init_params.rate = mixFrequency; + audio_init_params.channels = 2; + + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, + &audio_init_params); + + pw_stream_flags flags = (pw_stream_flags) ( + (int) PW_STREAM_FLAG_AUTOCONNECT + | (int) PW_STREAM_FLAG_MAP_BUFFERS + // | (int) PW_STREAM_FLAG_DRIVER + ); + + pw_stream_connect( + stream, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + flags, + params, 1); + + printf("Pipewire: Connected stream\n"); + + deviceHasStarted = 0; + AudioDriverBase::initDevice(periodSizeAsSamples, mixFrequency, mixer); + return periodSizeAsSamples * 2; +} + +mp_sint32 AudioDriver_Pipewire::closeDevice() +{ + printf("Pipewire: closing device\n"); + if (thread_loop) + { + pw_thread_loop_stop(thread_loop); + pw_stream_destroy(stream); + pw_thread_loop_destroy(thread_loop); + pw_deinit(); + } + return 0; +} + + +mp_sint32 AudioDriver_Pipewire::start() +{ + if (!deviceHasStarted) + { + pw_thread_loop_lock(thread_loop); + pw_thread_loop_start(thread_loop); + pw_thread_loop_unlock(thread_loop); + deviceHasStarted = 1; + printf("Pipewire: started device\n"); + } + return 0; +} + +mp_sint32 AudioDriver_Pipewire::stop() +{ + if (deviceHasStarted) + { + pw_thread_loop_stop(thread_loop); + deviceHasStarted = 0; + printf("Pipewire: stopped device\n"); + } + return 0; +} + +mp_sint32 AudioDriver_Pipewire::pause() +{ + stop(); + return 0; +} + +mp_sint32 AudioDriver_Pipewire::resume() +{ + start(); + return 0; +} + +#endif diff --git a/src/milkyplay/drivers/pipewire/AudioDriver_Pipewire.h b/src/milkyplay/drivers/pipewire/AudioDriver_Pipewire.h new file mode 100644 index 00000000..a73cb6ca --- /dev/null +++ b/src/milkyplay/drivers/pipewire/AudioDriver_Pipewire.h @@ -0,0 +1,40 @@ +#ifndef __AUDIODRIVER_PIPEWIRE_H__ +#define __AUDIODRIVER_PIPEWIRE_H__ +#include "AudioDriver_COMPENSATE.h" +#include +#include +#include + +class AudioDriver_Pipewire : public AudioDriver_COMPENSATE +{ +private: + struct pw_stream *stream; + struct pw_thread_loop *thread_loop; + struct pw_context *context; + struct pw_core *core; + static void on_process(void *userdata); + static void on_stream_param_change(void *userdata, uint32_t id, const struct spa_pod *param); + struct pw_stream_events *events; + mp_sint32 period; + bool initialized = false; + +public: + + AudioDriver_Pipewire(); + + virtual ~AudioDriver_Pipewire(); + + virtual mp_sint32 initDevice(mp_sint32 periodSizeAsSamples, mp_uint32 mixFrequency, MasterMixer* mixer); + virtual mp_sint32 closeDevice(); + + virtual mp_sint32 start(); + virtual mp_sint32 stop(); + + virtual mp_sint32 pause(); + virtual mp_sint32 resume(); + + virtual const char* getDriverID() { return "Pipewire"; } + virtual mp_sint32 getPreferredBufferSize() const { return 1024; } +}; + +#endif