From 61c9951dae64e79cbc1983098b5fd52317578103 Mon Sep 17 00:00:00 2001 From: Benjamin Barenblat Date: Thu, 27 Jan 2022 11:10:37 -0500 Subject: pavmon, a simple tool to monitor the volume of a PulseAudio sink --- pulse.cc | 360 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 pulse.cc (limited to 'pulse.cc') diff --git a/pulse.cc b/pulse.cc new file mode 100644 index 0000000..ebe72b9 --- /dev/null +++ b/pulse.cc @@ -0,0 +1,360 @@ +// Copyright 2022 Benjamin Barenblat +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +#include "pulse.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace pulse { + +// Adapters for libpulse callbacks. +// +// libpulse presents a callback-driven C API--callbacks must be C function +// pointers, and each callback can be passed a user-controlled void*. This isn't +// a great API for a C++ library, though, so this library accepts std::functions +// and pass them by pointer to statically defined callbacks. +// +// Even though these functions are defined in an anonymous namespace, the extern +// "C" means they need "static" to get the desired internal linkage. + +namespace { + +using SignalCallback = std::function; + +extern "C" { + +static void CallSignalCallback(pa_mainloop_api*, pa_signal_event*, + int /*signal*/, void* real_callback) noexcept { + assert(real_callback != nullptr); + (*reinterpret_cast(real_callback))(); +} + +static void DeleteSignalCallback(pa_mainloop_api*, pa_signal_event*, + void* real_callback) noexcept { + assert(real_callback != nullptr); + delete reinterpret_cast(real_callback); +} + +} // extern "C" + +using ContextNotifyCallback = std::function; + +extern "C" { + +static void CallContextNotifyCallback(pa_context* ctx, + void* real_callback) noexcept { + assert(real_callback != nullptr); + (*reinterpret_cast(real_callback))( + pa_context_get_state(ctx)); +} + +} // extern "C" + +struct SinkInfoCallbacks { + std::function on_failure; + std::function on_data; +}; + +extern "C" { + +static void CallOrDeleteSinkInfoCallbacks(pa_context*, const pa_sink_info* info, + int status, + void* real_callbacks) noexcept { + assert(real_callbacks != nullptr); + auto* sink_info_callbacks = + reinterpret_cast(real_callbacks); + + if (status == 0) { + // The RPC succeeded, and info contains sink information. Tell the user. + assert(info != nullptr); + sink_info_callbacks->on_data(*info); + } else { + // No sink information came through... + if (status < 0) { + // ...because of an error. Tell the user. + sink_info_callbacks->on_failure(); + } else { + // ...because no more data is available from the PulseAudio server. The + // user doesn't need to hear about that. + } + + // In any case, no more data are coming, so don't hang onto the callbacks + // anymore. + delete sink_info_callbacks; + } +} + +} // extern "C" + +struct RpcResultCallbacks { + std::function on_failure; + std::function on_success; +}; + +extern "C" { + +static void CallAndDeleteRpcResultCallbacks(pa_context*, int success, + void* real_callbacks) noexcept { + assert(real_callbacks != nullptr); + auto* rpc_result_callbacks = + reinterpret_cast(real_callbacks); + if (success) { + rpc_result_callbacks->on_success(); + } else { + rpc_result_callbacks->on_failure(); + } + delete rpc_result_callbacks; +} + +} // extern "C" + +using SubscribeCallback = std::function; + +extern "C" { + +static void CallSubscribeCallback(pa_context*, + pa_subscription_event_type_t type, + uint32_t /*index*/, + void* real_callback) noexcept { + assert(real_callback != nullptr); + (*reinterpret_cast(real_callback))(type); +} + +} // extern "C" + +} // namespace + +PropertyList::PropertyList() : list_(pa_proplist_new()) { + if (list_ == nullptr) { + throw Error::InFunction("pa_proplist_new"); + } +} + +PropertyList::PropertyList(const PropertyList& other) + : list_(pa_proplist_copy(other.list_)) { + if (list_ == nullptr) { + throw Error::InFunction("pa_proplist_copy"); + } +} + +PropertyList& PropertyList::operator=(const PropertyList& other) { + if (this != &other) { + PropertyList other2(other); + swap(*this, other2); + } + return *this; +} + +PropertyList::~PropertyList() noexcept { + if (list_ != nullptr) { + pa_proplist_free(list_); + } +} + +void PropertyList::Set(const char key[], std::string_view value) { + if (pa_proplist_set(list_, key, value.data(), value.size())) { + throw Error::InFunction("pa_proplist_set"); + } +} + +PollMainLoop::PollMainLoop() + : loop_(pa_mainloop_new()), handling_signals_(false) { + if (loop_ == nullptr) { + throw Error::InFunction("pa_mainloop_new"); + } + api_ = pa_mainloop_get_api(loop_); +} + +PollMainLoop::~PollMainLoop() noexcept { + if (handling_signals_) { + StopHandlingSignals(); + } + if (loop_ != nullptr) { + pa_mainloop_free(loop_); + } +} + +void PollMainLoop::HandleSignals() { + if (pa_signal_init(api_)) { + throw Error::InFunction("pa_signal_init"); + } + handling_signals_ = true; +} + +int PollMainLoop::Run() { + int r; + if (pa_mainloop_run(loop_, &r) < 0) { + throw Error::InFunction("pa_mainloop_run"); + } + return r; +} + +SignalEventSource::SignalEventSource(int signal, + std::function real_callback) { + // Construct source_ using CallSignalCallback as the C callback and + // real_callback as the real callback. real_callback is going to get passed by + // pointer, so that address needs to continue to point to real_callback until + // pa_signal_free. Happily, libpulse provides a mechanism to run an arbitrary + // callback during pa_signal_free, so move the callback onto the heap and + // delete it during pa_signal_free. This function is still responsible for + // deleting it if pa_signal_new fails, though, so stash it inside a unique_ptr + // and release the unique_ptr if pa_signal_new succeeds. + auto heap_callback = + std::make_unique(std::move(real_callback)); + source_ = pa_signal_new(signal, &CallSignalCallback, heap_callback.get()); + if (source_ == nullptr) { + throw Error::InFunction("pa_signal_new"); + } + pa_signal_set_destroy(source_, &DeleteSignalCallback); + heap_callback.release(); +} + +SignalEventSource::~SignalEventSource() noexcept { + if (source_ != nullptr) { + pa_signal_free(source_); + } +} + +Context::Context(const char application_name[], PropertyList& proplist, + PollMainLoop& loop) + : ctx_(pa_context_new_with_proplist(pa_mainloop_get_api(loop.get()), + application_name, proplist.get())) { + if (ctx_ == nullptr) { + throw Error::InFunction("pa_context_new_with_proplist"); + } +} + +Context::~Context() noexcept { + if (state_callback_ != nullptr) { + Disconnect(); + } + if (ctx_ != nullptr) { + pa_context_unref(ctx_); + } +} + +void Context::Connect( + pa_context_flags_t flags, + std::function on_state_change) noexcept { + // Set Up CallContextNotifyCallback to call on_state_change. on_state_change + // is going to get passed by pointer, so its address needs to continue to + // point to on_state_change until pa_context_disconnect. Saving the callback + // as a member ensures it stays alive long enough, but it doesn't ensure + // pointer stability; to get that, save it on the heap and keep a unique_ptr + // to it as a member. + state_callback_ = + std::make_unique(std::move(on_state_change)); + pa_context_set_state_callback(ctx_, &CallContextNotifyCallback, + state_callback_.get()); + if (pa_context_connect(ctx_, /*server=*/nullptr, flags, + /*spawn_api=*/nullptr)) { + // The RPC never even went out. libpulse won't call the callback in this + // case, so do it manually. + (*state_callback_)(PA_CONTEXT_FAILED); + + // The callback will never be called again at this point, and hanging onto + // it could cause resource leakage (e.g., if it captured a std::shared_ptr). + // Get rid of it. + state_callback_ = nullptr; + } +} + +void Context::GetSinkInfo( + const char name[], std::function on_failure, + std::function on_data) const noexcept { + // Set up CallOrDeleteSinkInfoCallbacks to call on_failure or on_data as + // appropriate. These callbacks are going to get passed by pointer, so they + // need to live on the heap. If constructing the get_sink_info_by_name RPC + // fails, though, libpulse won't call on_failure; to allow calling it + // manually, stash the callbacks inside a unique_ptr and release the + // unique_ptr if the RPC actually gets dispatched. + auto callbacks = std::make_unique(); + callbacks->on_failure = std::move(on_failure); + callbacks->on_data = std::move(on_data); + pa_operation* op = pa_context_get_sink_info_by_name( + ctx_, name, &CallOrDeleteSinkInfoCallbacks, callbacks.get()); + if (op == nullptr) { + // The RPC never even went out. libpulse won't call on_failure in this case, + // so do it manually. + callbacks->on_failure(); + } else { + pa_operation_unref(op); // The RPC handle is no longer needed. + + // The RPC is running, which means CallOrDeleteSinkInfoCallbacks will be + // called. (In fact, it's possible that CallOrDeleteSinkInfoCallbacks has + // already been called and the callbacks struct has already been deleted!) + // Stop managing the callbacks struct. + callbacks.release(); + } +} + +void Context::Subscribe( + pa_subscription_mask_t desired_events, std::function on_failure, + std::function on_event) const noexcept { + // Set up CallSubscribeCallback to call on_event. This callback is going to + // get passed by pointer, so its address needs to continue to point to + // on_event until pa_context_disconnect. Saving the callback as a member + // ensures it stays alive long enough, but it doesn't ensure pointer + // stability; to get that, save it on the heap and keep a unique_ptr to it as + // a member. + subscribe_callback_ = + std::make_unique(std::move(on_event)); + pa_context_set_subscribe_callback(ctx_, &CallSubscribeCallback, + subscribe_callback_.get()); + + // Set up CallAndDeleteRpcResultCallbacks to handle the result of the + // subscribe RPC. The real callbacks struct is going to get passed by pointer, + // so it needs to live on the heap. If constructing the subscribe RPC fails, + // though, libpulse won't call on_failure; to allow calling it manually, stash + // the callbacks inside a unique_ptr and release the unique_ptr if the RPC + // actually gets dispatched. + auto subscribe_completion_callbacks = std::make_unique(); + subscribe_completion_callbacks->on_failure = std::move(on_failure); + // We don't need to tell the user the RPC succeded; the event callback can + // tell them that. + subscribe_completion_callbacks->on_success = []() noexcept {}; + pa_operation* op = pa_context_subscribe(ctx_, desired_events, + &CallAndDeleteRpcResultCallbacks, + subscribe_completion_callbacks.get()); + if (op == nullptr) { + // The RPC never even went out. libpulse won't call + // CallAndDeleteRpcResultCallbacks in this case, call on_failure manually. + subscribe_completion_callbacks->on_failure(); + } else { + pa_operation_unref(op); // The RPC handle is no longer needed. + + // The RPC is running, which means CallAndDeleteRpcResultCallbacks will be + // called. (In fact, it's possible that CallAndDeleteRpcResultCallbacks has + // already been called and the result callbacks struct has already been + // deleted!) Stop managing the result callbacks struct. + subscribe_completion_callbacks.release(); + } +} + +} // namespace pulse -- cgit v1.2.3