Commit Diff


commit - /dev/null
commit + 64957a6419d22c2e1ccdcee51b7c25067ae6badc
blob - /dev/null
blob + 3e6d7786253c41479580428aa687557a1c0c7e68 (mode 644)
--- /dev/null
+++ Makefile
@@ -0,0 +1,11 @@
+PROG=		siomixer
+WARNINGS=	yes
+BINDIR?=	/usr/local/bin
+MANDIR?=	/usr/local/man/man
+CFLAGS+=	$$(pkg-config --cflags gtk4) -g -O0 -Wall
+LDADD+=		-lutil -lsndio $$(pkg-config --libs gtk4)
+SRCS=		siomixer.c audiowidget.c
+
+VERSION=	0.1
+
+.include <bsd.prog.mk>
blob - /dev/null
blob + 79a24e62adbe5a4d5142b94b336b9af035b42ff3 (mode 644)
--- /dev/null
+++ audiowidget.c
@@ -0,0 +1,103 @@
+#include <sndio.h>
+#include <gtk/gtk.h>
+
+#include "siomixer.h"
+
+extern struct app_state s;
+
+static gboolean
+_on_debounce_timeout(gpointer user_data)
+{
+	struct info *i = user_data;
+	double value = gtk_range_get_value(audiowidget_get_gtkrange(i->widget));
+	unsigned int uvalue = (unsigned int)(value + 0.5);
+	sioctl_setval(s.hdl, i->ctladdr, uvalue);
+
+	i->timeout = 0;
+	return G_SOURCE_REMOVE; // one-shot
+}
+
+static void
+_on_scale_value_changed(GtkRange *range, gpointer user_data)
+{
+	struct info *i = user_data;
+
+	/* Rate limit */
+	if (i->timeout != 0)
+		return;
+	i->timeout = g_timeout_add(150, _on_debounce_timeout, i);
+}
+
+/* Handle conversion from 0-255 to % */
+static char *
+_format_percent(GtkScale *scale, double value, gpointer user_data)
+{
+	GtkAdjustment *adj = gtk_range_get_adjustment(GTK_RANGE(scale));
+	double min = gtk_adjustment_get_lower(adj);
+	double max = gtk_adjustment_get_upper(adj);
+	double percent = (value - min) / (max - min) * 100.0;
+
+	// printf("(%f - %f) / (%f - %f) * 100.0 = %f\n", value, min, max, min, percent);
+
+	return g_strdup_printf("%.0f%%", percent);
+}
+
+GtkRange *
+audiowidget_get_gtkrange(AudioWidget *a)
+{
+	return GTK_RANGE(a->scale);
+}
+
+GtkWidget *
+audiowidget_get_gtkwidget(AudioWidget *a)
+{
+	return a->box;
+}
+
+AudioWidget *
+audiowidget_new(struct info *i)
+{
+	char *label = NULL;
+	char *group = NULL;
+	AudioWidget *a;
+
+	switch (i->desc.type) {
+		case SIOCTL_NUM:
+			/* Only numeric switches make sense as scales */
+			break;
+		default:
+			/* do nothing */
+			return NULL;
+	}
+	// printf("%s %s %d\n", i->desc.group, i->desc.func, i->curval);
+
+	a = calloc(1, sizeof(AudioWidget));
+	if (a == NULL)
+		return NULL;
+
+	if (i->desc.group[0] != '\0')
+		asprintf(&group, "%s/", i->desc.group);
+	asprintf(&label, "%s%s.%s", group ? group : "", i->desc.node0.name, i->desc.func);
+
+	GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
+	GtkWidget *l = gtk_label_new(label);
+	gtk_widget_set_vexpand(l, FALSE);
+	gtk_box_append(GTK_BOX(box), l);
+
+	GtkWidget *gscale = gtk_scale_new_with_range(GTK_ORIENTATION_VERTICAL, 0, i->desc.maxval, 1);
+	gtk_range_set_inverted(GTK_RANGE(gscale), TRUE);
+	gtk_widget_set_vexpand(gscale, TRUE);
+	gtk_scale_set_draw_value(GTK_SCALE(gscale), TRUE);
+	gtk_scale_set_format_value_func(GTK_SCALE(gscale), _format_percent, NULL, NULL);
+	g_signal_connect(gscale, "value-changed", G_CALLBACK(_on_scale_value_changed), i);
+	gtk_range_set_value(GTK_RANGE(gscale), i->curval);
+	gtk_box_append(GTK_BOX(box), gscale);
+
+	free(label);
+	free(group);
+
+	a->box = box;
+	a->scale = gscale;
+
+	return a;
+}
blob - /dev/null
blob + 5f3f825d0e7a4c9fa6585a849631da64e1881ec1 (mode 644)
--- /dev/null
+++ siomixer.c
@@ -0,0 +1,488 @@
+/*
+ * Copyright (c) 2026 Tobias Heider <tobhe@openbsd.org>
+ * Copyright (c) 2014-2020 Alexandre Ratchov <alex@caoua.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <poll.h>
+#include <sndio.h>
+#include <fcntl.h>
+
+#include <gtk/gtk.h>
+
+#include "siomixer.h"
+
+struct app_state s = { 0 };
+
+struct sioctl_source {
+	GSource source;
+	struct pollfd *pfds;
+	int nfds;
+};
+
+static gboolean sio_fd_prepare(GSource *source, gint *timeout_);
+static gboolean sio_fd_check(GSource *source);
+static gboolean sio_fd_dispatch(GSource *source, GSourceFunc callback, gpointer user_data);
+
+static GSourceFuncs sioctl_source_funcs = {
+	sio_fd_prepare,
+	sio_fd_check,
+	sio_fd_dispatch,
+	NULL, /* finalize */
+	NULL, NULL,
+};
+
+int cmpdesc(struct sioctl_desc *, struct sioctl_desc *);
+int isdiag(struct info *);
+int ismono(struct info *);
+struct info *vecent(struct info *, char *, int);
+struct info *nextfunc(struct info *);
+struct info *nextpar(struct info *);
+struct info *firstent(struct info *, char *);
+struct info *nextent(struct info *, int);
+void ondesc(void *, struct sioctl_desc *, int);
+void onctl(void *, unsigned, unsigned);
+
+/*
+ * compare two sioctl_desc structures, used to sort infolist
+ */
+int
+cmpdesc(struct sioctl_desc *d1, struct sioctl_desc *d2)
+{
+	int res;
+
+	res = strcmp(d1->group, d2->group);
+	if (res != 0)
+		return res;
+	res = strcmp(d1->node0.name, d2->node0.name);
+	if (res != 0)
+		return res;
+	res = d1->type - d2->type;
+	if (res != 0)
+		return res;
+	res = strcmp(d1->func, d2->func);
+	if (res != 0)
+		return res;
+	res = d1->node0.unit - d2->node0.unit;
+	if (d1->type == SIOCTL_SEL ||
+	    d1->type == SIOCTL_VEC ||
+	    d1->type == SIOCTL_LIST) {
+		if (res != 0)
+			return res;
+		res = strcmp(d1->node1.name, d2->node1.name);
+		if (res != 0)
+			return res;
+		res = d1->node1.unit - d2->node1.unit;
+	}
+	return res;
+}
+
+/*
+ * register a new knob/button, called from the poll() loop.  this may be
+ * called when label string changes, in which case we update the
+ * existing label widget rather than inserting a new one.
+ */
+void
+ondesc(void *arg, struct sioctl_desc *d, int curval)
+{
+	struct info *i, **pi;
+	int cmp;
+
+	if (d == NULL)
+		return;
+
+	/*
+	 * delete control
+	 */
+	for (pi = &s.infolist; (i = *pi) != NULL; pi = &i->next) {
+		if (d->addr == i->desc.addr) {
+			*pi = i->next;
+			/* XXX: free removed widgets */
+			free(i);
+			break;
+		}
+	}
+
+	switch (d->type) {
+	case SIOCTL_NUM:
+	case SIOCTL_SW:
+	case SIOCTL_VEC:
+	case SIOCTL_LIST:
+	case SIOCTL_SEL:
+		break;
+	case SIOCTL_NONE:
+		gtk_flow_box_remove_all(GTK_FLOW_BOX(s.flowbox));
+		/* XXX: free removed widgets */
+		for (i = s.infolist; i != NULL; i = nextfunc(i)) {
+			i->widget = audiowidget_new(i);
+			if (i->widget) {
+				gtk_flow_box_append(GTK_FLOW_BOX(s.flowbox),
+				    audiowidget_get_gtkwidget(i->widget));
+			}
+		}
+		/* fallthrough */
+	default:
+		return;
+	}
+
+	/*
+	 * find the right position to insert the new widget
+	 */
+	for (pi = &s.infolist; (i = *pi) != NULL; pi = &i->next) {
+		cmp = cmpdesc(d, &i->desc);
+		if (cmp <= 0)
+			break;
+	}
+	i = malloc(sizeof(struct info));
+	if (i == NULL) {
+		perror("malloc");
+		exit(1);
+	}
+	i->desc = *d;
+	i->ctladdr = d->addr;
+	i->curval = i->newval = curval;
+	i->mode = MODE_IGNORE;
+	i->next = *pi;
+	i->timeout = 0;
+	i->widget = NULL;
+
+	/* Add widget and link to list */
+	if (s.flowbox) {
+		i->widget = audiowidget_new(i);
+		if (i->widget) {
+			gtk_flow_box_append(GTK_FLOW_BOX(s.flowbox),
+			    audiowidget_get_gtkwidget(i->widget));
+		}
+	}
+
+	*pi = i;
+}
+
+/*
+ * return true of the vector entry is diagonal
+ */
+int
+isdiag(struct info *e)
+{
+	if (e->desc.node0.unit < 0 || e->desc.node1.unit < 0)
+		return 1;
+	return e->desc.node1.unit == e->desc.node0.unit;
+}
+
+/*
+ * find the selector or vector entry with the given name and channels
+ */
+struct info *
+vecent(struct info *i, char *vstr, int vunit)
+{
+	while (i != NULL) {
+		if ((strcmp(i->desc.node1.name, vstr) == 0) &&
+		    (vunit < 0 || i->desc.node1.unit == vunit))
+			break;
+		i = i->next;
+	}
+	return i;
+}
+
+/*
+ * skip all parameters with the same group, name, and func
+ */
+struct info *
+nextfunc(struct info *i)
+{
+	char *str, *group, *func;
+
+	group = i->desc.group;
+	func = i->desc.func;
+	str = i->desc.node0.name;
+	for (i = i->next; i != NULL; i = i->next) {
+		if (strcmp(i->desc.group, group) != 0 ||
+		    strcmp(i->desc.node0.name, str) != 0 ||
+		    strcmp(i->desc.func, func) != 0)
+			return i;
+	}
+	return NULL;
+}
+
+
+/*
+ * find the next parameter with the same group, name, func
+ */
+struct info *
+nextpar(struct info *i)
+{
+	char *str, *group, *func;
+	int unit;
+
+	group = i->desc.group;
+	func = i->desc.func;
+	str = i->desc.node0.name;
+	unit = i->desc.node0.unit;
+	for (i = i->next; i != NULL; i = i->next) {
+		if (strcmp(i->desc.group, group) != 0 ||
+		    strcmp(i->desc.node0.name, str) != 0 ||
+		    strcmp(i->desc.func, func) != 0)
+			break;
+		/* XXX: need to check for -1 ? */
+		if (i->desc.node0.unit != unit)
+			return i;
+	}
+	return NULL;
+}
+
+/*
+ * return the first vector entry with the given name
+ */
+struct info *
+firstent(struct info *g, char *vstr)
+{
+	char *astr, *group, *func;
+	struct info *i;
+
+	group = g->desc.group;
+	astr = g->desc.node0.name;
+	func = g->desc.func;
+	for (i = g; i != NULL; i = i->next) {
+		if (strcmp(i->desc.group, group) != 0 ||
+		    strcmp(i->desc.node0.name, astr) != 0 ||
+		    strcmp(i->desc.func, func) != 0)
+			break;
+		if (!isdiag(i))
+			continue;
+		if (strcmp(i->desc.node1.name, vstr) == 0)
+			return i;
+	}
+	return NULL;
+}
+
+/*
+ * find the next entry of the given vector, if the mono flag
+ * is set then the whole group is searched and off-diagonal entries are
+ * skipped
+ */
+struct info *
+nextent(struct info *i, int mono)
+{
+	char *str, *group, *func;
+	int unit;
+
+	group = i->desc.group;
+	func = i->desc.func;
+	str = i->desc.node0.name;
+	unit = i->desc.node0.unit;
+	for (i = i->next; i != NULL; i = i->next) {
+		if (strcmp(i->desc.group, group) != 0 ||
+		    strcmp(i->desc.node0.name, str) != 0 ||
+		    strcmp(i->desc.func, func) != 0)
+			return NULL;
+		if (mono)
+			return i;
+		if (i->desc.node0.unit == unit)
+			return i;
+	}
+	return NULL;
+}
+
+/*
+ * return true if the given group can be represented as a signle mono
+ * parameter
+ */
+int
+ismono(struct info *g)
+{
+	struct info *p1, *p2;
+	struct info *e1, *e2;
+
+	p1 = g;
+	switch (g->desc.type) {
+	case SIOCTL_NUM:
+	case SIOCTL_SW:
+		for (p2 = g; p2 != NULL; p2 = nextpar(p2)) {
+			if (p2->curval != p1->curval)
+				return 0;
+		}
+		break;
+	case SIOCTL_SEL:
+	case SIOCTL_VEC:
+	case SIOCTL_LIST:
+		for (p2 = g; p2 != NULL; p2 = nextpar(p2)) {
+			for (e2 = p2; e2 != NULL; e2 = nextent(e2, 0)) {
+				if (!isdiag(e2)) {
+					if (e2->curval != 0)
+						return 0;
+				} else {
+					e1 = vecent(p1,
+					    e2->desc.node1.name,
+					    p1->desc.node0.unit);
+					if (e1 == NULL)
+						continue;
+					if (e1->curval != e2->curval)
+						return 0;
+					if (strcmp(e1->desc.display,
+						e2->desc.display) != 0)
+						return 0;
+				}
+			}
+		}
+		break;
+	}
+	return 1;
+}
+
+static void
+activate (GtkApplication *app, gpointer user_data)
+{
+	struct info *i;
+	GtkWidget *window;
+	GtkWidget *scroll;
+
+	window = gtk_application_window_new (app);
+	gtk_window_set_title(GTK_WINDOW (window), "OpenMixer");
+	gtk_window_set_default_size(GTK_WINDOW (window), -1, 400);
+
+	scroll = gtk_scrolled_window_new();
+
+	s.flowbox = gtk_flow_box_new();
+	gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(s.flowbox), GTK_SELECTION_NONE);
+
+	/* Build initial list of widgets and append to flow box */
+	for (i = s.infolist; i != NULL; i = nextfunc(i)) {
+		if (i->widget == NULL)
+			i->widget = audiowidget_new(i);
+		if (i->widget)
+			gtk_flow_box_append(GTK_FLOW_BOX(s.flowbox),
+			    audiowidget_get_gtkwidget(i->widget));
+	}
+
+	gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), s.flowbox);
+
+	gtk_window_set_child(GTK_WINDOW(window), scroll);
+
+	gtk_window_present(GTK_WINDOW (window));
+}
+
+/*
+ * update a knob/button state, called from the poll() loop
+ */
+void
+onctl(void *arg, unsigned addr, unsigned val)
+{
+	struct info *i;
+
+	/* Update infolist */
+	i = s.infolist;
+	for (;;) {
+		if (i == NULL)
+			return;
+		if (i->ctladdr == addr)
+			break;
+		i = i->next;
+	}
+	if (i->desc.type == SIOCTL_SEL) {
+		if (strcmp(i->desc.node0.name, "server") == 0 &&
+		    strcmp(i->desc.func, "device") == 0) {
+			printf("controls changed, updating...\n");
+			/* XXX Should update the dropdown status here */
+		}
+	} else {
+		i->curval = val;
+		if (i->widget)
+			gtk_range_set_value(audiowidget_get_gtkrange(i->widget),
+			    i->curval);
+	}
+}
+
+static gboolean
+sio_fd_prepare(GSource *source, gint *timeout_)
+{
+	*timeout_ = -1; // 100 ms polling interval
+	return FALSE;
+}
+
+static gboolean
+sio_fd_check(GSource *source)
+{
+	struct sioctl_source *ss = (struct sioctl_source *)source;
+	int revents = sioctl_revents(s.hdl, ss->pfds);
+	return revents ? TRUE : FALSE;
+}
+
+static gboolean
+sio_fd_dispatch(GSource *source, GSourceFunc callback, gpointer user_data)
+{
+	struct sioctl_source *ss = (struct sioctl_source *)source;
+	printf("%s: __entry__\n", __func__);
+	for (int i = 0; i < ss->nfds; i++) {
+		printf("%d:\n", i);
+		printf("fd: %d\n", ss->pfds[i].fd);
+		printf("events: %d\n", ss->pfds[i].events);
+		printf("revents: %d\n", ss->pfds[i].revents);
+	}
+	return TRUE;
+}
+
+int
+main (int argc, char **argv)
+{
+	GSource *gs;
+	char *devname = SIO_DEVANY;
+	GtkApplication *app;
+	int status;
+
+	app = gtk_application_new ("de.tobhe.siomixer", G_APPLICATION_DEFAULT_FLAGS);
+	g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
+
+	s.hdl = sioctl_open(devname, SIOCTL_READ | SIOCTL_WRITE, 0);
+	if (s.hdl == NULL) {
+		fprintf(stderr, "%s: can't open control device\n", devname);
+		exit(1);
+	}
+
+	/* Initial state */
+	if (!sioctl_ondesc(s.hdl, ondesc, NULL)) {
+		fprintf(stderr, "sioctl_ondesc: can't get device description\n");
+		exit(1);
+	}
+	sioctl_onval(s.hdl, onctl, NULL);
+
+	gs = g_source_new(&sioctl_source_funcs, sizeof(struct sioctl_source));
+	struct sioctl_source *ss = (struct sioctl_source *)gs;
+	ss->pfds = calloc(sioctl_nfds(s.hdl), sizeof(struct pollfd));
+	if (ss->pfds == NULL) {
+		perror("calloc");
+		exit(1);
+	}
+	ss->nfds = sioctl_pollfd(s.hdl, ss->pfds, POLLIN);
+	if (ss->nfds == 0) {
+		perror("sioctl_pollfd");
+		exit(1);
+	}
+	for (int i = 0; i < ss->nfds; i++) {
+		printf("%d:\n", i);
+		printf("fd: %d\n", ss->pfds[i].fd);
+		if (fcntl(ss->pfds[i].fd, F_GETFL) == -1) { 
+			printf("Invalid FD");
+			exit(1);
+		}
+		printf("events: %d\n", ss->pfds[i].events);
+		printf("revents: %d\n", ss->pfds[i].revents);
+		g_source_add_poll(gs, (GPollFD *)&ss->pfds[i]);
+	}
+	g_source_attach(gs, NULL);
+
+	status = g_application_run (G_APPLICATION (app), argc, argv);
+	g_object_unref (app);
+
+	return status;
+}
blob - /dev/null
blob + 92dc556ba6b02aa36bcc4dc1584c6c75bb45d61c (mode 644)
--- /dev/null
+++ siomixer.h
@@ -0,0 +1,34 @@
+typedef struct _audiowidget AudioWidget;
+
+struct info {
+	struct info *next;
+	struct sioctl_desc desc;
+	unsigned ctladdr;
+#define MODE_IGNORE	0	/* ignore this value */
+#define MODE_PRINT	1	/* print-only, don't change value */
+#define MODE_SET	2	/* set to newval value */
+#define MODE_ADD	3	/* increase current value by newval */
+#define MODE_SUB	4	/* decrease current value by newval */
+#define MODE_TOGGLE	5	/* toggle current value */
+	unsigned mode;
+	int curval, newval;
+	unsigned timeout;
+	AudioWidget *widget;
+};
+
+struct app_state {
+	struct sioctl_hdl *hdl;
+	struct pollfd *pfds;
+	int nfds;
+	struct info *infolist;
+	GtkWidget *flowbox;	
+};
+
+struct _audiowidget {
+	GtkWidget *box;
+	GtkWidget *scale;
+};
+
+AudioWidget *audiowidget_new(struct info *);
+GtkWidget *audiowidget_get_gtkwidget(AudioWidget *);
+GtkRange *audiowidget_get_gtkrange(AudioWidget *);