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 --- pavmon.cc | 184 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 pavmon.cc (limited to 'pavmon.cc') diff --git a/pavmon.cc b/pavmon.cc new file mode 100644 index 0000000..df639e4 --- /dev/null +++ b/pavmon.cc @@ -0,0 +1,184 @@ +// 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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "pulse.h" + +namespace { + +constexpr std::string_view kShortUsage = "Usage: pavmon SINK_NAME\n"; + +constexpr std::string_view kHelp = R"( +Monitor the volume level of a PulseAudio sink, printing a new line each time it +changes. + +Options: + --help display this help and exit + --version display version information and exit +)"; + +constexpr std::string_view kAskForHelp = + "Try \"pavmon --help\" for more information.\n"; + +constexpr std::string_view kVersionInfo = R"(pavmon 1.0.0 +Copyright 2022 Benjamin Barenblat +Licensed under the Apache License, Version 2.0 +)"; + +enum { + kHelpLongOption = 128, + kVersionLongOption, +}; + +void GetAndPrintVolume(const std::string& sink_name, pulse::PollMainLoop& loop, + pulse::Context& ctx) noexcept { + ctx.GetSinkInfo( + sink_name.c_str(), + [&]() noexcept { + std::clog << "pavmon: failed to get volume" << std::endl; + loop.Quit(1); + }, + [&](const pa_sink_info& info) noexcept { + if (info.mute) { + std::cout << "muted ("; + } + std::cout << std::fixed << std::setprecision(1) + << pa_sw_volume_to_dB(pa_cvolume_avg(&info.volume)) << " dB"; + if (info.mute) { + std::cout << ')'; + } + std::cout << std::endl; + }); +} + +void SubscribeToEvents(const std::string& sink_name, pulse::PollMainLoop& loop, + pulse::Context& ctx) noexcept { + ctx.Subscribe( + PA_SUBSCRIPTION_MASK_SINK, + [&]() noexcept { + std::clog << "pavmon: failed to subscribe to events" << std::endl; + loop.Quit(1); + }, + [&](pa_subscription_event_type_t) noexcept { + GetAndPrintVolume(sink_name, loop, ctx); + }); +} + +} // namespace + +int main(int argc, char* argv[]) { + std::locale loc(""); + std::locale::global(loc); + std::cin.imbue(loc); + std::cout.imbue(loc); + std::cerr.imbue(loc); + std::clog.imbue(loc); + std::wcin.imbue(loc); + std::wcout.imbue(loc); + std::wcerr.imbue(loc); + std::wclog.imbue(loc); + + std::ios_base::sync_with_stdio(false); + + static option long_options[] = { + {"help", no_argument, nullptr, kHelpLongOption}, + {"version", no_argument, nullptr, kVersionLongOption}, + {nullptr, 0, nullptr, 0}, + }; + while (true) { + int c = getopt_long(argc, argv, "", long_options, /*longindex=*/nullptr); + if (c == -1) { + break; + } + switch (c) { + case kHelpLongOption: + std::cout << kShortUsage << kHelp; + return 0; + case kVersionLongOption: + std::cout << kVersionInfo; + return 0; + case '?': + std::clog << kAskForHelp; + return 1; + default: + std::clog << "Internal error; please report.\n"; + return 1; + } + } + if (optind != argc - 1) { + std::clog << kShortUsage << kAskForHelp; + return 1; + } + + std::string sink_name = argv[1]; + + pulse::PollMainLoop loop; + + loop.HandleSignals(); + auto quit_on_signal = [&]() noexcept { loop.Quit(0); }; + pulse::SignalEventSource quit_on_sigterm(SIGTERM, quit_on_signal); + pulse::SignalEventSource quit_on_sigint(SIGINT, quit_on_signal); + + pulse::PropertyList proplist; + proplist.Set(PA_PROP_APPLICATION_NAME, "PulseAudio volume monitor"); + proplist.Set(PA_PROP_APPLICATION_ID, "pavmon"); + proplist.Set(PA_PROP_APPLICATION_VERSION, "1.0.0"); + proplist.Set(PA_PROP_APPLICATION_PROCESS_ID, std::to_string(getpid())); + if (const char* user = getlogin(); user != nullptr) { + proplist.Set(PA_PROP_APPLICATION_PROCESS_USER, user); + } + // TODO(bbarenblat@gmail.com): More of these? + + pulse::Context ctx("PulseAudio volume meter", proplist, loop); + ctx.Connect(PA_CONTEXT_NOFAIL, [&](pa_context_state_t state) noexcept { + switch (state) { + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + + case PA_CONTEXT_READY: + GetAndPrintVolume(sink_name, loop, ctx); + SubscribeToEvents(sink_name, loop, ctx); + break; + + case PA_CONTEXT_TERMINATED: + loop.Quit(0); + break; + + case PA_CONTEXT_FAILED: + default: + std::clog << "pavmon: failed to connect to PulseAudio" << std::endl; + loop.Quit(1); + break; + } + }); + + return loop.Run(); +} -- cgit v1.2.3