From 4382c2acf0a893eb4b96c3465af73523560a4815 Mon Sep 17 00:00:00 2001 From: Dave Reisner Date: Tue, 14 Aug 2012 08:58:50 -0400 Subject: This repo is now about ponies. --- .gitignore | 4 +- Makefile | 14 +- ponymix.1 | 77 ++++++ ponymix.c | 860 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ pulsemix.1 | 77 ------ pulsemix.c | 860 ------------------------------------------------------------- runtests | 12 +- 7 files changed, 952 insertions(+), 952 deletions(-) create mode 100644 ponymix.1 create mode 100644 ponymix.c delete mode 100644 pulsemix.1 delete mode 100644 pulsemix.c diff --git a/.gitignore b/.gitignore index 04e757e..af83c41 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -pulsemix -pulsemix.o +ponymix +ponymix.o diff --git a/Makefile b/Makefile index 1b7df79..973b8c0 100644 --- a/Makefile +++ b/Makefile @@ -2,14 +2,14 @@ CC = gcc -std=gnu99 CFLAGS := -Wall -Wextra -pedantic -O2 -g -D_REENTRANT $(CFLAGS) LDFLAGS := -lpulse -lm $(LDFLAGS) -pulsemix: pulsemix.o +ponymix: ponymix.o -install: pulsemix - install -Dm755 pulsemix $(DESTDIR)/usr/bin/pulsemix - install -Dm644 pulsemix.1 $(DESTDIR)/usr/share/man/man1/pulsemix.1 +install: ponymix + install -Dm755 ponymix $(DESTDIR)/usr/bin/ponymix + install -Dm644 ponymix.1 $(DESTDIR)/usr/share/man/man1/ponymix.1 -check: pulsemix - ./runtests ./pulsemix +check: ponymix + ./runtests ./ponymix clean: - $(RM) pulsemix pulsemix.o + $(RM) ponymix ponymix.o diff --git a/ponymix.1 b/ponymix.1 new file mode 100644 index 0000000..4c1326e --- /dev/null +++ b/ponymix.1 @@ -0,0 +1,77 @@ +.TH ponymix "1" "August 12" "ponymix" "User Commands" +.SH NAME +ponymix \- cli volume control for PulseAudio +.SH SYNOPSIS +\fBponymix\fP [options] \fIoperation\fP [args] +.SH DESCRIPTION +\fBponymix\fP is a command line volume control for PulseAudio, letting you +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\-\-sink\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\-\-source\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. +.SH OPERATIONS +.SS Common Operations +These options work on both devices and applications. +.PP +.IP "\fBlist\fR" +List all available sinks and sources. +.IP "\fBget-volume\fR" +Get the volume of the target. +.IP "\fBset-volume\fR \fIVALUE\fR" +Set the volume of the target. VALUE is an integer between 0 and 150. +.IP "\fBget-balance\fR" +Get the balance of the target. +.IP "\fBset-balance\fR \fIVALUE\fR" +Set the balance of the target. VALUE 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. +.IP "\fBincrease\fR \fIVALUE\fR" +Increase the volume percentage of target 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. +.IP "\fBmute\fR, \fBunmute\fR, \fBtoggle\fR" +Mute, unmute, or toggle the mute status on the target. +.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 +.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. +.SH AUTHORS +.nf +Dave Reisner +Simon Gomizelj +.fi +.SH SEE ALSO +.BR pulseaudio (1) + diff --git a/ponymix.c b/ponymix.c new file mode 100644 index 0000000..1a57be4 --- /dev/null +++ b/ponymix.c @@ -0,0 +1,860 @@ +/* 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 +#include +#include +#include +#include +#include +#include +#include + +#include + +#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)); \ + }) + +enum connectstate { + STATE_CONNECTING = 0, + STATE_CONNECTED, + STATE_ERROR +}; + +enum mode { + MODE_DEVICE = 0, + MODE_APP, + MODE_INVALID +}; + +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_INVALID +}; + +static const char *actions[ACTION_INVALID] = { + [ACTION_DEFAULTS] = "defaults", + [ACTION_LIST] = "list", + [ACTION_GETVOL] = "get-volume", + [ACTION_SETVOL] = "set-volume", + [ACTION_GETBAL] = "get-balance", + [ACTION_SETBAL] = "set-balance", + [ACTION_ADJBAL] = "adj-balance", + [ACTION_INCREASE] = "increase", + [ACTION_DECREASE] = "decrease", + [ACTION_MUTE] = "mute", + [ACTION_UNMUTE] = "unmute", + [ACTION_TOGGLE] = "toggle", + [ACTION_ISMUTED] = "is-muted", + [ACTION_SETDEFAULT] = "set-default", + [ACTION_MOVE] = "move", + [ACTION_KILL] = "kill" +}; + +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 pulseaudio_t { + pa_context *cxt; + pa_mainloop *mainloop; + enum connectstate state; + int success; + + struct io_t *head; +}; + +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 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); \ + 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; + + IO_NEW(sink, info, "sink"); + sink->desc = strdup( + pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_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; + + IO_NEW(source, info, "source"); + source->desc = strdup( + pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_NAME)); + 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 pulseaudio_t *pulse = raw; + struct io_t *sink; + + if (eol) + return; + + sink = sink_new(i); + sink->next = pulse->head; + pulse->head = sink; +} + +static void sink_input_add_cb(pa_context UNUSED *c, const pa_sink_input_info *i, int eol, + void *raw) +{ + struct pulseaudio_t *pulse = raw; + struct io_t *sink; + + if (eol) + return; + + sink = sink_input_new(i); + sink->next = pulse->head; + pulse->head = sink; +} + +static void source_add_cb(pa_context UNUSED *c, const pa_source_info *i, int eol, void *raw) +{ + struct pulseaudio_t *pulse = raw; + struct io_t *source; + + if (eol) + return; + + source = source_new(i); + source->next = pulse->head; + pulse->head = source; +} + +static void source_output_add_cb(pa_context UNUSED *c, const pa_source_output_info *i, int eol, + void *raw) +{ + struct pulseaudio_t *pulse = raw; + struct io_t *source; + + if (eol) + return; + + source = source_output_new(i); + source->next = pulse->head; + pulse->head = source; +} + +static void server_info_cb(pa_context UNUSED *c, const pa_server_info *i, + void *raw) +{ + const char **sink_name = raw; + + *sink_name = i->default_sink_name; +} + +static void source_info_cb(pa_context UNUSED *c, const pa_server_info *i, void *raw) +{ + const char **source_name = raw; + + *source_name = i->default_source_name; +} + +static void state_cb(pa_context UNUSED *c, void *raw) +{ + struct pulseaudio_t *pulse = raw; + + switch (pa_context_get_state(pulse->cxt)) { + case PA_CONTEXT_READY: + pulse->state = STATE_CONNECTED; + break; + case PA_CONTEXT_FAILED: + pulse->state = STATE_ERROR; + break; + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_TERMINATED: + break; + } +} + +static void success_cb(pa_context UNUSED *c, int success, void *raw) +{ + struct pulseaudio_t *pulse = raw; + pulse->success = 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) +{ + pa_cvolume *vol = pa_cvolume_set(&dev->volume, dev->volume.channels, + (int)fmax((double)(v + .5) * PA_VOLUME_NORM / 100, 0)); + pa_operation *op = pulse->head->op.setvol(pulse->cxt, dev->idx, vol, + success_cb, pulse); + pulse_async_wait(pulse, op); + + if (pulse->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 !pulse->success; +} + +static int set_balance(struct pulseaudio_t *pulse, struct io_t *dev, long v) +{ + 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, pulse); + pulse_async_wait(pulse, op); + + if (pulse->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 !pulse->success; +} + +static int set_mute(struct pulseaudio_t *pulse, struct io_t *dev, int mute) +{ + pa_operation* op; + + /* new effective volume */ + printf("%d\n", mute ? 0 : dev->volume_percent); + + op = pulse->head->op.mute(pulse->cxt, dev->idx, mute, + success_cb, pulse); + + pulse_async_wait(pulse, op); + + if (!pulse->success) { + int err = pa_context_errno(pulse->cxt); + fprintf(stderr, "failed to mute device: %s\n", pa_strerror(err)); + } + + pa_operation_unref(op); + + return !pulse->success; +} + +static int kill_client(struct pulseaudio_t *pulse, struct io_t *dev) +{ + 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, pulse); + pulse_async_wait(pulse, op); + + if (!pulse->success) { + int err = pa_context_errno(pulse->cxt); + fprintf(stderr, "failed to kill client: %s\n", pa_strerror(err)); + } + + pa_operation_unref(op); + + return !pulse->success; +} + +static int move_client(struct pulseaudio_t *pulse, struct io_t *dev) +{ + pa_operation* op; + + if (dev->next == NULL) { + warnx("no destination to move to"); + return 1; + } + if (dev->next->op.move == NULL) { + warnx("only clients can be moved"); + return 1; + } + + op = dev->next->op.move(pulse->cxt, dev->next->idx, dev->idx, success_cb, + pulse); + + pulse_async_wait(pulse, op); + + if (!pulse->success) { + int err = pa_context_errno(pulse->cxt); + fprintf(stderr, "failed to move client: %s\n", pa_strerror(err)); + } + + pa_operation_unref(op); + + return !pulse->success; +} + +static void print(struct io_t *dev) +{ + printf("%s %2d: %s\n %s\n Avg. Volume: %d%% %s\n", dev->pp_name, + dev->idx, dev->name, dev->desc, dev->volume_percent, + dev->mute ? "[Muted]" : ""); +} + +static void print_all(struct pulseaudio_t *pulse) +{ + struct io_t *dev = pulse->head; + + while (dev) { + print(dev); + dev = dev->next; + } +} + +static void populate_sinks(struct pulseaudio_t *pulse, enum mode mode) +{ + pa_operation *op; + + switch (mode) { + case MODE_APP: + op = pa_context_get_sink_input_info_list(pulse->cxt, sink_input_add_cb, pulse); + break; + case MODE_DEVICE: + default: + op = pa_context_get_sink_info_list(pulse->cxt, sink_add_cb, pulse); + } + + pulse_async_wait(pulse, op); + pa_operation_unref(op); +} + +static int get_sink_by_name(struct pulseaudio_t *pulse, const char *name, enum mode mode) +{ + pa_operation *op; + + switch (mode) { + case MODE_APP: + { + long id; + if (xstrtol(name, &id) < 0) { + warnx("application sink not valid id: %s", name); + return 1; + } + op = pa_context_get_sink_input_info(pulse->cxt, (uint32_t)id, sink_input_add_cb, pulse); + break; + } + case MODE_DEVICE: + default: + op = pa_context_get_sink_info_by_name(pulse->cxt, name, sink_add_cb, pulse); + } + + pulse_async_wait(pulse, op); + pa_operation_unref(op); + + return 0; +} + +static int get_default_sink(struct pulseaudio_t *pulse) +{ + const char *sink_name; + pa_operation *op = pa_context_get_server_info(pulse->cxt, server_info_cb, + &sink_name); + pulse_async_wait(pulse, op); + pa_operation_unref(op); + + return get_sink_by_name(pulse, sink_name, MODE_DEVICE); +} + +static void populate_sources(struct pulseaudio_t *pulse, enum mode mode) +{ + pa_operation *op; + + switch (mode) { + case MODE_APP: + op = pa_context_get_source_output_info_list(pulse->cxt, source_output_add_cb, pulse); + break; + case MODE_DEVICE: + default: + op = pa_context_get_source_info_list(pulse->cxt, source_add_cb, pulse); + } + + pulse_async_wait(pulse, op); + pa_operation_unref(op); +} + +static int get_source_by_name(struct pulseaudio_t *pulse, const char *name, enum mode mode) +{ + pa_operation *op; + + switch (mode) { + case MODE_APP: + { + long id; + if (xstrtol(name, &id) < 0) { + warnx("application source not valid id: %s", name); + return 1; + } + op = pa_context_get_source_output_info(pulse->cxt, (uint32_t)id, source_output_add_cb, pulse); + break; + } + case MODE_DEVICE: + default: + op = pa_context_get_source_info_by_name(pulse->cxt, name, source_add_cb, pulse); + } + + pulse_async_wait(pulse, op); + pa_operation_unref(op); + + return 0; +} + +static int get_default_source(struct pulseaudio_t *pulse) +{ + const char *source_name; + pa_operation *op = pa_context_get_server_info(pulse->cxt, source_info_cb, + &source_name); + pulse_async_wait(pulse, op); + pa_operation_unref(op); + + return get_source_by_name(pulse, source_name, MODE_DEVICE); +} + +static int set_default(struct pulseaudio_t *pulse, struct io_t *dev) +{ + 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, pulse); + pulse_async_wait(pulse, op); + + if (!pulse->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 !pulse->success; +} + +static int pulse_connect(struct pulseaudio_t *pulse) +{ + pa_context_connect(pulse->cxt, NULL, PA_CONTEXT_NOFLAGS, NULL); + while (pulse->state == STATE_CONNECTING) + pa_mainloop_iterate(pulse->mainloop, 1, NULL); + + if (pulse->state == STATE_ERROR) { + int r = pa_context_errno(pulse->cxt); + fprintf(stderr, "failed to connect to pulse daemon: %s\n", + pa_strerror(r)); + return 1; + } + + return 0; +} + +static int pulse_init(struct pulseaudio_t *pulse) +{ + pulse->mainloop = pa_mainloop_new(); + pulse->cxt = pa_context_new(pa_mainloop_get_api(pulse->mainloop), "lolpulse"); + pulse->state = STATE_CONNECTING; + pulse->head = NULL; + + pa_context_set_state_callback(pulse->cxt, state_cb, pulse); + return pulse_connect(pulse); +} + +static void pulse_deinit(struct pulseaudio_t *pulse) +{ + struct io_t *node = pulse->head; + + pa_context_disconnect(pulse->cxt); + pa_mainloop_free(pulse->mainloop); + + while (node) { + node = pulse->head->next; + free(pulse->head->name); + free(pulse->head->desc); + free(pulse->head); + pulse->head = node; + } +} + +static void __attribute__((__noreturn__)) usage(FILE *out) +{ + fprintf(out, "usage: %s [options] ...\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, --sink= control a sink other than the default\n" + " -i, --source= control a source\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\n" + " set-default DEVICE_ID set default device by ID\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], string) == 0) + break; + + return i; +} + +static int do_verb(struct pulseaudio_t *pulse, enum action verb, int value) +{ + switch (verb) { + case ACTION_GETVOL: + printf("%d\n", pulse->head->volume_percent); + return 0; + case ACTION_SETVOL: + return set_volume(pulse, pulse->head, CLAMP(value, 0, 150)); + case ACTION_GETBAL: + printf("%d\n", pulse->head->balance); + return 0; + case ACTION_SETBAL: + return set_balance(pulse, pulse->head, CLAMP(value, -100, 100)); + case ACTION_ADJBAL: + return set_balance(pulse, pulse->head, + CLAMP(pulse->head->balance + value, -100, 100)); + case ACTION_INCREASE: + return set_volume(pulse, pulse->head, + CLAMP(pulse->head->volume_percent + value, 0, 100)); + case ACTION_DECREASE: + return set_volume(pulse, pulse->head, + CLAMP(pulse->head->volume_percent - value, 0, 100)); + case ACTION_MUTE: + return set_mute(pulse, pulse->head, 1); + case ACTION_UNMUTE: + return set_mute(pulse, pulse->head, 0); + case ACTION_TOGGLE: + return set_mute(pulse, pulse->head, !pulse->head->mute); + case ACTION_ISMUTED: + return !pulse->head->mute; + case ACTION_MOVE: + return move_client(pulse, pulse->head); + case ACTION_KILL: + return kill_client(pulse, pulse->head); + case ACTION_SETDEFAULT: + return set_default(pulse, pulse->head); + default: + errx(EXIT_FAILURE, "internal error: unhandled verb id %d\n", verb); + } +} + +int main(int argc, char *argv[]) +{ + struct pulseaudio_t pulse; + enum action verb; + char *id = NULL, *arg = NULL; + long value = 0; + enum mode mode = MODE_DEVICE; + int rc = EXIT_SUCCESS; + + const char *pp_name = "sink"; + int (*fn_get_default)(struct pulseaudio_t *) = get_default_sink; + int (*fn_get_by_name)(struct pulseaudio_t *, const char *, enum mode) = get_sink_by_name; + + 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' }, + { "source", 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': + mode = MODE_DEVICE; + break; + case 'a': + mode = MODE_APP; + break; + case 'o': + id = optarg; + fn_get_default = get_default_sink; + fn_get_by_name = get_sink_by_name; + pp_name = "sink"; + break; + case 'i': + id = optarg; + fn_get_default = get_default_source; + fn_get_by_name = get_source_by_name; + pp_name = "source"; + break; + default: + return EXIT_FAILURE; + } + } + + /* string -> enum */ + verb = (optind == argc) ? (mode ? ACTION_LIST : ACTION_DEFAULTS) : string_to_verb(argv[optind]); + if (verb == ACTION_INVALID) + errx(EXIT_FAILURE, "unknown action: %s", argv[optind]); + + optind++; + switch (verb) { + case ACTION_SETVOL: + case ACTION_SETBAL: + case ACTION_ADJBAL: + case ACTION_INCREASE: + case ACTION_DECREASE: + if (optind == argc) + errx(EXIT_FAILURE, "missing value for action '%s'", argv[optind - 1]); + else { + /* validate to number */ + if (xstrtol(argv[optind], &value) < 0) + errx(EXIT_FAILURE, "invalid number: %s", argv[optind]); + } + break; + case ACTION_SETDEFAULT: + case ACTION_KILL: + if (optind == argc) + errx(EXIT_FAILURE, "missing arguments for action '%s'", argv[optind - 1]); + else + id = argv[optind]; + break; + case ACTION_MOVE: + if (optind > argc - 2) + errx(EXIT_FAILURE, "missing arguments for action '%s'", argv[optind - 1]); + else { + id = argv[optind++]; + arg = argv[optind]; + } + break; + default: + break; + } + + /* initialize connection */ + if (pulse_init(&pulse) != 0) + return EXIT_FAILURE; + + switch (verb) { + case ACTION_DEFAULTS: + get_default_sink(&pulse); + get_default_source(&pulse); + print_all(&pulse); + goto done; + case ACTION_LIST: + populate_sinks(&pulse, mode); + populate_sources(&pulse, mode); + print_all(&pulse); + goto done; + default: + break; + } + + if (id && fn_get_by_name) { + if (fn_get_by_name(&pulse, id, mode) != 0) + goto done; + } else if (!mode && verb != ACTION_SETDEFAULT && fn_get_default) { + if (fn_get_default(&pulse) != 0) + goto done; + } + + if (pulse.head == NULL) { + if (mode && !id) + warnx("%s id not set, no default operations", pp_name); + else + warnx("%s not found: %s", pp_name, id ? id : "default"); + rc = EXIT_FAILURE; + goto done; + } + + if (arg && fn_get_by_name) + fn_get_by_name(&pulse, arg, MODE_DEVICE); + + rc = do_verb(&pulse, verb, value); + +done: + /* shut down */ + pulse_deinit(&pulse); + + return rc; +} + +/* vim: set noet ts=2 sw=2: */ diff --git a/pulsemix.1 b/pulsemix.1 deleted file mode 100644 index 3665078..0000000 --- a/pulsemix.1 +++ /dev/null @@ -1,77 +0,0 @@ -.TH PULSEMIX "1" "August 12" "pulsemix" "User Commands" -.SH NAME -pulsemix \- cli volume control for PulseAudio -.SH SYNOPSIS -\fBpulsemix\fP [options] \fIoperation\fP [args] -.SH DESCRIPTION -\fBpulsemix\fP is a command line volume control for PulseAudio, letting you -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\-\-sink\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\-\-source\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. -.SH OPERATIONS -.SS Common Operations -These options work on both devices and applications. -.PP -.IP "\fBlist\fR" -List all available sinks and sources. -.IP "\fBget-volume\fR" -Get the volume of the target. -.IP "\fBset-volume\fR \fIVALUE\fR" -Set the volume of the target. VALUE is an integer between 0 and 150. -.IP "\fBget-balance\fR" -Get the balance of the target. -.IP "\fBset-balance\fR \fIVALUE\fR" -Set the balance of the target. VALUE 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. -.IP "\fBincrease\fR \fIVALUE\fR" -Increase the volume percentage of target 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. -.IP "\fBmute\fR, \fBunmute\fR, \fBtoggle\fR" -Mute, unmute, or toggle the mute status on the target. -.IP "\fBis-muted\fR" -Check if the target is muted. pulsemix will exit with the status 0 if muted, -and non-zero otherwise. -.SS Device Operations -.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. -.SH AUTHORS -.nf -Dave Reisner -Simon Gomizelj -.fi -.SH SEE ALSO -.BR pulseaudio (1) - diff --git a/pulsemix.c b/pulsemix.c deleted file mode 100644 index c053db7..0000000 --- a/pulsemix.c +++ /dev/null @@ -1,860 +0,0 @@ -/* Copyright (c) 2012 Dave Reisner - * - * pulsemix.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 -#include -#include -#include -#include -#include -#include -#include - -#include - -#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)); \ - }) - -enum connectstate { - STATE_CONNECTING = 0, - STATE_CONNECTED, - STATE_ERROR -}; - -enum mode { - MODE_DEVICE = 0, - MODE_APP, - MODE_INVALID -}; - -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_INVALID -}; - -static const char *actions[ACTION_INVALID] = { - [ACTION_DEFAULTS] = "defaults", - [ACTION_LIST] = "list", - [ACTION_GETVOL] = "get-volume", - [ACTION_SETVOL] = "set-volume", - [ACTION_GETBAL] = "get-balance", - [ACTION_SETBAL] = "set-balance", - [ACTION_ADJBAL] = "adj-balance", - [ACTION_INCREASE] = "increase", - [ACTION_DECREASE] = "decrease", - [ACTION_MUTE] = "mute", - [ACTION_UNMUTE] = "unmute", - [ACTION_TOGGLE] = "toggle", - [ACTION_ISMUTED] = "is-muted", - [ACTION_SETDEFAULT] = "set-default", - [ACTION_MOVE] = "move", - [ACTION_KILL] = "kill" -}; - -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 pulseaudio_t { - pa_context *cxt; - pa_mainloop *mainloop; - enum connectstate state; - int success; - - struct io_t *head; -}; - -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 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); \ - 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; - - IO_NEW(sink, info, "sink"); - sink->desc = strdup( - pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_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; - - IO_NEW(source, info, "source"); - source->desc = strdup( - pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_NAME)); - 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 pulseaudio_t *pulse = raw; - struct io_t *sink; - - if (eol) - return; - - sink = sink_new(i); - sink->next = pulse->head; - pulse->head = sink; -} - -static void sink_input_add_cb(pa_context UNUSED *c, const pa_sink_input_info *i, int eol, - void *raw) -{ - struct pulseaudio_t *pulse = raw; - struct io_t *sink; - - if (eol) - return; - - sink = sink_input_new(i); - sink->next = pulse->head; - pulse->head = sink; -} - -static void source_add_cb(pa_context UNUSED *c, const pa_source_info *i, int eol, void *raw) -{ - struct pulseaudio_t *pulse = raw; - struct io_t *source; - - if (eol) - return; - - source = source_new(i); - source->next = pulse->head; - pulse->head = source; -} - -static void source_output_add_cb(pa_context UNUSED *c, const pa_source_output_info *i, int eol, - void *raw) -{ - struct pulseaudio_t *pulse = raw; - struct io_t *source; - - if (eol) - return; - - source = source_output_new(i); - source->next = pulse->head; - pulse->head = source; -} - -static void server_info_cb(pa_context UNUSED *c, const pa_server_info *i, - void *raw) -{ - const char **sink_name = raw; - - *sink_name = i->default_sink_name; -} - -static void source_info_cb(pa_context UNUSED *c, const pa_server_info *i, void *raw) -{ - const char **source_name = raw; - - *source_name = i->default_source_name; -} - -static void state_cb(pa_context UNUSED *c, void *raw) -{ - struct pulseaudio_t *pulse = raw; - - switch (pa_context_get_state(pulse->cxt)) { - case PA_CONTEXT_READY: - pulse->state = STATE_CONNECTED; - break; - case PA_CONTEXT_FAILED: - pulse->state = STATE_ERROR; - break; - case PA_CONTEXT_UNCONNECTED: - case PA_CONTEXT_AUTHORIZING: - case PA_CONTEXT_SETTING_NAME: - case PA_CONTEXT_CONNECTING: - case PA_CONTEXT_TERMINATED: - break; - } -} - -static void success_cb(pa_context UNUSED *c, int success, void *raw) -{ - struct pulseaudio_t *pulse = raw; - pulse->success = 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) -{ - pa_cvolume *vol = pa_cvolume_set(&dev->volume, dev->volume.channels, - (int)fmax((double)(v + .5) * PA_VOLUME_NORM / 100, 0)); - pa_operation *op = pulse->head->op.setvol(pulse->cxt, dev->idx, vol, - success_cb, pulse); - pulse_async_wait(pulse, op); - - if (pulse->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 !pulse->success; -} - -static int set_balance(struct pulseaudio_t *pulse, struct io_t *dev, long v) -{ - 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, pulse); - pulse_async_wait(pulse, op); - - if (pulse->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 !pulse->success; -} - -static int set_mute(struct pulseaudio_t *pulse, struct io_t *dev, int mute) -{ - pa_operation* op; - - /* new effective volume */ - printf("%d\n", mute ? 0 : dev->volume_percent); - - op = pulse->head->op.mute(pulse->cxt, dev->idx, mute, - success_cb, pulse); - - pulse_async_wait(pulse, op); - - if (!pulse->success) { - int err = pa_context_errno(pulse->cxt); - fprintf(stderr, "failed to mute device: %s\n", pa_strerror(err)); - } - - pa_operation_unref(op); - - return !pulse->success; -} - -static int kill_client(struct pulseaudio_t *pulse, struct io_t *dev) -{ - 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, pulse); - pulse_async_wait(pulse, op); - - if (!pulse->success) { - int err = pa_context_errno(pulse->cxt); - fprintf(stderr, "failed to kill client: %s\n", pa_strerror(err)); - } - - pa_operation_unref(op); - - return !pulse->success; -} - -static int move_client(struct pulseaudio_t *pulse, struct io_t *dev) -{ - pa_operation* op; - - if (dev->next == NULL) { - warnx("no destination to move to"); - return 1; - } - if (dev->next->op.move == NULL) { - warnx("only clients can be moved"); - return 1; - } - - op = dev->next->op.move(pulse->cxt, dev->next->idx, dev->idx, success_cb, - pulse); - - pulse_async_wait(pulse, op); - - if (!pulse->success) { - int err = pa_context_errno(pulse->cxt); - fprintf(stderr, "failed to move client: %s\n", pa_strerror(err)); - } - - pa_operation_unref(op); - - return !pulse->success; -} - -static void print(struct io_t *dev) -{ - printf("%s %2d: %s\n %s\n Avg. Volume: %d%% %s\n", dev->pp_name, - dev->idx, dev->name, dev->desc, dev->volume_percent, - dev->mute ? "[Muted]" : ""); -} - -static void print_all(struct pulseaudio_t *pulse) -{ - struct io_t *dev = pulse->head; - - while (dev) { - print(dev); - dev = dev->next; - } -} - -static void populate_sinks(struct pulseaudio_t *pulse, enum mode mode) -{ - pa_operation *op; - - switch (mode) { - case MODE_APP: - op = pa_context_get_sink_input_info_list(pulse->cxt, sink_input_add_cb, pulse); - break; - case MODE_DEVICE: - default: - op = pa_context_get_sink_info_list(pulse->cxt, sink_add_cb, pulse); - } - - pulse_async_wait(pulse, op); - pa_operation_unref(op); -} - -static int get_sink_by_name(struct pulseaudio_t *pulse, const char *name, enum mode mode) -{ - pa_operation *op; - - switch (mode) { - case MODE_APP: - { - long id; - if (xstrtol(name, &id) < 0) { - warnx("application sink not valid id: %s", name); - return 1; - } - op = pa_context_get_sink_input_info(pulse->cxt, (uint32_t)id, sink_input_add_cb, pulse); - break; - } - case MODE_DEVICE: - default: - op = pa_context_get_sink_info_by_name(pulse->cxt, name, sink_add_cb, pulse); - } - - pulse_async_wait(pulse, op); - pa_operation_unref(op); - - return 0; -} - -static int get_default_sink(struct pulseaudio_t *pulse) -{ - const char *sink_name; - pa_operation *op = pa_context_get_server_info(pulse->cxt, server_info_cb, - &sink_name); - pulse_async_wait(pulse, op); - pa_operation_unref(op); - - return get_sink_by_name(pulse, sink_name, MODE_DEVICE); -} - -static void populate_sources(struct pulseaudio_t *pulse, enum mode mode) -{ - pa_operation *op; - - switch (mode) { - case MODE_APP: - op = pa_context_get_source_output_info_list(pulse->cxt, source_output_add_cb, pulse); - break; - case MODE_DEVICE: - default: - op = pa_context_get_source_info_list(pulse->cxt, source_add_cb, pulse); - } - - pulse_async_wait(pulse, op); - pa_operation_unref(op); -} - -static int get_source_by_name(struct pulseaudio_t *pulse, const char *name, enum mode mode) -{ - pa_operation *op; - - switch (mode) { - case MODE_APP: - { - long id; - if (xstrtol(name, &id) < 0) { - warnx("application source not valid id: %s", name); - return 1; - } - op = pa_context_get_source_output_info(pulse->cxt, (uint32_t)id, source_output_add_cb, pulse); - break; - } - case MODE_DEVICE: - default: - op = pa_context_get_source_info_by_name(pulse->cxt, name, source_add_cb, pulse); - } - - pulse_async_wait(pulse, op); - pa_operation_unref(op); - - return 0; -} - -static int get_default_source(struct pulseaudio_t *pulse) -{ - const char *source_name; - pa_operation *op = pa_context_get_server_info(pulse->cxt, source_info_cb, - &source_name); - pulse_async_wait(pulse, op); - pa_operation_unref(op); - - return get_source_by_name(pulse, source_name, MODE_DEVICE); -} - -static int set_default(struct pulseaudio_t *pulse, struct io_t *dev) -{ - 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, pulse); - pulse_async_wait(pulse, op); - - if (!pulse->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 !pulse->success; -} - -static int pulse_connect(struct pulseaudio_t *pulse) -{ - pa_context_connect(pulse->cxt, NULL, PA_CONTEXT_NOFLAGS, NULL); - while (pulse->state == STATE_CONNECTING) - pa_mainloop_iterate(pulse->mainloop, 1, NULL); - - if (pulse->state == STATE_ERROR) { - int r = pa_context_errno(pulse->cxt); - fprintf(stderr, "failed to connect to pulse daemon: %s\n", - pa_strerror(r)); - return 1; - } - - return 0; -} - -static int pulse_init(struct pulseaudio_t *pulse) -{ - pulse->mainloop = pa_mainloop_new(); - pulse->cxt = pa_context_new(pa_mainloop_get_api(pulse->mainloop), "lolpulse"); - pulse->state = STATE_CONNECTING; - pulse->head = NULL; - - pa_context_set_state_callback(pulse->cxt, state_cb, pulse); - return pulse_connect(pulse); -} - -static void pulse_deinit(struct pulseaudio_t *pulse) -{ - struct io_t *node = pulse->head; - - pa_context_disconnect(pulse->cxt); - pa_mainloop_free(pulse->mainloop); - - while (node) { - node = pulse->head->next; - free(pulse->head->name); - free(pulse->head->desc); - free(pulse->head); - pulse->head = node; - } -} - -static void __attribute__((__noreturn__)) usage(FILE *out) -{ - fprintf(out, "usage: %s [options] ...\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, --sink= control a sink other than the default\n" - " -i, --source= control a source\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\n" - " set-default DEVICE_ID set default device by ID\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], string) == 0) - break; - - return i; -} - -static int do_verb(struct pulseaudio_t *pulse, enum action verb, int value) -{ - switch (verb) { - case ACTION_GETVOL: - printf("%d\n", pulse->head->volume_percent); - return 0; - case ACTION_SETVOL: - return set_volume(pulse, pulse->head, CLAMP(value, 0, 150)); - case ACTION_GETBAL: - printf("%d\n", pulse->head->balance); - return 0; - case ACTION_SETBAL: - return set_balance(pulse, pulse->head, CLAMP(value, -100, 100)); - case ACTION_ADJBAL: - return set_balance(pulse, pulse->head, - CLAMP(pulse->head->balance + value, -100, 100)); - case ACTION_INCREASE: - return set_volume(pulse, pulse->head, - CLAMP(pulse->head->volume_percent + value, 0, 100)); - case ACTION_DECREASE: - return set_volume(pulse, pulse->head, - CLAMP(pulse->head->volume_percent - value, 0, 100)); - case ACTION_MUTE: - return set_mute(pulse, pulse->head, 1); - case ACTION_UNMUTE: - return set_mute(pulse, pulse->head, 0); - case ACTION_TOGGLE: - return set_mute(pulse, pulse->head, !pulse->head->mute); - case ACTION_ISMUTED: - return !pulse->head->mute; - case ACTION_MOVE: - return move_client(pulse, pulse->head); - case ACTION_KILL: - return kill_client(pulse, pulse->head); - case ACTION_SETDEFAULT: - return set_default(pulse, pulse->head); - default: - errx(EXIT_FAILURE, "internal error: unhandled verb id %d\n", verb); - } -} - -int main(int argc, char *argv[]) -{ - struct pulseaudio_t pulse; - enum action verb; - char *id = NULL, *arg = NULL; - long value = 0; - enum mode mode = MODE_DEVICE; - int rc = EXIT_SUCCESS; - - const char *pp_name = "sink"; - int (*fn_get_default)(struct pulseaudio_t *) = get_default_sink; - int (*fn_get_by_name)(struct pulseaudio_t *, const char *, enum mode) = get_sink_by_name; - - 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' }, - { "source", 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': - mode = MODE_DEVICE; - break; - case 'a': - mode = MODE_APP; - break; - case 'o': - id = optarg; - fn_get_default = get_default_sink; - fn_get_by_name = get_sink_by_name; - pp_name = "sink"; - break; - case 'i': - id = optarg; - fn_get_default = get_default_source; - fn_get_by_name = get_source_by_name; - pp_name = "source"; - break; - default: - return EXIT_FAILURE; - } - } - - /* string -> enum */ - verb = (optind == argc) ? (mode ? ACTION_LIST : ACTION_DEFAULTS) : string_to_verb(argv[optind]); - if (verb == ACTION_INVALID) - errx(EXIT_FAILURE, "unknown action: %s", argv[optind]); - - optind++; - switch (verb) { - case ACTION_SETVOL: - case ACTION_SETBAL: - case ACTION_ADJBAL: - case ACTION_INCREASE: - case ACTION_DECREASE: - if (optind == argc) - errx(EXIT_FAILURE, "missing value for action '%s'", argv[optind - 1]); - else { - /* validate to number */ - if (xstrtol(argv[optind], &value) < 0) - errx(EXIT_FAILURE, "invalid number: %s", argv[optind]); - } - break; - case ACTION_SETDEFAULT: - case ACTION_KILL: - if (optind == argc) - errx(EXIT_FAILURE, "missing arguments for action '%s'", argv[optind - 1]); - else - id = argv[optind]; - break; - case ACTION_MOVE: - if (optind > argc - 2) - errx(EXIT_FAILURE, "missing arguments for action '%s'", argv[optind - 1]); - else { - id = argv[optind++]; - arg = argv[optind]; - } - break; - default: - break; - } - - /* initialize connection */ - if (pulse_init(&pulse) != 0) - return EXIT_FAILURE; - - switch (verb) { - case ACTION_DEFAULTS: - get_default_sink(&pulse); - get_default_source(&pulse); - print_all(&pulse); - goto done; - case ACTION_LIST: - populate_sinks(&pulse, mode); - populate_sources(&pulse, mode); - print_all(&pulse); - goto done; - default: - break; - } - - if (id && fn_get_by_name) { - if (fn_get_by_name(&pulse, id, mode) != 0) - goto done; - } else if (!mode && verb != ACTION_SETDEFAULT && fn_get_default) { - if (fn_get_default(&pulse) != 0) - goto done; - } - - if (pulse.head == NULL) { - if (mode && !id) - warnx("%s id not set, no default operations", pp_name); - else - warnx("%s not found: %s", pp_name, id ? id : "default"); - rc = EXIT_FAILURE; - goto done; - } - - if (arg && fn_get_by_name) - fn_get_by_name(&pulse, arg, MODE_DEVICE); - - rc = do_verb(&pulse, verb, value); - -done: - /* shut down */ - pulse_deinit(&pulse); - - return rc; -} - -/* vim: set noet ts=2 sw=2: */ diff --git a/runtests b/runtests index 747c5bf..a0762cb 100755 --- a/runtests +++ b/runtests @@ -1,14 +1,14 @@ #!/bin/bash -pulsemix=$1 +ponymix=$1 -if [[ -z $pulsemix ]]; then - printf 'usage: %s path-to-pulsemix\n' "${0##*/}" +if [[ -z $ponymix ]]; then + printf 'usage: %s path-to-ponymix\n' "${0##*/}" exit 1 fi -if [[ ! -x $pulsemix ]]; then - printf '==> ERROR: pulsemix binary not found at %s\n' "$pulsemix" +if [[ ! -x $ponymix ]]; then + printf '==> ERROR: ponymix binary not found at %s\n' "$ponymix" exit 1 fi @@ -19,7 +19,7 @@ do_test() { (( ++testno )) - result=$("$pulsemix" "$verb" -- ${arg+"$arg"} 2>/dev/null) + result=$("$ponymix" "$verb" -- ${arg+"$arg"} 2>/dev/null) if [[ $result != $expected ]]; then printf '==> test %d FAIL: expected %s, got %s\n' "$testno" "$expected" "$result" (( ++fail )) -- cgit v1.2.3