diff options
-rw-r--r-- | Makefile | 22 | ||||
-rw-r--r-- | ponymix.cc | 520 | ||||
-rw-r--r-- | pulse.cc | 561 | ||||
-rw-r--r-- | pulse.h | 216 |
4 files changed, 1319 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e6cb6ca --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +CXX = g++ -std=c++11 + +base_CFLAGS = -Wall -Wextra -pedantic -O2 -g +base_LIBS = -lm + +libpulse_CFLAGS = $(shell pkg-config --cflags libpulse) +libpulse_LIBS = $(shell pkg-config --libs libpulse) + +CXXFLAGS := $(base_CFLAGS) $(libpulse_CFLAGS) $(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 + +clean: + $(RM) ponymix pulse.o diff --git a/ponymix.cc b/ponymix.cc new file mode 100644 index 0000000..1bd256f --- /dev/null +++ b/ponymix.cc @@ -0,0 +1,520 @@ +#include "pulse.h" + +#include <err.h> +#include <getopt.h> + +#include <algorithm> +#include <map> +#include <stdexcept> + +#define _unused_ __attribute__((unused)) + +enum Action { + ACTION_DEFAULTS, + ACTION_LIST, + ACTION_LISTCARDS, + ACTION_LISTPROFILES, + ACTION_GETVOL, + ACTION_SETVOL, + ACTION_GETBAL, + ACTION_SETBAL, + ACTION_ADJBAL, + ACTION_INCREASE, + ACTION_DECREASE, + ACTION_MUTE, + ACTION_UNMUTE, + ACTION_TOGGLE, + ACTION_ISMUTED, + ACTION_SETDEFAULT, + ACTION_GETPROFILE, + ACTION_SETPROFILE, + ACTION_MOVE, + ACTION_KILL, + ACTION_INVALID, +}; + +static enum DeviceType opt_devtype; +static enum Action opt_action; +static const char* opt_device; +static const char* opt_card; + +static const int kMinVolume = 0; +static const int kMaxVolume = 150; +static const int kMinBalance = -100; +static const int kMaxBalance = 100; + +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! */ + return NULL; +} + +static enum Action string_to_action(const char* str) { + static std::map<string, enum Action> actionmap = { + { "defaults", ACTION_DEFAULTS }, + { "list", ACTION_LIST }, + { "list-cards", ACTION_LISTCARDS }, + { "list-profiles", ACTION_LISTPROFILES }, + { "get-volume", ACTION_GETVOL }, + { "set-volume", ACTION_SETVOL }, + { "get-balance", ACTION_GETBAL }, + { "set-balance", ACTION_SETBAL }, + { "adj-balance", ACTION_ADJBAL }, + { "increase", ACTION_INCREASE }, + { "decrease", ACTION_DECREASE }, + { "mute", ACTION_MUTE }, + { "unmute", ACTION_UNMUTE }, + { "toggle", ACTION_TOGGLE }, + { "is-muted", ACTION_ISMUTED }, + { "set-default", ACTION_SETDEFAULT }, + { "get-profile", ACTION_GETPROFILE }, + { "set-profile", ACTION_SETPROFILE }, + { "move", ACTION_MOVE }, + { "kill", ACTION_KILL } + }; + + try { + return actionmap.at(str); + } catch(std::out_of_range) { + errx(1, "error: Invalid option specified: %s", str); + } +} + +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(Pulse& ponymix, string arg, enum DeviceType type) { + switch (type) { + case DEVTYPE_SINK: + return ponymix.GetSink(arg); + case DEVTYPE_SOURCE: + return ponymix.GetSource(arg); + case DEVTYPE_SOURCE_OUTPUT: + return ponymix.GetSourceOutput(arg); + case DEVTYPE_SINK_INPUT: + return ponymix.GetSinkInput(arg); + default: + return nullptr; + } +} + +static Device* string_to_device_or_die(Pulse& ponymix, + string arg, + enum DeviceType type) { + Device* device = string_to_device(ponymix, arg, type); + if (device == nullptr) errx(1, "no match found for device: %s", arg.c_str()); + return device; +} + +static void Print(const Device& device) { + printf("%s %d: %s\n" + " %s\n" + " Avg. Volume: %d%%\n", + type_to_string(device.Type()), + device.Index(), + device.Name().c_str(), + device.Desc().c_str(), + device.Volume()); +} + +static void Print(const Card& card) { + printf("%s\n", card.Name().c_str()); +} + +static void Print(const Profile& profile, bool active) { + printf("%s: %s%s\n", + profile.name.c_str(), profile.desc.c_str(), active ? " [active]" : ""); +} + +static int ShowDefaults(Pulse& ponymix, + int argc _unused_, + char* argv[] _unused_) { + const auto& info = ponymix.GetDefaults(); + Print(*ponymix.GetSink(info.sink)); + Print(*ponymix.GetSource(info.source)); + return 0; +} + +static int List(Pulse& ponymix, int argc _unused_, char* argv[] _unused_) { + if (argc != 0) errx(1, "error: list requires 0 arguments"); + + const auto& sinks = ponymix.GetSinks(); + for (const auto& s : sinks) Print(s); + + const auto& sources = ponymix.GetSources(); + for (const auto& s : sources) Print(s); + + const auto& sinkinputs = ponymix.GetSinkInputs(); + for (const auto& s : sinkinputs) Print(s); + + const auto& sourceoutputs = ponymix.GetSourceOutputs(); + for (const auto& s : sourceoutputs) Print(s); + + return 0; +} + +static int ListCards(Pulse& ponymix, int argc _unused_, char* argv[] _unused_) { + if (argc != 0) errx(1, "error: list-cards requires 0 arguments"); + + const auto& cards = ponymix.GetCards(); + for (const auto& c : cards) Print(c); + + return 0; +} + +static int ListProfiles(Pulse& ponymix, int argc _unused_, char* argv[] _unused_) { + if (argc != 0) errx(1, "error: list-profiles requires 0 arguments"); + + // TODO: figure out how to get a list of cards? + auto card = ponymix.GetCard(opt_card); + if (card == nullptr) errx(1, "error: no match found for card: %s", opt_card); + + const auto& profiles = card->Profiles(); + for (const auto& p : profiles) Print(p, p.name == card->ActiveProfile().name); + + return 0; +} + +static int GetVolume(Pulse& ponymix, int argc, char* argv[] _unused_) { + if (argc != 0) errx(1, "error: get-volume requires 0 arguments"); + + auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype); + printf("%d\n", device->Volume()); + return 0; +} + +static int SetVolume(Pulse& ponymix, int argc, char* argv[]) { + if (argc != 1) errx(1, "error: set-volume requires exactly 1 argument"); + + 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(Pulse& ponymix, int argc, char* argv[] _unused_) { + if (argc != 0) errx(1, "error: get-balance requires 0 arguments"); + + auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype); + printf("%d\n", device->Balance()); + return 0; +} + +static int SetBalance(Pulse& ponymix, int argc, char* argv[]) { + if (argc != 1) errx(1, "error: set-balance requires exactly 1 argument"); + + 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(Pulse& ponymix, int argc, char* argv[]) { + if (argc != 1) errx(1, "error: adj-balance requires exactly 1 argument"); + + 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(Pulse& ponymix, int argc, char* argv[]) { + if (argc != 1) errx(1, "error: increase requires exactly 1 argument"); + + 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(Pulse& ponymix, int argc, char* argv[]) { + if (argc != 1) errx(1, "error: decrease requires exactly 1 argument"); + + 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(Pulse& ponymix, int argc, char* argv[] _unused_) { + if (argc != 0) errx(1, "error: mute requires 0 arguments"); + + 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(Pulse& ponymix, int argc, char* argv[] _unused_) { + if (argc != 0) errx(1, "error: unmute requires 0 arguments"); + + 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(Pulse& ponymix, int argc, char* argv[] _unused_) { + if (argc != 0) errx(1, "error: toggle requires 0 arguments"); + + 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(Pulse& ponymix, int argc, char* argv[] _unused_) { + if (argc != 0) errx(1, "error: is-muted requires 0 arguments"); + + auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype); + return !ponymix.IsMuted(*device); +} + +static int SetDefault(Pulse& ponymix, int argc, char* argv[] _unused_) { + if (argc != 0) errx(1, "error: set-default requires 0 arguments"); + + auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype); + + return !ponymix.SetDefault(*device); +} + +static int GetProfile(Pulse& ponymix, int argc, char* argv[] _unused_) { + if (argc != 0) errx(1, "error: get-profile requires 0 arguments"); + + auto card = ponymix.GetCard(opt_card); + if (card == nullptr) errx(1, "error: no match found for card: %s", opt_card); + + printf("%s\n", card->ActiveProfile().name.c_str()); + + return true; +} + +static int SetProfile(Pulse& ponymix, int argc, char* argv[] _unused_) { + if (argc != 1) errx(1, "error: set-profile requires 1 argument"); + + auto card = ponymix.GetCard(opt_card); + if (card == nullptr) errx(1, "error: no match found for card: %s", opt_card); + + return !ponymix.SetProfile(*card, argv[0]); +} + +static int Move(Pulse& ponymix, int argc, char* argv[]) { + if (argc != 1) errx(1, "error: move requires 1 argument"); + + // 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], opt_devtype); + + return !ponymix.Move(*source, *target); +} + +static int Kill(Pulse& ponymix, int argc, char* argv[] _unused_) { + if (argc != 0) errx(1, "error: set-default requires 0 arguments"); + + auto device = string_to_device_or_die(ponymix, opt_device, opt_devtype); + + return !ponymix.Kill(*device); +} + +static int (*fn[])(Pulse& ponymix, int argc, char* argv[]) = { + [ACTION_DEFAULTS] = ShowDefaults, + [ACTION_LIST] = List, + [ACTION_LISTCARDS] = ListCards, + [ACTION_LISTPROFILES] = ListProfiles, + [ACTION_GETVOL] = GetVolume, + [ACTION_SETVOL] = SetVolume, + [ACTION_GETBAL] = GetBalance, + [ACTION_SETBAL] = SetBalance, + [ACTION_ADJBAL] = AdjBalance, + [ACTION_INCREASE] = IncreaseVolume, + [ACTION_DECREASE] = DecreaseVolume, + [ACTION_MUTE] = Mute, + [ACTION_UNMUTE] = Unmute, + [ACTION_TOGGLE] = ToggleMute, + [ACTION_ISMUTED] = IsMuted, + [ACTION_SETDEFAULT] = SetDefault, + [ACTION_GETPROFILE] = GetProfile, + [ACTION_SETPROFILE] = SetProfile, + [ACTION_MOVE] = Move, + [ACTION_KILL] = Kill, +}; + +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", stdout); + + fputs("\nCommon Commands:\n" + " list list available devices\n" + " list-cards list available cards\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("\nCard Commands:\n" + " list-profiles list available profiles for a card\n" + " get-profile get active profile for card\n" + " set-profile PROFILE set profile for a card\n" + + "\nDevice Commands:\n" + " defaults list default devices (default command)\n" + " set-default DEVICE set default device by ID\n" + + "\nApplication Commands:\n" + " move DEVICE move target device to DEVICE\n" + " kill DEVICE kill target DEVICE\n", stdout); + + exit(EXIT_SUCCESS); +} + +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' }, + { 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; + default: + return false; + } + } + + return true; +} + +int main(int argc, char* argv[]) { + Pulse ponymix("ponymix"); + ponymix.Populate(); + + // defaults + opt_action = ACTION_DEFAULTS; + opt_devtype = DEVTYPE_SINK; + opt_device = ponymix.GetDefaults().sink.c_str(); + opt_card = ponymix.GetCards()[0].Name().c_str(); + + if (!parse_options(argc, argv)) return 1; + argc -= optind; + argv += optind; + + if (argc > 0) { + opt_action = string_to_action(argv[0]); + argc--; + argv++; + } + + return fn[opt_action](ponymix, argc, argv); +} + +// vim: set et ts=2 sw=2: diff --git a/pulse.cc b/pulse.cc new file mode 100644 index 0000000..c09b61b --- /dev/null +++ b/pulse.cc @@ -0,0 +1,561 @@ +// Self +#include "pulse.h" + +// C +#include <err.h> +#include <math.h> +#include <stdio.h> +#include <stdlib.h> + +// C++ +#include <string> +#include <algorithm> + +// 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 + +Pulse::Pulse(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 +// +Pulse::~Pulse() { + pa_context_unref(context_); + pa_mainloop_free(mainloop_); +} + +void Pulse::Populate() { + populate_server_info(); + populate_sinks(); + populate_sources(); + populate_cards(); +} + +Card* Pulse::GetCard(const uint32_t& index) { + for (Card& card : cards_) { + if (card.index_ == index) return &card; + } + return nullptr; +} + +Card* Pulse::GetCard(const string& name) { + for (Card& card : cards_) { + if (card.name_ == name) return &card; + } + return nullptr; +} + +Device* Pulse::get_device(vector<Device>& devices, const uint32_t& index) { + for (Device& device : devices) { + if (device.index_ == index) return &device; + } + return nullptr; +} + +Device* Pulse::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* Pulse::GetSink(const uint32_t& index) { + return get_device(sinks_, index); +} + +Device* Pulse::GetSink(const string& name) { + return get_device(sinks_, name); +} + +Device* Pulse::GetSource(const uint32_t& index) { + return get_device(sources_, index); +} + +Device* Pulse::GetSource(const string& name) { + return get_device(sources_, name); +} + +Device* Pulse::GetSinkInput(const uint32_t& index) { + return get_device(sink_inputs_, index); +} + +Device* Pulse::GetSinkInput(const string& name) { + return get_device(sink_inputs_, name); +} + +Device* Pulse::GetSourceOutput(const uint32_t& index) { + return get_device(source_outputs_, index); +} + +Device* Pulse::GetSourceOutput(const string& name) { + return get_device(source_outputs_, name); +} + +void Pulse::mainloop_iterate(pa_operation* op) { + int r; + while (pa_operation_get_state(op) == PA_OPERATION_RUNNING) { + pa_mainloop_iterate(mainloop_, 1, &r); + } +} + +void Pulse::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 Pulse::populate_server_info() { + pa_operation* op = pa_context_get_server_info(context_, + server_info_cb, + &defaults_); + mainloop_iterate(op); + pa_operation_unref(op); +} + +void Pulse::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 Pulse::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 Pulse::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 Pulse::SetVolume(Device& device, long volume) { + int success; + + if (device.ops_.SetVolume == nullptr) { + warnx("device does not support setting balance."); + return false; + } + + 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 Pulse::IncreaseVolume(Device& device, long increment) { + return SetVolume(device, + volume_range_.clamp(device.volume_percent_ + increment)); +} + +bool Pulse::DecreaseVolume(Device& device, long increment) { + return SetVolume(device, + volume_range_.clamp(device.volume_percent_ - increment)); +} + +bool Pulse::SetBalance(Device& device, long balance) { + int success; + + if (device.ops_.SetVolume == nullptr) { + warnx("device does not support setting balance."); + return false; + } + + 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 Pulse::IncreaseBalance(Device& device, long increment) { + return SetBalance(device, + balance_range_.clamp(device.balance_ + increment)); +} + +bool Pulse::DecreaseBalance(Device& device, long increment) { + return SetBalance(device, + balance_range_.clamp(device.balance_ - increment)); +} + +int Pulse::GetVolume(const Device& device) const { + return device.Volume(); +} + +int Pulse::GetBalance(const Device& device) const { + return device.Balance(); +} + +bool Pulse::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 Pulse::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 Pulse::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 Pulse::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 Pulse::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), + desc_(info->description), + mute_(info->mute) { + 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_.SetDefault = pa_context_set_default_sink; +} + +Device::Device(const pa_source_info* info) : + type_(DEVTYPE_SOURCE), + index_(info->index), + name_(info->name), + desc_(info->description), + mute_(info->mute) { + 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_.SetDefault = pa_context_set_default_source; +} + +Device::Device(const pa_sink_input_info* info) : + type_(DEVTYPE_SINK_INPUT), + index_(info->index), + name_(info->name), + desc_(pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_NAME)), + mute_(info->mute) { + 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_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; +} + +Device::Device(const pa_source_output_info* info) : + type_(DEVTYPE_SOURCE_OUTPUT), + index_(info->index), + name_(info->name), + desc_(pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_NAME)), + mute_(info->mute) { + update_volume(info->volume); + volume_percent_ = volume_as_percent(&volume_); + balance_ = pa_cvolume_get_balance(&volume_, &channels_) * 100.0; + + 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; +} + +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: @@ -0,0 +1,216 @@ +#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 Pulse; + + 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_; + 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 Pulse; + + uint32_t index_; + string name_; + uint32_t owner_module_; + string driver_; + vector<Profile> profiles_; + Profile active_profile_; +}; + +struct ServerInfo { + string sink; + string source; +}; + +template<typename T> +struct Range { + Range(T min, T max) : min(min), max(max) {} + + T clamp(T value) { + return value < min ? min : (value > max ? max : value); + } + + T min; + T max; +}; + +class Pulse { + public: + Pulse(string client_name); + ~Pulse(); + + // Populates all known devices and cards. Any currently known + // devices and cards are cleared before the new data is stored. + void Populate(); + + // 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, or all cards. + Card* GetCard(const uint32_t& index); + Card* GetCard(const string& name); + 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: |