minor peak-meter performance tweak
[ardour.git] / libs / gtkmm2ext / fastmeter.cc
1 /*
2     Copyright (C) 2003-2006 Paul Davis
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18     $Id$
19 */
20
21 #include <iostream>
22 #include <cmath>
23 #include <algorithm>
24 #include <cstring>
25
26 #include <gdkmm/rectangle.h>
27 #include <gtkmm2ext/fastmeter.h>
28 #include <gtkmm2ext/utils.h>
29
30 #define UINT_TO_RGB(u,r,g,b) { (*(r)) = ((u)>>16)&0xff; (*(g)) = ((u)>>8)&0xff; (*(b)) = (u)&0xff; }
31 #define UINT_TO_RGBA(u,r,g,b,a) { UINT_TO_RGB(((u)>>8),r,g,b); (*(a)) = (u)&0xff; }
32 using namespace Gtk;
33 using namespace Gdk;
34 using namespace Glib;
35 using namespace Gtkmm2ext;
36 using namespace std;
37
38 int FastMeter::min_pattern_metric_size = 16;
39 int FastMeter::max_pattern_metric_size = 1024;
40
41 FastMeter::Pattern10Map FastMeter::vm_pattern_cache;
42 FastMeter::PatternBgMap FastMeter::vb_pattern_cache;
43
44 FastMeter::FastMeter (long hold, unsigned long dimen, Orientation o, int len,
45                 int clr0, int clr1, int clr2, int clr3,
46                 int clr4, int clr5, int clr6, int clr7,
47                 int clr8, int clr9,
48                 int bgc0, int bgc1,
49                 int bgh0, int bgh1,
50                 float stp0, float stp1,
51                 float stp2, float stp3
52                 )
53 {
54         orientation = o;
55         hold_cnt = hold;
56         hold_state = 0;
57         bright_hold = false;
58         current_peak = 0;
59         current_level = 0;
60         last_peak_rect.width = 0;
61         last_peak_rect.height = 0;
62
63         highlight = false;
64
65         _clr[0] = clr0;
66         _clr[1] = clr1;
67         _clr[2] = clr2;
68         _clr[3] = clr3;
69         _clr[4] = clr4;
70         _clr[5] = clr5;
71         _clr[6] = clr6;
72         _clr[7] = clr7;
73         _clr[8] = clr8;
74         _clr[9] = clr9;
75
76         _bgc[0] = bgc0;
77         _bgc[1] = bgc1;
78
79         _bgh[0] = bgh0;
80         _bgh[1] = bgh1;
81
82         _stp[0] = stp0;
83         _stp[1] = stp1;
84         _stp[2] = stp2;
85         _stp[3] = stp3;
86
87         set_events (BUTTON_PRESS_MASK|BUTTON_RELEASE_MASK);
88
89         pixrect.x = 1;
90         pixrect.y = 1;
91
92         if (!len) {
93                 len = 250;
94         }
95         fgpattern = request_vertical_meter(dimen, len, _clr, _stp, true);
96         bgpattern = request_vertical_background (dimen, len, _bgc, false);
97         pixheight = len;
98         pixwidth = dimen;
99
100         pixrect.width = pixwidth;
101         pixrect.height = pixheight;
102
103         request_width = pixrect.width + 2;
104         request_height= pixrect.height + 2;
105
106         queue_draw ();
107 }
108
109 FastMeter::~FastMeter ()
110 {
111 }
112
113 Cairo::RefPtr<Cairo::Pattern>
114 FastMeter::generate_meter_pattern (
115                 int width, int height, int *clr, float *stp, bool shade)
116 {
117         guint8 r,g,b,a;
118         double knee;
119         double soft = 1.5 / (double) height;
120
121         cairo_pattern_t* pat = cairo_pattern_create_linear (0.0, 0.0, 0.0, height);
122
123         /*
124           Cairo coordinate space goes downwards as y value goes up, so invert
125           knee-based positions by using (1.0 - y)
126         */
127
128         UINT_TO_RGBA (clr[9], &r, &g, &b, &a); // top/clip
129         cairo_pattern_add_color_stop_rgb (pat, 0.0,
130                                           r/255.0, g/255.0, b/255.0);
131
132         knee = ((float)height * stp[3] / 115.0f); // -0dB
133
134         UINT_TO_RGBA (clr[8], &r, &g, &b, &a);
135         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) - soft,
136                                           r/255.0, g/255.0, b/255.0);
137
138         UINT_TO_RGBA (clr[7], &r, &g, &b, &a);
139         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) + soft,
140                                           r/255.0, g/255.0, b/255.0);
141
142         knee = ((float)height * stp[2]/ 115.0f); // -3dB || -2dB
143
144         UINT_TO_RGBA (clr[6], &r, &g, &b, &a);
145         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) - soft,
146                                           r/255.0, g/255.0, b/255.0);
147
148         UINT_TO_RGBA (clr[5], &r, &g, &b, &a);
149         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) + soft,
150                                           r/255.0, g/255.0, b/255.0);
151
152         knee = ((float)height * stp[1] / 115.0f); // -9dB
153
154         UINT_TO_RGBA (clr[4], &r, &g, &b, &a);
155         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) - soft,
156                                           r/255.0, g/255.0, b/255.0);
157
158         UINT_TO_RGBA (clr[3], &r, &g, &b, &a);
159         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) + soft,
160                                           r/255.0, g/255.0, b/255.0);
161
162         knee = ((float)height * stp[0] / 115.0f); // -18dB
163
164         UINT_TO_RGBA (clr[2], &r, &g, &b, &a);
165         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) - soft,
166                                           r/255.0, g/255.0, b/255.0);
167
168         UINT_TO_RGBA (clr[1], &r, &g, &b, &a);
169         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) + soft,
170                                           r/255.0, g/255.0, b/255.0);
171
172         UINT_TO_RGBA (clr[0], &r, &g, &b, &a); // bottom
173         cairo_pattern_add_color_stop_rgb (pat, 1.0,
174                                           r/255.0, g/255.0, b/255.0);
175
176         if (shade) {
177                 cairo_pattern_t* shade_pattern = cairo_pattern_create_linear (0.0, 0.0, width, 0.0);
178                 cairo_pattern_add_color_stop_rgba (shade_pattern, 0, 1.0, 1.0, 1.0, 0.2);
179                 cairo_pattern_add_color_stop_rgba (shade_pattern, 1, 0.0, 0.0, 0.0, 0.3);
180
181                 cairo_surface_t* surface;
182                 cairo_t* tc = 0;
183                 surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
184                 tc = cairo_create (surface);
185                 cairo_set_source (tc, pat);
186                 cairo_rectangle (tc, 0, 0, width, height);
187                 cairo_fill (tc);
188                 cairo_set_source (tc, shade_pattern);
189                 cairo_rectangle (tc, 0, 0, width, height);
190                 cairo_fill (tc);
191
192                 cairo_pattern_destroy (pat);
193                 cairo_pattern_destroy (shade_pattern);
194
195                 pat = cairo_pattern_create_for_surface (surface);
196
197                 cairo_destroy (tc);
198                 cairo_surface_destroy (surface);
199         }
200
201         Cairo::RefPtr<Cairo::Pattern> p (new Cairo::Pattern (pat, false));
202
203         return p;
204 }
205
206
207 Cairo::RefPtr<Cairo::Pattern>
208 FastMeter::generate_meter_background (
209                 int width, int height, int *clr, bool shade)
210 {
211         guint8 r0,g0,b0,r1,g1,b1,a;
212
213         cairo_pattern_t* pat = cairo_pattern_create_linear (0.0, 0.0, 0.0, height);
214
215         UINT_TO_RGBA (clr[0], &r0, &g0, &b0, &a);
216         UINT_TO_RGBA (clr[1], &r1, &g1, &b1, &a);
217
218         cairo_pattern_add_color_stop_rgb (pat, 0.0,
219                                           r1/255.0, g1/255.0, b1/255.0);
220
221         cairo_pattern_add_color_stop_rgb (pat, 1.0,
222                                           r0/255.0, g0/255.0, b0/255.0);
223
224         if (shade) {
225                 cairo_pattern_t* shade_pattern = cairo_pattern_create_linear (0.0, 0.0, width, 0.0);
226                 cairo_pattern_add_color_stop_rgba (shade_pattern, 0.0, 1.0, 1.0, 1.0, 0.15);
227                 cairo_pattern_add_color_stop_rgba (shade_pattern, 0.6, 0.0, 0.0, 0.0, 0.10);
228                 cairo_pattern_add_color_stop_rgba (shade_pattern, 1.0, 1.0, 1.0, 1.0, 0.20);
229
230                 cairo_surface_t* surface;
231                 cairo_t* tc = 0;
232                 surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
233                 tc = cairo_create (surface);
234                 cairo_set_source (tc, pat);
235                 cairo_rectangle (tc, 0, 0, width, height);
236                 cairo_fill (tc);
237                 cairo_set_source (tc, shade_pattern);
238                 cairo_rectangle (tc, 0, 0, width, height);
239                 cairo_fill (tc);
240
241                 cairo_pattern_destroy (pat);
242                 cairo_pattern_destroy (shade_pattern);
243
244                 pat = cairo_pattern_create_for_surface (surface);
245
246                 cairo_destroy (tc);
247                 cairo_surface_destroy (surface);
248         }
249
250         Cairo::RefPtr<Cairo::Pattern> p (new Cairo::Pattern (pat, false));
251
252         return p;
253 }
254
255 Cairo::RefPtr<Cairo::Pattern>
256 FastMeter::request_vertical_meter(
257                 int width, int height, int *clr, float *stp, bool shade)
258 {
259         if (height < min_pattern_metric_size)
260                 height = min_pattern_metric_size;
261         if (height > max_pattern_metric_size)
262                 height = max_pattern_metric_size;
263
264         const Pattern10MapKey key (width, height,
265                         stp[0], stp[1], stp[2], stp[3],
266                         clr[0], clr[1], clr[2], clr[3],
267                         clr[4], clr[5], clr[6], clr[7],
268                         clr[8], clr[9]);
269
270         Pattern10Map::iterator i;
271         if ((i = vm_pattern_cache.find (key)) != vm_pattern_cache.end()) {
272                 return i->second;
273         }
274         // TODO flush pattern cache if it gets too large
275
276         Cairo::RefPtr<Cairo::Pattern> p = generate_meter_pattern (
277                 width, height, clr, stp, shade);
278         vm_pattern_cache[key] = p;
279
280         return p;
281 }
282
283 Cairo::RefPtr<Cairo::Pattern>
284 FastMeter::request_vertical_background(
285                 int width, int height, int *bgc, bool shade)
286 {
287         if (height < min_pattern_metric_size)
288                 height = min_pattern_metric_size;
289         if (height > max_pattern_metric_size)
290                 height = max_pattern_metric_size;
291
292         const PatternBgMapKey key (width, height, bgc[0], bgc[1]);
293         PatternBgMap::iterator i;
294         if ((i = vb_pattern_cache.find (key)) != vb_pattern_cache.end()) {
295                 return i->second;
296         }
297         // TODO flush pattern cache if it gets too large
298
299         Cairo::RefPtr<Cairo::Pattern> p = generate_meter_background (
300                 width, height, bgc, shade);
301         vb_pattern_cache[key] = p;
302
303         return p;
304 }
305
306
307 void
308 FastMeter::set_hold_count (long val)
309 {
310         if (val < 1) {
311                 val = 1;
312         }
313
314         hold_cnt = val;
315         hold_state = 0;
316         current_peak = 0;
317
318         queue_draw ();
319 }
320
321 void
322 FastMeter::on_size_request (GtkRequisition* req)
323 {
324         req->height = request_height;
325         req->height = max(req->height, min_pattern_metric_size);
326         req->height = min(req->height, max_pattern_metric_size);
327         req->height += 2;
328
329         req->width  = request_width;
330 }
331
332 void
333 FastMeter::on_size_allocate (Gtk::Allocation &alloc)
334 {
335         if (alloc.get_width() != request_width) {
336                 alloc.set_width (request_width);
337         }
338
339         int h = alloc.get_height();
340         h = max (h, min_pattern_metric_size + 2);
341         h = min (h, max_pattern_metric_size + 2);
342
343         if (h != alloc.get_height()) {
344                 alloc.set_height (h);
345         }
346
347         if (pixheight != h) {
348                 fgpattern = request_vertical_meter (request_width, h, _clr, _stp, true);
349                 bgpattern = request_vertical_background (request_width, h, highlight ? _bgh : _bgc, highlight);
350                 pixheight = h - 2;
351                 pixwidth  = request_width - 2;
352         }
353
354         DrawingArea::on_size_allocate (alloc);
355 }
356
357 bool
358 FastMeter::on_expose_event (GdkEventExpose* ev)
359 {
360         return vertical_expose (ev);
361 }
362
363 bool
364 FastMeter::vertical_expose (GdkEventExpose* ev)
365 {
366         Glib::RefPtr<Gdk::Window> win = get_window ();
367         gint top_of_meter;
368         GdkRectangle intersection;
369         GdkRectangle background;
370
371         cairo_t* cr = gdk_cairo_create (get_window ()->gobj());
372
373         cairo_rectangle (cr, ev->area.x, ev->area.y, ev->area.width, ev->area.height);
374         cairo_clip (cr);
375
376         cairo_set_source_rgb (cr, 0, 0, 0); // black
377         rounded_rectangle (cr, 0, 0, pixrect.width + 2, pixheight + 2, 2);
378         cairo_stroke (cr);
379
380         top_of_meter = (gint) floor (pixheight * current_level);
381
382         /* reset the height & origin of the rect that needs to show the pixbuf
383          */
384
385         pixrect.height = top_of_meter;
386         pixrect.y = 1 + pixheight - top_of_meter;
387
388         background.x = 1;
389         background.y = 1;
390         background.width = pixrect.width;
391         background.height = pixheight - top_of_meter;
392
393         if (gdk_rectangle_intersect (&background, &ev->area, &intersection)) {
394                 cairo_set_source (cr, bgpattern->cobj());
395                 cairo_rectangle (cr, intersection.x, intersection.y, intersection.width, intersection.height);
396                 cairo_fill (cr);
397         }
398
399         if (gdk_rectangle_intersect (&pixrect, &ev->area, &intersection)) {
400                 // draw the part of the meter image that we need. the area we draw is bounded "in reverse" (top->bottom)
401                 cairo_set_source (cr, fgpattern->cobj());
402                 cairo_rectangle (cr, intersection.x, intersection.y, intersection.width, intersection.height);
403                 cairo_fill (cr);
404         }
405
406         // draw peak bar
407
408         if (hold_state) {
409                 last_peak_rect.x = 1;
410                 last_peak_rect.width = pixwidth;
411                 last_peak_rect.y = max(1, 1 + pixheight - (gint) floor (pixheight * current_peak));
412                 if (bright_hold) {
413                         last_peak_rect.height = max(0, min(4, pixheight - last_peak_rect.y -1 ));
414                 } else {
415                         last_peak_rect.height = max(0, min(2, pixheight - last_peak_rect.y -1 ));
416                 }
417
418                 cairo_set_source (cr, fgpattern->cobj());
419                 cairo_rectangle (cr, 1, last_peak_rect.y, pixwidth, last_peak_rect.height);
420                 if (bright_hold) {
421                         cairo_fill_preserve (cr);
422                         cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, 0.3);
423                 }
424                 cairo_fill (cr);
425
426         } else {
427                 last_peak_rect.width = 0;
428                 last_peak_rect.height = 0;
429         }
430
431         cairo_destroy (cr);
432
433         return TRUE;
434 }
435
436 void
437 FastMeter::set (float lvl, float peak)
438 {
439         float old_level = current_level;
440         float old_peak = current_peak;
441
442         if (peak == -1) {
443                 if (lvl >= current_peak) {
444                         current_peak = lvl;
445                         hold_state = hold_cnt;
446                 }
447
448                 if (hold_state > 0) {
449                         if (--hold_state == 0) {
450                                 current_peak = lvl;
451                         }
452                 }
453                 bright_hold = false;
454         } else {
455                 current_peak = peak;
456                 hold_state = 1;
457                 bright_hold = true;
458         }
459
460         current_level = lvl;
461
462         if (current_level == old_level && current_peak == old_peak && (hold_state == 0 || peak != -1)) {
463                 return;
464         }
465
466         Glib::RefPtr<Gdk::Window> win;
467
468         if ((win = get_window()) == 0) {
469                 queue_draw ();
470                 return;
471         }
472
473         queue_vertical_redraw (win, old_level);
474 }
475
476 void
477 FastMeter::queue_vertical_redraw (const Glib::RefPtr<Gdk::Window>& win, float old_level)
478 {
479         GdkRectangle rect;
480
481         gint new_top = (gint) floor (pixheight * current_level);
482
483         rect.x = 1;
484         rect.width = pixwidth;
485         rect.height = new_top;
486         rect.y = 1 + pixheight - new_top;
487
488         if (current_level > old_level) {
489                 /* colored/pixbuf got larger, just draw the new section */
490                 /* rect.y stays where it is because of X coordinates */
491                 /* height of invalidated area is between new.y (smaller) and old.y
492                    (larger).
493                    X coordinates just make my brain hurt.
494                 */
495                 rect.height = pixrect.y - rect.y;
496         } else {
497                 /* it got smaller, compute the difference */
498                 /* rect.y becomes old.y (the smaller value) */
499                 rect.y = pixrect.y;
500                 /* rect.height is the old.y (smaller) minus the new.y (larger)
501                 */
502                 rect.height = pixrect.height - rect.height;
503         }
504
505         GdkRegion* region = 0;
506         bool queue = false;
507
508         if (rect.height != 0) {
509
510                 /* ok, first region to draw ... */
511
512                 region = gdk_region_rectangle (&rect);
513                 queue = true;
514         }
515
516         /* redraw the last place where the last peak hold bar was;
517            the next expose will draw the new one whether its part of
518            expose region or not.
519         */
520
521         if (last_peak_rect.width * last_peak_rect.height != 0) {
522                 if (!queue) {
523                         region = gdk_region_new ();
524                         queue = true;
525                 }
526                 gdk_region_union_with_rect (region, &last_peak_rect);
527         }
528
529         if (hold_state && current_peak > 0) {
530                 if (!queue) {
531                         region = gdk_region_new ();
532                         queue = true;
533                 }
534                 rect.x = 1;
535                 rect.y = max(1, 1 + pixheight - (gint) floor (pixheight * current_peak));
536                 if (bright_hold) {
537                         rect.height = max(0, min(4, pixheight - last_peak_rect.y -1 ));
538                 } else {
539                         rect.height = max(0, min(2, pixheight - last_peak_rect.y -1 ));
540                 }
541                 rect.width = pixwidth;
542                 gdk_region_union_with_rect (region, &rect);
543         }
544
545         if (queue) {
546                 gdk_window_invalidate_region (win->gobj(), region, true);
547         }
548         if (region) {
549                 gdk_region_destroy(region);
550                 region = 0;
551         }
552 }
553
554 void
555 FastMeter::set_highlight (bool onoff)
556 {
557         if (highlight == onoff) {
558                 return;
559         }
560         highlight = onoff;
561         bgpattern = request_vertical_background (request_width, pixheight, highlight ? _bgh : _bgc, highlight);
562         queue_draw ();
563 }
564
565 void
566 FastMeter::clear ()
567 {
568         current_level = 0;
569         current_peak = 0;
570         hold_state = 0;
571         queue_draw ();
572 }