Initial revision

This commit is contained in:
Geoffrey D. Bennett
2022-03-14 09:24:43 +10:30
commit 17b4d2f055
89 changed files with 40806 additions and 0 deletions

54
src/Makefile Normal file
View File

@@ -0,0 +1,54 @@
# Credit to Tom Tromey and Paul D. Smith:
# http://make.mad-scientist.net/papers/advanced-auto-dependency-generation/
DEPDIR := .deps
DEPFLAGS = -MT $@ -MMD -MP -MF $(DEPDIR)/$*.d
CFLAGS := -Wall -Werror -ggdb -fno-omit-frame-pointer -O2 -D_FORTIFY_SOURCE=2
PKG_CONFIG=pkg-config
CFLAGS += $(shell $(PKG_CONFIG) --cflags glib-2.0)
CFLAGS += $(shell $(PKG_CONFIG) --cflags gtk4)
CFLAGS += $(shell $(PKG_CONFIG) --cflags alsa)
LDFLAGS += $(shell $(PKG_CONFIG) --libs glib-2.0)
LDFLAGS += $(shell $(PKG_CONFIG) --libs gtk4)
LDFLAGS += $(shell $(PKG_CONFIG) --libs alsa)
COMPILE.c = $(CC) $(DEPFLAGS) $(CFLAGS) -c
%.c: %.xml $(DEPDIR)/%-xml.d | $(DEPDIR)
echo $@: $(shell $(GLIB_COMPILE_RESOURCES) $< --generate-dependencies) > $(DEPDIR)/$*-xml.d
$(GLIB_COMPILE_RESOURCES) $< --target=$@ --generate-source
XML_SRC := $(wildcard *.xml)
XML_OBJ := $(patsubst %.xml,%.c,$(XML_SRC))
%.o: %.c
%.o: %.c Makefile $(DEPDIR)/%.d | $(DEPDIR)
$(COMPILE.c) $(OUTPUT_OPTION) $<
SRCS := $(sort $(wildcard *.c) $(XML_OBJ))
OBJS := $(patsubst %.c,%.o,$(SRCS))
TARGET := alsa-scarlett-gui
GLIB_COMPILE_RESOURCES := $(shell $(PKG_CONFIG) --variable=glib_compile_resources gio-2.0)
all: $(TARGET)
clean:
rm -f $(TARGET) $(OBJS) $(XML_OBJ)
depclean:
rm -rf $(DEPDIR)
$(DEPDIR): ; @mkdir -p $@
DEPFILES := $(SRCS:%.c=$(DEPDIR)/%.d) $(XML_SRC:%.xml=$(DEPDIR)/%-xml.d)
$(DEPFILES):
include $(wildcard $(DEPFILES))
$(TARGET): $(OBJS)
cc ${LDFLAGS} -lm -o $(TARGET) $(OBJS)

32
src/about.c Normal file
View File

@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "about.h"
void activate_about(
GSimpleAction *action,
GVariant *parameter,
gpointer data
) {
GtkWindow *w = GTK_WINDOW(data);
const char *authors[] = {
"Geoffrey D. Bennett",
NULL
};
gtk_show_about_dialog(
w,
"program-name", "ALSA Scarlett Gen 2/3 Control Panel",
"version", "Version 0.1",
"comments", "GTK4 interface to the ALSA Scarlett Gen 2/3 Mixer controls",
"website", "https://github.com/geoffreybennett/alsa-scarlett-gui",
"copyright", "Copyright 2022 Geoffrey D. Bennett",
"license-type", GTK_LICENSE_GPL_3_0,
"logo-icon-name", "alsa-scarlett-gui-logo",
"title", "About ALSA Scarlett Mixer Interface",
"authors", authors,
NULL
);
}

12
src/about.h Normal file
View File

@@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
void activate_about(
GSimpleAction *action,
GVariant *parameter,
gpointer data
);

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/vu/b4/alsa-scarlett-gui/icons">
<file alias="alsa-scarlett-gui-logo.png">img/alsa-scarlett-gui-logo.png</file>
<file alias="socket.svg">img/socket.svg</file>
</gresource>
<gresource prefix="/vu/b4/alsa-scarlett-gui/icons/48x48/apps">
<file alias="alsa-scarlett-gui.png">img/alsa-scarlett-gui-48.png</file>
</gresource>
<gresource prefix="/vu/b4/alsa-scarlett-gui/icons/256x256/apps">
<file alias="alsa-scarlett-gui.png">img/alsa-scarlett-gui-256.png</file>
</gresource>
<gresource prefix="/vu/b4/alsa-scarlett-gui">
<file>alsa-scarlett-gui.css</file>
</gresource>
</gresources>

View File

@@ -0,0 +1,4 @@
.route-label { font-size: smaller; }
.route-label:hover { background: #e0e0e0; }
.route-label:drop(active) { box-shadow: none; background: #e0e0e0; }
.button { padding: 0px 5px 0px 5px; }

400
src/alsa-sim.c Normal file
View File

@@ -0,0 +1,400 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "alsa.h"
#include "alsa-sim.h"
#include "error.h"
#include "window-iface.h"
// check that *config is a compound node, retrieve the first node
// within, check that that node is a compound node, optionally check
// its ID, and replace *config with the child
static void get_and_check_first_compound(
snd_config_t **config,
const char *expected_id
) {
const char *id, *child_id;
int err;
err = snd_config_get_id(*config, &id);
if (err < 0)
fatal_alsa_error("snd_config_get_id error", err);
if (snd_config_get_type(*config) != SND_CONFIG_TYPE_COMPOUND) {
printf("config node '%s' is not of type compound\n", id);
exit(1);
}
snd_config_iterator_t i = snd_config_iterator_first(*config);
if (i == snd_config_iterator_end(*config)) {
printf("compound config node '%s' has no children\n", id);
exit(1);
}
snd_config_t *config_child = snd_config_iterator_entry(i);
err = snd_config_get_id(config_child, &child_id);
if (err < 0)
fatal_alsa_error("snd_config_get_id error", err);
if (snd_config_get_type(config_child) != SND_CONFIG_TYPE_COMPOUND) {
printf("config node %s->%s is not of type compound\n", id, child_id);
exit(1);
}
*config = config_child;
if (!expected_id)
return;
if (!child_id) {
printf("config node has no id\n");
exit(1);
}
if (strcmp(child_id, expected_id) != 0) {
printf(
"found config node %s->%s instead of %s\n",
id, child_id, expected_id
);
exit(1);
}
}
static void alsa_parse_enum_items(
snd_config_t *items,
struct alsa_elem *elem
) {
int count = snd_config_is_array(items);
if (count < 0) {
printf("error: parse enum items array value %d\n", count);
return;
}
elem->item_count = count;
elem->item_names = calloc(count, sizeof(char *));
int item_num = 0;
snd_config_iterator_t i, next;
snd_config_for_each(i, next, items) {
snd_config_t *node = snd_config_iterator_entry(i);
const char *key;
int err = snd_config_get_id(node, &key);
if (err < 0)
fatal_alsa_error("snd_config_get_id error", err);
int type = snd_config_get_type(node);
if (type != SND_CONFIG_TYPE_STRING) {
printf("error: enum item %s type %d not string\n", key, type);
return;
}
const char *s;
err = snd_config_get_string(node, &s);
if (err < 0)
fatal_alsa_error("snd_config_get_string error", err);
elem->item_names[item_num++] = strdup(s);
}
}
// parse a comment node and update elem, e.g.:
//
// comment {
// access read
// type ENUMERATED
// count 1
// item.0 Line
// item.1 Inst
// }
static void alsa_parse_comment_node(
snd_config_t *comment,
struct alsa_elem *elem
) {
snd_config_iterator_t i, next;
snd_config_for_each(i, next, comment) {
snd_config_t *node = snd_config_iterator_entry(i);
const char *key;
int err = snd_config_get_id(node, &key);
if (err < 0)
fatal_alsa_error("snd_config_get_id error", err);
int type = snd_config_get_type(node);
if (strcmp(key, "access") == 0) {
if (type != SND_CONFIG_TYPE_STRING) {
printf("access type not string\n");
return;
}
const char *access;
err = snd_config_get_string(node, &access);
if (err < 0)
fatal_alsa_error("snd_config_get_string error", err);
if (strstr(access, "write"))
elem->writable = 1;
} else if (strcmp(key, "type") == 0) {
if (type != SND_CONFIG_TYPE_STRING) {
printf("type type not string\n");
return;
}
const char *type;
err = snd_config_get_string(node, &type);
if (err < 0)
fatal_alsa_error("snd_config_get_string error", err);
if (strcmp(type, "BOOLEAN") == 0)
elem->type = SND_CTL_ELEM_TYPE_BOOLEAN;
else if (strcmp(type, "ENUMERATED") == 0)
elem->type = SND_CTL_ELEM_TYPE_ENUMERATED;
else if (strcmp(type, "INTEGER") == 0)
elem->type = SND_CTL_ELEM_TYPE_INTEGER;
} else if (strcmp(key, "item") == 0) {
alsa_parse_enum_items(node, elem);
}
}
}
static int alsa_config_to_new_elem(
snd_config_t *config,
struct alsa_elem *elem
) {
const char *s;
int id;
char *iface = NULL, *name = NULL;
int seen_value;
int value_type = -1;
char *string_value = NULL;
long int_value;
int err;
err = snd_config_get_id(config, &s);
if (err < 0)
fatal_alsa_error("snd_config_get_id error", err);
id = atoi(s);
// loop through the nodes of the control element
snd_config_iterator_t i, next;
snd_config_for_each(i, next, config) {
snd_config_t *node = snd_config_iterator_entry(i);
const char *key;
err = snd_config_get_id(node, &key);
if (err < 0)
fatal_alsa_error("snd_config_get_id error", err);
int type = snd_config_get_type(node);
// iface node?
if (strcmp(key, "iface") == 0) {
if (type != SND_CONFIG_TYPE_STRING) {
printf("iface type for %d is %d not string", id, type);
goto fail;
}
err = snd_config_get_string(node, &s);
if (err < 0)
fatal_alsa_error("snd_config_get_string error", err);
iface = strdup(s);
// name node?
} else if (strcmp(key, "name") == 0) {
if (type != SND_CONFIG_TYPE_STRING) {
printf("name type for %d is %d not string", id, type);
goto fail;
}
err = snd_config_get_string(node, &s);
if (err < 0)
fatal_alsa_error("snd_config_get_string error", err);
name = strdup(s);
// value node?
} else if (strcmp(key, "value") == 0) {
seen_value = 1;
value_type = type;
if (type == SND_CONFIG_TYPE_INTEGER) {
err = snd_config_get_integer(node, &int_value);
if (err < 0)
fatal_alsa_error("snd_config_get_integer error", err);
} else if (type == SND_CONFIG_TYPE_STRING) {
err = snd_config_get_string(node, &s);
if (err < 0)
fatal_alsa_error("snd_config_get_string error", err);
string_value = strdup(s);
} else if (type == SND_CONFIG_TYPE_COMPOUND) {
elem->count = snd_config_is_array(node);
if (strcmp(name, "Level Meter") == 0) {
seen_value = 1;
value_type = SND_CONFIG_TYPE_INTEGER;
int_value = 0;
} else {
goto fail;
}
} else {
printf(
"skipping value type for %d; is %d, not int or string\n",
id, type
);
goto fail;
}
// comment node?
} else if (strcmp(key, "comment") == 0) {
alsa_parse_comment_node(node, elem);
} else {
printf("skipping unknown node %s for %d\n", key, id);
goto fail;
}
}
// check iface value; only interested in MIXER and PCM
if (!iface) {
printf("missing iface node in control id %d\n", id);
goto fail;
}
if (strcmp(iface, "MIXER") != 0 &&
strcmp(iface, "PCM") != 0)
goto fail;
// check for presence of name and value
if (!name) {
printf("missing name node in control id %d\n", id);
goto fail;
}
if (!seen_value) {
printf("missing value node in control id %d\n", id);
goto fail;
}
// set the element value
// integer in config
if (value_type == SND_CONFIG_TYPE_INTEGER) {
elem->value = int_value;
// string in config
} else if (value_type == SND_CONFIG_TYPE_STRING) {
// translate boolean true/false
if (elem->type == SND_CTL_ELEM_TYPE_BOOLEAN) {
if (strcmp(string_value, "true") == 0)
elem->value = 1;
// translate enum string value to integer
} else if (elem->type == SND_CTL_ELEM_TYPE_ENUMERATED) {
for (int i = 0; i < elem->item_count; i++) {
if (strcmp(string_value, elem->item_names[i]) == 0) {
elem->value = i;
break;
}
}
// string value not boolean/enum
} else {
goto fail;
}
}
elem->numid = id;
elem->name = name;
free(iface);
free(string_value);
return 0;
fail:
free(iface);
free(name);
free(string_value);
return -1;
}
static void alsa_config_to_new_card(
snd_config_t *top,
struct alsa_card *card
) {
snd_config_t *config = top;
// go down through the compound nodes state.X (usually USB), control
get_and_check_first_compound(&config, "state");
get_and_check_first_compound(&config, NULL);
get_and_check_first_compound(&config, "control");
// loop through the controls
snd_config_iterator_t i, next;
snd_config_for_each(i, next, config) {
snd_config_t *node = snd_config_iterator_entry(i);
// ignore non-compound controls
if (snd_config_get_type(config) != SND_CONFIG_TYPE_COMPOUND)
continue;
struct alsa_elem elem = {};
elem.card = card;
// create the element
int err = alsa_config_to_new_elem(node, &elem);
if (err)
continue;
if (card->elems->len <= elem.numid)
g_array_set_size(card->elems, elem.numid + 1);
g_array_index(card->elems, struct alsa_elem, elem.numid) = elem;
}
}
// return the basename of fn (no path, no extension)
// e.g. "/home/user/file.ext" -> "file"
static char *sim_card_name(const char *fn) {
// strdup fn and remove path (if any)
char *name = strrchr(fn, '/');
if (name)
name = strdup(name + 1);
else
name = strdup(fn);
// remove extension
char *dot = strrchr(name, '.');
if (dot)
*dot = '\0';
return name;
}
void create_sim_from_file(GtkWindow *w, char *fn) {
snd_config_t *config;
snd_input_t *in;
int err;
err = snd_config_top(&config);
if (err < 0)
fatal_alsa_error("snd_config_top error", err);
err = snd_input_stdio_open(&in, fn, "r");
if (err < 0) {
char *s = g_strdup_printf("Error opening %s: %s", fn, snd_strerror(err));
show_error(w, s);
free(s);
return;
}
err = snd_config_load(config, in);
snd_input_close(in);
if (err < 0)
fatal_alsa_error("snd_config_load error", err);
struct alsa_card *card = card_create(SIMULATED_CARD_NUM);
card->name = sim_card_name(fn);
alsa_config_to_new_card(config, card);
snd_config_delete(config);
create_card_window(card);
}

8
src/alsa-sim.h Normal file
View File

@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
void create_sim_from_file(GtkWindow *w, char *fn);

584
src/alsa.c Normal file
View File

@@ -0,0 +1,584 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <sys/inotify.h>
#include "alsa.h"
#include "stringhelper.h"
#include "window-iface.h"
// names for the port categories
const char *port_category_names[PC_COUNT] = {
"Hardware Outputs",
"Mixer Inputs",
"PCM Inputs"
};
// global array of cards
GArray *alsa_cards;
// static fd and wd for ALSA inotify
static int inotify_fd, inotify_wd;
// forward declaration
static void alsa_elem_change(struct alsa_elem *elem);
void fatal_alsa_error(const char *msg, int err) {
fprintf(stderr, "%s: %s\n", msg, snd_strerror(err));
exit(1);
}
//
// functions to locate elements or get information about them
//
// return the element with the exact matching name
struct alsa_elem *get_elem_by_name(GArray *elems, char *name) {
for (int i = 0; i < elems->len; i++) {
struct alsa_elem *elem = &g_array_index(elems, struct alsa_elem, i);
if (!elem->card)
continue;
if (strcmp(elem->name, name) == 0)
return elem;
}
return NULL;
}
// return the first element with a name starting with the given prefix
struct alsa_elem *get_elem_by_prefix(GArray *elems, char *prefix) {
int prefix_len = strlen(prefix);
for (int i = 0; i < elems->len; i++) {
struct alsa_elem *elem = &g_array_index(elems, struct alsa_elem, i);
if (!elem->card)
continue;
if (strncmp(elem->name, prefix, prefix_len) == 0)
return elem;
}
return NULL;
}
// find the maximum number in the matching elements
// search by element name prefix and substring
// e.g. get_max_elem_by_name(elems, "Line", "Pad Capture Switch")
// will return 8 when the last pad capture switch is
// "Line In 8 Pad Capture Switch"
int get_max_elem_by_name(GArray *elems, char *prefix, char *needle) {
int max = 0;
int l = strlen(prefix);
for (int i = 0; i < elems->len; i++) {
struct alsa_elem *elem = &g_array_index(elems, struct alsa_elem, i);
int num;
if (!elem->card)
continue;
if (strncmp(elem->name, prefix, l) != 0)
continue;
if (!strstr(elem->name, needle))
continue;
num = get_num_from_string(elem->name);
if (num > max)
max = num;
}
return max;
}
// return true if the element is an routing destination enum, e.g.:
// PCM xx Capture Enum
// Mixer Input xx Capture Enum
// Analogue Output xx Playback Enum
// S/PDIF Output xx Playback Enum
// ADAT Output xx Playback Enum
int is_elem_routing_dst(struct alsa_elem *elem) {
if (strstr(elem->name, "Capture Enum") &&
!strstr(elem->name, "Level"))
return 1;
if (strstr(elem->name, "Output") &&
strstr(elem->name, "Playback Enum"))
return 1;
return 0;
}
//
// alsa snd_ctl_elem_*() mediation functions
// for simulated elements, fake the ALSA element
// for real elements, pass through to snd_ctl_elem*()
//
// get the element type
int alsa_get_elem_type(struct alsa_elem *elem) {
snd_ctl_elem_info_t *elem_info;
snd_ctl_elem_info_alloca(&elem_info);
snd_ctl_elem_info_set_numid(elem_info, elem->numid);
snd_ctl_elem_info(elem->card->handle, elem_info);
return snd_ctl_elem_info_get_type(elem_info);
}
// get the element name
char *alsa_get_elem_name(struct alsa_elem *elem) {
snd_ctl_elem_info_t *elem_info;
snd_ctl_elem_info_alloca(&elem_info);
snd_ctl_elem_info_set_numid(elem_info, elem->numid);
snd_ctl_elem_info(elem->card->handle, elem_info);
const char *name = snd_ctl_elem_info_get_name(elem_info);
return strdup(name);
}
// get the element value
// boolean, enum, or int all returned as long ints
long alsa_get_elem_value(struct alsa_elem *elem) {
if (elem->card->num == SIMULATED_CARD_NUM)
return elem->value;
snd_ctl_elem_value_t *elem_value;
snd_ctl_elem_value_alloca(&elem_value);
snd_ctl_elem_value_set_numid(elem_value, elem->numid);
snd_ctl_elem_read(elem->card->handle, elem_value);
int type = elem->type;
if (type == SND_CTL_ELEM_TYPE_BOOLEAN) {
return snd_ctl_elem_value_get_boolean(elem_value, 0);
} else if (type == SND_CTL_ELEM_TYPE_ENUMERATED) {
return snd_ctl_elem_value_get_enumerated(elem_value, 0);
} else if (type == SND_CTL_ELEM_TYPE_INTEGER) {
return snd_ctl_elem_value_get_integer(elem_value, 0);
} else {
fprintf(
stderr,
"internal error: elem %s (%d) type %d not bool/enum/int\n",
elem->name,
elem->numid,
elem->type
);
return 0;
}
}
// for elements with multiple int values, return all the values
// the int array returned needs to be freed by the caller
int *alsa_get_elem_int_values(struct alsa_elem *elem) {
int *values = calloc(elem->count, sizeof(int));
if (elem->card->num == SIMULATED_CARD_NUM) {
for (int i = 0; i < elem->count; i++)
values[i] = 0;
return values;
}
snd_ctl_elem_value_t *elem_value;
snd_ctl_elem_value_alloca(&elem_value);
snd_ctl_elem_value_set_numid(elem_value, elem->numid);
snd_ctl_elem_read(elem->card->handle, elem_value);
for (int i = 0; i < elem->count; i++)
values[i] = snd_ctl_elem_value_get_integer(elem_value, i);
return values;
}
// set the element value
// boolean, enum, or int all set from long ints
void alsa_set_elem_value(struct alsa_elem *elem, long value) {
if (elem->card->num == SIMULATED_CARD_NUM) {
if (elem->value != value) {
elem->value = value;
alsa_elem_change(elem);
}
return;
}
snd_ctl_elem_value_t *elem_value;
snd_ctl_elem_value_alloca(&elem_value);
snd_ctl_elem_value_set_numid(elem_value, elem->numid);
int type = elem->type;
if (type == SND_CTL_ELEM_TYPE_BOOLEAN) {
snd_ctl_elem_value_set_boolean(elem_value, 0, value);
} else if (type == SND_CTL_ELEM_TYPE_ENUMERATED) {
snd_ctl_elem_value_set_enumerated(elem_value, 0, value);
} else if (type == SND_CTL_ELEM_TYPE_INTEGER) {
snd_ctl_elem_value_set_integer(elem_value, 0, value);
} else {
fprintf(
stderr,
"internal error: elem %s (%d) type %d not bool/enum/int\n",
elem->name,
elem->numid,
elem->type
);
return;
}
snd_ctl_elem_write(elem->card->handle, elem_value);
}
// return whether the element can be modified (is writable)
int alsa_get_elem_writable(struct alsa_elem *elem) {
if (elem->card->num == SIMULATED_CARD_NUM)
return elem->writable;
snd_ctl_elem_info_t *elem_info;
snd_ctl_elem_info_alloca(&elem_info);
snd_ctl_elem_info_set_numid(elem_info, elem->numid);
snd_ctl_elem_info(elem->card->handle, elem_info);
return snd_ctl_elem_info_is_writable(elem_info);
}
// get the number of values this element has
// (most are just 1; the levels element is the exception)
int alsa_get_elem_count(struct alsa_elem *elem) {
snd_ctl_elem_info_t *elem_info;
snd_ctl_elem_info_alloca(&elem_info);
snd_ctl_elem_info_set_numid(elem_info, elem->numid);
snd_ctl_elem_info(elem->card->handle, elem_info);
return snd_ctl_elem_info_get_count(elem_info);
}
// get the number of items this enum element has
int alsa_get_item_count(struct alsa_elem *elem) {
if (elem->card->num == SIMULATED_CARD_NUM)
return elem->item_count;
snd_ctl_elem_info_t *elem_info;
snd_ctl_elem_info_alloca(&elem_info);
snd_ctl_elem_info_set_numid(elem_info, elem->numid);
snd_ctl_elem_info(elem->card->handle, elem_info);
return snd_ctl_elem_info_get_items(elem_info);
}
// get the name of an item of the given enum element
char *alsa_get_item_name(struct alsa_elem *elem, int i) {
if (elem->card->num == SIMULATED_CARD_NUM)
return elem->item_names[i];
snd_ctl_elem_info_t *elem_info;
snd_ctl_elem_info_alloca(&elem_info);
snd_ctl_elem_info_set_numid(elem_info, elem->numid);
snd_ctl_elem_info_set_item(elem_info, i);
snd_ctl_elem_info(elem->card->handle, elem_info);
const char *name = snd_ctl_elem_info_get_item_name(elem_info);
return strdup(name);
}
//
// create/destroy alsa cards
//
// scan the ALSA ctl element list container and put the useful
// elements into the cards->elems array of struct alsa_elem
static void alsa_get_elem_list(struct alsa_card *card) {
snd_ctl_elem_list_t *list;
int count;
// get the list from ALSA
snd_ctl_elem_list_malloc(&list);
snd_ctl_elem_list(card->handle, list);
count = snd_ctl_elem_list_get_count(list);
snd_ctl_elem_list_alloc_space(list, count);
snd_ctl_elem_list(card->handle, list);
// for each element in the list
for (int i = 0; i < count; i++) {
// allocate a temporary struct alsa_elem (will be copied later if
// we want to keep it)
struct alsa_elem alsa_elem = {};
// keep a reference to the card in the element
alsa_elem.card = card;
// get the control's numeric identifier (different to the index
// into this array)
alsa_elem.numid = snd_ctl_elem_list_get_numid(list, i);
// get the control's info
alsa_elem.type = alsa_get_elem_type(&alsa_elem);
alsa_elem.name = alsa_get_elem_name(&alsa_elem);
alsa_elem.count = alsa_get_elem_count(&alsa_elem);
switch (alsa_elem.type) {
case SND_CTL_ELEM_TYPE_BOOLEAN:
case SND_CTL_ELEM_TYPE_ENUMERATED:
case SND_CTL_ELEM_TYPE_INTEGER:
break;
default:
continue;
}
if (strstr(alsa_elem.name, "Validity"))
continue;
if (strstr(alsa_elem.name, "Channel Map"))
continue;
if (card->elems->len <= alsa_elem.numid)
g_array_set_size(card->elems, alsa_elem.numid + 1);
g_array_index(card->elems, struct alsa_elem, alsa_elem.numid) = alsa_elem;
}
// free the ALSA list
snd_ctl_elem_list_free_space(list);
snd_ctl_elem_list_free(list);
}
static void alsa_elem_change(struct alsa_elem *elem) {
if (!elem->widget)
return;
if (!elem->widget_callback)
return;
elem->widget_callback(elem);
}
static gboolean alsa_card_callback(
GIOChannel *source,
GIOCondition condition,
void *data
) {
struct alsa_card *card = data;
snd_ctl_event_t *event;
unsigned int mask;
int err, numid;
struct alsa_elem *elem;
snd_ctl_event_alloca(&event);
if (!card->handle) {
printf("oops, no card handle??\n");
return 0;
}
err = snd_ctl_read(card->handle, event);
if (err == 0) {
printf("alsa_card_callback nothing to read??\n");
return 0;
}
if (err < 0) {
if (err == -ENODEV)
return 0;
printf("card_callback_error %d\n", err);
exit(1);
}
if (snd_ctl_event_get_type(event) != SND_CTL_EVENT_ELEM)
return 1;
numid = snd_ctl_event_elem_get_numid(event);
elem = &g_array_index(card->elems, struct alsa_elem, numid);
if (elem->numid != numid)
return 1;
mask = snd_ctl_event_elem_get_mask(event);
if (mask & (SND_CTL_EVENT_MASK_VALUE | SND_CTL_EVENT_MASK_INFO))
alsa_elem_change(elem);
return 1;
}
// go through the alsa_cards array and look for an entry with the
// matching card_num
static struct alsa_card *find_card_by_card_num(int card_num) {
for (int i = 0; i < alsa_cards->len; i++) {
struct alsa_card **card_ptr =
&g_array_index(alsa_cards, struct alsa_card *, i);
if (!*card_ptr)
continue;
if ((*card_ptr)->num == card_num)
return *card_ptr;
}
return NULL;
}
// create a new entry in the alsa_cards array (either an unused entry
// or add a new entry to the end)
struct alsa_card *card_create(int card_num) {
int i, found = 0;
struct alsa_card **card_ptr;
// look for an unused entry
for (i = 0; i < alsa_cards->len; i++) {
card_ptr = &g_array_index(alsa_cards, struct alsa_card *, i);
if (!*card_ptr) {
found = 1;
break;
}
}
// no unused entry? extend the array
if (!found) {
g_array_set_size(alsa_cards, i + 1);
card_ptr = &g_array_index(alsa_cards, struct alsa_card *, i);
}
*card_ptr = calloc(1, sizeof(struct alsa_card));
struct alsa_card *card = *card_ptr;
card->num = card_num;
card->elems = g_array_new(FALSE, TRUE, sizeof(struct alsa_elem));
return card;
}
static void card_destroy_callback(void *data) {
struct alsa_card *card = data;
// close the windows associated with this card
destroy_card_window(card);
// TODO: there is more to free
free(card->device);
free(card->name);
free(card);
// go through the alsa_cards array and clear the entry for this card
for (int i = 0; i < alsa_cards->len; i++) {
struct alsa_card **card_ptr =
&g_array_index(alsa_cards, struct alsa_card *, i);
if (*card_ptr == card)
*card_ptr = NULL;
}
}
static void alsa_add_card_callback(struct alsa_card *card) {
card->io_channel = g_io_channel_unix_new(card->pfd.fd);
card->event_source_id = g_io_add_watch_full(
card->io_channel, 0,
G_IO_IN | G_IO_ERR | G_IO_HUP,
alsa_card_callback, card, card_destroy_callback
);
}
static void alsa_subscribe(struct alsa_card *card) {
int count = snd_ctl_poll_descriptors_count(card->handle);
if (count != 1) {
printf("poll descriptors %d != 1", count);
exit(1);
}
snd_ctl_subscribe_events(card->handle, 1);
snd_ctl_poll_descriptors(card->handle, &card->pfd, 1);
}
void alsa_scan_cards(void) {
snd_ctl_card_info_t *info;
snd_ctl_t *ctl;
int card_num = -1;
char device[32];
struct alsa_card *card;
snd_ctl_card_info_alloca(&info);
while (1) {
int err = snd_card_next(&card_num);
if (err < 0)
fatal_alsa_error("snd_card_next", err);
if (card_num < 0)
break;
snprintf(device, 32, "hw:%d", card_num);
err = snd_ctl_open(&ctl, device, 0);
if (err < 0)
goto next;
err = snd_ctl_card_info(ctl, info);
if (err < 0)
goto next;
if (strncmp(snd_ctl_card_info_get_name(info), "Scarlett", 8) != 0)
goto next;
// is there already an entry for this card in alsa_cards?
card = find_card_by_card_num(card_num);
// yes: skip
if (card)
goto next;
// no: create
card = card_create(card_num);
card->device = strdup(device);
card->name = strdup(snd_ctl_card_info_get_name(info));
card->handle = ctl;
alsa_get_elem_list(card);
alsa_subscribe(card);
create_card_window(card);
alsa_add_card_callback(card);
continue;
next:
snd_ctl_close(ctl);
}
}
// inotify
static gboolean inotify_callback(
GIOChannel *source,
GIOCondition condition,
void *data
) {
char buf[4096] __attribute__ ((aligned(__alignof__(struct inotify_event))));
const struct inotify_event *event;
int len;
len = read(inotify_fd, &buf, sizeof(buf));
if (len < 0) {
perror("inotify read");
exit(1);
}
for (
event = (struct inotify_event *)buf;
(char *)event < buf + len;
event++
) {
if (event->mask & IN_CREATE &&
len &&
strncmp(event->name, "control", 7) == 0) {
// can't rescan for new cards too fast
sleep(1);
alsa_scan_cards();
}
}
return TRUE;
}
void alsa_inotify_init(void) {
GIOChannel *io_channel;
inotify_fd = inotify_init();
inotify_wd = inotify_add_watch(inotify_fd, "/dev/snd", IN_CREATE);
io_channel = g_io_channel_unix_new(inotify_fd);
g_io_add_watch_full(
io_channel, 0,
G_IO_IN | G_IO_ERR | G_IO_HUP,
inotify_callback, NULL, NULL
);
}

210
src/alsa.h Normal file
View File

@@ -0,0 +1,210 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <alsa/asoundlib.h>
#include <gtk/gtk.h>
#include "const.h"
// simulated cards have card->num set to -1
#define SIMULATED_CARD_NUM -1
// forward definitions
struct alsa_elem;
struct alsa_card;
// typedef for callbacks to update widgets when the alsa element
// notifies of a change
typedef void (AlsaElemCallback)(struct alsa_elem *);
// port categories for routing_src and routing_dst entries
// must match the level meter ordering from the driver
enum {
// Hardware inputs/outputs
PC_HW = 0,
// Mixer inputs/outputs
PC_MIX = 1,
// PCM inputs/outputs
PC_PCM = 2,
// number of port categories
PC_COUNT = 3
};
// names for the port categories
extern const char *port_category_names[PC_COUNT];
// is a drag active, and whether dragging from a routing source or a
// routing destination
enum {
DRAG_TYPE_NONE = 0,
DRAG_TYPE_SRC = 1,
DRAG_TYPE_DST = 2,
};
// entry in alsa_card routing_srcs (routing sources) array
// list of enums that are in the Mixer Input X Capture Enum elements
struct routing_src {
// pointer back to the card this entry is associated with
struct alsa_card *card;
// the enum id of the alsa item
int id;
// PC_MIX, PC_PCM, or PC_HW
int port_category;
// 0-based count within port_category
int port_num;
// the alsa item name
char *name;
// the number (or translated letter; A = 1) in the item name
int lr_num;
// on the routing page, the box widget containing the text and the
// "socket" widget for this routing source
GtkWidget *widget;
// the socket widget
GtkWidget *widget2;
};
// entry in alsa_card routing_dsts (routing destinations) array
// for alsa elements that are routing destinations like Analogue
// Output 01 Playback Enum
// port_category is set to PC_MIX, PC_PCM, PC_HW
// port_num is a count (0-based) within that category
struct routing_dst {
// location within the array
int idx;
// pointer back to the element this entry is associated with
struct alsa_elem *elem;
// PC_MIX, PC_PCM, or PC_HW
int port_category;
// 0-based count within port_category
int port_num;
// the mixer label widget for this destination
GtkWidget *mixer_label;
};
// entry in alsa_card elems (ALSA control elements) array
struct alsa_elem {
// pointer back to the card
struct alsa_card *card;
// ALSA element information
int numid;
const char *name;
int type;
int count;
// for the number (or translated letter; A = 1) in the item name
// TODO: move this to struct routing_dst?
int lr_num;
// the primary GTK widget and callback function for this ALSA
// control element
GtkWidget *widget;
AlsaElemCallback *widget_callback;
// text label for volume controls
// handle for routing controls
// second button for dual controls
GtkWidget *widget2;
// for boolean buttons, the two possible texts
// for dual buttons, the four possible texts
const char *bool_text[4];
// for simulated elements, the current state
int writable;
long value;
// for simulated enumerated elements, the items
int item_count;
char **item_names;
};
struct alsa_card {
int num;
char *device;
char *name;
snd_ctl_t *handle;
struct pollfd pfd;
GArray *elems;
struct alsa_elem *sample_capture_elem;
struct alsa_elem *level_meter_elem;
GArray *routing_srcs;
GArray *routing_dsts;
GIOChannel *io_channel;
guint event_source_id;
GtkWidget *window_main;
GtkWidget *window_routing;
GtkWidget *window_mixer;
GtkWidget *window_levels;
GtkWidget *window_startup;
GtkWidget *window_main_contents;
GtkWidget *routing_grid;
GtkWidget *routing_lines;
GtkWidget *routing_hw_in_grid;
GtkWidget *routing_hw_out_grid;
GtkWidget *routing_pcm_in_grid;
GtkWidget *routing_pcm_out_grid;
GtkWidget *routing_mixer_in_grid;
GtkWidget *routing_mixer_out_grid;
GtkWidget *meters[MAX_METERS];
guint meter_gsource_timer;
int has_speaker_switching;
int has_talkback;
int routing_out_count[PC_COUNT];
int routing_in_count[PC_COUNT];
GMenu *routing_src_menu;
GtkWidget *drag_line;
int drag_type;
struct routing_src *src_drag;
struct routing_dst *dst_drag;
double drag_x, drag_y;
};
// global array of cards
extern GArray *alsa_cards;
// utility
void fatal_alsa_error(const char *msg, int err);
// locate elements or get information about them
struct alsa_elem *get_elem_by_name(GArray *elems, char *name);
struct alsa_elem *get_elem_by_prefix(GArray *elems, char *prefix);
int get_max_elem_by_name(GArray *elems, char *prefix, char *needle);
int is_elem_routing_dst(struct alsa_elem *elem);
// alsa snd_ctl_elem_*() functions
int alsa_get_elem_type(struct alsa_elem *elem);
char *alsa_get_elem_name(struct alsa_elem *elem);
long alsa_get_elem_value(struct alsa_elem *elem);
int *alsa_get_elem_int_values(struct alsa_elem *elem);
void alsa_set_elem_value(struct alsa_elem *elem, long value);
int alsa_get_elem_writable(struct alsa_elem *elem);
int alsa_get_elem_count(struct alsa_elem *elem);
int alsa_get_item_count(struct alsa_elem *elem);
char *alsa_get_item_name(struct alsa_elem *elem, int i);
// add to alsa_cards array
struct alsa_card *card_create(int card_num);
// scan/rescan for cards
void alsa_scan_cards(void);
void alsa_inotify_init(void);

13
src/const.h Normal file
View File

@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
// maximum number of mix outputs
#define MAX_MIX_OUT 12
// maximum number of mux inputs
#define MAX_MUX_IN 25
// maximum number of meters
#define MAX_METERS 65

22
src/error.c Normal file
View File

@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "error.h"
void show_error(GtkWindow *w, char *s) {
if (!w) {
printf("%s\n", s);
return;
}
GtkWidget *dialog = gtk_message_dialog_new(
w,
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
GTK_MESSAGE_ERROR,
GTK_BUTTONS_CLOSE,
s
);
gtk_widget_show(dialog);
g_signal_connect(dialog, "response", G_CALLBACK(gtk_window_destroy), NULL);
}

8
src/error.h Normal file
View File

@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
void show_error(GtkWindow *w, char *s);

203
src/file.c Normal file
View File

@@ -0,0 +1,203 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "alsa.h"
#include "alsa-sim.h"
#include "error.h"
#include "file.h"
#include "stringhelper.h"
static void run_alsactl(
struct alsa_card *card,
char *cmd,
char *fn
) {
GtkWindow *w = GTK_WINDOW(card->window_main);
gchar *argv[] = {
"alsactl", cmd, card->device, "-f", fn, NULL
};
gchar *stdout;
gchar *stderr;
gint exit_status;
GError *error = NULL;
gboolean result = g_spawn_sync(
NULL,
argv,
NULL,
G_SPAWN_SEARCH_PATH,
NULL,
NULL,
&stdout,
&stderr,
&exit_status,
&error
);
if (result && WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0)
goto done;
char *error_message =
result
? g_strdup_printf("%s\n%s", stdout, stderr)
: g_strdup_printf("%s", error->message);
char *msg = g_strdup_printf(
"Error running “alsactl %s %s -f %s”: %s",
cmd, card->device, fn, error_message
);
show_error(w, msg);
g_free(msg);
g_free(error_message);
done:
g_free(stdout);
g_free(stderr);
if (error)
g_error_free(error);
}
static void add_state_filter(GtkFileChooserNative *native) {
GtkFileFilter *filter = gtk_file_filter_new();
gtk_file_filter_set_name(filter, "alsactl state file (.state)");
gtk_file_filter_add_pattern(filter, "*.state");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(native), filter);
}
static void load_response(
GtkNativeDialog *native,
int response,
gpointer data
) {
struct alsa_card *card = data;
if (response != GTK_RESPONSE_ACCEPT)
goto done;
GFile *file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(native));
char *fn = g_file_get_path(file);
run_alsactl(card, "restore", fn);
g_free(fn);
g_object_unref(file);
done:
g_object_unref(native);
}
void activate_load(
GSimpleAction *action,
GVariant *parameter,
gpointer data
) {
struct alsa_card *card = data;
GtkFileChooserNative *native = gtk_file_chooser_native_new(
"Load Configuration",
GTK_WINDOW(card->window_main),
GTK_FILE_CHOOSER_ACTION_OPEN,
"_Load",
"_Cancel"
);
add_state_filter(native);
g_signal_connect(native, "response", G_CALLBACK(load_response), card);
gtk_native_dialog_show(GTK_NATIVE_DIALOG(native));
}
static void save_response(
GtkNativeDialog *native,
int response,
gpointer data
) {
struct alsa_card *card = data;
if (response != GTK_RESPONSE_ACCEPT)
goto done;
GFile *file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(native));
char *fn = g_file_get_path(file);
// append .state if not present
char *fn_with_ext;
if (string_ends_with(fn, ".state"))
fn_with_ext = g_strdup_printf("%s", fn);
else
fn_with_ext = g_strdup_printf("%s.state", fn);
run_alsactl(card, "store", fn_with_ext);
g_free(fn);
g_free(fn_with_ext);
g_object_unref(file);
done:
g_object_unref(native);
}
void activate_save(
GSimpleAction *action,
GVariant *parameter,
gpointer data
) {
struct alsa_card *card = data;
GtkFileChooserNative *native = gtk_file_chooser_native_new(
"Save Configuration",
GTK_WINDOW(card->window_main),
GTK_FILE_CHOOSER_ACTION_SAVE,
"_Save",
"_Cancel"
);
add_state_filter(native);
g_signal_connect(native, "response", G_CALLBACK(save_response), card);
gtk_native_dialog_show(GTK_NATIVE_DIALOG(native));
}
static void sim_response(
GtkNativeDialog *native,
int response,
gpointer data
) {
GtkWindow *w = data;
if (response != GTK_RESPONSE_ACCEPT)
goto done;
GFile *file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(native));
char *fn = g_file_get_path(file);
create_sim_from_file(w, fn);
g_free(fn);
g_object_unref(file);
done:
g_object_unref(native);
}
void activate_sim(
GSimpleAction *action,
GVariant *parameter,
gpointer data
) {
GtkWidget *w = data;
GtkFileChooserNative *native = gtk_file_chooser_native_new(
"Load Configuration File for Interface Simulation",
GTK_WINDOW(w),
GTK_FILE_CHOOSER_ACTION_OPEN,
"_Load",
"_Cancel"
);
add_state_filter(native);
g_signal_connect(native, "response", G_CALLBACK(sim_response), w);
gtk_native_dialog_show(GTK_NATIVE_DIALOG(native));
}

8
src/file.h Normal file
View File

@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <gtk/gtk.h>
void activate_load(GSimpleAction *action, GVariant *parameter, gpointer data);
void activate_save(GSimpleAction *action, GVariant *parameter, gpointer data);
void activate_sim(GSimpleAction *action, GVariant *parameter, gpointer data);

840
src/gtkdial.c Normal file
View File

@@ -0,0 +1,840 @@
// SPDX-FileCopyrightText: 2021 Stiliyan Varbanov <https://www.fiverr.com/stilvar>
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: LGPL-3.0-or-later
/*
* A Dial widget for GTK-4 similar to GtkScale.
* 2021 Stiliyan Varbanov www.fiverr.com/stilvar
*/
#include <stdlib.h>
#include <glib-2.0/glib.h>
#include <glib-object.h>
#include <cairo/cairo.h>
#include <stdio.h>
#include <graphene-1.0/graphene.h>
#include <math.h>
#include "gtkdial.h"
static void set_value(GtkDial *dial, double newval);
static void gtk_dial_set_property(GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec);
static void gtk_dial_get_property(GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec);
static void gtk_dial_move_slider(GtkDial *dial,
GtkScrollType scroll);
static void
gtk_dial_drag_gesture_begin (GtkGestureDrag *gesture,
double offset_x,
double offset_y,
GtkDial *dial);
static void
gtk_dial_drag_gesture_update (GtkGestureDrag *gesture,
double offset_x,
double offset_y,
GtkDial *dial);
static void
gtk_dial_drag_gesture_end (GtkGestureDrag *gesture,
double offset_x,
double offset_y,
GtkDial *dial);
static void
gtk_dial_click_gesture_pressed (GtkGestureClick *gesture,
int n_press,
double x,
double y,
GtkDial *dial);
static gboolean
gtk_dial_scroll_controller_scroll (GtkEventControllerScroll *scroll,
double dx,
double dy,
GtkDial *dial);
static void gtk_dial_dispose(GObject *o);
typedef enum {
GRAB_NONE,
GRAB_SLIDER
} e_grab;
enum {
PROP_0,
PROP_ADJUSTMENT,
PROP_ROUND_DIGITS,
PROP_ZERO_DB,
LAST_PROP
};
enum {
SIGNAL_0,
VALUE_CHANGED,
MOVE_SLIDER,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL];
static GParamSpec *properties[LAST_PROP];
typedef unsigned char guint8;
typedef size_t gsize;
struct DialColors {
GdkRGBA trough_border,
trough_bg,
trough_fill,
pointer;
};
struct _GtkDial {
GtkWidget parent_instance;
GtkAdjustment *adj;
GtkGesture *drag_gesture, *click_gesture;
GtkEventController *scroll_controller;
e_grab grab;
struct DialColors colors;
int round_digits;
double zero_db;
double slider_cx, slider_cy, dvalp;
};
G_DEFINE_TYPE (GtkDial, gtk_dial, GTK_TYPE_WIDGET)
static void dial_snapshot (GtkWidget *widget, GtkSnapshot *snapshot);
static void dial_measure(GtkWidget *widget,
GtkOrientation orientation,
int for_size,
int *minimum,
int *natural,
int *minimum_baseline,
int *natural_baseline);
#define add_slider_binding(w_class, binding_set, keyval, mask, scroll) \
gtk_widget_class_add_binding_signal (w_class, \
keyval, mask, \
"move-slider", \
"(i)", scroll)
//BEGIN SECTION HELPERS
#define V1x 0.7316888688738209
#define V1y 0.6816387600233341
#define RAD_START (-M_PI-0.75)
#define RAD_END 0.75
#define RAD_SE_DIFF2 ( (2*M_PI+3)/2 )
#define DRAG_FACTOR 0.5
static inline double calc_valp(double val, double mn, double mx)
{
return (val - mn)/(mx-mn);
}
static inline double calc_val(double valp, double mn, double mx)
{
return (mx-mn)*valp+mn;
}
struct dial_properties
{
double w;
double h;
double radius;
double thickness;
double cx;
double cy;
double valp;
double slider_radius;
double slider_cx;
double slider_cy;
double start_x;
double start_y;
double end_x;
double end_y;
};
static void get_dial_properties(GtkDial *dial,
struct dial_properties *props)
{
props->w = gtk_widget_get_width (GTK_WIDGET(dial) );
props->h = gtk_widget_get_height (GTK_WIDGET(dial) );
props->cx = props->w / 2;
props->cy = props->h / 2;
props->radius = props->h < props->w ? props->h / 2 - 2 : props->w / 2 - 2;
props->thickness = 10;
props->slider_radius = props->thickness * 1.5;
props->radius -= props->slider_radius / 2;
double mn = dial->adj ? gtk_adjustment_get_lower(dial->adj) : 0;
double mx = dial->adj ? gtk_adjustment_get_upper(dial->adj) : 1;
double value = dial->adj ? gtk_adjustment_get_value(dial->adj) : 0.25;
props->valp = calc_valp(value, mn, mx);
double SIN = sin( (RAD_SE_DIFF2*(props->valp) ) );
double COS = cos( (RAD_SE_DIFF2*(props->valp) ) );
props->slider_cx = (-V1y*SIN - V1x*COS)*(2*(props->radius)-(props->thickness) )/2 + (props->cx);
props->slider_cy = (V1y*COS - V1x*SIN)*(2*(props->radius)-(props->thickness) )/2 + (props->cy);
props->start_x = V1x*(2*(props->radius)-(props->thickness) )/2 + (props->cx);
props->start_y = V1y*(2*(props->radius)-(props->thickness) )/2 + (props->cy);
SIN = -0.9974949866040545;
COS = -0.07073720166770303;
props->end_x = (-V1y*SIN - V1x*COS)*(2*(props->radius)-(props->thickness) )/2 + (props->cx);
props->end_y = (V1y*COS - V1x*SIN)*(2*(props->radius)-(props->thickness) )/2 + (props->cy);
}
static inline double pdist2(double x1, double y1, double x2, double y2)
{
double dx = x2 - x1;
double dy = y2 - y1;
return dx*dx + dy*dy;
}
static inline gboolean circle_contains_point(double cx, double cy, double r, double px, double py)
{
return pdist2(cx,cy, px,py) <= r*r;
}
//END SECTION HELPERS
static void gtk_dial_class_init(GtkDialClass *klass)
{
GtkWidgetClass *w_class = GTK_WIDGET_CLASS(klass);
GObjectClass *g_class = G_OBJECT_CLASS(klass);
GtkWidgetClass *p_class = GTK_WIDGET_CLASS(gtk_dial_parent_class);
g_class->set_property = &gtk_dial_set_property;
g_class->get_property = &gtk_dial_get_property;
g_class->dispose = &gtk_dial_dispose;
w_class->size_allocate = p_class->size_allocate;
w_class->measure = &dial_measure;
w_class->snapshot = &dial_snapshot;
w_class->grab_focus = p_class->grab_focus;
w_class->focus = p_class->focus;
klass->move_slider = &gtk_dial_move_slider;
klass->value_changed = NULL;
/**
* GtkDial:adjustment: (attributes org.gtk.Method.get=gtk_dial_get_adjustment org.gtk.Method.set=gtk_dial_set_adjustment)
*
* The GtkAdjustment that contains the current value of this range object.
*/
properties[PROP_ADJUSTMENT] =
g_param_spec_object ("adjustment",
"Adjustment",
"The GtkAdjustment that contains the current value of this range object",
GTK_TYPE_ADJUSTMENT,
G_PARAM_READWRITE|G_PARAM_CONSTRUCT);
/**
* GtkDial:round_digits: (attributes org.gtk.Method.get=gtk_dial_get_round_digits org.gtk.Method.set=gtk_dial_set_round_digits)
*
* Limits the number of decimal points this GtkDial will store (default 0: integers).
*/
properties[PROP_ROUND_DIGITS] =
g_param_spec_int("round_digits",
"RoundDigits",
"Limits the number of decimal points this GtkDial will store",
-1, 1000,
-1,
G_PARAM_READWRITE|G_PARAM_CONSTRUCT);
/**
* GtkDial:zero_db: (attributes org.gtk.Method.get=gtk_dial_get_zero_db org.gtk.Method.set=gtk_dial_set_zero_db)
*
* Limits the number of decimal points this GtkDial will store (default 0: integers).
*/
properties[PROP_ZERO_DB] =
g_param_spec_double("zero_db",
"ZerodB",
"The zero-dB value of the dial",
-G_MAXDOUBLE, G_MAXDOUBLE,
0.0,
G_PARAM_READWRITE|G_PARAM_CONSTRUCT);
g_object_class_install_properties(g_class, LAST_PROP, properties);
/**
* GtkRange::value-changed:
* @range: the `GtkRange` that received the signal
*
* Emitted when the range value changes.
*/
signals[VALUE_CHANGED] =
g_signal_new ("value-changed",
G_TYPE_FROM_CLASS (g_class),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GtkDialClass, value_changed),
NULL, NULL,
NULL,
G_TYPE_NONE, 0);
/**
* GtkDial::move-slider:
* @Dial: the `GtkDial` that received the signal
* @step: how to move the slider
*
* Virtual function that moves the slider.
*
* Used for keybindings.
*/
signals[MOVE_SLIDER] =
g_signal_new ("move-slider",
G_TYPE_FROM_CLASS (g_class),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (GtkDialClass, move_slider),
NULL, NULL,
NULL,
G_TYPE_NONE, 1,
GTK_TYPE_SCROLL_TYPE);
add_slider_binding (w_class, binding_set, GDK_KEY_Left, 0,
GTK_SCROLL_STEP_LEFT);
add_slider_binding (w_class, binding_set, GDK_KEY_Down, 0,
GTK_SCROLL_STEP_LEFT);
add_slider_binding (w_class, binding_set, GDK_KEY_Right, 0,
GTK_SCROLL_STEP_RIGHT);
add_slider_binding (w_class, binding_set, GDK_KEY_Up, 0,
GTK_SCROLL_STEP_RIGHT);
}
static void gtk_dial_init(GtkDial *dial)
{
// gtk_dial_set_style(dial, "#cdc7c2", "white", "#3584e4");
gtk_dial_set_style(dial, "#cdc7c2", "#f0f0f0", "#3584e4", "#808080");
//gdk_rgba_parse(&dial->colors.trough_border, "#cdc7c2");
gtk_widget_set_focusable (GTK_WIDGET (dial), TRUE);
//gtk_widget_set_parent(dial->slider_container, GTK_WIDGET(dial) );
dial->adj = NULL;
dial->slider_cx = gtk_widget_get_width(GTK_WIDGET(dial) ) / 2.0;
dial->slider_cy = 0;
dial->grab = GRAB_NONE;
dial->drag_gesture = gtk_gesture_drag_new();
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (dial->drag_gesture), 0);
g_signal_connect (dial->drag_gesture, "drag-begin",
G_CALLBACK (gtk_dial_drag_gesture_begin), dial);
g_signal_connect (dial->drag_gesture, "drag-update",
G_CALLBACK (gtk_dial_drag_gesture_update), dial);
g_signal_connect (dial->drag_gesture, "drag-end",
G_CALLBACK (gtk_dial_drag_gesture_end), dial);
gtk_widget_add_controller (GTK_WIDGET (dial), GTK_EVENT_CONTROLLER (dial->drag_gesture) );
dial->click_gesture = gtk_gesture_click_new();
//gtk_gesture_long_press_set_delay_factor (GTK_GESTURE_LONG_PRESS (dial->click_gesture), 0.1);
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (dial->click_gesture), 0);
g_signal_connect (dial->click_gesture, "pressed",
G_CALLBACK (gtk_dial_click_gesture_pressed), dial);
gtk_widget_add_controller (GTK_WIDGET (dial), GTK_EVENT_CONTROLLER (dial->click_gesture) );
gtk_gesture_group (dial->click_gesture, dial->drag_gesture);
dial->scroll_controller = gtk_event_controller_scroll_new (GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES);
g_signal_connect (dial->scroll_controller, "scroll",
G_CALLBACK (gtk_dial_scroll_controller_scroll), dial);
gtk_widget_add_controller (GTK_WIDGET (dial), dial->scroll_controller);
}
static void dial_measure(GtkWidget *widget,
GtkOrientation orientation,
int for_size,
int *minimum,
int *natural,
int *minimum_baseline,
int *natural_baseline)
{
*minimum = 50;
*natural = 50;
*minimum_baseline = for_size;
*natural_baseline = for_size;
}
static void dial_snapshot(GtkWidget *widget, GtkSnapshot *snapshot)
{
GtkDial *dial = GTK_DIAL(widget);
struct dial_properties p;
get_dial_properties(dial, &p);
p.valp = CLAMP(p.valp, 0.0001, 1.0);
cairo_t *cr = gtk_snapshot_append_cairo(snapshot, &GRAPHENE_RECT_INIT(0, 0, p.w, p.h) );
// draw border
cairo_set_line_width(cr, 2);
gdk_cairo_set_source_rgba(cr, &dial->colors.trough_border);
cairo_arc(cr, p.cx, p.cy, p.radius-p.thickness, RAD_START, RAD_END/*8*M_PI/5*/);
cairo_line_to(cr, V1x*(p.radius-p.thickness) + p.cx, V1y*(p.radius-p.thickness) + p.cy);
cairo_arc_negative(cr, p.cx, p.cy, p.radius, RAD_END, RAD_START/*8*M_PI/5*/);
cairo_close_path(cr);
cairo_stroke(cr);
// bg trough
cairo_arc(cr, p.cx, p.cy, (2*p.radius-p.thickness)/2.0, RAD_START, RAD_END/*8*M_PI/5*/);
cairo_set_line_width(cr, p.thickness);
gdk_cairo_set_source_rgba(cr, &dial->colors.trough_bg);
cairo_stroke(cr);
// fill trough
cairo_arc(cr, p.cx, p.cy, (2*p.radius-p.thickness)/2.0, RAD_START, RAD_END - (1.0-p.valp)*(RAD_END-RAD_START)/*8*M_PI/5*/);
cairo_set_line_width(cr, p.thickness);
gdk_cairo_set_source_rgba(cr, &dial->colors.trough_fill);
cairo_stroke(cr);
// pointer
gdk_cairo_set_source_rgba(cr, &dial->colors.pointer);
cairo_set_line_width(cr, 2);
cairo_move_to(cr, p.cx, p.cy);
cairo_line_to(cr, p.slider_cx, p.slider_cy);
cairo_stroke(cr);
cairo_destroy(cr);
// //FILL SLIDER
// cairo_arc(cr, p.slider_cx, p.slider_cy, p.slider_radius, 0, 2*M_PI);
// gdk_cairo_set_source_rgba(cr, &dial->colors.trough_bg);
// cairo_fill(cr);
//
// //STROKE SLIDER
// GdkRGBA *scolor = dial->grab != GRAB_SLIDER ? &dial->colors.trough_border : &dial->colors.trough_fill;
// cairo_arc(cr, p.slider_cx, p.slider_cy, p.slider_radius, 0, 2*M_PI);
// cairo_set_line_width(cr, 1);
// gdk_cairo_set_source_rgba(cr, scolor);
// cairo_stroke(cr);
}
GtkWidget* gtk_dial_new(GtkAdjustment *adjustment)
{
g_return_val_if_fail (adjustment == NULL || GTK_IS_ADJUSTMENT (adjustment),
NULL);
return g_object_new (GTK_TYPE_DIAL,
"adjustment", adjustment,
NULL);
}
GtkWidget *
gtk_dial_new_with_range ( double min,
double max,
double step)
{
GtkAdjustment *adj;
int digits;
g_return_val_if_fail (min < max, NULL);
g_return_val_if_fail (step != 0.0, NULL);
adj = gtk_adjustment_new(min, min, max, step, 10 * step, 0);
if (fabs(step) >= 1.0 || step == 0.0)
{
digits = 0;
}
else
{
digits = abs ( (int) floor(log10(fabs(step) ) ) );
if (digits > 5)
digits = 5;
}
return g_object_new (GTK_TYPE_DIAL,
"adjustment", adj,
"round_digits", 0,
NULL);
}
static void gtk_dial_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GtkDial *dial = GTK_DIAL(object);
switch(prop_id)
{
case PROP_ADJUSTMENT:
gtk_dial_set_adjustment(dial, g_value_get_object(value) );
break;
case PROP_ROUND_DIGITS:
gtk_dial_set_round_digits(dial, g_value_get_int(value) );
break;
case PROP_ZERO_DB:
gtk_dial_set_zero_db(dial, g_value_get_double(value) );
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void gtk_dial_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
GtkDial *dial = GTK_DIAL(object);
switch(prop_id)
{
case PROP_ADJUSTMENT:
g_value_set_object(value, dial->adj);
break;
case PROP_ROUND_DIGITS:
g_value_set_int(value, dial->round_digits);
break;
case PROP_ZERO_DB:
g_value_set_double(value, dial->zero_db);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
double gtk_dial_get_value (GtkDial *dial)
{
return gtk_adjustment_get_value(dial->adj);
}
void gtk_dial_set_value (GtkDial *dial,
double value)
{
set_value(dial, value);
gtk_widget_queue_draw(GTK_WIDGET(dial) );
}
void gtk_dial_set_round_digits (GtkDial *dial,
int round_digits)
{
dial->round_digits = round_digits;
gtk_dial_set_value(dial, gtk_dial_get_value(dial) );
}
int gtk_dial_get_round_digits (GtkDial *dial)
{
return dial->round_digits;
}
void gtk_dial_set_zero_db(GtkDial *dial, double zero_db)
{
dial->zero_db = zero_db;
}
double gtk_dial_get_zero_db(GtkDial *dial)
{
return dial->zero_db;
}
gboolean gtk_dial_set_style(GtkDial *dial,
const char *trough_border,
const char *trough_bg,
const char *trough_fill,
const char *pointer)
{
gboolean out = TRUE;
if (trough_border)
out = out && gdk_rgba_parse(&dial->colors.trough_border, trough_border);
if (trough_bg)
out = out && gdk_rgba_parse(&dial->colors.trough_bg, trough_bg);
if (trough_fill)
out = out && gdk_rgba_parse(&dial->colors.trough_fill, trough_fill);
if (pointer)
out = out && gdk_rgba_parse(&dial->colors.pointer, pointer);
return out;
}
void gtk_dial_set_adjustment (GtkDial *dial,
GtkAdjustment *adj)
{
if (!(adj == NULL || GTK_IS_ADJUSTMENT (adj) ) )
return;
if (dial->adj)
g_object_unref(dial->adj);
dial->adj = adj;
g_object_ref_sink(dial->adj);
g_signal_emit(dial, signals[VALUE_CHANGED], 0);
gtk_widget_queue_draw(GTK_WIDGET(dial) );
}
GtkAdjustment* gtk_dial_get_adjustment (GtkDial *dial)
{
return dial->adj;
}
static void
set_value (GtkDial *dial, double newval)
{
if (dial->round_digits >= 0)
{
double power;
int i;
i = dial->round_digits;
power = 1;
while (i--)
power *= 10;
newval = floor( (newval * power) + 0.5) / power;
}
gtk_adjustment_set_value(dial->adj, newval);
g_signal_emit(dial, signals[VALUE_CHANGED], 0);
}
static void
step_back (GtkDial *dial)
{
double newval;
newval = gtk_adjustment_get_value (dial->adj) - gtk_adjustment_get_step_increment (dial->adj);
set_value(dial, newval);
}
static void
step_forward (GtkDial *dial)
{
double newval;
newval = gtk_adjustment_get_value (dial->adj) + gtk_adjustment_get_step_increment (dial->adj);
set_value(dial, newval);
}
static void
page_back (GtkDial *dial)
{
double newval;
newval = gtk_adjustment_get_value (dial->adj) - gtk_adjustment_get_page_increment (dial->adj);
set_value(dial, newval);
}
static void
page_forward (GtkDial *dial)
{
double newval;
newval = gtk_adjustment_get_value (dial->adj) + gtk_adjustment_get_page_increment (dial->adj);
set_value(dial, newval);
}
static void
scroll_begin (GtkDial *dial)
{
}
static void scroll_end(GtkDial *dial)
{
double newval = gtk_adjustment_get_upper (dial->adj) - gtk_adjustment_get_page_size (dial->adj);
set_value(dial, newval);
}
static gboolean should_invert_move(GtkDial *dial, GtkOrientation o)
{
return FALSE;
}
static void gtk_dial_move_slider(GtkDial *dial,
GtkScrollType scroll)
{
switch (scroll) {
case GTK_SCROLL_STEP_LEFT:
if (should_invert_move (dial, GTK_ORIENTATION_HORIZONTAL))
step_forward (dial);
else
step_back (dial);
break;
case GTK_SCROLL_STEP_UP:
if (should_invert_move (dial, GTK_ORIENTATION_VERTICAL))
step_forward (dial);
else
step_back (dial);
break;
case GTK_SCROLL_STEP_RIGHT:
if (should_invert_move (dial, GTK_ORIENTATION_HORIZONTAL))
step_back (dial);
else
step_forward (dial);
break;
case GTK_SCROLL_STEP_DOWN:
if (should_invert_move (dial, GTK_ORIENTATION_VERTICAL))
step_back (dial);
else
step_forward (dial);
break;
case GTK_SCROLL_STEP_BACKWARD:
step_back (dial);
break;
case GTK_SCROLL_STEP_FORWARD:
step_forward (dial);
break;
case GTK_SCROLL_PAGE_LEFT:
if (should_invert_move (dial, GTK_ORIENTATION_HORIZONTAL))
page_forward (dial);
else
page_back (dial);
break;
case GTK_SCROLL_PAGE_UP:
if (should_invert_move (dial, GTK_ORIENTATION_VERTICAL))
page_forward (dial);
else
page_back (dial);
break;
case GTK_SCROLL_PAGE_RIGHT:
if (should_invert_move (dial, GTK_ORIENTATION_HORIZONTAL))
page_back (dial);
else
page_forward (dial);
break;
case GTK_SCROLL_PAGE_DOWN:
if (should_invert_move (dial, GTK_ORIENTATION_VERTICAL))
page_back (dial);
else
page_forward (dial);
break;
case GTK_SCROLL_PAGE_BACKWARD:
page_back (dial);
break;
case GTK_SCROLL_PAGE_FORWARD:
page_forward (dial);
break;
case GTK_SCROLL_START:
scroll_begin (dial);
break;
case GTK_SCROLL_END:
scroll_end (dial);
break;
case GTK_SCROLL_JUMP:
case GTK_SCROLL_NONE:
default:
break;
}
gtk_widget_queue_draw(GTK_WIDGET(dial) );
}
static void
gtk_dial_drag_gesture_begin (GtkGestureDrag *gesture,
double offset_x,
double offset_y,
GtkDial *dial)
{
dial->dvalp = calc_valp(gtk_dial_get_value(dial), gtk_adjustment_get_lower(dial->adj), gtk_adjustment_get_upper(dial->adj) );
gtk_gesture_set_state(dial->drag_gesture, GTK_EVENT_SEQUENCE_CLAIMED);
}
static void
gtk_dial_drag_gesture_update (GtkGestureDrag *gesture,
double offset_x,
double offset_y,
GtkDial *dial)
{
double start_x, start_y;
gtk_gesture_drag_get_start_point (gesture, &start_x, &start_y);
struct dial_properties p;
get_dial_properties(dial, &p);
double valp = dial->dvalp - DRAG_FACTOR*(offset_y/p.h);
valp = CLAMP(valp, 0.0, 1.0);
double val = calc_val(valp, gtk_adjustment_get_lower(dial->adj), gtk_adjustment_get_upper(dial->adj) );
set_value(dial, val);
gtk_widget_queue_draw(GTK_WIDGET(dial) );
}
static void
gtk_dial_drag_gesture_end (GtkGestureDrag *gesture,
double offset_x,
double offset_y,
GtkDial *dial)
{
dial->grab = GRAB_NONE;
gtk_widget_queue_draw(GTK_WIDGET(dial) );
}
static void
gtk_dial_click_gesture_pressed (GtkGestureClick *gesture,
int n_press,
double x,
double y,
GtkDial *dial)
{
// on double (or more) click, toggle between lower and zero_db value
if (n_press >= 2) {
double lower = gtk_adjustment_get_lower(dial->adj);
if (gtk_dial_get_value(dial) != lower)
set_value(dial, lower);
else
set_value(dial, dial->zero_db);
return;
}
struct dial_properties p;
get_dial_properties(dial, &p);
if (circle_contains_point(p.slider_cx, p.slider_cy, p.slider_radius, x, y) )
dial->grab = GRAB_SLIDER;
else
dial->grab = GRAB_NONE;
gtk_widget_queue_draw(GTK_WIDGET(dial) );
gtk_gesture_set_state(GTK_GESTURE(gesture), GTK_EVENT_SEQUENCE_CLAIMED);
}
static gboolean
gtk_dial_scroll_controller_scroll (GtkEventControllerScroll *scroll,
double dx,
double dy,
GtkDial *dial)
{
double delta = dx ? dx : dy;
if (abs(delta) > 1)
delta *= abs(delta);
double step = -gtk_adjustment_get_step_increment(dial->adj)*delta;
set_value(dial, gtk_adjustment_get_value(dial->adj) + step);
gtk_widget_queue_draw(GTK_WIDGET(dial) );
return GDK_EVENT_STOP;
}
void gtk_dial_dispose(GObject *o)
{
GtkDial *dial = GTK_DIAL(o);
g_object_unref(dial->adj);
dial->adj = NULL;
G_OBJECT_CLASS (gtk_dial_parent_class)->dispose(o);
}

105
src/gtkdial.h Normal file
View File

@@ -0,0 +1,105 @@
// SPDX-FileCopyrightText: 2021 Stiliyan Varbanov <https://www.fiverr.com/stilvar>
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: LGPL-3.0-or-later
/*
* A Dial widget for GTK-4 similar to GtkScale.
* 2021 Stiliyan Varbanov www.fiverr.com/stilvar
*/
#ifndef __GTK_DIAL_H__
#define __GTK_DIAL_H__
#include <gtk/gtk.h>
G_BEGIN_DECLS
#define GTK_TYPE_DIAL (gtk_dial_get_type ())
#define GTK_DIAL(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_DIAL, GtkDial))
#define GTK_DIAL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_DIAL, GtkDialClass))
#define GTK_IS_DIAL(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_DIAL))
#define GTK_IS_DIAL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_DIAL))
#define GTK_DIAL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_DIAL, GtkDialClass))
typedef struct _GtkDial GtkDial;
typedef struct _GtkDialClass GtkDialClass;
struct _GtkDialClass
{
GtkWidgetClass parent_class;
void (* value_changed) (GtkDial *dial);
/* action signals for keybindings */
void (* move_slider) (GtkDial *dial,
GtkScrollType scroll);
gboolean (*change_value) (GtkDial *dial,
GtkScrollType scroll,
double new_value);
};
typedef char * (*GtkDialFormatValueFunc) (GtkDial *dial,
double value,
gpointer user_data);
GDK_AVAILABLE_IN_ALL
GType gtk_dial_get_type (void) G_GNUC_CONST;
GDK_AVAILABLE_IN_ALL
GtkWidget * gtk_dial_new (GtkAdjustment *adjustment);
GDK_AVAILABLE_IN_ALL
GtkWidget * gtk_dial_new_with_range (double min,
double max,
double step);
GDK_AVAILABLE_IN_ALL
void gtk_dial_set_has_origin (GtkDial *dial,
gboolean has_origin);
GDK_AVAILABLE_IN_ALL
gboolean gtk_dial_get_has_origin (GtkDial *dial);
GDK_AVAILABLE_IN_ALL
void gtk_dial_set_adjustment (GtkDial *dial,
GtkAdjustment *adj);
GDK_AVAILABLE_IN_ALL
GtkAdjustment* gtk_dial_get_adjustment (GtkDial *dial);
GDK_AVAILABLE_IN_ALL
double gtk_dial_get_value (GtkDial *dial);
GDK_AVAILABLE_IN_ALL
void gtk_dial_set_value (GtkDial *dial,
double value);
GDK_AVAILABLE_IN_ALL
void gtk_dial_set_round_digits (GtkDial *dial,
int round_digits);
GDK_AVAILABLE_IN_ALL
int gtk_dial_get_round_digits (GtkDial *range);
GDK_AVAILABLE_IN_ALL
void gtk_dial_set_zero_db (GtkDial *dial,
double zero_db);
GDK_AVAILABLE_IN_ALL
double gtk_dial_get_zero_db (GtkDial *range);
/**
* @brief Set the colors which this dial uses. String codes can be one of the following:
* A standard name (Taken from the X11 rgb.txt file)
* A hexadecimal value in the form “#rgb”, “#rrggbb”, “#rrrgggbbb” or ”#rrrrggggbbbb”
* A RGB color in the form “rgb(r,g,b)” (In this case the color will have full opacity)
* A RGBA color in the form “rgba(r,g,b,a)”
* NULL if the color is to remain unchanged
*
* @param dial: The dial
* @param trough_border: String code for trough border color
* @param trough_bg: String code for trough background color
* @param trough_fill: String code for trough fill color
* @return TRUE if all the colors were set successfully, FALSE otherwise
*/
gboolean gtk_dial_set_style(GtkDial *dial,
const char *trough_border,
const char *trough_bg,
const char *trough_fill,
const char *pointer);
G_END_DECLS
#endif

31
src/gtkhelper.c Normal file
View File

@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "gtkhelper.h"
void gtk_widget_set_margin(GtkWidget *w, int margin) {
gtk_widget_set_margin_top(w, margin);
gtk_widget_set_margin_bottom(w, margin);
gtk_widget_set_margin_start(w, margin);
gtk_widget_set_margin_end(w, margin);
}
void gtk_widget_set_expand(GtkWidget *w, gboolean expand) {
gtk_widget_set_hexpand(w, expand);
gtk_widget_set_vexpand(w, expand);
}
void gtk_widget_set_align(GtkWidget *w, GtkAlign x, GtkAlign y) {
gtk_widget_set_halign(w, x);
gtk_widget_set_valign(w, y);
}
void gtk_grid_set_spacing(GtkGrid *grid, int spacing) {
gtk_grid_set_row_spacing(grid, spacing);
gtk_grid_set_column_spacing(grid, spacing);
}
void gtk_widget_add_class(GtkWidget *w, const char *class) {
GtkStyleContext *style_context = gtk_widget_get_style_context(w);
gtk_style_context_add_class(style_context, class);
}

12
src/gtkhelper.h Normal file
View File

@@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
void gtk_widget_set_margin(GtkWidget *w, int margin);
void gtk_widget_set_expand(GtkWidget *w, gboolean expand);
void gtk_widget_set_align(GtkWidget *w, GtkAlign x, GtkAlign y);
void gtk_grid_set_spacing(GtkGrid *grid, int spacing);
void gtk_widget_add_class(GtkWidget *w, const char *class);

471
src/iface-mixer.c Normal file
View File

@@ -0,0 +1,471 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "gtkhelper.h"
#include "iface-mixer.h"
#include "stringhelper.h"
#include "tooltips.h"
#include "widget-boolean.h"
#include "widget-combo.h"
#include "widget-dual.h"
#include "widget-volume.h"
#include "window-helper.h"
#include "window-levels.h"
#include "window-mixer.h"
#include "window-routing.h"
#include "window-startup.h"
static void add_clock_source_control(
struct alsa_card *card,
GtkWidget *global_controls
) {
GArray *elems = card->elems;
struct alsa_elem *clock_source = get_elem_by_prefix(elems, "Clock Source");
if (!clock_source)
return;
GtkWidget *b = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
gtk_widget_set_tooltip_text(
b,
"Clock Source selects where the interface receives its digital "
"clock from. If you arent using S/PDIF or ADAT inputs, set this "
"to Internal."
);
gtk_box_append(GTK_BOX(global_controls), b);
GtkWidget *l = gtk_label_new("Clock Source");
GtkWidget *w = make_combo_box_alsa_elem(clock_source);
gtk_box_append(GTK_BOX(b), l);
gtk_box_append(GTK_BOX(b), w);
}
static void add_sync_status_control(
struct alsa_card *card,
GtkWidget *global_controls
) {
GArray *elems = card->elems;
struct alsa_elem *sync_status = get_elem_by_name(elems, "Sync Status");
if (!sync_status)
return;
GtkWidget *b = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
if (get_elem_by_prefix(elems, "Clock Source")) {
gtk_widget_set_tooltip_text(
b,
"Sync Status indicates if the interface is locked to a valid "
"digital clock. If you arent using S/PDIF or ADAT inputs and "
"the Sync Status is Unlocked, change the Clock Source to "
"Internal."
);
} else {
gtk_widget_set_tooltip_text(
b,
"Sync Status indicates if the interface is locked to a valid "
"digital clock. Since the Clock Source is fixed to internal on "
"this interface, this should stay locked."
);
}
gtk_box_append(GTK_BOX(global_controls), b);
GtkWidget *l = gtk_label_new("Sync Status");
gtk_box_append(GTK_BOX(b), l);
GtkWidget *w = make_boolean_alsa_elem(sync_status, "Unlocked", "Locked");
gtk_box_append(GTK_BOX(b), w);
}
static void add_speaker_switching_controls(
struct alsa_card *card,
GtkWidget *global_controls
) {
GArray *elems = card->elems;
struct alsa_elem *speaker_switching = get_elem_by_name(
elems, "Speaker Switching Playback Enum"
);
if (!speaker_switching)
return;
make_dual_boolean_alsa_elems(speaker_switching, "Off", "On", "Main", "Alt");
GtkWidget *b = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
gtk_widget_set_tooltip_text(
b,
"Speaker Switching lets you swap between two pairs of "
"monitoring speakers very easily."
);
GtkWidget *l = gtk_label_new("Speaker Switching");
gtk_box_append(GTK_BOX(global_controls), b);
gtk_box_append(GTK_BOX(b), l);
gtk_box_append(GTK_BOX(b), speaker_switching->widget);
gtk_box_append(GTK_BOX(b), speaker_switching->widget2);
}
static void add_talkback_controls(
struct alsa_card *card,
GtkWidget *global_controls
) {
GArray *elems = card->elems;
struct alsa_elem *talkback = get_elem_by_name(
elems, "Talkback Playback Enum"
);
if (!talkback)
return;
make_dual_boolean_alsa_elems(talkback, "Disabled", "Enabled", "Off", "On");
GtkWidget *b = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
gtk_widget_set_tooltip_text(
b,
"Talkback lets you add another channel (usually the talkback "
"mic) to a mix with a button push, usually to talk to "
"musicians, and without using an additional mic channel."
);
GtkWidget *l = gtk_label_new("Talkback");
gtk_box_append(GTK_BOX(global_controls), b);
gtk_box_append(GTK_BOX(b), l);
gtk_box_append(GTK_BOX(b), talkback->widget);
gtk_box_append(GTK_BOX(b), talkback->widget2);
}
static GtkWidget *create_global_box(GtkWidget *grid, int *x, int orient) {
GtkWidget *label = gtk_label_new("Global");
GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
GtkWidget *controls = gtk_box_new(orient, 15);
gtk_widget_set_margin(controls, 10);
gtk_grid_attach(GTK_GRID(grid), label, *x, 0, 1, 1);
gtk_grid_attach(GTK_GRID(grid), sep, *x, 1, 1, 1);
gtk_grid_attach(GTK_GRID(grid), controls, *x, 2, 1, 1);
(*x)++;
return controls;
}
static void create_input_controls(
struct alsa_card *card,
GtkWidget *top,
int *x
) {
GArray *elems = card->elems;
// there's consistently a pad capture for each analogue input that
// has a control
int input_count = get_max_elem_by_name(elems, "Line", "Pad Capture Switch");
// Only the 18i20 Gen 2 has no input controls
if (!input_count)
return;
GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_VERTICAL);
gtk_widget_set_halign(sep, GTK_ALIGN_CENTER);
gtk_grid_attach(GTK_GRID(top), sep, (*x)++, 0, 1, 3);
GtkWidget *label_ic = gtk_label_new("Analogue Inputs");
gtk_grid_attach(GTK_GRID(top), label_ic, *x, 0, 1, 1);
GtkWidget *horiz_input_sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
gtk_grid_attach(GTK_GRID(top), horiz_input_sep, *x, 1, 1, 1);
GtkWidget *input_grid = gtk_grid_new();
gtk_grid_set_spacing(GTK_GRID(input_grid), 10);
gtk_grid_attach(GTK_GRID(top), input_grid, *x, 2, 1, 1);
for (int i = 1; i <= input_count; i++) {
char s[20];
snprintf(s, 20, "%d", i);
GtkWidget *label = gtk_label_new(s);
gtk_grid_attach(GTK_GRID(input_grid), label, i, 0, 1, 1);
}
GtkWidget *level_label = NULL;
GtkWidget *air_label = NULL;
GtkWidget *pad_label = NULL;
for (int i = 0; i < elems->len; i++) {
struct alsa_elem *elem = &g_array_index(elems, struct alsa_elem, i);
GtkWidget *w;
// if no card entry, it's an empty slot
if (!elem->card)
continue;
int line_num = get_num_from_string(elem->name);
// input controls
if (strstr(elem->name, "Level Capture Enum")) {
if (!level_label) {
level_label = gtk_label_new("Level");
gtk_grid_attach(GTK_GRID(input_grid), level_label, 0, 1, 1, 1);
}
w = make_boolean_alsa_elem(elem, "Line", "Inst");
gtk_widget_set_tooltip_text(w, level_descr);
gtk_grid_attach(GTK_GRID(input_grid), w, line_num, 1, 1, 1);
} else if (strstr(elem->name, "Air Capture Switch")) {
if (!air_label) {
air_label = gtk_label_new("Air");
gtk_grid_attach(GTK_GRID(input_grid), air_label, 0, 2, 1, 1);
}
w = make_boolean_alsa_elem(elem, "Off", "On");
gtk_widget_set_tooltip_text(w, air_descr);
gtk_grid_attach(GTK_GRID(input_grid), w, line_num, 2, 1, 1);
} else if (strstr(elem->name, "Pad Capture Switch")) {
if (!pad_label) {
pad_label = gtk_label_new("Pad");
gtk_grid_attach(GTK_GRID(input_grid), pad_label, 0, 3, 1, 1);
}
w = make_boolean_alsa_elem(elem, "Off", "On");
gtk_widget_set_tooltip_text(
w,
"Enabling Pad engages an attenuator in the channel, giving "
"you more headroom for very hot signals."
);
gtk_grid_attach(GTK_GRID(input_grid), w, line_num, 3, 1, 1);
} else if (strstr(elem->name, "Phantom Power Capture Switch")) {
int from, to;
get_two_num_from_string(elem->name, &from, &to);
w = make_boolean_alsa_elem(elem, "48V Off", "48V On");
gtk_widget_set_tooltip_text(w, phantom_descr);
gtk_grid_attach(GTK_GRID(input_grid), w, from, 4, to - from + 1, 1);
}
}
(*x)++;
}
static void create_output_controls(
struct alsa_card *card,
GtkWidget *top,
int *x,
int y,
int x_span
) {
GArray *elems = card->elems;
if (*x) {
GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_VERTICAL);
gtk_grid_attach(GTK_GRID(top), sep, (*x)++, y, x_span, 3);
}
GtkWidget *label_oc = gtk_label_new("Analogue Outputs");
gtk_grid_attach(GTK_GRID(top), label_oc, *x, y, x_span, 1);
GtkWidget *horiz_output_sep = gtk_separator_new(GTK_ORIENTATION_VERTICAL);
gtk_grid_attach(GTK_GRID(top), horiz_output_sep, *x, y + 1, x_span, 1);
GtkWidget *output_grid = gtk_grid_new();
gtk_grid_set_spacing(GTK_GRID(output_grid), 10);
gtk_grid_attach(GTK_GRID(top), output_grid, *x, y + 2, x_span, 1);
gtk_widget_set_hexpand(output_grid, TRUE);
int output_count = get_max_elem_by_name(elems, "Line", "Playback Volume");
int has_hw_vol = !!get_elem_by_name(elems, "Master HW Playback Volume");
int line_1_col = has_hw_vol;
for (int i = 0; i < output_count; i++) {
char s[20];
snprintf(s, 20, "%d", i + 1);
GtkWidget *label = gtk_label_new(s);
gtk_grid_attach(GTK_GRID(output_grid), label, i + line_1_col, 0, 1, 1);
}
for (int i = 0; i < elems->len; i++) {
struct alsa_elem *elem = &g_array_index(elems, struct alsa_elem, i);
GtkWidget *w;
// if no card entry, it's an empty slot
if (!elem->card)
continue;
int line_num = get_num_from_string(elem->name);
// output controls
if (strncmp(elem->name, "Line", 4) == 0) {
if (strstr(elem->name, "Playback Volume")) {
w = make_volume_alsa_elem(elem);
gtk_grid_attach(
GTK_GRID(output_grid), w, line_num - 1 + line_1_col, 1, 1, 1
);
} else if (strstr(elem->name, "Mute Playback Switch")) {
w = make_boolean_alsa_elem(
elem, "*audio-volume-high", "*audio-volume-muted"
);
if (has_hw_vol) {
gtk_widget_set_tooltip_text(
w,
"Mute (only available when under software control)"
);
} else {
gtk_widget_set_tooltip_text(w, "Mute");
}
gtk_grid_attach(
GTK_GRID(output_grid), w, line_num - 1 + line_1_col, 2, 1, 1
);
} else if (strstr(elem->name, "Volume Control Playback Enum")) {
w = make_boolean_alsa_elem(elem, "SW", "HW");
gtk_widget_set_tooltip_text(
w,
"Set software-controlled (SW) or hardware-controlled (HW) "
"volume for this analogue output."
);
gtk_grid_attach(
GTK_GRID(output_grid), w, line_num - 1 + line_1_col, 3, 1, 1
);
}
// master output controls
} else if (strcmp(elem->name, "Master HW Playback Volume") == 0) {
GtkWidget *l = gtk_label_new("HW");
gtk_widget_set_tooltip_text(
l,
"This control shows the setting of the physical (hardware) "
"volume knob, which controls the volume of the analogue "
"outputs which have been set to “HW”."
);
gtk_grid_attach(GTK_GRID(output_grid), l, 0, 0, 1, 1);
w = make_volume_alsa_elem(elem);
gtk_grid_attach(GTK_GRID(output_grid), w, 0, 1, 1, 1);
} else if (strcmp(elem->name, "Mute Playback Switch") == 0) {
w = make_boolean_alsa_elem(
elem, "*audio-volume-high", "*audio-volume-muted"
);
gtk_widget_set_tooltip_text(w, "Mute HW controlled outputs");
gtk_grid_attach(GTK_GRID(output_grid), elem->widget, 0, 2, 1, 1);
} else if (strcmp(elem->name, "Dim Playback Switch") == 0) {
w = make_boolean_alsa_elem(
elem, "*audio-volume-medium", "*audio-volume-low"
);
gtk_widget_set_tooltip_text(
w, "Dim (lower volume) of HW controlled outputs"
);
gtk_grid_attach(GTK_GRID(output_grid), w, 0, 3, 1, 1);
}
}
(*x)++;
}
static void create_global_controls(
struct alsa_card *card,
GtkWidget *top,
int *x
) {
int orient = card->has_speaker_switching
? GTK_ORIENTATION_HORIZONTAL
: GTK_ORIENTATION_VERTICAL;
GtkWidget *global_controls = create_global_box(top, x, orient);
GtkWidget *left = global_controls;
GtkWidget *right = global_controls;
if (card->has_speaker_switching) {
left = gtk_box_new(GTK_ORIENTATION_VERTICAL, 15);
right = gtk_box_new(GTK_ORIENTATION_VERTICAL, 15);
gtk_box_append(GTK_BOX(global_controls), left);
gtk_box_append(GTK_BOX(global_controls), right);
}
add_clock_source_control(card, left);
add_sync_status_control(card, right);
add_speaker_switching_controls(card, left);
add_talkback_controls(card, right);
}
static GtkWidget *create_main_window_controls(struct alsa_card *card) {
int x = 0;
GtkWidget *top = gtk_grid_new();
gtk_widget_set_margin(top, 10);
gtk_grid_set_spacing(GTK_GRID(top), 10);
create_global_controls(card, top, &x);
create_input_controls(card, top, &x);
if (card->has_speaker_switching) {
x = 0;
GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
gtk_grid_attach(GTK_GRID(top), sep, 0, 3, 3, 1);
create_output_controls(card, top, &x, 4, 3);
} else {
create_output_controls(card, top, &x, 0, 1);
}
return top;
}
static gboolean window_routing_close_request(GtkWindow *w, gpointer data) {
struct alsa_card *card = data;
gtk_widget_activate_action(
GTK_WIDGET(card->window_main), "win.routing", NULL
);
return true;
}
static gboolean window_mixer_close_request(GtkWindow *w, gpointer data) {
struct alsa_card *card = data;
gtk_widget_activate_action(
GTK_WIDGET(card->window_main), "win.mixer", NULL
);
return true;
}
static gboolean window_levels_close_request(GtkWindow *w, gpointer data) {
struct alsa_card *card = data;
gtk_widget_activate_action(
GTK_WIDGET(card->window_main), "win.levels", NULL
);
return true;
}
GtkWidget *create_iface_mixer_main(struct alsa_card *card) {
card->has_speaker_switching =
!!get_elem_by_name(card->elems, "Speaker Switching Playback Enum");
card->has_talkback =
!!get_elem_by_name(card->elems, "Talkback Playback Enum");
GtkWidget *top = create_main_window_controls(card);
GtkWidget *routing_top = create_routing_controls(card);
if (!routing_top)
return NULL;
card->window_routing = create_subwindow(
card, "Routing", G_CALLBACK(window_routing_close_request)
);
gtk_window_set_child(GTK_WINDOW(card->window_routing), routing_top);
GtkWidget *mixer_top = create_mixer_controls(card);
card->window_mixer = create_subwindow(
card, "Mixer", G_CALLBACK(window_mixer_close_request)
);
gtk_window_set_child(GTK_WINDOW(card->window_mixer), mixer_top);
GtkWidget *levels_top = create_levels_controls(card);
card->window_levels = create_subwindow(
card, "Levels", G_CALLBACK(window_levels_close_request)
);
gtk_window_set_child(GTK_WINDOW(card->window_levels), levels_top);
card->window_startup = create_subwindow(
card, "Startup Configuration", G_CALLBACK(window_startup_close_request)
);
GtkWidget *startup = create_startup_controls(card);
gtk_window_set_child(GTK_WINDOW(card->window_startup), startup);
return top;
}

8
src/iface-mixer.h Normal file
View File

@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "alsa.h"
GtkWidget *create_iface_mixer_main(struct alsa_card *card);

116
src/iface-no-mixer.c Normal file
View File

@@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "gtkhelper.h"
#include "iface-no-mixer.h"
#include "stringhelper.h"
#include "tooltips.h"
#include "widget-boolean.h"
#include "widget-combo.h"
#include "window-helper.h"
#include "window-startup.h"
GtkWidget *create_iface_no_mixer_main(struct alsa_card *card) {
GArray *elems = card->elems;
GtkWidget *grid = gtk_grid_new();
GtkWidget *label_ic = gtk_label_new("Input Controls");
GtkWidget *vert_sep = gtk_separator_new(GTK_ORIENTATION_VERTICAL);
GtkWidget *label_oc = gtk_label_new("Output Controls");
gtk_widget_set_margin(grid, 10);
gtk_grid_set_spacing(GTK_GRID(grid), 10);
gtk_grid_attach(GTK_GRID(grid), label_ic, 0, 0, 1, 1);
gtk_grid_attach(GTK_GRID(grid), vert_sep, 1, 0, 1, 3);
gtk_grid_attach(GTK_GRID(grid), label_oc, 2, 0, 1, 1);
GtkWidget *horiz_input_sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
gtk_grid_attach(GTK_GRID(grid), horiz_input_sep, 0, 1, 1, 1);
GtkWidget *input_grid = gtk_grid_new();
gtk_grid_set_spacing(GTK_GRID(input_grid), 10);
gtk_grid_attach(GTK_GRID(grid), input_grid, 0, 2, 1, 1);
GtkWidget *horiz_output_sep = gtk_separator_new(GTK_ORIENTATION_VERTICAL);
gtk_grid_attach(GTK_GRID(grid), horiz_output_sep, 2, 1, 1, 1);
GtkWidget *output_grid = gtk_grid_new();
gtk_grid_set_spacing(GTK_GRID(output_grid), 10);
gtk_grid_attach(GTK_GRID(grid), output_grid, 2, 2, 1, 1);
// Solo or 2i2?
// Solo Phantom Power is Line 1 only
// 2i2 Phantom Power is Line 1-2
int is_solo = !!get_elem_by_name(
elems, "Line In 1 Phantom Power Capture Switch"
);
for (int i = 0; i < 2; i++) {
char s[20];
snprintf(s, 20, "Analogue %d", i + 1);
GtkWidget *label = gtk_label_new(s);
gtk_grid_attach(GTK_GRID(input_grid), label, i, 0, 1, 1);
}
for (int i = 0; i < elems->len; i++) {
struct alsa_elem *elem = &g_array_index(elems, struct alsa_elem, i);
GtkWidget *w;
// if no card entry, it's not a bool/enum/int elem
if (!elem->card)
continue;
if (strstr(elem->name, "Validity"))
continue;
int line_num = get_num_from_string(elem->name);
if (strstr(elem->name, "Level Capture Enum")) {
w = make_boolean_alsa_elem(elem, "Line", "Inst");
gtk_widget_set_tooltip_text(w, level_descr);
gtk_grid_attach(GTK_GRID(input_grid), w, line_num - 1, 1, 1, 1);
} else if (strstr(elem->name, "Air Capture Switch")) {
w = make_boolean_alsa_elem(elem, "Air Off", "Air On");
gtk_widget_set_tooltip_text(w, air_descr);
gtk_grid_attach(
GTK_GRID(input_grid), w, line_num - 1, 1 + !is_solo, 1, 1
);
} else if (strstr(elem->name, "Phantom Power Capture Switch")) {
w = make_boolean_alsa_elem(elem, "48V Off", "48V On");
gtk_widget_set_tooltip_text(w, phantom_descr);
gtk_grid_attach(GTK_GRID(input_grid), w, 0, 3, 1 + !is_solo, 1);
} else if (strcmp(elem->name, "Direct Monitor Playback Switch") == 0) {
w = make_boolean_alsa_elem(
elem, "Direct Monitor Off", "Direct Monitor On"
);
gtk_widget_set_tooltip_text(
w,
"Direct Monitor sends the analogue input signals to the "
"analogue outputs for zero-latency monitoring."
);
gtk_grid_attach(GTK_GRID(output_grid), w, 0, 0, 1, 1);
} else if (strcmp(elem->name, "Direct Monitor Playback Enum") == 0) {
GtkWidget *l = gtk_label_new("Direct Monitor");
gtk_grid_attach(GTK_GRID(output_grid), l, 0, 0, 1, 1);
w = make_combo_box_alsa_elem(elem);
gtk_widget_set_tooltip_text(
w,
"Direct Monitor sends the analogue input signals to the "
"analogue outputs for zero-latency monitoring. Mono sends "
"both inputs to the left and right outputs. Stereo sends "
"input 1 to the left, and input 2 to the right output."
);
gtk_grid_attach(GTK_GRID(output_grid), w, 0, 1, 1, 1);
}
}
card->window_startup = create_subwindow(
card, "Startup Configuration", G_CALLBACK(window_startup_close_request)
);
GtkWidget *startup = create_startup_controls(card);
gtk_window_set_child(GTK_WINDOW(card->window_startup), startup);
return grid;
}

8
src/iface-no-mixer.h Normal file
View File

@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "alsa.h"
GtkWidget *create_iface_no_mixer_main(struct alsa_card *card);

30
src/iface-none.c Normal file
View File

@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "iface-none.h"
#include "gtkhelper.h"
#include "menu.h"
GtkWidget *create_window_iface_none(GtkApplication *app) {
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 50);
gtk_widget_set_margin(box, 50);
GtkWidget *picture = gtk_picture_new_for_resource(
"/vu/b4/alsa-scarlett-gui/icons/alsa-scarlett-gui-logo.png"
);
GtkWidget *label = gtk_label_new("No Scarlett Gen 2/3 interface found.");
gtk_box_append(GTK_BOX(box), picture);
gtk_box_append(GTK_BOX(box), label);
GtkWidget *w = gtk_application_window_new(app);
gtk_window_set_resizable(GTK_WINDOW(w), FALSE);
gtk_window_set_title(GTK_WINDOW(w), "ALSA Scarlett Gen 2/3 Control Panel");
gtk_window_set_child(GTK_WINDOW(w), box);
gtk_application_window_set_show_menubar(
GTK_APPLICATION_WINDOW(w), TRUE
);
add_window_action_map(GTK_WINDOW(w));
gtk_widget_show(w);
return w;
}

8
src/iface-none.h Normal file
View File

@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
GtkWidget *create_window_iface_none(GtkApplication *app);

29
src/iface-unknown.c Normal file
View File

@@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "gtkhelper.h"
#include "iface-unknown.h"
GtkWidget *create_iface_unknown_main(void) {
GtkWidget *label = gtk_label_new(
"Sorry, I dont recognise the controls on this card.\n\n"
"These Focusrite Scarlett models should be supported:\n"
" Gen 2: 6i6/18i8/18i20\n"
" Gen 3: Solo/2i2/4i4/8i6/18i8/18i20\n\n"
"Are you running a recent kernel with Scarlett Gen 2/3 support "
"enabled?\n\n"
"Check dmesg output for “Focusrite Scarlett Gen 2/3 Mixer "
"Driver”:\n\n"
"dmesg | grep Scarlett\n\n"
"You may need to create a file /etc/modprobe.d/scarlett.conf\n"
"with an “options snd_usb_audio ...” line and reboot."
);
gtk_widget_set_margin(label, 30);
return label;
}

8
src/iface-unknown.h Normal file
View File

@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
GtkWidget *create_iface_unknown_main(void);

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

18
src/img/socket.svg Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
width="4mm"
height="4mm"
viewBox="0 0 4 4">
<circle
style="opacity:1;fill:#808080;fill-opacity:1;stroke:none"
cx="2"
cy="2"
r="2" />
<circle
style="opacity:1;fill:#000000;fill-opacity:1;stroke:#0000ff;stroke-width:0.2;stroke-opacity:1;paint-order:markers stroke fill"
cx="2"
cy="2"
r="1" />
</svg>

After

Width:  |  Height:  |  Size: 466 B

75
src/main.c Normal file
View File

@@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "alsa.h"
#include "alsa-sim.h"
#include "main.h"
#include "menu.h"
#include "window-hardware.h"
#include "window-iface.h"
GtkApplication *app;
// CSS
static void load_css(void) {
GtkCssProvider *css = gtk_css_provider_new();
GdkDisplay *display = gdk_display_get_default();
gtk_style_context_add_provider_for_display(
display, GTK_STYLE_PROVIDER(css), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION
);
gtk_css_provider_load_from_resource(
css,
"/vu/b4/alsa-scarlett-gui/alsa-scarlett-gui.css"
);
g_object_unref(css);
}
// gtk init
static void startup(GtkApplication *app, gpointer user_data) {
gtk_window_set_default_icon_name("alsa-scarlett-gui");
gtk_application_set_menubar(app, G_MENU_MODEL(create_app_menu(app)));
alsa_inotify_init();
alsa_cards = g_array_new(FALSE, TRUE, sizeof(struct alsa_card *));
load_css();
alsa_scan_cards();
create_no_card_window();
create_hardware_window(app);
}
// not called when any files are opened from the command-line so we do
// everything in startup(), but GTK wants this signal handled
// regardless
static void activate(GtkApplication *app, gpointer user_data) {
}
static void open_cb(
GtkApplication *app,
GFile **files,
gint n_files,
const gchar *hint
) {
for (int i = 0; i < n_files; i++) {
char *fn = g_file_get_path(files[i]);
create_sim_from_file(NULL, fn);
g_free(fn);
}
}
int main(int argc, char **argv) {
app = gtk_application_new("vu.b4.alsa-scarlett-gui", G_APPLICATION_HANDLES_OPEN);
g_signal_connect(app, "startup", G_CALLBACK(startup), NULL);
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
g_signal_connect(app, "open", G_CALLBACK(open_cb), NULL);
int status = g_application_run(G_APPLICATION(app), argc, argv);
g_object_unref(app);
return status;
}

8
src/main.h Normal file
View File

@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
extern GtkApplication *app;

190
src/menu.c Normal file
View File

@@ -0,0 +1,190 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "about.h"
#include "file.h"
#include "menu.h"
#include "window-hardware.h"
static void activate_hardware(
GSimpleAction *action,
GVariant *parameter,
gpointer data
) {
GVariant *state = g_action_get_state(G_ACTION(action));
int new_state = !g_variant_get_boolean(state);
g_action_change_state(G_ACTION(action), g_variant_new_boolean(new_state));
if (new_state)
gtk_widget_show(window_hardware);
else
gtk_widget_hide(window_hardware);
}
static void activate_quit(
GSimpleAction *action,
GVariant *parameter,
gpointer data
) {
g_application_quit(G_APPLICATION(data));
}
static void activate_routing(
GSimpleAction *action,
GVariant *parameter,
gpointer data
) {
struct alsa_card *card = data;
GVariant *state = g_action_get_state(G_ACTION(action));
int new_state = !g_variant_get_boolean(state);
g_action_change_state(G_ACTION(action), g_variant_new_boolean(new_state));
if (new_state)
gtk_widget_show(card->window_routing);
else
gtk_widget_hide(card->window_routing);
}
static void activate_mixer(
GSimpleAction *action,
GVariant *parameter,
gpointer data
) {
struct alsa_card *card = data;
GVariant *state = g_action_get_state(G_ACTION(action));
int new_state = !g_variant_get_boolean(state);
g_action_change_state(G_ACTION(action), g_variant_new_boolean(new_state));
if (new_state)
gtk_widget_show(card->window_mixer);
else
gtk_widget_hide(card->window_mixer);
}
static void activate_levels(
GSimpleAction *action,
GVariant *parameter,
gpointer data
) {
struct alsa_card *card = data;
GVariant *state = g_action_get_state(G_ACTION(action));
int new_state = !g_variant_get_boolean(state);
g_action_change_state(G_ACTION(action), g_variant_new_boolean(new_state));
if (new_state)
gtk_widget_show(card->window_levels);
else
gtk_widget_hide(card->window_levels);
}
static void activate_startup(
GSimpleAction *action,
GVariant *parameter,
gpointer data
) {
struct alsa_card *card = data;
GVariant *state = g_action_get_state(G_ACTION(action));
int new_state = !g_variant_get_boolean(state);
g_action_change_state(G_ACTION(action), g_variant_new_boolean(new_state));
if (new_state)
gtk_widget_show(card->window_startup);
else
gtk_widget_hide(card->window_startup);
}
static const GActionEntry app_entries[] = {
{"hardware", activate_hardware, NULL, "false"},
{"quit", activate_quit},
};
GMenu *create_app_menu(GtkApplication *app) {
g_action_map_add_action_entries(
G_ACTION_MAP(app), app_entries, G_N_ELEMENTS(app_entries), app
);
GMenu *menu = g_menu_new();
GMenu *file_menu = g_menu_new();
g_menu_append_submenu(menu, "_File", G_MENU_MODEL(file_menu));
g_menu_append(file_menu, "_Load Configuration", "win.load");
g_menu_append(file_menu, "_Save Configuration", "win.save");
g_menu_append(file_menu, "_Interface Simulation", "win.sim");
g_menu_append(file_menu, "E_xit", "app.quit");
GMenu *view_menu = g_menu_new();
g_menu_append_submenu(menu, "_View", G_MENU_MODEL(view_menu));
g_menu_append(view_menu, "_Routing", "win.routing");
g_menu_append(view_menu, "_Mixer", "win.mixer");
//g_menu_append(view_menu, "_Levels", "win.levels");
g_menu_append(view_menu, "_Startup", "win.startup");
GMenu *help_menu = g_menu_new();
g_menu_append_submenu(menu, "_Help", G_MENU_MODEL(help_menu));
g_menu_append(help_menu, "_Supported Hardware", "app.hardware");
g_menu_append(help_menu, "_About", "win.about");
return menu;
}
static const GActionEntry win_entries[] = {
{"about", activate_about},
{"sim", activate_sim}
};
void add_window_action_map(GtkWindow *w) {
g_action_map_add_action_entries(
G_ACTION_MAP(w), win_entries, G_N_ELEMENTS(win_entries), w
);
}
static const GActionEntry load_save_entries[] = {
{"load", activate_load},
{"save", activate_save}
};
void add_load_save_action_map(struct alsa_card *card) {
g_action_map_add_action_entries(
G_ACTION_MAP(card->window_main),
load_save_entries,
G_N_ELEMENTS(load_save_entries),
card
);
}
static const GActionEntry startup_entry[] = {
{"startup", activate_startup, NULL, "false"}
};
void add_startup_action_map(struct alsa_card *card) {
g_action_map_add_action_entries(
G_ACTION_MAP(card->window_main),
startup_entry,
G_N_ELEMENTS(startup_entry),
card
);
}
static const GActionEntry mixer_entries[] = {
{"routing", activate_routing, NULL, "false"},
{"mixer", activate_mixer, NULL, "false"},
{"levels", activate_levels, NULL, "false"}
};
void add_mixer_action_map(struct alsa_card *card) {
g_action_map_add_action_entries(
G_ACTION_MAP(card->window_main),
mixer_entries,
G_N_ELEMENTS(mixer_entries),
card
);
}

14
src/menu.h Normal file
View File

@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
#include "alsa.h"
GMenu *create_app_menu(GtkApplication *app);
void add_window_action_map(GtkWindow *w);
void add_load_save_action_map(struct alsa_card *card);
void add_startup_action_map(struct alsa_card *card);
void add_mixer_action_map(struct alsa_card *card);

68
src/routing-drag-line.c Normal file
View File

@@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "routing-drag-line.h"
#include "routing-lines.h"
static void drag_enter(
GtkDropControllerMotion *motion,
gdouble x,
gdouble y,
gpointer data
) {
struct alsa_card *card = data;
card->drag_x = x;
card->drag_y = y;
gtk_widget_queue_draw(card->drag_line);
gtk_widget_queue_draw(card->routing_lines);
}
static void drag_leave(
GtkDropControllerMotion *motion,
gpointer data
) {
struct alsa_card *card = data;
card->drag_x = -1;
card->drag_y = -1;
gtk_widget_queue_draw(card->drag_line);
gtk_widget_queue_draw(card->routing_lines);
}
static void drag_motion(
GtkDropControllerMotion *motion,
gdouble x,
gdouble y,
gpointer data
) {
struct alsa_card *card = data;
card->drag_x = x;
card->drag_y = y;
gtk_widget_queue_draw(card->drag_line);
gtk_widget_queue_draw(card->routing_lines);
}
void add_drop_controller_motion(
struct alsa_card *card,
GtkWidget *routing_overlay
) {
// create an area to draw the drag line on
card->drag_line = gtk_drawing_area_new();
gtk_widget_set_can_target(card->drag_line, FALSE);
gtk_drawing_area_set_draw_func(
GTK_DRAWING_AREA(card->drag_line), draw_drag_line, card, NULL
);
gtk_overlay_add_overlay(
GTK_OVERLAY(routing_overlay), card->drag_line
);
// create a controller to handle the dragging
GtkEventController *controller = gtk_drop_controller_motion_new();
g_signal_connect(controller, "enter", G_CALLBACK(drag_enter), card);
g_signal_connect(controller, "leave", G_CALLBACK(drag_leave), card);
g_signal_connect(controller, "motion", G_CALLBACK(drag_motion), card);
gtk_widget_add_controller(card->routing_grid, controller);
}

11
src/routing-drag-line.h Normal file
View File

@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "alsa.h"
void add_drop_controller_motion(
struct alsa_card *card,
GtkWidget *routing_overlay
);

377
src/routing-lines.c Normal file
View File

@@ -0,0 +1,377 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "alsa.h"
#include "routing-lines.h"
// dotted dash when a destination is going to be removed by a drag
static const double dash_dotted[] = { 1, 10 };
// dash when dragging and not connected
static const double dash[] = { 4 };
static void choose_line_colour(
struct routing_src *r_src,
struct routing_dst *r_dst,
double *r,
double *g,
double *b
) {
int odd_in = r_src->lr_num & 1;
int odd_out = r_dst->elem->lr_num & 1;
int in2 = ((r_src->lr_num - 1) / 2 & 1);
int out2 = ((r_dst->elem->lr_num - 1) / 2 & 1);
if (odd_in && odd_out) {
*r = 0;
*g = 0;
*b = 0;
} else if (!odd_in && !odd_out) {
*r = 1;
*g = 0;
*b = 0;
} else if (odd_in) {
*r = 0;
*g = 0.25;
*b = 0;
} else {
*r = 0.25;
*g = 0.25;
*b = 0;
}
if ((r_src->port_category == PC_MIX) !=
(r_dst->port_category == PC_MIX)) {
*b = 0.5;
}
if (in2) {
*r = (*r + 1) / 2;
*g = (*g + 1) / 2;
}
if (out2) {
*b = (*b + 1) / 2;
}
}
// draw a bezier curve given the end and control points
static void curve(
cairo_t *cr,
double x1,
double y1,
double x2,
double y2,
double x3,
double y3,
double x4,
double y4
) {
cairo_move_to(cr, x1, y1);
cairo_curve_to(cr, x2, y2, x3, y3, x4, y4);
}
// given the bezier end & control points and t-value, return the
// position and tangent angle at that point
static void point_and_angle_on_bezier(
double x1,
double y1,
double x2,
double y2,
double x3,
double y3,
double x4,
double y4,
double t,
double *x,
double *y,
double *a
) {
double t2 = t * t;
double t3 = t2 * t;
double ti = 1 - t;
double ti2 = ti * ti;
*x = x1 +
(-x1 * 3 + t * (3 * x1 - x1 * t)) * t +
(3 * x2 + t * (-6 * x2 + x2 * 3 * t)) * t +
(x3 * 3 - x3 * 3 * t) * t2 +
x4 * t3;
*y = y1 +
(-y1 * 3 + t * (3 * y1 - y1 * t)) * t +
(3 * y2 + t * (-6 * y2 + y2 * 3 * t)) * t +
(y3 * 3 - y3 * 3 * t) * t2 +
y4 * t3;
double dx = ti2 * (x2 - x1) +
2 * ti * t * (x3 - x2) +
t2 * (x4 - x3);
double dy = ti2 * (y2 - y1) +
2 * ti * t * (y3 - y2) +
t2 * (y4 - y3);
*a = atan2(dy, dx);
}
// draw an arrow in the middle of the line drawn by curve()
static void arrow(
cairo_t *cr,
double x1,
double y1,
double x2,
double y2,
double x3,
double y3,
double x4,
double y4
) {
// get midpoint and angle
double mx, my, a;
point_and_angle_on_bezier(x1, y1, x2, y2, x3, y3, x4, y4, 0.5, &mx, &my, &a);
// calculate point of arrow
double px = mx + cos(a) * 12;
double py = my + sin(a) * 12;
// calculate sides of arrow
double s1x = mx + cos(a - M_PI_2) * 2;
double s1y = my + sin(a - M_PI_2) * 2;
double s2x = mx + cos(a + M_PI_2) * 2;
double s2y = my + sin(a + M_PI_2) * 2;
// draw triangle
cairo_move_to(cr, px, py);
cairo_line_to(cr, s1x, s1y);
cairo_line_to(cr, s2x, s2y);
cairo_close_path(cr);
}
// draw a nice curved line connecting a source at (x1, y1) and a
// destination at (x2, y2)
static void draw_connection(
cairo_t *cr,
double x1,
double y1,
int src_is_mixer,
double x2,
double y2,
int dst_is_mixer,
double r,
double g,
double b,
double w
) {
double x3 = x1, y3 = y1, x4 = x2, y4 = y2;
// vertical/horizontal?
if (src_is_mixer == dst_is_mixer) {
double f1 = 0.3;
double f2 = 1 - f1;
// vertical
if (src_is_mixer) {
y3 = y1 * f2 + y2 * f1;
y4 = y1 * f1 + y2 * f2;
// horizontal
} else {
x3 = x1 * f2 + x2 * f1;
x4 = x1 * f1 + x2 * f2;
}
// diagonal
} else {
// calculate a fraction f1 close to 0 when approaching 45°
// and close to 0.5 when approaching 0°/90°
double a = fmod((atan2(y1 - y2, x2 - x1) * 180 / M_PI) + 360, 360);
double f1 = fabs(fmod(a, 90) - 45) / 90;
double f2 = 1 - f1;
// bottom to right
if (src_is_mixer) {
y3 = y1 * f2 + y2 * f1;
x4 = x1 * f1 + x2 * f2;
// left to top
} else {
x3 = x1 * f2 + x2 * f1;
y4 = y1 * f1 + y2 * f2;
}
}
cairo_set_source_rgb(cr, r, g, b);
cairo_set_line_width(cr, w);
curve(cr, x1, y1, x3, y3, x4, y4, x2, y2);
arrow(cr, x1, y1, x3, y3, x4, y4, x2, y2);
cairo_stroke(cr);
}
// locate the center of a widget in the parent coordinates
// used for drawing lines to/from the "socket" widget of routing
// sources and destinations
static void get_widget_center(
GtkWidget *w,
GtkWidget *parent,
double *x,
double *y
) {
double src_x = gtk_widget_get_allocated_width(w) / 2;
double src_y = gtk_widget_get_allocated_height(w) / 2;
gtk_widget_translate_coordinates(w, parent, src_x, src_y, x, y);
}
static void get_src_center(
struct routing_src *r_src,
GtkWidget *parent,
double *x,
double *y
) {
get_widget_center(r_src->widget2, parent, x, y);
if (r_src->port_category == PC_MIX)
(*y)++;
}
static void get_dst_center(
struct routing_dst *r_dst,
GtkWidget *parent,
double *x,
double *y
) {
get_widget_center(r_dst->elem->widget2, parent, x, y);
if (r_dst->port_category == PC_MIX)
(*y)++;
}
// redraw the overlay lines between the routing sources and
// destinations
void draw_routing_lines(
GtkDrawingArea *drawing_area,
cairo_t *cr,
int width,
int height,
void *user_data
) {
struct alsa_card *card = user_data;
GtkWidget *parent = card->routing_lines;
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
int dragging = card->drag_type != DRAG_TYPE_NONE;
// go through all the routing destinations
for (int i = 0; i < card->routing_dsts->len; i++) {
struct routing_dst *r_dst = &g_array_index(
card->routing_dsts, struct routing_dst, i
);
// if dragging and a routing destination is being reconnected then
// draw it with dots
int dragging_this = dragging && card->dst_drag == r_dst;
if (dragging_this)
cairo_set_dash(cr, dash_dotted, 2, 0);
else
cairo_set_dash(cr, NULL, 0, 0);
// get the destination and skip if it's "Off"
int r_src_idx = alsa_get_elem_value(r_dst->elem);
if (!r_src_idx)
continue;
// look up the source
struct routing_src *r_src = &g_array_index(
card->routing_srcs, struct routing_src, r_src_idx
);
// locate the source and destination coordinates
double x1, y1, x2, y2;
get_src_center(r_src, parent, &x1, &y1);
get_dst_center(r_dst, parent, &x2, &y2);
// pick a colour
double r, g, b;
choose_line_colour(r_src, r_dst, &r, &g, &b);
// make the colour lighter if it's being shown dotted
if (dragging_this) {
r = (r + 1) / 2;
g = (g + 1) / 2;
b = (b + 1) / 2;
}
// draw the connection
draw_connection(
cr,
x1, y1, r_src->port_category == PC_MIX,
x2, y2, r_dst->port_category == PC_MIX,
r, g, b, 2
);
}
}
// draw the overlay dragging line
void draw_drag_line(
GtkDrawingArea *drawing_area,
cairo_t *cr,
int width,
int height,
void *user_data
) {
struct alsa_card *card = user_data;
GtkWidget *parent = card->drag_line;
// if not dragging or routing src & dst not specified or drag out of
// bounds then do nothing
if (card->drag_type == DRAG_TYPE_NONE ||
(!card->src_drag && !card->dst_drag) ||
card->drag_x < 0 ||
card->drag_y < 0)
return;
// the drag mouse position is relative to card->routing_grid
// translate it to the overlay card->drag_line
// (don't need to do this if both src_drag and dst_drag are set)
double drag_x, drag_y;
if (!card->src_drag || !card->dst_drag)
gtk_widget_translate_coordinates(
card->routing_grid, parent,
card->drag_x, card->drag_y,
&drag_x, &drag_y
);
// get the line start position; either a routing source socket
// widget or the drag mouse position
double x1, y1;
if (card->src_drag) {
get_src_center(card->src_drag, parent, &x1, &y1);
} else {
x1 = drag_x;
y1 = drag_y;
}
// get the line end position; either a routing destination socket
// widget or the drag mouse position
double x2, y2;
if (card->dst_drag) {
get_dst_center(card->dst_drag, parent, &x2, &y2);
} else {
x2 = drag_x;
y2 = drag_y;
}
// if routing src & dst both specified then draw a curved line as if
// it was connected (except black)
if (card->src_drag && card->dst_drag) {
draw_connection(
cr,
x1, y1, card->src_drag->port_category == PC_MIX,
x2, y2, card->dst_drag->port_category == PC_MIX,
0, 0, 0, 2
);
// otherwise draw a straight line
} else {
cairo_set_dash(cr, dash, 1, 0);
cairo_set_source_rgb(cr, 0, 0, 0);
cairo_set_line_width(cr, 2);
cairo_move_to(cr, x1, y1);
cairo_line_to(cr, x2, y2);
cairo_stroke(cr);
}
}

22
src/routing-lines.h Normal file
View File

@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
void draw_routing_lines(
GtkDrawingArea *drawing_area,
cairo_t *cr,
int width,
int height,
void *user_data
);
void draw_drag_line(
GtkDrawingArea *drawing_area,
cairo_t *cr,
int width,
int height,
void *user_data
);

71
src/stringhelper.c Normal file
View File

@@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include "stringhelper.h"
// return the first number found in the string
int get_num_from_string(const char *s) {
int num;
while (*s) {
if (isdigit(*s))
break;
s++;
}
if (!*s)
return -1;
if (!sscanf(s, "%d", &num))
return 0;
return num;
}
// return the first two numbers found in the string
void get_two_num_from_string(const char *s, int *a, int *b) {
*a = -1;
*b = -1;
while (*s) {
if (isdigit(*s))
break;
s++;
}
if (!*s)
return;
if (!sscanf(s, "%d", a))
return;
while (*s) {
if (!isdigit(*s))
break;
s++;
}
while (*s) {
if (isdigit(*s))
break;
s++;
}
if (!sscanf(s, "%d", b))
return;
}
// check if the given string ends with the given suffix
int string_ends_with(const char *s, const char *suffix) {
if (!s || !suffix)
return 0;
int s_len = strlen(s);
int suffix_len = strlen(suffix);
if (s_len < suffix_len)
return 0;
return strcmp(s + s_len - suffix_len, suffix) == 0;
}

8
src/stringhelper.h Normal file
View File

@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
int get_num_from_string(const char *s);
void get_two_num_from_string(const char *s, int *a, int *b);
int string_ends_with(const char *s, const char *suffix);

19
src/tooltips.c Normal file
View File

@@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "tooltips.h"
// tooltips that are used from multiple files
const char *level_descr =
"Mic/Line or Instrument Level (Impedance)";
const char *air_descr =
"Enabling Air will transform your recordings and inspire you while "
"making music.";
const char *phantom_descr =
"Enabling 48V sends “Phantom Power” to the XLR microphone input. "
"This is required for some microphones (such as condensor "
"microphones), and damaging to some microphones (particularly "
"vintage ribbon microphones).";

8
src/tooltips.h Normal file
View File

@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
extern const char *level_descr;
extern const char *air_descr;
extern const char *phantom_descr;

50
src/widget-boolean.c Normal file
View File

@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "widget-boolean.h"
static void button_clicked(GtkWidget *widget, struct alsa_elem *elem) {
int value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
alsa_set_elem_value(elem, value);
}
static void toggle_button_updated(struct alsa_elem *elem) {
int is_writable = alsa_get_elem_writable(elem);
gtk_widget_set_sensitive(elem->widget, is_writable);
int value = alsa_get_elem_value(elem);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(elem->widget), value);
const char *text = elem->bool_text[value];
if (text) {
if (*text == '*') {
GtkWidget *icon = gtk_image_new_from_icon_name(text + 1);
gtk_button_set_child(GTK_BUTTON(elem->widget), icon);
} else {
gtk_button_set_label(
GTK_BUTTON(elem->widget), elem->bool_text[value]
);
}
}
}
GtkWidget *make_boolean_alsa_elem(
struct alsa_elem *elem,
const char *disabled_text,
const char *enabled_text
) {
GtkWidget *button = gtk_toggle_button_new();
g_signal_connect(
button, "clicked", G_CALLBACK(button_clicked), elem
);
elem->widget = button;
elem->widget_callback = toggle_button_updated;
elem->bool_text[0] = disabled_text;
elem->bool_text[1] = enabled_text;
toggle_button_updated(elem);
return button;
}

14
src/widget-boolean.h Normal file
View File

@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
#include "alsa.h"
GtkWidget *make_boolean_alsa_elem(
struct alsa_elem *alsa_elem,
const char *disabled_text,
const char *enabled_text
);

35
src/widget-combo.c Normal file
View File

@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "widget-combo.h"
static void combo_box_changed(GtkWidget *widget, struct alsa_elem *elem) {
int value = gtk_combo_box_get_active(GTK_COMBO_BOX(widget));
alsa_set_elem_value(elem, value);
}
static void combo_box_updated(struct alsa_elem *elem) {
int value = alsa_get_elem_value(elem);
gtk_combo_box_set_active(GTK_COMBO_BOX(elem->widget), value);
}
GtkWidget *make_combo_box_alsa_elem(struct alsa_elem *elem) {
GtkWidget *combo_box = gtk_combo_box_text_new();
int count = alsa_get_item_count(elem);
for (int i = 0; i < count; i++) {
const char *text = alsa_get_item_name(elem, i);
gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(combo_box), NULL, text);
}
g_signal_connect(
combo_box, "changed", G_CALLBACK(combo_box_changed), elem
);
elem->widget = combo_box;
elem->widget_callback = combo_box_updated;
combo_box_updated(elem);
return combo_box;
}

10
src/widget-combo.h Normal file
View File

@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
#include "alsa.h"
GtkWidget *make_combo_box_alsa_elem(struct alsa_elem *elem);

68
src/widget-dual.c Normal file
View File

@@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "widget-dual.h"
static void dual_button_clicked(GtkWidget *widget, struct alsa_elem *elem) {
int value1 = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(elem->widget));
int value2 = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(elem->widget2));
int value = value1 ? value2 + 1 : 0;
alsa_set_elem_value(elem, value);
gtk_widget_set_sensitive(elem->widget2, value1);
}
static void dual_button_updated(struct alsa_elem *elem) {
// value (from ALSA control) is 0/1/2
// value1 (first button) is 0/1/1
// value2 (second button) is X/0/1
int value = alsa_get_elem_value(elem);
int value1 = !!value;
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(elem->widget), value1);
gtk_button_set_label(GTK_BUTTON(elem->widget), elem->bool_text[value1]);
gtk_widget_set_sensitive(elem->widget2, value1);
if (value1) {
int value2 = value - 1;
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(elem->widget2), value2);
gtk_button_set_label(
GTK_BUTTON(elem->widget2), elem->bool_text[value2 + 2]
);
}
}
// speaker switch and talkback have three states, controlled by two
// buttons:
// first button disables/enables the feature
// second button switches between the two enabled states
void make_dual_boolean_alsa_elems(
struct alsa_elem *elem,
const char *disabled_text_1,
const char *enabled_text_1,
const char *disabled_text_2,
const char *enabled_text_2
) {
GtkWidget *button1 = gtk_toggle_button_new();
GtkWidget *button2 = gtk_toggle_button_new();
g_signal_connect(
button1, "clicked", G_CALLBACK(dual_button_clicked), elem
);
g_signal_connect(
button2, "clicked", G_CALLBACK(dual_button_clicked), elem
);
elem->widget = button1;
elem->widget2 = button2;
elem->widget_callback = dual_button_updated;
elem->bool_text[0] = disabled_text_1;
elem->bool_text[1] = enabled_text_1;
elem->bool_text[2] = disabled_text_2;
elem->bool_text[3] = enabled_text_2;
gtk_button_set_label(GTK_BUTTON(elem->widget2), disabled_text_2);
dual_button_updated(elem);
}

18
src/widget-dual.h Normal file
View File

@@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "alsa.h"
// speaker switch and talkback have three states, controlled by two
// buttons:
// first button disables/enables the feature
// second button switches between the two features states
void make_dual_boolean_alsa_elems(
struct alsa_elem *alsa_elem,
const char *disabled_text_1,
const char *enabled_text_1,
const char *disabled_text_2,
const char *enabled_text_2
);

56
src/widget-gain.c Normal file
View File

@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "gtkdial.h"
#include "widget-gain.h"
// gain controls -80dB - +6dB, 0.5dB steps
#define DIAL_MIN_VALUE 0
#define DIAL_MAX_VALUE 172
#define DIAL_ZERO_DB_VALUE 160
static void gain_changed(GtkWidget *widget, struct alsa_elem *elem) {
int value = gtk_dial_get_value(GTK_DIAL(widget));
alsa_set_elem_value(elem, value);
}
static void gain_updated(struct alsa_elem *elem) {
int is_writable = alsa_get_elem_writable(elem);
gtk_widget_set_sensitive(elem->widget, is_writable);
int value = alsa_get_elem_value(elem);
gtk_dial_set_value(GTK_DIAL(elem->widget), value);
char s[20];
snprintf(s, 20, "%.1f", (value / 2.0) - 80);
gtk_label_set_text(GTK_LABEL(elem->widget2), s);
}
GtkWidget *make_gain_alsa_elem(struct alsa_elem *elem) {
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_hexpand(vbox, TRUE);
GtkWidget *dial = gtk_dial_new_with_range(
DIAL_MIN_VALUE, DIAL_MAX_VALUE, 1
);
gtk_dial_set_zero_db(GTK_DIAL(dial), DIAL_ZERO_DB_VALUE);
gtk_widget_set_vexpand(dial, TRUE);
g_signal_connect(
dial, "value-changed", G_CALLBACK(gain_changed), elem
);
elem->widget = dial;
elem->widget_callback = gain_updated;
GtkWidget *label = gtk_label_new(NULL);
elem->widget2 = label;
gain_updated(elem);
gtk_box_append(GTK_BOX(vbox), dial);
gtk_box_append(GTK_BOX(vbox), label);
return vbox;
}

10
src/widget-gain.h Normal file
View File

@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
#include "alsa.h"
GtkWidget *make_gain_alsa_elem(struct alsa_elem *alsa_elem);

56
src/widget-volume.c Normal file
View File

@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "gtkdial.h"
#include "widget-volume.h"
// volume controls -127dB - 0dB
#define DIAL_MIN_VALUE 0
#define DIAL_MAX_VALUE 127
#define DIAL_ZERO_DB_VALUE 127
static void volume_changed(GtkWidget *widget, struct alsa_elem *elem) {
int value = gtk_dial_get_value(GTK_DIAL(widget));
alsa_set_elem_value(elem, value);
}
static void volume_updated(struct alsa_elem *elem) {
int is_writable = alsa_get_elem_writable(elem);
gtk_widget_set_sensitive(elem->widget, is_writable);
int value = alsa_get_elem_value(elem);
gtk_dial_set_value(GTK_DIAL(elem->widget), value);
char s[20];
snprintf(s, 20, "%ddB", value - 127);
gtk_label_set_text(GTK_LABEL(elem->widget2), s);
}
GtkWidget *make_volume_alsa_elem(struct alsa_elem *elem) {
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_hexpand(vbox, TRUE);
GtkWidget *dial = gtk_dial_new_with_range(
DIAL_MIN_VALUE, DIAL_MAX_VALUE, 1
);
gtk_dial_set_zero_db(GTK_DIAL(dial), DIAL_ZERO_DB_VALUE);
gtk_widget_set_vexpand(dial, TRUE);
g_signal_connect(
dial, "value-changed", G_CALLBACK(volume_changed), elem
);
elem->widget = dial;
elem->widget_callback = volume_updated;
GtkWidget *label = gtk_label_new(NULL);
elem->widget2 = label;
volume_updated(elem);
gtk_box_append(GTK_BOX(vbox), dial);
gtk_box_append(GTK_BOX(vbox), label);
return vbox;
}

10
src/widget-volume.h Normal file
View File

@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
#include "alsa.h"
GtkWidget *make_volume_alsa_elem(struct alsa_elem *alsa_elem);

98
src/window-hardware.c Normal file
View File

@@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "window-hardware.h"
GtkWidget *window_hardware;
struct hw_info {
char *name;
};
struct hw_cat {
char *name;
struct hw_info *info;
};
struct hw_info gen_2_info[] = {
{ "Scarlett 6i6 2nd Gen" },
{ "Scarlett 18i8 2nd Gen" },
{ "Scarlett 18i20 2nd Gen" },
{ }
};
struct hw_info gen_3_small_info[] = {
{ "Scarlett Solo 3rd Gen" },
{ "Scarlett 2i2 3rd Gen" },
{ }
};
struct hw_info gen_3_big_info[] = {
{ "Scarlett 4i4 3rd Gen" },
{ "Scarlett 8i6 3rd Gen" },
{ "Scarlett 18i8 3rd Gen" },
{ "Scarlett 18i20 3rd Gen" },
{ }
};
struct hw_cat hw_cat[] = {
{ "2nd Gen",
gen_2_info
},
{ "Small 3rd Gen",
gen_3_small_info
},
{ "Big 3rd Gen",
gen_3_big_info
},
{ }
};
gboolean window_hardware_close_request(
GtkWindow *w,
gpointer data
) {
GtkApplication *app = data;
g_action_group_activate_action(
G_ACTION_GROUP(app), "hardware", NULL
);
return true;
}
GtkWidget *make_notebook_page(struct hw_cat *cat) {
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
for (struct hw_info *info = cat->info; info->name; info++) {
GtkWidget *label = gtk_label_new(info->name);
gtk_box_append(GTK_BOX(box), label);
}
return box;
}
void add_notebook_pages(GtkWidget *notebook) {
for (struct hw_cat *cat = hw_cat; cat->name; cat++) {
GtkWidget *page = make_notebook_page(cat);
GtkWidget *label = gtk_label_new(cat->name);
gtk_notebook_append_page(GTK_NOTEBOOK(notebook), page, label);
}
}
void create_hardware_window(GtkApplication *app) {
window_hardware = gtk_window_new();
g_signal_connect(
window_hardware,
"close_request",
G_CALLBACK(window_hardware_close_request),
app
);
gtk_window_set_title(
GTK_WINDOW(window_hardware),
"ALSA Scarlett Supported Hardware"
);
GtkWidget *notebook = gtk_notebook_new();
gtk_window_set_child(GTK_WINDOW(window_hardware), notebook);
add_notebook_pages(notebook);
}

10
src/window-hardware.h Normal file
View File

@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
extern GtkWidget *window_hardware;
void create_hardware_window(GtkApplication *app);

29
src/window-helper.c Normal file
View File

@@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "window-helper.h"
gboolean window_startup_close_request(GtkWindow *w, gpointer data) {
struct alsa_card *card = data;
gtk_widget_activate_action(
GTK_WIDGET(card->window_main), "win.startup", NULL
);
return true;
}
GtkWidget *create_subwindow(
struct alsa_card *card,
const char *name,
GCallback close_callback
) {
char *title = g_strdup_printf("%s %s", card->name, name);
GtkWidget *w = gtk_window_new();
gtk_window_set_resizable(GTK_WINDOW(w), FALSE);
gtk_window_set_title(GTK_WINDOW(w), title);
g_signal_connect(w, "close_request", G_CALLBACK(close_callback), card);
g_free(title);
return w;
}

16
src/window-helper.h Normal file
View File

@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
#include "alsa.h"
gboolean window_startup_close_request(GtkWindow *w, gpointer data);
GtkWidget *create_subwindow(
struct alsa_card *card,
const char *name,
GCallback close_callback
);

97
src/window-iface.c Normal file
View File

@@ -0,0 +1,97 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <gtk/gtk.h>
#include "iface-mixer.h"
#include "iface-no-mixer.h"
#include "iface-none.h"
#include "iface-unknown.h"
#include "main.h"
#include "menu.h"
#include "window-iface.h"
#include "window-startup.h"
static GtkWidget *no_cards_window;
static int window_count;
void create_card_window(struct alsa_card *card) {
struct alsa_elem *msd_elem;
if (no_cards_window) {
gtk_window_destroy(GTK_WINDOW(no_cards_window));
no_cards_window = NULL;
}
window_count++;
int has_startup = true;
int has_mixer = true;
// Gen 2 or Gen 3 4i4+
if (get_elem_by_prefix(card->elems, "Mixer")) {
card->window_main_contents = create_iface_mixer_main(card);
// Gen 3 Solo or 2i2
} else if (get_elem_by_prefix(card->elems, "Phantom")) {
card->window_main_contents = create_iface_no_mixer_main(card);
has_mixer = false;
// Gen 3 in MSD Mode
} else if ((msd_elem = get_elem_by_name(card->elems, "MSD Mode Switch"))) {
card->window_main_contents = create_startup_controls(card);
has_startup = false;
has_mixer = false;
// Unknown
} else {
card->window_main_contents = create_iface_unknown_main();
has_startup = false;
has_mixer = false;
}
card->window_main = gtk_application_window_new(app);
gtk_window_set_resizable(GTK_WINDOW(card->window_main), FALSE);
gtk_window_set_title(GTK_WINDOW(card->window_main), card->name);
gtk_application_window_set_show_menubar(
GTK_APPLICATION_WINDOW(card->window_main), TRUE
);
add_window_action_map(GTK_WINDOW(card->window_main));
if (has_startup)
add_startup_action_map(card);
if (has_mixer)
add_mixer_action_map(card);
if (card->device)
add_load_save_action_map(card);
gtk_window_set_child(
GTK_WINDOW(card->window_main),
card->window_main_contents
);
gtk_widget_show(card->window_main);
}
void create_no_card_window(void) {
if (!window_count)
no_cards_window = create_window_iface_none(app);
}
void destroy_card_window(struct alsa_card *card) {
// remove the windows
gtk_window_destroy(GTK_WINDOW(card->window_main));
if (card->window_routing)
gtk_window_destroy(GTK_WINDOW(card->window_routing));
if (card->window_mixer)
gtk_window_destroy(GTK_WINDOW(card->window_mixer));
if (card->window_levels)
gtk_window_destroy(GTK_WINDOW(card->window_levels));
if (card->window_startup)
gtk_window_destroy(GTK_WINDOW(card->window_startup));
// disable the level meter timer source
if (card->meter_gsource_timer)
g_source_remove(card->meter_gsource_timer);
// if last window, display the "no card found" blank window
window_count--;
create_no_card_window();
}

10
src/window-iface.h Normal file
View File

@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "alsa.h"
void create_card_window(struct alsa_card *card);
void create_no_card_window(void);
void destroy_card_window(struct alsa_card *card);

112
src/window-levels.c Normal file
View File

@@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <gtk/gtk.h>
#include "gtkdial.h"
#include "gtkhelper.h"
#include "stringhelper.h"
#include "widget-gain.h"
#include "window-levels.h"
static int update_levels_controls(void *user_data) {
struct alsa_card *card = user_data;
struct alsa_elem *level_meter_elem = card->level_meter_elem;
int *values = alsa_get_elem_int_values(level_meter_elem);
int meter_num = 0;
// go through the port categories
for (int i = 0; i < PC_COUNT; i++) {
// go through the ports in that category
for (int j = 0; j < card->routing_out_count[i]; j++) {
GtkWidget *meter = card->meters[meter_num];
gtk_dial_set_value(GTK_DIAL(meter), values[meter_num]);
meter_num++;
}
}
free(values);
return 1;
}
static GtkWidget *add_count_label(GtkGrid *grid, int count) {
char s[20];
sprintf(s, "%d", count + 1);
GtkWidget *l = gtk_label_new(s);
gtk_grid_attach(grid, l, count + 1, 0, 1, 1);
return l;
}
static struct alsa_elem *get_level_meter_elem(struct alsa_card *card) {
GArray *elems = card->elems;
for (int i = 0; i < elems->len; i++) {
struct alsa_elem *elem = &g_array_index(elems, struct alsa_elem, i);
if (!elem->card)
continue;
if (strcmp(elem->name, "Level Meter") == 0)
return elem;
}
return NULL;
}
GtkWidget *create_levels_controls(struct alsa_card *card) {
GtkWidget *levels_top = gtk_grid_new();
GtkGrid *grid = GTK_GRID(levels_top);
gtk_widget_set_margin(GTK_WIDGET(grid), 5);
GtkWidget *count_labels[MAX_MUX_IN] = { NULL };
int meter_num = 0;
card->level_meter_elem = get_level_meter_elem(card);
if (!card->level_meter_elem) {
printf("Level Meter control not found\n");
return NULL;
}
// go through the port categories
for (int i = 0; i < PC_COUNT; i++) {
GtkWidget *l = gtk_label_new(port_category_names[i]);
gtk_widget_set_halign(l, GTK_ALIGN_END);
// add the label
gtk_grid_attach(GTK_GRID(grid), l, 0, i + 1, 1, 1);
// go through the ports in that category
for (int j = 0; j < card->routing_out_count[i]; j++) {
// add a count label if that hasn't already been done
if (!count_labels[j])
count_labels[j] = add_count_label(grid, j);
// create the meter widget and attach to the grid
GtkWidget *meter = gtk_dial_new_with_range(0, 4096, 1);
card->meters[meter_num++] = meter;
gtk_grid_attach(GTK_GRID(grid), meter, j + 1, i + 1, 1, 1);
}
}
int elem_count = card->level_meter_elem->count;
if (meter_num != elem_count) {
printf("meter_num is %d but elem count is %d\n", meter_num, elem_count);
return NULL;
}
card->level_meter_elem->count = elem_count;
card->meter_gsource_timer = g_timeout_add(50, update_levels_controls, card);
return levels_top;
}

8
src/window-levels.h Normal file
View File

@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "alsa.h"
GtkWidget *create_levels_controls(struct alsa_card *card);

126
src/window-mixer.c Normal file
View File

@@ -0,0 +1,126 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <gtk/gtk.h>
#include "gtkhelper.h"
#include "stringhelper.h"
#include "widget-gain.h"
#include "window-mixer.h"
static struct routing_dst *get_mixer_r_dst(
struct alsa_card *card,
int input_num
) {
for (int i = 0; i < card->routing_dsts->len; i++) {
struct routing_dst *r_dst = &g_array_index(
card->routing_dsts, struct routing_dst, i
);
if (r_dst->port_category != PC_MIX)
continue;
if (r_dst->elem->lr_num == input_num)
return r_dst;
}
return NULL;
}
GtkWidget *create_mixer_controls(struct alsa_card *card) {
GtkWidget *mixer_top = gtk_grid_new();
GArray *elems = card->elems;
gtk_widget_set_margin(mixer_top, 5);
gtk_grid_set_column_homogeneous(GTK_GRID(mixer_top), TRUE);
// create the Mix X labels on the left and right of the grid
for (int i = 0; i < card->routing_in_count[PC_MIX]; i++) {
char name[10];
snprintf(name, 10, "Mix %c", i + 'A');
GtkWidget *l_left = gtk_label_new(name);
gtk_grid_attach(
GTK_GRID(mixer_top), l_left,
0, i, 1, 1
);
GtkWidget *l_right = gtk_label_new(name);
gtk_grid_attach(
GTK_GRID(mixer_top), l_right,
card->routing_out_count[PC_MIX] + 1, i, 1, 1
);
}
// go through each element and create the mixer
for (int i = 0; i < elems->len; i++) {
struct alsa_elem *elem = &g_array_index(elems, struct alsa_elem, i);
// if no card entry, it's an empty slot
if (!elem->card)
continue;
// looking for "Mix X Input Y Playback Volume" elements
if (strncmp(elem->name, "Mix ", 4) != 0)
continue;
if (!strstr(elem->name, "Playback Volume"))
continue;
// extract the mix number and input number from the element name
int mix_num = elem->name[4] - 'A';
int input_num = get_num_from_string(elem->name) - 1;
if (mix_num >= MAX_MIX_OUT) {
printf("mix_num %d >= MAX_MIX_OUT %d\n", mix_num, MAX_MIX_OUT);
continue;
}
// create the gain control and attach to the grid
GtkWidget *w = make_gain_alsa_elem(elem);
gtk_grid_attach(GTK_GRID(mixer_top), w, input_num + 1, mix_num, 1, 1);
// look up the r_dst entry for the mixer input number
struct routing_dst *r_dst = get_mixer_r_dst(card, input_num + 1);
if (!r_dst) {
printf("missing mixer input %d\n", input_num);
continue;
}
// lookup the label for the mixer input
GtkWidget *l = r_dst->mixer_label;
// if the label doesn't already exist, create it and attach it to
// the bottom of the grid
if (!l) {
l = r_dst->mixer_label = gtk_label_new("");
gtk_grid_attach(
GTK_GRID(mixer_top), l,
input_num, card->routing_in_count[PC_MIX] + input_num % 2, 3, 1
);
}
}
update_mixer_labels(card);
return mixer_top;
}
void update_mixer_labels(struct alsa_card *card) {
for (int i = 0; i < card->routing_dsts->len; i++) {
struct routing_dst *r_dst = &g_array_index(
card->routing_dsts, struct routing_dst, i
);
if (r_dst->port_category != PC_MIX)
continue;
struct alsa_elem *elem = r_dst->elem;
int routing_src_idx = alsa_get_elem_value(elem);
struct routing_src *r_src = &g_array_index(
card->routing_srcs, struct routing_src, routing_src_idx
);
if (r_dst->mixer_label)
gtk_label_set_text(GTK_LABEL(r_dst->mixer_label), r_src->name);
}
}

9
src/window-mixer.h Normal file
View File

@@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "alsa.h"
GtkWidget *create_mixer_controls(struct alsa_card *card);
void update_mixer_labels(struct alsa_card *card);

916
src/window-routing.c Normal file
View File

@@ -0,0 +1,916 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "gtkhelper.h"
#include "iface-mixer.h"
#include "routing-drag-line.h"
#include "routing-lines.h"
#include "stringhelper.h"
#include "widget-boolean.h"
#include "window-mixer.h"
#include "window-routing.h"
static void get_routing_srcs(struct alsa_card *card) {
struct alsa_elem *elem = card->sample_capture_elem;
int count = alsa_get_item_count(elem);
card->routing_srcs = g_array_new(
FALSE, TRUE, sizeof(struct routing_src)
);
g_array_set_size(card->routing_srcs, count);
for (int i = 0; i < count; i++) {
char *name = alsa_get_item_name(elem, i);
struct routing_src *r = &g_array_index(
card->routing_srcs, struct routing_src, i
);
r->card = card;
r->id = i;
if (strncmp(name, "Mix", 3) == 0)
r->port_category = PC_MIX;
else if (strncmp(name, "PCM", 3) == 0)
r->port_category = PC_PCM;
else
r->port_category = PC_HW;
r->name = name;
r->lr_num =
r->port_category == PC_MIX
? name[4] - 'A' + 1
: get_num_from_string(name);
r->port_num = card->routing_in_count[r->port_category]++;
}
assert(card->routing_in_count[PC_MIX] <= MAX_MIX_OUT);
}
static void get_routing_dsts(struct alsa_card *card) {
GArray *elems = card->elems;
int count = 0;
// count and label routing dsts
for (int i = 0; i < elems->len; i++) {
struct alsa_elem *elem = &g_array_index(elems, struct alsa_elem, i);
if (!elem->card)
continue;
if (!is_elem_routing_dst(elem))
continue;
int i = get_num_from_string(elem->name);
if (i < 0)
continue;
elem->lr_num = i;
count++;
}
// create an array of routing dsts pointing to those elements
card->routing_dsts = g_array_new(
FALSE, TRUE, sizeof(struct routing_dst)
);
g_array_set_size(card->routing_dsts, count);
// count through card->rounting_dsts
int j = 0;
for (int i = 0; i < elems->len; i++) {
struct alsa_elem *elem = &g_array_index(elems, struct alsa_elem, i);
if (!elem->lr_num)
continue;
struct routing_dst *r = &g_array_index(
card->routing_dsts, struct routing_dst, j
);
r->idx = j;
j++;
r->elem = elem;
if (strncmp(elem->name, "Mixer Input", 11) == 0) {
r->port_category = PC_MIX;
} else if (strncmp(elem->name, "PCM", 3) == 0) {
r->port_category = PC_PCM;
} else if (strstr(elem->name, "Playback Enum")) {
r->port_category = PC_HW;
} else {
printf("unknown mixer routing elem %s\n", elem->name);
continue;
}
r->port_num = card->routing_out_count[r->port_category]++;
}
assert(j == count);
}
static void routing_grid_label(char *s, GtkGrid *g) {
GtkWidget *l = gtk_label_new(s);
gtk_grid_attach(g, l, 0, 0, 1, 1);
}
// clear all the routing destinations
static void routing_preset_clear(struct alsa_card *card) {
for (int i = 0; i < card->routing_dsts->len; i++) {
struct routing_dst *r_dst = &g_array_index(
card->routing_dsts, struct routing_dst, i
);
alsa_set_elem_value(r_dst->elem, 0);
}
}
static void routing_preset_link(
struct alsa_card *card,
int src_port_category,
int src_mod,
int dst_port_category
) {
// find the first src port with the selected port category
int start_src_idx;
for (start_src_idx = 1;
start_src_idx < card->routing_srcs->len;
start_src_idx++) {
struct routing_src *r_src = &g_array_index(
card->routing_srcs, struct routing_src, start_src_idx
);
if (r_src->port_category == src_port_category)
break;
}
// find the first dst port with the selected port category
int dst_idx;
for (dst_idx = 0;
dst_idx < card->routing_dsts->len;
dst_idx++) {
struct routing_dst *r_dst = &g_array_index(
card->routing_dsts, struct routing_dst, dst_idx
);
if (r_dst->port_category == dst_port_category)
break;
}
// start assigning
int src_idx = start_src_idx;
int src_count = 0;
while (src_idx < card->routing_srcs->len &&
dst_idx < card->routing_dsts->len) {
// stop if no more of the selected src port category
struct routing_src *r_src = &g_array_index(
card->routing_srcs, struct routing_src, src_idx
);
if (r_src->port_category != src_port_category)
break;
// stop if no more of the selected dst port category
struct routing_dst *r_dst = &g_array_index(
card->routing_dsts, struct routing_dst, dst_idx
);
if (r_dst->port_category != dst_port_category)
break;
// do the assignment
alsa_set_elem_value(r_dst->elem, r_src->id);
// get the next index
src_idx++;
src_count++;
dst_idx++;
if (src_count == src_mod) {
src_idx = start_src_idx;
src_count = 0;
}
}
}
static void routing_preset_direct(struct alsa_card *card) {
routing_preset_link(card, PC_HW, 0, PC_PCM);
routing_preset_link(card, PC_PCM, 0, PC_HW);
}
static void routing_preset_preamp(struct alsa_card *card) {
routing_preset_link(card, PC_HW, 0, PC_HW);
}
static void routing_preset_stereo_out(struct alsa_card *card) {
routing_preset_link(card, PC_PCM, 2, PC_HW);
}
static void routing_preset(
GSimpleAction *action,
GVariant *value,
struct alsa_card *card
) {
const char *s = g_variant_get_string(value, NULL);
if (strcmp(s, "clear") == 0) {
routing_preset_clear(card);
} else if (strcmp(s, "direct") == 0) {
routing_preset_direct(card);
} else if (strcmp(s, "preamp") == 0) {
routing_preset_preamp(card);
} else if (strcmp(s, "stereo_out") == 0) {
routing_preset_stereo_out(card);
}
}
static GtkWidget *make_preset_menu_button(struct alsa_card *card) {
GMenu *menu = g_menu_new();
g_menu_append(menu, "Clear", "routing.preset('clear')");
g_menu_append(menu, "Direct", "routing.preset('direct')");
g_menu_append(menu, "Preamp", "routing.preset('preamp')");
g_menu_append(menu, "Stereo Out", "routing.preset('stereo_out')");
GtkWidget *button = gtk_menu_button_new();
gtk_menu_button_set_label(GTK_MENU_BUTTON(button), "Presets");
gtk_menu_button_set_menu_model(
GTK_MENU_BUTTON(button),
G_MENU_MODEL(menu)
);
GSimpleActionGroup *action_group = g_simple_action_group_new();
GSimpleAction *action = g_simple_action_new_stateful(
"preset", G_VARIANT_TYPE_STRING, NULL
);
g_action_map_add_action(G_ACTION_MAP(action_group), G_ACTION(action));
g_signal_connect(
action, "activate", G_CALLBACK(routing_preset), card
);
gtk_widget_insert_action_group(
button, "routing", G_ACTION_GROUP(action_group)
);
return button;
}
static void create_routing_grid(struct alsa_card *card) {
GtkWidget *routing_grid = card->routing_grid = gtk_grid_new();
GtkWidget *preset_menu_button = make_preset_menu_button(card);
gtk_grid_attach(
GTK_GRID(routing_grid), preset_menu_button, 0, 0, 1, 1
);
card->routing_hw_in_grid = gtk_grid_new();
card->routing_pcm_in_grid = gtk_grid_new();
card->routing_pcm_out_grid = gtk_grid_new();
card->routing_hw_out_grid = gtk_grid_new();
card->routing_mixer_in_grid = gtk_grid_new();
card->routing_mixer_out_grid = gtk_grid_new();
gtk_grid_attach(
GTK_GRID(routing_grid), card->routing_hw_in_grid, 0, 1, 1, 1
);
gtk_grid_attach(
GTK_GRID(routing_grid), card->routing_pcm_in_grid, 0, 2, 1, 1
);
gtk_grid_attach(
GTK_GRID(routing_grid), card->routing_pcm_out_grid, 2, 1, 1, 1
);
gtk_grid_attach(
GTK_GRID(routing_grid), card->routing_hw_out_grid, 2, 2, 1, 1
);
gtk_grid_attach(
GTK_GRID(routing_grid), card->routing_mixer_in_grid, 1, 0, 1, 1
);
gtk_grid_attach(
GTK_GRID(routing_grid), card->routing_mixer_out_grid, 1, 3, 1, 1
);
gtk_widget_set_margin(routing_grid, 10);
gtk_grid_set_spacing(GTK_GRID(routing_grid), 10);
gtk_grid_set_spacing(GTK_GRID(card->routing_hw_in_grid), 2);
gtk_grid_set_spacing(GTK_GRID(card->routing_pcm_in_grid), 2);
gtk_grid_set_spacing(GTK_GRID(card->routing_pcm_out_grid), 2);
gtk_grid_set_spacing(GTK_GRID(card->routing_hw_out_grid), 2);
gtk_grid_set_spacing(GTK_GRID(card->routing_mixer_in_grid), 2);
gtk_grid_set_spacing(GTK_GRID(card->routing_mixer_out_grid), 10);
gtk_grid_set_row_spacing(GTK_GRID(card->routing_mixer_out_grid), 2);
gtk_grid_set_column_spacing(GTK_GRID(card->routing_mixer_out_grid), 10);
gtk_widget_set_vexpand(card->routing_hw_in_grid, TRUE);
gtk_widget_set_vexpand(card->routing_pcm_in_grid, TRUE);
gtk_widget_set_vexpand(card->routing_pcm_out_grid, TRUE);
gtk_widget_set_vexpand(card->routing_hw_out_grid, TRUE);
gtk_widget_set_hexpand(card->routing_mixer_in_grid, TRUE);
gtk_widget_set_hexpand(card->routing_mixer_out_grid, TRUE);
gtk_widget_set_align(
card->routing_hw_in_grid, GTK_ALIGN_END, GTK_ALIGN_CENTER
);
gtk_widget_set_align(
card->routing_pcm_in_grid, GTK_ALIGN_END, GTK_ALIGN_CENTER
);
gtk_widget_set_align(
card->routing_hw_out_grid, GTK_ALIGN_START, GTK_ALIGN_CENTER
);
gtk_widget_set_align(
card->routing_pcm_out_grid, GTK_ALIGN_START, GTK_ALIGN_CENTER
);
gtk_widget_set_align(
card->routing_mixer_in_grid, GTK_ALIGN_CENTER, GTK_ALIGN_END
);
gtk_widget_set_align(
card->routing_mixer_out_grid, GTK_ALIGN_CENTER, GTK_ALIGN_START
);
routing_grid_label("Hardware Inputs", GTK_GRID(card->routing_hw_in_grid));
routing_grid_label("Hardware Outputs", GTK_GRID(card->routing_hw_out_grid));
routing_grid_label("PCM Outputs", GTK_GRID(card->routing_pcm_in_grid));
routing_grid_label("PCM Inputs", GTK_GRID(card->routing_pcm_out_grid));
GtkWidget *src_label = gtk_label_new("\nSources →");
gtk_label_set_justify(GTK_LABEL(src_label), GTK_JUSTIFY_CENTER);
gtk_grid_attach(GTK_GRID(routing_grid), src_label, 0, 3, 1, 1);
GtkWidget *dst_label = gtk_label_new("← Destinations\n");
gtk_label_set_justify(GTK_LABEL(dst_label), GTK_JUSTIFY_CENTER);
gtk_grid_attach(GTK_GRID(routing_grid), dst_label, 2, 0, 1, 1);
}
static GtkWidget *make_socket_widget(void) {
return gtk_picture_new_for_resource(
"/vu/b4/alsa-scarlett-gui/icons/socket.svg"
);
}
// something was dropped on a routing source
static gboolean dropped_on_src(
GtkDropTarget *dest,
const GValue *value,
double x,
double y,
gpointer data
) {
struct routing_src *src = data;
int dst_id = g_value_get_int(value);
// don't accept src -> src drops
if (!(dst_id & 0x8000))
return FALSE;
// convert the int to a r_dst_idx
int r_dst_idx = dst_id & ~0x8000;
// check the index is in bounds
GArray *r_dsts = src->card->routing_dsts;
if (r_dst_idx < 0 || r_dst_idx >= r_dsts->len)
return FALSE;
struct routing_dst *r_dst = &g_array_index(
r_dsts, struct routing_dst, r_dst_idx
);
alsa_set_elem_value(r_dst->elem, src->id);
return TRUE;
}
// something was dropped on a routing destination
static gboolean dropped_on_dst(
GtkDropTarget *dest,
const GValue *value,
double x,
double y,
gpointer data
) {
struct alsa_elem *elem = data;
int src_id = g_value_get_int(value);
// don't accept dst -> dst drops
if (src_id & 0x8000)
return FALSE;
alsa_set_elem_value(elem, src_id);
return TRUE;
}
static void src_routing_clicked(
GtkWidget *widget,
int n_press,
double x,
double y,
struct routing_src *r_src
) {
struct alsa_card *card = r_src->card;
// go through all the routing destinations
for (int i = 0; i < card->routing_dsts->len; i++) {
struct routing_dst *r_dst = &g_array_index(
card->routing_dsts, struct routing_dst, i
);
int r_src_idx = alsa_get_elem_value(r_dst->elem);
if (r_src_idx == r_src->id)
alsa_set_elem_value(r_dst->elem, 0);
}
}
static void dst_routing_clicked(
GtkWidget *widget,
int n_press,
double x,
double y,
struct alsa_elem *elem
) {
alsa_set_elem_value(elem, 0);
}
static void src_drag_begin(
GtkDragSource *source,
GdkDrag *drag,
gpointer user_data
) {
struct routing_src *r_src = user_data;
struct alsa_card *card = r_src->card;
card->drag_type = DRAG_TYPE_SRC;
card->src_drag = r_src;
}
static void dst_drag_begin(
GtkDragSource *source,
GdkDrag *drag,
gpointer user_data
) {
struct routing_dst *r_dst = user_data;
struct alsa_card *card = r_dst->elem->card;
card->drag_type = DRAG_TYPE_DST;
card->dst_drag = r_dst;
}
static void src_drag_end(
GtkDragSource *source,
GdkDrag *drag,
gboolean delete_data,
gpointer user_data
) {
struct routing_src *r_src = user_data;
struct alsa_card *card = r_src->card;
card->drag_type = DRAG_TYPE_NONE;
card->src_drag = NULL;
gtk_widget_queue_draw(card->drag_line);
gtk_widget_queue_draw(card->routing_lines);
}
static void dst_drag_end(
GtkDragSource *source,
GdkDrag *drag,
gboolean delete_data,
gpointer user_data
) {
struct routing_dst *r_dst = user_data;
struct alsa_card *card = r_dst->elem->card;
card->drag_type = DRAG_TYPE_NONE;
card->dst_drag = NULL;
gtk_widget_queue_draw(card->drag_line);
gtk_widget_queue_draw(card->routing_lines);
}
static gboolean src_drop_accept(
GtkDropTarget *source,
GdkDrop *drop,
gpointer user_data
) {
struct routing_src *r_src = user_data;
struct alsa_card *card = r_src->card;
return card->drag_type == DRAG_TYPE_DST;
}
static gboolean dst_drop_accept(
GtkDropTarget *source,
GdkDrop *drop,
gpointer user_data
) {
struct routing_dst *r_dst = user_data;
struct alsa_card *card = r_dst->elem->card;
return card->drag_type == DRAG_TYPE_SRC;
}
static GdkDragAction src_drop_enter(
GtkDropTarget *dest,
gdouble x,
gdouble y,
gpointer user_data
) {
struct routing_src *r_src = user_data;
struct alsa_card *card = r_src->card;
if (card->drag_type != DRAG_TYPE_DST)
return 0;
card->src_drag = r_src;
return GDK_ACTION_COPY;
}
static GdkDragAction dst_drop_enter(
GtkDropTarget *dest,
gdouble x,
gdouble y,
gpointer user_data
) {
struct routing_dst *r_dst = user_data;
struct alsa_card *card = r_dst->elem->card;
if (card->drag_type != DRAG_TYPE_SRC)
return 0;
card->dst_drag = r_dst;
return GDK_ACTION_COPY;
}
static void src_drop_leave(
GtkDropTarget *dest,
gpointer user_data
) {
struct routing_src *r_src = user_data;
struct alsa_card *card = r_src->card;
if (card->drag_type != DRAG_TYPE_DST)
return;
card->src_drag = NULL;
}
static void dst_drop_leave(
GtkDropTarget *dest,
gpointer user_data
) {
struct routing_dst *r_dst = user_data;
struct alsa_card *card = r_dst->elem->card;
if (card->drag_type != DRAG_TYPE_SRC)
return;
card->dst_drag = NULL;
}
static void setup_src_drag(struct routing_src *r_src) {
GtkWidget *box = r_src->widget;
// handle drags on the box
GtkDragSource *source = gtk_drag_source_new();
g_signal_connect(source, "drag-begin", G_CALLBACK(src_drag_begin), r_src);
g_signal_connect(source, "drag-end", G_CALLBACK(src_drag_end), r_src);
// set the box as a drag source
gtk_drag_source_set_actions(source, GDK_ACTION_COPY);
gtk_widget_add_controller(box, GTK_EVENT_CONTROLLER(source));
// set the content
GdkContentProvider *content = gdk_content_provider_new_typed(
G_TYPE_INT, r_src->id
);
gtk_drag_source_set_content(source, content);
g_object_unref(content);
// set a blank icon
GdkPaintable *paintable = gdk_paintable_new_empty(1, 1);
gtk_drag_source_set_icon(source, paintable, 0, 0);
g_object_unref(paintable);
// set the box as a drop target
GtkDropTarget *dest = gtk_drop_target_new(G_TYPE_INT, GDK_ACTION_COPY);
gtk_widget_add_controller(box, GTK_EVENT_CONTROLLER(dest));
g_signal_connect(dest, "drop", G_CALLBACK(dropped_on_src), r_src);
g_signal_connect(dest, "accept", G_CALLBACK(src_drop_accept), r_src);
g_signal_connect(dest, "enter", G_CALLBACK(src_drop_enter), r_src);
g_signal_connect(dest, "leave", G_CALLBACK(src_drop_leave), r_src);
}
static void setup_dst_drag(struct routing_dst *r_dst) {
struct alsa_elem *elem = r_dst->elem;
GtkWidget *box = elem->widget;
// handle drags on the box
GtkDragSource *source = gtk_drag_source_new();
g_signal_connect(source, "drag-begin", G_CALLBACK(dst_drag_begin), r_dst);
g_signal_connect(source, "drag-end", G_CALLBACK(dst_drag_end), r_dst);
// set the box as a drag source
gtk_drag_source_set_actions(source, GDK_ACTION_COPY);
gtk_widget_add_controller(box, GTK_EVENT_CONTROLLER(source));
// set the content
// 0x8000 flag indicates alsa_elem numid value
GdkContentProvider *content = gdk_content_provider_new_typed(
G_TYPE_INT, 0x8000 | r_dst->idx
);
gtk_drag_source_set_content(source, content);
g_object_unref(content);
// set a blank icon
GdkPaintable *paintable = gdk_paintable_new_empty(1, 1);
gtk_drag_source_set_icon(source, paintable, 0, 0);
g_object_unref(paintable);
// set the box as a drop target
GtkDropTarget *dest = gtk_drop_target_new(G_TYPE_INT, GDK_ACTION_COPY);
gtk_widget_add_controller(box, GTK_EVENT_CONTROLLER(dest));
g_signal_connect(dest, "drop", G_CALLBACK(dropped_on_dst), elem);
g_signal_connect(dest, "accept", G_CALLBACK(dst_drop_accept), r_dst);
g_signal_connect(dest, "enter", G_CALLBACK(dst_drop_enter), r_dst);
g_signal_connect(dest, "leave", G_CALLBACK(dst_drop_leave), r_dst);
}
static void make_src_routing_widget(
struct alsa_card *card,
struct routing_src *r_src,
char *name,
GtkOrientation orientation
) {
// create a box, a "socket", and a label
GtkWidget *box = r_src->widget = gtk_box_new(orientation, 5);
GtkWidget *socket = r_src->widget2 = make_socket_widget();
// create label for mixer inputs (length > 1) and mixer outputs if
// not talkback (talkback has a button outside the box instead of a
// label inside the box)
if (strlen(name) > 1 || !card->has_talkback) {
GtkWidget *label = gtk_label_new(name);
gtk_box_append(GTK_BOX(box), label);
gtk_widget_add_class(box, "route-label");
}
if (orientation == GTK_ORIENTATION_HORIZONTAL) {
gtk_box_append(GTK_BOX(box), socket);
gtk_widget_set_halign(box, GTK_ALIGN_END);
} else {
gtk_box_prepend(GTK_BOX(box), socket);
gtk_widget_set_margin_start(box, 5);
gtk_widget_set_margin_end(box, 5);
}
// handle clicks on the box
GtkGesture *gesture = gtk_gesture_click_new();
g_signal_connect(
gesture, "released", G_CALLBACK(src_routing_clicked), r_src
);
gtk_widget_add_controller(
GTK_WIDGET(box), GTK_EVENT_CONTROLLER(gesture)
);
// handle dragging to or from the box
setup_src_drag(r_src);
}
static GtkWidget *make_talkback_mix_widget(
struct alsa_card *card,
struct routing_src *r_src,
char *name
) {
char talkback_elem_name[80];
snprintf(talkback_elem_name, 80, "Talkback Mix %s Playback Switch", name);
struct alsa_elem *talkback_elem =
get_elem_by_name(card->elems, talkback_elem_name);
if (!talkback_elem)
return NULL;
return make_boolean_alsa_elem(talkback_elem, name, name);
}
static void make_dst_routing_widget(
struct routing_dst *r_dst,
char *name,
GtkOrientation orientation
) {
struct alsa_elem *elem = r_dst->elem;
// create a box, a "socket", and a label
GtkWidget *box = elem->widget = gtk_box_new(orientation, 5);
gtk_widget_add_class(box, "route-label");
GtkWidget *label = gtk_label_new(name);
gtk_box_append(GTK_BOX(box), label);
GtkWidget *socket = elem->widget2 = make_socket_widget();
if (orientation == GTK_ORIENTATION_VERTICAL) {
gtk_box_append(GTK_BOX(box), socket);
gtk_widget_set_margin_start(box, 5);
gtk_widget_set_margin_end(box, 5);
} else {
gtk_box_prepend(GTK_BOX(box), socket);
gtk_widget_set_halign(box, GTK_ALIGN_START);
}
// handle clicks on the box
GtkGesture *gesture = gtk_gesture_click_new();
g_signal_connect(
gesture, "released", G_CALLBACK(dst_routing_clicked), elem
);
gtk_widget_add_controller(
GTK_WIDGET(box), GTK_EVENT_CONTROLLER(gesture)
);
// handle dragging to or from the box
setup_dst_drag(r_dst);
}
static void routing_updated(struct alsa_elem *elem) {
struct alsa_card *card = elem->card;
update_mixer_labels(card);
gtk_widget_queue_draw(card->routing_lines);
}
static void make_routing_alsa_elem(struct routing_dst *r_dst) {
struct alsa_elem *elem = r_dst->elem;
struct alsa_card *card = elem->card;
// "Mixer Input X Capture Enum" controls (Mixer Inputs) go along
// the top, in card->routing_mixer_in_grid
if (r_dst->port_category == PC_MIX) {
char name[10];
snprintf(name, 10, "%d", elem->lr_num);
make_dst_routing_widget(r_dst, name, GTK_ORIENTATION_VERTICAL);
gtk_grid_attach(
GTK_GRID(card->routing_mixer_in_grid), elem->widget,
r_dst->port_num + 1, 0, 1, 1
);
// "PCM X Capture Enum" controls (PCM Inputs) go along the right,
// in card->routing_pcm_out_grid
} else if (r_dst->port_category == PC_PCM) {
char *name = strdup(elem->name);
char *name_end = strchr(name, ' ');
// in case the number is zero-padded
if (name_end)
snprintf(name_end, strlen(name_end) + 1, " %d", elem->lr_num);
make_dst_routing_widget(r_dst, name, GTK_ORIENTATION_HORIZONTAL);
free(name);
gtk_grid_attach(
GTK_GRID(card->routing_pcm_out_grid), elem->widget,
0, r_dst->port_num + 1, 1, 1
);
// "* Output X Playback Enum" controls go along the right, in
// card->routing_hw_out_grid
} else if (r_dst->port_category == PC_HW) {
// Convert "Analogue 01 Output Playback Enum" to "Analogue 1"
char *name = strdup(elem->name);
char *name_end = strstr(name, " Output ");
// in case the number is zero-padded
if (name_end)
snprintf(name_end, strlen(name_end) + 1, " %d", elem->lr_num);
make_dst_routing_widget(r_dst, name, GTK_ORIENTATION_HORIZONTAL);
free(name);
gtk_grid_attach(
GTK_GRID(card->routing_hw_out_grid), elem->widget,
0, r_dst->port_num + 1, 1, 1
);
} else {
printf("invalid port category %d\n", r_dst->port_category);
}
elem->widget_callback = routing_updated;
}
static void add_routing_widgets(
struct alsa_card *card,
GtkWidget *routing_overlay
) {
GArray *r_dsts = card->routing_dsts;
// go through each routing destination and create its control
for (int i = 0; i < r_dsts->len; i++) {
struct routing_dst *r_dst = &g_array_index(r_dsts, struct routing_dst, i);
make_routing_alsa_elem(r_dst);
}
if (!card->routing_out_count[PC_MIX]) {
printf("no mixer inputs??\n");
return;
}
GtkWidget *l_mixer_in = gtk_label_new("Mixer\nInputs");
gtk_label_set_justify(GTK_LABEL(l_mixer_in), GTK_JUSTIFY_CENTER);
gtk_grid_attach(
GTK_GRID(card->routing_mixer_in_grid), l_mixer_in,
0, 0, 1, 1
);
// start at 1 to skip the "Off" input
for (int i = 1; i < card->routing_srcs->len; i++) {
struct routing_src *r_src = &g_array_index(
card->routing_srcs, struct routing_src, i
);
if (r_src->port_category == PC_MIX) {
make_src_routing_widget(
card, r_src, r_src->name + 4, GTK_ORIENTATION_VERTICAL
);
gtk_grid_attach(
GTK_GRID(card->routing_mixer_out_grid), r_src->widget,
r_src->port_num + 1, 0, 1, 1
);
if (card->has_talkback) {
GtkWidget *w = make_talkback_mix_widget(card, r_src, r_src->name + 4);
gtk_grid_attach(
GTK_GRID(card->routing_mixer_out_grid), w,
r_src->port_num + 1, 1, 1, 1
);
}
} else if (r_src->port_category == PC_PCM) {
make_src_routing_widget(
card, r_src, r_src->name, GTK_ORIENTATION_HORIZONTAL
);
gtk_grid_attach(
GTK_GRID(card->routing_pcm_in_grid), r_src->widget,
0, r_src->port_num + 1, 1, 1
);
} else if (r_src->port_category == PC_HW) {
make_src_routing_widget(
card, r_src, r_src->name, GTK_ORIENTATION_HORIZONTAL
);
gtk_grid_attach(
GTK_GRID(card->routing_hw_in_grid), r_src->widget,
0, r_src->port_num + 1, 1, 1
);
} else {
printf("invalid port category %d\n", r_src->port_category);
}
}
GtkWidget *l_mixer_out = gtk_label_new(
card->has_talkback ? "Mixer Outputs" : "Mixer\nOutputs"
);
gtk_label_set_justify(GTK_LABEL(l_mixer_out), GTK_JUSTIFY_CENTER);
gtk_grid_attach(
GTK_GRID(card->routing_mixer_out_grid), l_mixer_out,
0, 0, 1, 1
);
if (card->has_talkback) {
GtkWidget *l_talkback = gtk_label_new("Talkback");
gtk_widget_set_tooltip_text(
l_talkback,
"Mixer Outputs with Talkback enabled will have the level of "
"Mixer Input 25 internally raised and lowered when the "
"Talkback control is turned On and Off."
);
gtk_grid_attach(
GTK_GRID(card->routing_mixer_out_grid), l_talkback,
0, 1, 1, 1
);
}
card->routing_lines = gtk_drawing_area_new();
gtk_widget_set_can_target(card->routing_lines, FALSE);
gtk_drawing_area_set_draw_func(
GTK_DRAWING_AREA(card->routing_lines), draw_routing_lines, card, NULL
);
gtk_overlay_add_overlay(
GTK_OVERLAY(routing_overlay), card->routing_lines
);
update_mixer_labels(card);
}
GtkWidget *create_routing_controls(struct alsa_card *card) {
// check that we can find a routing control
card->sample_capture_elem =
get_elem_by_name(card->elems, "PCM 01 Capture Enum");
if (!card->sample_capture_elem) {
printf("couldn't find PCM 01 Capture Enum control; can't create GUI\n");
return NULL;
}
get_routing_srcs(card);
get_routing_dsts(card);
create_routing_grid(card);
GtkWidget *routing_overlay = gtk_overlay_new();
gtk_overlay_set_child(GTK_OVERLAY(routing_overlay), card->routing_grid);
add_routing_widgets(card, routing_overlay);
add_drop_controller_motion(card, routing_overlay);
return routing_overlay;
}

10
src/window-routing.h Normal file
View File

@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
#include "alsa.h"
GtkWidget *create_routing_controls(struct alsa_card *card);

168
src/window-startup.c Normal file
View File

@@ -0,0 +1,168 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "gtkhelper.h"
#include "widget-boolean.h"
#include "window-startup.h"
static GtkWidget *small_label(char *text) {
GtkWidget *w = gtk_label_new(NULL);
char *s = g_strdup_printf("<b>%s</b>", text);
gtk_label_set_markup(GTK_LABEL(w), s);
free(s);
gtk_widget_set_valign(w, GTK_ALIGN_START);
return w;
}
static GtkWidget *big_label(char *text) {
GtkWidget *w = gtk_label_new(text);
gtk_widget_set_halign(w, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(w), true);
gtk_label_set_max_width_chars(GTK_LABEL(w), 60);
return w;
}
static void add_sep(GtkWidget *grid, int *grid_y) {
if (!*grid_y)
return;
GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
gtk_widget_set_margin(sep, 20);
gtk_grid_attach(GTK_GRID(grid), sep, 0, (*grid_y)++, 3, 1);
}
static void add_standalone_control(
GArray *elems,
GtkWidget *grid,
int *grid_y
) {
struct alsa_elem *standalone = get_elem_by_name(elems, "Standalone Switch");
if (!standalone)
return;
add_sep(grid, grid_y);
GtkWidget *w;
w = small_label("Standalone");
gtk_grid_attach(GTK_GRID(grid), w, 0, *grid_y, 1, 1);
w = make_boolean_alsa_elem(standalone, "Disabled", "Enabled");
gtk_grid_attach(GTK_GRID(grid), w, 0, *grid_y + 1, 1, 1);
w = big_label(
"When Standalone mode is enabled, the interface will continue to "
"route audio as per the previous routing and mixer settings "
"after it has been disconnected from a computer. By configuring "
"the routing between the hardware and mixer inputs and outputs "
"appropriately, the interface can act as a standalone preamp or "
"mixer."
);
gtk_grid_attach(GTK_GRID(grid), w, 1, *grid_y, 1, 2);
*grid_y += 2;
}
static void add_phantom_persistence_control(
GArray *elems,
GtkWidget *grid,
int *grid_y
) {
struct alsa_elem *phantom = get_elem_by_name(
elems, "Phantom Power Persistence Capture Switch"
);
if (!phantom)
return;
add_sep(grid, grid_y);
GtkWidget *w;
w = small_label("Phantom Power Persistance");
gtk_grid_attach(GTK_GRID(grid), w, 0, *grid_y, 1, 1);
w = make_boolean_alsa_elem(phantom, "Disabled", "Enabled");
gtk_grid_attach(GTK_GRID(grid), w, 0, *grid_y + 1, 1, 1);
w = big_label(
"When Phantom Power Persistence is enabled, the interface will "
"restore the previous Phantom Power/48V setting when the "
"interface is turned on. For the safety of microphones which can "
"be damaged by phantom power, the interface defaults to having "
"phantom power disabled when it is turned on."
);
gtk_grid_attach(GTK_GRID(grid), w, 1, *grid_y, 1, 2);
*grid_y += 2;
}
static void add_msd_control(
GArray *elems,
GtkWidget *grid,
int *grid_y
) {
struct alsa_elem *msd = get_elem_by_name(
elems, "MSD Mode Switch"
);
if (!msd)
return;
add_sep(grid, grid_y);
GtkWidget *w;
w = small_label("MSD (Mass Storage Device) Mode");
gtk_grid_attach(GTK_GRID(grid), w, 0, *grid_y, 1, 1);
w = make_boolean_alsa_elem(msd, "Disabled", "Enabled");
gtk_grid_attach(GTK_GRID(grid), w, 0, *grid_y + 1, 1, 1);
w = big_label(
"When MSD Mode is enabled (as it is from the factory), the "
"interface has reduced functionality. Youll want to have this "
"disabled. On the other hand, when MSD Mode is enabled, the "
"interface presents itself as a Mass Storage Device (like a USB "
"stick), containing a link to the Focusrite web site encouraging "
"you to register your product and download the proprietary "
"drivers which cant be used on Linux."
);
gtk_grid_attach(GTK_GRID(grid), w, 1, *grid_y, 1, 2);
*grid_y += 2;
}
static void add_no_startup_controls_msg(GtkWidget *grid) {
GtkWidget *w = big_label(
"It appears that there are no startup controls. You probably "
"need to upgrade your kernel to see something here."
);
gtk_grid_attach(GTK_GRID(grid), w, 0, 0, 1, 1);
}
GtkWidget *create_startup_controls(struct alsa_card *card) {
GArray *elems = card->elems;
int grid_y = 0;
GtkWidget *grid = gtk_grid_new();
gtk_widget_set_margin(grid, 20);
gtk_grid_set_column_spacing(GTK_GRID(grid), 20);
add_standalone_control(elems, grid, &grid_y);
add_phantom_persistence_control(elems, grid, &grid_y);
add_msd_control(elems, grid, &grid_y);
if (!grid_y)
add_no_startup_controls_msg(grid);
return grid;
}

8
src/window-startup.h Normal file
View File

@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "alsa.h"
GtkWidget *create_startup_controls(struct alsa_card *card);