diff --git a/src/gtkdial.c b/src/gtkdial.c index 438f648..fce6bcb 100644 --- a/src/gtkdial.c +++ b/src/gtkdial.c @@ -21,6 +21,8 @@ #define DIAL_MIN_WIDTH 50 #define DIAL_MAX_WIDTH 70 +#define HISTORY_COUNT 50 + static int set_value(GtkDial *dial, double newval); static void gtk_dial_set_property( @@ -90,6 +92,7 @@ enum { PROP_OFF_DB, PROP_TAPER, PROP_CAN_CONTROL, + PROP_PEAK_HOLD, LAST_PROP }; @@ -117,6 +120,7 @@ struct _GtkDial { double off_db; int taper; gboolean can_control; + int peak_hold; int properties_updated; @@ -155,6 +159,15 @@ struct _GtkDial { double angle; double slider_cx; double slider_cy; + + // same for the peak angle + double peak_angle; + + // value history for displaying peak + double hist_values[HISTORY_COUNT]; + long long hist_time[HISTORY_COUNT]; + double current_peak; + int hist_head, hist_tail, hist_count; }; G_DEFINE_TYPE(GtkDial, gtk_dial, GTK_TYPE_WIDGET) @@ -176,6 +189,17 @@ static void dial_measure( "move-slider", \ "(i)", scroll) +long long current_time = 0; + +void gtk_dial_peak_tick(void) { + struct timespec ts; + + if (clock_gettime(CLOCK_BOOTTIME, &ts) < 0) + return; + + current_time = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + // BEGIN SECTION HELPERS #define TOTAL_ROTATION_DEGREES 290 @@ -404,6 +428,12 @@ static void update_dial_values(GtkDial *dial) { dial->angle = calc_val(dial->valp, ANGLE_START, ANGLE_END); dial->slider_cx = cos(dial->angle) * dial->slider_radius + dial->cx; dial->slider_cy = sin(dial->angle) * dial->slider_radius + dial->cy; + + if (!dial->peak_hold) + return; + + double peak_valp = calc_taper(dial, dial->current_peak); + dial->peak_angle = calc_val(peak_valp, ANGLE_START, ANGLE_END); } static double pdist2(double x1, double y1, double x2, double y2) { @@ -530,6 +560,20 @@ static void gtk_dial_class_init(GtkDialClass *klass) { G_PARAM_READWRITE | G_PARAM_CONSTRUCT ); + /** + * GtkDial:peak-hold: (attributes org.gtk.Method.get=gtk_dial_get_peak_hold org.gtk.Method.set=gtk_dial_set_peak_hold) + * + * The number of milliseconds to hold the peak value. + */ + properties[PROP_PEAK_HOLD] = g_param_spec_int( + "peak-hold", + "PeakHold", + "The number of milliseconds to hold the peak value", + 0, 1000, + 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT + ); + g_object_class_install_properties(g_class, LAST_PROP, properties); /** @@ -648,6 +692,11 @@ static void gtk_dial_init(GtkDial *dial) { g_signal_connect( dial, "notify::sensitive", G_CALLBACK(gtk_dial_notify_sensitive_cb), dial ); + + dial->current_peak = -INFINITY; + dial->hist_head = 0; + dial->hist_tail = 0; + dial->hist_count = 0; } static void dial_measure( @@ -686,6 +735,42 @@ static void cairo_set_source_rgba_dim( cairo_set_source_rgba(cr, r, g, b, a); } +static void draw_peak(GtkDial *dial, cairo_t *cr, double radius) { + + double angle_start = dial->peak_angle - M_PI / 180; + if (angle_start < ANGLE_START) + return; + + // determine the colour of the peak + int count = dial->level_breakpoints_count; + + // if there are no colours, don't draw the peak + if (!count) + return; + + int i; + + for (i = 0; i < count - 1; i++) + if (dial->current_peak < dial->level_breakpoints[i + 1]) + break; + + const double *colours = &dial->level_colours[i * 3]; + + cairo_set_source_rgba_dim( + cr, colours[0], colours[1], colours[2], 0.5, dial->dim + ); + cairo_set_line_width(cr, 2); + cairo_arc(cr, dial->cx, dial->cy, radius, ANGLE_START, dial->peak_angle); + cairo_stroke(cr); + + cairo_set_source_rgba_dim( + cr, colours[0], colours[1], colours[2], 1, dial->dim + ); + cairo_set_line_width(cr, 4); + cairo_arc(cr, dial->cx, dial->cy, radius, angle_start, dial->peak_angle); + cairo_stroke(cr); +} + static void draw_slider( GtkDial *dial, cairo_t *cr, @@ -781,6 +866,10 @@ static void dial_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) { draw_slider(dial, cr, dial->slider_radius, 6, 0.3); } + // peak hold + if (dial->peak_hold) + draw_peak(dial, cr, dial->slider_radius); + // draw line to zero db double zero_db = gtk_dial_get_zero_db(dial); if (zero_db != -G_MAXDOUBLE) { @@ -903,6 +992,9 @@ static void gtk_dial_set_property( case PROP_CAN_CONTROL: gtk_dial_set_can_control(dial, g_value_get_boolean(value)); break; + case PROP_PEAK_HOLD: + gtk_dial_set_peak_hold(dial, g_value_get_int(value)); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; @@ -936,6 +1028,9 @@ static void gtk_dial_get_property( case PROP_CAN_CONTROL: g_value_set_boolean(value, dial->can_control); break; + case PROP_PEAK_HOLD: + g_value_set_int(value, dial->peak_hold); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; @@ -1044,6 +1139,14 @@ void gtk_dial_set_level_meter_colours( dial->properties_updated = 1; } +void gtk_dial_set_peak_hold(GtkDial *dial, int peak_hold) { + dial->peak_hold = peak_hold; +} + +int gtk_dial_get_peak_hold(GtkDial *dial) { + return dial->peak_hold; +} + void gtk_dial_set_adjustment(GtkDial *dial, GtkAdjustment *adj) { if (!(adj == NULL || GTK_IS_ADJUSTMENT(adj))) return; @@ -1059,6 +1162,46 @@ GtkAdjustment *gtk_dial_get_adjustment(GtkDial *dial) { return dial->adj; } +static void gtk_dial_add_hist_value(GtkDial *dial, double value) { + + int need_peak_update = 0; + + // remove the oldest value(s) if they are too old or if the history + // is full + while (dial->hist_count > 0 && + (dial->hist_time[dial->hist_head] < current_time - dial->peak_hold || + dial->hist_count == HISTORY_COUNT)) { + + // check if the value removed is the current peak + if (dial->hist_values[dial->hist_head] >= dial->current_peak) + need_peak_update = 1; + + // move the head forward + dial->hist_head = (dial->hist_head + 1) % HISTORY_COUNT; + dial->hist_count--; + } + + // recalculate the peak if needed + if (need_peak_update) { + dial->current_peak = -INFINITY; + for (int i = dial->hist_head; + i != dial->hist_tail; + i = (i + 1) % HISTORY_COUNT) + if (dial->hist_values[i] > dial->current_peak) + dial->current_peak = dial->hist_values[i]; + } + + // add the new value + dial->hist_values[dial->hist_tail] = value; + dial->hist_time[dial->hist_tail] = current_time; + dial->hist_tail = (dial->hist_tail + 1) % HISTORY_COUNT; + dial->hist_count++; + + // update the peak if needed + if (value > dial->current_peak) + dial->current_peak = value; +} + static int set_value(GtkDial *dial, double newval) { if (dial->round_digits >= 0) { double power; @@ -1079,7 +1222,10 @@ static int set_value(GtkDial *dial, double newval) { double oldval = gtk_adjustment_get_value(dial->adj); - if (oldval == newval) + double old_peak = dial->current_peak; + gtk_dial_add_hist_value(dial, newval); + + if (oldval == newval && old_peak == dial->current_peak) return 0; gtk_adjustment_set_value(dial->adj, newval); @@ -1088,7 +1234,7 @@ static int set_value(GtkDial *dial, double newval) { double old_valp = dial->valp; update_dial_values(dial); - return old_valp != dial->valp; + return old_valp != dial->valp || old_peak != dial->current_peak; } static void step_back(GtkDial *dial) { diff --git a/src/gtkdial.h b/src/gtkdial.h index b0c26c9..9b62bc0 100644 --- a/src/gtkdial.h +++ b/src/gtkdial.h @@ -94,6 +94,10 @@ void gtk_dial_set_level_meter_colours( int count ); +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); + G_END_DECLS #endif diff --git a/src/window-levels.c b/src/window-levels.c index 671271d..a492ef1 100644 --- a/src/window-levels.c +++ b/src/window-levels.c @@ -40,6 +40,8 @@ static int update_levels_controls(void *user_data) { int meter_num = 0; + gtk_dial_peak_tick(); + // go through the port categories for (int i = 0; i < PC_COUNT; i++) { @@ -125,6 +127,7 @@ GtkWidget *create_levels_controls(struct alsa_card *card) { GtkWidget *meter = gtk_dial_new_with_range(-80, 0, 0, 0); gtk_dial_set_taper(GTK_DIAL(meter), GTK_DIAL_TAPER_LINEAR); gtk_dial_set_can_control(GTK_DIAL(meter), FALSE); + gtk_dial_set_peak_hold(GTK_DIAL(meter), 1000); gtk_dial_set_level_meter_colours( GTK_DIAL(meter), (i == PC_DSP || i == PC_PCM)