1 /* $Id$ */
2 /* Copyright (c) 2011-2015 Pierre Pronchery <khorben@defora.org> */
3 /* This file is part of DeforaOS Desktop Panel */
4 /* This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, version 3 of the License.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>. */
15 /* TODO:
16 * - update the image according to the volume in notification mode
17 * - add a pulseaudio version */
18
19
20
21 #include <sys/ioctl.h>
22 #if defined(__NetBSD__)
23 # include <sys/audioio.h>
24 #elif defined(__linux__)
25 # include <alsa/asoundlib.h>
26 #else
27 # include <sys/soundcard.h>
28 #endif
29 #include <fcntl.h>
30 #include <unistd.h>
31 #include <stdlib.h>
32 #ifdef DEBUG
33 # include <stdio.h>
34 #endif
35 #include <errno.h>
36 #include <System.h>
37 #include "Panel/applet.h"
38
39
40 /* Volume */
41 /* types */
42 typedef struct _PanelApplet
43 {
44 PanelAppletHelper * helper;
45
46 char const * device;
47 char const * control;
48 #if defined(AUDIO_MIXER_DEVINFO) /* audioio */
49 int fd;
50 int mix;
51 int outputs;
52 #elif defined(SND_LIB_MAJOR) /* Alsa */
53 snd_mixer_t * mixer;
54 snd_mixer_elem_t * mixer_elem;
55 long mixer_elem_max;
56 #else /* OSS */
57 int fd;
58 #endif
59 guint source;
60
61 /* widgets */
62 GtkWidget * widget;
63 GtkWidget * button;
64 GtkWidget * progress;
65 } Volume;
66
67
68 /* private */
69 /* prototypes */
70 static Volume * _volume_init(PanelAppletHelper * helper, GtkWidget ** widget);
71 #if GTK_CHECK_VERSION(2, 12, 0)
72 static void _volume_destroy(Volume * volume);
73 #endif
74
75 #if GTK_CHECK_VERSION(2, 12, 0)
76 static Volume * _volume_new(PanelAppletHelper * helper);
77 static void _volume_delete(Volume * volume);
78
79 /* accessors */
80 static gdouble _volume_get(Volume * volume);
81 static int _volume_set(Volume * volume, gdouble value);
82 # ifdef AUDIO_MIXER_DEVINFO
83 static int _volume_match(Volume * volume, mixer_devinfo_t * md);
84 # endif
85
86 /* callbacks */
87 static void _volume_on_value_changed(gpointer data);
88 static gboolean _volume_on_volume_timeout(gpointer data);
89 #endif
90
91
92 /* public */
93 /* variables */
94 PanelAppletDefinition applet =
95 {
96 "Volume",
97 "stock_volume",
98 NULL,
99 _volume_init,
100 #if GTK_CHECK_VERSION(2, 12, 0)
101 _volume_destroy,
102 #else
103 NULL,
104 #endif
105 NULL,
106 FALSE,
107 TRUE
108 };
109
110
111 /* functions */
112 /* volume_init */
_volume_init(PanelAppletHelper * helper,GtkWidget ** widget)113 static Volume * _volume_init(PanelAppletHelper * helper, GtkWidget ** widget)
114 {
115 #if GTK_CHECK_VERSION(2, 12, 0)
116 Volume * volume;
117 GtkIconSize iconsize;
118 GtkWidget * vbox;
119
120 if((volume = _volume_new(helper)) == NULL)
121 return NULL;
122 volume->helper = helper;
123 volume->button = NULL;
124 volume->progress = NULL;
125 iconsize = panel_window_get_icon_size(helper->window);
126 if(panel_window_get_type(helper->window)
127 == PANEL_WINDOW_TYPE_NOTIFICATION)
128 {
129 #if GTK_CHECK_VERSION(3, 0, 0)
130 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
131 #else
132 vbox = gtk_vbox_new(FALSE, 4);
133 #endif
134 volume->widget = gtk_image_new_from_icon_name(
135 "stock_volume-med", iconsize);
136 gtk_box_pack_start(GTK_BOX(vbox), volume->widget, TRUE, TRUE,
137 0);
138 volume->progress = gtk_progress_bar_new();
139 gtk_box_pack_start(GTK_BOX(vbox), volume->progress, TRUE, TRUE,
140 0);
141 volume->widget = vbox;
142 }
143 else
144 {
145 volume->button = gtk_volume_button_new();
146 /* FIXME doesn't like registered sizes */
147 g_object_set(volume->button, "size", iconsize, NULL);
148 g_signal_connect_swapped(volume->button, "value-changed",
149 G_CALLBACK(_volume_on_value_changed), volume);
150 volume->widget = volume->button;
151 }
152 _volume_on_volume_timeout(volume);
153 gtk_widget_show_all(volume->widget);
154 *widget = volume->widget;
155 return volume;
156 #else
157 return NULL;
158 #endif
159 }
160
161
162 #if GTK_CHECK_VERSION(2, 12, 0)
163 /* volume_destroy */
_volume_destroy(Volume * volume)164 static void _volume_destroy(Volume * volume)
165 {
166 gtk_widget_destroy(volume->widget);
167 _volume_delete(volume);
168 }
169
170
171 /* volume_new */
_volume_new(PanelAppletHelper * helper)172 static Volume * _volume_new(PanelAppletHelper * helper)
173 {
174 const int timeout = 500;
175 Volume * volume;
176 #if defined(AUDIO_MIXER_DEVINFO)
177 int i;
178 mixer_devinfo_t md;
179 #elif defined(SND_LIB_MAJOR)
180 int err;
181 snd_mixer_elem_t * elem = NULL;
182 long min;
183 #endif
184
185 if((volume = malloc(sizeof(*volume))) == NULL)
186 {
187 error_set("%s: %s", applet.name, strerror(errno));
188 return NULL;
189 }
190 volume->helper = helper;
191 volume->device = helper->config_get(helper->panel, "volume", "device");
192 volume->control = helper->config_get(helper->panel, "volume",
193 "control");
194 volume->source = 0;
195 #if defined(AUDIO_MIXER_DEVINFO)
196 if(volume->device == NULL)
197 volume->device = "/dev/mixer";
198 volume->mix = -1;
199 volume->outputs = -1;
200 if((volume->fd = open(volume->device, O_RDWR)) < 0)
201 {
202 error_set("%s: %s: %s", applet.name, volume->device,
203 strerror(errno));
204 helper->error(NULL, error_get(NULL), 1);
205 return volume;
206 }
207 for(i = 0; volume->outputs == -1 || volume->mix == -1; i++)
208 {
209 md.index = i;
210 if(ioctl(volume->fd, AUDIO_MIXER_DEVINFO, &md) < 0)
211 break;
212 if(md.type != AUDIO_MIXER_CLASS)
213 continue;
214 if(strcmp(md.label.name, AudioCoutputs) == 0)
215 volume->outputs = i;
216 else if(strcmp(md.label.name, "mix") == 0)
217 volume->mix = i;
218 }
219 volume->source = g_timeout_add(timeout, _volume_on_volume_timeout,
220 volume);
221 #elif defined(SND_LIB_MAJOR)
222 if(volume->device == NULL)
223 volume->device = "hw:0";
224 if(volume->control == NULL)
225 volume->control = "Master";
226 if((err = snd_mixer_open(&volume->mixer, 0)) != 0
227 || (err = snd_mixer_attach(volume->mixer,
228 volume->device)) != 0
229 || (err = snd_mixer_selem_register(volume->mixer, NULL,
230 NULL)) != 0
231 || (err = snd_mixer_load(volume->mixer)) != 0)
232 error_set("%s: %s: %s", applet.name, volume->device,
233 snd_strerror(err));
234 else
235 for(elem = snd_mixer_first_elem(volume->mixer); elem != NULL;
236 elem = snd_mixer_elem_next(elem))
237 if(strcmp(snd_mixer_selem_get_name(elem),
238 volume->control) == 0)
239 break;
240 if((volume->mixer_elem = elem) != NULL
241 && snd_mixer_selem_get_playback_volume_range(
242 volume->mixer_elem, &min,
243 &volume->mixer_elem_max) == 0)
244 volume->source = g_timeout_add(timeout,
245 _volume_on_volume_timeout, volume);
246 else
247 volume->mixer_elem = NULL;
248 #else
249 if(volume->device == NULL)
250 volume->device = "/dev/mixer";
251 if((volume->fd = open(volume->device, O_RDWR)) < 0)
252 {
253 error_set("%s: %s: %s", applet.name, volume->device,
254 strerror(errno));
255 helper->error(NULL, error_get(NULL), 1);
256 }
257 else
258 volume->source = g_timeout_add(timeout,
259 _volume_on_volume_timeout, volume);
260 #endif
261 return volume;
262 }
263
264
265 /* volume_delete */
_volume_delete(Volume * volume)266 static void _volume_delete(Volume * volume)
267 {
268 if(volume->source != 0)
269 g_source_remove(volume->source);
270 #if defined(AUDIO_MIXER_DEVINFO)
271 if(volume->fd >= 0 && close(volume->fd) != 0)
272 {
273 error_set("%s: %s: %s", applet.name, volume->device,
274 strerror(errno));
275 volume->helper->error(NULL, error_get(NULL), 1);
276 }
277 #elif defined(SND_LIB_MAJOR)
278 if(volume->mixer != NULL)
279 snd_mixer_close(volume->mixer);
280 #else /* XXX equivalent for now */
281 if(volume->fd >= 0 && close(volume->fd) != 0)
282 {
283 error_set("%s: %s: %s", applet.name, volume->device,
284 strerror(errno));
285 volume->helper->error(NULL, error_get(NULL), 1);
286 }
287 #endif
288 free(volume);
289 }
290
291
292 /* accessors */
293 /* volume_get */
_volume_get(Volume * volume)294 static gdouble _volume_get(Volume * volume)
295 {
296 PanelAppletHelper * helper = volume->helper;
297 gdouble ret = -1.0;
298 #if defined(AUDIO_MIXER_DEVINFO)
299 mixer_devinfo_t md;
300 mixer_ctrl_t mc;
301 int i;
302
303 if(volume->fd < 0)
304 return ret;
305 if(volume->outputs < 0 && volume->mix < 0)
306 return ret;
307 for(i = 0;; i++)
308 {
309 md.index = i;
310 if(ioctl(volume->fd, AUDIO_MIXER_DEVINFO, &md) < 0)
311 {
312 error_set("%s: %s: %s", applet.name,
313 "AUDIO_MIXER_DEVINFO", strerror(errno));
314 helper->error(NULL, error_get(NULL), 1);
315 close(volume->fd);
316 volume->fd = -1;
317 break;
318 }
319 if(_volume_match(volume, &md) != 1)
320 continue;
321 mc.dev = i;
322 mc.type = AUDIO_MIXER_VALUE;
323 mc.un.value.num_channels = md.un.v.num_channels;
324 if(ioctl(volume->fd, AUDIO_MIXER_READ, &mc) < 0)
325 {
326 error_set("%s: %s: %s", applet.name, "AUDIO_MIXER_READ",
327 strerror(errno));
328 helper->error(NULL, error_get(NULL), 1);
329 }
330 else
331 ret = mc.un.value.level[0] / 255.0;
332 break;
333 }
334 #elif defined(SND_LIB_MAJOR)
335 long value;
336
337 if(volume->mixer_elem == NULL)
338 return ret;
339 if(snd_mixer_selem_get_playback_volume(volume->mixer_elem,
340 SND_MIXER_SCHN_FRONT_LEFT, &value) != 0)
341 return ret;
342 ret = value;
343 ret /= volume->mixer_elem_max;
344 #else
345 int value;
346
347 if(volume->fd < 0)
348 return ret;
349 if(ioctl(volume->fd, MIXER_READ(SOUND_MIXER_VOLUME), &value) < 0)
350 {
351 error_set("%s: %s: %s", applet.name, "MIXER_READ",
352 strerror(errno));
353 helper->error(NULL, error_get(NULL), 1);
354 close(volume->fd);
355 volume->fd = -1;
356 }
357 else
358 ret = ((value & 0xff) + ((value & 0xff00) >> 8)) / 200.0;
359 #endif
360 #ifdef DEBUG
361 fprintf(stderr, "DEBUG: %s() => %f\n", __func__, ret);
362 #endif
363 return ret;
364 }
365
366
367 /* volume_set */
_volume_set(Volume * volume,gdouble value)368 int _volume_set(Volume * volume, gdouble value)
369 {
370 int ret = 0;
371 PanelAppletHelper * helper = volume->helper;
372 #if defined(AUDIO_MIXER_DEVINFO)
373 mixer_devinfo_t md;
374 mixer_ctrl_t mc;
375 int i;
376 int j;
377
378 # ifdef DEBUG
379 fprintf(stderr, "DEBUG: %s(%f)\n", __func__, value);
380 # endif
381 if(volume->fd < 0)
382 return 1;
383 if(volume->outputs < 0 && volume->mix < 0)
384 return 1;
385 for(i = 0;; i++)
386 {
387 md.index = i;
388 if(ioctl(volume->fd, AUDIO_MIXER_DEVINFO, &md) < 0)
389 break;
390 /* FIXME use volume->control */
391 if(_volume_match(volume, &md) != 1)
392 continue;
393 mc.dev = i;
394 mc.type = AUDIO_MIXER_VALUE;
395 mc.un.value.num_channels = md.un.v.num_channels;
396 mc.un.value.level[0] = value * 255;
397 for(j = 1; j < mc.un.value.num_channels; j++) /* XXX overflow */
398 mc.un.value.level[j] = mc.un.value.level[0];
399 if(ioctl(volume->fd, AUDIO_MIXER_WRITE, &mc) < 0)
400 {
401 error_set("%s: %s: %s", applet.name,
402 "AUDIO_MIXER_WRITE", strerror(errno));
403 ret |= helper->error(NULL, error_get(NULL), 1);
404 }
405 break;
406 }
407 #elif defined(SND_LIB_MAJOR)
408 long v = value * volume->mixer_elem_max;
409
410 if(volume->mixer_elem == NULL)
411 return 0;
412 snd_mixer_selem_set_playback_volume_all(volume->mixer_elem, v);
413 #else
414 int v = value * 100;
415
416 if(volume->fd < 0)
417 return 1;
418 v |= v << 8;
419 # ifdef DEBUG
420 fprintf(stderr, "DEBUG: %s(%f) 0x%04x\n", __func__, value, v);
421 # endif
422 if(ioctl(volume->fd, MIXER_WRITE(SOUND_MIXER_VOLUME), &v) < 0)
423 {
424 error_set("%s: %s: %s", applet.name, "MIXER_WRITE",
425 strerror(errno));
426 ret |= helper->error(NULL, error_get(NULL), 1);
427 }
428 #endif
429 return ret;
430 }
431
432
433 #if defined(AUDIO_MIXER_DEVINFO)
434 /* volume_match */
_volume_match(Volume * volume,mixer_devinfo_t * md)435 static int _volume_match(Volume * volume, mixer_devinfo_t * md)
436 {
437 if(md->type != AUDIO_MIXER_VALUE)
438 return 0;
439 if(md->mixer_class == volume->mix /* mix.master */
440 && strcmp(md->label.name, "master") == 0)
441 return 1;
442 if(md->mixer_class == volume->outputs /* outputs.lineout */
443 && strcmp(md->label.name, "lineout") == 0)
444 return 1;
445 if(md->mixer_class == volume->outputs /* outputs.master */
446 || strcmp(md->label.name, "master") == 0)
447 return 1;
448 return 0;
449 }
450 #endif
451
452
453 /* callbacks */
454 /* volume_on_value_changed */
_volume_on_value_changed(gpointer data)455 static void _volume_on_value_changed(gpointer data)
456 {
457 Volume * volume = data;
458 gdouble value;
459
460 value = gtk_scale_button_get_value(GTK_SCALE_BUTTON(volume->button));
461 _volume_set(volume, value);
462 }
463
464
465 /* volume_on_volume_timeout */
_volume_on_volume_timeout(gpointer data)466 static gboolean _volume_on_volume_timeout(gpointer data)
467 {
468 Volume * volume = data;
469 gdouble value;
470
471 if((value = _volume_get(volume)) < 0.0)
472 {
473 volume->source = 0;
474 return FALSE;
475 }
476 if(volume->button != NULL)
477 gtk_scale_button_set_value(GTK_SCALE_BUTTON(volume->button),
478 value);
479 if(volume->progress != NULL)
480 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(
481 volume->progress), value);
482 return TRUE;
483 }
484 #endif
485