From c75c2cbeb5e3459165ee5a97c450f8fffc27176e Mon Sep 17 00:00:00 2001 From: ch1p Date: Thu, 14 Dec 2017 16:28:14 +0300 Subject: initial commit --- Makefile | 12 ++ README.md | 22 ++++ main.cpp | 396 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 430 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 main.cpp diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5240a8e --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +CC=g++ +LIBS=libpulse dbus-1 +CFLAGS=-std=c++11 -Wall -pedantic -Wextra `pkg-config --cflags ${LIBS}` +LDFLAGS=`pkg-config --libs ${LIBS}` + +all: main + +main: main.o + ${CC} main.o -o pulse-volume-monitor ${LDFLAGS} + +main.o: + ${CC} ${CFLAGS} -c main.cpp diff --git a/README.md b/README.md new file mode 100644 index 0000000..9311a6b --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# pulse-volume-monitor + +`pulse-volume-monitor` is a simple C++ program that listens to PulseAudio's sink and source events and emits DBus signals `sinkChanged` and `sourceChanged` when something has been changed (volume, sink or source muted, etc). + +For use with AwesomeWM in volume indicator widget. + +### Usage +`./pulse-volume-monitor dbus` or `./pulse-volume-monitor stdout` (for debugging) + +### AwesomeWM LUA example + +``` +dbus.request_name("session", "com.ch1p.pvm") +dbus.add_match("session", "interface='com.ch1p.pvm',member='sinkChanged'") +dbus.add_match("session", "interface='com.ch1p.pvm',member='sourceChanged'") +dbus.connect_signal("com.ch1p.pvm", + function(info) + -- info.member is "sinkChanged" or "sourceChanged" + -- your code here + end +) +``` diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..c786b51 --- /dev/null +++ b/main.cpp @@ -0,0 +1,396 @@ +/** + * Author: Jason White (https://gist.github.com/jasonwhite/1df6ee4b5039358701d2) + * Author: ch1p + * + * License: Public Domain + */ +#include +#include +#include +#include +#include +#include + +#define DBUS_NAME "com.ch1p.pvm" + + +class DBus +{ +private: + DBusConnection *_connection; + DBusError _error; + +public: + DBus() + : _connection(NULL) + { + } + + ~DBus() + { + if (_connection) { + dbus_connection_close(_connection); + } + } + + bool initialize() + { + dbus_error_init(&_error); + _connection = dbus_bus_get(DBUS_BUS_SESSION, &_error); + + if (dbus_error_is_set(&_error)) { + fprintf(stderr, "DBus Connection Error (%s)\n", _error.message); + dbus_error_free(&_error); + } + + if (!_connection) { + fprintf(stderr, "Failed to initialize dbus connection\n"); + return false; + } + + dbus_bus_request_name(_connection, DBUS_NAME, + DBUS_NAME_FLAG_REPLACE_EXISTING , &_error); + if (dbus_error_is_set(&_error)) { + fprintf(stderr, "DBus Name Error (%s)\n", _error.message); + dbus_error_free(&_error); + return false; + } + + return true; + } + + bool notify(char *signal_name) + { + DBusMessage *msg; + dbus_uint32_t serial = 0; + + //msg = dbus_message_new_signal("/com/ch1p/Object", DBUS_NAME, "valueChanged"); + msg = dbus_message_new_signal("/com/ch1p/Object", DBUS_NAME, signal_name); + if (NULL == msg) { + fprintf(stderr, "DBus: Message Null\n"); + return false; + } + + if (!dbus_connection_send(_connection, msg, &serial)) { + fprintf(stderr, "DBus send: Out Of Memory!\n"); + return false; + } + dbus_connection_flush(_connection); + + dbus_message_unref(msg); + return true; + } +}; + + +struct pa_myuserdata { + bool use_dbus; + pa_mainloop_api *mainloop_api; + DBus *dbus; +}; + + +class PulseAudio +{ +private: + pa_mainloop* _mainloop; + pa_mainloop_api* _mainloop_api; + pa_context* _context; + pa_signal_event* _signal; + DBus _dbus; + bool _use_dbus; + struct pa_myuserdata _myuserdata; + +public: + PulseAudio() + : _mainloop(NULL), + _mainloop_api(NULL), + _context(NULL), + _signal(NULL), + _use_dbus(false) + { + } + + /** + * Initializes state and connects to the PulseAudio server. + */ + bool initialize(bool use_dbus) + { + _mainloop = pa_mainloop_new(); + if (!_mainloop) + { + fprintf(stderr, "pa_mainloop_new() failed.\n"); + return false; + } + + _mainloop_api = pa_mainloop_get_api(_mainloop); + + if (pa_signal_init(_mainloop_api) != 0) + { + fprintf(stderr, "pa_signal_init() failed\n"); + return false; + } + + _signal = pa_signal_new(SIGINT, exit_signal_callback, this); + if (!_signal) + { + fprintf(stderr, "pa_signal_new() failed\n"); + return false; + } + signal(SIGPIPE, SIG_IGN); + + _context = pa_context_new(_mainloop_api, "PulseAudio Test"); + if (!_context) + { + fprintf(stderr, "pa_context_new() failed\n"); + return false; + } + + if (pa_context_connect(_context, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL) < 0) + { + fprintf(stderr, "pa_context_connect() failed: %s\n", pa_strerror(pa_context_errno(_context))); + return false; + } + + if (use_dbus) { + _use_dbus = true; + if (!_dbus.initialize()) { + return false; + } + } + + _myuserdata.use_dbus = _use_dbus; + _myuserdata.mainloop_api = _mainloop_api; + _myuserdata.dbus = &_dbus; + + pa_context_set_state_callback(_context, context_state_callback, &_myuserdata); + + return true; + } + + /** + * Runs the main PulseAudio event loop. Calling quit will cause the event + * loop to exit. + */ + int run() + { + int ret = 1; + if (pa_mainloop_run(_mainloop, &ret) < 0) + { + fprintf(stderr, "pa_mainloop_run() failed.\n"); + return ret; + } + + return ret; + } + + /** + * Exits the main loop with the specified return code. + */ + void quit(int ret = 0) + { + _mainloop_api->quit(_mainloop_api, ret); + } + + /** + * Called when the PulseAudio system is to be destroyed. + */ + void destroy() + { + if (_context) + { + pa_context_unref(_context); + _context = NULL; + } + + if (_signal) + { + pa_signal_free(_signal); + pa_signal_done(); + _signal = NULL; + } + + if (_mainloop) + { + pa_mainloop_free(_mainloop); + _mainloop = NULL; + _mainloop_api = NULL; + } + } + + ~PulseAudio() + { + destroy(); + } + +private: + + /* + * Called on SIGINT. + */ + static void exit_signal_callback(pa_mainloop_api *m, pa_signal_event *e, int sig, void *userdata) + { + PulseAudio* pa = (PulseAudio*)userdata; + if (pa) pa->quit(); + } + + /* + * Called whenever the context status changes. + */ + static void context_state_callback(pa_context *c, void *userdata) + { + struct pa_myuserdata *myuserdata = (struct pa_myuserdata *)userdata; + + assert(c && myuserdata->mainloop_api); + + PulseAudio* pa = (PulseAudio*)(myuserdata->mainloop_api); + + switch (pa_context_get_state(c)) + { + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + + case PA_CONTEXT_READY: + fprintf(stderr, "PulseAudio connection established.\n"); + pa_context_get_server_info(c, server_info_callback, userdata); + + // Subscribe to sink events from the server. This is how we get + // volume change notifications from the server. + pa_context_set_subscribe_callback(c, subscribe_callback, userdata); + pa_context_subscribe(c, (pa_subscription_mask_t)(PA_SUBSCRIPTION_MASK_SINK|PA_SUBSCRIPTION_MASK_SOURCE), NULL, NULL); + break; + + case PA_CONTEXT_TERMINATED: + pa->quit(0); + fprintf(stderr, "PulseAudio connection terminated.\n"); + break; + + case PA_CONTEXT_FAILED: + default: + fprintf(stderr, "Connection failure: %s\n", pa_strerror(pa_context_errno(c))); + pa->quit(1); + break; + } + } + + /* + * Called when an event we subscribed to occurs. + */ + static void subscribe_callback(pa_context *c, + pa_subscription_event_type_t type, uint32_t idx, void *userdata) + { + unsigned facility = type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; + //type &= PA_SUBSCRIPTION_EVENT_TYPE_MASK; + + pa_operation *op = NULL; + + switch (facility) + { + case PA_SUBSCRIPTION_EVENT_SINK: + pa_context_get_sink_info_by_index(c, idx, sink_info_callback, userdata); + break; + + case PA_SUBSCRIPTION_EVENT_SOURCE: + pa_context_get_source_info_by_index(c, idx, source_info_callback, userdata); + break; + + default: + printf("Unknown event %d\n", facility); + //assert(0); // Got event we aren't expecting. + break; + } + + if (op) + pa_operation_unref(op); + } + + /* + * Called when the requested sink information is ready. + */ + static void sink_info_callback(pa_context *c, const pa_sink_info *i, + int eol, void *userdata) + { + if (!i) { + return; + } + + struct pa_myuserdata *myuserdata = (struct pa_myuserdata *)userdata; + if (myuserdata->use_dbus) { + myuserdata->dbus->notify("sinkChanged"); + } else { + float volume = (float)pa_cvolume_avg(&(i->volume)) / (float)PA_VOLUME_NORM; + printf("[sink ] percent volume = %.0f%%%s\n", volume * 100.0f, i->mute ? " (muted)" : ""); + } + } + + /* + * Called when the requested source information is ready. + */ + static void source_info_callback(pa_context *c, const pa_source_info *i, + int eol, void *userdata) + { + if (!i) { + return; + } + + struct pa_myuserdata *myuserdata = (struct pa_myuserdata *)userdata; + if (myuserdata->use_dbus) { + myuserdata->dbus->notify("sourceChanged"); + } else { + float volume = (float)pa_cvolume_avg(&(i->volume)) / (float)PA_VOLUME_NORM; + printf("[source] percent volume = %.0f%%%s\n", volume * 100.0f, i->mute ? " (muted)" : ""); + } + } + + /* + * Called when the requested information on the server is ready. This is + * used to find the default PulseAudio sink. + */ + static void server_info_callback(pa_context *c, const pa_server_info *i, + void *userdata) + { + printf("[info ] default sink name = %s\n", i->default_sink_name); + printf("[info ] default source name = %s\n", i->default_source_name); + pa_context_get_sink_info_by_name(c, i->default_sink_name, sink_info_callback, userdata); + pa_context_get_source_info_by_name(c, i->default_source_name, source_info_callback, userdata); + } +}; + + + +void usage(char *name) +{ + fprintf(stderr, "Usage:\n" + "%s dbus\n" + "%s stdout\n", + name, name); + + exit(1); +} + + +int main(int argc, char *argv[]) +{ + if (argc < 2) { + usage(argv[0]); + } + + // Check input + bool use_dbus = false; + if (strcmp(argv[1], "dbus") == 0) { + use_dbus = true; + } else if (strcmp(argv[1], "stdout") != 0) { + usage(argv[0]); + } + + PulseAudio pa = PulseAudio(); + if (!pa.initialize(use_dbus)) { + return 1; + } + + int ret = pa.run(); + + return ret; +} -- cgit v1.2.3