1 /*
2 * ui_infoarea.c
3 * Copyright 2010-2012 William Pitcock and John Lindgren
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice,
9 * this list of conditions, and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions, and the following disclaimer in the documentation
13 * provided with the distribution.
14 *
15 * This software is provided "as is" and without any warranty, express or
16 * implied. In no event shall the authors be liable for any damages arising from
17 * the use of this software.
18 */
19
20 #include <math.h>
21 #include <string.h>
22
23 #include <gtk/gtk.h>
24
25 #include <libaudcore/drct.h>
26 #include <libaudcore/hook.h>
27 #include <libaudcore/interface.h>
28 #include <libaudgui/libaudgui-gtk.h>
29
30 #include "ui_infoarea.h"
31
32 #define VIS_BANDS 12
33 #define VIS_DELAY 2 /* delay before falloff in frames */
34 #define VIS_FALLOFF 2 /* falloff in decibels per frame */
35
36 int SPACING, ICON_SIZE, HEIGHT, BAND_WIDTH, BAND_SPACING, VIS_WIDTH, VIS_SCALE, VIS_CENTER;
37
compute_sizes()38 static void compute_sizes ()
39 {
40 int dpi = audgui_get_dpi ();
41
42 SPACING = aud::rescale (dpi, 12, 1);
43 ICON_SIZE = 2 * aud::rescale (dpi, 3, 1); // should be divisible by 2
44 HEIGHT = ICON_SIZE + 2 * SPACING;
45 BAND_WIDTH = aud::rescale (dpi, 16, 1);
46 BAND_SPACING = aud::rescale (dpi, 48, 1);
47 VIS_WIDTH = VIS_BANDS * (BAND_WIDTH + BAND_SPACING) - BAND_SPACING + 2 * SPACING;
48 VIS_SCALE = aud::rescale (ICON_SIZE, 8, 5);
49 VIS_CENTER = VIS_SCALE + SPACING;
50 }
51
52 typedef struct {
53 GtkWidget * box, * main;
54
55 String title, artist, album;
56 String last_title, last_artist, last_album;
57 AudguiPixbuf pb, last_pb;
58 float alpha, last_alpha;
59
60 bool show_art;
61 bool stopped;
62 } UIInfoArea;
63
64 class InfoAreaVis : public Visualizer
65 {
66 public:
InfoAreaVis()67 constexpr InfoAreaVis () :
68 Visualizer (Freq) {}
69
70 GtkWidget * widget = nullptr;
71 float bars[VIS_BANDS] {};
72 char delay[VIS_BANDS] {};
73
74 void clear ();
75 void render_freq (const float * freq);
76 };
77
78 static InfoAreaVis vis;
79
80 /****************************************************************************/
81
82 static UIInfoArea * area = nullptr;
83
render_freq(const float * freq)84 void InfoAreaVis::render_freq (const float * freq)
85 {
86 /* xscale[i] = pow (256, i / VIS_BANDS) - 0.5; */
87 const float xscale[VIS_BANDS + 1] = {0.5, 1.09, 2.02, 3.5, 5.85, 9.58,
88 15.5, 24.9, 39.82, 63.5, 101.09, 160.77, 255.5};
89
90 for (int i = 0; i < VIS_BANDS; i ++)
91 {
92 /* 40 dB range */
93 float x = 40 + compute_freq_band (freq, xscale, i, VIS_BANDS);
94
95 bars[i] -= aud::max (0, VIS_FALLOFF - delay[i]);
96
97 if (delay[i])
98 delay[i] --;
99
100 if (x > bars[i])
101 {
102 bars[i] = x;
103 delay[i] = VIS_DELAY;
104 }
105 }
106
107 if (widget)
108 gtk_widget_queue_draw (widget);
109 }
110
clear()111 void InfoAreaVis::clear ()
112 {
113 memset (bars, 0, sizeof bars);
114 memset (delay, 0, sizeof delay);
115
116 if (widget)
117 gtk_widget_queue_draw (widget);
118 }
119
120 /****************************************************************************/
121
clear(GtkWidget * widget,cairo_t * cr)122 static void clear (GtkWidget * widget, cairo_t * cr)
123 {
124 GtkAllocation alloc;
125 gtk_widget_get_allocation (widget, & alloc);
126
127 auto & c = (gtk_widget_get_style (widget))->base[GTK_STATE_NORMAL];
128 cairo_pattern_t * gradient = audgui_dark_bg_gradient (c, alloc.height);
129
130 cairo_set_source (cr, gradient);
131 cairo_rectangle (cr, 0, 0, alloc.width, alloc.height);
132 cairo_fill (cr);
133
134 cairo_pattern_destroy (gradient);
135 }
136
draw_text(GtkWidget * widget,cairo_t * cr,int x,int y,int width,float r,float g,float b,float a,const char * font,const char * text)137 static void draw_text (GtkWidget * widget, cairo_t * cr, int x, int y, int
138 width, float r, float g, float b, float a, const char * font,
139 const char * text)
140 {
141 cairo_move_to (cr, x, y);
142 cairo_set_source_rgba (cr, r, g, b, a);
143
144 PangoFontDescription * desc = pango_font_description_from_string (font);
145 PangoLayout * pl = gtk_widget_create_pango_layout (widget, nullptr);
146 pango_layout_set_text (pl, text, -1);
147 pango_layout_set_font_description (pl, desc);
148 pango_font_description_free (desc);
149 pango_layout_set_width (pl, width * PANGO_SCALE);
150 pango_layout_set_ellipsize (pl, PANGO_ELLIPSIZE_END);
151
152 pango_cairo_show_layout (cr, pl);
153
154 g_object_unref (pl);
155 }
156
157 /****************************************************************************/
158
expose_vis_cb(GtkWidget * widget,GdkEventExpose * event)159 static int expose_vis_cb (GtkWidget * widget, GdkEventExpose * event)
160 {
161 cairo_t * cr = gdk_cairo_create (gtk_widget_get_window (widget));
162 auto & c = (gtk_widget_get_style (widget))->base[GTK_STATE_SELECTED];
163
164 clear (widget, cr);
165
166 for (int i = 0; i < VIS_BANDS; i++)
167 {
168 int x = SPACING + i * (BAND_WIDTH + BAND_SPACING);
169 int v = aud::clamp ((int) (vis.bars[i] * VIS_SCALE / 40), 0, VIS_SCALE);
170 int m = aud::min (VIS_CENTER + v, HEIGHT);
171
172 float r, g, b;
173 audgui_vis_bar_color (c, i, VIS_BANDS, r, g, b);
174
175 cairo_set_source_rgb (cr, r, g, b);
176 cairo_rectangle (cr, x, VIS_CENTER - v, BAND_WIDTH, v);
177 cairo_fill (cr);
178
179 cairo_set_source_rgb (cr, r * 0.3, g * 0.3, b * 0.3);
180 cairo_rectangle (cr, x, VIS_CENTER, BAND_WIDTH, m - VIS_CENTER);
181 cairo_fill (cr);
182 }
183
184 cairo_destroy (cr);
185 return true;
186 }
187
draw_album_art(cairo_t * cr)188 static void draw_album_art (cairo_t * cr)
189 {
190 g_return_if_fail (area);
191
192 if (area->pb)
193 {
194 int left = SPACING + (ICON_SIZE - area->pb.width ()) / 2;
195 int top = SPACING + (ICON_SIZE - area->pb.height ()) / 2;
196 gdk_cairo_set_source_pixbuf (cr, area->pb.get (), left, top);
197 cairo_paint_with_alpha (cr, area->alpha);
198 }
199
200 if (area->last_pb)
201 {
202 int left = SPACING + (ICON_SIZE - area->last_pb.width ()) / 2;
203 int top = SPACING + (ICON_SIZE - area->last_pb.height ()) / 2;
204 gdk_cairo_set_source_pixbuf (cr, area->last_pb.get (), left, top);
205 cairo_paint_with_alpha (cr, area->last_alpha);
206 }
207 }
208
draw_title(cairo_t * cr)209 static void draw_title (cairo_t * cr)
210 {
211 g_return_if_fail (area);
212
213 GtkAllocation alloc;
214 gtk_widget_get_allocation (area->main, & alloc);
215
216 int x = area->show_art ? HEIGHT : SPACING;
217 int y_offset1 = ICON_SIZE / 2;
218 int y_offset2 = ICON_SIZE * 3 / 4;
219 int width = alloc.width - x;
220
221 if (area->title)
222 draw_text (area->main, cr, x, SPACING, width, 1, 1, 1, area->alpha,
223 "18", area->title);
224 if (area->last_title)
225 draw_text (area->main, cr, x, SPACING, width, 1, 1, 1, area->last_alpha,
226 "18", area->last_title);
227 if (area->artist)
228 draw_text (area->main, cr, x, SPACING + y_offset1, width, 1, 1, 1,
229 area->alpha, "9", area->artist);
230 if (area->last_artist)
231 draw_text (area->main, cr, x, SPACING + y_offset1, width, 1, 1, 1,
232 area->last_alpha, "9", area->last_artist);
233 if (area->album)
234 draw_text (area->main, cr, x, SPACING + y_offset2, width, 0.7,
235 0.7, 0.7, area->alpha, "9", area->album);
236 if (area->last_album)
237 draw_text (area->main, cr, x, SPACING + y_offset2, width, 0.7,
238 0.7, 0.7, area->last_alpha, "9", area->last_album);
239 }
240
expose_cb(GtkWidget * widget,GdkEventExpose * event)241 static int expose_cb (GtkWidget * widget, GdkEventExpose * event)
242 {
243 cairo_t * cr = gdk_cairo_create (gtk_widget_get_window (widget));
244
245 clear (widget, cr);
246
247 draw_album_art (cr);
248 draw_title (cr);
249
250 cairo_destroy (cr);
251 return true;
252 }
253
ui_infoarea_do_fade(void *)254 static void ui_infoarea_do_fade (void *)
255 {
256 g_return_if_fail (area);
257 bool done = true;
258
259 if (aud_drct_get_playing () && area->alpha < 1)
260 {
261 area->alpha += 0.1;
262 done = false;
263 }
264
265 if (area->last_alpha > 0)
266 {
267 area->last_alpha -= 0.1;
268 done = false;
269 }
270
271 gtk_widget_queue_draw (area->main);
272
273 if (done)
274 timer_remove (TimerRate::Hz30, ui_infoarea_do_fade);
275 }
276
ui_infoarea_set_title()277 static void ui_infoarea_set_title ()
278 {
279 g_return_if_fail (area);
280
281 Tuple tuple = aud_drct_get_tuple ();
282 String title = tuple.get_str (Tuple::Title);
283 String artist = tuple.get_str (Tuple::Artist);
284 String album = tuple.get_str (Tuple::Album);
285
286 if (! g_strcmp0 (title, area->title) && ! g_strcmp0 (artist, area->artist)
287 && ! g_strcmp0 (album, area->album))
288 return;
289
290 area->title = std::move (title);
291 area->artist = std::move (artist);
292 area->album = std::move (album);
293
294 gtk_widget_queue_draw (area->main);
295 }
296
set_album_art()297 static void set_album_art ()
298 {
299 g_return_if_fail (area);
300
301 if (! area->show_art)
302 {
303 area->pb = AudguiPixbuf ();
304 return;
305 }
306
307 area->pb = audgui_pixbuf_request_current ();
308 if (area->pb)
309 audgui_pixbuf_scale_within (area->pb, ICON_SIZE);
310 else
311 area->pb = audgui_pixbuf_fallback ();
312 }
313
infoarea_next()314 static void infoarea_next ()
315 {
316 g_return_if_fail (area);
317
318 area->last_title = std::move (area->title);
319 area->last_artist = std::move (area->artist);
320 area->last_album = std::move (area->album);
321 area->last_pb = std::move (area->pb);
322
323 area->last_alpha = area->alpha;
324 area->alpha = 0;
325
326 gtk_widget_queue_draw (area->main);
327 }
328
ui_infoarea_playback_start()329 static void ui_infoarea_playback_start ()
330 {
331 g_return_if_fail (area);
332
333 if (! area->stopped) /* moved to the next song without stopping? */
334 infoarea_next ();
335 area->stopped = false;
336
337 ui_infoarea_set_title ();
338 set_album_art ();
339
340 timer_add (TimerRate::Hz30, ui_infoarea_do_fade);
341 }
342
ui_infoarea_playback_stop()343 static void ui_infoarea_playback_stop ()
344 {
345 g_return_if_fail (area);
346
347 infoarea_next ();
348 area->stopped = true;
349
350 timer_add (TimerRate::Hz30, ui_infoarea_do_fade);
351 }
352
realize_cb(GtkWidget * widget)353 static void realize_cb (GtkWidget * widget)
354 {
355 /* using a native window avoids redrawing parent widgets */
356 gdk_window_ensure_native (gtk_widget_get_window (widget));
357 }
358
ui_infoarea_show_art(bool show)359 void ui_infoarea_show_art (bool show)
360 {
361 if (! area)
362 return;
363
364 area->show_art = show;
365 set_album_art ();
366 gtk_widget_queue_draw (area->main);
367 }
368
ui_infoarea_show_vis(bool show)369 void ui_infoarea_show_vis (bool show)
370 {
371 if (! area)
372 return;
373
374 if (show)
375 {
376 if (vis.widget)
377 return;
378
379 vis.widget = gtk_drawing_area_new ();
380
381 /* note: "realize" signal must be connected before adding to box */
382 g_signal_connect (vis.widget, "realize", (GCallback) realize_cb, nullptr);
383
384 gtk_widget_set_size_request (vis.widget, VIS_WIDTH, HEIGHT);
385 gtk_box_pack_start ((GtkBox *) area->box, vis.widget, false, false, 0);
386
387 g_signal_connect (vis.widget, "expose-event", (GCallback) expose_vis_cb, nullptr);
388 gtk_widget_show (vis.widget);
389
390 aud_visualizer_add (& vis);
391 }
392 else
393 {
394 if (! vis.widget)
395 return;
396
397 aud_visualizer_remove (& vis);
398
399 gtk_widget_destroy (vis.widget);
400 vis.widget = nullptr;
401
402 vis.clear ();
403 }
404 }
405
destroy_cb(GtkWidget * widget)406 static void destroy_cb (GtkWidget * widget)
407 {
408 g_return_if_fail (area);
409
410 ui_infoarea_show_vis (false);
411
412 hook_dissociate ("tuple change", (HookFunction) ui_infoarea_set_title);
413 hook_dissociate ("playback ready", (HookFunction) ui_infoarea_playback_start);
414 hook_dissociate ("playback stop", (HookFunction) ui_infoarea_playback_stop);
415
416 timer_remove (TimerRate::Hz30, ui_infoarea_do_fade);
417
418 delete area;
419 area = nullptr;
420 }
421
ui_infoarea_new()422 GtkWidget * ui_infoarea_new ()
423 {
424 g_return_val_if_fail (! area, nullptr);
425
426 compute_sizes ();
427
428 area = new UIInfoArea ();
429 area->box = gtk_hbox_new (false, 0);
430
431 area->main = gtk_drawing_area_new ();
432 gtk_widget_set_size_request (area->main, HEIGHT, HEIGHT);
433 gtk_box_pack_start ((GtkBox *) area->box, area->main, true, true, 0);
434
435 g_signal_connect (area->main, "expose-event", (GCallback) expose_cb, nullptr);
436
437 hook_associate ("tuple change", (HookFunction) ui_infoarea_set_title, nullptr);
438 hook_associate ("playback ready", (HookFunction) ui_infoarea_playback_start, nullptr);
439 hook_associate ("playback stop", (HookFunction) ui_infoarea_playback_stop, nullptr);
440
441 g_signal_connect (area->box, "destroy", (GCallback) destroy_cb, nullptr);
442
443 if (aud_drct_get_ready ())
444 {
445 ui_infoarea_set_title ();
446 set_album_art ();
447
448 /* skip fade-in */
449 area->alpha = 1;
450 }
451
452 GtkWidget * frame = gtk_frame_new (nullptr);
453 gtk_frame_set_shadow_type ((GtkFrame *) frame, GTK_SHADOW_IN);
454 gtk_container_add ((GtkContainer *) frame, area->box);
455 return frame;
456 }
457