commit 64957a6419d22c2e1ccdcee51b7c25067ae6badc from: Tobias Heider date: Sat Feb 14 20:46:45 2026 UTC Initial import Controls and automatic update on sndioctl messages are working. Currently only scales for volume values are supported. There isn't a selection for the actual device yet and there are some known issues around mono vs stereo devices. 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 blob - /dev/null blob + 79a24e62adbe5a4d5142b94b336b9af035b42ff3 (mode 644) --- /dev/null +++ audiowidget.c @@ -0,0 +1,103 @@ +#include +#include + +#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 + * Copyright (c) 2014-2020 Alexandre Ratchov + * + * 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 +#include +#include + +#include + +#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 *);