aboutsummaryrefslogtreecommitdiffstats
Commit message (Expand)AuthorAgeFilesLines
...
* Before the next fix round.Eric S. Raymond2002-02-014-6/+33
* Two new FAQ entries.Eric S. Raymond2001-12-251-31/+90
* Make silent mode silent.Eric S. Raymond2001-12-251-2/+5
* Minor fixes from Henrique.Eric S. Raymond2001-12-194-5/+9
* Ready to ship.Eric S. Raymond2001-12-142-3/+5
* Version bump.Eric S. Raymond2001-12-141-1/+1
* Slight change in bounce logic.Eric S. Raymond2001-12-141-3/+7
* Revised dup-killer code.Eric S. Raymond2001-12-143-38/+56
* LMTP fix.Eric S. Raymond2001-12-142-7/+12
* Documentation fix.Eric S. Raymond2001-12-141-1/+4
* Note some recent problems.Eric S. Raymond2001-12-141-5/+9
* Sunil Shetye's disconect-reconnect fix.Eric S. Raymond2001-12-141-3/+7
* Note a new bug.Eric S. Raymond2001-12-141-2/+8
* Fix for OPIE operation.Eric S. Raymond2001-12-141-9/+18
* Better handling of malformed messages.Eric S. Raymond2001-12-041-1/+1
* Initial revisionEric S. Raymond2001-12-041-0/+74
* LSM generator is now Python.Eric S. Raymond2001-11-091-2/+2
* Ready to ship.Eric S. Raymond2001-11-081-0/+4
* Avoid UIDL core dump.Eric S. Raymond2001-11-082-19/+25
* Version bump.Eric S. Raymond2001-11-081-1/+1
* Sicket leak avoidance.Eric S. Raymond2001-11-082-2/+37
* Cosmetic fix.Eric S. Raymond2001-11-081-3/+3
* Easy bug fixes for this round.Eric S. Raymond2001-11-0814-29/+62
* Avoid troff glitches.Eric S. Raymond2001-10-161-10/+10
* Markup fix.Eric S. Raymond2001-10-131-3/+3
* Logging logic changed. Verbosity lowered.Eric S. Raymond2001-10-042-7/+11
* Don't call with a potentially bad value.Eric S. Raymond2001-10-041-0/+4
* Don't pass through characters with codes < 16.Eric S. Raymond2001-10-041-1/+1
* We are ready to rock.Eric S. Raymond2001-10-041-3/+1
* Back out the attempt to build fetchmail.pot.Eric S. Raymond2001-10-031-2/+2
* Security audit fix.Eric S. Raymond2001-10-036-10/+12
* Ready to go.Eric S. Raymond2001-10-031-0/+9
* Note copyright assignment.Eric S. Raymond2001-10-031-0/+1
* HMH's changes.Eric S. Raymond2001-10-023-3/+6
* Recommend new gettext.Eric S. Raymond2001-10-021-0/+7
* HMH's latest round of fixes.Eric S. Raymond2001-10-012-4/+6
* HMH fixes.Eric S. Raymond2001-10-011-8/+6
* error.c -> report.cEric S. Raymond2001-09-304-6/+11
* More refactoring.Eric S. Raymond2001-09-301-15/+7
* More license cleanup.Eric S. Raymond2001-09-301-2/+4
* More refactoring.Eric S. Raymond2001-09-301-174/+173
* De-GPL-lock this.Eric S. Raymond2001-09-301-42/+30
* Refactor sink.c preparatory to changing defaults.Eric S. Raymond2001-09-301-369/+387
* Version bump.Eric S. Raymond2001-09-301-1/+1
* Have to ship getopt.h, tooEric S. Raymond2001-09-301-1/+1
* Start refactoriing.Eric S. Raymond2001-09-301-81/+81
* Ready to try changing default.Eric S. Raymond2001-09-301-1/+2
* We're now GPL-clean.Eric S. Raymond2001-09-301-4/+26
* License cleanup.Eric S. Raymond2001-09-301-10/+2
* Ready to ship.Eric S. Raymond2001-09-303-2/+13
-weight: bold } /* Name.Class */ .highlight .no { color: #003366; font-weight: bold } /* Name.Constant */ .highlight .nd { color: #555555 } /* Name.Decorator */ .highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */ .highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */ .highlight .nl { color: #336699; font-style: italic } /* Name.Label */ .highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */ .highlight .py { color: #336699; font-weight: bold } /* Name.Property */ .highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #336699 } /* Name.Variable */ .highlight .ow { color: #008800 } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */ .highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ .highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ .highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
/* 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 <errno.h>
#include <err.h>
#include <getopt.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.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)); \
	})

union arg_t {
	long l;
	char *c;
};

enum connectstate {
	STATE_CONNECTING = 0,
	STATE_CONNECTED,
	STATE_ERROR
};

enum action {
	ACTION_LIST = 0,
	ACTION_GETVOL,
	ACTION_SETVOL,
	ACTION_INCREASE,
	ACTION_DECREASE,
	ACTION_MUTE,
	ACTION_UNMUTE,
	ACTION_TOGGLE,
	ACTION_SETSINK,
	ACTION_INVALID
};

struct sink_t {
	uint32_t idx;
	const char *name;
	const char *desc;
	pa_cvolume volume;
	int volume_percent;
	int mute;

	struct sink_t *next_sink;
};

struct pulseaudio_t {
	pa_context *cxt;
	pa_mainloop *mainloop;
	pa_mainloop_api *mainloop_api;
	enum connectstate state;

	struct sink_t *sink;
};

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 int pulse_async_wait(struct pulseaudio_t *pulse, pa_operation *op)
{
	int r = 0;

	while (pa_operation_get_state(op) == PA_OPERATION_RUNNING)
		pa_mainloop_iterate(pulse->mainloop, 1, &r);
	
	return r;
}

static void sink_get_volume(struct pulseaudio_t *pulse)
{
	printf("%d\n", pulse->sink->volume_percent);
}

static void sink_set_volume(struct pulseaudio_t *pulse, struct sink_t *sink, long v)
{
	int r;
	pa_cvolume *vol = pa_cvolume_set(&sink->volume, sink->volume.channels,
			(int)fmax((double)v * PA_VOLUME_NORM / 100, 0));
	pa_operation *op = pa_context_set_sink_volume_by_index(pulse->cxt,
			sink->idx, vol, NULL, NULL);
	r = pulse_async_wait(pulse, op);

	if (r == 0)
		printf("%ld\n", v);

	pa_operation_unref(op);
}

static void sink_set_mute(struct pulseaudio_t *pulse, struct sink_t *sink, int mute)
{
	pa_operation* op = pa_context_set_sink_mute_by_index(pulse->cxt, sink->idx,
			mute, NULL, NULL);
	pulse_async_wait(pulse, op);

	pa_operation_unref(op);
}

static void sink_unmute(struct pulseaudio_t *pulse, struct sink_t *sink)
{
	sink_set_mute(pulse, sink, 0);
}

static void sink_mute(struct pulseaudio_t *pulse, struct sink_t *sink)
{
	sink_set_mute(pulse, sink, 1);
}

static struct sink_t *sink_new(const pa_sink_info *info)
{
	struct sink_t *sink = calloc(1, sizeof(struct sink_t));

	sink->idx = info->index;
	sink->name = info->name;
	sink->desc = info->description;
	memcpy(&sink->volume, &info->volume, sizeof(pa_cvolume));
	sink->volume_percent = (int)(((double)pa_cvolume_avg(&sink->volume) * 100)
			/ PA_VOLUME_NORM);
	sink->mute = info->mute;

	return sink;
}

static void print_sink(struct sink_t *sink)
{
	printf("sink %2d: %s\n  %s\n  Avg. Volume: %d%%\n",
			sink->idx, sink->name, sink->desc, sink->volume_percent);
}

static void print_sinks(struct pulseaudio_t *pulse)
{
	struct sink_t *sink = pulse->sink;

	while (sink) {
		print_sink(sink);
		sink = sink->next_sink;
	}
}

static void server_info_cb(pa_context UNUSED *c, const pa_server_info *i,
		void *raw)
{
	const char **sink_name = (const char **)raw;

	*sink_name = i->default_sink_name;
}

static void sink_add_cb(pa_context UNUSED *c, const pa_sink_info *i, int eol,
		void *raw)
{
	struct pulseaudio_t *pulse = raw;
	struct sink_t *s, *sink;

	if (eol)
		return;

	sink = sink_new(i);

	if (pulse->sink == NULL)
		pulse->sink = sink;
	else {
		s = pulse->sink;
		sink->next_sink = s;
		pulse->sink = sink;
	}
}

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 get_sinks(struct pulseaudio_t *pulse)
{
	pa_operation *op = pa_context_get_sink_info_list(pulse->cxt,
			sink_add_cb, pulse);
	pulse_async_wait(pulse, op);
	pa_operation_unref(op);
}

static void get_sink_by_name(struct pulseaudio_t *pulse, const char *name)
{
	pa_operation *op = pa_context_get_sink_info_by_name(pulse->cxt, name,
			sink_add_cb, pulse);
	pulse_async_wait(pulse, op);
	pa_operation_unref(op);
}

static void 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);

	get_sink_by_name(pulse, sink_name);
}

static void set_default_sink(struct pulseaudio_t *pulse, const char *sinkname)
{
	pa_operation *op;

	get_sink_by_name(pulse, sinkname);
	if (pulse->sink == NULL) {
		warnx("failed to get sink by name\n");
		return;
	}
 
	op = pa_context_set_default_sink(pulse->cxt, sinkname, NULL, pulse);
	pulse_async_wait(pulse, op);
	pa_operation_unref(op);
}

static void pulse_deinit(struct pulseaudio_t *pulse)
{
	struct sink_t *sink = pulse->sink;

	pa_context_disconnect(pulse->cxt);
	pa_mainloop_free(pulse->mainloop);

	while (sink) {
		sink = pulse->sink->next_sink;
		free(pulse->sink);
		pulse->sink = sink;
	}
}

static void pulse_init(struct pulseaudio_t *pulse, const char *clientname)
{
	/* allocate */
	pulse->mainloop = pa_mainloop_new();
	pulse->mainloop_api = pa_mainloop_get_api(pulse->mainloop);
	pulse->cxt = pa_context_new(pulse->mainloop_api, clientname);
	pulse->state = STATE_CONNECTING;
	pulse->sink = NULL;

	/* set state callback for connection */
	pa_context_set_state_callback(pulse->cxt, state_cb, pulse);
}

static int pulse_connect(struct pulseaudio_t *pulse)
{
	int r;

	pa_context_connect(pulse->cxt, NULL, PA_CONTEXT_NOFLAGS, NULL);
	while (pulse->state == STATE_CONNECTING)
		pa_mainloop_iterate(pulse->mainloop, 1, &r);

	if (pulse->state == STATE_ERROR) {
		r = pa_context_errno(pulse->cxt);
		fprintf(stderr, "failed to connect to pulse daemon: %s\n",
				pa_strerror(r));
		return 1;
	}

	return 0;
}

void usage(FILE *out)
{
	fprintf(out, "usage: %s [options] <command>...\n", program_invocation_short_name);
	fputs("\nOptions:\n", out);
	fputs(" -h, --help,        display this help and exit\n", out);
	fputs(" -s, --sink <name>  control a sink other than the default\n", out);

	fputs("\nCommands:\n", out);
	fputs("  list               list available sinks\n", out);
	fputs("  get-volume         get volume for sink\n", out);
	fputs("  set-volume VALUE   set volume for sink\n", out);
	fputs("  increase VALUE     increase volume\n", out);
	fputs("  decrease VALUE     decrease volume\n", out);
	fputs("  mute               mute active sink\n", out);
	fputs("  unmute             unmute active sink\n", out);
	fputs("  toggle             toggle mute\n", out);
	fputs("  set-sink SINKNAME  set default sink\n", out);

	exit(out == stderr ? EXIT_FAILURE : EXIT_SUCCESS);
}

enum action string_to_verb(const char *string)
{
	if (strcmp(string, "list") == 0)
		return ACTION_LIST;
	else if (strcmp(string, "get-volume") == 0)
		return ACTION_GETVOL;
	else if (strcmp(string, "set-volume") == 0)
		return ACTION_SETVOL;
	else if (strcmp(string, "increase") == 0)
		return ACTION_INCREASE;
	else if (strcmp(string, "decrease") == 0)
		return ACTION_DECREASE;
	else if (strcmp(string, "mute") == 0)
		return ACTION_MUTE;
	else if (strcmp(string, "unmute") == 0)
		return ACTION_UNMUTE;
	else if (strcmp(string, "toggle") == 0)
		return ACTION_TOGGLE;
	else if (strcmp(string, "set-sink") == 0)
		return ACTION_SETSINK;

	return ACTION_INVALID;
}

int main(int argc, char *argv[])
{
	struct pulseaudio_t pulse;
	enum action verb;
	char *sink = NULL;
	union arg_t value;

	static const struct option opts[] = {
		{ "help", no_argument, 0, 'h' },
		{ "sink", required_argument, 0, 's' },
		{ 0, 0, 0, 0 },
	};

	for (;;) {
		int opt = getopt_long(argc, argv, "hs:", opts, NULL);
		if (opt == -1)
			break;

		switch (opt) {
		case 'h':
			usage(stdout);
		case 's':
			sink = optarg;
			break;
		default:
			exit(1);
		}
	}

	/* string -> enum */
	verb = (optind == argc) ? ACTION_LIST : string_to_verb(argv[optind]);
	if (verb == ACTION_INVALID)
		errx(EXIT_FAILURE, "unknown action: %s", argv[optind]);

	optind++;
	if (verb == ACTION_SETVOL ||
			verb == ACTION_INCREASE ||
			verb == ACTION_DECREASE)
		if (optind == argc)
			errx(EXIT_FAILURE, "missing value for action '%s'", argv[optind - 1]);
		else {
			/* validate to number */
			int r = xstrtol(argv[optind], &value.l);
			if (r < 0)
				errx(EXIT_FAILURE, "invalid number: %s", argv[optind]);
		}
	else if (verb == ACTION_SETSINK) {
		if (optind == argc)
			errx(EXIT_FAILURE, "missing value for action '%s'", argv[optind - 1]);
		else
			value.c = argv[optind];
	}

	/* initialize connection */
	pulse_init(&pulse, "lolpulse");
	if (pulse_connect(&pulse) != 0)
		return 1;

	if (verb == ACTION_LIST) {
		get_sinks(&pulse);
		print_sinks(&pulse);
	} else {
		/* determine sink */
		if (sink) {
			get_sink_by_name(&pulse, sink);
		} else
			get_default_sink(&pulse);

		if(pulse.sink == NULL)
			errx(EXIT_FAILURE, "sink not found: %s", sink ? sink : "default");

		switch (verb) {
		case ACTION_GETVOL:
			sink_get_volume(&pulse);
			break;
		case ACTION_SETVOL:
			sink_set_volume(&pulse, pulse.sink, value.l);
			break;
		case ACTION_INCREASE:
			sink_set_volume(&pulse, pulse.sink,
					CLAMP(pulse.sink->volume_percent + value.l, 0, 150));
			break;
		case ACTION_DECREASE:
			sink_set_volume(&pulse, pulse.sink,
					CLAMP(pulse.sink->volume_percent - value.l, 0, 150));
			break;
		case ACTION_MUTE:
			sink_mute(&pulse, pulse.sink);
			break;
		case ACTION_UNMUTE:
			sink_unmute(&pulse, pulse.sink);
			break;
		case ACTION_TOGGLE:
			sink_set_mute(&pulse, pulse.sink, !pulse.sink->mute);
			break;
		case ACTION_SETSINK:
			set_default_sink(&pulse, value.c);
		default:
			break;
		}
	}

	/* shut down */
	pulse_deinit(&pulse);

	return 0;
}

/* vim: set noet ts=2 sw=2: */