chiark / gitweb /
linux build fixes
[disorder] / disobedience / control.c
1 /*
2  * This file is part of DisOrder.
3  * Copyright (C) 2006, 2007 Richard Kettlewell
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18  * USA
19  */
20 /** @file disobedience/control.c
21  * @brief Volume control and buttons
22  */
23
24 #include "disobedience.h"
25
26 /* Forward declarations ---------------------------------------------------- */
27
28 WT(adjustment);
29 WT(hscale);
30 WT(hbox);
31 WT(button);
32 WT(image);
33 WT(label);
34 WT(vbox);
35
36 struct icon;
37
38 static void update_pause(const struct icon *);
39 static void update_play(const struct icon *);
40 static void update_scratch(const struct icon *);
41 static void update_random_enable(const struct icon *);
42 static void update_random_disable(const struct icon *);
43 static void update_enable(const struct icon *);
44 static void update_disable(const struct icon *);
45 static void update_rtp(const struct icon *);
46 static void update_nortp(const struct icon *);
47 static void clicked_icon(GtkButton *, gpointer);
48
49 static int enable_rtp(disorder_eclient *c,
50                       disorder_eclient_no_response *completed,
51                       void *v);
52 static int disable_rtp(disorder_eclient *c,
53                        disorder_eclient_no_response *completed,
54                        void *v);
55
56 static double left(double v, double b);
57 static double right(double v, double b);
58 static double volume(double l, double r);
59 static double balance(double l, double r);
60
61 static void volume_adjusted(GtkAdjustment *a, gpointer user_data);
62 static gchar *format_volume(GtkScale *scale, gdouble value);
63 static gchar *format_balance(GtkScale *scale, gdouble value);
64
65 /* Control bar ------------------------------------------------------------- */
66
67 /** @brief Guard against feedback loop in volume control */
68 static int suppress_set_volume;
69
70 /** @brief Definition of an icon
71  *
72  * The design here is rather mad: rather than changing the image displayed by
73  * icons according to their state, we flip the visibility of pairs of icons.
74  */
75 struct icon {
76   /** @brief Filename for image */
77   const char *icon;
78
79   /** @brief Text for tooltip */
80   const char *tip;
81
82   /** @brief Called when button is clicked (activated) */
83   void (*clicked)(GtkButton *button, gpointer userdata);
84
85   /** @brief Called to update button when state may have changed */
86   void (*update)(const struct icon *i);
87
88   /** @brief @ref eclient.h function to call */
89   int (*action)(disorder_eclient *c,
90                 disorder_eclient_no_response *completed,
91                 void *v);
92
93   /** @brief Pointer to button */
94   GtkWidget *button;
95 };
96
97 /** @brief Table of all icons */
98 static struct icon icons[] = {
99   { "pause.png", "Pause playing track", clicked_icon, update_pause,
100     disorder_eclient_pause, 0 },
101   { "play.png", "Resume playing track", clicked_icon, update_play,
102     disorder_eclient_resume, 0 },
103   { "cross.png", "Cancel playing track", clicked_icon, update_scratch,
104     disorder_eclient_scratch_playing, 0 },
105   { "random.png", "Enable random play", clicked_icon, update_random_enable,
106     disorder_eclient_random_enable, 0 },
107   { "randomcross.png", "Disable random play", clicked_icon, update_random_disable,
108     disorder_eclient_random_disable, 0 },
109   { "notes.png", "Enable play", clicked_icon, update_enable,
110     disorder_eclient_enable, 0 },
111   { "notescross.png", "Disable play", clicked_icon, update_disable,
112     disorder_eclient_disable, 0 },
113   { "speaker.png", "Play network stream", clicked_icon, update_rtp,
114     enable_rtp, 0 },
115   { "speakercross.png", "Stop playing network stream", clicked_icon, update_nortp,
116     disable_rtp, 0 },
117 };
118
119 /** @brief Count of icons */
120 #define NICONS (int)(sizeof icons / sizeof *icons)
121
122 static GtkAdjustment *volume_adj;
123 static GtkAdjustment *balance_adj;
124
125 /** @brief Called whenever last_state changes in any way */
126 void control_monitor(void attribute((unused)) *u) {
127   int n;
128
129   D(("control_monitor"));
130   for(n = 0; n < NICONS; ++n)
131     icons[n].update(&icons[n]);
132 }
133
134 /** @brief Create the control bar */
135 GtkWidget *control_widget(void) {
136   GtkWidget *hbox = gtk_hbox_new(FALSE, 1), *vbox;
137   GtkWidget *content;
138   GdkPixbuf *pb;
139   GtkWidget *v, *b;
140   int n;
141
142   NW(hbox);
143   D(("control_widget"));
144   for(n = 0; n < NICONS; ++n) {
145     NW(button);
146     icons[n].button = gtk_button_new();
147     if((pb = find_image(icons[n].icon))) {
148       NW(image);
149       content = gtk_image_new_from_pixbuf(pb);
150     } else {
151       NW(label);
152       content = gtk_label_new(icons[n].icon);
153     }
154     gtk_container_add(GTK_CONTAINER(icons[n].button), content);
155     gtk_tooltips_set_tip(tips, icons[n].button, icons[n].tip, "");
156     g_signal_connect(G_OBJECT(icons[n].button), "clicked",
157                      G_CALLBACK(icons[n].clicked), &icons[n]);
158     /* pop the icon in a vbox so it doesn't get vertically stretch if there are
159      * taller things in the control bar */
160     NW(vbox);
161     vbox = gtk_vbox_new(FALSE, 0);
162     gtk_box_pack_start(GTK_BOX(vbox), icons[n].button, TRUE, FALSE, 0);
163     gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE, FALSE, 0);
164   }
165   /* create the adjustments for the volume control */
166   NW(adjustment);
167   volume_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0, 0, goesupto,
168                                                  goesupto / 20, goesupto / 20,
169                                                  0));
170   NW(adjustment);
171   balance_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0, -1, 1,
172                                                   0.2, 0.2, 0));
173   /* the volume control */
174   NW(hscale);
175   v = gtk_hscale_new(volume_adj);
176   NW(hscale);
177   b = gtk_hscale_new(balance_adj);
178   gtk_scale_set_digits(GTK_SCALE(v), 10);
179   gtk_scale_set_digits(GTK_SCALE(b), 10);
180   gtk_widget_set_size_request(v, 192, -1);
181   gtk_widget_set_size_request(b, 192, -1);
182   gtk_tooltips_set_tip(tips, v, "Volume", "");
183   gtk_tooltips_set_tip(tips, b, "Balance", "");
184   gtk_box_pack_start(GTK_BOX(hbox), v, FALSE, TRUE, 0);
185   gtk_box_pack_start(GTK_BOX(hbox), b, FALSE, TRUE, 0);
186   /* space updates rather than hammering the server */
187   gtk_range_set_update_policy(GTK_RANGE(v), GTK_UPDATE_DELAYED);
188   gtk_range_set_update_policy(GTK_RANGE(b), GTK_UPDATE_DELAYED);
189   /* notice when the adjustments are changed */
190   g_signal_connect(G_OBJECT(volume_adj), "value-changed",
191                    G_CALLBACK(volume_adjusted), 0);
192   g_signal_connect(G_OBJECT(balance_adj), "value-changed",
193                    G_CALLBACK(volume_adjusted), 0);
194   /* format the volume/balance values ourselves */
195   g_signal_connect(G_OBJECT(v), "format-value",
196                    G_CALLBACK(format_volume), 0);
197   g_signal_connect(G_OBJECT(b), "format-value",
198                    G_CALLBACK(format_balance), 0);
199   register_monitor(control_monitor, 0, -1UL);
200   return hbox;
201 }
202
203 /** @brief Update the volume control when it changes */
204 void volume_update(void) {
205   double l, r;
206
207   D(("volume_update"));
208   l = volume_l / 100.0;
209   r = volume_r / 100.0;
210   ++suppress_set_volume;
211   gtk_adjustment_set_value(volume_adj, volume(l, r) * goesupto);
212   gtk_adjustment_set_value(balance_adj, balance(l, r));
213   --suppress_set_volume;
214 }
215
216 /** @brief Update the state of one of the control icons
217  * @param icon Target icon
218  * @param visible True if this version of the button should be visible
219  * @param usable True if the button is currently usable
220  *
221  * Several of the icons, rather bizarrely, come in pairs: for instance exactly
222  * one of the play and pause buttons is supposed to be visible at any given
223  * moment.
224  *
225  * @p usable need not take into account server availability, that is done
226  * automatically.
227  */
228 static void update_icon(const struct icon *icon,
229                         int visible, int usable) {
230   /* If the connection is down nothing is ever usable */
231   if(!(last_state & DISORDER_CONNECTED))
232     usable = 0;
233   (visible ? gtk_widget_show : gtk_widget_hide)(icon->button);
234   /* Only both updating usability if the button is visible */
235   if(visible)
236     gtk_widget_set_sensitive(icon->button, usable);
237 }
238
239 static void update_pause(const struct icon *icon) {
240   const int visible = !(last_state & DISORDER_TRACK_PAUSED);
241   const int usable = !!(last_state & DISORDER_PLAYING); /* TODO: might be a lie */
242   update_icon(icon, visible, usable);
243 }
244
245 static void update_play(const struct icon *icon) {
246   const int visible = !!(last_state & DISORDER_TRACK_PAUSED);
247   const int usable = !!(last_state & DISORDER_PLAYING);
248   update_icon(icon, visible, usable);
249 }
250
251 static void update_scratch(const struct icon *icon) {
252   const int visible = 1;
253   const int usable = !!(last_state & DISORDER_PLAYING);
254   update_icon(icon, visible, usable);
255 }
256
257 static void update_random_enable(const struct icon *icon) {
258   const int visible = !(last_state & DISORDER_RANDOM_ENABLED);
259   const int usable = 1;
260   update_icon(icon, visible, usable);
261 }
262
263 static void update_random_disable(const struct icon *icon) {
264   const int visible = !!(last_state & DISORDER_RANDOM_ENABLED);
265   const int usable = 1;
266   update_icon(icon, visible, usable);
267 }
268
269 static void update_enable(const struct icon *icon) {
270   const int visible = !(last_state & DISORDER_PLAYING_ENABLED);
271   const int usable = 1;
272   update_icon(icon, visible, usable);
273 }
274
275 static void update_disable(const struct icon *icon) {
276   const int visible = !!(last_state & DISORDER_PLAYING_ENABLED);
277   const int usable = 1;
278   update_icon(icon, visible, usable);
279 }
280
281 static void update_rtp(const struct icon *icon) {
282   const int visible = !rtp_is_running;
283   const int usable = rtp_supported;
284   update_icon(icon, visible, usable);
285 }
286
287 static void update_nortp(const struct icon *icon) {
288   const int visible = rtp_is_running;
289   const int usable = rtp_supported;
290   update_icon(icon, visible, usable);
291 }
292
293 static void clicked_icon(GtkButton attribute((unused)) *button,
294                          gpointer userdata) {
295   const struct icon *icon = userdata;
296
297   icon->action(client, 0, 0);
298 }
299
300 /** @brief Called when the volume has been adjusted */
301 static void volume_adjusted(GtkAdjustment attribute((unused)) *a,
302                             gpointer attribute((unused)) user_data) {
303   double v = gtk_adjustment_get_value(volume_adj) / goesupto;
304   double b = gtk_adjustment_get_value(balance_adj);
305
306   if(suppress_set_volume)
307     /* This is the result of an update from the server, not a change from the
308      * user.  Don't feedback! */
309     return;
310   D(("volume_adjusted"));
311   /* force to 'stereotypical' values */
312   v = nearbyint(100 * v) / 100;
313   b = nearbyint(5 * b) / 5;
314   /* Set the volume.  We don't want a reply, we'll get the actual new volume
315    * from the log. */
316   disorder_eclient_volume(client, 0,
317                           nearbyint(left(v, b) * 100),
318                           nearbyint(right(v, b) * 100),
319                           0);
320 }
321
322 /** @brief Formats the volume value */
323 static gchar *format_volume(GtkScale attribute((unused)) *scale,
324                             gdouble value) {
325   char s[32];
326
327   snprintf(s, sizeof s, "%.1f", (double)value);
328   return g_strdup(s);
329 }
330
331 /** @brief Formats the balance value */
332 static gchar *format_balance(GtkScale attribute((unused)) *scale,
333                              gdouble value) {
334   char s[32];
335
336   if(fabs(value) < 0.1)
337     return g_strdup("0");
338   snprintf(s, sizeof s, "%+.1f", (double)value);
339   return g_strdup(s);
340 }
341
342 /* Volume mapping.  We consider left, right, volume to be in [0,1]
343  * and balance to be in [-1,1].
344  * 
345  * First, we just have volume = max(left, right).
346  *
347  * Balance we consider to linearly represent the amount by which the quieter
348  * channel differs from the louder.  In detail:
349  *
350  *  if right > left then balance > 0:
351  *   balance = 0 => left = right  (as an endpoint, not an instance)
352  *   balance = 1 => left = 0
353  *   fitting to linear, left = right * (1 - balance)
354  *                so balance = 1 - left / right
355  *   (right > left => right > 0 so no division by 0.)
356  * 
357  *  if left > right then balance < 0:
358  *   balance = 0 => right = left  (same caveat as above)
359  *   balance = -1 => right = 0
360  *   again fitting to linear, right = left * (1 + balance)
361  *                       so balance = right / left - 1
362  *   (left > right => left > 0 so no division by 0.)
363  *
364  *  if left = right then we just have balance = 0.
365  *
366  * Thanks to Clive and Andrew.
367  */
368
369 /** @brief Return the greater of @p x and @p y */
370 static double max(double x, double y) {
371   return x > y ? x : y;
372 }
373
374 /** @brief Compute the left channel volume */
375 static double left(double v, double b) {
376   if(b > 0)                             /* volume = right */
377     return v * (1 - b);
378   else                                  /* volume = left */
379     return v;
380 }
381
382 /** @brief Compute the right channel volume */
383 static double right(double v, double b) {
384   if(b > 0)                             /* volume = right */
385     return v;
386   else                                  /* volume = left */
387     return v * (1 + b);
388 }
389
390 /** @brief Compute the overall volume */
391 static double volume(double l, double r) {
392   return max(l, r);
393 }
394
395 /** @brief Compute the balance */
396 static double balance(double l, double r) {
397   if(l > r)
398     return r / l - 1;
399   else if(r > l)
400     return 1 - l / r;
401   else                                  /* left = right */
402     return 0;
403 }
404
405 /** @brief Called to enable RTP play
406  *
407  * Rather odd signature is to fit in with the other icons which all call @ref
408  * lib/eclient.h functions.
409  */
410 static int enable_rtp(disorder_eclient attribute((unused)) *c,
411                       disorder_eclient_no_response attribute((unused)) *completed,
412                       void attribute((unused)) *v) {
413   start_rtp();
414   return 0;
415 }
416
417 /** @brief Called to disable RTP play
418  *
419  * Rather odd signature is to fit in with the other icons which all call @ref
420  * lib/eclient.h functions.
421  */
422 static int disable_rtp(disorder_eclient attribute((unused)) *c,
423                        disorder_eclient_no_response attribute((unused)) *completed,
424                        void attribute((unused)) *v) {
425   stop_rtp();
426   return 0;
427 }
428
429 /*
430 Local Variables:
431 c-basic-offset:2
432 comment-column:40
433 fill-column:79
434 indent-tabs-mode:nil
435 End:
436 */