// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett // 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; }