256 lines
6.7 KiB
C
256 lines
6.7 KiB
C
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
||
#include "gtkdial.h"
|
||
#include "stringhelper.h"
|
||
#include "widget-gain.h"
|
||
#include "db.h"
|
||
|
||
struct gain {
|
||
struct alsa_elem *elem;
|
||
struct alsa_elem *direct_monitor_elem;
|
||
struct alsa_elem *monitor_mix_elem[2];
|
||
GtkWidget *vbox;
|
||
GtkWidget *dial;
|
||
GtkWidget *label;
|
||
int zero_is_off;
|
||
float scale;
|
||
};
|
||
|
||
static void gain_changed(GtkWidget *widget, struct gain *data) {
|
||
int value = gtk_dial_get_value(GTK_DIAL(data->dial));
|
||
alsa_set_elem_value(data->elem, value);
|
||
|
||
// check if there is a corresponding Direct Monitor Mix control to
|
||
// update as well
|
||
|
||
// Direct Monitor control?
|
||
if (!data->direct_monitor_elem)
|
||
return;
|
||
|
||
// Direct Monitor enabled?
|
||
int direct_monitor = alsa_get_elem_value(data->direct_monitor_elem);
|
||
|
||
if (!direct_monitor)
|
||
return;
|
||
|
||
// Get the corresponding Mix control
|
||
struct alsa_elem *monitor_mix = data->monitor_mix_elem[direct_monitor - 1];
|
||
if (!monitor_mix)
|
||
return;
|
||
|
||
// Update it
|
||
alsa_set_elem_value(monitor_mix, value);
|
||
}
|
||
|
||
static void gain_updated(
|
||
struct alsa_elem *elem,
|
||
void *private
|
||
) {
|
||
struct gain *data = private;
|
||
|
||
int is_writable = alsa_get_elem_writable(elem);
|
||
gtk_widget_set_sensitive(data->dial, is_writable);
|
||
|
||
int alsa_value = alsa_get_elem_value(elem);
|
||
gtk_dial_set_value(GTK_DIAL(data->dial), alsa_value);
|
||
|
||
char s[20];
|
||
char *p = s;
|
||
float value;
|
||
int min_db = round(elem->min_cdB / 100.0);
|
||
int max_db = round(elem->max_cdB / 100.0);
|
||
|
||
if (elem->dB_type == SND_CTL_TLVT_DB_LINEAR) {
|
||
value = linear_value_to_db(
|
||
alsa_value,
|
||
elem->min_val,
|
||
elem->max_val,
|
||
min_db,
|
||
max_db
|
||
);
|
||
} else {
|
||
value = ((float)(alsa_value - elem->min_val)) * data->scale + (elem->min_cdB / 100.0);
|
||
if (value > max_db)
|
||
value = max_db;
|
||
else if (value < min_db)
|
||
value = min_db;
|
||
}
|
||
|
||
if (data->zero_is_off && value == min_db) {
|
||
p += sprintf(p, "−∞");
|
||
} else {
|
||
if (data->scale <= 0.5)
|
||
value = round(value * 10) / 10;
|
||
if (value < 0)
|
||
p += sprintf(p, "−");
|
||
else if (value > 0)
|
||
p += sprintf(p, "+");
|
||
if (data->scale <= 0.5)
|
||
p += snprintf(p, 10, "%.1f", fabs(value));
|
||
else
|
||
p += snprintf(p, 10, "%.0f", fabs(value));
|
||
}
|
||
if (data->scale > 0.5)
|
||
p += sprintf(p, "dB");
|
||
|
||
gtk_label_set_text(GTK_LABEL(data->label), s);
|
||
}
|
||
|
||
// 4th Gen Solo and 2i2 have Mix & Direct Monitor controls which
|
||
// interact. If direct monitor is enabled and the Mix A/B controls are
|
||
// changed, then the Monitor Mix Playback Volume controls are changed
|
||
// too so that the mix settings are restored when direct monitor is
|
||
// later enabled again.
|
||
static void find_direct_monitor_controls(struct gain *data) {
|
||
struct alsa_elem *elem = data->elem;
|
||
GArray *elems = elem->card->elems;
|
||
|
||
// Card has no direct monitor control?
|
||
struct alsa_elem *direct_monitor_elem = get_elem_by_prefix(
|
||
elems,
|
||
"Direct Monitor Playback"
|
||
);
|
||
if (!direct_monitor_elem)
|
||
return;
|
||
|
||
// Card has no mixer?
|
||
if (strncmp(elem->name, "Mix ", 4) != 0 ||
|
||
!strstr(elem->name, "Playback Volume"))
|
||
return;
|
||
|
||
char mix_letter = elem->name[4];
|
||
int input_num = get_num_from_string(elem->name);
|
||
|
||
// Find the Monitor Mix control for the 4th Gen Solo
|
||
if (strstr(direct_monitor_elem->name, "Switch")) {
|
||
char s[80];
|
||
sprintf(
|
||
s,
|
||
"Monitor Mix %c Input %02d Playback Volume",
|
||
mix_letter, input_num
|
||
);
|
||
|
||
struct alsa_elem *monitor_mix_elem = get_elem_by_name(elems, s);
|
||
if (!monitor_mix_elem)
|
||
return;
|
||
|
||
data->direct_monitor_elem = direct_monitor_elem;
|
||
data->monitor_mix_elem[0] = monitor_mix_elem;
|
||
|
||
// Find the Monitor Mix controls for the 4th Gen 2i2
|
||
} else if (strstr(direct_monitor_elem->name, "Enum")) {
|
||
for (int i = 0; i <= 1; i++) {
|
||
char s[80];
|
||
sprintf(
|
||
s,
|
||
"Monitor %d Mix %c Input %02d Playback Volume",
|
||
i + 1, mix_letter, input_num
|
||
);
|
||
|
||
struct alsa_elem *monitor_mix_elem = get_elem_by_name(elems, s);
|
||
if (!monitor_mix_elem)
|
||
return;
|
||
|
||
data->direct_monitor_elem = direct_monitor_elem;
|
||
data->monitor_mix_elem[i] = monitor_mix_elem;
|
||
}
|
||
|
||
} else {
|
||
fprintf(stderr, "Couldn't find direct monitor mix control\n");
|
||
}
|
||
}
|
||
|
||
//GList *make_gain_alsa_elem(struct alsa_elem *elem) {
|
||
GtkWidget *make_gain_alsa_elem(
|
||
struct alsa_elem *elem,
|
||
int zero_is_off,
|
||
int widget_taper,
|
||
int can_control
|
||
) {
|
||
struct gain *data = calloc(1, sizeof(struct gain));
|
||
data->elem = elem;
|
||
data->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||
gtk_widget_set_hexpand(data->vbox, TRUE);
|
||
gtk_widget_set_valign(data->vbox, GTK_ALIGN_START);
|
||
gtk_widget_set_vexpand(data->vbox, TRUE);
|
||
|
||
gboolean is_linear = elem->dB_type == SND_CTL_TLVT_DB_LINEAR;
|
||
double step;
|
||
|
||
if (is_linear) {
|
||
data->scale = 0.5;
|
||
step = 0.5;
|
||
} else {
|
||
data->scale = (float)(elem->max_cdB - elem->min_cdB) / 100.0 /
|
||
(elem->max_val - elem->min_val);
|
||
step = 1;
|
||
}
|
||
data->dial = gtk_dial_new_with_range(
|
||
elem->min_val,
|
||
elem->max_val,
|
||
step,
|
||
3 / data->scale
|
||
);
|
||
|
||
// calculate 0dB value
|
||
int zero_db_value;
|
||
|
||
if (is_linear) {
|
||
zero_db_value = cdb_to_linear_value(
|
||
0,
|
||
elem->min_val,
|
||
elem->max_val,
|
||
elem->min_cdB,
|
||
elem->max_cdB
|
||
);
|
||
} else {
|
||
zero_db_value =
|
||
(int)((0 - elem->min_cdB) / 100.0 / data->scale + elem->min_val);
|
||
}
|
||
|
||
gtk_dial_set_zero_db(GTK_DIAL(data->dial), zero_db_value);
|
||
gtk_dial_set_is_linear(GTK_DIAL(data->dial), is_linear);
|
||
|
||
// convert from widget_taper to gtk_dial_taper
|
||
int gtk_dial_taper;
|
||
if (widget_taper == WIDGET_GAIN_TAPER_LINEAR)
|
||
gtk_dial_taper = GTK_DIAL_TAPER_LINEAR;
|
||
else if (widget_taper == WIDGET_GAIN_TAPER_LOG)
|
||
gtk_dial_taper = GTK_DIAL_TAPER_LOG;
|
||
else
|
||
gtk_dial_taper = GTK_DIAL_TAPER_LINEAR;
|
||
gtk_dial_set_taper(GTK_DIAL(data->dial), gtk_dial_taper);
|
||
|
||
if (widget_taper == WIDGET_GAIN_TAPER_GEN4_VOLUME)
|
||
gtk_dial_set_taper_linear_breakpoints(
|
||
GTK_DIAL(data->dial),
|
||
(const double[]){ 0.488, 0.76 },
|
||
(const double[]){ 0.07, 0.4 },
|
||
2
|
||
);
|
||
|
||
gtk_dial_set_can_control(GTK_DIAL(data->dial), can_control);
|
||
|
||
data->label = gtk_label_new(NULL);
|
||
gtk_widget_add_css_class(data->label, "gain");
|
||
gtk_widget_set_vexpand(data->dial, TRUE);
|
||
|
||
data->zero_is_off = zero_is_off;
|
||
|
||
find_direct_monitor_controls(data);
|
||
|
||
g_signal_connect(
|
||
data->dial, "value-changed", G_CALLBACK(gain_changed), data
|
||
);
|
||
|
||
alsa_elem_add_callback(elem, gain_updated, data);
|
||
|
||
gain_updated(elem, data);
|
||
|
||
gtk_box_append(GTK_BOX(data->vbox), data->dial);
|
||
gtk_box_append(GTK_BOX(data->vbox), data->label);
|
||
|
||
return data->vbox;
|
||
}
|