diff --git a/.vscode/launch.json b/.vscode/launch.json index bb49bf9..9dce54a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -142,7 +142,7 @@ //"[pipedal_alsa_test]" // "[wifi_channels_test]" // "[locale]" - "[pipedal_alsa_seq_test]" + "[pipewire_input_stream]" ], "stopAtEntry": false, diff --git a/cmake/FindPipeWire.cmake b/cmake/FindPipeWire.cmake new file mode 100644 index 0000000..c90c3e0 --- /dev/null +++ b/cmake/FindPipeWire.cmake @@ -0,0 +1,122 @@ +#.rst: +# FindPipeWire +# ------- +# +# Try to find PipeWire on a Unix system. +# +# This will define the following variables: +# +# ``PipeWire_FOUND`` +# True if (the requested version of) PipeWire is available +# ``PipeWire_VERSION`` +# The version of PipeWire +# ``PipeWire_LIBRARIES`` +# This can be passed to target_link_libraries() instead of the ``PipeWire::PipeWire`` +# target +# ``PipeWire_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``PipeWire_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``PipeWire_FOUND`` is TRUE, it will also define the following imported target: +# +# ``PipeWire::PipeWire`` +# The PipeWire library +# +# In general we recommend using the imported target, as it is easier to use. +# Bear in mind, however, that if the target is in the link interface of an +# exported library, it must be made available by the package config file. + +#============================================================================= +# Copyright 2014 Alex Merry +# Copyright 2014 Martin Gräßlin +# Copyright 2018-2020 Jan Grulich +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. +#============================================================================= + +# Use pkg-config to get the directories and then use these values +# in the FIND_PATH() and FIND_LIBRARY() calls +find_package(PkgConfig QUIET) + +pkg_search_module(PKG_PipeWire QUIET libpipewire-0.3 libpipewire-0.2) +pkg_search_module(PKG_Spa QUIET libspa-0.2 libspa-0.1) + +set(PipeWire_DEFINITIONS "${PKG_PipeWire_CFLAGS}" "${PKG_Spa_CFLAGS}") +set(PipeWire_VERSION "${PKG_PipeWire_VERSION}") + +find_path(PipeWire_INCLUDE_DIRS + NAMES + pipewire/pipewire.h + HINTS + ${PKG_PipeWire_INCLUDE_DIRS} + ${PKG_PipeWire_INCLUDE_DIRS}/pipewire-0.3 +) + +find_path(Spa_INCLUDE_DIRS + NAMES + spa/param/props.h + HINTS + ${PKG_Spa_INCLUDE_DIRS} + ${PKG_Spa_INCLUDE_DIRS}/spa-0.2 +) + +find_library(PipeWire_LIBRARIES + NAMES + pipewire-0.3 + pipewire-0.2 + HINTS + ${PKG_PipeWire_LIBRARY_DIRS} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(PipeWire + FOUND_VAR + PipeWire_FOUND + REQUIRED_VARS + PipeWire_LIBRARIES + PipeWire_INCLUDE_DIRS + Spa_INCLUDE_DIRS + VERSION_VAR + PipeWire_VERSION +) + +if(PipeWire_FOUND AND NOT TARGET PipeWire::PipeWire) + add_library(PipeWire::PipeWire UNKNOWN IMPORTED) + set_target_properties(PipeWire::PipeWire PROPERTIES + IMPORTED_LOCATION "${PipeWire_LIBRARIES}" + INTERFACE_COMPILE_OPTIONS "${PipeWire_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${PipeWire_INCLUDE_DIRS};${Spa_INCLUDE_DIRS}" + ) +endif() + +mark_as_advanced(PipeWire_LIBRARIES PipeWire_INCLUDE_DIRS) + +include(FeatureSummary) +set_package_properties(PipeWire PROPERTIES + URL "https://www.pipewire.org" + DESCRIPTION "PipeWire - multimedia processing" +) \ No newline at end of file diff --git a/docs/BuildPrerequisites.md b/docs/BuildPrerequisites.md index 40adbc5..d3c9d28 100644 --- a/docs/BuildPrerequisites.md +++ b/docs/BuildPrerequisites.md @@ -26,7 +26,8 @@ Run the following commands to install dependent libraries required by the PiPeda libsystemd-dev catch libasound2-dev uuid-dev \ authbind libavahi-client-dev libnm-dev libicu-dev \ libsdbus-c++-dev libzip-dev google-perftools \ - libgoogle-perftools-dev + libgoogle-perftools-dev \ + libpipewire-0.3-dev ### Installing Sources diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b0d36a6..7055495 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,9 +9,19 @@ set (ENABLE_BACKTRACE 0) set (USE_SANITIZE OFF) # seems to be broken on Ubuntu 24.10 + set(CXX_STANDARD 20) +find_package(PkgConfig REQUIRED) +pkg_check_modules(PipeWire REQUIRED libpipewire-0.3) + + +message(STATUS "PipeWire_INCLUDE_DIRS: ${PipeWire_INCLUDE_DIRS}" + "PipeWire_LIBRARIES: ${PipeWire_LIBRARIES}" + "PipeWire_CFLAGS: ${PipeWire_CFLAGS}" + "PipeWire_DEFINES: ${PipeWire_DEFINES}" + "PipeWire_VERSION: ${PipeWire_VERSION}") include(FetchContent) @@ -196,6 +206,7 @@ else() endif() set (PIPEDAL_SOURCES + PipewireInputStream.cpp PipewireInputStream.hpp AudioFiles.cpp AudioFiles.hpp AudioFileMetadataReader.cpp AudioFileMetadataReader.hpp AudioFileMetadata.hpp AudioFileMetadata.cpp @@ -313,6 +324,7 @@ set (PIPEDAL_INCLUDES ${JACK_INCLUDE_DIRS} ${LILV_0_INCLUDE_DIRS} ${VST3_INCLUDES} ${WEBSOCKETPP_INCLUDE_DIRS} + ${PipeWire_INCLUDE_DIRS} . ) @@ -332,13 +344,17 @@ set(PIPEDAL_LIBS libpipedald zip add_library(libpipedald STATIC ${PIPEDAL_SOURCES}) -target_include_directories(libpipedald PRIVATE ${PIPEDAL_INCLUDES} +target_compile_definitions(libpipedald PUBLIC "_REENTRANT") + +target_include_directories(libpipedald PUBLIC ${PIPEDAL_INCLUDES} ) target_link_libraries(libpipedald PUBLIC PiPedalCommon - SQLiteCpp) + SQLiteCpp + ${PipeWire_LIBRARIES} + ) if(${USE_PCH}) target_precompile_headers(libpipedald PRIVATE pch.h) @@ -379,6 +395,7 @@ target_link_libraries(pipedald PRIVATE PiPedalCommon ${PIPEDAL_LIBS} ) + ################################# add_executable(hotspotManagerTest hotspotManagerTestMain.cpp @@ -397,6 +414,7 @@ add_executable(AuxInTest add_executable(pipedaltest testMain.cpp + PipewireInputStreamTest.cpp AudioFilesTest.cpp LRUCacheTest.cpp diff --git a/src/PipewireInputStream.cpp b/src/PipewireInputStream.cpp new file mode 100644 index 0000000..b6ff96a --- /dev/null +++ b/src/PipewireInputStream.cpp @@ -0,0 +1,244 @@ +/* + * MIT License + * + * Copyright (c) 2025 Robin E. R. Davies + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "PipewireInputStream.hpp" +#include +#include +extern "C" +{ +#include +#include +#include +} +#include +#include + +using namespace pipedal; + +namespace pipedal::impl +{ + + class PipeWireInputStreamImpl : public PipeWireInputStream + { + private: + pw_main_loop *loop = nullptr; + pw_stream *stream = nullptr; + Callback callback; + + std::atomic is_running_{false}; + + static void on_process(void *userData) + { + PipeWireInputStreamImpl *this_ = (PipeWireInputStreamImpl *)userData; + this_->on_process(); + } + + void on_process() + { + pw_buffer *buf; + + if ((buf = pw_stream_dequeue_buffer(stream)) == NULL) + return; + + // Process buffer data + spa_buffer *sbuf = buf->buffer; + for (uint32_t i = 0; i < sbuf->n_datas; i++) + { + // Access audio data through sbuf->datas[i].data + // Size is sbuf->datas[i].chunk->size + } + + pw_stream_queue_buffer(stream, buf); + } + + static void on_stream_state_changed(void *userData, enum pw_stream_state old, + enum pw_stream_state state, const char *error) + { + PipeWireInputStreamImpl *this_ = (PipeWireInputStreamImpl *)userData; + this_->on_stream_state_changed( + old, state, error); + } + + void on_stream_state_changed(enum pw_stream_state old, + enum pw_stream_state state, const char *error) + { + if (state == PW_STREAM_STATE_ERROR) + { + pw_main_loop_quit(loop); + is_running_ = false; + } + } + + static const struct pw_stream_events stream_events_; + + public: + PipeWireInputStreamImpl(const std::string &stream_name, + uint32_t channels, + uint32_t rate ) + : is_running_(false) + { + + // Initialize PipeWire + pw_init(nullptr, nullptr); + + // Create main loop + loop = pw_main_loop_new(nullptr); + if (!loop) + { + throw std::runtime_error("Failed to create main loop"); + } + + // Create stream + stream = pw_stream_new_simple( + pw_main_loop_get_loop(loop), + stream_name.c_str(), + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_CLASS, "Audio/Sink", + PW_KEY_NODE_NAME, stream_name.c_str(), + PW_KEY_NODE_DESCRIPTION, stream_name.c_str(), + PW_KEY_APP_NAME, "PiPedal Test", + PW_KEY_APP_PROCESS_BINARY, "pipedaltest", + PW_KEY_MEDIA_ROLE, "DSP", + nullptr), + &stream_events_, + this); + + if (!stream) + { + pw_main_loop_destroy(loop); + loop = nullptr; + throw std::runtime_error("Failed to create stream"); + } + + // Setup audio format + uint8_t buffer[1024]; + spa_pod_builder b = {0}; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + const spa_pod *params[1]; + spa_audio_info_raw format = { + .format = SPA_AUDIO_FORMAT_S16, + .flags = SPA_AUDIO_FLAG_NONE, + .rate = rate, + .channels = channels}; + if (channels == 1) + { + format.position[0] = SPA_AUDIO_CHANNEL_MONO; // Mono channel + } + else if (channels == 2) + { + format.position[0] = SPA_AUDIO_CHANNEL_FL; // Front Left + format.position[1] = SPA_AUDIO_CHANNEL_FR; // Front Right + } + else if (channels == 4) + { + // 3.1 + format.position[0] = SPA_AUDIO_CHANNEL_FL; + format.position[1] = SPA_AUDIO_CHANNEL_FR; + format.position[2] = SPA_AUDIO_CHANNEL_FC; + format.position[3] = SPA_AUDIO_CHANNEL_LFE; + } + else + { + throw std::runtime_error("Unsupported number of channels: " + std::to_string(channels)); + } + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &format); + + // Connect stream + int res = pw_stream_connect( + stream, + PW_DIRECTION_INPUT, + PW_ID_ANY, + pw_stream_flags(PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_RT_PROCESS), + params, 1); + + if (res < 0) + { + pw_stream_destroy(stream); + stream = nullptr; + pw_main_loop_destroy(loop); + loop = nullptr; + throw std::runtime_error("Failed to connect stream: " + std::string(strerror(-res))); + } + + // xxx: delete me + pw_main_loop_run(loop); + } + + ~PipeWireInputStreamImpl() + { + if (stream) + { + pw_stream_destroy(stream); + } + if (loop) + { + pw_main_loop_destroy(loop); + } + pw_deinit(); + } + + std::unique_ptr serviceThread; + + + virtual void Activate(Callback &&callback) override + { + serviceThread = std::make_unique([this]() { + if (!is_running_) + { + is_running_ = true; + pw_main_loop_run(loop); + is_running_ = false; + } + }); + } + + virtual void Deactivate() override + { + if (is_running_) + { + pw_main_loop_quit(loop); + } + serviceThread = nullptr; // and join. + } + + bool IsActive() const { return is_running_; } + }; + + const struct pw_stream_events PipeWireInputStreamImpl::stream_events_ = { + .version = PW_VERSION_STREAM_EVENTS, + .state_changed = on_stream_state_changed, + .process = on_process, + }; +} + +using namespace pipedal::impl; + +PipeWireInputStream::ptr PipeWireInputStream::Create(const std::string &streamName, int sampleRate, int channels) +{ + return std::make_shared(streamName, channels, sampleRate); +} \ No newline at end of file diff --git a/src/PipewireInputStream.hpp b/src/PipewireInputStream.hpp new file mode 100644 index 0000000..fe67370 --- /dev/null +++ b/src/PipewireInputStream.hpp @@ -0,0 +1,50 @@ +/* + * MIT License + * + * Copyright (c) 2025 Robin E. R. Davies + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#pragma once + +#include +#include +#include + +namespace pipedal +{ + class PipeWireInputStream + { + protected: + PipeWireInputStream() {} + + public: + virtual ~PipeWireInputStream() {} + + using self = PipeWireInputStream; + using ptr = std::shared_ptr; + + using Callback = std::function; + static ptr Create(const std::string &streamName, int sampleRate, int channels); + + virtual void Activate(Callback &&callback) = 0; + virtual void Deactivate() = 0; + virtual bool IsActive() const = 0; + }; +} \ No newline at end of file diff --git a/src/PipewireInputStreamTest.cpp b/src/PipewireInputStreamTest.cpp new file mode 100644 index 0000000..09cb03c --- /dev/null +++ b/src/PipewireInputStreamTest.cpp @@ -0,0 +1,61 @@ +// Copyright (c) 2022 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "pch.h" +#include "catch.hpp" +#include +#include +#include +#include +#include "PipewireInputStream.hpp" + +#include "PiPedalAlsa.hpp" +#include +#include + +using namespace pipedal; +using namespace std; + + +TEST_CASE("PipeWire Input Stream Test", "[pipewire_input_stream]") +{ + + auto stream = PipeWireInputStream::Create("PiPedalTest Stream", 48000, 2); + REQUIRE(stream != nullptr); + REQUIRE(!stream->IsActive()); + std::atomic frameCount = 0; + std::atomic *pFrameCount = &frameCount; + stream->Activate([pFrameCount] ( + const float*buffer, + size_t size) + { + *pFrameCount += size; + }); + + size_t lastCount = frameCount; + while (true) { + size_t t = frameCount; + if (lastCount != t) { + lastCount = t; + cout << "Frame count: " << lastCount << endl; + } + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } +} +