From 697a05e55b9f6ea17dac8b6ee620fbaf917ea1fd Mon Sep 17 00:00:00 2001 From: Curve Date: Thu, 13 May 2021 13:58:55 +0200 Subject: [PATCH] feat(pipewire): begin implementation This is adds partial (incomplete) PipeWire support (#106). Null Sink creation is still missing! --- CMakeLists.txt | 7 +- FindPipeWire.cmake | 122 ++++++ src/helper/audio/linux/pipewire/pipewire.cpp | 398 +++++++++++++++++++ src/helper/audio/linux/pipewire/pipewire.hpp | 94 +++++ 4 files changed, 619 insertions(+), 2 deletions(-) create mode 100644 FindPipeWire.cmake create mode 100644 src/helper/audio/linux/pipewire/pipewire.cpp create mode 100644 src/helper/audio/linux/pipewire/pipewire.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 712c132..30ac309 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,13 +54,16 @@ find_package(Threads REQUIRED) target_link_libraries(soundux PRIVATE Threads::Threads ${CMAKE_DL_LIBS}) if (UNIX) + list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) + find_package(PipeWire REQUIRED) + find_package(PulseAudio) find_package(X11 REQUIRED) - include_directories(${X11_INCLUDE_DIR} ${PULSEAUDIO_INCLUDE_DIR}) + target_include_directories(soundux SYSTEM PRIVATE ${X11_INCLUDE_DIR} ${PULSEAUDIO_INCLUDE_DIR} ${PipeWire_INCLUDE_DIRS} ${Spa_INCLUDE_DIRS}) find_package(PkgConfig REQUIRED) pkg_check_modules(WNCK libwnck-3.0) - target_link_libraries(soundux PRIVATE ${X11_LIBRARIES} ${X11_Xinput_LIB} ${X11_XTest_LIB}) + target_link_libraries(soundux PRIVATE ${X11_LIBRARIES} ${X11_Xinput_LIB} ${X11_XTest_LIB} ${PipeWire_LIBRARIES}) endif() if (WIN32) target_compile_definitions(soundux PRIVATE _CRT_SECURE_NO_WARNINGS=1 _SILENCE_ALL_CXX17_DEPRECATION_WARNINGS=1 _UNICODE=1) diff --git a/FindPipeWire.cmake b/FindPipeWire.cmake new file mode 100644 index 0000000..c90c3e0 --- /dev/null +++ b/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/src/helper/audio/linux/pipewire/pipewire.cpp b/src/helper/audio/linux/pipewire/pipewire.cpp new file mode 100644 index 0000000..710b89b --- /dev/null +++ b/src/helper/audio/linux/pipewire/pipewire.cpp @@ -0,0 +1,398 @@ +#include "pipewire.hpp" +#include +#include +#include + +namespace Soundux::Objects +{ + void PipeWire::sync() + { + spa_hook coreListener; + int pending = 0; + + pw_core_events coreEvents = {}; + coreEvents.version = PW_VERSION_CORE_EVENTS; + coreEvents.done = [](void *data, uint32_t id, int seq) { + auto *info = reinterpret_cast *>(data); + if (info) + { + if (id == PW_ID_CORE && seq == *info->second) + { + *info->second = -1; + pw_main_loop_quit(info->first->loop); + } + } + }; + + spa_zero(coreListener); + auto data = std::make_pair(this, &pending); + pw_core_add_listener(core, &coreListener, &coreEvents, &data); // NOLINT + + pending = pw_core_sync(core, PW_ID_CORE, 0); // NOLINT + while (pending != -1) + { + pw_main_loop_run(loop); + } + + spa_hook_remove(&coreListener); + } + + void PipeWire::onGlobalAdded(void *data, std::uint32_t id, [[maybe_unused]] std::uint32_t perms, const char *type, + [[maybe_unused]] std::uint32_t version, const spa_dict *props) + { + auto *thiz = reinterpret_cast(data); + if (thiz && data && props && type) + { + if (strcmp(type, PW_TYPE_INTERFACE_Port) == 0) + { + const auto *alias = spa_dict_lookup(props, "port.alias"); + const auto *portName = spa_dict_lookup(props, "port.name"); + + if (alias && portName) + { + //* This is the only reliable way to get the name + //* PW_KEY_APP_NAME or PW_KEY_APP_PROCESS_BINARY are almost certainly never set. + auto name = std::string(alias); + name = name.substr(0, name.find_first_of(':')); + + Direction direction; + if (strstr(portName, "FR")) + { + direction = Direction::FrontRight; + } + else + { + direction = Direction::FrontLeft; + } + + if (strstr(portName, "output")) + { + auto outputApp = std::make_shared(); + + outputApp->id = id; + outputApp->name = name; + outputApp->application = name; + outputApp->direction = direction; + + std::lock_guard lock(thiz->playbackMutex); + thiz->playbackApps.emplace_back(outputApp); + } + else if (strstr(portName, "input")) + { + auto recordingApp = std::make_shared(); + + recordingApp->id = id; + recordingApp->name = name; + recordingApp->application = name; + recordingApp->direction = direction; + + std::lock_guard lock(thiz->recordingMutex); + thiz->recordingApps.emplace_back(recordingApp); + } + } + } + else if (strcmp(type, PW_TYPE_INTERFACE_Link) == 0) + { + //* Should be used later to delete left over Soundux Links. + const auto *inputPort = spa_dict_lookup(props, "link.input.port"); + const auto *outputPort = spa_dict_lookup(props, "link.output.port"); + + if (inputPort && outputPort) + { + const auto in = std::stol(inputPort); + const auto out = std::stol(outputPort); + std::lock_guard lock(thiz->playbackMutex); + + for (auto &app : thiz->playbackApps) + { + auto pipeWireApp = std::dynamic_pointer_cast(app); + + if (pipeWireApp->id == out) + { + pipeWireApp->links.emplace_back(Link{static_cast(in)}); + } + } + } + } + } + } + + void PipeWire::onGlobalRemoved(void *data, std::uint32_t id) + { + auto *thiz = reinterpret_cast(data); + + if (thiz) + { + thiz->playbackMutex.lock(); + for (auto it = thiz->playbackApps.begin(); it != thiz->playbackApps.end();) + { + auto pipeWireApp = std::dynamic_pointer_cast(*it); + if (pipeWireApp && pipeWireApp->id == id) + { + it = thiz->playbackApps.erase(it); + } + else + { + it++; + } + } + thiz->playbackMutex.unlock(); + + thiz->recordingMutex.lock(); + for (auto it = thiz->recordingApps.begin(); it != thiz->recordingApps.end();) + { + auto pipeWireApp = std::dynamic_pointer_cast(*it); + if (pipeWireApp && pipeWireApp->id == id) + { + it = thiz->recordingApps.erase(it); + } + else + { + it++; + } + } + thiz->recordingMutex.unlock(); + } + } + + void PipeWire::setup() + { + pw_init(nullptr, nullptr); + loop = pw_main_loop_new(nullptr); + if (!loop) + { + throw std::runtime_error("Failed to create main loop"); + } + context = pw_context_new(pw_main_loop_get_loop(loop), nullptr, 0); + if (!context) + { + throw std::runtime_error("Failed to create context"); + } + + core = pw_context_connect(context, nullptr, 0); + if (!core) + { + throw std::runtime_error("Failed to connect context"); + } + registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, 0); + if (!registry) + { + throw std::runtime_error("Failed to get registry"); + } + + spa_zero(registryListener); + registryEvents.global = onGlobalAdded; + registryEvents.global_remove = onGlobalRemoved; + registryEvents.version = PW_VERSION_REGISTRY_EVENTS; + + pw_registry_add_listener(registry, ®istryListener, ®istryEvents, this); // NOLINT + sync(); + } + + void PipeWire::destroy() + { + pw_proxy_destroy(reinterpret_cast(registry)); + pw_core_disconnect(core); + pw_context_destroy(context); + pw_main_loop_destroy(loop); + } + + bool PipeWire::deleteLink(std::uint32_t id) + { + pw_registry_destroy(registry, id); // NOLINT + sync(); + + return true; + } + + std::optional PipeWire::linkPorts(std::uint32_t in, std::uint32_t out) + { + pw_properties *props = pw_properties_new(nullptr, nullptr); + + pw_properties_set(props, PW_KEY_APP_NAME, "soundux"); + // pw_properties_set(props, PW_KEY_OBJECT_LINGER, "true"); + pw_properties_setf(props, PW_KEY_LINK_INPUT_PORT, "%u", in); + pw_properties_setf(props, PW_KEY_LINK_OUTPUT_PORT, "%u", out); + + auto *proxy = reinterpret_cast( + pw_core_create_object(core, "link-factory", PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, &props->dict, 0)); + + if (!proxy) + { + pw_properties_free(props); + return std::nullopt; + } + + spa_hook listener; + std::optional result; + + pw_proxy_events linkEvent = {}; + linkEvent.version = PW_VERSION_PROXY_EVENTS; + linkEvent.bound = [](void *data, std::uint32_t id) { + *reinterpret_cast *>(data) = id; + }; + linkEvent.error = [](void *data, [[maybe_unused]] int a, [[maybe_unused]] int b, const char *message) { + printf("Error: %s\n", message); + *reinterpret_cast *>(data) = std::nullopt; + }; + + spa_zero(listener); + pw_proxy_add_listener(proxy, &listener, &linkEvent, &result); + + sync(); + + spa_hook_remove(&listener); + pw_properties_free(props); + pw_proxy_destroy(proxy); + + return result; + } + + std::vector> PipeWire::getRecordingApps() + { + sync(); + return recordingApps; + } + + std::vector> PipeWire::getPlaybackApps() + { + sync(); + std::lock_guard lock(playbackMutex); + std::vector> rtn; + + for (const auto &app : playbackApps) + { + auto pipeWireApp = std::dynamic_pointer_cast(app); + if (!pipeWireApp->links.empty()) + { + rtn.emplace_back(app); + } + } + + return rtn; + } + + std::shared_ptr PipeWire::getPlaybackApp(const std::string &name) + { + std::lock_guard lock(playbackMutex); + for (const auto &app : playbackApps) + { + if (app->name == name) + { + return app; + } + } + + return nullptr; + } + + std::shared_ptr PipeWire::getRecordingApp(const std::string &name) + { + std::lock_guard lock(recordingMutex); + for (const auto &app : recordingApps) + { + if (app->name == name) + { + return app; + } + } + + return nullptr; + } + + bool PipeWire::useAsDefault() + { + // TODO(pipewire): Find a way to connect the output to the microphone + return false; + } + + bool PipeWire::revertDefault() + { + // TODO(pipewire): Delete link created by `useAsDefault` + return true; + } + + bool PipeWire::muteInput(bool state) + { + // TODO(pipewire): Maybe we could delete any link from the microphone to the output app and recreate it? + (void)state; + return false; + } + + bool PipeWire::inputSoundTo(std::shared_ptr app) + { + std::vector toMove; + + recordingMutex.lock(); + for (const auto &recordingApp : recordingApps) + { + if (recordingApp->name == app->name) + { + auto pipeWireApp = std::dynamic_pointer_cast(recordingApp); + toMove.emplace_back(*pipeWireApp); + } + } + recordingMutex.unlock(); + + for (const auto &pipeWireApp : toMove) + { + (void)pipeWireApp; + // TODO(pipewire): Link null sink to each app + // TODO(pipewire): Save id of each created link + } + + return true; + } + + bool PipeWire::stopSoundInput() + { + for (const auto &id : soundInputLinks) + { + deleteLink(id); + } + soundInputLinks.clear(); + + return true; + } + + bool PipeWire::passthroughFrom(std::shared_ptr app) + { + std::vector toMove; + + playbackMutex.lock(); + for (const auto &playbackApp : playbackApps) + { + if (playbackApp->name == app->name) + { + auto pipeWireApp = std::dynamic_pointer_cast(playbackApp); + toMove.emplace_back(*pipeWireApp); + } + } + playbackMutex.unlock(); + + for (const auto &pipeWireApp : toMove) + { + (void)pipeWireApp; + // TODO(pipewire): Link each app to null sink + // TODO(pipewire): Save id of each created link + } + + return true; + } + + bool PipeWire::isCurrentlyPassingThrough() + { + return !passthroughLinks.empty(); + } + + bool PipeWire::stopPassthrough() + { + for (const auto &id : passthroughLinks) + { + deleteLink(id); + } + passthroughLinks.clear(); + + return true; + } +} // namespace Soundux::Objects \ No newline at end of file diff --git a/src/helper/audio/linux/pipewire/pipewire.hpp b/src/helper/audio/linux/pipewire/pipewire.hpp new file mode 100644 index 0000000..0aca403 --- /dev/null +++ b/src/helper/audio/linux/pipewire/pipewire.hpp @@ -0,0 +1,94 @@ +#include "../backend.hpp" +#include +#include +#include +#include +#include + +namespace Soundux +{ + namespace Objects + { + enum class Direction : std::uint8_t + { + FrontLeft, + FrontRight + }; + struct Link + { + std::uint32_t destination; + }; + struct PipeWirePlaybackApp : public PlaybackApp + { + std::uint32_t id; + Direction direction; + std::vector links; + ~PipeWirePlaybackApp() override = default; + }; + + struct PipeWireRecordingApp : public RecordingApp + { + std::uint32_t id; + Direction direction; + ~PipeWireRecordingApp() override = default; + }; + + template struct MovedApp + { + T app; + std::vector createdLinks; + }; + + class PipeWire : public AudioBackend + { + pw_core *core; + pw_main_loop *loop; + pw_context *context; + + pw_registry *registry; + spa_hook registryListener; + pw_registry_events registryEvents; + + std::mutex playbackMutex; + std::vector> playbackApps; + + std::mutex recordingMutex; + std::vector> recordingApps; + + private: + std::vector soundInputLinks; + std::vector passthroughLinks; + + private: + void sync(); + bool deleteLink(std::uint32_t); + std::optional linkPorts(std::uint32_t, std::uint32_t); + + static void onGlobalRemoved(void *, std::uint32_t); + static void onGlobalAdded(void *, std::uint32_t, std::uint32_t, const char *, std::uint32_t, + const spa_dict *); + + public: + PipeWire() = default; + void setup() override; + void destroy() override; + + bool useAsDefault() override; + bool revertDefault() override; + bool muteInput(bool state) override; + + bool stopPassthrough() override; + bool isCurrentlyPassingThrough() override; + bool passthroughFrom(std::shared_ptr app) override; + + bool stopSoundInput() override; + bool inputSoundTo(std::shared_ptr app) override; + + std::shared_ptr getPlaybackApp(const std::string &name) override; + std::shared_ptr getRecordingApp(const std::string &name) override; + + std::vector> getPlaybackApps() override; + std::vector> getRecordingApps() override; + }; + } // namespace Objects +} // namespace Soundux \ No newline at end of file