git.strcat.st

/strcat/mixer.git/ - summarytreelogarchive

subject
we on git now nurga
commit
2b142697a8e93fb740fe96bde00e5d7949127075
date
2026-04-17T20:27:48Z
message
diff
 Makefile  |  12 +
 README.md |  39 +++
 mixer.c   | 794 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 845 insertions(+)

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..21e95c9
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,12 @@
+CC		?= cc
+CFLAGS		?= -O2 -Wall -Wextra -Werror -std=c89
+ALSA_CFLAGS	= $(shell pkg-config --cflags alsa)
+ALSA_LIBS	= $(shell pkg-config --libs alsa)
+
+all: mixer
+
+mixer: mixer.c
+	$(CC) $(CFLAGS) $(ALSA_CFLAGS) -o $@ mixer.c $(ALSA_LIBS)
+
+clean:
+	rm -f mixer
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..76d8353
--- /dev/null
+++ b/README.md
@@ -0,0 +1,39 @@
+mixer
+
+Jank (ASS) Alsa Mixer.
+
+dependencies:
+        alsa
+
+build:
+        make
+
+make (un)install:
+        make install
+        make uninstall
+
+usage:
+        mixer -t
+        mixer -l
+        mixer -c Master -g
+        mixer -c Master -s 80
+        mixer -c Master -m 1
+
+arguments:
+        -t       TUI                    [default if no arguments passed.]
+        -l       list playback controls
+        -d card  sound card             [default: "default"             ]
+        -c name  control name           [required for -g/-s/-m          ]
+        -g       get volume             [percent                        ]
+        -s pct   set volume             [0-100                          ]
+        -m 0|1   mute                   [off/on                         ]
+
+keybinds:
+        j/k or arrows   move
+        +/-             volume
+        [/]             switch ALSA card
+        m               mute
+        M               mute all
+        U               unmute all
+        ?               help
+        q               quit
diff --git a/mixer.c b/mixer.c
new file mode 100644
index 0000000..cdff6bd
--- /dev/null
+++ b/mixer.c
@@ -0,0 +1,794 @@
+#include <sys/types.h>
+
+#include <alsa/asoundlib.h>
+#include <err.h>
+#include <errno.h>
+#include <getopt.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <termios.h>
+#include <unistd.h>
+
+#define	MAX_ELEMS	1024
+#define	MAX_CARDNAME	64
+#define	NAME_W		24
+
+struct elem_list {
+	snd_mixer_elem_t **elems;
+	int n;
+};
+
+struct card_list {
+	int *ids;
+	char **names;
+	int n;
+	int cur;
+};
+
+static const char	*control;
+static const char	*progname;
+static int		do_get;
+static int		do_list;
+static int		do_mute = -1;
+static int		do_set = -1;
+static int		do_tui;
+
+static struct termios	saved_termios;
+static volatile sig_atomic_t	sig_exit;
+
+static void		die(const char *);
+static int		elem_can_playback(snd_mixer_elem_t *);
+static int		get_elem_name(snd_mixer_elem_t *, char *, size_t);
+static int		get_elem_mute(snd_mixer_elem_t *, int *);
+static int		get_elem_volume(snd_mixer_elem_t *, long *);
+static const char	*getprogname(void);
+static int		list_elems(snd_mixer_t *);
+static int		load_cards(struct card_list *);
+static int		load_elems(snd_mixer_t *, struct elem_list *);
+static int		open_mixer(snd_mixer_t **, const char *);
+static int		set_elem_mute(snd_mixer_elem_t *, int);
+static int		set_elem_volume(snd_mixer_elem_t *, long);
+static long		volume_step(snd_mixer_elem_t *);
+static int		set_all_mute(snd_mixer_t *, int);
+static void		show_help(void);
+static int		update_card(struct card_list *, char *, size_t);
+static void		usage(void);
+static size_t		xstrlcpy(char *, const char *, size_t);
+
+static const char *
+getprogname(void)
+{
+	if (progname == NULL || *progname == '\0')
+		return "mixer";
+	return progname;
+}
+
+static size_t
+xstrlcpy(char *dst, const char *src, size_t size)
+{
+	size_t i;
+
+	if (size == 0)
+		return strlen(src);
+	for (i = 0; i + 1 < size && src[i] != '\0'; i++)
+		dst[i] = src[i];
+	dst[i] = '\0';
+	while (src[i] != '\0')
+		i++;
+	return i;
+}
+
+static void
+usage(void)
+{
+	fprintf(stderr, "usage: %s [-t] [-l] [-d card] [-c control] "
+	    "[-g] [-m 0|1] [-s percent]\n", getprogname());
+	exit(1);
+}
+
+static void
+signal_handler(int sig)
+{
+	(void)sig;
+	sig_exit = 1;
+}
+
+static void
+set_signal_handlers(void)
+{
+	struct sigaction sa;
+
+	memset(&sa, 0, sizeof(sa));
+	sa.sa_handler = signal_handler;
+	sigemptyset(&sa.sa_mask);
+	sa.sa_flags = 0;
+
+	if (sigaction(SIGINT, &sa, NULL) == -1)
+		warn("sigaction SIGINT");
+	if (sigaction(SIGTERM, &sa, NULL) == -1)
+		warn("sigaction SIGTERM");
+}
+
+static void
+check_termios(void)
+{
+	if (tcgetattr(STDIN_FILENO, &saved_termios) == -1)
+		die("tcgetattr");
+}
+
+static void
+restore_termios(void)
+{
+	if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &saved_termios) == -1)
+		warn("tcsetattr");
+}
+
+static void
+raw_termios(void)
+{
+	struct termios raw;
+
+	raw = saved_termios;
+	raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
+	raw.c_cflag |= (CS8);
+	raw.c_lflag &= ~(ECHO | ICANON | IEXTEN);
+	raw.c_cc[VMIN] = 1;
+	raw.c_cc[VTIME] = 0;
+
+	if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1)
+		die("tcsetattr");
+}
+
+static void
+cleanup(void)
+{
+	restore_termios();
+}
+
+static void
+setup_tui(void)
+{
+	check_termios();
+	raw_termios();
+	atexit(cleanup);
+}
+
+static void
+clear_screen(void)
+{
+	printf("\033[2J\033[H");
+}
+
+static void
+hide_cursor(void)
+{
+	printf("\033[?25l");
+}
+
+static void
+show_cursor(void)
+{
+	printf("\033[?25h");
+}
+
+static void
+flush_screen(void)
+{
+	fflush(stdout);
+}
+
+static void
+print_header(const char *card)
+{
+	printf("mixer [%.20s]  j/k:move  +/-:vol\n", card);
+	printf("[/]:card  m:mute  ?:help  q:quit\n\n");
+}
+
+static void
+print_elem_line(int idx, int cur, const char *name, long vol, int mute)
+{
+	if (idx == cur)
+		printf("> ");
+	else
+		printf("  ");
+
+	if (mute)
+		printf("%-*.*s  %3ld%%  muted\n", NAME_W, NAME_W, name, vol);
+	else
+		printf("%-*.*s  %3ld%%\n", NAME_W, NAME_W, name, vol);
+}
+
+static int
+read_key(void)
+{
+	unsigned char c;
+
+	if (read(STDIN_FILENO, &c, 1) != 1)
+		return -1;
+	if (c != '\033')
+		return c;
+	if (read(STDIN_FILENO, &c, 1) != 1)
+		return '\033';
+	if (c != '[')
+		return '\033';
+	if (read(STDIN_FILENO, &c, 1) != 1)
+		return '\033';
+
+	switch (c) {
+	case 'A':
+		return 'k';
+	case 'B':
+		return 'j';
+	case 'C':
+		return '+';
+	case 'D':
+		return '-';
+	default:
+		return '\033';
+	}
+}
+
+static void
+show_help(void)
+{
+	clear_screen();
+	printf("mixer help\n\n");
+	printf("j/k or arrows:\tmove\n");
+	printf("+/-:\t\tvolume\n");
+	printf("[/]:\t\tcard\n");
+	printf("m:\t\tmute\n");
+	printf("M:\t\tmute all\n");
+	printf("U:\t\tunmute all\n");
+	printf("q:\t\tquit\n");
+	printf("\npress any key to return\n");
+	flush_screen();
+	(void)read_key();
+}
+
+static void
+free_elems(struct elem_list *list)
+{
+	free(list->elems);
+	list->elems = NULL;
+	list->n = 0;
+}
+
+static void
+update_volume(snd_mixer_elem_t *elem, int delta)
+{
+	long step;
+	long max;
+	long min;
+	long raw;
+
+	if (snd_mixer_selem_get_playback_volume_range(elem, &min, &max) < 0)
+		return;
+	if (max <= min)
+		return;
+	if (snd_mixer_selem_get_playback_volume(elem,
+	    SND_MIXER_SCHN_FRONT_LEFT, &raw) < 0)
+		return;
+	step = volume_step(elem);
+	raw += delta * step;
+	if (raw < min)
+		raw = min;
+	if (raw > max)
+		raw = max;
+	if (snd_mixer_selem_set_playback_volume_all(elem, raw) < 0)
+		return;
+}
+
+static void
+render_loop(snd_mixer_t **mixer, struct card_list *cards)
+{
+	snd_mixer_t *curm;
+	struct elem_list list;
+	char card[MAX_CARDNAME];
+	char name[128];
+	int cur;
+	int key;
+	int mute;
+	long vol;
+
+	memset(&list, 0, sizeof(list));
+	cur = 0;
+	update_card(cards, card, sizeof(card));
+	if (open_mixer(mixer, card) == -1)
+		die("open mixer");
+	curm = *mixer;
+	if (load_elems(curm, &list) == -1)
+		die("load_elems");
+
+	while (!sig_exit) {
+		clear_screen();
+		print_header(card);
+		if (list.n == 0)
+			printf("(no playback controls)\n");
+		for (key = 0; key < list.n; key++) {
+			if (get_elem_name(list.elems[key], name,
+			    sizeof(name)) == -1)
+				xstrlcpy(name, "?", sizeof(name));
+			if (get_elem_volume(list.elems[key], &vol) == -1)
+				vol = 0;
+			if (get_elem_mute(list.elems[key], &mute) == -1)
+				mute = 0;
+			print_elem_line(key, cur, name, vol, mute);
+		}
+		flush_screen();
+
+		key = read_key();
+		switch (key) {
+		case 'q':
+		case 'Q':
+			free_elems(&list);
+			return;
+		case 'j':
+			if (cur + 1 < list.n)
+				cur++;
+			break;
+		case 'k':
+			if (cur > 0)
+				cur--;
+			break;
+		case '+':
+		case '=':
+			if (list.n > 0)
+				update_volume(list.elems[cur], 1);
+			break;
+		case '-':
+			if (list.n > 0)
+				update_volume(list.elems[cur], -1);
+			break;
+		case 'm':
+			if (list.n > 0) {
+				if (get_elem_mute(list.elems[cur], &mute) == 0)
+					(void)set_elem_mute(list.elems[cur], !mute);
+			}
+			break;
+		case 'M':
+			(void)set_all_mute(curm, 1);
+			break;
+		case 'U':
+			(void)set_all_mute(curm, 0);
+			break;
+		case '[':
+			if (cards->n > 1) {
+				cards->cur--;
+				if (cards->cur < 0)
+					cards->cur = cards->n - 1;
+				free_elems(&list);
+				snd_mixer_close(curm);
+				update_card(cards, card, sizeof(card));
+				if (open_mixer(&curm, card) == -1)
+					die("open mixer");
+				*mixer = curm;
+				if (load_elems(curm, &list) == -1)
+					die("load_elems");
+				if (cur >= list.n)
+					cur = 0;
+			}
+			break;
+		case ']':
+			if (cards->n > 1) {
+				cards->cur++;
+				if (cards->cur >= cards->n)
+					cards->cur = 0;
+				free_elems(&list);
+				snd_mixer_close(curm);
+				update_card(cards, card, sizeof(card));
+				if (open_mixer(&curm, card) == -1)
+					die("open mixer");
+				*mixer = curm;
+				if (load_elems(curm, &list) == -1)
+					die("load_elems");
+				if (cur >= list.n)
+					cur = 0;
+			}
+			break;
+		case '?':
+			show_help();
+			break;
+		default:
+			break;
+		}
+	}
+}
+
+static void
+run_tui(snd_mixer_t **mixer)
+{
+	struct card_list cards;
+
+	memset(&cards, 0, sizeof(cards));
+	if (load_cards(&cards) == -1)
+		die("load cards");
+	setup_tui();
+	hide_cursor();
+	render_loop(mixer, &cards);
+	show_cursor();
+	free(cards.ids);
+	if (cards.names != NULL) {
+		int i;
+		for (i = 0; i < cards.n; i++)
+			free(cards.names[i]);
+		free(cards.names);
+	}
+}
+
+static int
+elem_can_playback(snd_mixer_elem_t *elem)
+{
+	if (snd_mixer_selem_is_active(elem) == 0)
+		return 0;
+	if (snd_mixer_selem_has_playback_volume(elem) == 0)
+		return 0;
+	return 1;
+}
+
+static int
+get_elem_name(snd_mixer_elem_t *elem, char *buf, size_t len)
+{
+	const char *name;
+
+	name = snd_mixer_selem_get_name(elem);
+	if (name == NULL)
+		return -1;
+	if (xstrlcpy(buf, name, len) >= len)
+		buf[len - 1] = '\0';
+	return 0;
+}
+
+static int
+get_elem_volume(snd_mixer_elem_t *elem, long *out)
+{
+	long max, min, v;
+
+	if (snd_mixer_selem_get_playback_volume_range(elem, &min, &max) < 0)
+		return -1;
+	if (max <= min)
+		return -1;
+	if (snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT,
+	    &v) < 0)
+		return -1;
+	v = (v - min) * 100 / (max - min);
+	if (v < 0)
+		v = 0;
+	if (v > 100)
+		v = 100;
+	*out = v;
+	return 0;
+}
+
+static long
+volume_step(snd_mixer_elem_t *elem)
+{
+	long max, min, v;
+
+	if (snd_mixer_selem_get_playback_volume_range(elem, &min, &max) < 0)
+		return 1;
+	if (max <= min)
+		return 1;
+	v = (max - min) / 50;
+	if (v < 1)
+		v = 1;
+	return v;
+}
+
+static int
+set_elem_volume(snd_mixer_elem_t *elem, long percent)
+{
+	long max, min, v;
+
+	if (snd_mixer_selem_get_playback_volume_range(elem, &min, &max) < 0)
+		return -1;
+	if (percent < 0)
+		percent = 0;
+	if (percent > 100)
+		percent = 100;
+	v = min + (percent * (max - min) / 100);
+	if (snd_mixer_selem_set_playback_volume_all(elem, v) < 0)
+		return -1;
+	return 0;
+}
+
+static int
+get_elem_mute(snd_mixer_elem_t *elem, int *out)
+{
+	int sw;
+
+	if (snd_mixer_selem_has_playback_switch(elem) == 0) {
+		*out = 0;
+		return 0;
+	}
+	if (snd_mixer_selem_get_playback_switch(elem, SND_MIXER_SCHN_FRONT_LEFT,
+	    &sw) < 0)
+		return -1;
+	*out = (sw == 0);
+	return 0;
+}
+
+static int
+set_elem_mute(snd_mixer_elem_t *elem, int mute)
+{
+	int sw;
+
+	if (snd_mixer_selem_has_playback_switch(elem) == 0)
+		return 0;
+	sw = mute ? 0 : 1;
+	if (snd_mixer_selem_set_playback_switch_all(elem, sw) < 0)
+		return -1;
+	return 0;
+}
+
+static int
+set_all_mute(snd_mixer_t *mixer, int mute)
+{
+	snd_mixer_elem_t *elem;
+
+	for (elem = snd_mixer_first_elem(mixer); elem != NULL;
+	    elem = snd_mixer_elem_next(elem)) {
+		if (!elem_can_playback(elem))
+			continue;
+		(void)set_elem_mute(elem, mute);
+	}
+	return 0;
+}
+
+static int
+load_elems(snd_mixer_t *mixer, struct elem_list *list)
+{
+	snd_mixer_elem_t *elem;
+
+	list->elems = calloc(MAX_ELEMS, sizeof(list->elems[0]));
+	if (list->elems == NULL)
+		return -1;
+	list->n = 0;
+
+	for (elem = snd_mixer_first_elem(mixer); elem != NULL;
+	    elem = snd_mixer_elem_next(elem)) {
+		if (!elem_can_playback(elem))
+			continue;
+		if (list->n >= MAX_ELEMS)
+			break;
+		list->elems[list->n++] = elem;
+	}
+	return 0;
+}
+
+static int
+list_elems(snd_mixer_t *mixer)
+{
+	snd_mixer_elem_t *elem;
+	char name[128];
+	int mute;
+	long l;
+
+	for (elem = snd_mixer_first_elem(mixer); elem != NULL;
+	    elem = snd_mixer_elem_next(elem)) {
+		if (!elem_can_playback(elem))
+			continue;
+		if (get_elem_name(elem, name, sizeof(name)) == -1)
+			xstrlcpy(name, "?", sizeof(name));
+		if (get_elem_volume(elem, &l) == -1)
+			l = 0;
+		if (get_elem_mute(elem, &mute) == -1)
+			mute = 0;
+		printf("%s\t%ld\t%s\n", name, l, mute ? "mute" : "play");
+	}
+	return 0;
+}
+
+static snd_mixer_elem_t *
+find_elem(snd_mixer_t *mixer, const char *name)
+{
+	snd_mixer_elem_t *elem;
+	char buf[128];
+
+	for (elem = snd_mixer_first_elem(mixer); elem != NULL;
+	    elem = snd_mixer_elem_next(elem)) {
+		if (!elem_can_playback(elem))
+			continue;
+		if (get_elem_name(elem, buf, sizeof(buf)) == -1)
+			continue;
+		if (strcmp(buf, name) == 0)
+			return elem;
+	}
+	return NULL;
+}
+
+static int
+open_mixer(snd_mixer_t **out, const char *card)
+{
+	snd_mixer_t *mixer;
+	int r;
+
+	r = snd_mixer_open(&mixer, 0);
+	if (r < 0)
+		return -1;
+	r = snd_mixer_attach(mixer, card);
+	if (r < 0)
+		goto fail;
+	r = snd_mixer_selem_register(mixer, NULL, NULL);
+	if (r < 0)
+		goto fail;
+	r = snd_mixer_load(mixer);
+	if (r < 0)
+		goto fail;
+	*out = mixer;
+	return 0;
+
+fail:
+	snd_mixer_close(mixer);
+	return -1;
+}
+
+static int
+load_cards(struct card_list *list)
+{
+	int card;
+	int count;
+	int r;
+
+	list->ids = calloc(1, sizeof(list->ids[0]));
+	list->names = calloc(1, sizeof(list->names[0]));
+	if (list->ids == NULL || list->names == NULL)
+		return -1;
+	list->ids[0] = -1;
+	list->names[0] = strdup("default");
+	if (list->names[0] == NULL)
+		return -1;
+	list->n = 1;
+	list->cur = 0;
+
+	card = -1;
+	count = 0;
+	while ((r = snd_card_next(&card)) >= 0 && card >= 0) {
+		char *name;
+		int *ids;
+		char **names;
+
+		name = NULL;
+		if (snd_card_get_name(card, &name) < 0)
+			name = NULL;
+		ids = realloc(list->ids, sizeof(list->ids[0]) * (list->n + 1));
+		names = realloc(list->names, sizeof(list->names[0]) * (list->n + 1));
+		if (ids == NULL || names == NULL)
+			return -1;
+		list->ids = ids;
+		list->names = names;
+		list->ids[list->n] = card;
+		if (name != NULL)
+			list->names[list->n] = name;
+		else
+			list->names[list->n] = strdup("unknown");
+		if (list->names[list->n] == NULL)
+			return -1;
+		list->n++;
+		count++;
+	}
+	if (r < 0)
+		return -1;
+	if (count == 0 && list->n > 0)
+		list->cur = 0;
+	return 0;
+}
+
+static int
+update_card(struct card_list *list, char *buf, size_t len)
+{
+	int id;
+
+	if (list->n == 0)
+		return -1;
+	id = list->ids[list->cur];
+	if (id < 0)
+		xstrlcpy(buf, "default", len);
+	else
+		sprintf(buf, "hw:%d", id);
+	return 0;
+}
+
+static void
+die(const char *msg)
+{
+	err(1, "%s", msg);
+}
+
+int
+main(int argc, char *argv[])
+{
+	snd_mixer_t *mixer;
+	snd_mixer_elem_t *elem;
+	const char *card;
+	int ch;
+	long l;
+
+	progname = argv[0];
+	card = "default";
+
+	while ((ch = getopt(argc, argv, "c:d:glms:t")) != -1) {
+		switch (ch) {
+		case 'c':
+			control = optarg;
+			break;
+		case 'd':
+			card = optarg;
+			break;
+		case 'g':
+			do_get = 1;
+			break;
+		case 'l':
+			do_list = 1;
+			break;
+		case 'm':
+			do_mute = atoi(optarg);
+			if (do_mute != 0 && do_mute != 1)
+				usage();
+			break;
+		case 's':
+			do_set = atoi(optarg);
+			if (do_set < 0 || do_set > 100)
+				usage();
+			break;
+		case 't':
+			do_tui = 1;
+			break;
+		default:
+			usage();
+		}
+	}
+	argc -= optind;
+	argv += optind;
+
+	if (argc != 0)
+		usage();
+
+	if (do_list + do_get + (do_set != -1) + (do_mute != -1) + do_tui == 0)
+		do_tui = 1;
+
+	if (open_mixer(&mixer, card) == -1)
+		die("open mixer");
+
+	set_signal_handlers();
+
+	if (do_list) {
+		list_elems(mixer);
+		snd_mixer_close(mixer);
+		return 0;
+	}
+
+	if (do_tui) {
+		snd_mixer_close(mixer);
+		run_tui(&mixer);
+		snd_mixer_close(mixer);
+		return 0;
+	}
+
+	if (control == NULL)
+		usage();
+
+	elem = find_elem(mixer, control);
+	if (elem == NULL)
+		errx(1, "control not found: %s", control);
+
+	if (do_set != -1) {
+		if (set_elem_volume(elem, do_set) == -1)
+			die("set volume");
+	}
+
+	if (do_mute != -1) {
+		if (set_elem_mute(elem, do_mute) == -1)
+			die("set mute");
+	}
+
+	if (do_get) {
+		if (get_elem_volume(elem, &l) == -1)
+			die("get volume");
+		printf("%ld\n", l);
+	}
+
+	snd_mixer_close(mixer);
+	return 0;
+}