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