diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | README | 2 | ||||
-rw-r--r-- | ponymix.1 | 1 | ||||
-rw-r--r-- | ponymix.c | 1025 | ||||
-rwxr-xr-x | runtests | 63 |
5 files changed, 1092 insertions, 1 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af83c41 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +ponymix +ponymix.o @@ -0,0 +1,2 @@ +This was a quick hack to create a CLI pulse volume control. +I have no idea how (or if) it works with multiple sinks. @@ -93,7 +93,6 @@ List profiles for a card. 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.c b/ponymix.c new file mode 100644 index 0000000..a3844b7 --- /dev/null +++ b/ponymix.c @@ -0,0 +1,1025 @@ +/* Copyright (c) 2012 Dave Reisner + * + * ponymix.c + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#define _GNU_SOURCE +#include <errno.h> +#include <err.h> +#include <getopt.h> +#include <math.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <pulse/pulseaudio.h> + +#define UNUSED __attribute__((unused)) + +#define CLAMP(x, low, high) \ + __extension__ ({ \ + typeof(x) _x = (x); \ + typeof(low) _low = (low); \ + typeof(high) _high = (high); \ + ((_x > _high) ? _high : ((_x < _low) ? _low : _x)); \ + }) + +#define COLOR_RESET "\033[0m" +#define COLOR_BOLD "\033[1m" +#define COLOR_ITALIC "\033[3m" +#define COLOR_UL "\033[4m" +#define COLOR_BLINK "\033[5m" +#define COLOR_REV "\033[7m" +#define COLOR_RED "\033[31m" +#define COLOR_GREEN "\033[32m" +#define COLOR_YELLOW "\033[33m" +#define COLOR_BLUE "\033[34m" +#define COLOR_MAGENTA "\033[35m" +#define COLOR_CYAN "\033[36m" +#define COLOR_WHITE "\033[37m" + +enum mode { + MODE_DEVICE = 1 << 0, + MODE_APP = 1 << 1, + MODE_ANY = (1 << 2) - 1, +}; + +enum action { + ACTION_DEFAULTS = 0, + ACTION_LIST, + 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_MOVE, + ACTION_KILL, + ACTION_GETPROFILE, + ACTION_SETPROFILE, + ACTION_INVALID, +}; + +struct action_t { + const char *cmd; + int argreq; + enum mode argmode; +}; + +static struct action_t actions[ACTION_INVALID] = { + [ACTION_DEFAULTS] = { "defaults", 0, MODE_DEVICE }, + [ACTION_LIST] = { "list", 0, MODE_ANY }, + [ACTION_GETVOL] = { "get-volume", 0, MODE_ANY }, + [ACTION_SETVOL] = { "set-volume", 1, MODE_ANY }, + [ACTION_GETBAL] = { "get-balance", 0, MODE_ANY }, + [ACTION_SETBAL] = { "set-balance", 1, MODE_ANY }, + [ACTION_ADJBAL] = { "adj-balance", 1, MODE_ANY }, + [ACTION_INCREASE] = { "increase", 1, MODE_ANY }, + [ACTION_DECREASE] = { "decrease", 1, MODE_ANY }, + [ACTION_MUTE] = { "mute", 0, MODE_ANY }, + [ACTION_UNMUTE] = { "unmute", 0, MODE_ANY }, + [ACTION_TOGGLE] = { "toggle", 0, MODE_ANY }, + [ACTION_ISMUTED] = { "is-muted", 0, MODE_ANY }, + [ACTION_SETDEFAULT] = { "set-default", 1, MODE_DEVICE }, + [ACTION_MOVE] = { "move", 2, MODE_APP }, + [ACTION_KILL] = { "kill", 1, MODE_APP }, + [ACTION_GETPROFILE] = { "list-profiles", 0, MODE_DEVICE }, + [ACTION_SETPROFILE] = { "set-profile", 1, MODE_DEVICE }, +}; + +struct io_t { + uint32_t idx; + char *name; + char *desc; + const char *pp_name; + pa_cvolume volume; + pa_channel_map channels; + int volume_percent; + int balance; + int mute; + + struct ops_t { + pa_operation *(*mute)(pa_context *, uint32_t, int, pa_context_success_cb_t, void *); + pa_operation *(*setvol)(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 *(*move)(pa_context *, uint32_t, uint32_t, pa_context_success_cb_t, void *); + pa_operation *(*kill)(pa_context *, uint32_t, pa_context_success_cb_t, void *); + } op; + + struct io_t *next; + struct io_t *prev; +}; + +struct cb_data_t { + struct io_t **list; + const char *glob; +}; + +struct pulseaudio_t { + pa_context *cxt; + pa_mainloop *mainloop; + + char *default_sink; + char *default_source; +}; + +struct colstr_t { + const char *name; + const char *over9000; + const char *veryhigh; + const char *high; + const char *mid; + const char *low; + const char *verylow; + const char *mute; + const char *nc; +}; + +struct runtime_t { + enum mode mode; + const char *pp_name; + + int (*get_default)(struct pulseaudio_t *, struct io_t **); + int (*get_by_name)(struct pulseaudio_t *, struct io_t **, const char *, enum mode); +}; + +struct arg_t { + long value; + struct io_t *devices; + struct io_t *target; +}; + +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; +} + +static void io_list_add(struct io_t **list, struct io_t *node) +{ + struct io_t *head = *list; + + if (head == NULL) + head = node; + else { + head->prev->next = node; + node->prev = head->prev; + } + + head->prev = node; + *list = head; +} + +static void populate_levels(struct io_t *node) +{ + node->volume_percent = (int)(((double)pa_cvolume_avg(&node->volume) * 100) + / PA_VOLUME_NORM); + node->balance = (int)((double)pa_cvolume_get_balance(&node->volume, + &node->channels) * 100); +} + +#define IO_NEW(io, info, pp) \ + io = calloc(1, sizeof(struct io_t)); \ + io->idx = info->index; \ + io->mute = info->mute; \ + io->name = strdup(info->name ? info->name : ""); \ + io->pp_name = pp; \ + memcpy(&io->volume, &info->volume, sizeof(pa_cvolume)); \ + memcpy(&io->channels, &info->channel_map, sizeof(pa_channel_map)); \ + populate_levels(io); + +static struct io_t *sink_new(const pa_sink_info *info) +{ + struct io_t *sink; + + IO_NEW(sink, info, "sink"); + sink->desc = strdup(info->description); + sink->op.mute = pa_context_set_sink_mute_by_index; + sink->op.setvol = pa_context_set_sink_volume_by_index; + sink->op.setdefault = pa_context_set_default_sink; + + return sink; +} + +static struct io_t *sink_input_new(const pa_sink_input_info *info) +{ + struct io_t *sink; + const char *app_name; + + IO_NEW(sink, info, "output"); + app_name = pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_NAME); + if (app_name) + sink->desc = strdup(app_name ? app_name : ""); + sink->op.mute = pa_context_set_sink_input_mute; + sink->op.setvol = pa_context_set_sink_input_volume; + sink->op.move = pa_context_move_sink_input_by_index; + sink->op.kill = pa_context_kill_sink_input; + + return sink; +} + +static struct io_t *source_new(const pa_source_info *info) +{ + struct io_t *source; + + IO_NEW(source, info, "source"); + source->desc = strdup(info->description); + source->op.mute = pa_context_set_source_mute_by_index; + source->op.setvol = pa_context_set_source_volume_by_index; + source->op.setdefault = pa_context_set_default_source; + + return source; +} + +static struct io_t *source_output_new(const pa_source_output_info *info) +{ + struct io_t *source; + const char *desc; + + IO_NEW(source, info, "input"); + desc = pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_NAME); + source->desc = strdup(desc ? desc : ""); + source->op.mute = pa_context_set_source_output_mute; + source->op.setvol = pa_context_set_source_output_volume; + source->op.move = pa_context_move_source_output_by_index; + source->op.kill = pa_context_kill_source_output; + + return source; +} + +static void sink_add_cb(pa_context UNUSED *c, const pa_sink_info *i, int eol, + void *raw) +{ + struct cb_data_t *pony = raw; + if (eol) + return; + if (pony->glob && strstr(i->name, pony->glob) == NULL) + return; + io_list_add(pony->list, sink_new(i)); +} + +static void sink_input_add_cb(pa_context UNUSED *c, const pa_sink_input_info *i, int eol, + void *raw) +{ + struct cb_data_t *pony = raw; + if (eol) + return; + if (pony->glob && strstr(i->name, pony->glob) == NULL && + strstr(pa_proplist_gets(i->proplist, PA_PROP_APPLICATION_NAME), pony->glob) == NULL) + return; + io_list_add(pony->list, sink_input_new(i)); +} + +static void source_add_cb(pa_context UNUSED *c, const pa_source_info *i, int eol, void *raw) +{ + struct cb_data_t *pony = raw; + if (eol) + return; + if (pony->glob && strstr(i->name, pony->glob) == NULL) + return; + io_list_add(pony->list, source_new(i)); +} + +static void source_output_add_cb(pa_context UNUSED *c, const pa_source_output_info *i, int eol, + void *raw) +{ + struct cb_data_t *pony = raw; + if (eol) + return; + if (pony->glob && strstr(i->name, pony->glob) == NULL && + strstr(pa_proplist_gets(i->proplist, PA_PROP_APPLICATION_NAME), pony->glob) == NULL) + return; + io_list_add(pony->list, source_output_new(i)); +} + +static void server_info_cb(pa_context UNUSED *c, const pa_server_info *i, void *raw) +{ + struct pulseaudio_t *pulse = raw; + pulse->default_sink = strdup(i->default_sink_name); + pulse->default_source = strdup(i->default_source_name); +} + +static void connect_state_cb(pa_context *cxt, void *raw) +{ + enum pa_context_state *state = raw; + *state = pa_context_get_state(cxt); +} + +static void success_cb(pa_context UNUSED *c, int success, void *raw) +{ + int *rc = raw; + *rc = success; +} + +static void pulse_async_wait(struct pulseaudio_t *pulse, pa_operation *op) +{ + while (pa_operation_get_state(op) == PA_OPERATION_RUNNING) + pa_mainloop_iterate(pulse->mainloop, 1, NULL); +} + +static int set_volume(struct pulseaudio_t *pulse, struct io_t *dev, long v) +{ + int success = 0; + pa_cvolume *vol = pa_cvolume_set(&dev->volume, dev->volume.channels, + (int)fmax((double)(v + .5) * PA_VOLUME_NORM / 100, 0)); + + pa_operation *op = dev->op.setvol(pulse->cxt, dev->idx, vol, + success_cb, &success); + pulse_async_wait(pulse, op); + + if (success) + printf("%ld\n", v); + else { + int err = pa_context_errno(pulse->cxt); + fprintf(stderr, "failed to set volume: %s\n", pa_strerror(err)); + } + + pa_operation_unref(op); + + return !success; +} + +static int set_balance(struct pulseaudio_t *pulse, struct io_t *dev, long v) +{ + int success = 0; + pa_cvolume *vol; + pa_operation *op; + + if (pa_channel_map_valid(&dev->channels) == 0) { + warnx("can't set balance on that device."); + return 1; + } + + vol = pa_cvolume_set_balance(&dev->volume, &dev->channels, v / 100.0); + op = dev->op.setvol(pulse->cxt, dev->idx, vol, success_cb, &success); + pulse_async_wait(pulse, op); + + if (success) + printf("%ld\n", v); + else { + int err = pa_context_errno(pulse->cxt); + warnx("failed to set balance: %s", pa_strerror(err)); + } + + pa_operation_unref(op); + + return !success; +} + +static int set_mute(struct pulseaudio_t *pulse, struct io_t *dev, int mute) +{ + int success = 0; + pa_operation* op; + + /* new effective volume */ + printf("%d\n", mute ? 0 : dev->volume_percent); + + op = dev->op.mute(pulse->cxt, dev->idx, mute, + success_cb, &success); + + pulse_async_wait(pulse, op); + + if (!success) { + int err = pa_context_errno(pulse->cxt); + fprintf(stderr, "failed to mute device: %s\n", pa_strerror(err)); + } + + pa_operation_unref(op); + + return !success; +} + +static int kill_client(struct pulseaudio_t *pulse, struct io_t *dev) +{ + int success = 0; + pa_operation *op; + + if (dev->op.kill == NULL) { + warnx("only clients can be killed"); + return 1; + } + + op = dev->op.kill(pulse->cxt, dev->idx, success_cb, &success); + pulse_async_wait(pulse, op); + + if (!success) { + int err = pa_context_errno(pulse->cxt); + fprintf(stderr, "failed to kill client: %s\n", pa_strerror(err)); + } + + pa_operation_unref(op); + + return !success; +} + +static int move_client(struct pulseaudio_t *pulse, struct io_t *dev, struct io_t *target) +{ + int success = 0; + pa_operation* op; + + if (dev->op.move == NULL) { + warnx("only clients can be moved"); + return 1; + } + + if (target == NULL) { + warnx("no destination to move to"); + return 1; + } + + op = dev->op.move(pulse->cxt, dev->idx, target->idx, success_cb, &success); + pulse_async_wait(pulse, op); + + if (!success) { + int err = pa_context_errno(pulse->cxt); + fprintf(stderr, "failed to move client: %s\n", pa_strerror(err)); + } + + pa_operation_unref(op); + + return !success; +} + +static void print_one(struct colstr_t *colstr, struct io_t *dev) +{ + const char *level; + const char *mute = dev->mute ? "[Muted]" : ""; + + if (dev->volume_percent < 20) + level = colstr->verylow; + else if (dev->volume_percent < 40) + level = colstr->low; + else if (dev->volume_percent < 60) + level = colstr->mid; + else if (dev->volume_percent < 80) + level = colstr->high; + else if (dev->volume_percent <= 100) + level = colstr->veryhigh; + else + level = colstr->over9000; + + printf("%s%s %d:%s %s\n", colstr->name, dev->pp_name, dev->idx, + colstr->nc, dev->name); + printf(" %s\n", dev->desc); + printf(" Avg. Volume: %s%d%%%s %s%s%s\n", level, dev->volume_percent, + colstr->nc, colstr->mute, mute, colstr->nc); +} + +static void print_all(struct io_t *head) +{ + struct io_t *node = head; + struct colstr_t colstr; + + if (isatty(fileno(stdout))) { + colstr.name = COLOR_BOLD; + colstr.over9000 = COLOR_REV COLOR_RED; + colstr.veryhigh = COLOR_RED; + colstr.high = COLOR_MAGENTA; + colstr.mid = COLOR_YELLOW; + colstr.low = COLOR_GREEN; + colstr.verylow = COLOR_BLUE; + colstr.mute = COLOR_BOLD COLOR_RED; + colstr.nc = COLOR_RESET; + } else { + colstr.name = ""; + colstr.over9000 = ""; + colstr.veryhigh = ""; + colstr.high = ""; + colstr.mid = ""; + colstr.low = ""; + colstr.verylow = ""; + colstr.mute = ""; + colstr.nc = ""; + } + + while (node) { + print_one(&colstr, node); + node = node->next; + } +} + +static int populate_sinks(struct pulseaudio_t *pulse, struct io_t **list, const char *filter, enum mode mode) +{ + pa_operation *op; + struct cb_data_t pony = { .list = list, .glob = filter }; + + switch (mode) { + case MODE_APP: + op = pa_context_get_sink_input_info_list(pulse->cxt, sink_input_add_cb, &pony); + break; + case MODE_DEVICE: + default: + op = pa_context_get_sink_info_list(pulse->cxt, sink_add_cb, &pony); + } + + pulse_async_wait(pulse, op); + pa_operation_unref(op); + return 0; +} + +static int find_sink(struct pulseaudio_t *pulse, struct io_t **list, const char *name, enum mode mode) +{ + long id; + + if (xstrtol(name, &id) < 0) + populate_sinks(pulse, list, name, mode); + else { + pa_operation *op; + struct cb_data_t pony = { .list = list }; + + switch (mode) { + case MODE_APP: + op = pa_context_get_sink_input_info(pulse->cxt, (uint32_t)id, sink_input_add_cb, &pony); + break; + case MODE_DEVICE: + default: + op = pa_context_get_sink_info_by_index(pulse->cxt, (uint32_t)id, sink_add_cb, &pony); + } + + pulse_async_wait(pulse, op); + pa_operation_unref(op); + } + return 0; +} + +static int get_default_sink(struct pulseaudio_t *pulse, struct io_t **list) +{ + pa_operation *op; + struct cb_data_t pony = { .list = list }; + + op = pa_context_get_sink_info_by_name(pulse->cxt, pulse->default_sink, sink_add_cb, &pony); + + pulse_async_wait(pulse, op); + pa_operation_unref(op); + return 0; +} + +static int populate_sources(struct pulseaudio_t *pulse, struct io_t **list, const char *filter, enum mode mode) +{ + pa_operation *op; + struct cb_data_t pony = { .list = list, .glob = filter }; + + switch (mode) { + case MODE_APP: + op = pa_context_get_source_output_info_list(pulse->cxt, source_output_add_cb, &pony); + break; + case MODE_DEVICE: + default: + op = pa_context_get_source_info_list(pulse->cxt, source_add_cb, &pony); + } + + pulse_async_wait(pulse, op); + pa_operation_unref(op); + return 0; +} + +static void pa_card_info_cb(pa_context UNUSED *c, const pa_card_info *info, int eol, void UNUSED *userdata) { + int i; + const char *active_profile; + + if (eol) return; + + active_profile = info->active_profile->name; + + for (i = 0; info->profiles && info->profiles[i].name; i++) { + printf("profile %d: %s%s\n %s\n", + i, info->profiles[i].name, + strcmp(info->profiles[i].name, active_profile) == 0 ? " (active)" : "", + info->profiles[i].description); + } +} + +static int list_profiles(struct pulseaudio_t *pulse, struct io_t *device) { + pa_operation *op; + + op = pa_context_get_card_info_by_index(pulse->cxt, device->idx, pa_card_info_cb, pulse); + pulse_async_wait(pulse, op); + pa_operation_unref(op); + + return 0; +} + +static int set_profile(struct pulseaudio_t *pulse, struct io_t *device, const char *profile) { + int success = 0; + pa_operation *op; + + op = pa_context_set_card_profile_by_index(pulse->cxt, device->idx, profile, success_cb, &success); + pulse_async_wait(pulse, op); + pa_operation_unref(op); + + return !success; +} + +static int find_source(struct pulseaudio_t *pulse, struct io_t **list, const char *name, enum mode mode) +{ + long id; + + if (xstrtol(name, &id) < 0) + populate_sources(pulse, list, name, mode); + else { + pa_operation *op; + struct cb_data_t pony = { .list = list }; + + switch (mode) { + case MODE_APP: + op = pa_context_get_source_output_info(pulse->cxt, (uint32_t)id, source_output_add_cb, &pony); + break; + case MODE_DEVICE: + default: + op = pa_context_get_source_info_by_index(pulse->cxt, (uint32_t)id, source_add_cb, &pony); + } + + pulse_async_wait(pulse, op); + pa_operation_unref(op); + } + + return 0; +} + +static int get_default_source(struct pulseaudio_t *pulse, struct io_t **list) +{ + pa_operation *op; + struct cb_data_t pony = { .list = list }; + + op = pa_context_get_source_info_by_name(pulse->cxt, pulse->default_source, source_add_cb, &pony); + + pulse_async_wait(pulse, op); + pa_operation_unref(op); + return 0; +} + +static int set_default(struct pulseaudio_t *pulse, struct io_t *dev) +{ + int success = 0; + pa_operation *op; + + if (dev->op.setdefault == NULL) { + warnx("valid operation only for devices"); + return 1; + } + + op = dev->op.setdefault(pulse->cxt, dev->name, success_cb, &success); + pulse_async_wait(pulse, op); + + if (!success) { + int err = pa_context_errno(pulse->cxt); + fprintf(stderr, "failed to set default %s to %s: %s\n", dev->pp_name, + dev->name, pa_strerror(err)); + } + + pa_operation_unref(op); + + return !success; +} + +static int pulse_init(struct pulseaudio_t *pulse) +{ + pa_operation *op; + enum pa_context_state state = PA_CONTEXT_CONNECTING; + + pulse->mainloop = pa_mainloop_new(); + pulse->cxt = pa_context_new(pa_mainloop_get_api(pulse->mainloop), "bestpony"); + pulse->default_sink = NULL; + pulse->default_source = NULL; + + pa_context_set_state_callback(pulse->cxt, connect_state_cb, &state); + pa_context_connect(pulse->cxt, NULL, PA_CONTEXT_NOFLAGS, NULL); + while (state != PA_CONTEXT_READY && state != PA_CONTEXT_FAILED) + pa_mainloop_iterate(pulse->mainloop, 1, NULL); + + if (state != PA_CONTEXT_READY) { + fprintf(stderr, "failed to connect to pulse daemon: %s\n", + pa_strerror(pa_context_errno(pulse->cxt))); + return 1; + } + + op = pa_context_get_server_info(pulse->cxt, server_info_cb, pulse); + pulse_async_wait(pulse, op); + pa_operation_unref(op); + return 0; +} + +static void pulse_deinit(struct pulseaudio_t *pulse) +{ + pa_context_disconnect(pulse->cxt); + pa_mainloop_free(pulse->mainloop); + free(pulse->default_sink); + free(pulse->default_source); +} + +static void __attribute__((__noreturn__)) usage(FILE *out) +{ + fprintf(out, "usage: %s [options] <command>...\n", program_invocation_short_name); + fputs("\nOptions:\n" + " -h, --help display this help and exit\n\n" + + " -d, --device set device mode (default)\n" + " -a, --app set application mode\n\n" + + " -o<name>, --sink=<name> control a sink other than the default\n" + " --output=<name>\n" + " -i<name>, --source=<name> control a source\n" + " --input=<name>\n", out); + + fputs("\nCommon Commands:\n" + " list list available devices\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", out); + fputs(" decrease VALUE decrease volume\n" + " mute mute device\n" + " unmute unmute device\n" + " toggle toggle mute\n" + " is-muted check if muted\n", out); + + fputs("\nDevice Commands:\n" + " defaults list default devices (default command)\n" + " set-default DEVICE_ID set default device by ID\n" + " list-profiles list available device profiles\n" + " set-profile PROFILE set profile for device\n" + + "\nApplication Commands:\n" + " move APP_ID DEVICE_ID move application stream by ID to device ID\n" + " kill APP_ID kill an application stream by ID\n", out); + + exit(out == stderr ? EXIT_FAILURE : EXIT_SUCCESS); +} + +static enum action string_to_verb(const char *string) +{ + size_t i; + + for (i = 0; i < ACTION_INVALID; i++) + if (strcmp(actions[i].cmd, string) == 0) + break; + + return i; +} + +static int do_verb(struct pulseaudio_t *pulse, enum action verb, struct arg_t *arg, char **argv) +{ + struct io_t *device = arg->devices; + struct io_t *target = arg->target; + + switch (verb) { + case ACTION_GETVOL: + printf("%d\n", device->volume_percent); + return 0; + case ACTION_SETVOL: + return set_volume(pulse, device, CLAMP(arg->value, 0, 150)); + case ACTION_GETBAL: + printf("%d\n", device->balance); + return 0; + case ACTION_SETBAL: + return set_balance(pulse, device, CLAMP(arg->value, -100, 100)); + case ACTION_ADJBAL: + return set_balance(pulse, device, + CLAMP(device->balance + arg->value, -100, 100)); + case ACTION_INCREASE: + if (device->volume_percent > 100) { + printf("%d\n", device->volume_percent); + return 0; + } + + return set_volume(pulse, device, + CLAMP(device->volume_percent + arg->value, 0, 100)); + case ACTION_DECREASE: + return set_volume(pulse, device, + CLAMP(device->volume_percent - arg->value, 0, 100)); + case ACTION_MUTE: + return set_mute(pulse, device, 1); + case ACTION_UNMUTE: + return set_mute(pulse, device, 0); + case ACTION_TOGGLE: + return set_mute(pulse, device, !device->mute); + case ACTION_ISMUTED: + return !device->mute; + case ACTION_MOVE: + return move_client(pulse, device, target); + case ACTION_KILL: + return kill_client(pulse, device); + case ACTION_SETDEFAULT: + return set_default(pulse, device); + case ACTION_GETPROFILE: + return list_profiles(pulse, device); + case ACTION_SETPROFILE: + return set_profile(pulse, device, argv[0]); + default: + errx(EXIT_FAILURE, "internal error: unhandled verb id %d\n", verb); + } +} + +static int get_device(struct pulseaudio_t *pulse, const char *id, + struct io_t **device, struct runtime_t *run) +{ + int rc = 0; + + /* try to find device by id or a default device */ + if (id && run->get_by_name) { + rc = run->get_by_name(pulse, device, id, run->mode); + } else if (run->get_default) { + rc = run->get_default(pulse, device); + } + + if (rc != 0) + return rc; + + /* if no device found, report an error */ + if (!*device) { + if (!run->get_default) + warnx("a valid %s id is required", run->pp_name); + else + warnx("%s not found: %s", run->pp_name, id ? id : "default"); + return 1; + } + + /* if more then one device found, report an error */ + if ((*device)->next) { + warnx("%s does not uniquely identify a %s", id, run->pp_name); + return 1; + } + + return 0; +} + +int main(int argc, char *argv[]) +{ + struct pulseaudio_t pulse; + enum action verb; + char *id = NULL; + int rc = EXIT_SUCCESS; + + struct arg_t arg = { 0, NULL, NULL }; + struct runtime_t run = { + .mode = MODE_DEVICE, + .pp_name = "sink", + .get_default = get_default_sink, + .get_by_name = find_sink + }; + + static const struct option opts[] = { + { "app", no_argument, 0, 'a' }, + { "device", no_argument, 0, 'd' }, + { "help", no_argument, 0, 'h' }, + { "sink", optional_argument, 0, 'o' }, + { "output", optional_argument, 0, 'o' }, + { "source", optional_argument, 0, 'i' }, + { "input", optional_argument, 0, 'i' }, + { 0, 0, 0, 0 }, + }; + + for (;;) { + int opt = getopt_long(argc, argv, "adhi::o::", opts, NULL); + if (opt == -1) + break; + + switch (opt) { + case 'h': + usage(stdout); + case 'd': + run.mode = MODE_DEVICE; + break; + case 'a': + run.mode = MODE_APP; + break; + case 'o': + id = optarg; + run.get_default = get_default_sink; + run.get_by_name = find_sink; + run.pp_name = "sink"; + break; + case 'i': + id = optarg; + run.get_default = get_default_source; + run.get_by_name = find_source; + run.pp_name = "source"; + break; + default: + return EXIT_FAILURE; + } + } + + if (run.mode == MODE_APP) + run.get_default = NULL; + + if (optind == argc) + verb = run.mode == MODE_DEVICE ? ACTION_DEFAULTS : ACTION_LIST; + else + verb = string_to_verb(argv[optind++]); + + if (verb == ACTION_INVALID) + errx(EXIT_FAILURE, "unknown action: %s", argv[optind - 1]); + + if (actions[verb].argreq != (argc - optind)) + errx(EXIT_FAILURE, "wrong number of args for %s command (requires %d)", + argv[optind - 1], actions[verb].argreq); + + if (!(actions[verb].argmode & run.mode)) + errx(EXIT_FAILURE, "wrong mode for %s command", argv[optind - 1]); + + /* initialize connection */ + if (pulse_init(&pulse) != 0) + return EXIT_FAILURE; + + switch (verb) { + case ACTION_DEFAULTS: + get_default_sink(&pulse, &arg.devices); + get_default_source(&pulse, &arg.devices); + print_all(arg.devices); + goto done; + case ACTION_LIST: + populate_sinks(&pulse, &arg.devices, NULL, run.mode); + populate_sources(&pulse, &arg.devices, NULL, run.mode); + print_all(arg.devices); + goto done; + case ACTION_SETVOL: + case ACTION_SETBAL: + case ACTION_ADJBAL: + case ACTION_INCREASE: + case ACTION_DECREASE: + if (xstrtol(argv[optind], &arg.value) < 0) + errx(EXIT_FAILURE, "invalid number: %s", argv[optind]); + case ACTION_GETVOL: + case ACTION_GETBAL: + case ACTION_MUTE: + case ACTION_UNMUTE: + case ACTION_TOGGLE: + case ACTION_ISMUTED: + rc = get_device(&pulse, id, &arg.devices, &run); + if (rc) + goto done; + break; + case ACTION_SETDEFAULT: + case ACTION_KILL: + case ACTION_MOVE: + rc = get_device(&pulse, argv[optind], &arg.devices, &run); + if (rc) + goto done; + break; + case ACTION_SETPROFILE: + case ACTION_GETPROFILE: + rc = get_device(&pulse, id, &arg.devices, &run); + if (rc) + goto done; + default: + break; + } + + if (verb == ACTION_MOVE) { + run.mode = MODE_DEVICE; + rc = get_device(&pulse, argv[optind + 1], &arg.target, &run); + if (rc) + goto done; + } + + rc = do_verb(&pulse, verb, &arg, &argv[optind]); + +done: + /* shut down */ + pulse_deinit(&pulse); + + return rc; +} + +/* vim: set noet ts=2 sw=2: */ diff --git a/runtests b/runtests new file mode 100755 index 0000000..d22b73b --- /dev/null +++ b/runtests @@ -0,0 +1,63 @@ +#!/bin/bash + +ponymix=$1 + +if [[ -z $ponymix ]]; then + printf 'usage: %s path-to-ponymix\n' "${0##*/}" + exit 1 +fi + +if [[ ! -x $ponymix ]]; then + printf '==> ERROR: ponymix binary not found at %s\n' "$ponymix" + exit 1 +fi + +testno=0 fail=0 pass=0 + +do_test() { + local expected=$1 verb=$2 arg=$3 result= + + (( ++testno )) + + result=$("$ponymix" "$verb" -- ${3+"$arg"} 2>/dev/null) + if [[ $result != $expected ]]; then + printf '==> test %d FAIL: expected %s, got %s\n' "$testno" "$expected" "$result" + (( ++fail )) + else + (( ++pass )) + fi +} + +# strictly invalid +do_test '' 'herp' +do_test '' 'derp' 100 + +# volume +do_test 50 'set-volume' 50 +do_test 10 'decrease' 40 +do_test 0 'decrease' 9001 +do_test 0 'get-volume' +do_test 50 'increase' 50 +do_test 0 'mute' +do_test 50 'unmute' +do_test 0 'toggle' +do_test 50 'toggle' +do_test '' 'set-volume' +do_test '' 'increase' foo +do_test '' 'decrease' bar + +# balance +do_test 30 'set-balance' 30 +do_test 30 'get-balance' +do_test 100 'set-balance' 9001 +do_test -5 'adj-balance' -105 +do_test 45 'adj-balance' 50 +do_test 100 'adj-balance' 9001 +do_test 0 'set-balance' 0 + +if (( ! fail )); then + printf '==> All %d tests successful\n' "$testno" +else + printf '==> %d/%d tests failed\n' "$fail" "$testno" + exit 1 +fi |