commit - /dev/null
commit + 64957a6419d22c2e1ccdcee51b7c25067ae6badc
blob - /dev/null
blob + 3e6d7786253c41479580428aa687557a1c0c7e68 (mode 644)
--- /dev/null
+++ Makefile
+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
+#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
+/*
+ * 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
+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 *);