diff --git a/src/db.c b/src/db.c new file mode 100644 index 0000000..fe9db6d --- /dev/null +++ b/src/db.c @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +static double db_to_linear(double db) { + if (db <= SND_CTL_TLV_DB_GAIN_MUTE) + return 0.0; + return pow(10.0, db / 20.0); +} + +static double linear_to_db(double linear) { + if (linear <= 0.0) + return SND_CTL_TLV_DB_GAIN_MUTE; + return 20.0 * log10(linear); +} + +int cdb_to_linear_value( + int cdb, int min_val, int max_val, int min_cdb, int max_cdb +) { + if (cdb <= min_cdb) + return min_val; + if (cdb >= max_cdb) + return max_val; + + // Convert centidB to dB + double db = (double)cdb / 100.0; + double max_db = (double)max_cdb / 100.0; + + // Convert dB relative to max_db to linear scale 0.0-1.0 + double linear = db_to_linear(db - max_db); + + // Scale to full ALSA range + double scaled = linear * (double)max_val; + int value = (int)round(scaled); + if (value < min_val) + return min_val; + if (value > max_val) + return max_val; + return value; +} + +int linear_value_to_cdb( + int value, int min_val, int max_val, int min_cdb, int max_cdb +) { + if (value <= min_val) + return min_cdb; + if (value >= max_val) + return max_cdb; + + // Convert to 0.0-1.0 linear scale + double linear = (double)value / (double)max_val; + double max_db = (double)max_cdb / 100.0; + + // Convert to dB relative to max_db and back to centidB + int cdb = (int)round((linear_to_db(linear) + max_db) * 100.0); + if (cdb < min_cdb) + return min_cdb; + if (cdb > max_cdb) + return max_cdb; + return cdb; +} + +double linear_value_to_db( + int value, int min_val, int max_val, int min_db, int max_db +) { + if (value <= min_val) + return min_db; + if (value >= max_val) + return max_db; + + // Convert to 0.0-1.0 linear scale + double linear = (double)value / (double)max_val; + + // Convert to dB relative to max_db + double db = linear_to_db(linear) + max_db; + if (db < min_db) + return min_db; + if (db > max_db) + return max_db; + return db; +} diff --git a/src/db.h b/src/db.h new file mode 100644 index 0000000..c3c563a --- /dev/null +++ b/src/db.h @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +int cdb_to_linear_value( + int cdb, int min_val, int max_val, int min_cdb, int max_cdb +); + +int linear_value_to_cdb( + int value, int min_val, int max_val, int min_cdb, int max_cdb +); + +double linear_value_to_db( + int value, int min_val, int max_val, int min_db, int max_db +); diff --git a/src/gtkdial.c b/src/gtkdial.c index b86366f..f431de4 100644 --- a/src/gtkdial.c +++ b/src/gtkdial.c @@ -17,6 +17,7 @@ #include #include "gtkdial.h" +#include "db.h" #define DIAL_MIN_WIDTH 50 #define DIAL_MAX_WIDTH 70 @@ -90,6 +91,7 @@ enum { PROP_ROUND_DIGITS, PROP_ZERO_DB, PROP_OFF_DB, + PROP_IS_LINEAR, PROP_TAPER, PROP_CAN_CONTROL, PROP_PEAK_HOLD, @@ -118,6 +120,7 @@ struct _GtkDial { int round_digits; double zero_db; double off_db; + gboolean is_linear; int taper; gboolean can_control; int peak_hold; @@ -254,6 +257,13 @@ static double calc_taper(GtkDial *dial, double val) { double mn = gtk_adjustment_get_lower(dial->adj); double mx = gtk_adjustment_get_upper(dial->adj); double off_db = gtk_dial_get_off_db(dial); + gboolean is_linear = gtk_dial_get_is_linear(dial); + + if (is_linear) { + val = linear_value_to_cdb(val, mn, mx, -8000, 1200) / 100.0; + mn = -60; + mx = 12; + } // if off_db is set, then values below it are considered as // almost-silence, so we clamp them to 0.01 @@ -551,6 +561,19 @@ static void gtk_dial_class_init(GtkDialClass *klass) { G_PARAM_READWRITE | G_PARAM_CONSTRUCT ); + /** + * GtkDial:is_linear: (attributes org.gtk.Method.get=gtk_dial_get_is_linear org.gtk.Method.set=gtk_dial_set_is_linear) + * + * Whether the dial values are linear or dB. + */ + properties[PROP_IS_LINEAR] = g_param_spec_boolean( + "is_linear", + "IsLinear", + "Whether the dial values are linear or dB", + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT + ); + /** * GtkDial:taper: (attributes org.gtk.Method.get=gtk_dial_get_taper org.gtk.Method.set=gtk_dial_set_taper) * @@ -1037,6 +1060,9 @@ static void gtk_dial_set_property( case PROP_OFF_DB: gtk_dial_set_off_db(dial, g_value_get_double(value)); break; + case PROP_IS_LINEAR: + gtk_dial_set_is_linear(dial, g_value_get_boolean(value)); + break; case PROP_TAPER: gtk_dial_set_taper(dial, g_value_get_int(value)); break; @@ -1073,6 +1099,9 @@ static void gtk_dial_get_property( case PROP_OFF_DB: g_value_set_double(value, dial->off_db); break; + case PROP_IS_LINEAR: + g_value_set_boolean(value, dial->is_linear); + break; case PROP_TAPER: g_value_set_int(value, dial->taper); break; @@ -1124,6 +1153,15 @@ double gtk_dial_get_off_db(GtkDial *dial) { return dial->off_db; } +void gtk_dial_set_is_linear(GtkDial *dial, gboolean is_linear) { + dial->is_linear = is_linear; + dial->properties_updated = 1; +} + +gboolean gtk_dial_get_is_linear(GtkDial *dial) { + return dial->is_linear; +} + void gtk_dial_set_taper(GtkDial *dial, int taper) { dial->taper = taper; dial->properties_updated = 1; @@ -1288,32 +1326,41 @@ static int set_value(GtkDial *dial, double newval) { return old_valp != dial->valp || old_peak != dial->current_peak; } -static void step_back(GtkDial *dial) { - double newval; +static double do_step(GtkDial *dial, double step_amount) { + double mn = gtk_adjustment_get_lower(dial->adj); + double mx = gtk_adjustment_get_upper(dial->adj); + double newval = gtk_adjustment_get_value(dial->adj); + double step = gtk_adjustment_get_step_increment(dial->adj); - newval = gtk_adjustment_get_value(dial->adj) - gtk_adjustment_get_step_increment(dial->adj); - set_value(dial, newval); + if (gtk_dial_get_is_linear(dial)) { + double db_val = linear_value_to_cdb(newval, mn, mx, -8000, 1200) / 100.0; + db_val = round(db_val / step) * step + step_amount; + + newval = cdb_to_linear_value(db_val * 100.0, mn, mx, -8000, 1200); + if (newval == gtk_adjustment_get_value(dial->adj)) { + newval = CLAMP(newval + (step_amount > 0 ? 1 : -1), mn, mx); + } + } else { + newval += step_amount; + } + + return newval; +} + +static void step_back(GtkDial *dial) { + set_value(dial, do_step(dial, -gtk_adjustment_get_step_increment(dial->adj))); } 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); + set_value(dial, do_step(dial, gtk_adjustment_get_step_increment(dial->adj))); } 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); + set_value(dial, do_step(dial, -gtk_adjustment_get_page_increment(dial->adj))); } 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); + set_value(dial, do_step(dial, gtk_adjustment_get_page_increment(dial->adj))); } static void scroll_begin(GtkDial *dial) { @@ -1447,16 +1494,43 @@ static void gtk_dial_drag_gesture_update( gtk_gesture_drag_get_start_point(gesture, &start_x, &start_y); - double valp = dial->dvalp - DRAG_FACTOR * (offset_y / dial->h); - valp = CLAMP(valp, 0.0, 1.0); + double mn = gtk_adjustment_get_lower(dial->adj); + double mx = gtk_adjustment_get_upper(dial->adj); + gboolean is_linear = gtk_dial_get_is_linear(dial); - double val = calc_val( - valp, - gtk_adjustment_get_lower(dial->adj), - gtk_adjustment_get_upper(dial->adj) - ); + double valp; + + if (is_linear) { + double step = gtk_adjustment_get_step_increment(dial->adj); + + // Convert initial value from linear to dB space + double db_val = linear_value_to_cdb( + calc_val(dial->dvalp, mn, mx), + mn, mx, + -8000, 1200 + ) / 100.0; + + // Adjust in dB space + db_val -= 30.0 * DRAG_FACTOR * (offset_y / dial->h); + + // Round + db_val = round(db_val / step) * step; + + // Convert back to linear space and normalise + double val = cdb_to_linear_value( + db_val * 100.0, + mn, mx, + -8000, 1200 + ); + + valp = calc_valp(val, mn, mx); + } else { + valp = dial->dvalp - DRAG_FACTOR * (offset_y / dial->h); + valp = CLAMP(valp, 0.0, 1.0); + } + + set_value(dial, calc_val(valp, mn, mx)); - set_value(dial, val); gtk_widget_queue_draw(GTK_WIDGET(dial)); } @@ -1519,7 +1593,7 @@ static gboolean gtk_dial_scroll_controller_scroll( double step = -gtk_adjustment_get_step_increment(dial->adj) * delta; - set_value(dial, gtk_adjustment_get_value(dial->adj) + step); + set_value(dial, do_step(dial, step)); gtk_widget_queue_draw(GTK_WIDGET(dial)); return GDK_EVENT_STOP; diff --git a/src/gtkdial.h b/src/gtkdial.h index 9b62bc0..4969a9f 100644 --- a/src/gtkdial.h +++ b/src/gtkdial.h @@ -68,6 +68,9 @@ double gtk_dial_get_zero_db(GtkDial *dial); void gtk_dial_set_off_db(GtkDial *dial, double off_db); double gtk_dial_get_off_db(GtkDial *dial); +void gtk_dial_set_is_linear(GtkDial *dial, gboolean is_linear); +gboolean gtk_dial_get_is_linear(GtkDial *dial); + // taper functions enum { GTK_DIAL_TAPER_LINEAR, @@ -98,6 +101,13 @@ void gtk_dial_set_peak_hold(GtkDial *dial, int peak_hold); int gtk_dial_get_peak_hold(GtkDial *dial); void gtk_dial_peak_tick(void); +int cdb_to_linear_value( + int db, int min_val, int max_val, int min_db, int max_db +); +int linear_value_to_cdb( + int value, int min_val, int max_val, int min_db, int max_db +); + G_END_DECLS #endif