aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDave Reisner <dreisner@archlinux.org>2013-01-03 15:22:14 -0500
committerDave Reisner <dreisner@archlinux.org>2013-01-03 15:22:14 -0500
commit648525dc88eb37f102ae588ad152b1ff7c18b9c8 (patch)
tree05624fbe8f928194e81a7f0fb88fb04ef13122bd
parentd5eeab41f026d97b25efe00e3df9d7ee75e8d04a (diff)
parent270910b82218a840b99b72859b308451c73c5c1b (diff)
downloadmirror-ponymix-648525dc88eb37f102ae588ad152b1ff7c18b9c8.tar.gz
mirror-ponymix-648525dc88eb37f102ae588ad152b1ff7c18b9c8.tar.bz2
mirror-ponymix-648525dc88eb37f102ae588ad152b1ff7c18b9c8.zip
Merge remote-tracking branch 'pwnymix/master'
* pwnymix/master: (25 commits) show description in list-short properly clamp values in Set{Balance,Volume} resolve the card choice as late as possible allow card lookup by index Cleanup manpage, reintroduce application commands section add manpage reorg usage output remove defunct enum implement -short variations of list verbs A dash of color... fix abort on set-profile after removing profile whitespace police remove string_to_device; PulseClient can do this now always defined behaviors for devices Derive the card from the targetted device ponymix: validate arg count before invoking function rename class Pulse to PulseClient makefile: CFLAGS -> CXXFLAGS eschew _unused_ attribute alias sane types to --device flags ... Conflicts: Makefile ponymix.1
-rw-r--r--Makefile29
-rw-r--r--ponymix.1116
-rw-r--r--ponymix.cc642
-rw-r--r--pulse.cc628
-rw-r--r--pulse.h243
5 files changed, 1605 insertions, 53 deletions
diff --git a/Makefile b/Makefile
index a6b89f2..b41b05c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,15 +1,28 @@
-CC = gcc -std=gnu99
-CFLAGS := -Wall -Wextra -pedantic -O2 -g -D_REENTRANT $(CFLAGS)
-LDLIBS := -lpulse -lm
+CXX = g++ -std=c++11
-ponymix: ponymix.o
+base_CXXFLAGS = -Wall -Wextra -pedantic -O2 -g
+base_LIBS = -lm
+
+libpulse_CXXFLAGS = $(shell pkg-config --cflags libpulse)
+libpulse_LIBS = $(shell pkg-config --libs libpulse)
+
+CXXFLAGS := \
+ $(base_CXXFLAGS) \
+ $(libpulse_CXXFLAGS) \
+ $(CXXFLAGS)
+
+LDLIBS := \
+ $(base_LIBS) \
+ $(libpulse_LIBS)
+
+all: ponymix
+
+ponymix: ponymix.cc pulse.o
+pulse.o: pulse.cc pulse.h
install: ponymix
install -Dm755 ponymix $(DESTDIR)/usr/bin/ponymix
install -Dm644 ponymix.1 $(DESTDIR)/usr/share/man/man1/ponymix.1
-check: ponymix
- ./runtests ./ponymix
-
clean:
- $(RM) ponymix ponymix.o
+ $(RM) ponymix pulse.o
diff --git a/ponymix.1 b/ponymix.1
index ad709b4..a396db9 100644
--- a/ponymix.1
+++ b/ponymix.1
@@ -1,4 +1,4 @@
-.TH ponymix "1" "August 12" "ponymix" "User Commands"
+.TH ponymix "1" "2013-01-02" "ponymix" "User Commands"
.SH NAME
ponymix \- cli volume control for PulseAudio
.SH SYNOPSIS
@@ -8,65 +8,91 @@ ponymix \- cli volume control for PulseAudio
perform many operations on both device and application sinks and source.
.SH OPTIONS
.PP
-.IP "\fB\-d\fR, \fB\-\-device\fR"
-Control input and output devices.
-.IP "\fB\-a\fR, \fB\-\-app\fR"
-Control the playback and recording streams.
-.IP "\fB\-o\fR[\fB=\fR\fISINK\fR]\fR, \fB\-\-sink\fR[\fB=\fR\fISINK\fR], \fB\-\-output\fR[\fB=\fR\fISINK\fR]"
-Specify the sink to control. If the argument is omitted, the default
-sink is used.
-\fISINK\fR must be the numeric index of the sink.
-.IP "\fB\-i\fR[\fB=\fR\fISOURCE\fR]\fR, \fB\-\-source\fR[\fB=\fR\fISOURCE\fR], \fB\-\-input\fR[\fB=\fR\fISOURCE\fR]"
-Specify the source to control. If the argument is omitted,
-the default source is assumed.
-\fISOURCE\fR must be the numeric index of the source.
-.PP
-When neither \fB\-o\fR nor \fB\-i\fR are specified, the default
-source is used. When neither \fB\-d\fR nor \fB\-a\fR are specified,
-devices are operated on.
+.IP "\fB\-c\fR, \fB\-\-card\fR \fICARD\fR"
+Specify a card. By default, the card associated with the specified device
+is used. Cards can be specified by name or numeric index.
+.IP "\fB\-d\fR, \fB\-\-device\fR \fIDEVICE\fR"
+Specify a device other than the default. Devices can be specified by name
+or numeric index.
+.IP "\fB\-t\fR, \fB\-\-devtype\fR \fITYPE\fR"
+Specify a device type to examine, usually in conjunction with the \fB--device\fR
+flag. \fITYPE\fR must be one of \fIsink\fR, \fIsource\fR, \fIsink-input\fR, or
+\fIsource-output\fR.
+.IP "\fB--sink\fR, \fB--output\fR"
+Aliases to \fB--devtype\fR \fIsink\fR.
+.IP "\fB--source\fR, \fB--input\fR"
+Aliases to \fB--devtype\fR \fIsource\fR.
+.IP "\fB--source-output\fR"
+Alias to \fB--devtype\fR \fIsource-output\fR.
+.IP "\fB--sink-input\fR"
+Alias to \fB--devtype\fR \fIsink-input\fR.
.SH OPERATIONS
-.SS Common Operations
-These options work on both devices and applications.
+.SS Generic Commands
+.IP "\fBhelp\fR"
+Display the usage and exit.
+.SS Device Commands
+These commands are specific to devices. Not all devices will support
+all the listed commands.
.PP
.IP "\fBlist\fR"
-List all available sinks and sources.
+List all available devices. This can be filtered using the \fB--devtype\fR flag.
+.IP "\fBlist-short\fR"
+List all available devices in a parseable format. This can be filtered using the
+\fB--devtype\fR flag.
+.IP "\fBdefaults\fR"
+Display the default sink and source. This is the default command if none
+is specified.
+.IP "\fBset-default\fR \fIDEVICE\fR"
+Set a sink or source by name or numeric index as the default.
.IP "\fBget-volume\fR"
-Get the volume of the target.
+Get the volume of a device.
.IP "\fBset-volume\fR \fIVALUE\fR"
-Set the volume of the target. VALUE is an integer between 0 and 150.
+Set the volume of a device. \fIVALUE\fR is an integer between 0 and 150.
.IP "\fBget-balance\fR"
-Get the balance of the target.
+Get the balance of a device.
.IP "\fBset-balance\fR \fIVALUE\fR"
-Set the balance of the target. VALUE is an integer from -100 (all left) to 100
+Set the balance of a device. \fIVALUE\fR is an integer from -100 (all left) to 100
(all right).
.IP "\fBadj-balance\fR \fIVALUE\fR"
-Adjust balance by the integer increment VALUE. The resulting balance has the same
-bounds as those set by \fBset-balance\fR. The end-of-options indicator (\fI--\fR) is
-required when passing a negative increment.
+Adjust balance by the integer increment \fIVALUE\fR. The resulting balance has
+the same bounds as those set by \fBset-balance\fR. The end-of-options indicator
+(\fI--\fR) is required when passing a negative increment.
.IP "\fBincrease\fR \fIVALUE\fR"
-Increase the volume percentage of target device or application by integer
+Increase the volume percentage of a device or application by integer
VALUE. Increasing the volume in this way is capped at 100.
.IP "\fBdecrease\fR \fIVALUE\fR"
-Decrease the volume percentage of target by the integer VALUE. Decreasing the
-volume in this way is capped at 0.
+Decrease the volume percentage of a device by the integer \fIVALUE\fR.
+Decreasing the volume in this way is capped at 0.
.IP "\fBmute\fR, \fBunmute\fR, \fBtoggle\fR"
-Mute, unmute, or toggle the mute status on the target.
+Mute, unmute, or toggle the mute status of a device.
.IP "\fBis-muted\fR"
-Check if the target is muted. ponymix will exit with the status 0 if muted,
-and non-zero otherwise.
-.SS Device Operations
+Check if a device is muted. ponymix will exit zero if muted, and non-zero
+otherwise.
+.SS Application Commands
+These commands are specific to devices which refer to streams of applications.
+For these commands, \fIsink\fR and \fIsource\fR are synonymous with \fIsink-input\fR
+and \fIsource-output\fR, respectively.
+.IP "\fBmove\fR \fIDEVICE\fR"
+Move a device's stream to the given device, specified using the \fB--devtype\fR
+and \fB--device\fR flags. Note that a source output can only be moved to
+another source, and a sink input can only be moved to another sink. The type of
+the target device will be inferred using this logic.
+.IP "\fBkill\fR
+Kill a device's stream, specified using the \fB--device\fR and \fB--devtype\fR
+flags. This only applies to sink-inputs and source-outputs.
+.SS Card Commands
+These commands are specific to cards.
.PP
-.IP "\fBdefaults\fR"
-Display the default sink and source.
-.IP "\fBset-default\fR \fIDEVICE_ID\fR"
-Set the default sink or source by numeric index, depending on whether
-\fI\-i\fR or \fI\-o\fR was set.
-.SS Application Operations
-
-.IP "\fBmove\fR \fIAPP_ID\fR \fIDEVICE_ID\fR"
-Move the application's stream by numeric index to another device by numeric index.
-.IP "\fBkill\fR \fIAPP_ID\fR"
-Kill an application's stream by numeric index.
+.IP "\fBlist-cards\fR"
+List all available cards.
+.IP "\fBlist-cards-short\fR"
+List names for all available cards.
+.IP "\fBlist-profiles\fR"
+List profiles for a card.
+.IP "\fBlist-profiles-short\fR"
+List only names for the available profiles for a card.
+.IP "\fBset-profile\fR" \fIPROFILE\fR
+Set the specified profile for a card.
.SH AUTHORS
.nf
Dave Reisner <dreisner@archlinux.org>
diff --git a/ponymix.cc b/ponymix.cc
new file mode 100644
index 0000000..fc20315
--- /dev/null
+++ b/ponymix.cc
@@ -0,0 +1,642 @@
+#include "pulse.h"
+
+#include <err.h>
+#include <getopt.h>
+#include <unistd.h>
+
+#include <algorithm>
+#include <map>
+#include <stdexcept>
+
+struct Command {
+ int (*fn)(PulseClient&, int, char*[]);
+ Range<int> args;
+};
+
+struct Color {
+ Color() {
+ if (isatty(fileno(stdout))) {
+ name = "\033[1m";
+ reset = "\033[0m";
+ over9000 = "\033[7;31m";
+ veryhigh = "\033[31m";
+ high = "\033[35m";
+ mid = "\033[33m";
+ low = "\033[32m";
+ verylow = "\033[34m";
+ mute = "\033[1;31m";
+ } else {
+ name = "";
+ reset = "";
+ over9000 = "";
+ veryhigh = "";
+ high = "";
+ mid = "";
+ low = "";
+ verylow = "";
+ mute = "";
+ }
+ }
+
+ const char* name;
+ const char* reset;
+
+ // Volume levels
+ const char* over9000;
+ const char* veryhigh;
+ const char* high;
+ const char* mid;
+ const char* low;
+ const char* verylow;
+ const char* mute;
+};
+
+static enum DeviceType opt_devtype;
+static bool opt_listrestrict;
+static const char* opt_action;
+static const char* opt_device;
+static const char* opt_card;
+static Color color;
+
+static const char* type_to_string(enum DeviceType t) {
+ switch (t) {
+ case DEVTYPE_SINK:
+ return "sink";
+ case DEVTYPE_SOURCE:
+ return "source";
+ case DEVTYPE_SINK_INPUT:
+ return "sink-input";
+ case DEVTYPE_SOURCE_OUTPUT:
+ return "source-output";
+ }
+
+ /* impossibiru! */
+ throw std::out_of_range("device type out of range");
+}
+
+static enum DeviceType string_to_devtype_or_die(const char* str) {
+ static std::map<string, enum DeviceType> typemap = {
+ { "sink", DEVTYPE_SINK },
+ { "source", DEVTYPE_SOURCE },
+ { "sink-input", DEVTYPE_SINK_INPUT },
+ { "source-output", DEVTYPE_SOURCE_OUTPUT },
+ };
+ try {
+ return typemap.at(str);
+ } catch(std::out_of_range) {
+ errx(1, "error: Invalid device type specified: %s", str);
+ }
+}
+
+static Device* string_to_device_or_die(PulseClient& ponymix,
+ string arg,
+ enum DeviceType type) {
+ Device* device = ponymix.GetDevice(arg, type);
+ if (device == nullptr) errx(1, "no match found for device: %s", arg.c_str());
+ return device;
+}
+
+static void Print(const Device& device, bool shirt) {
+ if (shirt) {
+ printf("%s\t%d\t%s\t%s\n",
+ type_to_string(device.Type()),
+ device.Index(),
+ device.Name().c_str(),
+ device.Desc().c_str());
+ return;
+ }
+
+ const char *mute = device.Muted() ? " [Muted]" : "";
+ const char *volume_color;
+
+ if (device.Volume() < 20) {
+ volume_color = color.verylow;
+ } else if (device.Volume() < 40) {
+ volume_color = color.low;
+ } else if (device.Volume() < 60) {
+ volume_color = color.mid;
+ } else if (device.Volume() < 80) {
+ volume_color = color.high;
+ } else if (device.Volume() <= 100) {
+ volume_color = color.veryhigh;
+ } else {
+ volume_color = color.over9000;
+ }
+
+ printf("%s%s %d:%s %s\n"
+ " %s\n"
+ " Avg. Volume: %s%d%%%s%s%s%s\n",
+ color.name,
+ type_to_string(device.Type()),
+ device.Index(),
+ color.reset,
+ device.Name().c_str(),
+ device.Desc().c_str(),
+ volume_color,
+ device.Volume(),
+ color.reset,
+ color.mute,
+ mute,
+ color.reset);
+}
+
+static void Print(const Card& card, bool shirt) {
+ if (shirt) {
+ printf("%s\n", card.Name().c_str());
+ return;
+ }
+
+ printf("%scard %d:%s %s\n"
+ " Driver: %s\n"
+ " Active Profile: %s\n",
+ color.name,
+ card.Index(),
+ color.reset,
+ card.Name().c_str(),
+ card.Driver().c_str(),
+ card.ActiveProfile().name.c_str());
+}
+
+static void Print(const Profile& profile, bool active, bool shirt) {
+ if (shirt) {
+ printf("%s\n", profile.name.c_str());
+ return;
+ }
+
+ const char* active_str = active ? " [active]" : "";
+ printf("%s%s%s%s%s%s\n"
+ " %s\n",
+ color.name,
+ profile.name.c_str(),
+ color.reset,
+ color.low,
+ active_str,
+ color.reset,
+ profile.desc.c_str());
+}
+
+static int ShowDefaults(PulseClient& ponymix, int, char*[]) {
+ const auto& info = ponymix.GetDefaults();
+ Print(*ponymix.GetSink(info.sink), false);
+ Print(*ponymix.GetSource(info.source), false);
+ return 0;
+}
+
+static int list_devices(PulseClient& ponymix, bool shirt) {
+ if (opt_listrestrict) {
+ const auto& devices = ponymix.GetDevices(opt_devtype);
+ for (const auto& d : devices) Print(d, shirt);
+ return 0;
+ }
+
+ const auto& sinks = ponymix.GetSinks();
+ for (const auto& s : sinks) Print(s, shirt);
+
+ const auto& sources = ponymix.GetSources();
+ for (const auto& s : sources) Print(s, shirt);
+
+ const auto& sinkinputs = ponymix.GetSinkInputs();
+ for (const auto& s : sinkinputs) Print(s, shirt);
+
+ const auto& sourceoutputs = ponymix.GetSourceOutputs();
+ for (const auto& s : sourceoutputs) Print(s, shirt);
+
+ return 0;
+}
+
+static int List(PulseClient& ponymix, int, char*[]) {
+ return list_devices(ponymix, false);
+}
+
+static int ListShort(PulseClient& ponymix, int, char*[]) {
+ return list_devices(ponymix, true);
+}
+
+static int list_cards(PulseClient& ponymix, bool shirt) {
+ const auto& cards = ponymix.GetCards();
+ for (const auto& c : cards) Print(c, shirt);
+
+ return 0;
+}
+
+static int ListCards(PulseClient& ponymix, int, char*[]) {
+ return list_cards(ponymix, false);
+}
+
+static int ListCardsShort(PulseClient& ponymix, int, char*[]) {
+ return list_cards(ponymix, true);
+}
+
+static Card* resolve_active_card_or_die(PulseClient& ponymix) {
+ Card* card;
+ if (opt_card == nullptr) {
+ auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype);
+ card = ponymix.GetCard(*device);
+ if (card == nullptr) errx(1, "error: no card found or selected.");
+ } else {
+ card = ponymix.GetCard(opt_card);
+ if (card == nullptr) {
+ errx(1, "error: no match found for card: %s", opt_card);
+ }
+ }
+
+ return card;
+}
+
+static int list_profiles(PulseClient& ponymix, bool shirt) {
+ auto card = resolve_active_card_or_die(ponymix);
+
+ const auto& profiles = card->Profiles();
+ for (const auto& p : profiles) Print(p,
+ p.name == card->ActiveProfile().name,
+ shirt);
+
+ return 0;
+}
+
+static int ListProfiles(PulseClient& ponymix, int, char*[]) {
+ return list_profiles(ponymix, false);
+}
+
+static int ListProfilesShort(PulseClient& ponymix, int, char*[]) {
+ return list_profiles(ponymix, true);
+}
+
+static int GetVolume(PulseClient& ponymix, int, char*[]) {
+ auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype);
+ printf("%d\n", device->Volume());
+ return 0;
+}
+
+static int SetVolume(PulseClient& ponymix, int, char* argv[]) {
+ auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype);
+
+ long volume;
+ try {
+ volume = std::stol(argv[0]);
+ } catch (std::invalid_argument) {
+ errx(1, "error: failed to convert string to integer: %s", argv[0]);
+ }
+
+ if (!ponymix.SetVolume(*device, volume)) return 1;
+
+ printf("%d\n", device->Volume());
+
+ return 0;
+}
+
+static int GetBalance(PulseClient& ponymix, int, char*[]) {
+ auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype);
+ printf("%d\n", device->Balance());
+ return 0;
+}
+
+static int SetBalance(PulseClient& ponymix, int, char* argv[]) {
+ auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype);
+
+ long balance;
+ try {
+ balance = std::stol(argv[0]);
+ } catch (std::invalid_argument) {
+ errx(1, "error: failed to convert string to integer: %s", argv[0]);
+ }
+
+ if (!ponymix.SetBalance(*device, balance)) return 1;
+
+ printf("%d\n", device->Balance());
+
+ return 0;
+}
+
+static int AdjBalance(PulseClient& ponymix, int, char* argv[]) {
+ auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype);
+
+ long balance;
+ try {
+ balance = std::stol(argv[0]);
+ } catch (std::invalid_argument) {
+ errx(1, "error: failed to convert string to integer: %s", argv[0]);
+ }
+
+ if (!ponymix.SetBalance(*device, device->Balance() + balance)) return 1;
+
+ printf("%d\n", device->Balance());
+
+ return 0;
+}
+
+static int IncreaseVolume(PulseClient& ponymix, int, char* argv[]) {
+ auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype);
+
+ long delta;
+ try {
+ delta = std::stol(argv[0]);
+ } catch (std::invalid_argument) {
+ errx(1, "error: failed to convert string to integer: %s", argv[0]);
+ }
+
+ if (!ponymix.SetVolume(*device, device->Volume() + delta)) return 1;
+
+ printf("%d\n", device->Volume());
+
+ return 0;
+}
+
+static int DecreaseVolume(PulseClient& ponymix, int, char* argv[]) {
+ auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype);
+
+ long delta;
+ try {
+ delta = std::stol(argv[0]);
+ } catch (std::invalid_argument) {
+ errx(1, "error: failed to convert string to integer: %s", argv[0]);
+ }
+
+ if (!ponymix.SetVolume(*device, device->Volume() - delta)) return 1;
+
+ printf("%d\n", device->Volume());
+
+ return 0;
+}
+
+static int Mute(PulseClient& ponymix, int, char*[]) {
+ auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype);
+
+ if (!ponymix.SetMute(*device, true)) return 1;
+
+ printf("%d\n", device->Volume());
+
+ return 0;
+}
+
+static int Unmute(PulseClient& ponymix, int, char*[]) {
+ auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype);
+
+ if (!ponymix.SetMute(*device, false)) return 1;
+
+ printf("%d\n", device->Volume());
+
+ return 0;
+}
+
+static int ToggleMute(PulseClient& ponymix, int, char*[]) {
+ auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype);
+
+ if (!ponymix.SetMute(*device, !ponymix.IsMuted(*device))) return 1;
+
+ printf("%d\n", device->Volume());
+
+ return 0;
+}
+
+static int IsMuted(PulseClient& ponymix, int, char*[]) {
+ auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype);
+ return !ponymix.IsMuted(*device);
+}
+
+static int SetDefault(PulseClient& ponymix, int, char*[]) {
+ auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype);
+
+ return !ponymix.SetDefault(*device);
+}
+
+static int GetProfile(PulseClient& ponymix, int, char*[]) {
+ auto card = resolve_active_card_or_die(ponymix);
+ printf("%s\n", card->ActiveProfile().name.c_str());
+
+ return true;
+}
+
+static int SetProfile(PulseClient& ponymix, int, char* argv[]) {
+ auto card = resolve_active_card_or_die(ponymix);
+ return !ponymix.SetProfile(*card, argv[0]);
+}
+
+static int Move(PulseClient& ponymix, int, char* argv[]) {
+ // this assignment is a lie. stfu g++
+ enum DeviceType target_devtype = opt_devtype;
+ switch (opt_devtype) {
+ case DEVTYPE_SOURCE:
+ opt_devtype = DEVTYPE_SOURCE_OUTPUT;
+ case DEVTYPE_SOURCE_OUTPUT:
+ target_devtype = DEVTYPE_SOURCE;
+ break;
+ case DEVTYPE_SINK:
+ opt_devtype = DEVTYPE_SINK_INPUT;
+ case DEVTYPE_SINK_INPUT:
+ target_devtype = DEVTYPE_SINK;
+ break;
+ }
+
+ // Does this even work?
+ auto source = string_to_device_or_die(ponymix, opt_device, opt_devtype);
+ auto target = string_to_device_or_die(ponymix, argv[0], target_devtype);
+
+ return !ponymix.Move(*source, *target);
+}
+
+static int Kill(PulseClient& ponymix, int, char*[]) {
+ switch (opt_devtype) {
+ case DEVTYPE_SOURCE:
+ opt_devtype = DEVTYPE_SOURCE_OUTPUT;
+ break;
+ case DEVTYPE_SINK:
+ opt_devtype = DEVTYPE_SINK_INPUT;
+ break;
+ default:
+ break;
+ }
+
+ auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype);
+
+ return !ponymix.Kill(*device);
+}
+
+static const Command& string_to_command(const char* str) {
+ static std::map<string, const Command> actionmap = {
+ // command name function arg min arg max
+ { "defaults", { ShowDefaults, { 0, 0 } } },
+ { "list", { List, { 0, 0 } } },
+ { "list-short", { ListShort, { 0, 0 } } },
+ { "list-cards", { ListCards, { 0, 0 } } },
+ { "list-cards-short", { ListCardsShort, { 0, 0 } } },
+ { "list-profiles", { ListProfiles, { 0, 0 } } },
+ { "list-profiles-short", { ListProfilesShort, { 0, 0 } } },
+ { "get-volume", { GetVolume, { 0, 0 } } },
+ { "set-volume", { SetVolume, { 1, 1 } } },
+ { "get-balance", { GetBalance, { 0, 0 } } },
+ { "set-balance", { SetBalance, { 1, 1 } } },
+ { "adj-balance", { AdjBalance, { 1, 1 } } },
+ { "increase", { IncreaseVolume, { 1, 1 } } },
+ { "decrease", { DecreaseVolume, { 1, 1 } } },
+ { "mute", { Mute, { 0, 0 } } },
+ { "unmute", { Unmute, { 0, 0 } } },
+ { "toggle", { ToggleMute, { 0, 0 } } },
+ { "is-muted", { IsMuted, { 0, 0 } } },
+ { "set-default", { SetDefault, { 0, 0 } } },
+ { "get-profile", { GetProfile, { 0, 0 } } },
+ { "set-profile", { SetProfile, { 1, 1 } } },
+ { "move", { Move, { 1, 1 } } },
+ { "kill", { Kill, { 0, 0 } } }
+ };
+
+ try {
+ return actionmap.at(str);
+ } catch(std::out_of_range) {
+ errx(1, "error: Invalid action specified: %s", str);
+ }
+}
+
+static void usage() {
+ printf("usage: %s [options] <command>...\n", program_invocation_short_name);
+ fputs("\nOptions:\n"
+ " -h, --help display this help and exit\n\n"
+
+ " -c, --card CARD target card (index or name)\n"
+ " -d, --device DEVICE target device (index or name)\n"
+ " -t, --devtype TYPE device type\n"
+ " --source alias to -t source\n"
+ " --input alais to -t source\n"
+ " --sink alias to -t sink\n"
+ " --output alias to -t sink\n"
+ " --sink-input alias to -t sink-input\n"
+ " --source-output alias to -t source-output\n", stdout);
+
+ fputs("\nDevice Commands:\n"
+ " help display this message\n"
+ " defaults list default devices (default command)\n"
+ " set-default set default device by ID\n"
+ " list list available devices\n"
+ " list-short list available devices (short form)\n"
+ " list-cards list available cards\n"
+ " list-cards-short list available cards (short form)\n"
+ " get-volume get volume for device\n"
+ " set-volume VALUE set volume for device\n"
+ " get-balance get balance for device\n"
+ " set-balance VALUE set balance for device\n"
+ " adj-balance VALUE increase or decrease balance for device\n"
+ " increase VALUE increase volume\n", stdout);
+ fputs(" decrease VALUE decrease volume\n"
+ " mute mute device\n"
+ " unmute unmute device\n"
+ " toggle toggle mute\n"
+ " is-muted check if muted\n", stdout);
+ fputs("\nApplication Commands:\n"
+ " move DEVICE move target device to DEVICE\n"
+ " kill DEVICE kill target DEVICE\n", stdout);
+
+ fputs("\nCard Commands:\n"
+ " list-profiles list available profiles for a card\n"
+ " list-profiles-short list available profiles for a card (short form)\n"
+ " get-profile get active profile for card\n"
+ " set-profile PROFILE set profile for a card\n", stdout);
+
+ exit(EXIT_SUCCESS);
+}
+
+void error_wrong_args(const Command& cmd, const char* cmdname) {
+ if (cmd.args.min == cmd.args.max) {
+ errx(1, "error: %s takes exactly %d argument%c",
+ cmdname, cmd.args.min, cmd.args.min == 1 ? '\0' : 's');
+ } else {
+ errx(1, "error: %s takes %d to %d arguments\n",
+ cmdname, cmd.args.min, cmd.args.max);
+ }
+}
+
+static int CommandDispatch(PulseClient& ponymix, int argc, char *argv[]) {
+ if (argc > 0) {
+ opt_action = argv[0];
+ argv++;
+ argc--;
+ }
+
+ if (strcmp(opt_action, "help") == 0) {
+ usage();
+ return 0;
+ }
+
+ const Command& cmd = string_to_command(opt_action);
+ if (cmd.args.InRange(argc) != 0) error_wrong_args(cmd, opt_action);
+
+ return cmd.fn(ponymix, argc, argv);
+}
+
+bool parse_options(int argc, char** argv) {
+ static const struct option opts[] = {
+ { "card", required_argument, 0, 'c' },
+ { "device", required_argument, 0, 'd' },
+ { "help", no_argument, 0, 'h' },
+ { "type", required_argument, 0, 't' },
+ { "sink", no_argument, 0, 0x100 },
+ { "output", no_argument, 0, 0x101 },
+ { "source", no_argument, 0, 0x102 },
+ { "input", no_argument, 0, 0x103 },
+ { "sink-input", no_argument, 0, 0x104 },
+ { "source-output", no_argument, 0, 0x105 },
+ { 0, 0, 0, 0 },
+ };
+
+ for (;;) {
+ int opt = getopt_long(argc, argv, "c:d:ht:", opts, NULL);
+ if (opt == -1)
+ break;
+
+ switch (opt) {
+ case 'c':
+ opt_card = optarg;
+ break;
+ case 'd':
+ opt_device = optarg;
+ break;
+ case 'h':
+ usage();
+ break;
+ case 't':
+ opt_devtype = string_to_devtype_or_die(optarg);
+ break;
+ case 0x100:
+ case 0x101:
+ opt_devtype = DEVTYPE_SINK;
+ opt_listrestrict = true;
+ break;
+ case 0x102:
+ case 0x103:
+ opt_devtype = DEVTYPE_SOURCE;
+ opt_listrestrict = true;
+ break;
+ case 0x104:
+ opt_devtype = DEVTYPE_SINK_INPUT;
+ opt_listrestrict = true;
+ break;
+ case 0x105:
+ opt_devtype = DEVTYPE_SOURCE_OUTPUT;
+ opt_listrestrict = true;
+ break;
+ default:
+ return false;
+ }
+ }
+
+ return true;
+}
+
+int main(int argc, char* argv[]) {
+ PulseClient ponymix("ponymix");
+ ponymix.Populate();
+
+ // defaults. intentionally, we don't set a card -- only get
+ // that on demand if a function needs it.
+ ServerInfo defaults = ponymix.GetDefaults();
+ opt_action = "defaults";
+ opt_devtype = DEVTYPE_SINK;
+ opt_device = defaults.sink.c_str();
+
+ if (!parse_options(argc, argv)) return 1;
+ argc -= optind;
+ argv += optind;
+
+ return CommandDispatch(ponymix, argc, argv);
+}
+
+// vim: set et ts=2 sw=2:
diff --git a/pulse.cc b/pulse.cc
new file mode 100644
index 0000000..0488717
--- /dev/null
+++ b/pulse.cc
@@ -0,0 +1,628 @@
+// Self
+#include "pulse.h"
+
+// C
+#include <err.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+// C++
+#include <string>
+#include <algorithm>
+#include <stdexcept>
+
+// External
+#include <pulse/pulseaudio.h>
+
+namespace {
+static void connect_state_cb(pa_context* context, void* raw) {
+ enum pa_context_state *state = (enum pa_context_state*)raw;
+ *state = pa_context_get_state(context);
+}
+
+static void success_cb(pa_context* context, int success, void* raw) {
+ int *r = (int*)raw;
+ *r = success;
+ if (!success) {
+ fprintf(stderr,
+ "operation failed: %s\n",
+ pa_strerror(pa_context_errno(context)));
+ }
+}
+
+static void card_info_cb(pa_context* context,
+ const pa_card_info* info,
+ int eol,
+ void* raw) {
+ if (eol < 0) {
+ fprintf(stderr, "%s error in %s: \n", __func__,
+ pa_strerror(pa_context_errno(context)));
+ return;
+ }
+
+ if (!eol) {
+ vector<Card>* cards = (vector<Card>*)raw;
+ cards->push_back(info);
+ }
+}
+
+template<typename T>
+static void device_info_cb(pa_context* context,
+ const T* info,
+ int eol,
+ void* raw) {
+ if (eol < 0) {
+ fprintf(stderr, "%s error in %s: \n", __func__,
+ pa_strerror(pa_context_errno(context)));
+ return;
+ }
+
+ if (!eol) {
+ vector<Device>* devices_ = (vector<Device>*)raw;
+ devices_->push_back(info);
+ }
+}
+
+static void server_info_cb(pa_context* context __attribute__((unused)),
+ const pa_server_info* i,
+ void* raw) {
+ ServerInfo* defaults = (ServerInfo*)raw;
+ defaults->sink = i->default_sink_name;
+ defaults->source = i->default_source_name;
+}
+
+static pa_cvolume* value_to_cvol(long value, pa_cvolume *cvol) {
+ return pa_cvolume_set(cvol, cvol->channels,
+ fmax((double)(value + .5) * PA_VOLUME_NORM / 100, 0));
+}
+
+static int volume_as_percent(const pa_cvolume* cvol) {
+ return pa_cvolume_avg(cvol) * 100.0 / PA_VOLUME_NORM;
+}
+
+static int xstrtol(const char *str, long *out) {
+ char *end = NULL;
+
+ if (str == NULL || *str == '\0') return -1;
+ errno = 0;
+
+ *out = strtol(str, &end, 10);
+ if (errno || str == end || (end && *end)) return -1;
+
+ return 0;
+}
+
+
+} // anonymous namespace
+
+PulseClient::PulseClient(string client_name) :
+ client_name_(client_name),
+ volume_range_(0, 150),
+ balance_range_(-100, 100) {
+ enum pa_context_state state = PA_CONTEXT_CONNECTING;
+
+ pa_proplist* proplist = pa_proplist_new();
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, client_name.c_str());
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, "com.falconindy.ponymix");
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_VERSION, "0.1");
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_ICON_NAME, "audio-card");
+
+ mainloop_ = pa_mainloop_new();
+ context_ = pa_context_new_with_proplist(pa_mainloop_get_api(mainloop_),
+ nullptr, proplist);
+
+ pa_proplist_free(proplist);
+
+ pa_context_set_state_callback(context_, connect_state_cb, &state);
+ pa_context_connect(context_, nullptr, PA_CONTEXT_NOFAIL, nullptr);
+ while (state != PA_CONTEXT_READY && state != PA_CONTEXT_FAILED) {
+ pa_mainloop_iterate(mainloop_, 1, nullptr);
+ }
+
+ if (state != PA_CONTEXT_READY) {
+ fprintf(stderr, "failed to connect to pulse daemon: %s\n",
+ pa_strerror(pa_context_errno(context_)));
+ exit(EXIT_FAILURE);
+ }
+}
+
+//
+// Pulse Client
+//
+PulseClient::~PulseClient() {
+ pa_context_unref(context_);
+ pa_mainloop_free(mainloop_);
+}
+
+void PulseClient::Populate() {
+ populate_server_info();
+ populate_sinks();
+ populate_sources();
+ populate_cards();
+}
+
+Card* PulseClient::GetCard(const uint32_t& index) {
+ for (Card& card : cards_) {
+ if (card.index_ == index) return &card;
+ }
+ return nullptr;
+}
+
+Card* PulseClient::GetCard(const string& name) {
+ long val;
+ if (xstrtol(name.c_str(), &val) == 0) return GetCard(val);
+
+ for (Card& card : cards_) {
+ if (card.name_ == name) return &card;
+ }
+ return nullptr;
+}
+
+Card* PulseClient::GetCard(const Device& device) {
+ for (Card& card : cards_) {
+ if (device.card_idx_ == card.index_) return &card;
+ }
+ return nullptr;
+}
+
+Device* PulseClient::get_device(vector<Device>& devices, const uint32_t& index) {
+ for (Device& device : devices) {
+ if (device.index_ == index) return &device;
+ }
+ return nullptr;
+}
+
+Device* PulseClient::get_device(vector<Device>& devices, const string& name) {
+ long val;
+ if (xstrtol(name.c_str(), &val) == 0) return get_device(devices, val);
+
+ for (Device& device : devices) {
+ if (device.name_ == name) return &device;
+ }
+ return nullptr;
+}
+
+Device* PulseClient::GetDevice(const uint32_t& index, enum DeviceType type) {
+ switch (type) {
+ case DEVTYPE_SINK:
+ return GetSink(index);
+ case DEVTYPE_SOURCE:
+ return GetSource(index);
+ case DEVTYPE_SINK_INPUT:
+ return GetSinkInput(index);
+ case DEVTYPE_SOURCE_OUTPUT:
+ return GetSourceOutput(index);
+ }
+ throw std::runtime_error("Impossible DeviceType encountered in GetDevice");
+}
+
+Device* PulseClient::GetDevice(const string& name, enum DeviceType type) {
+ switch (type) {
+ case DEVTYPE_SINK:
+ return GetSink(name);
+ case DEVTYPE_SOURCE:
+ return GetSource(name);
+ case DEVTYPE_SINK_INPUT:
+ return GetSinkInput(name);
+ case DEVTYPE_SOURCE_OUTPUT:
+ return GetSourceOutput(name);
+ }
+ throw std::runtime_error("Impossible DeviceType encountered in GetDevice");
+}
+
+const vector<Device>& PulseClient::GetDevices(enum DeviceType type) const {
+ switch (type) {
+ case DEVTYPE_SINK:
+ return GetSinks();
+ case DEVTYPE_SOURCE:
+ return GetSources();
+ case DEVTYPE_SINK_INPUT:
+ return GetSinkInputs();
+ case DEVTYPE_SOURCE_OUTPUT:
+ return GetSourceOutputs();
+ }
+ throw std::runtime_error("Impossible DeviceType encountered in GetDevices");
+}
+
+Device* PulseClient::GetSink(const uint32_t& index) {
+ return get_device(sinks_, index);
+}
+
+Device* PulseClient::GetSink(const string& name) {
+ return get_device(sinks_, name);
+}
+
+Device* PulseClient::GetSource(const uint32_t& index) {
+ return get_device(sources_, index);
+}
+
+Device* PulseClient::GetSource(const string& name) {
+ return get_device(sources_, name);
+}
+
+Device* PulseClient::GetSinkInput(const uint32_t& index) {
+ return get_device(sink_inputs_, index);
+}
+
+Device* PulseClient::GetSinkInput(const string& name) {
+ return get_device(sink_inputs_, name);
+}
+
+Device* PulseClient::GetSourceOutput(const uint32_t& index) {
+ return get_device(source_outputs_, index);
+}
+
+Device* PulseClient::GetSourceOutput(const string& name) {
+ return get_device(source_outputs_, name);
+}
+
+void PulseClient::mainloop_iterate(pa_operation* op) {
+ int r;
+ while (pa_operation_get_state(op) == PA_OPERATION_RUNNING) {
+ pa_mainloop_iterate(mainloop_, 1, &r);
+ }
+}
+
+void PulseClient::populate_cards() {
+ cards_.clear();
+ pa_operation* op = pa_context_get_card_info_list(context_,
+ card_info_cb,
+ (void*)&cards_);
+ mainloop_iterate(op);
+ pa_operation_unref(op);
+}
+
+void PulseClient::populate_server_info() {
+ pa_operation* op = pa_context_get_server_info(context_,
+ server_info_cb,
+ &defaults_);
+ mainloop_iterate(op);
+ pa_operation_unref(op);
+}
+
+void PulseClient::populate_sinks() {
+ sinks_.clear();
+ pa_operation* op = pa_context_get_sink_info_list(context_,
+ device_info_cb,
+ (void*)&sinks_);
+ mainloop_iterate(op);
+ pa_operation_unref(op);
+
+ sink_inputs_.clear();
+ op = pa_context_get_sink_input_info_list(context_,
+ device_info_cb,
+ (void*)&sink_inputs_);
+ mainloop_iterate(op);
+ pa_operation_unref(op);
+}
+
+void PulseClient::populate_sources() {
+ sources_.clear();
+ pa_operation* op = pa_context_get_source_info_list(context_,
+ device_info_cb,
+ (void*)&sources_);
+ mainloop_iterate(op);
+ pa_operation_unref(op);
+
+ source_outputs_.clear();
+ op = pa_context_get_source_output_info_list(context_,
+ device_info_cb,
+ (void*)&source_outputs_);
+ mainloop_iterate(op);
+ pa_operation_unref(op);
+}
+
+bool PulseClient::SetMute(Device& device, bool mute) {
+ int success;
+
+ if (device.ops_.Mute == nullptr) {
+ warnx("device does not support muting.");
+ return false;
+ }
+
+ pa_operation* op = device.ops_.Mute(context_,
+ device.index_,
+ mute,
+ success_cb,
+ &success);
+ mainloop_iterate(op);
+ pa_operation_unref(op);
+
+ if (success) device.mute_ = mute;
+
+ return success;
+}
+
+bool PulseClient::SetVolume(Device& device, long volume) {
+ int success;
+
+ if (device.ops_.SetVolume == nullptr) {
+ warnx("device does not support setting balance.");
+ return false;
+ }
+
+ volume = volume_range_.Clamp(volume);
+ const pa_cvolume *cvol = value_to_cvol(volume, &device.volume_);
+ pa_operation* op = device.ops_.SetVolume(context_,
+ device.index_,
+ cvol,
+ success_cb,
+ &success);
+ mainloop_iterate(op);
+ pa_operation_unref(op);
+
+ if (success) device.update_volume(*cvol);
+
+ return success;
+}
+
+bool PulseClient::IncreaseVolume(Device& device, long increment) {
+ return SetVolume(device, device.volume_percent_ + increment);
+}
+
+bool PulseClient::DecreaseVolume(Device& device, long increment) {
+ return SetVolume(device, device.volume_percent_ - increment);
+}
+
+bool PulseClient::SetBalance(Device& device, long balance) {
+ int success;
+
+ if (device.ops_.SetVolume == nullptr) {
+ warnx("device does not support setting balance.");
+ return false;
+ }
+
+ balance = balance_range_.Clamp(balance);
+ pa_cvolume *cvol = pa_cvolume_set_balance(&device.volume_,
+ &device.channels_,
+ balance / 100.0);
+ pa_operation* op = device.ops_.SetVolume(context_,
+ device.index_,
+ cvol,
+ success_cb,
+ &success);
+ mainloop_iterate(op);
+ pa_operation_unref(op);
+
+ if (success) device.update_volume(*cvol);
+
+ return success;
+}
+
+bool PulseClient::IncreaseBalance(Device& device, long increment) {
+ return SetBalance(device, device.balance_ + increment);
+}
+
+bool PulseClient::DecreaseBalance(Device& device, long increment) {
+ return SetBalance(device, device.balance_ - increment);
+}
+
+int PulseClient::GetVolume(const Device& device) const {
+ return device.Volume();
+}
+
+int PulseClient::GetBalance(const Device& device) const {
+ return device.Balance();
+}
+
+bool PulseClient::SetProfile(Card& card, const string& profile) {
+ int success;
+ pa_operation* op =
+ pa_context_set_card_profile_by_index(context_,
+ card.index_,
+ profile.c_str(),
+ success_cb,
+ &success);
+ mainloop_iterate(op);
+ pa_operation_unref(op);
+
+ if (success) {
+ // Update the profile
+ for (const Profile p : card.profiles_) {
+ if (p.name == profile) {
+ card.active_profile_ = p;
+ break;
+ }
+ }
+ }
+
+ return success;
+}
+
+bool PulseClient::Move(Device& source, Device& dest) {
+ int success;
+
+ if (source.ops_.Move == nullptr) {
+ warnx("source device does not support moving.");
+ return false;
+ }
+
+ pa_operation* op = source.ops_.Move(context_,
+ source.index_,
+ dest.index_,
+ success_cb,
+ &success);
+ mainloop_iterate(op);
+ pa_operation_unref(op);
+
+ return success;
+}
+
+bool PulseClient::Kill(Device& device) {
+ int success;
+
+ if (device.ops_.Kill == nullptr) {
+ warnx("source device does not support being killed.");
+ return false;
+ }
+
+ pa_operation* op = device.ops_.Kill(context_,
+ device.index_,
+ success_cb,
+ &success);
+ mainloop_iterate(op);
+ pa_operation_unref(op);
+
+ if (success) remove_device(device);
+
+ return success;
+}
+
+bool PulseClient::SetDefault(Device& device) {
+ int success;
+
+ if (device.ops_.SetDefault == nullptr) {
+ warnx("device does not support defaults");
+ return false;
+ }
+
+ pa_operation* op = device.ops_.SetDefault(context_,
+ device.name_.c_str(),
+ success_cb,
+ &success);
+ mainloop_iterate(op);
+ pa_operation_unref(op);
+
+ if (success) {
+ switch (device.type_) {
+ case DEVTYPE_SINK:
+ defaults_.sink = device.name_;
+ break;
+ case DEVTYPE_SOURCE:
+ defaults_.source = device.name_;
+ break;
+ default:
+ errx(1, "impossible to set a default for device type %d",
+ device.type_);
+ }
+ }
+
+ return success;
+}
+
+void PulseClient::remove_device(Device& device) {
+ vector<Device>* devlist;
+
+ switch (device.type_) {
+ case DEVTYPE_SINK:
+ devlist = &sinks_;
+ case DEVTYPE_SINK_INPUT:
+ devlist = &sink_inputs_;
+ break;
+ case DEVTYPE_SOURCE:
+ devlist = &sources_;
+ break;
+ case DEVTYPE_SOURCE_OUTPUT:
+ devlist = &source_outputs_;
+ break;
+ }
+ devlist->erase(
+ std::remove_if(
+ devlist->begin(), devlist->end(),
+ [=](Device& d) { return d.index_ == device.index_; }),
+ devlist->end());
+}
+
+//
+// Cards
+//
+Card::Card(const pa_card_info* info) :
+ index_(info->index),
+ name_(info->name),
+ owner_module_(info->owner_module),
+ driver_(info->driver),
+ active_profile_(*info->active_profile) {
+ for (int i = 0; info->profiles[i].name != nullptr; i++) {
+ profiles_.push_back(info->profiles[i]);
+ }
+}
+
+//
+// Devices
+//
+Device::Device(const pa_sink_info* info) :
+ type_(DEVTYPE_SINK),
+ index_(info->index),
+ name_(info->name ? info->name : ""),
+ desc_(info->description),
+ mute_(info->mute),
+ card_idx_(info->card) {
+ update_volume(info->volume);
+ memcpy(&channels_, &info->channel_map, sizeof(pa_channel_map));
+ balance_ = pa_cvolume_get_balance(&volume_, &channels_) * 100.0;
+
+ ops_.SetVolume = pa_context_set_sink_volume_by_index;
+ ops_.Mute = pa_context_set_sink_mute_by_index;
+ ops_.Kill = nullptr;
+ ops_.Move = nullptr;
+ ops_.SetDefault = pa_context_set_default_sink;
+}
+
+Device::Device(const pa_source_info* info) :
+ type_(DEVTYPE_SOURCE),
+ index_(info->index),
+ name_(info->name ? info->name : ""),
+ desc_(info->description),
+ mute_(info->mute),
+ card_idx_(info->card) {
+ update_volume(info->volume);
+ memcpy(&channels_, &info->channel_map, sizeof(pa_channel_map));
+ balance_ = pa_cvolume_get_balance(&volume_, &channels_) * 100.0;
+
+ ops_.SetVolume = pa_context_set_source_volume_by_index;
+ ops_.Mute = pa_context_set_source_mute_by_index;
+ ops_.Kill = nullptr;
+ ops_.Move = nullptr;
+ ops_.SetDefault = pa_context_set_default_source;
+}
+
+Device::Device(const pa_sink_input_info* info) :
+ type_(DEVTYPE_SINK_INPUT),
+ index_(info->index),
+ name_(info->name ? info->name : ""),
+ mute_(info->mute),
+ card_idx_(-1) {
+ update_volume(info->volume);
+ memcpy(&channels_, &info->channel_map, sizeof(pa_channel_map));
+ balance_ = pa_cvolume_get_balance(&volume_, &channels_) * 100.0;
+
+ const char *desc = pa_proplist_gets(info->proplist,
+ PA_PROP_APPLICATION_NAME);
+ if (desc) desc_ = desc;
+
+ ops_.SetVolume = pa_context_set_sink_input_volume;
+ ops_.Mute = pa_context_set_sink_input_mute;
+ ops_.Kill = pa_context_kill_sink_input;
+ ops_.Move = pa_context_move_sink_input_by_index;
+ ops_.SetDefault = nullptr;
+}
+
+Device::Device(const pa_source_output_info* info) :
+ type_(DEVTYPE_SOURCE_OUTPUT),
+ index_(info->index),
+ name_(info->name ? info->name : ""),
+ mute_(info->mute),
+ card_idx_(-1) {
+ update_volume(info->volume);
+ volume_percent_ = volume_as_percent(&volume_);
+ balance_ = pa_cvolume_get_balance(&volume_, &channels_) * 100.0;
+
+ const char *desc = pa_proplist_gets(info->proplist,
+ PA_PROP_APPLICATION_NAME);
+ if (desc) desc_ = desc;
+
+ ops_.SetVolume = pa_context_set_source_output_volume;
+ ops_.Mute = pa_context_set_source_output_mute;
+ ops_.Kill = pa_context_kill_source_output;
+ ops_.Move = pa_context_move_source_output_by_index;
+ ops_.SetDefault = nullptr;
+}
+
+void Device::update_volume(const pa_cvolume& newvol) {
+ memcpy(&volume_, &newvol, sizeof(pa_cvolume));
+ volume_percent_ = volume_as_percent(&volume_);
+ balance_ = pa_cvolume_get_balance(&volume_, &channels_) * 100.0;
+}
+
+// vim: set et ts=2 sw=2:
diff --git a/pulse.h b/pulse.h
new file mode 100644
index 0000000..2c010b2
--- /dev/null
+++ b/pulse.h
@@ -0,0 +1,243 @@
+#pragma once
+
+// C
+#include <string.h>
+
+// C++
+#include <memory>
+#include <string>
+#include <vector>
+
+// external
+#include <pulse/pulseaudio.h>
+
+using std::string;
+using std::vector;
+using std::unique_ptr;
+
+enum DeviceType {
+ DEVTYPE_SINK,
+ DEVTYPE_SOURCE,
+ DEVTYPE_SINK_INPUT,
+ DEVTYPE_SOURCE_OUTPUT,
+};
+
+struct Profile {
+ Profile(const pa_card_profile_info& info) :
+ name(info.name),
+ desc(info.description) {}
+
+ string name;
+ string desc;
+};
+
+struct Operations {
+ pa_operation* (*Mute)(pa_context*, uint32_t, int, pa_context_success_cb_t, void*);
+ pa_operation* (*SetVolume)(pa_context*, uint32_t, const pa_cvolume*, pa_context_success_cb_t, void*);
+ pa_operation* (*SetDefault)(pa_context*, const char*, pa_context_success_cb_t, void*);
+ pa_operation* (*Kill)(pa_context*, uint32_t, pa_context_success_cb_t, void*);
+ pa_operation* (*Move)(pa_context *, uint32_t, uint32_t, pa_context_success_cb_t, void *);
+};
+
+class Device {
+ public:
+ Device(const pa_source_info* info);
+ Device(const pa_sink_info* info);
+ Device(const pa_sink_input_info* info);
+ Device(const pa_source_output_info* info);
+
+ uint32_t Index() const { return index_; }
+ const string& Name() const { return name_; }
+ const string& Desc() const { return desc_; }
+ int Volume() const { return volume_percent_; }
+ int Balance() const { return balance_; }
+ bool Muted() const { return mute_; }
+ enum DeviceType Type() const { return type_; }
+
+ private:
+ friend class PulseClient;
+
+ void update_volume(const pa_cvolume& newvol);
+
+ enum DeviceType type_;
+ uint32_t index_;
+ string name_;
+ string desc_;
+ pa_cvolume volume_;
+ int volume_percent_;
+ pa_channel_map channels_;
+ int mute_;
+ int balance_;
+ uint32_t card_idx_;
+ Operations ops_;
+};
+
+class Card {
+ public:
+ Card(const pa_card_info* info);
+
+ const string& Name() const { return name_; }
+ uint32_t Index() const { return index_; }
+ const string& Driver() const { return driver_; }
+
+ const vector<Profile>& Profiles() const { return profiles_; }
+ const Profile& ActiveProfile() const { return active_profile_; }
+
+ private:
+ friend class PulseClient;
+
+ uint32_t index_;
+ string name_;
+ uint32_t owner_module_;
+ string driver_;
+ vector<Profile> profiles_;
+ Profile active_profile_;
+};
+
+struct ServerInfo {
+ string sink;
+ string source;
+
+ const string GetDefault(enum DeviceType type) {
+ switch (type) {
+ case DEVTYPE_SINK:
+ return sink;
+ case DEVTYPE_SOURCE:
+ return source;
+ default:
+ return "";
+ }
+ }
+};
+
+template<typename T>
+struct Range {
+ Range(T min, T max) : min(min), max(max) {}
+
+ // Clamps a value to the stored range
+ T Clamp(T value) {
+ return value < min ? min : (value > max ? max : value);
+ }
+
+ // Determine if the passed value is within the range. Returns 0
+ // on success, else -1 if lower than the minimum, and 1 if higher
+ // than the maximum.
+ int InRange(T value) const {
+ return value < min ? -1 : (value > max ? 1 : 0);
+ }
+
+ T min;
+ T max;
+};
+
+class PulseClient {
+ public:
+ PulseClient(string client_name);
+ ~PulseClient();
+
+ // Populates all known devices and cards. Any currently known
+ // devices and cards are cleared before the new data is stored.
+ void Populate();
+
+ // Get a device by index or name and type, or all devices by type.
+ Device* GetDevice(const uint32_t& index, enum DeviceType type);
+ Device* GetDevice(const string& name, enum DeviceType type);
+ const vector<Device>& GetDevices(enum DeviceType type) const;
+
+ // Get a sink by index or name, or all sinks.
+ Device* GetSink(const uint32_t& index);
+ Device* GetSink(const string& name);
+ const vector<Device>& GetSinks() const { return sinks_; }
+
+ // Get a source by index or name, or all sources.
+ Device* GetSource(const uint32_t& index);
+ Device* GetSource(const string& name);
+ const vector<Device>& GetSources() const { return sources_; }
+
+ // Get a sink input by index or name, or all sink inputs.
+ Device* GetSinkInput(const uint32_t& name);
+ Device* GetSinkInput(const string& name);
+ const vector<Device>& GetSinkInputs() const { return sink_inputs_; }
+
+ // Get a source output by index or name, or all source outputs.
+ Device* GetSourceOutput(const uint32_t& name);
+ Device* GetSourceOutput(const string& name);
+ const vector<Device>& GetSourceOutputs() const { return source_outputs_; }
+
+ // Get a card by index or name, all cards, or get the card which
+ // a sink is attached to.
+ Card* GetCard(const uint32_t& index);
+ Card* GetCard(const string& name);
+ Card* GetCard(const Device& device);
+ const vector<Card>& GetCards() const { return cards_; }
+
+ // Get or set the volume of a device.
+ int GetVolume(const Device& device) const;
+ bool SetVolume(Device& device, long value);
+
+ // Convenience wrappers for adjusting volume
+ bool IncreaseVolume(Device& device, long increment);
+ bool DecreaseVolume(Device& device, long decrement);
+
+ // Get or set the volume of a device. Not all devices support this.
+ int GetBalance(const Device& device) const;
+ bool SetBalance(Device& device, long value);
+
+ // Convenience wrappers for adjusting balance
+ bool IncreaseBalance(Device& device, long increment);
+ bool DecreaseBalance(Device& device, long decrement);
+
+ // Get and set mute for a device.
+ bool IsMuted(const Device& device) const { return device.mute_; };
+ bool SetMute(Device& device, bool mute);
+
+ // Set the profile for a card by name.
+ bool SetProfile(Card& card, const string& profile);
+
+ // Move a given source output or sink input to the destination.
+ bool Move(Device& source, Device& dest);
+
+ // Kill a source output or sink input.
+ bool Kill(Device& device);
+
+ // Get or set the default sink and source.
+ const ServerInfo& GetDefaults() const { return defaults_; }
+ bool SetDefault(Device& device);
+
+ // Set minimum and maximum allowed volume
+ void SetVolumeRange(int min, int max) {
+ volume_range_ = { min, max };
+ }
+
+ // Set minimum and maximum allowed balance
+ void SetBalanceRange(int min, int max) {
+ balance_range_ = { min, max };
+ }
+
+ private:
+ void mainloop_iterate(pa_operation* op);
+
+ void populate_server_info();
+ void populate_cards();
+ void populate_sinks();
+ void populate_sources();
+
+ Device* get_device(vector<Device>& devices, const uint32_t& index);
+ Device* get_device(vector<Device>& devices, const string& name);
+
+ void remove_device(Device& device);
+
+ string client_name_;
+ pa_context* context_;
+ pa_mainloop* mainloop_;
+ vector<Device> sinks_;
+ vector<Device> sources_;
+ vector<Device> sink_inputs_;
+ vector<Device> source_outputs_;
+ vector<Card> cards_;
+ ServerInfo defaults_;
+ Range<int> volume_range_;
+ Range<int> balance_range_;
+};
+
+// vim: set et ts=2 sw=2: