summaryrefslogtreecommitdiff
path: root/indra/media_plugins/cef/linux/volume_catcher_pipewire.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'indra/media_plugins/cef/linux/volume_catcher_pipewire.cpp')
-rwxr-xr-xindra/media_plugins/cef/linux/volume_catcher_pipewire.cpp336
1 files changed, 336 insertions, 0 deletions
diff --git a/indra/media_plugins/cef/linux/volume_catcher_pipewire.cpp b/indra/media_plugins/cef/linux/volume_catcher_pipewire.cpp
new file mode 100755
index 0000000000..a779cf868e
--- /dev/null
+++ b/indra/media_plugins/cef/linux/volume_catcher_pipewire.cpp
@@ -0,0 +1,336 @@
+/**
+ * @file volume_catcher_pipewire.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_linux.h"
+
+extern "C" {
+#include <spa/pod/builder.h>
+#include <spa/param/props.h>
+}
+
+SymbolGrabber pwSymbolGrabber;
+#undef LL_SYMBOL_GRABBER
+#define LL_SYMBOL_GRABBER pwSymbolGrabber
+
+#include "volume_catcher_pipewire_syms.inc"
+
+////////////////////////////////////////////////////
+
+VolumeCatcherPipeWire::VolumeCatcherPipeWire()
+{
+ init();
+}
+
+VolumeCatcherPipeWire::~VolumeCatcherPipeWire()
+{
+ cleanup();
+}
+
+static void registryEventGlobal(
+ void *data, uint32_t id, uint32_t permissions, const char *type,
+ uint32_t version, const struct spa_dict *props
+)
+{
+ static_cast<VolumeCatcherPipeWire*>(data)->handleRegistryEventGlobal(
+ id, permissions, type, version, props
+ );
+}
+
+static const struct pw_registry_events REGISTRY_EVENTS = {
+ .version = PW_VERSION_REGISTRY_EVENTS,
+ .global = registryEventGlobal,
+};
+
+bool VolumeCatcherPipeWire::loadsyms(std::string pw_dso_name)
+{
+ return pwSymbolGrabber.grabSymbols({ pw_dso_name });
+}
+
+void VolumeCatcherPipeWire::init()
+{
+ // debugPrint("init\n");
+
+ mGotSyms = loadsyms("libpipewire-0.3.so.0");
+
+ if (!mGotSyms)
+ return;
+
+ // debugPrint("got syms\n");
+
+ 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, &REGISTRY_EVENTS, this
+ );
+
+ llpw_thread_loop_start(mThreadLoop);
+
+ pwUnlock();
+
+ // debugPrint("started thread loop\n");
+}
+
+void VolumeCatcherPipeWire::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 VolumeCatcherPipeWire::pwLock() {
+ if (!mThreadLoop)
+ return;
+
+ llpw_thread_loop_lock(mThreadLoop);
+}
+
+void VolumeCatcherPipeWire::pwUnlock() {
+ if (!mThreadLoop)
+ return;
+
+ llpw_thread_loop_unlock(mThreadLoop);
+}
+
+// #define INV_LERP(a, b, v) (v - a) / (b - a)
+
+// #include <sys/time.h>
+
+void VolumeCatcherPipeWire::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 VolumeCatcherPipeWire::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 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<VolumeCatcherPipeWire::ChildNode*>(data);
+ // debugPrint("init volume to: %f\n", childNode->mImpl->mVolume);
+
+ 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<VolumeCatcherPipeWire::ChildNode*>(data);
+ childNode->destroy();
+}
+
+static void proxyEventRemoved(void* data)
+{
+ auto* const childNode = static_cast<VolumeCatcherPipeWire::ChildNode*>(data);
+ childNode->destroy();
+}
+
+static const struct pw_proxy_events PROXY_EVENTS = {
+ .version = PW_VERSION_PROXY_EVENTS,
+ .destroy = proxyEventDestroy,
+ .removed = proxyEventRemoved,
+};
+
+void VolumeCatcherPipeWire::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 VolumeCatcherPipeWire::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 VolumeCatcherPipeWire::setPan(F32 pan)
+{
+ // mPan = pan;
+ // setVolume(mVolume);
+}
+
+void VolumeCatcherPipeWire::pump()
+{
+} \ No newline at end of file