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