From 104a65f18358067b0eb0b1a49bee2ce477d1ce54 Mon Sep 17 00:00:00 2001 From: casey langen Date: Wed, 3 Mar 2021 22:53:59 -0800 Subject: [PATCH 01/24] Added scaffolding for a PipeWire output plugin. --- CMakeLists.txt | 4 + src/plugins/pipewireout/CMakeLists.txt | 9 ++ src/plugins/pipewireout/PipeWireOut.cpp | 114 ++++++++++++++++++ src/plugins/pipewireout/PipeWireOut.h | 72 +++++++++++ .../pipewireout/pipewireout_plugin.cpp | 63 ++++++++++ 5 files changed, 262 insertions(+) create mode 100644 src/plugins/pipewireout/CMakeLists.txt create mode 100644 src/plugins/pipewireout/PipeWireOut.cpp create mode 100644 src/plugins/pipewireout/PipeWireOut.h create mode 100644 src/plugins/pipewireout/pipewireout_plugin.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e5825fc98..d6248b562 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -155,6 +155,10 @@ add_dependencies(musikcubed musikcube) if (CMAKE_SYSTEM_NAME MATCHES "Linux") add_subdirectory(src/plugins/alsaout) add_subdirectory(src/plugins/pulseout) + if (${ENABLE_PIPEWIRE} MATCHES "true") + add_subdirectory(src/plugins/pipewireout) + add_dependencies(musikcube pipewireout) + endif() if (${ENABLE_MPRIS} MATCHES "true") add_subdirectory(src/plugins/mpris) add_dependencies(musikcube mpris) diff --git a/src/plugins/pipewireout/CMakeLists.txt b/src/plugins/pipewireout/CMakeLists.txt new file mode 100644 index 000000000..fcd67615c --- /dev/null +++ b/src/plugins/pipewireout/CMakeLists.txt @@ -0,0 +1,9 @@ +set (pipewireout_SOURCES + pipewireout_plugin.cpp + PipeWireOut.cpp +) + +message(STATUS "[pipewireout] plugin enabled") + +add_library(pipewireout SHARED ${pipewireout_SOURCES}) +target_link_libraries(pipewireout ${musikcube_LINK_LIBS} pipewire-0.3) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp new file mode 100644 index 000000000..12884c869 --- /dev/null +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -0,0 +1,114 @@ +////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2004-2020 musikcube 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 author nor the names of other 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 OWNER 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. +// +////////////////////////////////////////////////////////////////////////////// + +#include "PipeWireOut.h" + +#include +#include +#include +#include + +static IPreferences* prefs = nullptr; + +extern "C" void SetPreferences(IPreferences* prefs) { + ::prefs = prefs; +} + +extern "C" musik::core::sdk::ISchema* GetSchema() { + auto schema = new TSchema<>(); + return schema; +} + +PipeWireOut::PipeWireOut() { + this->volume = 1.0f; + this->state = StateStopped; +} + +PipeWireOut::~PipeWireOut() { +} + +void PipeWireOut::Release() { + delete this; +} + +void PipeWireOut::Pause() { + this->state = StatePaused; +} + +void PipeWireOut::Resume() { + this->state = StatePlaying; +} + +void PipeWireOut::SetVolume(double volume) { + this->volume = volume; +} + +double PipeWireOut::GetVolume() { + return this->volume; +} + +void PipeWireOut::Stop() { + this->state = StateStopped; +} + +void PipeWireOut::Drain() { +} + +IDeviceList* PipeWireOut::GetDeviceList() { + return nullptr; +} + +bool PipeWireOut::SetDefaultDevice(const char* deviceId) { + return false; +} + +IDevice* PipeWireOut::GetDefaultDevice() { + return nullptr; +} + +OutputState PipeWireOut::Play(IBuffer *buffer, IBufferProvider *provider) { + if (this->state == StatePaused) { + return OutputState::InvalidState; + } + + /* order of operations matters, otherwise overflow. */ + int micros = ((buffer->Samples() * 1000) / buffer->SampleRate() * 1000) / buffer->Channels(); + usleep((long)((float) micros)); + provider->OnBufferProcessed(buffer); + return OutputState::BufferWritten; +} + +double PipeWireOut::Latency() { + return 0.0; +} \ No newline at end of file diff --git a/src/plugins/pipewireout/PipeWireOut.h b/src/plugins/pipewireout/PipeWireOut.h new file mode 100644 index 000000000..6de47b565 --- /dev/null +++ b/src/plugins/pipewireout/PipeWireOut.h @@ -0,0 +1,72 @@ +////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2004-2020 musikcube 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 author nor the names of other 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 OWNER 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. +// +////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +using namespace musik::core::sdk; + +class PipeWireOut : public IOutput { + public: + PipeWireOut(); + ~PipeWireOut(); + + /* IPlugin */ + const char* Name() override { return "PipeWire"; }; + void Release() override; + + /* IOutput */ + void Pause() override; + void Resume() override; + void SetVolume(double volume) override; + double GetVolume() override; + void Stop() override; + OutputState Play(IBuffer *buffer, IBufferProvider *provider) override; + double Latency() override; + void Drain() override; + IDeviceList* GetDeviceList() override; + bool SetDefaultDevice(const char* deviceId) override; + IDevice* GetDefaultDevice() override; + + private: + enum State { + StateStopped, + StatePaused, + StatePlaying + }; + + State state; + double volume; +}; diff --git a/src/plugins/pipewireout/pipewireout_plugin.cpp b/src/plugins/pipewireout/pipewireout_plugin.cpp new file mode 100644 index 000000000..23193902b --- /dev/null +++ b/src/plugins/pipewireout/pipewireout_plugin.cpp @@ -0,0 +1,63 @@ +////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2004-2020 musikcube 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 author nor the names of other 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 OWNER 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. +// +////////////////////////////////////////////////////////////////////////////// + +#include "config.h" + +#include +#include +#include +#include "PipeWireOut.h" + +class PipeWirePlugin : public musik::core::sdk::IPlugin { + public: + void Release() noexcept override { delete this; } + const char* Name() override { return "PipeWire IOutput"; } + const char* Version() override { return "0.1.0"; } + const char* Author() override { return "clangen"; } + const char* Guid() override { return "ab79e0f2-53d8-4774-ad00-266a30c50427"; } + bool Configurable() override { return false; } + void Configure() override { } + void Reload() override { } + int SdkVersion() override { return musik::core::sdk::SdkVersion; } +}; + +extern "C" musik::core::sdk::IPlugin* GetPlugin() { + return new PipeWirePlugin(); +} + +extern "C" musik::core::sdk::IOutput* GetAudioOutput() { + return new PipeWireOut(); +} + +extern "C" musik::core::sdk::ISchema* GetSchema(); \ No newline at end of file From dee5150c2e51bbcaf39653fea880aeaefd6244f9 Mon Sep 17 00:00:00 2001 From: casey langen Date: Fri, 5 Mar 2021 09:32:02 -0800 Subject: [PATCH 02/24] Fleshed out some PipeWire implementation, but no way to test it yet. --- src/plugins/pipewireout/PipeWireOut.cpp | 131 ++++++++++++++++++++++-- src/plugins/pipewireout/PipeWireOut.h | 32 ++++-- 2 files changed, 150 insertions(+), 13 deletions(-) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp index 12884c869..0418fd878 100644 --- a/src/plugins/pipewireout/PipeWireOut.cpp +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -37,7 +37,10 @@ #include #include #include +#include +#include #include +#include static IPreferences* prefs = nullptr; @@ -50,9 +53,26 @@ extern "C" musik::core::sdk::ISchema* GetSchema() { return schema; } +static void onStreamStageChanged( + void *data, + pw_stream_state old, + pw_stream_state state, + const char *error) +{ + std::cerr << "[PipeWire] state changed from " << old << " to " << state << "\n"; +} + +static void onStreamProcess(void *userdata) { + std::cerr << "clclcl onProcess\n"; +} + +static const struct pw_stream_events streamEvents = { + PW_VERSION_STREAM_EVENTS, + .process = onStreamProcess, + .state_changed = onStreamStageChanged +}; + PipeWireOut::PipeWireOut() { - this->volume = 1.0f; - this->state = StateStopped; } PipeWireOut::~PipeWireOut() { @@ -63,11 +83,11 @@ void PipeWireOut::Release() { } void PipeWireOut::Pause() { - this->state = StatePaused; + this->state = State::Paused; } void PipeWireOut::Resume() { - this->state = StatePlaying; + this->state = State::Playing; } void PipeWireOut::SetVolume(double volume) { @@ -79,7 +99,7 @@ double PipeWireOut::GetVolume() { } void PipeWireOut::Stop() { - this->state = StateStopped; + this->state = State::Stopped; } void PipeWireOut::Drain() { @@ -97,8 +117,105 @@ IDevice* PipeWireOut::GetDefaultDevice() { return nullptr; } +void PipeWireOut::StopPipeWire() { + std::unique_lock lock(this->mutex); + + if (this->pwThreadLoop) { + pw_thread_loop_stop(this->pwThreadLoop); + pw_thread_loop_destroy(this->pwThreadLoop); + this->pwThreadLoop = nullptr; + } + + if (this->pwStream) { + pw_stream_destroy(this->pwStream); + this->pwStream = nullptr; + } + + this->initialized = false; +} + +bool PipeWireOut::StartPipeWire(IBuffer* buffer) { + std::unique_lock lock(this->mutex); + + pw_init(nullptr, nullptr); + + this->pwThreadLoop = pw_thread_loop_new("musikcube", nullptr); + if (this->pwThreadLoop) { + int result; + + if ((result = pw_thread_loop_start(this->pwThreadLoop)) != 0) { + std::cerr << "[PipeWire] error starting thread loop: " << spa_strerror(result) << "\n"; + goto cleanup; + }; + + pw_thread_loop_lock(this->pwThreadLoop); + + this->pwStream = pw_stream_new_simple( + pw_thread_loop_get_loop(this->pwThreadLoop), + "musikcube", + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Music", + NULL), + &streamEvents, + this); + + if (this->pwStream) { + uint8_t intBuffer[1024]; + + spa_pod_builder builder = + SPA_POD_BUILDER_INIT(intBuffer, sizeof(intBuffer)); + + const spa_pod *params[1]; + + spa_audio_info_raw audioInfo = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .channels = (uint32_t) buffer->Channels(), + .rate = (uint32_t) buffer->SampleRate()); + + params[0] = spa_format_audio_raw_build( + &builder, SPA_PARAM_EnumFormat, &audioInfo); + + result = pw_stream_connect( + this->pwStream, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + (pw_stream_flags)(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), + params, + 1); + + if (result == 0) { + pw_thread_loop_unlock(this->pwThreadLoop); + std::cerr << "[PipeWire] stream created and connected\n"; + this->initialized = true; + return true; + } + else { + std::cerr << "[PipeWire] error starting stream: " << spa_strerror(result) << "\n"; + } + } + } + +cleanup: + pw_thread_loop_unlock(this->pwThreadLoop); + std::cerr << "[PipeWire] stream not initialized.\n"; + this->StopPipeWire(); + return false; +} + OutputState PipeWireOut::Play(IBuffer *buffer, IBufferProvider *provider) { - if (this->state == StatePaused) { + if (!this->initialized) { + if (!this->StartPipeWire(buffer)) { + return OutputState::InvalidState; + } + } + + // std::cerr << "clclcl here\n"; + + /* TODO: if buffer format changes, drain, then update the params! */ + + if (this->state == State::Paused) { return OutputState::InvalidState; } @@ -111,4 +228,4 @@ OutputState PipeWireOut::Play(IBuffer *buffer, IBufferProvider *provider) { double PipeWireOut::Latency() { return 0.0; -} \ No newline at end of file +} diff --git a/src/plugins/pipewireout/PipeWireOut.h b/src/plugins/pipewireout/PipeWireOut.h index 6de47b565..b611ccb6e 100644 --- a/src/plugins/pipewireout/PipeWireOut.h +++ b/src/plugins/pipewireout/PipeWireOut.h @@ -35,6 +35,11 @@ #pragma once #include +#include +#include +#include +#include +#include using namespace musik::core::sdk; @@ -61,12 +66,27 @@ class PipeWireOut : public IOutput { IDevice* GetDefaultDevice() override; private: - enum State { - StateStopped, - StatePaused, - StatePlaying + bool StartPipeWire(IBuffer* buffer); + void StopPipeWire(); + + struct BufferState { + BufferState(IBuffer* buffer, IBufferProvider* provider) { + this->buffer = buffer; this->provider = provider; + } + IBuffer* buffer; + IBufferProvider* provider; }; - State state; - double volume; + enum class State { + Stopped, Paused, Playing + }; + + std::unordered_set buffers; + std::recursive_mutex mutex; + std::atomic initialized { false }; + std::atomic state { State::Stopped }; + double volume { 1.0 }; + pw_thread_loop* pwThreadLoop { nullptr }; + pw_stream* pwStream { nullptr }; + }; From beb95e55a577ef658891218281e8f0faa6b4ace6 Mon Sep 17 00:00:00 2001 From: casey langen Date: Sun, 14 Mar 2021 18:52:12 -0700 Subject: [PATCH 03/24] More scaffholding. --- CMakeLists.txt | 6 +- src/plugins/mpris/mpris.h | 1 + src/plugins/pipewireout/CMakeLists.txt | 3 + src/plugins/pipewireout/PipeWireOut.cpp | 111 +++++++++++++++++------- src/plugins/pipewireout/PipeWireOut.h | 21 +++-- 5 files changed, 104 insertions(+), 38 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d6248b562..d638e8adb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -155,10 +155,8 @@ add_dependencies(musikcubed musikcube) if (CMAKE_SYSTEM_NAME MATCHES "Linux") add_subdirectory(src/plugins/alsaout) add_subdirectory(src/plugins/pulseout) - if (${ENABLE_PIPEWIRE} MATCHES "true") - add_subdirectory(src/plugins/pipewireout) - add_dependencies(musikcube pipewireout) - endif() + add_subdirectory(src/plugins/pipewireout) + add_dependencies(musikcube pipewireout) if (${ENABLE_MPRIS} MATCHES "true") add_subdirectory(src/plugins/mpris) add_dependencies(musikcube mpris) diff --git a/src/plugins/mpris/mpris.h b/src/plugins/mpris/mpris.h index e10752b9e..92d145c75 100644 --- a/src/plugins/mpris/mpris.h +++ b/src/plugins/mpris/mpris.h @@ -5,6 +5,7 @@ #include #include #include +#include extern "C" { #include diff --git a/src/plugins/pipewireout/CMakeLists.txt b/src/plugins/pipewireout/CMakeLists.txt index fcd67615c..7a4fb8ee3 100644 --- a/src/plugins/pipewireout/CMakeLists.txt +++ b/src/plugins/pipewireout/CMakeLists.txt @@ -5,5 +5,8 @@ set (pipewireout_SOURCES message(STATUS "[pipewireout] plugin enabled") +include_directories("/usr/include/spa-0.2") +include_directories("/usr/include/pipewire-0.3") + add_library(pipewireout SHARED ${pipewireout_SOURCES}) target_link_libraries(pipewireout ${musikcube_LINK_LIBS} pipewire-0.3) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp index 0418fd878..c378a9323 100644 --- a/src/plugins/pipewireout/PipeWireOut.cpp +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -42,6 +42,8 @@ #include #include +constexpr size_t MAX_BUFFERS = 16; + static IPreferences* prefs = nullptr; extern "C" void SetPreferences(IPreferences* prefs) { @@ -53,26 +55,70 @@ extern "C" musik::core::sdk::ISchema* GetSchema() { return schema; } -static void onStreamStageChanged( - void *data, - pw_stream_state old, - pw_stream_state state, - const char *error) -{ +void PipeWireOut::OnStreamStateChanged(void* data, enum pw_stream_state old, enum pw_stream_state state, const char* error) { std::cerr << "[PipeWire] state changed from " << old << " to " << state << "\n"; } -static void onStreamProcess(void *userdata) { - std::cerr << "clclcl onProcess\n"; +void PipeWireOut::OnStreamProcess(void* data) { + PipeWireOut* output = static_cast(data); + + struct pw_buffer* pwBuffer; + + if ((pwBuffer = pw_stream_dequeue_buffer(output->pwStream)) == nullptr) { + std::cerr << "[PipeWire] no more output buffers available to fill\n"; + return; + } + + BufferContext* outputBufferContext = nullptr; + + { + std::unique_lock lock(output->mutex); + if (output->buffers.empty()) { + std::cerr << "[PipeWire] no more input buffers available\n"; + return; + } + outputBufferContext = output->buffers.front(); + output->buffers.pop_front(); + } + + struct spa_buffer* spaBuffer = pwBuffer->buffer; + auto& outBufferData = spaBuffer->datas[0]; + void* outBufferPtr = outBufferData.data; + uint32_t outBufferSize = outBufferData.maxsize; + void* inBufferPtr = outputBufferContext->buffer->BufferPointer() ; + uint32_t inBufferSize = (uint32_t) outputBufferContext->buffer->Bytes(); + uint32_t frameSize = sizeof(float) * outputBufferContext->buffer->Channels(); + + std::cerr << "[PipeWire] onProcess " << inBufferSize << " vs " << outBufferSize << "\n"; + + /* TODO: assumes dest size >= src size. need to do additional book keeping in + BufferContext to store offset if target isn't large enough, then push the + struct back */ + + if (inBufferSize > outBufferSize) { + std::cerr << "[PipeWire] onProcess output buffer too small\n"; + exit(0); + } + + memcpy(outBufferPtr, inBufferPtr, inBufferSize); + outBufferData.chunk->offset = 0; + outBufferData.chunk->stride = frameSize; + outBufferData.chunk->size = inBufferSize; + pwBuffer->size = inBufferSize / frameSize; + + outputBufferContext->provider->OnBufferProcessed(outputBufferContext->buffer); + + delete outputBufferContext; + + pw_stream_queue_buffer(output->pwStream, pwBuffer); } -static const struct pw_stream_events streamEvents = { - PW_VERSION_STREAM_EVENTS, - .process = onStreamProcess, - .state_changed = onStreamStageChanged -}; - PipeWireOut::PipeWireOut() { + this->pwStreamEvents = { + PW_VERSION_STREAM_EVENTS, + }; + this->pwStreamEvents.state_changed = PipeWireOut::OnStreamStateChanged; + this->pwStreamEvents.process = OnStreamProcess; } PipeWireOut::~PipeWireOut() { @@ -158,25 +204,32 @@ bool PipeWireOut::StartPipeWire(IBuffer* buffer) { PW_KEY_MEDIA_CATEGORY, "Playback", PW_KEY_MEDIA_ROLE, "Music", NULL), - &streamEvents, + &this->pwStreamEvents, this); if (this->pwStream) { uint8_t intBuffer[1024]; - spa_pod_builder builder = + spa_pod_builder builder = SPA_POD_BUILDER_INIT(intBuffer, sizeof(intBuffer)); const spa_pod *params[1]; - spa_audio_info_raw audioInfo = SPA_AUDIO_INFO_RAW_INIT( - .format = SPA_AUDIO_FORMAT_F32, - .channels = (uint32_t) buffer->Channels(), - .rate = (uint32_t) buffer->SampleRate()); + spa_audio_info_raw audioInfo; + spa_zero(audioInfo); + audioInfo.flags = 0; + audioInfo.format = SPA_AUDIO_FORMAT_F32; + audioInfo.channels = (uint32_t) buffer->Channels(); + audioInfo.rate = (uint32_t) buffer->SampleRate(); params[0] = spa_format_audio_raw_build( &builder, SPA_PARAM_EnumFormat, &audioInfo); + if (!params[0]) { + std::cerr << "[PipeWire] failed to create audio format\n"; + goto cleanup; + } + result = pw_stream_connect( this->pwStream, PW_DIRECTION_OUTPUT, @@ -211,18 +264,18 @@ OutputState PipeWireOut::Play(IBuffer *buffer, IBufferProvider *provider) { } } - // std::cerr << "clclcl here\n"; - - /* TODO: if buffer format changes, drain, then update the params! */ - - if (this->state == State::Paused) { + if (this->state != State::Playing) { return OutputState::InvalidState; } - /* order of operations matters, otherwise overflow. */ - int micros = ((buffer->Samples() * 1000) / buffer->SampleRate() * 1000) / buffer->Channels(); - usleep((long)((float) micros)); - provider->OnBufferProcessed(buffer); + { + std::unique_lock lock(this->mutex); + if (this->buffers.size() >= MAX_BUFFERS) { + return OutputState::BufferFull; + } + this->buffers.push_back(new BufferContext(buffer, provider)); + } + return OutputState::BufferWritten; } diff --git a/src/plugins/pipewireout/PipeWireOut.h b/src/plugins/pipewireout/PipeWireOut.h index b611ccb6e..41b450dae 100644 --- a/src/plugins/pipewireout/PipeWireOut.h +++ b/src/plugins/pipewireout/PipeWireOut.h @@ -39,7 +39,7 @@ #include #include #include -#include +#include using namespace musik::core::sdk; @@ -69,8 +69,19 @@ class PipeWireOut : public IOutput { bool StartPipeWire(IBuffer* buffer); void StopPipeWire(); - struct BufferState { - BufferState(IBuffer* buffer, IBufferProvider* provider) { + static void OnStreamStateChanged( + void* userdata, + enum pw_stream_state old, + enum pw_stream_state state, + const char* error); + + static void OnStreamProcess(void* userdata); + + struct BufferContext { + BufferContext() { + this->buffer = nullptr; this->provider = nullptr; + } + BufferContext(IBuffer* buffer, IBufferProvider* provider) { this->buffer = buffer; this->provider = provider; } IBuffer* buffer; @@ -81,12 +92,12 @@ class PipeWireOut : public IOutput { Stopped, Paused, Playing }; - std::unordered_set buffers; + std::deque buffers; std::recursive_mutex mutex; std::atomic initialized { false }; std::atomic state { State::Stopped }; double volume { 1.0 }; + pw_stream_events pwStreamEvents; pw_thread_loop* pwThreadLoop { nullptr }; pw_stream* pwStream { nullptr }; - }; From 8172cac91f4da8469d43912fde7dd74e82439c26 Mon Sep 17 00:00:00 2001 From: casey langen Date: Sun, 14 Mar 2021 22:22:06 -0700 Subject: [PATCH 04/24] Still not producing accurate sound, but this update ensures we completely fill our input buffers before returning them... at least in theory... we're not producing sound yet in the VM. --- src/plugins/pipewireout/PipeWireOut.cpp | 92 ++++++++++++++++--------- src/plugins/pipewireout/PipeWireOut.h | 23 ++++--- 2 files changed, 75 insertions(+), 40 deletions(-) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp index c378a9323..bdd80033d 100644 --- a/src/plugins/pipewireout/PipeWireOut.cpp +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -60,57 +60,84 @@ void PipeWireOut::OnStreamStateChanged(void* data, enum pw_stream_state old, enu } void PipeWireOut::OnStreamProcess(void* data) { - PipeWireOut* output = static_cast(data); + PipeWireOut* self = static_cast(data); struct pw_buffer* pwBuffer; - if ((pwBuffer = pw_stream_dequeue_buffer(output->pwStream)) == nullptr) { + if ((pwBuffer = pw_stream_dequeue_buffer(self->pwStream)) == nullptr) { std::cerr << "[PipeWire] no more output buffers available to fill\n"; return; } - BufferContext* outputBufferContext = nullptr; - - { - std::unique_lock lock(output->mutex); - if (output->buffers.empty()) { - std::cerr << "[PipeWire] no more input buffers available\n"; - return; - } - outputBufferContext = output->buffers.front(); - output->buffers.pop_front(); - } - struct spa_buffer* spaBuffer = pwBuffer->buffer; auto& outBufferData = spaBuffer->datas[0]; void* outBufferPtr = outBufferData.data; - uint32_t outBufferSize = outBufferData.maxsize; - void* inBufferPtr = outputBufferContext->buffer->BufferPointer() ; - uint32_t inBufferSize = (uint32_t) outputBufferContext->buffer->Bytes(); - uint32_t frameSize = sizeof(float) * outputBufferContext->buffer->Channels(); + uint32_t outBufferRemaining = outBufferData.maxsize; + int channelCount = 2; - std::cerr << "[PipeWire] onProcess " << inBufferSize << " vs " << outBufferSize << "\n"; + size_t D_PROCESSED = 0; + size_t D_RELEASED = 0; - /* TODO: assumes dest size >= src size. need to do additional book keeping in - BufferContext to store offset if target isn't large enough, then push the - struct back */ + while (outBufferRemaining > 0) { + BufferContext* inputBufferContext = nullptr; + uint32_t inBufferSize = 0; + uint32_t inBufferRemaining = 0; + void* inBufferPtr; + uint32_t bytesToCopy = 0; - if (inBufferSize > outBufferSize) { - std::cerr << "[PipeWire] onProcess output buffer too small\n"; - exit(0); + std::cerr << "[PipeWire] " << outBufferRemaining << " bytes still need to be filled...\n"; + + { + std::unique_lock lock(self->mutex); + while (self->buffers.empty()) { + self->bufferCondition.wait(lock); + } + + if (self->state != State::Playing) { + std::cerr << "[PipeWire] not playing, so buffers will not be filled.\n"; + return; + } + + inputBufferContext = self->buffers.front(); + + channelCount = inputBufferContext->buffer->Channels(); + inBufferSize = (uint32_t) inputBufferContext->buffer->Bytes(); + inBufferRemaining = inputBufferContext->remaining; + inBufferPtr = inputBufferContext->buffer->BufferPointer(); + + if (outBufferRemaining >= inBufferRemaining) { + self->buffers.pop_front(); + } + + bytesToCopy = std::min(outBufferRemaining, inBufferRemaining); + + std::cerr << "[PipeWire] popped buffer #" << D_PROCESSED++ << ". Will fill with " << bytesToCopy << " bytes\n"; + } + + inBufferPtr += (inBufferSize - inBufferRemaining); + + memcpy(outBufferPtr, inBufferPtr, bytesToCopy); + inputBufferContext->remaining -= bytesToCopy; + outBufferRemaining -= bytesToCopy; + outBufferPtr += bytesToCopy; + + if (inputBufferContext->remaining == 0) { + std::cerr << "[PipeWire] released buffer #" << D_RELEASED++ << "\n"; + inputBufferContext->Release(); + } } - memcpy(outBufferPtr, inBufferPtr, inBufferSize); + std::cerr << "[PipeWire] adding PW buffer to queue\n"; + + uint32_t frameSize = sizeof(float) * channelCount; outBufferData.chunk->offset = 0; outBufferData.chunk->stride = frameSize; - outBufferData.chunk->size = inBufferSize; - pwBuffer->size = inBufferSize / frameSize; + outBufferData.chunk->size = outBufferData.maxsize; + pwBuffer->size = outBufferData.maxsize; - outputBufferContext->provider->OnBufferProcessed(outputBufferContext->buffer); + pw_stream_queue_buffer(self->pwStream, pwBuffer); - delete outputBufferContext; - - pw_stream_queue_buffer(output->pwStream, pwBuffer); + // exit(0); } PipeWireOut::PipeWireOut() { @@ -274,6 +301,7 @@ OutputState PipeWireOut::Play(IBuffer *buffer, IBufferProvider *provider) { return OutputState::BufferFull; } this->buffers.push_back(new BufferContext(buffer, provider)); + this->bufferCondition.notify_all(); } return OutputState::BufferWritten; diff --git a/src/plugins/pipewireout/PipeWireOut.h b/src/plugins/pipewireout/PipeWireOut.h index 41b450dae..d1c9c84b2 100644 --- a/src/plugins/pipewireout/PipeWireOut.h +++ b/src/plugins/pipewireout/PipeWireOut.h @@ -39,6 +39,7 @@ #include #include #include +#include #include using namespace musik::core::sdk; @@ -79,13 +80,18 @@ class PipeWireOut : public IOutput { struct BufferContext { BufferContext() { - this->buffer = nullptr; this->provider = nullptr; } BufferContext(IBuffer* buffer, IBufferProvider* provider) { this->buffer = buffer; this->provider = provider; + this->remaining = (uint32_t) buffer->Bytes(); } - IBuffer* buffer; - IBufferProvider* provider; + void Release() { + this->provider->OnBufferProcessed(this->buffer); + delete this; + } + IBuffer* buffer{nullptr}; + IBufferProvider* provider{nullptr}; + uint32_t remaining{0}; }; enum class State { @@ -94,10 +100,11 @@ class PipeWireOut : public IOutput { std::deque buffers; std::recursive_mutex mutex; - std::atomic initialized { false }; - std::atomic state { State::Stopped }; - double volume { 1.0 }; + std::atomic initialized{false}; + std::atomic state{State::Stopped}; + double volume{1.0}; pw_stream_events pwStreamEvents; - pw_thread_loop* pwThreadLoop { nullptr }; - pw_stream* pwStream { nullptr }; + pw_thread_loop* pwThreadLoop {nullptr}; + pw_stream* pwStream {nullptr}; + std::condition_variable_any bufferCondition; }; From aa7efd3d5bd1202c8b5754843ad77482acfdcc7e Mon Sep 17 00:00:00 2001 From: casey langen Date: Sun, 14 Mar 2021 22:24:48 -0700 Subject: [PATCH 05/24] Fix pointer arithmetic --- src/plugins/pipewireout/PipeWireOut.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp index bdd80033d..4437e02a4 100644 --- a/src/plugins/pipewireout/PipeWireOut.cpp +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -71,7 +71,7 @@ void PipeWireOut::OnStreamProcess(void* data) { struct spa_buffer* spaBuffer = pwBuffer->buffer; auto& outBufferData = spaBuffer->datas[0]; - void* outBufferPtr = outBufferData.data; + char* outBufferPtr = (char*) outBufferData.data; uint32_t outBufferRemaining = outBufferData.maxsize; int channelCount = 2; @@ -82,7 +82,7 @@ void PipeWireOut::OnStreamProcess(void* data) { BufferContext* inputBufferContext = nullptr; uint32_t inBufferSize = 0; uint32_t inBufferRemaining = 0; - void* inBufferPtr; + char* inBufferPtr; uint32_t bytesToCopy = 0; std::cerr << "[PipeWire] " << outBufferRemaining << " bytes still need to be filled...\n"; @@ -103,7 +103,7 @@ void PipeWireOut::OnStreamProcess(void* data) { channelCount = inputBufferContext->buffer->Channels(); inBufferSize = (uint32_t) inputBufferContext->buffer->Bytes(); inBufferRemaining = inputBufferContext->remaining; - inBufferPtr = inputBufferContext->buffer->BufferPointer(); + inBufferPtr = (char*) inputBufferContext->buffer->BufferPointer(); if (outBufferRemaining >= inBufferRemaining) { self->buffers.pop_front(); @@ -133,7 +133,7 @@ void PipeWireOut::OnStreamProcess(void* data) { outBufferData.chunk->offset = 0; outBufferData.chunk->stride = frameSize; outBufferData.chunk->size = outBufferData.maxsize; - pwBuffer->size = outBufferData.maxsize; + pwBuffer->size = outBufferData.maxsize / frameSize; pw_stream_queue_buffer(self->pwStream, pwBuffer); From b561bf8fb67f3297f548163a27592503ae508fcd Mon Sep 17 00:00:00 2001 From: casey langen Date: Mon, 15 Mar 2021 21:50:03 -0700 Subject: [PATCH 06/24] Minor updates and more debugging cruft. Need to test on a machine with a working PipeWire installation. --- src/plugins/pipewireout/PipeWireOut.cpp | 87 ++++++++++++++----------- src/plugins/pipewireout/PipeWireOut.h | 18 ++++- 2 files changed, 65 insertions(+), 40 deletions(-) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp index 4437e02a4..4bf55839c 100644 --- a/src/plugins/pipewireout/PipeWireOut.cpp +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -42,6 +42,8 @@ #include #include +constexpr size_t SAMPLES_PER_BUFFER = 2048; +constexpr size_t SAMPLE_SIZE_BYTES = sizeof(float); constexpr size_t MAX_BUFFERS = 16; static IPreferences* prefs = nullptr; @@ -75,17 +77,15 @@ void PipeWireOut::OnStreamProcess(void* data) { uint32_t outBufferRemaining = outBufferData.maxsize; int channelCount = 2; - size_t D_PROCESSED = 0; - size_t D_RELEASED = 0; + static size_t D_PROCESSED = 0; while (outBufferRemaining > 0) { - BufferContext* inputBufferContext = nullptr; + BufferContext* inContext = nullptr; uint32_t inBufferSize = 0; uint32_t inBufferRemaining = 0; - char* inBufferPtr; uint32_t bytesToCopy = 0; - std::cerr << "[PipeWire] " << outBufferRemaining << " bytes still need to be filled...\n"; + //std::cerr << "[PipeWire] " << outBufferRemaining << " bytes still need to be filled...\n"; { std::unique_lock lock(self->mutex); @@ -94,50 +94,54 @@ void PipeWireOut::OnStreamProcess(void* data) { } if (self->state != State::Playing) { - std::cerr << "[PipeWire] not playing, so buffers will not be filled.\n"; + //std::cerr << "[PipeWire] not playing, so buffers will not be filled.\n"; return; } - inputBufferContext = self->buffers.front(); + inContext = self->buffers.front(); - channelCount = inputBufferContext->buffer->Channels(); - inBufferSize = (uint32_t) inputBufferContext->buffer->Bytes(); - inBufferRemaining = inputBufferContext->remaining; - inBufferPtr = (char*) inputBufferContext->buffer->BufferPointer(); + channelCount = inContext->buffer->Channels(); + inBufferSize = (uint32_t) inContext->buffer->Bytes(); + inBufferRemaining = inContext->remaining; if (outBufferRemaining >= inBufferRemaining) { self->buffers.pop_front(); } bytesToCopy = std::min(outBufferRemaining, inBufferRemaining); - - std::cerr << "[PipeWire] popped buffer #" << D_PROCESSED++ << ". Will fill with " << bytesToCopy << " bytes\n"; } - inBufferPtr += (inBufferSize - inBufferRemaining); - - memcpy(outBufferPtr, inBufferPtr, bytesToCopy); - inputBufferContext->remaining -= bytesToCopy; + //std::cerr << "[PipeWire] popped buffer #" << D_PROCESSED++ << ". Will fill with " << bytesToCopy << " bytes.\n"; + memcpy(outBufferPtr, inContext->readPtr, bytesToCopy); + //memset(outBufferPtr, 345345345, bytesToCopy); + inContext->Advance(bytesToCopy); /* will auto release if empty */ outBufferRemaining -= bytesToCopy; outBufferPtr += bytesToCopy; - - if (inputBufferContext->remaining == 0) { - std::cerr << "[PipeWire] released buffer #" << D_RELEASED++ << "\n"; - inputBufferContext->Release(); - } } - std::cerr << "[PipeWire] adding PW buffer to queue\n"; + //std::cerr << "[PipeWire] adding PW buffer to queue\n"; - uint32_t frameSize = sizeof(float) * channelCount; + // std::cerr << "[PipeWire] input\n" << + // " pwBuffer->size: " << pwBuffer->size << "\n" << + // " data.maxsize: " << outBufferData.maxsize << "\n" << + // " data.chunk->offset: " << outBufferData.chunk->offset << "\n" << + // " data.chunk->stride: " << outBufferData.chunk->stride << "\n" << + // " data.chunk->size: " << outBufferData.chunk->size << "\n"; + + uint32_t stride = SAMPLE_SIZE_BYTES * channelCount; outBufferData.chunk->offset = 0; - outBufferData.chunk->stride = frameSize; + outBufferData.chunk->stride = stride; outBufferData.chunk->size = outBufferData.maxsize; - pwBuffer->size = outBufferData.maxsize / frameSize; + pwBuffer->size = outBufferData.maxsize / stride; + + // std::cerr << "[PipeWire] output\n" << + // " pwBuffer->size: " << pwBuffer->size << "\n" << + // " data.maxsize: " << outBufferData.maxsize << "\n" << + // " data.chunk->offset: " << outBufferData.chunk->offset << "\n" << + // " data.chunk->stride: " << outBufferData.chunk->stride << "\n" << + // " data.chunk->size: " << outBufferData.chunk->size << "\n"; pw_stream_queue_buffer(self->pwStream, pwBuffer); - - // exit(0); } PipeWireOut::PipeWireOut() { @@ -164,6 +168,7 @@ void PipeWireOut::Resume() { } void PipeWireOut::SetVolume(double volume) { + //pw_stream_set_control(stream->stream, SPA_PROP_channelVolumes, stream->volume.channels, stream->volume.values, 0); this->volume = volume; } @@ -235,33 +240,41 @@ bool PipeWireOut::StartPipeWire(IBuffer* buffer) { this); if (this->pwStream) { - uint8_t intBuffer[1024]; - - spa_pod_builder builder = - SPA_POD_BUILDER_INIT(intBuffer, sizeof(intBuffer)); - + uint8_t builderBuffer[4096]; + spa_pod_builder builder = SPA_POD_BUILDER_INIT(builderBuffer, sizeof(builderBuffer)); const spa_pod *params[1]; spa_audio_info_raw audioInfo; spa_zero(audioInfo); - audioInfo.flags = 0; audioInfo.format = SPA_AUDIO_FORMAT_F32; audioInfo.channels = (uint32_t) buffer->Channels(); audioInfo.rate = (uint32_t) buffer->SampleRate(); + audioInfo.position[0] = SPA_AUDIO_CHANNEL_FL; + audioInfo.position[1] = SPA_AUDIO_CHANNEL_FR; - params[0] = spa_format_audio_raw_build( - &builder, SPA_PARAM_EnumFormat, &audioInfo); + params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat, &audioInfo); if (!params[0]) { std::cerr << "[PipeWire] failed to create audio format\n"; goto cleanup; } + // params[1] = (spa_pod*) spa_pod_builder_add_object( + // &builder, + // SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + // SPA_PARAM_BUFFERS_buffers, SPA_POD_Int(MAX_BUFFERS), + // SPA_PARAM_BUFFERS_size, SPA_POD_Int(SAMPLES_PER_BUFFER * SAMPLE_SIZE_BYTES * buffer->Channels()), + // SPA_PARAM_BUFFERS_stride, SPA_POD_Int(SAMPLE_SIZE_BYTES * audioInfo.channels)); + + pw_stream_flags streamFlags = (pw_stream_flags)( + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS); + result = pw_stream_connect( this->pwStream, PW_DIRECTION_OUTPUT, PW_ID_ANY, - (pw_stream_flags)(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), + streamFlags, params, 1); diff --git a/src/plugins/pipewireout/PipeWireOut.h b/src/plugins/pipewireout/PipeWireOut.h index d1c9c84b2..db3b14ff5 100644 --- a/src/plugins/pipewireout/PipeWireOut.h +++ b/src/plugins/pipewireout/PipeWireOut.h @@ -41,6 +41,7 @@ #include #include #include +#include using namespace musik::core::sdk; @@ -83,15 +84,26 @@ class PipeWireOut : public IOutput { } BufferContext(IBuffer* buffer, IBufferProvider* provider) { this->buffer = buffer; this->provider = provider; + this->readPtr = (char*) buffer->BufferPointer(); this->remaining = (uint32_t) buffer->Bytes(); } - void Release() { - this->provider->OnBufferProcessed(this->buffer); - delete this; + void Advance(int count) { + static int consumed = 0; + consumed += count; + + bool release = count >= remaining; + this->remaining -= count; + this->readPtr += count; + if (release) { + // std::cerr << "[PipeWire] released BufferContext. Total consumed= " << consumed / sizeof(float) / buffer->SampleRate() / buffer->Channels() << "sec\n"; + this->provider->OnBufferProcessed(this->buffer); + delete this; + } } IBuffer* buffer{nullptr}; IBufferProvider* provider{nullptr}; uint32_t remaining{0}; + char* readPtr; }; enum class State { From dcb4e3892162fe18fe9171c4cb1f7c0a51fe62e3 Mon Sep 17 00:00:00 2001 From: casey langen Date: Mon, 15 Mar 2021 22:09:25 -0700 Subject: [PATCH 07/24] We have audio! Cleaned up some debug logging. --- src/plugins/pipewireout/PipeWireOut.cpp | 54 +++++++++++-------------- src/plugins/pipewireout/PipeWireOut.h | 5 --- 2 files changed, 23 insertions(+), 36 deletions(-) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp index 4bf55839c..c41a428c8 100644 --- a/src/plugins/pipewireout/PipeWireOut.cpp +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -89,7 +89,7 @@ void PipeWireOut::OnStreamProcess(void* data) { { std::unique_lock lock(self->mutex); - while (self->buffers.empty()) { + while (self->buffers.empty() && self->state == State::Playing) { self->bufferCondition.wait(lock); } @@ -113,33 +113,14 @@ void PipeWireOut::OnStreamProcess(void* data) { //std::cerr << "[PipeWire] popped buffer #" << D_PROCESSED++ << ". Will fill with " << bytesToCopy << " bytes.\n"; memcpy(outBufferPtr, inContext->readPtr, bytesToCopy); - //memset(outBufferPtr, 345345345, bytesToCopy); inContext->Advance(bytesToCopy); /* will auto release if empty */ outBufferRemaining -= bytesToCopy; outBufferPtr += bytesToCopy; } - //std::cerr << "[PipeWire] adding PW buffer to queue\n"; - - // std::cerr << "[PipeWire] input\n" << - // " pwBuffer->size: " << pwBuffer->size << "\n" << - // " data.maxsize: " << outBufferData.maxsize << "\n" << - // " data.chunk->offset: " << outBufferData.chunk->offset << "\n" << - // " data.chunk->stride: " << outBufferData.chunk->stride << "\n" << - // " data.chunk->size: " << outBufferData.chunk->size << "\n"; - - uint32_t stride = SAMPLE_SIZE_BYTES * channelCount; outBufferData.chunk->offset = 0; - outBufferData.chunk->stride = stride; + outBufferData.chunk->stride = SAMPLE_SIZE_BYTES * channelCount; outBufferData.chunk->size = outBufferData.maxsize; - pwBuffer->size = outBufferData.maxsize / stride; - - // std::cerr << "[PipeWire] output\n" << - // " pwBuffer->size: " << pwBuffer->size << "\n" << - // " data.maxsize: " << outBufferData.maxsize << "\n" << - // " data.chunk->offset: " << outBufferData.chunk->offset << "\n" << - // " data.chunk->stride: " << outBufferData.chunk->stride << "\n" << - // " data.chunk->size: " << outBufferData.chunk->size << "\n"; pw_stream_queue_buffer(self->pwStream, pwBuffer); } @@ -153,6 +134,7 @@ PipeWireOut::PipeWireOut() { } PipeWireOut::~PipeWireOut() { + this->StopPipeWire(); } void PipeWireOut::Release() { @@ -161,10 +143,18 @@ void PipeWireOut::Release() { void PipeWireOut::Pause() { this->state = State::Paused; + { + std::unique_lock lock(this->mutex); + this->bufferCondition.notify_all(); + } } void PipeWireOut::Resume() { this->state = State::Playing; + { + std::unique_lock lock(this->mutex); + this->bufferCondition.notify_all(); + } } void PipeWireOut::SetVolume(double volume) { @@ -200,6 +190,10 @@ void PipeWireOut::StopPipeWire() { if (this->pwThreadLoop) { pw_thread_loop_stop(this->pwThreadLoop); + + this->state = State::Stopped; + this->bufferCondition.notify_all(); + pw_thread_loop_destroy(this->pwThreadLoop); this->pwThreadLoop = nullptr; } @@ -242,15 +236,13 @@ bool PipeWireOut::StartPipeWire(IBuffer* buffer) { if (this->pwStream) { uint8_t builderBuffer[4096]; spa_pod_builder builder = SPA_POD_BUILDER_INIT(builderBuffer, sizeof(builderBuffer)); - const spa_pod *params[1]; + const spa_pod *params[2]; spa_audio_info_raw audioInfo; spa_zero(audioInfo); audioInfo.format = SPA_AUDIO_FORMAT_F32; audioInfo.channels = (uint32_t) buffer->Channels(); audioInfo.rate = (uint32_t) buffer->SampleRate(); - audioInfo.position[0] = SPA_AUDIO_CHANNEL_FL; - audioInfo.position[1] = SPA_AUDIO_CHANNEL_FR; params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat, &audioInfo); @@ -259,12 +251,12 @@ bool PipeWireOut::StartPipeWire(IBuffer* buffer) { goto cleanup; } - // params[1] = (spa_pod*) spa_pod_builder_add_object( - // &builder, - // SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, - // SPA_PARAM_BUFFERS_buffers, SPA_POD_Int(MAX_BUFFERS), - // SPA_PARAM_BUFFERS_size, SPA_POD_Int(SAMPLES_PER_BUFFER * SAMPLE_SIZE_BYTES * buffer->Channels()), - // SPA_PARAM_BUFFERS_stride, SPA_POD_Int(SAMPLE_SIZE_BYTES * audioInfo.channels)); + params[1] = (spa_pod*) spa_pod_builder_add_object( + &builder, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_buffers, SPA_POD_Int(MAX_BUFFERS), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(SAMPLES_PER_BUFFER * SAMPLE_SIZE_BYTES * buffer->Channels()), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(SAMPLE_SIZE_BYTES * audioInfo.channels)); pw_stream_flags streamFlags = (pw_stream_flags)( PW_STREAM_FLAG_AUTOCONNECT | @@ -276,7 +268,7 @@ bool PipeWireOut::StartPipeWire(IBuffer* buffer) { PW_ID_ANY, streamFlags, params, - 1); + 2); if (result == 0) { pw_thread_loop_unlock(this->pwThreadLoop); diff --git a/src/plugins/pipewireout/PipeWireOut.h b/src/plugins/pipewireout/PipeWireOut.h index db3b14ff5..d439eb042 100644 --- a/src/plugins/pipewireout/PipeWireOut.h +++ b/src/plugins/pipewireout/PipeWireOut.h @@ -41,7 +41,6 @@ #include #include #include -#include using namespace musik::core::sdk; @@ -88,14 +87,10 @@ class PipeWireOut : public IOutput { this->remaining = (uint32_t) buffer->Bytes(); } void Advance(int count) { - static int consumed = 0; - consumed += count; - bool release = count >= remaining; this->remaining -= count; this->readPtr += count; if (release) { - // std::cerr << "[PipeWire] released BufferContext. Total consumed= " << consumed / sizeof(float) / buffer->SampleRate() / buffer->Channels() << "sec\n"; this->provider->OnBufferProcessed(this->buffer); delete this; } From a602b0efaf1bb6206e8dc8c22d7bd486f52e5e88 Mon Sep 17 00:00:00 2001 From: casey langen Date: Mon, 15 Mar 2021 22:28:31 -0700 Subject: [PATCH 08/24] Ensure PipeWireOut shuts down cleanly --- src/plugins/pipewireout/PipeWireOut.cpp | 32 ++++++++++++++++--------- src/plugins/pipewireout/PipeWireOut.h | 2 +- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp index c41a428c8..71dcc2d3d 100644 --- a/src/plugins/pipewireout/PipeWireOut.cpp +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -89,12 +89,16 @@ void PipeWireOut::OnStreamProcess(void* data) { { std::unique_lock lock(self->mutex); - while (self->buffers.empty() && self->state == State::Playing) { + while (self->buffers.empty() && self->state != State::Shutdown) { + std::cerr << "[PipeWire] waiting for data...\n"; self->bufferCondition.wait(lock); + if (self->state == State::Shutdown) { + break; + } } - if (self->state != State::Playing) { - //std::cerr << "[PipeWire] not playing, so buffers will not be filled.\n"; + if (self->state == State::Shutdown) { + std::cerr << "[PipeWire] shutdown detected, so buffers will not be filled.\n"; return; } @@ -186,24 +190,30 @@ IDevice* PipeWireOut::GetDefaultDevice() { } void PipeWireOut::StopPipeWire() { - std::unique_lock lock(this->mutex); + std::cerr << "[PipeWire] shutdown started...\n"; + + { + std::unique_lock lock(this->mutex); + this->state = State::Shutdown; + this->bufferCondition.notify_all(); + } + if (this->pwThreadLoop) { pw_thread_loop_stop(this->pwThreadLoop); - this->state = State::Stopped; - this->bufferCondition.notify_all(); + if (this->pwStream) { + pw_stream_destroy(this->pwStream); + this->pwStream = nullptr; + } pw_thread_loop_destroy(this->pwThreadLoop); this->pwThreadLoop = nullptr; } - if (this->pwStream) { - pw_stream_destroy(this->pwStream); - this->pwStream = nullptr; - } - this->initialized = false; + + std::cerr << "[PipeWire] shutdown complete.\n"; } bool PipeWireOut::StartPipeWire(IBuffer* buffer) { diff --git a/src/plugins/pipewireout/PipeWireOut.h b/src/plugins/pipewireout/PipeWireOut.h index d439eb042..b098b81c5 100644 --- a/src/plugins/pipewireout/PipeWireOut.h +++ b/src/plugins/pipewireout/PipeWireOut.h @@ -102,7 +102,7 @@ class PipeWireOut : public IOutput { }; enum class State { - Stopped, Paused, Playing + Stopped, Paused, Playing, Shutdown }; std::deque buffers; From 95564ef5cdd517bd22756d3d93977dc019b12957 Mon Sep 17 00:00:00 2001 From: casey langen Date: Mon, 15 Mar 2021 22:32:01 -0700 Subject: [PATCH 09/24] Fix pause/resume in PipeWireOut --- src/plugins/pipewireout/PipeWireOut.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp index 71dcc2d3d..685ca9757 100644 --- a/src/plugins/pipewireout/PipeWireOut.cpp +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -151,14 +151,31 @@ void PipeWireOut::Pause() { std::unique_lock lock(this->mutex); this->bufferCondition.notify_all(); } + + { + if (this->pwThreadLoop && this->pwStream) { + pw_thread_loop_lock(this->pwThreadLoop); + pw_stream_set_active(this->pwStream, false); + pw_thread_loop_unlock(this->pwThreadLoop); + } + } } void PipeWireOut::Resume() { this->state = State::Playing; + { std::unique_lock lock(this->mutex); this->bufferCondition.notify_all(); } + + { + if (this->pwThreadLoop && this->pwStream) { + pw_thread_loop_lock(this->pwThreadLoop); + pw_stream_set_active(this->pwStream, true); + pw_thread_loop_unlock(this->pwThreadLoop); + } + } } void PipeWireOut::SetVolume(double volume) { From 4e2a46267cca2e2c5c6fc2bc8049d406edb1bcee Mon Sep 17 00:00:00 2001 From: casey langen Date: Mon, 15 Mar 2021 22:33:40 -0700 Subject: [PATCH 10/24] TODOify --- src/plugins/pipewireout/PipeWireOut.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp index 685ca9757..92faeef88 100644 --- a/src/plugins/pipewireout/PipeWireOut.cpp +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -180,6 +180,7 @@ void PipeWireOut::Resume() { void PipeWireOut::SetVolume(double volume) { //pw_stream_set_control(stream->stream, SPA_PROP_channelVolumes, stream->volume.channels, stream->volume.values, 0); + /* CAL TODO */ this->volume = volume; } @@ -192,17 +193,21 @@ void PipeWireOut::Stop() { } void PipeWireOut::Drain() { + /* CAL TODO */ } IDeviceList* PipeWireOut::GetDeviceList() { + /* CAL TODO */ return nullptr; } bool PipeWireOut::SetDefaultDevice(const char* deviceId) { + /* CAL TODO */ return false; } IDevice* PipeWireOut::GetDefaultDevice() { + /* CAL TODO */ return nullptr; } @@ -215,7 +220,6 @@ void PipeWireOut::StopPipeWire() { this->bufferCondition.notify_all(); } - if (this->pwThreadLoop) { pw_thread_loop_stop(this->pwThreadLoop); @@ -323,6 +327,8 @@ OutputState PipeWireOut::Play(IBuffer *buffer, IBufferProvider *provider) { } } + /* CAL TODO: re-init stream if buffer format changes */ + if (this->state != State::Playing) { return OutputState::InvalidState; } @@ -340,5 +346,6 @@ OutputState PipeWireOut::Play(IBuffer *buffer, IBufferProvider *provider) { } double PipeWireOut::Latency() { + /* CAL TODO */ return 0.0; } From 75dac589d0c0ed8e6af61e13c4176803e5517f12 Mon Sep 17 00:00:00 2001 From: casey langen Date: Tue, 16 Mar 2021 23:44:47 -0700 Subject: [PATCH 11/24] Stabilized Pipewire. Just a few TODOs left. --- src/plugins/pipewireout/PipeWireOut.cpp | 176 +++++++++++++----------- src/plugins/pipewireout/PipeWireOut.h | 66 +++++++-- 2 files changed, 157 insertions(+), 85 deletions(-) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp index 92faeef88..3fb607eee 100644 --- a/src/plugins/pipewireout/PipeWireOut.cpp +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -41,6 +41,7 @@ #include #include #include +#include constexpr size_t SAMPLES_PER_BUFFER = 2048; constexpr size_t SAMPLE_SIZE_BYTES = sizeof(float); @@ -61,80 +62,63 @@ void PipeWireOut::OnStreamStateChanged(void* data, enum pw_stream_state old, enu std::cerr << "[PipeWire] state changed from " << old << " to " << state << "\n"; } +void PipeWireOut::OnDrained(void* data) { + std::cerr << "[PipeWire] drained\n"; + PipeWireOut* self = static_cast(data); + self->drainCondition.notify_all(); +} + void PipeWireOut::OnStreamProcess(void* data) { PipeWireOut* self = static_cast(data); - struct pw_buffer* pwBuffer; + { + std::unique_lock lock(self->mutex); - if ((pwBuffer = pw_stream_dequeue_buffer(self->pwStream)) == nullptr) { - std::cerr << "[PipeWire] no more output buffers available to fill\n"; - return; - } - - struct spa_buffer* spaBuffer = pwBuffer->buffer; - auto& outBufferData = spaBuffer->datas[0]; - char* outBufferPtr = (char*) outBufferData.data; - uint32_t outBufferRemaining = outBufferData.maxsize; - int channelCount = 2; - - static size_t D_PROCESSED = 0; - - while (outBufferRemaining > 0) { - BufferContext* inContext = nullptr; - uint32_t inBufferSize = 0; - uint32_t inBufferRemaining = 0; - uint32_t bytesToCopy = 0; - - //std::cerr << "[PipeWire] " << outBufferRemaining << " bytes still need to be filled...\n"; - - { - std::unique_lock lock(self->mutex); - while (self->buffers.empty() && self->state != State::Shutdown) { - std::cerr << "[PipeWire] waiting for data...\n"; - self->bufferCondition.wait(lock); - if (self->state == State::Shutdown) { - break; - } - } - - if (self->state == State::Shutdown) { - std::cerr << "[PipeWire] shutdown detected, so buffers will not be filled.\n"; - return; - } - - inContext = self->buffers.front(); - - channelCount = inContext->buffer->Channels(); - inBufferSize = (uint32_t) inContext->buffer->Bytes(); - inBufferRemaining = inContext->remaining; - - if (outBufferRemaining >= inBufferRemaining) { - self->buffers.pop_front(); - } - - bytesToCopy = std::min(outBufferRemaining, inBufferRemaining); + if (self->state != State::Playing) { + return; } - //std::cerr << "[PipeWire] popped buffer #" << D_PROCESSED++ << ". Will fill with " << bytesToCopy << " bytes.\n"; - memcpy(outBufferPtr, inContext->readPtr, bytesToCopy); - inContext->Advance(bytesToCopy); /* will auto release if empty */ - outBufferRemaining -= bytesToCopy; - outBufferPtr += bytesToCopy; + OutBufferContext& outContext = self->outBufferContext; + + if (!outContext.Valid()) { + outContext.Initialize(pw_stream_dequeue_buffer(self->pwStream)); + if (!outContext.Valid()) { + // std::cerr << "[PipeWire] no more output buffers available to fill\n"; + return; + } + } + + uint32_t channelCount; + + while (outContext.remaining > 0 && !self->buffers.empty()) { + //std::cerr << "[PipeWire] " << outContext.remaining << " bytes still need to be filled...\n"; + + InBufferContext* inContext = self->buffers.front(); + channelCount = (uint32_t) inContext->buffer->Channels(); + uint32_t inBufferSize = (uint32_t) inContext->buffer->Bytes(); + uint32_t bytesToCopy = std::min(outContext.remaining, inContext->remaining); + + if (outContext.remaining >= inContext->remaining) { + self->buffers.pop_front(); + self->bufferCondition.notify_all(); + } + + memcpy(outContext.writePtr, inContext->readPtr, bytesToCopy); + inContext->Advance(bytesToCopy); /* will `delete this` if emptied */ + outContext.Advance(bytesToCopy); + } + + if (outContext.remaining == 0) { + outContext.Finalize(self->pwStream, SAMPLE_SIZE_BYTES * channelCount); + } } - - outBufferData.chunk->offset = 0; - outBufferData.chunk->stride = SAMPLE_SIZE_BYTES * channelCount; - outBufferData.chunk->size = outBufferData.maxsize; - - pw_stream_queue_buffer(self->pwStream, pwBuffer); } PipeWireOut::PipeWireOut() { - this->pwStreamEvents = { - PW_VERSION_STREAM_EVENTS, - }; + this->pwStreamEvents = { PW_VERSION_STREAM_EVENTS }; this->pwStreamEvents.state_changed = PipeWireOut::OnStreamStateChanged; - this->pwStreamEvents.process = OnStreamProcess; + this->pwStreamEvents.process = PipeWireOut::OnStreamProcess; + this->pwStreamEvents.drained = PipeWireOut::OnDrained; } PipeWireOut::~PipeWireOut() { @@ -146,12 +130,10 @@ void PipeWireOut::Release() { } void PipeWireOut::Pause() { - this->state = State::Paused; { std::unique_lock lock(this->mutex); - this->bufferCondition.notify_all(); + this->state = State::Paused; } - { if (this->pwThreadLoop && this->pwStream) { pw_thread_loop_lock(this->pwThreadLoop); @@ -162,13 +144,10 @@ void PipeWireOut::Pause() { } void PipeWireOut::Resume() { - this->state = State::Playing; - { std::unique_lock lock(this->mutex); - this->bufferCondition.notify_all(); + this->state = State::Playing; } - { if (this->pwThreadLoop && this->pwStream) { pw_thread_loop_lock(this->pwThreadLoop); @@ -189,11 +168,37 @@ double PipeWireOut::GetVolume() { } void PipeWireOut::Stop() { + std::unique_lock lock(this->mutex); + this->DiscardInputBuffers(); this->state = State::Stopped; + if (this->pwThreadLoop && this->pwStream) { + pw_thread_loop_lock(this->pwThreadLoop); + pw_stream_set_active(this->pwStream, false); + pw_stream_flush(this->pwStream, false); + pw_thread_loop_unlock(this->pwThreadLoop); + } +} + +void PipeWireOut::DiscardInputBuffers() { + std::unique_lock lock(this->mutex); + for (auto& buffer : this->buffers) { + buffer->Discard(); + } + this->buffers.clear(); + this->bufferCondition.notify_all(); } void PipeWireOut::Drain() { - /* CAL TODO */ + std::unique_lock lock(this->mutex); + while (this->buffers.size()) { + bufferCondition.wait(lock); + } + if (this->pwThreadLoop && this->pwStream) { + pw_thread_loop_lock(this->pwThreadLoop); + pw_stream_flush(this->pwStream, true); + pw_thread_loop_unlock(this->pwThreadLoop); + drainCondition.wait_for(lock, std::chrono::milliseconds(10000)); + } } IDeviceList* PipeWireOut::GetDeviceList() { @@ -214,16 +219,20 @@ IDevice* PipeWireOut::GetDefaultDevice() { void PipeWireOut::StopPipeWire() { std::cerr << "[PipeWire] shutdown started...\n"; + this->Stop(); + { std::unique_lock lock(this->mutex); this->state = State::Shutdown; - this->bufferCondition.notify_all(); } if (this->pwThreadLoop) { pw_thread_loop_stop(this->pwThreadLoop); if (this->pwStream) { + this->outBufferContext.Finalize( + this->pwStream, + SAMPLE_SIZE_BYTES * this->channelCount); pw_stream_destroy(this->pwStream); this->pwStream = nullptr; } @@ -233,6 +242,8 @@ void PipeWireOut::StopPipeWire() { } this->initialized = false; + this->channelCount = 0; + this->sampleRate = 0; std::cerr << "[PipeWire] shutdown complete.\n"; } @@ -269,11 +280,14 @@ bool PipeWireOut::StartPipeWire(IBuffer* buffer) { spa_pod_builder builder = SPA_POD_BUILDER_INIT(builderBuffer, sizeof(builderBuffer)); const spa_pod *params[2]; + this->channelCount = buffer->Channels(); + this->sampleRate = buffer->SampleRate(); + spa_audio_info_raw audioInfo; spa_zero(audioInfo); audioInfo.format = SPA_AUDIO_FORMAT_F32; - audioInfo.channels = (uint32_t) buffer->Channels(); - audioInfo.rate = (uint32_t) buffer->SampleRate(); + audioInfo.channels = (uint32_t) this->channelCount; + audioInfo.rate = (uint32_t) this->sampleRate; params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat, &audioInfo); @@ -327,7 +341,15 @@ OutputState PipeWireOut::Play(IBuffer *buffer, IBufferProvider *provider) { } } - /* CAL TODO: re-init stream if buffer format changes */ + if (this->channelCount != buffer->Channels() || this->sampleRate != buffer->SampleRate()) { + State lastState = this->state; + this->Drain(); + this->StopPipeWire(); + if (!this->StartPipeWire(buffer)) { + return OutputState::InvalidState; + } + this->state = lastState; + } if (this->state != State::Playing) { return OutputState::InvalidState; @@ -338,8 +360,8 @@ OutputState PipeWireOut::Play(IBuffer *buffer, IBufferProvider *provider) { if (this->buffers.size() >= MAX_BUFFERS) { return OutputState::BufferFull; } - this->buffers.push_back(new BufferContext(buffer, provider)); - this->bufferCondition.notify_all(); + this->buffers.push_back(new InBufferContext(buffer, provider)); + bufferCondition.notify_all(); } return OutputState::BufferWritten; diff --git a/src/plugins/pipewireout/PipeWireOut.h b/src/plugins/pipewireout/PipeWireOut.h index b098b81c5..25230a6a4 100644 --- a/src/plugins/pipewireout/PipeWireOut.h +++ b/src/plugins/pipewireout/PipeWireOut.h @@ -39,8 +39,8 @@ #include #include #include -#include #include +#include using namespace musik::core::sdk; @@ -69,6 +69,7 @@ class PipeWireOut : public IOutput { private: bool StartPipeWire(IBuffer* buffer); void StopPipeWire(); + void DiscardInputBuffers(); static void OnStreamStateChanged( void* userdata, @@ -78,10 +79,10 @@ class PipeWireOut : public IOutput { static void OnStreamProcess(void* userdata); - struct BufferContext { - BufferContext() { - } - BufferContext(IBuffer* buffer, IBufferProvider* provider) { + static void OnDrained(void* userdata); + + struct InBufferContext { + InBufferContext(IBuffer* buffer, IBufferProvider* provider) { this->buffer = buffer; this->provider = provider; this->readPtr = (char*) buffer->BufferPointer(); this->remaining = (uint32_t) buffer->Bytes(); @@ -95,23 +96,72 @@ class PipeWireOut : public IOutput { delete this; } } + void Discard() { + this->provider->OnBufferProcessed(this->buffer); + delete this; + } IBuffer* buffer{nullptr}; IBufferProvider* provider{nullptr}; uint32_t remaining{0}; char* readPtr; }; + struct OutBufferContext { + void Initialize(pw_buffer* buffer) { + this->buffer = buffer; + if (buffer) { + struct spa_buffer* spaBuffer = buffer->buffer; + this->writePtr = (char*) spaBuffer->datas[0].data; + this->remaining = spaBuffer->datas[0].maxsize; + this->total = this->remaining; + } + else { + this->Reset(); + } + } + void Reset() { + this->buffer = nullptr; + this->writePtr = nullptr; + this->remaining = 0; + this->total = 0; + } + void Advance(int count) { + this->remaining -= count; + this->writePtr += count; + } + void Finalize(pw_stream* stream, uint32_t stride) { + if (this->Valid()) { + spa_data& data = this->buffer->buffer->datas[0]; + data.chunk->offset = 0; + data.chunk->stride = stride; + data.chunk->size = this->total - this->remaining; + pw_stream_queue_buffer(stream, this->buffer); + this->Reset(); + } + } + bool Valid() { + return this->buffer != nullptr; + } + pw_buffer* buffer{nullptr}; + uint32_t remaining{0}; + uint32_t total{0}; + char* writePtr{nullptr}; + }; + enum class State { Stopped, Paused, Playing, Shutdown }; - std::deque buffers; + std::deque buffers; std::recursive_mutex mutex; std::atomic initialized{false}; std::atomic state{State::Stopped}; + std::condition_variable_any bufferCondition, drainCondition; double volume{1.0}; pw_stream_events pwStreamEvents; pw_thread_loop* pwThreadLoop {nullptr}; - pw_stream* pwStream {nullptr}; - std::condition_variable_any bufferCondition; + pw_stream* pwStream{nullptr}; + OutBufferContext outBufferContext; + long channelCount{0}; + long sampleRate{0}; }; From ae800d788d85e1f494c090b10ea88831980b0934 Mon Sep 17 00:00:00 2001 From: casey langen Date: Wed, 17 Mar 2021 19:15:04 -0700 Subject: [PATCH 12/24] Implemented PipeWireOut::SetVolume --- src/plugins/pipewireout/PipeWireOut.cpp | 28 +++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp index 3fb607eee..041788d71 100644 --- a/src/plugins/pipewireout/PipeWireOut.cpp +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +50,8 @@ constexpr size_t MAX_BUFFERS = 16; static IPreferences* prefs = nullptr; +/* CAL TODO: use IDebug instead of stderr */ + extern "C" void SetPreferences(IPreferences* prefs) { ::prefs = prefs; } @@ -158,12 +161,27 @@ void PipeWireOut::Resume() { } void PipeWireOut::SetVolume(double volume) { - //pw_stream_set_control(stream->stream, SPA_PROP_channelVolumes, stream->volume.channels, stream->volume.values, 0); - /* CAL TODO */ - this->volume = volume; + std::unique_lock lock(this->mutex); + if (this->pwThreadLoop && this->pwStream) { + pw_thread_loop_lock(this->pwThreadLoop); + float* channelVolumes = new float[this->channelCount]; + for (long i = 0; i < this->channelCount; i++) { + channelVolumes[i] = volume; + } + pw_stream_set_control( + this->pwStream, + SPA_PROP_channelVolumes, + this->channelCount, + channelVolumes, + 0); + delete[] channelVolumes; + pw_thread_loop_unlock(this->pwThreadLoop); + } + this->volume = volume; } double PipeWireOut::GetVolume() { + std::unique_lock lock(this->mutex); return this->volume; } @@ -339,6 +357,7 @@ OutputState PipeWireOut::Play(IBuffer *buffer, IBufferProvider *provider) { if (!this->StartPipeWire(buffer)) { return OutputState::InvalidState; } + this->SetVolume(this->volume); } if (this->channelCount != buffer->Channels() || this->sampleRate != buffer->SampleRate()) { @@ -368,6 +387,7 @@ OutputState PipeWireOut::Play(IBuffer *buffer, IBufferProvider *provider) { } double PipeWireOut::Latency() { - /* CAL TODO */ + /* CAL TODO: i see how to set latency, but not a good way to query + PipeWire to see what it actually is */ return 0.0; } From 89bbdf6f5403d0a45d86a679ec4dd0a0f008dea1 Mon Sep 17 00:00:00 2001 From: casey langen Date: Wed, 17 Mar 2021 20:22:21 -0700 Subject: [PATCH 13/24] Let PipeWire determine the best buffer size automatically. We had selected buffers that were too small and causing gapless playback to not always work properly. --- src/plugins/pipewireout/PipeWireOut.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp index 041788d71..cb2f8f437 100644 --- a/src/plugins/pipewireout/PipeWireOut.cpp +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -296,7 +296,7 @@ bool PipeWireOut::StartPipeWire(IBuffer* buffer) { if (this->pwStream) { uint8_t builderBuffer[4096]; spa_pod_builder builder = SPA_POD_BUILDER_INIT(builderBuffer, sizeof(builderBuffer)); - const spa_pod *params[2]; + const spa_pod *params[1]; this->channelCount = buffer->Channels(); this->sampleRate = buffer->SampleRate(); @@ -314,12 +314,12 @@ bool PipeWireOut::StartPipeWire(IBuffer* buffer) { goto cleanup; } - params[1] = (spa_pod*) spa_pod_builder_add_object( - &builder, - SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, - SPA_PARAM_BUFFERS_buffers, SPA_POD_Int(MAX_BUFFERS), - SPA_PARAM_BUFFERS_size, SPA_POD_Int(SAMPLES_PER_BUFFER * SAMPLE_SIZE_BYTES * buffer->Channels()), - SPA_PARAM_BUFFERS_stride, SPA_POD_Int(SAMPLE_SIZE_BYTES * audioInfo.channels)); + // params[1] = (spa_pod*) spa_pod_builder_add_object( + // &builder, + // SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + // SPA_PARAM_BUFFERS_buffers, SPA_POD_Int(MAX_BUFFERS), + // SPA_PARAM_BUFFERS_size, SPA_POD_Int(SAMPLES_PER_BUFFER * SAMPLE_SIZE_BYTES * buffer->Channels()), + // SPA_PARAM_BUFFERS_stride, SPA_POD_Int(SAMPLE_SIZE_BYTES * audioInfo.channels)); pw_stream_flags streamFlags = (pw_stream_flags)( PW_STREAM_FLAG_AUTOCONNECT | @@ -331,7 +331,7 @@ bool PipeWireOut::StartPipeWire(IBuffer* buffer) { PW_ID_ANY, streamFlags, params, - 2); + 1); if (result == 0) { pw_thread_loop_unlock(this->pwThreadLoop); From 0a0a192bbf5125c44a5ea681fe243da9b14bd62f Mon Sep 17 00:00:00 2001 From: casey langen Date: Wed, 17 Mar 2021 23:56:39 -0700 Subject: [PATCH 14/24] Incremental changes to support device enumeration. TODO: (1) resolving device name, (2) actually changing to a non-default device. --- src/plugins/pipewireout/PipeWireOut.cpp | 101 ++++++++++++++++++++-- src/plugins/pipewireout/PipeWireOut.h | 109 ++++++++++++++++++++++++ 2 files changed, 203 insertions(+), 7 deletions(-) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp index cb2f8f437..2ee4f2865 100644 --- a/src/plugins/pipewireout/PipeWireOut.cpp +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -48,6 +48,8 @@ constexpr size_t SAMPLES_PER_BUFFER = 2048; constexpr size_t SAMPLE_SIZE_BYTES = sizeof(float); constexpr size_t MAX_BUFFERS = 16; +static std::atomic pipeWireInitialized(false); + static IPreferences* prefs = nullptr; /* CAL TODO: use IDebug instead of stderr */ @@ -65,6 +67,27 @@ void PipeWireOut::OnStreamStateChanged(void* data, enum pw_stream_state old, enu std::cerr << "[PipeWire] state changed from " << old << " to " << state << "\n"; } +void PipeWireOut::OnCoreDone(void* userdata, uint32_t id, int seq) { + auto context = static_cast(userdata); + if (seq == context->eventId) { + std::cerr << "[PipeWire] device refresh finished\n"; + pw_main_loop_quit(context->loop); + } +} + +void PipeWireOut::OnCoreError(void* userdata, uint32_t id, int seq, int res, const char *message) { + auto context = static_cast(userdata); + pw_main_loop_quit(context->loop); +} + +void PipeWireOut::OnRegistryGlobal(void *userdata, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props) { + auto context = static_cast(userdata); + if (std::string(type) == "PipeWire:Interface:Device") { + std::cerr << "[PipeWire] object with id changed from " << id << "\n"; + context->instance->deviceList.Add(std::to_string(id), "TODO FIXME"); + } +} + void PipeWireOut::OnDrained(void* data) { std::cerr << "[PipeWire] drained\n"; PipeWireOut* self = static_cast(data); @@ -177,7 +200,7 @@ void PipeWireOut::SetVolume(double volume) { delete[] channelVolumes; pw_thread_loop_unlock(this->pwThreadLoop); } - this->volume = volume; + this->volume = volume; } double PipeWireOut::GetVolume() { @@ -220,8 +243,9 @@ void PipeWireOut::Drain() { } IDeviceList* PipeWireOut::GetDeviceList() { - /* CAL TODO */ - return nullptr; + std::unique_lock lock(this->mutex); + this->RefreshDeviceList(); + return this->deviceList.Clone(); } bool PipeWireOut::SetDefaultDevice(const char* deviceId) { @@ -230,8 +254,8 @@ bool PipeWireOut::SetDefaultDevice(const char* deviceId) { } IDevice* PipeWireOut::GetDefaultDevice() { - /* CAL TODO */ - return nullptr; + std::unique_lock lock(this->mutex); + return this->deviceList.Default(); } void PipeWireOut::StopPipeWire() { @@ -269,8 +293,6 @@ void PipeWireOut::StopPipeWire() { bool PipeWireOut::StartPipeWire(IBuffer* buffer) { std::unique_lock lock(this->mutex); - pw_init(nullptr, nullptr); - this->pwThreadLoop = pw_thread_loop_new("musikcube", nullptr); if (this->pwThreadLoop) { int result; @@ -354,6 +376,12 @@ cleanup: OutputState PipeWireOut::Play(IBuffer *buffer, IBufferProvider *provider) { if (!this->initialized) { + std::unique_lock lock(this->mutex); + if (!pipeWireInitialized) { + pw_init(nullptr, nullptr); + pipeWireInitialized = true; + } + this->RefreshDeviceList(); if (!this->StartPipeWire(buffer)) { return OutputState::InvalidState; } @@ -391,3 +419,62 @@ double PipeWireOut::Latency() { PipeWire to see what it actually is */ return 0.0; } + +void PipeWireOut::RefreshDeviceList() { + /* https://github.com/PipeWire/pipewire/blob/master/src/tools/pw-dump.c */ + + if (!pipeWireInitialized) { + pw_init(nullptr, nullptr); + pipeWireInitialized = true; + } + + DeviceListContext deviceListContext(this); + + deviceListContext.loop = pw_main_loop_new(nullptr); + if (!deviceListContext.loop) { + std::cerr << "[PipeWire] RefreshDeviceList: could not create main loop.\n"; + return; + } + + auto loop = pw_main_loop_get_loop(deviceListContext.loop); + if (!loop) { + std::cerr << "[PipeWire] RefreshDeviceList: could not resolve loop from main_loop??\n"; + return; + } + + deviceListContext.context = pw_context_new(loop, nullptr, 0); + if (!deviceListContext.context) { + std::cerr << "[PipeWire] RefreshDeviceList: could not create context.\n"; + return; + } + + deviceListContext.core = pw_context_connect(deviceListContext.context, nullptr, 0); + if (deviceListContext.core == nullptr) { + std::cerr << "[PipeWire] RefreshDeviceList: could not connect to core.\n"; + return; + } + + pw_core_add_listener( + deviceListContext.core, + &deviceListContext.coreListener, + &deviceListContext.coreEvents, + &deviceListContext); + + deviceListContext.registry = pw_core_get_registry( + deviceListContext.core, PW_VERSION_REGISTRY, 0); + + if (deviceListContext.registry) { + pw_registry_add_listener( + deviceListContext.registry, + &deviceListContext.registryListener, + &deviceListContext.registryEvents, + &deviceListContext); + + deviceListContext.eventId = pw_core_sync( + deviceListContext.core, PW_ID_CORE, 0); + + this->deviceList.Reset(); + + pw_main_loop_run(deviceListContext.loop); + } +} diff --git a/src/plugins/pipewireout/PipeWireOut.h b/src/plugins/pipewireout/PipeWireOut.h index 25230a6a4..68c51370a 100644 --- a/src/plugins/pipewireout/PipeWireOut.h +++ b/src/plugins/pipewireout/PipeWireOut.h @@ -40,6 +40,7 @@ #include #include #include +#include #include using namespace musik::core::sdk; @@ -70,6 +71,28 @@ class PipeWireOut : public IOutput { bool StartPipeWire(IBuffer* buffer); void StopPipeWire(); void DiscardInputBuffers(); + void RefreshDeviceList(); + + static void OnCoreDone( + void* userdata, + uint32_t id, + int seq); + + static void OnCoreError( + void *userdata, + uint32_t id, + int seq, + int res, + const char *message + ); + + static void OnRegistryGlobal( + void *userdata, + uint32_t id, + uint32_t permissions, + const char *type, + uint32_t version, + const struct spa_dict *props); static void OnStreamStateChanged( void* userdata, @@ -148,6 +171,91 @@ class PipeWireOut : public IOutput { char* writePtr{nullptr}; }; + class Device: public musik::core::sdk::IDevice { + public: + Device(const std::string& id, const std::string& name) { + this->id = id; + this->name = name; + } + void Release() override { + delete this; + } + const char* Name() const override { + return name.c_str(); + } + const char* Id() const override { + return id.c_str(); + } + Device* Clone() { + return new Device(this->id, this->name); + } + private: + std::string id, name; + }; + + class DeviceList: public musik::core::sdk::IDeviceList { + public: + void Release() override { + delete this; + } + size_t Count() const override { + return devices.size(); + } + const Device* At(size_t index) const override { + return &devices.at(index); + } + void Add(const std::string& id, const std::string& name) { + devices.push_back(Device(id, name)); + } + Device* Default() { + return this->devices.empty() ? nullptr : this->devices.at(0).Clone(); + } + void Reset() { + this->devices.clear(); + } + DeviceList* Clone() { + auto result = new DeviceList(); + result->devices = this->devices; + return result; + } + private: + std::vector devices; + }; + + struct DeviceListContext { + DeviceListContext(PipeWireOut* instance) { + this->instance = instance; + this->coreEvents = { PW_VERSION_CORE_EVENTS }; + this->coreEvents.done = PipeWireOut::OnCoreDone; + this->coreEvents.error = PipeWireOut::OnCoreError; + spa_zero(this->coreListener); + this->registryEvents = { PW_VERSION_REGISTRY_EVENTS }; + this->registryEvents.global = PipeWireOut::OnRegistryGlobal; + spa_zero(this->registryListener); + } + ~DeviceListContext() { + if (this->registry) { + pw_proxy_destroy(reinterpret_cast(this->registry)); + } + if (this->context) { + pw_context_destroy(this->context); + } + if (this->loop) { + pw_main_loop_destroy(this->loop); + } + } + pw_main_loop* loop{nullptr}; + pw_context* context{nullptr}; + pw_core* core{nullptr}; + spa_hook coreListener; + pw_core_events coreEvents; + pw_registry* registry{nullptr}; + spa_hook registryListener; + pw_registry_events registryEvents; + int eventId{0}; + PipeWireOut* instance{nullptr}; + }; + enum class State { Stopped, Paused, Playing, Shutdown }; @@ -164,4 +272,5 @@ class PipeWireOut : public IOutput { OutBufferContext outBufferContext; long channelCount{0}; long sampleRate{0}; + DeviceList deviceList; }; From c8867a3b969a5c87e23fc16e8202d81a71446791 Mon Sep 17 00:00:00 2001 From: casey langen Date: Thu, 18 Mar 2021 00:41:41 -0700 Subject: [PATCH 15/24] PipeWire device selection and persistence by ID. TODO: still need to resolve the name. --- src/plugins/pipewireout/PipeWireOut.cpp | 18 ++++++++++++++---- src/plugins/pipewireout/PipeWireOut.h | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp index 2ee4f2865..7ee8db7de 100644 --- a/src/plugins/pipewireout/PipeWireOut.cpp +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -47,6 +47,7 @@ constexpr size_t SAMPLES_PER_BUFFER = 2048; constexpr size_t SAMPLE_SIZE_BYTES = sizeof(float); constexpr size_t MAX_BUFFERS = 16; +constexpr const char* PREF_DEVICE_ID = "device_id"; static std::atomic pipeWireInitialized(false); @@ -56,6 +57,8 @@ static IPreferences* prefs = nullptr; extern "C" void SetPreferences(IPreferences* prefs) { ::prefs = prefs; + prefs->GetString(PREF_DEVICE_ID, nullptr, 0, ""); + prefs->Save(); } extern "C" musik::core::sdk::ISchema* GetSchema() { @@ -63,6 +66,10 @@ extern "C" musik::core::sdk::ISchema* GetSchema() { return schema; } +static std::string getDeviceId() { + return getPreferenceString(prefs, PREF_DEVICE_ID, ""); +} + void PipeWireOut::OnStreamStateChanged(void* data, enum pw_stream_state old, enum pw_stream_state state, const char* error) { std::cerr << "[PipeWire] state changed from " << old << " to " << state << "\n"; } @@ -249,13 +256,16 @@ IDeviceList* PipeWireOut::GetDeviceList() { } bool PipeWireOut::SetDefaultDevice(const char* deviceId) { - /* CAL TODO */ - return false; + if (getDeviceId() != deviceId) { + setDefaultDevice(prefs, this, PREF_DEVICE_ID, deviceId); + } + return true; } IDevice* PipeWireOut::GetDefaultDevice() { std::unique_lock lock(this->mutex); - return this->deviceList.Default(); + this->RefreshDeviceList(); + return this->deviceList.ResolveDevice(getDeviceId()); } void PipeWireOut::StopPipeWire() { @@ -350,7 +360,7 @@ bool PipeWireOut::StartPipeWire(IBuffer* buffer) { result = pw_stream_connect( this->pwStream, PW_DIRECTION_OUTPUT, - PW_ID_ANY, + this->deviceList.ResolveId(getDeviceId()), streamFlags, params, 1); diff --git a/src/plugins/pipewireout/PipeWireOut.h b/src/plugins/pipewireout/PipeWireOut.h index 68c51370a..b59caefbe 100644 --- a/src/plugins/pipewireout/PipeWireOut.h +++ b/src/plugins/pipewireout/PipeWireOut.h @@ -213,6 +213,27 @@ class PipeWireOut : public IOutput { void Reset() { this->devices.clear(); } + uint32_t ResolveId(const std::string& id) { + for (auto device: this->devices) { + if (device.Id() == id) { + try { + return (uint32_t) std::stoi(id); + } + catch(...) { + /* return default below... */ + } + } + } + return PW_ID_ANY; + } + Device* ResolveDevice(const std::string& id) { + for (auto device: this->devices) { + if (device.Id() == id) { + return device.Clone(); + } + } + return nullptr; + } DeviceList* Clone() { auto result = new DeviceList(); result->devices = this->devices; From bd0db547ff4c674eda430358ef2b9cb1aa36a422 Mon Sep 17 00:00:00 2001 From: casey langen Date: Thu, 18 Mar 2021 11:38:41 -0700 Subject: [PATCH 16/24] Query device name. --- src/plugins/pipewireout/PipeWireOut.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp index 7ee8db7de..067859701 100644 --- a/src/plugins/pipewireout/PipeWireOut.cpp +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -90,8 +90,23 @@ void PipeWireOut::OnCoreError(void* userdata, uint32_t id, int seq, int res, con void PipeWireOut::OnRegistryGlobal(void *userdata, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props) { auto context = static_cast(userdata); if (std::string(type) == "PipeWire:Interface:Device") { + std::string formattedName = "unknown device"; + if (props) { + auto dict = pw_properties_new_dict(props); + auto name = pw_properties_get(dict, "device.nick"); + if (!name || !strlen(name)) name = pw_properties_get(dict, "alsa.card_name"); + if (!name || !strlen(name)) name = pw_properties_get(dict, "api.alsa.card.name"); + if (!name || !strlen(name)) name = "unknown device"; + auto addr = pw_properties_get(dict, "object.path"); + if (!addr || !strlen(addr)) addr = pw_properties_get(dict, "device.bus-path"); + formattedName = name; + if (addr && strlen(addr)) { + formattedName += " (" + std::string(addr) + ")"; + } + pw_properties_free(dict); + } std::cerr << "[PipeWire] object with id changed from " << id << "\n"; - context->instance->deviceList.Add(std::to_string(id), "TODO FIXME"); + context->instance->deviceList.Add(std::to_string(id), formattedName); } } From fa80e7f0414acf5b4d3f956ddf9f3d5237bff03d Mon Sep 17 00:00:00 2001 From: casey langen Date: Thu, 18 Mar 2021 11:57:32 -0700 Subject: [PATCH 17/24] Fix curses version check. --- src/musikcube/cursespp/Colors.cpp | 2 +- src/musikcube/cursespp/Window.cpp | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/musikcube/cursespp/Colors.cpp b/src/musikcube/cursespp/Colors.cpp index 11be45dc0..1b622a954 100755 --- a/src/musikcube/cursespp/Colors.cpp +++ b/src/musikcube/cursespp/Colors.cpp @@ -379,7 +379,7 @@ struct Theme { /* main */ init_pair(Color::ContentColorDefault, foregroundId, backgroundId); init_pair(Color::FrameDefault, foregroundId, backgroundId); - init_pair(Color::FrameFocused, focusedFrame.Id(mode, COLOR_RED),backgroundId); + init_pair(Color::FrameFocused, focusedFrame.Id(mode, COLOR_RED), backgroundId); init_pair(Color::FrameImportant, importantFrame.Id(mode, COLOR_BLUE), backgroundId); /* text */ diff --git a/src/musikcube/cursespp/Window.cpp b/src/musikcube/cursespp/Window.cpp index fd87d9e7b..c221fd26d 100755 --- a/src/musikcube/cursespp/Window.cpp +++ b/src/musikcube/cursespp/Window.cpp @@ -419,11 +419,10 @@ void Window::Redraw() { } if (this->frame) { -#ifdef __FreeBSD__ - /* somehow related to curses version, but i don't know how; - if we don't do this on FreeBSD we get missing background - colors for things like overlays. if we do this on other - platforms we get weird repainting artifacts. */ +#if defined(__FreeBSD__) || (NCURSES_VERSION_PATCH >= 20200301) + /* but depending on curses version we'll get redraw artifacts if we do or don't + repaint the background. the changelog mentions changes to wbkgd() and wbkgrnd() + at revision 20200301, but it's unclear if this is the actual root cause or not. */ this->RepaintBackground(); #endif this->OnRedraw(); From 672a5fdb45fc46c57650b0d80f5cccd4caab9134 Mon Sep 17 00:00:00 2001 From: casey langen Date: Thu, 18 Mar 2021 21:07:42 -0700 Subject: [PATCH 18/24] Fixed a bug where switching between crossfade and gapless transport could cause the app to stop responding. --- src/musikcore/audio/Crossfader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/musikcore/audio/Crossfader.cpp b/src/musikcore/audio/Crossfader.cpp index 46d707775..7c1e74d91 100644 --- a/src/musikcore/audio/Crossfader.cpp +++ b/src/musikcore/audio/Crossfader.cpp @@ -73,10 +73,10 @@ Crossfader::Crossfader(ITransport& transport) } Crossfader::~Crossfader() { - this->messageQueue.Unregister(this); this->quit = true; this->messageQueue.Post(Message::Create(this, MESSAGE_QUIT, 0, 0)); this->thread->join(); + this->messageQueue.Unregister(this); } void Crossfader::Fade( From 05bef7936653230e68ed83cb5893ceaf98d7913a Mon Sep 17 00:00:00 2001 From: casey langen Date: Thu, 18 Mar 2021 21:35:02 -0700 Subject: [PATCH 19/24] Fixed a bug where updating the transport wouldn't take effect until an app restart. --- src/musikcore/audio/PlaybackService.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/musikcore/audio/PlaybackService.cpp b/src/musikcore/audio/PlaybackService.cpp index 2b6fcb2dc..dc7971031 100755 --- a/src/musikcore/audio/PlaybackService.cpp +++ b/src/musikcore/audio/PlaybackService.cpp @@ -454,8 +454,8 @@ void PlaybackService::ProcessMessage(IMessage &message) { if (masterTransport) { const TransportType transportType = static_cast( playbackPrefs->GetInt( - keys::Transport.c_str()), - static_cast(TransportType::Gapless)); + keys::Transport, + static_cast(TransportType::Gapless))); if (masterTransport->GetType() != transportType) { masterTransport->SwitchTo(transportType); From 717b6066ec4bca9b729da9a4179077767dcee6a0 Mon Sep 17 00:00:00 2001 From: casey langen Date: Thu, 18 Mar 2021 22:19:35 -0700 Subject: [PATCH 20/24] Converted PipeWireOut to use IDebug --- src/plugins/pipewireout/PipeWireOut.cpp | 44 +++++++++++++++---------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/plugins/pipewireout/PipeWireOut.cpp b/src/plugins/pipewireout/PipeWireOut.cpp index 067859701..b4f20e89e 100644 --- a/src/plugins/pipewireout/PipeWireOut.cpp +++ b/src/plugins/pipewireout/PipeWireOut.cpp @@ -37,13 +37,15 @@ #include #include #include +#include +#include #include #include #include #include -#include #include +constexpr const char* TAG = "PipeWireOut"; constexpr size_t SAMPLES_PER_BUFFER = 2048; constexpr size_t SAMPLE_SIZE_BYTES = sizeof(float); constexpr size_t MAX_BUFFERS = 16; @@ -52,8 +54,11 @@ constexpr const char* PREF_DEVICE_ID = "device_id"; static std::atomic pipeWireInitialized(false); static IPreferences* prefs = nullptr; +static IDebug* debug = nullptr; -/* CAL TODO: use IDebug instead of stderr */ +extern "C" void SetDebug(IDebug* debug) { + ::debug = debug; +} extern "C" void SetPreferences(IPreferences* prefs) { ::prefs = prefs; @@ -71,13 +76,13 @@ static std::string getDeviceId() { } void PipeWireOut::OnStreamStateChanged(void* data, enum pw_stream_state old, enum pw_stream_state state, const char* error) { - std::cerr << "[PipeWire] state changed from " << old << " to " << state << "\n"; + ::debug->Info(TAG, str::format("state changed from %d to %d", old, state).c_str()); } void PipeWireOut::OnCoreDone(void* userdata, uint32_t id, int seq) { auto context = static_cast(userdata); if (seq == context->eventId) { - std::cerr << "[PipeWire] device refresh finished\n"; + ::debug->Info(TAG, "device refresh finished"); pw_main_loop_quit(context->loop); } } @@ -105,13 +110,13 @@ void PipeWireOut::OnRegistryGlobal(void *userdata, uint32_t id, uint32_t permiss } pw_properties_free(dict); } - std::cerr << "[PipeWire] object with id changed from " << id << "\n"; + ::debug->Info(TAG, str::format("detected PipeWire:Interface:Device with id=%d", id).c_str()); context->instance->deviceList.Add(std::to_string(id), formattedName); } } void PipeWireOut::OnDrained(void* data) { - std::cerr << "[PipeWire] drained\n"; + ::debug->Info(TAG, "drained"); PipeWireOut* self = static_cast(data); self->drainCondition.notify_all(); } @@ -284,7 +289,7 @@ IDevice* PipeWireOut::GetDefaultDevice() { } void PipeWireOut::StopPipeWire() { - std::cerr << "[PipeWire] shutdown started...\n"; + ::debug->Info(TAG, "shutdown started"); this->Stop(); @@ -312,7 +317,7 @@ void PipeWireOut::StopPipeWire() { this->channelCount = 0; this->sampleRate = 0; - std::cerr << "[PipeWire] shutdown complete.\n"; + ::debug->Info(TAG, "shutdown complete"); } bool PipeWireOut::StartPipeWire(IBuffer* buffer) { @@ -323,7 +328,7 @@ bool PipeWireOut::StartPipeWire(IBuffer* buffer) { int result; if ((result = pw_thread_loop_start(this->pwThreadLoop)) != 0) { - std::cerr << "[PipeWire] error starting thread loop: " << spa_strerror(result) << "\n"; + ::debug->Error(TAG, str::format("error starting thread loop: %s", spa_strerror(result)).c_str()); goto cleanup; }; @@ -357,7 +362,10 @@ bool PipeWireOut::StartPipeWire(IBuffer* buffer) { params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat, &audioInfo); if (!params[0]) { - std::cerr << "[PipeWire] failed to create audio format\n"; + ::debug->Error(TAG, str::format( + "failed to create audio format. channels=%d, rate=%d", + this->channelCount, + this->sampleRate).c_str()); goto cleanup; } @@ -382,19 +390,21 @@ bool PipeWireOut::StartPipeWire(IBuffer* buffer) { if (result == 0) { pw_thread_loop_unlock(this->pwThreadLoop); - std::cerr << "[PipeWire] stream created and connected\n"; + ::debug->Info(TAG, "new stream created and connected successfully"); this->initialized = true; return true; } else { - std::cerr << "[PipeWire] error starting stream: " << spa_strerror(result) << "\n"; + ::debug->Error(TAG, str::format( + "error starting stream: %s", + spa_strerror(result)).c_str()); } } } cleanup: pw_thread_loop_unlock(this->pwThreadLoop); - std::cerr << "[PipeWire] stream not initialized.\n"; + ::debug->Error(TAG, "stream not initialized"); this->StopPipeWire(); return false; } @@ -457,25 +467,25 @@ void PipeWireOut::RefreshDeviceList() { deviceListContext.loop = pw_main_loop_new(nullptr); if (!deviceListContext.loop) { - std::cerr << "[PipeWire] RefreshDeviceList: could not create main loop.\n"; + ::debug->Error(TAG, "RefreshDeviceList() could not create main loop"); return; } auto loop = pw_main_loop_get_loop(deviceListContext.loop); if (!loop) { - std::cerr << "[PipeWire] RefreshDeviceList: could not resolve loop from main_loop??\n"; + ::debug->Error(TAG, "RefreshDeviceList() could not resolve loop from main_loop??"); return; } deviceListContext.context = pw_context_new(loop, nullptr, 0); if (!deviceListContext.context) { - std::cerr << "[PipeWire] RefreshDeviceList: could not create context.\n"; + ::debug->Error(TAG, "RefreshDeviceList() could not create context"); return; } deviceListContext.core = pw_context_connect(deviceListContext.context, nullptr, 0); if (deviceListContext.core == nullptr) { - std::cerr << "[PipeWire] RefreshDeviceList: could not connect to core.\n"; + ::debug->Error(TAG, "RefreshDeviceList() could not connect to core"); return; } From 7b4864bed9a94d334d665fdae8b1603734832743 Mon Sep 17 00:00:00 2001 From: casey langen Date: Thu, 18 Mar 2021 22:41:29 -0700 Subject: [PATCH 21/24] Use a stubbed debugger during shutdown. --- src/musikcore/plugin/Plugins.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/musikcore/plugin/Plugins.cpp b/src/musikcore/plugin/Plugins.cpp index 9f8420010..ad13948f0 100644 --- a/src/musikcore/plugin/Plugins.cpp +++ b/src/musikcore/plugin/Plugins.cpp @@ -123,6 +123,14 @@ static class Debug: public IDebug { } } debugger; +static class NullDebug: public IDebug { /* used during shutdown */ + public: + void Verbose(const char* tag, const char* message) override {} + void Info(const char* tag, const char* message) override {} + void Warning(const char* tag, const char* message) override {} + void Error(const char* tag, const char* message) override {} +} nullDebugger; + static class Environment: public IEnvironment { public: virtual size_t GetPath(PathType type, char* dst, int size) override { @@ -439,7 +447,7 @@ namespace musik { namespace core { namespace plugin { PluginFactory::Instance().QueryFunction( "SetDebug", [](musik::core::sdk::IPlugin* plugin, SetDebug func) { - func(nullptr); + func(&nullDebugger); }); } From 9cacfb9832d5a0ad47989126a862d891e5df9799 Mon Sep 17 00:00:00 2001 From: casey langen Date: Fri, 19 Mar 2021 23:28:11 -0700 Subject: [PATCH 22/24] Disable PipeWire by default; added ENABLE_PIPEWIRE flag to CMake. Added Hirsute build target that includes PipeWire. --- .circleci/config.yml | 14 ++++++++++++++ CMakeLists.txt | 6 ++++-- src/plugins/pipewireout/CMakeLists.txt | 2 ++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 983b53263..edce0a1d8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,6 +42,20 @@ jobs: - run: mkdir -p /root/debs && mv /root/project/*.deb /root/debs/ - store_artifacts: path: /root/debs/ + build_ubuntu_hirsute: + docker: + - image: ubuntu:hirsute + steps: + - run: apt-get update + - run: DEBIAN_FRONTEND="noninteractive" TZ="America/Los_Angeles" apt-get install -y git ssh sshpass build-essential clang cmake libboost-thread1.71-dev libboost-system1.71-dev libboost-filesystem1.71-dev libboost-date-time1.71-dev libboost-atomic1.71-dev libboost-chrono1.71-dev libogg-dev libvorbis-dev libavutil-dev libavformat-dev libswresample-dev libncursesw5-dev libasound2-dev libpulse-dev pulseaudio libmicrohttpd-dev libmp3lame-dev libcurl4-openssl-dev libev-dev libssl-dev libtag1-dev libsystemd-dev libpipewire-0.3-dev libspa-0.2-dev + - checkout + - run: cmake -DENABLE_PCH=true -DENABLE_PIPEWIRE=true -DGENERATE_DEB=1 -DDEB_ARCHITECTURE=amd64 -DDEB_PLATFORM=ubuntu -DDEB_DISTRO=groovy -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release . + - run: make -j2 + - run: make package + - run: ./script/copy-artifacts-to-host.sh /root/project/*.deb + - run: mkdir -p /root/debs && mv /root/project/*.deb /root/debs/ + - store_artifacts: + path: /root/debs/ build_fedora_31: docker: - image: fedora:31 diff --git a/CMakeLists.txt b/CMakeLists.txt index d638e8adb..d6248b562 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -155,8 +155,10 @@ add_dependencies(musikcubed musikcube) if (CMAKE_SYSTEM_NAME MATCHES "Linux") add_subdirectory(src/plugins/alsaout) add_subdirectory(src/plugins/pulseout) - add_subdirectory(src/plugins/pipewireout) - add_dependencies(musikcube pipewireout) + if (${ENABLE_PIPEWIRE} MATCHES "true") + add_subdirectory(src/plugins/pipewireout) + add_dependencies(musikcube pipewireout) + endif() if (${ENABLE_MPRIS} MATCHES "true") add_subdirectory(src/plugins/mpris) add_dependencies(musikcube mpris) diff --git a/src/plugins/pipewireout/CMakeLists.txt b/src/plugins/pipewireout/CMakeLists.txt index 7a4fb8ee3..5edf57fa8 100644 --- a/src/plugins/pipewireout/CMakeLists.txt +++ b/src/plugins/pipewireout/CMakeLists.txt @@ -6,7 +6,9 @@ set (pipewireout_SOURCES message(STATUS "[pipewireout] plugin enabled") include_directories("/usr/include/spa-0.2") +include_directories("/usr/local/include/spa-0.2") include_directories("/usr/include/pipewire-0.3") +include_directories("/usr/local/include/pipewire-0.3") add_library(pipewireout SHARED ${pipewireout_SOURCES}) target_link_libraries(pipewireout ${musikcube_LINK_LIBS} pipewire-0.3) From 3b6768cb2d1ec04449f044246223d39dfa27a346 Mon Sep 17 00:00:00 2001 From: casey langen Date: Fri, 19 Mar 2021 23:29:03 -0700 Subject: [PATCH 23/24] FFMPEG_ENABLED -> ENABLE_FFMPEG for consistency. --- CMakeLists.txt | 4 ++-- script/archive-macos.sh | 2 +- src/plugins/stockencoders/CMakeLists.txt | 8 ++++---- src/plugins/stockencoders/FfmpegEncoder.cpp | 4 ++-- src/plugins/stockencoders/FfmpegEncoder.h | 6 +++--- src/plugins/stockencoders/main.cpp | 6 +++--- src/plugins/taglib_plugin/CMakeLists.txt | 8 ++++---- src/plugins/taglib_plugin/TaglibMetadataReader.cpp | 4 ++-- src/plugins/taglib_plugin/taglib_plugin.cpp | 2 +- 9 files changed, 22 insertions(+), 22 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d6248b562..45ce90534 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,7 +120,7 @@ else() add_subdirectory(src/plugins/libopenmptdecoder) endif() -if (${FFMPEG_ENABLED} MATCHES "false") +if (${ENABLE_FFMPEG} MATCHES "false") message(STATUS "[ffmpeg] enabled = false") add_subdirectory(src/plugins/m4adecoder) add_subdirectory(src/plugins/oggdecoder) @@ -299,7 +299,7 @@ if (GENERATE_DEB MATCHES "1") set(DEPENDENCIES "libboost-thread${DEB_BOOST_VERSION}, libboost-system${DEB_BOOST_VERSION}, libboost-chrono${DEB_BOOST_VERSION}, libboost-filesystem${DEB_BOOST_VERSION}, libboost-date-time${DEB_BOOST_VERSION}, libmicrohttpd${DEB_MICROHTTPD_VERSION}, libcurl${DEB_LIBCURL_VERSION}, libogg0, libvorbis0a, libvorbisfile3, libncursesw${DEB_NCURSES_VERSION}, libasound2, libpulse0, pulseaudio, libmp3lame0, libev4, libopenmpt0, libssl1.1") - if (${FFMPEG_ENABLED} MATCHES "false") + if (${ENABLE_FFMPEG} MATCHES "false") set(DEPENDENCIES "${DEPENDENCIES}, libflac8, libfaad2") else() set(DEPENDENCIES "${DEPENDENCIES}, libavcodec-extra, libavutil${DEB_AVUTIL_VERSION}, libavformat${DEB_AVFORMAT_VERSION}, libswresample${DEB_SWRESAMPLE_VERSION}") diff --git a/script/archive-macos.sh b/script/archive-macos.sh index b5a910a32..8b9c4c426 100755 --- a/script/archive-macos.sh +++ b/script/archive-macos.sh @@ -12,7 +12,7 @@ SCRIPTDIR=`dirname "$0"` rm -rf bin/ ${SCRIPTDIR}/clean-nix.sh -cmake -DCMAKE_BUILD_TYPE=Release -DLINK_STATICALLY=true -DFFMPEG_ENABLED=false . +cmake -DCMAKE_BUILD_TYPE=Release -DLINK_STATICALLY=true -DENABLE_FFMPEG=false . make -j7 DIRNAME="musikcube_macos_$VERSION" diff --git a/src/plugins/stockencoders/CMakeLists.txt b/src/plugins/stockencoders/CMakeLists.txt index 78e74d59f..4ff9ec510 100644 --- a/src/plugins/stockencoders/CMakeLists.txt +++ b/src/plugins/stockencoders/CMakeLists.txt @@ -6,11 +6,11 @@ set (stockencoders_SOURCES add_library(stockencoders SHARED ${stockencoders_SOURCES}) -if (${FFMPEG_ENABLED} MATCHES "false") - message(STATUS "[stockencoders] *not* defining FFMPEG_ENABLED") +if (${ENABLE_FFMPEG} MATCHES "false") + message(STATUS "[stockencoders] *not* defining ENABLE_FFMPEG") else() - message(STATUS "[stockencoders] defining FFMPEG_ENABLED") - add_definitions(-DFFMPEG_ENABLED) + message(STATUS "[stockencoders] defining ENABLE_FFMPEG") + add_definitions(-DENABLE_FFMPEG) # fedora (and probably other RPM-based distros) put ffmpeg includes here... include_directories("/usr/include/ffmpeg") include_directories("/usr/local/include/ffmpeg") diff --git a/src/plugins/stockencoders/FfmpegEncoder.cpp b/src/plugins/stockencoders/FfmpegEncoder.cpp index 63ae8ae5f..4490692ab 100644 --- a/src/plugins/stockencoders/FfmpegEncoder.cpp +++ b/src/plugins/stockencoders/FfmpegEncoder.cpp @@ -34,7 +34,7 @@ #include "FfmpegEncoder.h" -#ifdef FFMPEG_ENABLED +#ifdef ENABLE_FFMPEG #include "shared.h" #include @@ -637,4 +637,4 @@ AVFrame* FfmpegEncoder::ReallocFrame( return original; } -#endif // FFMPEG_ENABLED \ No newline at end of file +#endif // ENABLE_FFMPEG \ No newline at end of file diff --git a/src/plugins/stockencoders/FfmpegEncoder.h b/src/plugins/stockencoders/FfmpegEncoder.h index 05f6a4445..763926823 100644 --- a/src/plugins/stockencoders/FfmpegEncoder.h +++ b/src/plugins/stockencoders/FfmpegEncoder.h @@ -35,10 +35,10 @@ #pragma once #ifdef WIN32 -#define FFMPEG_ENABLED +#define ENABLE_FFMPEG #endif -#ifdef FFMPEG_ENABLED +#ifdef ENABLE_FFMPEG #include #include @@ -98,4 +98,4 @@ class FfmpegEncoder : public musik::core::sdk::IBlockingEncoder { int inputSampleRate; }; -#endif // FFMPEG_ENABLED \ No newline at end of file +#endif // ENABLE_FFMPEG \ No newline at end of file diff --git a/src/plugins/stockencoders/main.cpp b/src/plugins/stockencoders/main.cpp index 9bdbb526e..2ec303804 100644 --- a/src/plugins/stockencoders/main.cpp +++ b/src/plugins/stockencoders/main.cpp @@ -62,7 +62,7 @@ static IEnvironment* environment = nullptr; static std::set supportedFormats = { ".mp3", -#ifdef FFMPEG_ENABLED +#ifdef ENABLE_FFMPEG "audio/mpeg", ".ogg", "audio/ogg", @@ -86,7 +86,7 @@ static class Plugin : public IPlugin { } virtual void Release() { } -#if defined(FFMPEG_ENABLED) || defined(WIN32) +#if defined(ENABLE_FFMPEG) || defined(WIN32) virtual const char* Name() { return "Stock Encoders (MP3 + ffmpeg)"; } #else virtual const char* Name() { return "Stock Encoders (MP3)"; } @@ -116,7 +116,7 @@ static class EncoderFactory: public IEncoderFactory { if (isMp3(lowerType)) { return new LameEncoder(); } -#ifdef FFMPEG_ENABLED +#ifdef ENABLE_FFMPEG else if (supportedFormats.find(lowerType) != supportedFormats.end()) { return new FfmpegEncoder(lowerType); } diff --git a/src/plugins/taglib_plugin/CMakeLists.txt b/src/plugins/taglib_plugin/CMakeLists.txt index 54085991e..2f6d5da3d 100644 --- a/src/plugins/taglib_plugin/CMakeLists.txt +++ b/src/plugins/taglib_plugin/CMakeLists.txt @@ -3,11 +3,11 @@ set (taglibreader_SOURCES TaglibMetadataReader.cpp ) -if (${FFMPEG_ENABLED} MATCHES "false") - message(STATUS "[taglibmetadatareader] *not* defining FFMPEG_ENABLED") +if (${ENABLE_FFMPEG} MATCHES "false") + message(STATUS "[taglibmetadatareader] *not* defining ENABLE_FFMPEG") else() - message(STATUS "[taglibmetadatareader] defining FFMPEG_ENABLED") - add_definitions(-DFFMPEG_ENABLED) + message(STATUS "[taglibmetadatareader] defining ENABLE_FFMPEG") + add_definitions(-DENABLE_FFMPEG) endif() add_library(taglibreader SHARED ${taglibreader_SOURCES}) diff --git a/src/plugins/taglib_plugin/TaglibMetadataReader.cpp b/src/plugins/taglib_plugin/TaglibMetadataReader.cpp index 74b660fbe..c9b3354ad 100644 --- a/src/plugins/taglib_plugin/TaglibMetadataReader.cpp +++ b/src/plugins/taglib_plugin/TaglibMetadataReader.cpp @@ -93,7 +93,7 @@ #include #ifdef WIN32 -#define FFMPEG_ENABLED +#define ENABLE_FFMPEG #endif using namespace musik::core::sdk; @@ -243,7 +243,7 @@ bool TaglibMetadataReader::CanRead(const char *extension) { if (extension && strlen(extension)) { std::string ext(str::lower(extension[0] == '.' ? &extension[1] : extension)); return -#ifdef FFMPEG_ENABLED +#ifdef ENABLE_FFMPEG ext.compare("opus") == 0 || ext.compare("wv") == 0 || ext.compare("wma") == 0 || diff --git a/src/plugins/taglib_plugin/taglib_plugin.cpp b/src/plugins/taglib_plugin/taglib_plugin.cpp index eb39a0920..dee44b350 100644 --- a/src/plugins/taglib_plugin/taglib_plugin.cpp +++ b/src/plugins/taglib_plugin/taglib_plugin.cpp @@ -52,7 +52,7 @@ class TaglibPlugin : public musik::core::sdk::IPlugin { public: virtual void Release() { delete this; } -#if defined(FFMPEG_ENABLED) || defined(WIN32) +#if defined(ENABLE_FFMPEG) || defined(WIN32) virtual const char* Name() { return "Taglib 1.11 ITagReader (+ffmpeg)"; } #else virtual const char* Name() { return "Taglib 1.11 ITagReader"; } From 017bc1fddea7752ecdcb06e8b53929002c27d9a9 Mon Sep 17 00:00:00 2001 From: casey langen Date: Fri, 19 Mar 2021 23:29:41 -0700 Subject: [PATCH 24/24] Fixed StockEncoder's plugin name --- src/plugins/stockencoders/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/stockencoders/main.cpp b/src/plugins/stockencoders/main.cpp index 2ec303804..e832f6254 100644 --- a/src/plugins/stockencoders/main.cpp +++ b/src/plugins/stockencoders/main.cpp @@ -87,9 +87,9 @@ static class Plugin : public IPlugin { virtual void Release() { } #if defined(ENABLE_FFMPEG) || defined(WIN32) - virtual const char* Name() { return "Stock Encoders (MP3 + ffmpeg)"; } + virtual const char* Name() { return "Stock Encoders (lame + ffmpeg)"; } #else - virtual const char* Name() { return "Stock Encoders (MP3)"; } + virtual const char* Name() { return "Stock Encoders (lame)"; } #endif virtual const char* Version() { return "0.7.0"; } virtual const char* Author() { return "clangen"; }