Initial revision
This commit is contained in:
54
src/Makefile
Normal file
54
src/Makefile
Normal 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
32
src/about.c
Normal 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
12
src/about.h
Normal 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
|
||||
);
|
||||
16
src/alsa-scarlett-gui-resources.xml
Normal file
16
src/alsa-scarlett-gui-resources.xml
Normal 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>
|
||||
4
src/alsa-scarlett-gui.css
Normal file
4
src/alsa-scarlett-gui.css
Normal 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
400
src/alsa-sim.c
Normal 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
8
src/alsa-sim.h
Normal 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
584
src/alsa.c
Normal 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
210
src/alsa.h
Normal 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
13
src/const.h
Normal 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
22
src/error.c
Normal 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
8
src/error.h
Normal 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
203
src/file.c
Normal 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
8
src/file.h
Normal 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
840
src/gtkdial.c
Normal 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 = >k_dial_set_property;
|
||||
g_class->get_property = >k_dial_get_property;
|
||||
g_class->dispose = >k_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 = >k_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
105
src/gtkdial.h
Normal 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
31
src/gtkhelper.c
Normal 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
12
src/gtkhelper.h
Normal 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
471
src/iface-mixer.c
Normal 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 aren’t 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 aren’t 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
8
src/iface-mixer.h
Normal 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
116
src/iface-no-mixer.c
Normal 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
8
src/iface-no-mixer.h
Normal 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
30
src/iface-none.c
Normal 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
8
src/iface-none.h
Normal 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
29
src/iface-unknown.c
Normal 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 don’t 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
8
src/iface-unknown.h
Normal 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);
|
||||
BIN
src/img/alsa-scarlett-gui-256.png
Normal file
BIN
src/img/alsa-scarlett-gui-256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
src/img/alsa-scarlett-gui-48.png
Normal file
BIN
src/img/alsa-scarlett-gui-48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src/img/alsa-scarlett-gui-logo.png
Normal file
BIN
src/img/alsa-scarlett-gui-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
18
src/img/socket.svg
Normal file
18
src/img/socket.svg
Normal 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
75
src/main.c
Normal 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
8
src/main.h
Normal 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
190
src/menu.c
Normal 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
14
src/menu.h
Normal 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
68
src/routing-drag-line.c
Normal 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
11
src/routing-drag-line.h
Normal 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
377
src/routing-lines.c
Normal 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
22
src/routing-lines.h
Normal 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
71
src/stringhelper.c
Normal 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
8
src/stringhelper.h
Normal 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
19
src/tooltips.c
Normal 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
8
src/tooltips.h
Normal 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
50
src/widget-boolean.c
Normal 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
14
src/widget-boolean.h
Normal 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
35
src/widget-combo.c
Normal 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
10
src/widget-combo.h
Normal 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
68
src/widget-dual.c
Normal 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
18
src/widget-dual.h
Normal 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
56
src/widget-gain.c
Normal 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
10
src/widget-gain.h
Normal 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
56
src/widget-volume.c
Normal 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
10
src/widget-volume.h
Normal 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
98
src/window-hardware.c
Normal 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
10
src/window-hardware.h
Normal 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
29
src/window-helper.c
Normal 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
16
src/window-helper.h
Normal 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
97
src/window-iface.c
Normal 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
10
src/window-iface.h
Normal 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
112
src/window-levels.c
Normal 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
8
src/window-levels.h
Normal 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
126
src/window-mixer.c
Normal 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
9
src/window-mixer.h
Normal 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
916
src/window-routing.c
Normal 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
10
src/window-routing.h
Normal 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
168
src/window-startup.c
Normal 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. You’ll 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 can’t 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
8
src/window-startup.h
Normal 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);
|
||||
Reference in New Issue
Block a user