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