1 /*
2 * Copyright (C) 2006-2009 David Robillard <d@drobilla.net>
3 * Copyright (C) 2006-2017 Paul Davis <paul@linuxaudiosystems.com>
4 * Copyright (C) 2006 Sampo Savolainen <v2@iki.fi>
5 * Copyright (C) 2016-2017 Robin Gareus <robin@gareus.org>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 */
21
22 #include <iostream>
23
24 #include <glibmm.h>
25 #include <glibmm/refptr.h>
26
27 #include <gdkmm/gc.h>
28
29 #include <gtkmm/widget.h>
30 #include <gtkmm/style.h>
31 #include <gtkmm/treemodel.h>
32 #include <gtkmm/treepath.h>
33
34 #include "pbd/stl_delete.h"
35
36 #include <math.h>
37
38 #include "fft_graph.h"
39 #include "analysis_window.h"
40 #include "public_editor.h"
41
42 #include "pbd/i18n.h"
43
44 using namespace std;
45 using namespace Gtk;
46 using namespace Gdk;
47
FFTGraph(int windowSize)48 FFTGraph::FFTGraph (int windowSize)
49 {
50 _logScale = 0;
51
52 _in = 0;
53 _out = 0;
54 _hanning = 0;
55 _logScale = 0;
56
57 _surface = 0;
58 _a_window = 0;
59
60 _show_minmax = false;
61 _show_normalized = false;
62 _show_proportional = false;
63
64 _ann_x = _ann_y = -1;
65 _yoff = v_margin;
66 _ann_area.width = 0;
67 _ann_area.height = 0;
68
69 setWindowSize (windowSize);
70 set_events (Gdk::POINTER_MOTION_MASK | Gdk::LEAVE_NOTIFY_MASK | Gdk::BUTTON_PRESS_MASK);
71 }
72
73 void
setWindowSize(int windowSize)74 FFTGraph::setWindowSize (int windowSize)
75 {
76 if (_a_window) {
77 Glib::Threads::Mutex::Lock lm (_a_window->track_list_lock);
78 setWindowSize_internal (windowSize);
79 } else {
80 setWindowSize_internal (windowSize);
81 }
82 }
83
84 void
setWindowSize_internal(int windowSize)85 FFTGraph::setWindowSize_internal (int windowSize)
86 {
87 // remove old tracklist & graphs
88 if (_a_window) {
89 _a_window->clear_tracklist ();
90 }
91
92 _windowSize = windowSize;
93 _dataSize = windowSize / 2;
94 if (_in != 0) {
95 fftwf_destroy_plan (_plan);
96 free (_in);
97 _in = 0;
98 }
99
100 if (_out != 0) {
101 free (_out);
102 _out = 0;
103 }
104
105 if (_hanning != 0) {
106 free (_hanning);
107 _hanning = 0;
108 }
109
110 if (_logScale != 0) {
111 free (_logScale);
112 _logScale = 0;
113 }
114
115 // When destroying, window size is set to zero to free up memory
116 if (windowSize == 0) {
117 return;
118 }
119
120 // FFT input & output buffers
121 _in = (float *) fftwf_malloc (sizeof (float) * _windowSize);
122 _out = (float *) fftwf_malloc (sizeof (float) * _windowSize);
123
124 // Hanning window
125 _hanning = (float *) malloc (sizeof (float) * _windowSize);
126
127 // normalize the window
128 double sum = 0.0;
129
130 for (unsigned int i = 0; i < _windowSize; ++i) {
131 _hanning[i] = 0.5f - (0.5f * (float) cos (2.0f * M_PI * (float)i / (float)(_windowSize)));
132 sum += _hanning[i];
133 }
134
135 double isum = 2.0 / sum;
136
137 for (unsigned int i = 0; i < _windowSize; i++) {
138 _hanning[i] *= isum;
139 }
140
141 _logScale = (int *) malloc (sizeof (int) * _dataSize);
142
143 for (unsigned int i = 0; i < _dataSize; i++) {
144 _logScale[i] = 0;
145 }
146 _plan = fftwf_plan_r2r_1d (_windowSize, _in, _out, FFTW_R2HC, FFTW_MEASURE);
147 }
148
~FFTGraph()149 FFTGraph::~FFTGraph ()
150 {
151 // This will free everything
152 setWindowSize (0);
153
154 if (_surface) {
155 cairo_surface_destroy (_surface);
156 }
157 }
158
159 bool
on_expose_event(GdkEventExpose * event)160 FFTGraph::on_expose_event (GdkEventExpose* event)
161 {
162 cairo_t* cr = gdk_cairo_create (GDK_DRAWABLE (get_window ()->gobj ()));
163 cairo_rectangle (cr, event->area.x, event->area.y, event->area.width, event->area.height);
164 cairo_clip (cr);
165
166 cairo_set_source_surface(cr, _surface, 0, 0);
167 cairo_paint (cr);
168
169
170 if (_ann_x > 0 && _ann_y > 0) {
171 const float x = _ann_x - hl_margin;
172 const float freq = expf(_fft_log_base * x / currentScaleWidth) * _fft_start;
173
174 std::stringstream ss;
175 if (freq >= 10000) {
176 ss << std::setprecision (1) << std::fixed << freq / 1000 << " kHz";
177 } else if (freq >= 1000) {
178 ss << std::setprecision (2) << std::fixed << freq / 1000 << " kHz";
179 } else {
180 ss << std::setprecision (0) << std::fixed << freq << " Hz";
181 }
182 layout->set_text (ss.str ());
183 int lw, lh;
184 layout->get_pixel_size (lw, lh);
185 lw|=1; lh|=1;
186
187 const float y0 = _ann_y - lh - 7;
188
189 _ann_area.x = _ann_x - 1 - lw * .5;
190 _ann_area.y = y0 - 1;
191 _ann_area.width = lw + 3;
192 _ann_area.height = lh + 8;
193
194 cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, 0.7);
195 cairo_rectangle (cr, _ann_x - 1 - lw * .5, y0 - 1, lw + 2, lh + 2);
196 cairo_fill (cr);
197
198 cairo_move_to (cr, _ann_x , _ann_y - 0.5);
199 cairo_rel_line_to (cr, -3.0, -5.5);
200 cairo_rel_line_to (cr, 6, 0);
201 cairo_close_path (cr);
202 cairo_fill (cr);
203
204 cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 1.0);
205 cairo_move_to (cr, _ann_x - lw / 2, y0);
206 pango_cairo_update_layout (cr, layout->gobj ());
207 pango_cairo_show_layout (cr, layout->gobj ());
208
209 }
210
211 #ifdef HARLEQUIN_DEBUGGING
212 cairo_rectangle (cr, 0, 0, width, height);
213 cairo_set_source_rgba (cr, (random() % 255) / 255.f, (random() % 255) / 255.f, 0.0, 0.5);
214 cairo_fill (cr);
215 #endif
216
217 cairo_destroy (cr);
218 return true;
219 }
220
221 bool
on_motion_notify_event(GdkEventMotion * ev)222 FFTGraph::on_motion_notify_event (GdkEventMotion* ev)
223 {
224 gint x, y;
225
226 x = (int) floor (ev->x);
227 y = (int) floor (ev->y);
228
229 if (x <= hl_margin + 1 || x >= width - hr_margin) {
230 x = -1;
231 }
232 if (y <= _yoff || y >= height - v_margin - 1) {
233 y = -1;
234 }
235
236 if (x == _ann_x && y == _ann_y) {
237 return true;
238 }
239 _ann_x = x;
240 _ann_y = y;
241
242 if (_ann_area.width == 0 || _ann_area.height == 0) {
243 queue_draw ();
244 } else {
245 queue_draw_area (_ann_area.x, _ann_area.y, _ann_area.width, _ann_area.height + 1);
246 }
247
248 if (_ann_x > 0 &&_ann_y > 0) {
249 queue_draw_area (_ann_x - _ann_area.width, _ann_y - _ann_area.height - 1, _ann_area.width * 2, _ann_area.height + 2);
250 }
251
252 return true;
253 }
254
255 bool
on_leave_notify_event(GdkEventCrossing *)256 FFTGraph::on_leave_notify_event (GdkEventCrossing *)
257 {
258 if (_ann_x == -1 && _ann_y == -1) {
259 return true;
260 }
261 _ann_x = _ann_y = -1;
262 if (_ann_area.width == 0 || _ann_area.height == 0) {
263 queue_draw ();
264 } else {
265 queue_draw_area (_ann_area.x, _ann_area.y, _ann_area.width, _ann_area.height + 1);
266 }
267 _ann_area.width = _ann_area.height = 0;
268 return false;
269 }
270
271 FFTResult *
prepareResult(Gdk::Color color,string trackname)272 FFTGraph::prepareResult (Gdk::Color color, string trackname)
273 {
274 FFTResult *res = new FFTResult (this, color, trackname);
275
276 return res;
277 }
278
279 void
set_analysis_window(AnalysisWindow * a_window)280 FFTGraph::set_analysis_window (AnalysisWindow *a_window)
281 {
282 _a_window = a_window;
283 }
284
285 int
draw_scales(cairo_t * cr)286 FFTGraph::draw_scales (cairo_t* cr)
287 {
288 int label_height = v_margin;
289
290 cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 1.0);
291 cairo_rectangle (cr, 0, 0, width, height);
292 cairo_fill (cr);
293
294 /*
295 * 1 5
296 * _ _
297 * | |
298 * 2 | | 4
299 * |________|
300 * 3
301 */
302
303 cairo_set_line_width (cr, 1.0);
304 cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, 1.0);
305 cairo_move_to (cr, 3 , .5 + v_margin);
306 cairo_line_to (cr, .5 + hl_margin , .5 + v_margin); // 1
307 cairo_line_to (cr, .5 + hl_margin , .5 + height - v_margin); // 2
308 cairo_line_to (cr, 1.5 + width - hr_margin, .5 + height - v_margin); // 3
309 cairo_line_to (cr, 1.5 + width - hr_margin, .5 + v_margin); // 4
310 cairo_line_to (cr, width - 3 , .5 + v_margin); // 5
311 cairo_stroke (cr);
312
313 if (! layout) {
314 layout = create_pango_layout ("");
315 layout->set_font_description (get_style ()->get_font ());
316 }
317
318 // Draw x-axis scale 1/3 octaves centered around 1K
319 int overlap = 0;
320
321 // make sure 1K (x=0) is visible
322 for (int x = 0; x < 27; ++x) {
323 float freq = powf (2.f, x / 3.0) * 1000.f;
324 if (freq <= _fft_start) { continue; }
325 if (freq >= _fft_end) { break; }
326
327 const float pos = currentScaleWidth * logf (freq / _fft_start) / _fft_log_base;
328 const int coord = floor (hl_margin + pos);
329
330 if (coord < overlap) {
331 continue;
332 }
333
334 std::stringstream ss;
335 if (freq >= 10000) {
336 ss << std::setprecision (1) << std::fixed << freq / 1000 << "k";
337 } else if (freq >= 1000) {
338 ss << std::setprecision (2) << std::fixed << freq / 1000 << "k";
339 } else {
340 ss << std::setprecision (0) << std::fixed << freq << "Hz";
341 }
342 layout->set_text (ss.str ());
343 int lw, lh;
344 layout->get_pixel_size (lw, lh);
345 overlap = coord + lw + 3;
346
347 if (coord + lw / 2 > width - hr_margin - 2) {
348 break;
349 }
350 if (v_margin / 2 + lh > label_height) {
351 label_height = v_margin / 2 + lh;
352 }
353
354 cairo_set_source_rgba (cr, 0.2, 0.2, 0.2, 1.0);
355 cairo_move_to (cr, coord, v_margin);
356 cairo_line_to (cr, coord, height - v_margin - 1);
357 cairo_stroke (cr);
358
359 cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, 1.0);
360 cairo_move_to (cr, coord - lw / 2, v_margin / 2);
361 pango_cairo_update_layout (cr, layout->gobj ());
362 pango_cairo_show_layout (cr, layout->gobj ());
363 }
364
365 // now from 1K down to 4Hz
366 for (int x = 0; x > -24; --x) {
367 float freq = powf (2.f, x / 3.0) * 1000.f;
368 if (freq >= _fft_end) { continue; }
369 if (freq <= _fft_start) { break; }
370
371 const float pos = currentScaleWidth * logf (freq / _fft_start) / _fft_log_base;
372 const int coord = floor (hl_margin + pos);
373
374 if (x != 0 && coord > overlap) {
375 continue;
376 }
377
378 std::stringstream ss;
379 if (freq >= 10000) {
380 ss << std::setprecision (1) << std::fixed << freq / 1000 << "k";
381 } else if (freq >= 1000) {
382 ss << std::setprecision (2) << std::fixed << freq / 1000 << "k";
383 } else {
384 ss << std::setprecision (0) << std::fixed << freq << "Hz";
385 }
386 layout->set_text (ss.str ());
387 int lw, lh;
388 layout->get_pixel_size (lw, lh);
389
390 overlap = coord - lw - 3;
391
392 if (coord - lw / 2 < hl_margin + 2) {
393 break;
394 }
395 if (x == 0) {
396 // just get overlap position
397 continue;
398 }
399 if (v_margin / 2 + lh > label_height) {
400 label_height = v_margin / 2 + lh;
401 }
402
403
404 cairo_set_source_rgba (cr, 0.2, 0.2, 0.2, 1.0);
405 cairo_move_to (cr, coord, v_margin);
406 cairo_line_to (cr, coord, height - v_margin - 1);
407 cairo_stroke (cr);
408
409 cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, 1.0);
410 cairo_move_to (cr, coord - lw / 2, v_margin / 2);
411 pango_cairo_update_layout (cr, layout->gobj ());
412 pango_cairo_show_layout (cr, layout->gobj ());
413 }
414
415 return label_height;
416 }
417
418 void
redraw()419 FFTGraph::redraw ()
420 {
421 assert (_surface);
422 cairo_t* cr = cairo_create (_surface);
423
424 _yoff = draw_scales (cr);
425
426 if (_a_window == 0) {
427 cairo_destroy (cr);
428 queue_draw ();
429 return;
430 }
431
432 Glib::Threads::Mutex::Lock lm (_a_window->track_list_lock);
433
434 if (!_a_window->track_list_ready) {
435 cairo_destroy (cr);
436 queue_draw ();
437 return;
438 }
439
440 float minf;
441 float maxf;
442
443 TreeNodeChildren track_rows = _a_window->track_list.get_model ()->children ();
444
445 if (!_show_normalized) {
446 maxf = 0.0f;
447 minf = -108.0f;
448 } else {
449 minf = 999.0f;
450 maxf = -999.0f;
451 for (TreeIter i = track_rows.begin (); i != track_rows.end (); i++) {
452 TreeModel::Row row = *i;
453 FFTResult *res = row[_a_window->tlcols.graph];
454
455 // disregard fft analysis from empty signals
456 if (res->minimum (_show_proportional) == res->maximum (_show_proportional)) {
457 continue;
458 }
459 // don't include invisible graphs
460 if (!row[_a_window->tlcols.visible]) {
461 continue;
462 }
463
464 minf = std::min (minf, res->minimum (_show_proportional));
465 maxf = std::max (maxf, res->maximum (_show_proportional));
466 }
467 }
468
469 // clamp range, > -200dBFS, at least 24dB (two y-axis labels) range
470 minf = std::max (-200.f, minf);
471 if (maxf <= minf) {
472 cairo_destroy (cr);
473 queue_draw ();
474 return;
475 }
476
477 if (maxf - minf < 24) {
478 maxf += 6.f;
479 minf = maxf - 24.f;
480 }
481
482 cairo_set_line_width (cr, 1.5);
483 cairo_translate (cr, hl_margin + 1, _yoff);
484
485 float fft_pane_size_w = width - hl_margin - hr_margin;
486 float fft_pane_size_h = height - v_margin - 1 - _yoff;
487 double pixels_per_db = (double)fft_pane_size_h / (double)(maxf - minf);
488
489 // draw y-axis dB
490 cairo_set_source_rgba (cr, .8, .8, .8, 1.0);
491
492 int btm_lbl = fft_pane_size_h;
493 {
494 // y-axis legend
495 layout->set_text (_("dBFS"));
496 int lw, lh;
497 layout->get_pixel_size (lw, lh);
498 cairo_move_to (cr, -2 - lw, fft_pane_size_h - lh / 2);
499 pango_cairo_update_layout (cr, layout->gobj ());
500 pango_cairo_show_layout (cr, layout->gobj ());
501 btm_lbl = fft_pane_size_h - lh;
502 }
503
504 for (int x = -6; x >= -200; x -= 12) {
505 float yp = 1.5 + fft_pane_size_h - rint ((x - minf) * pixels_per_db);
506
507 assert (layout);
508 std::stringstream ss;
509 ss << x;
510 layout->set_text (ss.str ());
511 int lw, lh;
512 layout->get_pixel_size (lw, lh);
513
514 if (yp + 2 + lh / 2 > btm_lbl) {
515 continue;
516 }
517 if (yp < 2 + lh / 2) {
518 continue;
519 }
520
521 cairo_set_source_rgba (cr, .8, .8, .8, 1.0);
522 cairo_move_to (cr, -2 - lw, yp - lh / 2);
523 pango_cairo_update_layout (cr, layout->gobj ());
524 pango_cairo_show_layout (cr, layout->gobj ());
525
526 cairo_set_source_rgba (cr, .2, .2, .2, 1.0);
527 cairo_move_to (cr, 0, yp);
528 cairo_line_to (cr, fft_pane_size_w, yp);
529 cairo_stroke (cr);
530 }
531
532 cairo_rectangle (cr, 1, 1, fft_pane_size_w, fft_pane_size_h);
533 cairo_clip (cr);
534
535 cairo_set_line_cap (cr, CAIRO_LINE_CAP_BUTT);
536 cairo_set_line_join (cr, CAIRO_LINE_JOIN_ROUND);
537
538 for (TreeIter i = track_rows.begin (); i != track_rows.end (); i++) {
539 TreeModel::Row row = *i;
540
541 // don't show graphs for tracks which are deselected
542 if (!row[_a_window->tlcols.visible]) {
543 continue;
544 }
545
546 FFTResult *res = row[_a_window->tlcols.graph];
547
548 // don't show graphs for empty signals
549 if (res->minimum (_show_proportional) == res->maximum (_show_proportional)) {
550 continue;
551 }
552
553 float mpp;
554 float X,Y;
555
556 if (_show_minmax) {
557
558 X = 0.5f + _logScale[0];
559 Y = 1.5f + fft_pane_size_h - pixels_per_db * (res->maxAt (0, _show_proportional) - minf);
560 cairo_move_to (cr, X, Y);
561
562 // Draw the line of maximum values
563 mpp = minf;
564 for (unsigned int x = 1; x < res->length () - 1; ++x) {
565 mpp = std::max (mpp, res->maxAt (x, _show_proportional));
566
567 if (_logScale[x] == _logScale[x + 1]) {
568 continue;
569 }
570
571 mpp = fmin (mpp, maxf);
572 X = 0.5f + _logScale[x];
573 Y = 1.5f + fft_pane_size_h - pixels_per_db * (mpp - minf);
574 cairo_line_to (cr, X, Y);
575 mpp = minf;
576 }
577
578 mpp = maxf;
579 // Draw back to the start using the minimum value
580 for (int x = res->length () - 1; x >= 0; --x) {
581 mpp = std::min (mpp, res->minAt (x, _show_proportional));
582
583 if (_logScale[x] == _logScale[x + 1]) {
584 continue;
585 }
586
587 mpp = fmax (mpp, minf);
588 X = 0.5f + _logScale[x];
589 Y = 1.5f + fft_pane_size_h - pixels_per_db * (mpp - minf);
590 cairo_line_to (cr, X, Y);
591 mpp = maxf;
592 }
593
594 cairo_set_source_rgba (cr, res->get_color ().get_red_p (), res->get_color ().get_green_p (), res->get_color ().get_blue_p (), 0.30);
595 cairo_close_path (cr);
596 cairo_fill (cr);
597 }
598
599 // draw max of averages
600 X = 0.5f + _logScale[0];
601 Y = 1.5f + fft_pane_size_h - pixels_per_db * (res->avgAt (0, _show_proportional) - minf);
602 cairo_move_to (cr, X, Y);
603
604 mpp = minf;
605 for (unsigned int x = 0; x < res->length () - 1; x++) {
606 mpp = std::max (mpp, res->avgAt (x, _show_proportional));
607
608 if (_logScale[x] == _logScale[x + 1]) {
609 continue;
610 }
611
612 mpp = fmax (mpp, minf);
613 mpp = fmin (mpp, maxf);
614
615 X = 0.5f + _logScale[x];
616 Y = 1.5f + fft_pane_size_h - pixels_per_db * (mpp - minf);
617 cairo_line_to (cr, X, Y);
618 mpp = minf;
619 }
620
621 cairo_set_source_rgb (cr, res->get_color ().get_red_p (), res->get_color ().get_green_p (), res->get_color ().get_blue_p ());
622 cairo_stroke (cr);
623 }
624 cairo_destroy (cr);
625 queue_draw ();
626 }
627
628 void
on_size_request(Gtk::Requisition * requisition)629 FFTGraph::on_size_request (Gtk::Requisition* requisition)
630 {
631 width = max (requisition->width, minScaleWidth + hl_margin + hr_margin);
632 height = max (requisition->height, minScaleHeight + 2 + v_margin * 2);
633
634 requisition->width = width;;
635 requisition->height = height;
636 }
637
638 void
on_size_allocate(Gtk::Allocation & alloc)639 FFTGraph::on_size_allocate (Gtk::Allocation & alloc)
640 {
641 width = alloc.get_width ();
642 height = alloc.get_height ();
643
644 update_size ();
645
646 DrawingArea::on_size_allocate (alloc);
647 }
648
649 void
update_size()650 FFTGraph::update_size ()
651 {
652 samplecnt_t SR = PublicEditor::instance ().session ()->nominal_sample_rate ();
653 _fft_start = SR / (double)_dataSize;
654 _fft_end = .5 * SR;
655 _fft_log_base = logf (.5 * _dataSize);
656 currentScaleWidth = width - hl_margin - hr_margin;
657 _logScale[0] = 0;
658 for (unsigned int i = 1; i < _dataSize; ++i) {
659 _logScale[i] = floor (currentScaleWidth * logf (.5 * i) / _fft_log_base);
660 }
661 if (_surface) {
662 cairo_surface_destroy (_surface);
663 }
664 _surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
665 redraw ();
666 }
667