summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile12
-rw-r--r--README.md22
-rw-r--r--main.cpp396
3 files changed, 430 insertions, 0 deletions
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 <stdio.h>
+#include <assert.h>
+#include <signal.h>
+#include <string.h>
+#include <pulse/pulseaudio.h>
+#include <dbus/dbus.h>
+
+#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;
+}