From df5d0960ddb717f7c38dc4b37bfca0d374fd784d Mon Sep 17 00:00:00 2001 From: "Geoffrey D. Bennett" Date: Wed, 17 Jan 2024 17:54:11 +1030 Subject: [PATCH] Add support for piecewise linear interpolation taper to GtkDial This commit adds support for piecewise linear interpolation tapers to GtkDial and the gain widget so that the 4th Gen 4i4 volume knob taper can be modelled correctly. --- src/gtkdial.c | 71 ++++++++++++++++++++++++++++++++++++++++++++++- src/gtkdial.h | 7 +++++ src/iface-mixer.c | 8 ++++-- src/widget-gain.c | 8 ++++++ src/widget-gain.h | 3 +- 5 files changed, 93 insertions(+), 4 deletions(-) diff --git a/src/gtkdial.c b/src/gtkdial.c index 30f7945..bc65a43 100644 --- a/src/gtkdial.c +++ b/src/gtkdial.c @@ -119,6 +119,11 @@ struct _GtkDial { int round_digits; double zero_db; int taper; + + // linear taper breakpoints array + double *taper_breakpoints; + double *taper_outputs; + int taper_breakpoints_count; }; G_DEFINE_TYPE(GtkDial, gtk_dial, GTK_TYPE_WIDGET) @@ -159,6 +164,24 @@ static double calc_valp(double val, double mn, double mx) { return (val - mn) / (mx - mn); } +static double taper_linear(double val, double *bp, double *out, int count) { + if (count < 3) + return val; + + if (val <= bp[0]) + return out[0]; + + for (int i = 0; i < count - 1; i++) { + if (val > bp[i + 1]) + continue; + + double scale = (out[i + 1] - out[i]) / (bp[i + 1] - bp[i]); + return out[i] + scale * (val - bp[i]); + } + + return out[count - 1]; +} + static double taper_log(double val) { // 10^(val - 1) converts it to 0.1..1 with a nice curve @@ -170,7 +193,12 @@ static double taper_log(double val) { static double calc_taper(GtkDial *dial, double val) { if (dial->taper == GTK_DIAL_TAPER_LINEAR) - return val; + return taper_linear( + val, + dial->taper_breakpoints, + dial->taper_outputs, + dial->taper_breakpoints_count + ); if (dial->taper == GTK_DIAL_TAPER_LOG) return taper_log(val); @@ -623,6 +651,40 @@ int gtk_dial_get_taper(GtkDial *dial) { return dial->taper; } +void gtk_dial_set_taper_linear_breakpoints( + GtkDial *dial, + const double *breakpoints, + const double *outputs, + int count +) { + free(dial->taper_breakpoints); + free(dial->taper_outputs); + dial->taper_breakpoints = NULL; + dial->taper_outputs = NULL; + dial->taper_breakpoints_count = 0; + + if (count < 1) + return; + + int total_count = count + 2; + + dial->taper_breakpoints = malloc(total_count * sizeof(double)); + dial->taper_outputs = malloc(total_count * sizeof(double)); + + dial->taper_breakpoints[0] = 0; + dial->taper_outputs[0] = 0; + + for (int i = 0; i < count; i++) { + dial->taper_breakpoints[i + 1] = breakpoints[i]; + dial->taper_outputs[i + 1] = outputs[i]; + } + + dial->taper_breakpoints[total_count - 1] = 1; + dial->taper_outputs[total_count - 1] = 1; + + dial->taper_breakpoints_count = total_count; +} + gboolean gtk_dial_set_style( GtkDial *dial, const char *trough_border, @@ -919,6 +981,13 @@ static gboolean gtk_dial_scroll_controller_scroll( void gtk_dial_dispose(GObject *o) { GtkDial *dial = GTK_DIAL(o); + + free(dial->taper_breakpoints); + dial->taper_breakpoints = NULL; + free(dial->taper_outputs); + dial->taper_outputs = NULL; + dial->taper_breakpoints_count = 0; + g_object_unref(dial->adj); dial->adj = NULL; G_OBJECT_CLASS(gtk_dial_parent_class)->dispose(o); diff --git a/src/gtkdial.h b/src/gtkdial.h index 3ab09ae..bcaf53a 100644 --- a/src/gtkdial.h +++ b/src/gtkdial.h @@ -74,6 +74,13 @@ enum { void gtk_dial_set_taper(GtkDial *dial, int taper); int gtk_dial_get_taper(GtkDial *dial); +void gtk_dial_set_taper_linear_breakpoints( + GtkDial *dial, + const double *breakpoints, + const double *outputs, + int count +); + /** * @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) diff --git a/src/iface-mixer.c b/src/iface-mixer.c index 7ca54d6..d0e5b7d 100644 --- a/src/iface-mixer.c +++ b/src/iface-mixer.c @@ -503,7 +503,11 @@ static void create_output_controls( GtkWidget *l = gtk_label_new(gen4 ? "Line 1–2" : "HW"); gtk_grid_attach(GTK_GRID(output_grid), l, 0, 0, 1, 1); - w = make_gain_alsa_elem(elem, 1, WIDGET_GAIN_TAPER_LOG); + if (gen4) { + w = make_gain_alsa_elem(elem, 1, WIDGET_GAIN_TAPER_GEN4_VOLUME); + } else { + w = make_gain_alsa_elem(elem, 1, WIDGET_GAIN_TAPER_LOG); + } gtk_widget_set_tooltip_text( w, gen4 @@ -522,7 +526,7 @@ static void create_output_controls( "This control shows the setting of the headphone volume knob." ); gtk_grid_attach(GTK_GRID(output_grid), l, 1, 0, 1, 1); - w = make_gain_alsa_elem(elem, 1, WIDGET_GAIN_TAPER_LOG); + w = make_gain_alsa_elem(elem, 1, WIDGET_GAIN_TAPER_GEN4_VOLUME); gtk_grid_attach(GTK_GRID(output_grid), w, 1, 1, 1, 1); } else if (strcmp(elem->name, "Mute Playback Switch") == 0) { w = make_boolean_alsa_elem( diff --git a/src/widget-gain.c b/src/widget-gain.c index f167156..0a410e4 100644 --- a/src/widget-gain.c +++ b/src/widget-gain.c @@ -92,6 +92,14 @@ GtkWidget *make_gain_alsa_elem( 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 + ); + data->label = gtk_label_new(NULL); gtk_widget_set_vexpand(data->dial, TRUE); diff --git a/src/widget-gain.h b/src/widget-gain.h index 63c2f0a..d9dbe67 100644 --- a/src/widget-gain.h +++ b/src/widget-gain.h @@ -9,7 +9,8 @@ enum { WIDGET_GAIN_TAPER_LINEAR, - WIDGET_GAIN_TAPER_LOG + WIDGET_GAIN_TAPER_LOG, + WIDGET_GAIN_TAPER_GEN4_VOLUME }; GtkWidget *make_gain_alsa_elem(