diff options
author | Maki <maki@hotmilk.space> | 2024-04-17 23:06:29 -0400 |
---|---|---|
committer | Maki <maki@hotmilk.space> | 2024-04-17 23:06:29 -0400 |
commit | 1e5b77547fd4a70888ab24834bfdb634206cafc3 (patch) | |
tree | 4d32017ffc78b955992dc65da43ba0f79e986e59 /indra | |
parent | 7d2608947ccfa2215fa66d162134bf67bf27b383 (diff) |
Use PipeWire for Linux volume catcher
Diffstat (limited to 'indra')
-rw-r--r-- | indra/cmake/FindPipeWire.cmake | 100 | ||||
-rw-r--r-- | indra/media_plugins/cef/CMakeLists.txt | 26 | ||||
-rwxr-xr-x | indra/media_plugins/cef/linux_volume_catcher_pa.cpp (renamed from indra/media_plugins/cef/linux_volume_catcher.cpp) | 2 | ||||
-rwxr-xr-x | indra/media_plugins/cef/linux_volume_catcher_pw.cpp | 451 | ||||
-rw-r--r-- | indra/media_plugins/cef/linux_volume_catcher_pw_syms.inc | 22 |
5 files changed, 589 insertions, 12 deletions
diff --git a/indra/cmake/FindPipeWire.cmake b/indra/cmake/FindPipeWire.cmake new file mode 100644 index 0000000000..868acf5ec1 --- /dev/null +++ b/indra/cmake/FindPipeWire.cmake @@ -0,0 +1,100 @@ +# cmake-format: off +# .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_COMPILE_FLAGS`` 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 <alex.merry@kde.org> Copyright 2014 Martin Gräßlin +# <mgraesslin@kde.org> Copyright 2018-2020 Jan Grulich <jgrulich@redhat.com> +# +# 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. +# ============================================================================= +# cmake-format: on + +# 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) +pkg_search_module(PKG_SPA QUIET libspa-0.2) + +set(PIPEWIRE_COMPILE_FLAGS "${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 + 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_COMPILE_FLAGS}" + 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") diff --git a/indra/media_plugins/cef/CMakeLists.txt b/indra/media_plugins/cef/CMakeLists.txt index bbd2eb222a..9e129f2185 100644 --- a/indra/media_plugins/cef/CMakeLists.txt +++ b/indra/media_plugins/cef/CMakeLists.txt @@ -24,17 +24,21 @@ set(media_plugin_cef_HEADER_FILES # Select which VolumeCatcher implementation to use if (LINUX) - foreach( PULSE_FILE pulse/introspect.h pulse/context.h pulse/subscribe.h pulse/glib-mainloop.h ) - find_path( PULSE_FILE_${PULSE_FILE}_FOUND ${PULSE_FILE} NO_CACHE) - if( NOT PULSE_FILE_${PULSE_FILE}_FOUND ) - message( "Looking for ${PULSE_FILE} ... not found") - message( FATAL_ERROR "Pulse header not found" ) - else() - message( "Looking for ${PULSE_FILE} ... found") - endif() - endforeach() - message( "Building with linux volume catcher" ) - set(LINUX_VOLUME_CATCHER linux_volume_catcher.cpp) + # foreach( PULSE_FILE pulse/introspect.h pulse/context.h pulse/subscribe.h pulse/glib-mainloop.h ) + # find_path( PULSE_FILE_${PULSE_FILE}_FOUND ${PULSE_FILE} NO_CACHE) + # if( NOT PULSE_FILE_${PULSE_FILE}_FOUND ) + # message( "Looking for ${PULSE_FILE} ... not found") + # message( FATAL_ERROR "Pulse header not found" ) + # else() + # message( "Looking for ${PULSE_FILE} ... found") + # endif() + # endforeach() + + include(FindPipeWire) + include_directories(SYSTEM ${PIPEWIRE_INCLUDE_DIRS} ${SPA_INCLUDE_DIRS}) + + message( "Building with Linux volume catcher using PipeWire" ) + set(LINUX_VOLUME_CATCHER linux_volume_catcher_pw.cpp) list(APPEND media_plugin_cef_SOURCE_FILES ${LINUX_VOLUME_CATCHER}) set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--build-id -Wl,-rpath,'$ORIGIN:$ORIGIN/../../lib'") diff --git a/indra/media_plugins/cef/linux_volume_catcher.cpp b/indra/media_plugins/cef/linux_volume_catcher_pa.cpp index 9e6fe461a6..80f58954d4 100755 --- a/indra/media_plugins/cef/linux_volume_catcher.cpp +++ b/indra/media_plugins/cef/linux_volume_catcher_pa.cpp @@ -1,5 +1,5 @@ /** - * @file linux_volume_catcher.cpp + * @file linux_volume_catcher_pa.cpp * @brief A Linux-specific, PulseAudio-specific hack to detect and volume-adjust new audio sources * * @cond diff --git a/indra/media_plugins/cef/linux_volume_catcher_pw.cpp b/indra/media_plugins/cef/linux_volume_catcher_pw.cpp new file mode 100755 index 0000000000..b7171d10f9 --- /dev/null +++ b/indra/media_plugins/cef/linux_volume_catcher_pw.cpp @@ -0,0 +1,451 @@ +/** + * @file linux_volume_catcher_pw.cpp + * @brief A Linux-specific, PipeWire-specific hack to detect and volume-adjust new audio sources + * + * @cond + * $LicenseInfo:firstyear=2010&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + * @endcond + */ + +/* + The high-level design is as follows: + 1) Connect to the PipeWire daemon + 2) Find all existing and new audio nodes + 3) Examine PID and parent PID's to see if it belongs to our process + 4) If so, tell PipeWire to adjust the volume of that node + 5) Keep a list of all audio nodes and adjust when we setVolume() + */ + +#include "linden_common.h" + +#include "volume_catcher.h" + +#include <unordered_set> +#include <mutex> + +extern "C" { +#include <pipewire/pipewire.h> +#include <spa/pod/builder.h> +#include <spa/param/props.h> + +#include "apr_pools.h" +#include "apr_dso.h" +} + +#include "media_plugin_base.h" + +SymbolGrabber gSymbolGrabber; + +#include "linux_volume_catcher_pw_syms.inc" + +//////////////////////////////////////////////////// + +class VolumeCatcherImpl +{ +public: + VolumeCatcherImpl(); + ~VolumeCatcherImpl(); + + bool loadsyms(std::string pw_dso_name); + void init(); + void cleanup(); + + void pwLock(); + void pwUnlock(); + + void setVolume(F32 volume); + // void setPan(F32 pan); + + void handleRegistryEventGlobal( + uint32_t id, uint32_t permissions, const char* type, + uint32_t version, const struct spa_dict* props + ); + + class ChildNode + { + public: + bool mActive = false; + + pw_proxy* mProxy = nullptr; + spa_hook mNodeListener {}; + spa_hook mProxyListener {}; + VolumeCatcherImpl* mImpl = nullptr; + + void updateVolume(); + void destroy(); + }; + + bool mGotSyms = false; + + F32 mVolume = 1.0f; // max by default + // F32 mPan = 0.0f; // center + + pw_thread_loop* mThreadLoop; + pw_context* mContext; + pw_core* mCore; + pw_registry* mRegistry; + spa_hook mRegistryListener; + + std::unordered_set<ChildNode*> mChildNodes; + std::mutex mChildNodesMutex; +}; + +VolumeCatcherImpl::VolumeCatcherImpl() +{ + init(); +} + +VolumeCatcherImpl::~VolumeCatcherImpl() +{ + cleanup(); +} + +// static void debugClear() +// { +// auto file = fopen("/home/maki/git/firestorm-viewer/log.txt", "w"); +// fprintf(file, "\n"); +// fclose(file); +// } + +// static void debugPrint(const char* format, ...) +// { +// va_list args; +// va_start(args, format); +// auto file = fopen("/home/maki/git/firestorm-viewer/log.txt", "a"); +// vfprintf(file, format, args); +// fclose(file); +// va_end(args); +// } + +static void registryEventGlobal( + void *data, uint32_t id, uint32_t permissions, const char *type, + uint32_t version, const struct spa_dict *props +) +{ + static_cast<VolumeCatcherImpl*>(data)->handleRegistryEventGlobal( + id, permissions, type, version, props + ); +} + +static const struct pw_registry_events REGISTRY_EVENTS = { + .version = PW_VERSION_REGISTRY_EVENTS, + .global = registryEventGlobal, +}; + +bool VolumeCatcherImpl::loadsyms(std::string pw_dso_name) +{ + return gSymbolGrabber.grabSymbols({ pw_dso_name }); +} + +void VolumeCatcherImpl::init() +{ + // debugClear(); + // debugPrint("init\n"); + + mGotSyms = loadsyms("libpipewire-0.3.so.0"); + if (!mGotSyms) return; + + llpw_init(NULL, NULL); + + mThreadLoop = llpw_thread_loop_new("SL Plugin Volume Adjuster", NULL); + if (!mThreadLoop) return; + + pwLock(); + + mContext = llpw_context_new( + llpw_thread_loop_get_loop(mThreadLoop), NULL, 0 + ); + if (!mContext) return pwUnlock(); + + mCore = llpw_context_connect(mContext, NULL, 0); + if (!mCore) return pwUnlock(); + + mRegistry = pw_core_get_registry(mCore, PW_VERSION_REGISTRY, 0); + + // debugPrint("got registry\n"); + + spa_zero(mRegistryListener); + + pw_registry_add_listener( + mRegistry, &mRegistryListener, ®ISTRY_EVENTS, this + ); + + llpw_thread_loop_start(mThreadLoop); + + pwUnlock(); + + // debugPrint("started thread loop\n"); +} + +void VolumeCatcherImpl::cleanup() +{ + mChildNodesMutex.lock(); + for (auto* childNode : mChildNodes) { + childNode->destroy(); + } + mChildNodes.clear(); + mChildNodesMutex.unlock(); + + pwLock(); + if (mRegistry) llpw_proxy_destroy((struct pw_proxy*)mRegistry); + spa_zero(mRegistryListener); + if (mCore) llpw_core_disconnect(mCore); + if (mContext) llpw_context_destroy(mContext); + pwUnlock(); + + if (!mThreadLoop) return; + + llpw_thread_loop_stop(mThreadLoop); + llpw_thread_loop_destroy(mThreadLoop); + + // debugPrint("cleanup done\n"); +} + +void VolumeCatcherImpl::pwLock() { + if (!mThreadLoop) return; + llpw_thread_loop_lock(mThreadLoop); +} + +void VolumeCatcherImpl::pwUnlock() { + if (!mThreadLoop) return; + llpw_thread_loop_unlock(mThreadLoop); +} + +// #define INV_LERP(a, b, v) (v - a) / (b - a) + +// #include <sys/time.h> + +void VolumeCatcherImpl::ChildNode::updateVolume() +{ + if (!mActive) return; + + F32 volume = std::clamp(mImpl->mVolume, 0.0f, 1.0f); + // F32 pan = std::clamp(mImpl->mPan, -1.0f, 1.0f); + + // debugClear(); + // struct timeval time; + // gettimeofday(&time, NULL); + // double t = (double)time.tv_sec + (double)(time.tv_usec / 1000) / 1000; + // debugPrint("time is %f\n", t); + // F32 pan = std::sin(t * 2.0d); + // debugPrint("pan is %f\n", pan); + + // uint32_t channels = 2; + // float volumes[channels]; + // volumes[1] = INV_LERP(-1.0f, 0.0f, pan) * volume; // left + // volumes[0] = INV_LERP(1.0f, 0.0f, pan) * volume; // right + + uint32_t channels = 1; + float volumes[channels]; + volumes[0] = volume; + + uint8_t buffer[512]; + + spa_pod_builder builder; + spa_pod_builder_init(&builder, buffer, sizeof(buffer)); + + spa_pod_frame frame; + spa_pod_builder_push_object(&builder, &frame, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props); + spa_pod_builder_prop(&builder, SPA_PROP_channelVolumes, 0); + spa_pod_builder_array(&builder, sizeof(float), SPA_TYPE_Float, channels, volumes); + spa_pod* pod = static_cast<spa_pod*>(spa_pod_builder_pop(&builder, &frame)); + + mImpl->pwLock(); + pw_node_set_param(mProxy, SPA_PARAM_Props, 0, pod); + mImpl->pwUnlock(); +} + +void VolumeCatcherImpl::ChildNode::destroy() +{ + if (!mActive) return; + mActive = false; + + mImpl->mChildNodesMutex.lock(); + mImpl->mChildNodes.erase(this); + mImpl->mChildNodesMutex.unlock(); + + spa_hook_remove(&mNodeListener); + spa_hook_remove(&mProxyListener); + + mImpl->pwLock(); + llpw_proxy_destroy(mProxy); + mImpl->pwUnlock(); +} + +static pid_t getParentPid(pid_t aPid) +{ + std::stringstream strm; + strm << "/proc/" << aPid << "/status"; + std::ifstream in{ strm.str() }; + + if (!in.is_open()) return 0; + + pid_t res {0}; + while (!in.eof() && res == 0) + { + std::string line; + line.resize( 1024, 0 ); + in.getline( &line[0], line.length() ); + + auto i = line.find("PPid:"); + + if (i == std::string::npos) continue; + + char const *pIn = line.c_str() + 5; // Skip over pid; + while (*pIn != 0 && isspace( *pIn )) ++pIn; + + if (*pIn) res = atoll(pIn); + } + return res; +} + +static bool isPluginPid(pid_t aPid) +{ + auto myPid = getpid(); + + do + { + if(aPid == myPid) return true; + aPid = getParentPid( aPid ); + } while(aPid > 1); + + return false; +} + +static void nodeEventInfo(void* data, const struct pw_node_info* info) +{ + const char* processId = spa_dict_lookup(info->props, PW_KEY_APP_PROCESS_ID); + if (processId == nullptr) return; + + pid_t pid = atoll(processId); + if (!isPluginPid(pid)) return; + + // const char* appName = spa_dict_lookup(info->props, PW_KEY_APP_NAME); + // debugPrint("got app: %s\n", appName); + + auto* const childNode = static_cast<VolumeCatcherImpl::ChildNode*>(data); + // debugPrint("init volume to: %f\n", childNode->mImpl->mDesiredVolume); + + childNode->updateVolume(); + + childNode->mImpl->mChildNodesMutex.lock(); + childNode->mImpl->mChildNodes.insert(childNode); + childNode->mImpl->mChildNodesMutex.unlock(); +} + +static const struct pw_node_events NODE_EVENTS = { + .version = PW_VERSION_CLIENT_EVENTS, + .info = nodeEventInfo, +}; + +static void proxyEventDestroy(void* data) +{ + auto* const childNode = static_cast<VolumeCatcherImpl::ChildNode*>(data); + childNode->destroy(); +} + +static void proxyEventRemoved(void* data) +{ + auto* const childNode = static_cast<VolumeCatcherImpl::ChildNode*>(data); + childNode->destroy(); +} + +static const struct pw_proxy_events PROXY_EVENTS = { + .version = PW_VERSION_PROXY_EVENTS, + .destroy = proxyEventDestroy, + .removed = proxyEventRemoved, +}; + +void VolumeCatcherImpl::handleRegistryEventGlobal( + uint32_t id, uint32_t permissions, const char *type, uint32_t version, + const struct spa_dict *props +) { + if (props == nullptr || strcmp(type, PW_TYPE_INTERFACE_Node) != 0) return; + + const char* mediaClass = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); + if (mediaClass == nullptr || strcmp(mediaClass, "Stream/Output/Audio") != 0) return; + + pw_proxy* proxy = static_cast<pw_proxy*>( + pw_registry_bind(mRegistry, id, type, PW_VERSION_CLIENT, sizeof(ChildNode)) + ); + + auto* const childNode = static_cast<ChildNode*>(llpw_proxy_get_user_data(proxy)); + + childNode->mActive = true; + childNode->mProxy = proxy; + childNode->mImpl = this; + + pw_node_add_listener(proxy, &childNode->mNodeListener, &NODE_EVENTS, childNode); + llpw_proxy_add_listener(proxy, &childNode->mProxyListener, &PROXY_EVENTS, childNode); +} + +void VolumeCatcherImpl::setVolume(F32 volume) +{ + // debugPrint("setting all volume to: %f\n", volume); + + mVolume = volume; + + mChildNodesMutex.lock(); + std::unordered_set<ChildNode*> copyOfChildNodes(mChildNodes); + mChildNodesMutex.unlock(); + + // debugPrint("for %d nodes\n", copyOfChildNodes.size()); + + for (auto* childNode : copyOfChildNodes) { + childNode->updateVolume(); + } +} + +// void VolumeCatcherImpl::setPan(F32 pan) +// { +// mPan = pan; +// setVolume(mVolume); +// } + +///////////////////////////////////////////////////// + +VolumeCatcher::VolumeCatcher() +{ + pimpl = new VolumeCatcherImpl(); +} + +VolumeCatcher::~VolumeCatcher() +{ + delete pimpl; + pimpl = nullptr; +} + +void VolumeCatcher::setVolume(F32 volume) +{ + llassert(pimpl); + pimpl->setVolume(volume); +} + +void VolumeCatcher::setPan(F32 pan) +{ + // llassert(pimpl); + // pimpl->setPan(pan); +} + +void VolumeCatcher::pump() +{ +} diff --git a/indra/media_plugins/cef/linux_volume_catcher_pw_syms.inc b/indra/media_plugins/cef/linux_volume_catcher_pw_syms.inc new file mode 100644 index 0000000000..14e4a42f1d --- /dev/null +++ b/indra/media_plugins/cef/linux_volume_catcher_pw_syms.inc @@ -0,0 +1,22 @@ +// required symbols to grab +LL_GRAB_SYM(true, pw_init, void, int *argc, char **argv[]); +// LL_GRAB_SYM(true, pw_main_loop_new, struct pw_main_loop *, const struct spa_dict *props); +// LL_GRAB_SYM(true, pw_main_loop_get_loop, struct pw_loop *, struct pw_main_loop *loop); +// LL_GRAB_SYM(true, pw_main_loop_destroy, void, struct pw_main_loop *loop); +// LL_GRAB_SYM(true, pw_main_loop_run, void, struct pw_main_loop *loop); +LL_GRAB_SYM(true, pw_context_new, struct pw_context *, struct pw_loop *main_loop, struct pw_properties *props, size_t user_data_size); +LL_GRAB_SYM(true, pw_context_destroy, void, struct pw_context *context); +LL_GRAB_SYM(true, pw_context_connect, struct pw_core *, struct pw_context *context, struct pw_properties *properties, size_t user_data_size); +LL_GRAB_SYM(true, pw_thread_loop_new, struct pw_thread_loop *, const char *name, const struct spa_dict *props); +LL_GRAB_SYM(true, pw_thread_loop_destroy, void, struct pw_thread_loop *loop); +LL_GRAB_SYM(true, pw_thread_loop_get_loop, struct pw_loop *, struct pw_thread_loop *loop); +LL_GRAB_SYM(true, pw_thread_loop_start, int, struct pw_thread_loop *loop); +LL_GRAB_SYM(true, pw_thread_loop_stop, void, struct pw_thread_loop *loop); +LL_GRAB_SYM(true, pw_thread_loop_lock, void, struct pw_thread_loop *loop); +LL_GRAB_SYM(true, pw_thread_loop_unlock, void, struct pw_thread_loop *loop); +LL_GRAB_SYM(true, pw_proxy_add_listener, void, struct pw_proxy *proxy, struct spa_hook *listener, const struct pw_proxy_events *events, void *data); +LL_GRAB_SYM(true, pw_proxy_destroy, void, struct pw_proxy *proxy); +LL_GRAB_SYM(true, pw_proxy_get_user_data, void *, struct pw_proxy *proxy); +LL_GRAB_SYM(true, pw_core_disconnect, int, struct pw_core *core); + +// optional symbols to grab |