1/*
2 * mixer_widget.c - mixer widget and keys handling
3 * Copyright (c) 1998,1999 Tim Janik
4 *                         Jaroslav Kysela <perex@perex.cz>
5 * Copyright (c) 2009      Clemens Ladisch <clemens@ladisch.de>
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 */
20
21#include "aconfig.h"
22#include <stdlib.h>
23#include <string.h>
24#include <errno.h>
25#include <alsa/asoundlib.h>
26#include "gettext_curses.h"
27#include "version.h"
28#include "utils.h"
29#include "die.h"
30#include "mem.h"
31#include "colors.h"
32#include "widget.h"
33#include "textbox.h"
34#include "proc_files.h"
35#include "card_select.h"
36#include "volume_mapping.h"
37#include "mixer_clickable.h"
38#include "mixer_controls.h"
39#include "mixer_display.h"
40#include "mixer_widget.h"
41#include "bindings.h"
42
43snd_mixer_t *mixer;
44char *mixer_device_name;
45bool unplugged;
46
47struct widget mixer_widget;
48
49enum view_mode view_mode;
50
51int focus_control_index;
52snd_mixer_selem_id_t *current_selem_id;
53unsigned int current_control_flags;
54
55bool control_values_changed;
56bool controls_changed;
57
58unsigned int mouse_wheel_step = 1;
59bool mouse_wheel_focuses_control = 1;
60
61static int elem_callback(snd_mixer_elem_t *elem ATTRIBUTE_UNUSED, unsigned int mask)
62{
63	if (mask == SND_CTL_EVENT_MASK_REMOVE) {
64		controls_changed = TRUE;
65	} else {
66		if (mask & SND_CTL_EVENT_MASK_VALUE)
67			control_values_changed = TRUE;
68
69		if (mask & SND_CTL_EVENT_MASK_INFO)
70			controls_changed = TRUE;
71	}
72
73	return 0;
74}
75
76static int mixer_callback(snd_mixer_t *mixer ATTRIBUTE_UNUSED, unsigned int mask, snd_mixer_elem_t *elem)
77{
78	if (mask & SND_CTL_EVENT_MASK_ADD) {
79		snd_mixer_elem_set_callback(elem, elem_callback);
80		controls_changed = TRUE;
81	}
82	return 0;
83}
84
85void create_mixer_object(struct snd_mixer_selem_regopt *selem_regopt)
86{
87	int err;
88
89	err = snd_mixer_open(&mixer, 0);
90	if (err < 0)
91		fatal_alsa_error(_("cannot open mixer"), err);
92
93	mixer_device_name = cstrdup(selem_regopt->device);
94	err = snd_mixer_selem_register(mixer, selem_regopt, NULL);
95	if (err < 0)
96		fatal_alsa_error(_("cannot open mixer"), err);
97
98	snd_mixer_set_callback(mixer, mixer_callback);
99
100	err = snd_mixer_load(mixer);
101	if (err < 0)
102		fatal_alsa_error(_("cannot load mixer controls"), err);
103
104	err = snd_mixer_selem_id_malloc(&current_selem_id);
105	if (err < 0)
106		fatal_error("out of memory");
107}
108
109static void set_view_mode(enum view_mode m)
110{
111	view_mode = m;
112	create_controls();
113}
114
115static void close_hctl(void)
116{
117	free_controls();
118	if (mixer_device_name) {
119		snd_mixer_detach(mixer, mixer_device_name);
120		free(mixer_device_name);
121		mixer_device_name = NULL;
122	}
123}
124
125static void check_unplugged(void)
126{
127	snd_hctl_t *hctl;
128	snd_ctl_t *ctl;
129	unsigned int state;
130	int err;
131
132	unplugged = FALSE;
133	if (mixer_device_name) {
134		err = snd_mixer_get_hctl(mixer, mixer_device_name, &hctl);
135		if (err >= 0) {
136			ctl = snd_hctl_ctl(hctl);
137			/* just any random function that does an ioctl() */
138			err = snd_ctl_get_power_state(ctl, &state);
139			if (err == -ENODEV)
140				unplugged = TRUE;
141		}
142	}
143}
144
145void close_mixer_device(void)
146{
147	check_unplugged();
148	close_hctl();
149
150	display_card_info();
151	set_view_mode(view_mode);
152}
153
154bool select_card_by_name(const char *device_name)
155{
156	int err;
157	bool opened;
158	char *msg;
159
160	close_hctl();
161	unplugged = FALSE;
162
163	opened = FALSE;
164	if (device_name) {
165		err = snd_mixer_attach(mixer, device_name);
166		if (err >= 0)
167			opened = TRUE;
168		else {
169			msg = casprintf(_("Cannot open mixer device '%s'."), device_name);
170			show_alsa_error(msg, err);
171			free(msg);
172		}
173	}
174	if (opened) {
175		mixer_device_name = cstrdup(device_name);
176
177		err = snd_mixer_load(mixer);
178		if (err < 0)
179			fatal_alsa_error(_("cannot load mixer controls"), err);
180	}
181
182	display_card_info();
183	set_view_mode(view_mode);
184	return opened;
185}
186
187static void show_help(void)
188{
189	const char *help[] = {
190		_("Esc     Exit"),
191		_("F1 ? H  Help"),
192		_("F2 /    System information"),
193		_("F3      Show playback controls"),
194		_("F4      Show capture controls"),
195		_("F5      Show all controls"),
196		_("Tab     Toggle view mode (F3/F4/F5)"),
197		_("F6 S    Select sound card"),
198		_("L       Redraw screen"),
199		"",
200		_("Left    Move to the previous control"),
201		_("Right   Move to the next control"),
202		"",
203		_("Up/Down    Change volume"),
204		_("+ -        Change volume"),
205		_("Page Up/Dn Change volume in big steps"),
206		_("End        Set volume to 0%"),
207		_("0-9        Set volume to 0%-90%"),
208		_("Q W E      Increase left/both/right volumes"),
209		/* TRANSLATORS: or Y instead of Z */
210		_("Z X C      Decrease left/both/right volumes"),
211		_("B          Balance left and right volumes"),
212		"",
213		_("M          Toggle mute"),
214		/* TRANSLATORS: or , . */
215		_("< >        Toggle left/right mute"),
216		"",
217		_("Space      Toggle capture"),
218		/* TRANSLATORS: or Insert Delete */
219		_("; '        Toggle left/right capture"),
220		"",
221		_("Authors:"),
222		_("  Tim Janik"),
223		_("  Jaroslav Kysela <perex@perex.cz>"),
224		_("  Clemens Ladisch <clemens@ladisch.de>"),
225	};
226	show_text(help, ARRAY_SIZE(help), _("Help"));
227}
228
229void refocus_control(void)
230{
231	if (focus_control_index >= 0 &&
232	    focus_control_index < (int)controls_count) {
233		snd_mixer_selem_get_id(controls[focus_control_index].elem, current_selem_id);
234		current_control_flags = controls[focus_control_index].flags;
235	}
236
237	display_controls();
238}
239
240static struct control *get_focus_control(unsigned int type)
241{
242	if (focus_control_index >= 0 &&
243	    focus_control_index < (int)controls_count &&
244	    (controls[focus_control_index].flags & IS_ACTIVE) &&
245	    (controls[focus_control_index].flags & type))
246		return &controls[focus_control_index];
247	else
248		return NULL;
249}
250
251static void change_enum_to_percent(struct control *control, int value)
252{
253	unsigned int i;
254	unsigned int index;
255	unsigned int new_index;
256	int items;
257	int err;
258
259	i = ffs(control->enum_channel_bits) - 1;
260	err = snd_mixer_selem_get_enum_item(control->elem, i, &index);
261	if (err < 0)
262		return;
263	new_index = index;
264	if (value == 0) {
265		new_index = 0;
266	} else if (value == 100) {
267		items = snd_mixer_selem_get_enum_items(control->elem);
268		if (items < 1)
269			return;
270		new_index = items - 1;
271	}
272	if (new_index == index)
273		return;
274	for (i = 0; i <= SND_MIXER_SCHN_LAST; ++i)
275		if (control->enum_channel_bits & (1 << i))
276			snd_mixer_selem_set_enum_item(control->elem, i, new_index);
277}
278
279static void change_enum_relative(struct control *control, int delta)
280{
281	int items;
282	unsigned int i;
283	unsigned int index;
284	int new_index;
285	int err;
286
287	items = snd_mixer_selem_get_enum_items(control->elem);
288	if (items < 1)
289		return;
290	err = snd_mixer_selem_get_enum_item(control->elem, 0, &index);
291	if (err < 0)
292		return;
293	new_index = (int)index + delta;
294	if (new_index < 0)
295		new_index = 0;
296	else if (new_index >= items)
297		new_index = items - 1;
298	if (new_index == (int)index)
299		return;
300	for (i = 0; i <= SND_MIXER_SCHN_LAST; ++i)
301		if (control->enum_channel_bits & (1 << i))
302			snd_mixer_selem_set_enum_item(control->elem, i, new_index);
303}
304
305static void change_volume_to_percent(struct control *control, int value, unsigned int channels)
306{
307	int (*set_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, double, int);
308
309	if (!(control->flags & HAS_VOLUME_1))
310		channels = LEFT;
311	if (control->flags & TYPE_PVOLUME)
312		set_func = set_normalized_playback_volume;
313	else
314		set_func = set_normalized_capture_volume;
315	if (channels & LEFT)
316		set_func(control->elem, control->volume_channels[0], value / 100.0, 0);
317	if (channels & RIGHT)
318		set_func(control->elem, control->volume_channels[1], value / 100.0, 0);
319}
320
321static double clamp_volume(double v)
322{
323	if (v < 0)
324		return 0;
325	if (v > 1)
326		return 1;
327	return v;
328}
329
330static void change_volume_relative(struct control *control, int delta, unsigned int channels)
331{
332	double (*get_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t);
333	int (*set_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, double, int);
334	double left = 0, right = 0;
335	int dir;
336
337	if (!(control->flags & HAS_VOLUME_1))
338		channels = LEFT;
339	if (control->flags & TYPE_PVOLUME) {
340		get_func = get_normalized_playback_volume;
341		set_func = set_normalized_playback_volume;
342	} else {
343		get_func = get_normalized_capture_volume;
344		set_func = set_normalized_capture_volume;
345	}
346	if (channels & LEFT)
347		left = get_func(control->elem, control->volume_channels[0]);
348	if (channels & RIGHT)
349		right = get_func(control->elem, control->volume_channels[1]);
350	dir = delta > 0 ? 1 : -1;
351	if (channels & LEFT) {
352		left = clamp_volume(left + delta / 100.0);
353		set_func(control->elem, control->volume_channels[0], left, dir);
354	}
355	if (channels & RIGHT) {
356		right = clamp_volume(right + delta / 100.0);
357		set_func(control->elem, control->volume_channels[1], right, dir);
358	}
359}
360
361static void change_control_to_percent(int value, unsigned int channels)
362{
363	struct control *control;
364
365	control = get_focus_control(TYPE_PVOLUME | TYPE_CVOLUME | TYPE_ENUM);
366	if (!control)
367		return;
368	if (control->flags & TYPE_ENUM)
369		change_enum_to_percent(control, value);
370	else
371		change_volume_to_percent(control, value, channels);
372	display_controls();
373}
374
375static void change_control_relative(int delta, unsigned int channels)
376{
377	struct control *control;
378
379	control = get_focus_control(TYPE_PVOLUME | TYPE_CVOLUME | TYPE_ENUM);
380	if (!control)
381		return;
382	if (control->flags & TYPE_ENUM)
383		change_enum_relative(control, delta);
384	else
385		change_volume_relative(control, delta, channels);
386	display_controls();
387}
388
389static void toggle_switches(unsigned int type, unsigned int channels)
390{
391	struct control *control;
392	unsigned int switch_1_mask;
393	int (*get_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, int *);
394	int (*set_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, int);
395	snd_mixer_selem_channel_id_t channel_ids[2];
396	int left, right;
397	int err;
398
399	control = get_focus_control(type);
400	if (!control)
401		return;
402	if (type == TYPE_PSWITCH) {
403		switch_1_mask = HAS_PSWITCH_1;
404		get_func = snd_mixer_selem_get_playback_switch;
405		set_func = snd_mixer_selem_set_playback_switch;
406		channel_ids[0] = control->pswitch_channels[0];
407		channel_ids[1] = control->pswitch_channels[1];
408	} else {
409		switch_1_mask = HAS_CSWITCH_1;
410		get_func = snd_mixer_selem_get_capture_switch;
411		set_func = snd_mixer_selem_set_capture_switch;
412		channel_ids[0] = control->cswitch_channels[0];
413		channel_ids[1] = control->cswitch_channels[1];
414	}
415	if (!(control->flags & switch_1_mask))
416		channels = LEFT;
417	if (channels & LEFT) {
418		err = get_func(control->elem, channel_ids[0], &left);
419		if (err < 0)
420			return;
421	}
422	if (channels & RIGHT) {
423		err = get_func(control->elem, channel_ids[1], &right);
424		if (err < 0)
425			return;
426	}
427	if (channels & LEFT)
428		set_func(control->elem, channel_ids[0], !left);
429	if (channels & RIGHT)
430		set_func(control->elem, channel_ids[1], !right);
431	display_controls();
432}
433
434static void toggle_mute(unsigned int channels)
435{
436	toggle_switches(TYPE_PSWITCH, channels);
437}
438
439static void toggle_capture(unsigned int channels)
440{
441	toggle_switches(TYPE_CSWITCH, channels);
442}
443
444static void balance_volumes(void)
445{
446	struct control *control;
447	double left, right;
448
449	control = get_focus_control(TYPE_PVOLUME | TYPE_CVOLUME);
450	if (!control || !(control->flags & HAS_VOLUME_1))
451		return;
452	if (control->flags & TYPE_PVOLUME) {
453		left = get_normalized_playback_volume(control->elem, control->volume_channels[0]);
454		right = get_normalized_playback_volume(control->elem, control->volume_channels[1]);
455	} else {
456		left = get_normalized_capture_volume(control->elem, control->volume_channels[0]);
457		right = get_normalized_capture_volume(control->elem, control->volume_channels[1]);
458	}
459	left = (left + right) / 2;
460	if (control->flags & TYPE_PVOLUME) {
461		set_normalized_playback_volume(control->elem, control->volume_channels[0], left, 0);
462		set_normalized_playback_volume(control->elem, control->volume_channels[1], left, 0);
463	} else {
464		set_normalized_capture_volume(control->elem, control->volume_channels[0], left, 0);
465		set_normalized_capture_volume(control->elem, control->volume_channels[1], left, 0);
466	}
467	display_controls();
468}
469
470static int on_mouse_key() {
471	MEVENT m;
472	command_enum cmd = 0;
473	unsigned int channels = LEFT | RIGHT;
474	unsigned int index;
475	struct control *control;
476	struct clickable_rect *rect;
477
478	if (getmouse(&m) == ERR)
479		return 0;
480
481	if (m.bstate & (
482				BUTTON1_PRESSED|BUTTON1_RELEASED|
483				BUTTON2_PRESSED|BUTTON2_RELEASED|
484				BUTTON3_PRESSED|BUTTON3_RELEASED))
485		return 0;
486
487	rect = clickable_find(m.y, m.x);
488	if (rect)
489		cmd = rect->command;
490
491#if NCURSES_MOUSE_VERSION > 1
492	if (m.bstate & (BUTTON4_CLICKED|BUTTON4_PRESSED|BUTTON5_CLICKED|BUTTON5_PRESSED)) {
493		switch (cmd) {
494			case CMD_MIXER_MOUSE_CLICK_CONTROL_ENUM:
495				focus_control_index = rect->arg1;
496				return CMD_WITH_ARG((
497							m.bstate & (BUTTON4_CLICKED|BUTTON4_PRESSED)
498							? CMD_MIXER_CONTROL_UP
499							: CMD_MIXER_CONTROL_DOWN
500						), 1);
501
502			case CMD_MIXER_MOUSE_CLICK_VOLUME_BAR:
503				if (mouse_wheel_focuses_control)
504					focus_control_index = rect->arg1;
505				/* fall through */
506
507			default:
508				return CMD_WITH_ARG((
509							m.bstate & (BUTTON4_CLICKED|BUTTON4_PRESSED)
510							? CMD_MIXER_CONTROL_UP
511							: CMD_MIXER_CONTROL_DOWN
512						), mouse_wheel_step);
513		}
514	}
515#endif
516
517	/* If the rectangle has got an argument (value != -1) it is used for
518	 * setting `focus_control_index` */
519	if (rect && rect->arg1 >= 0)
520		focus_control_index = rect->arg1;
521
522	switch (cmd) {
523	case CMD_MIXER_MOUSE_CLICK_VOLUME_BAR:
524		if (m.bstate & (BUTTON3_CLICKED|BUTTON3_DOUBLE_CLICKED|BUTTON3_TRIPLE_CLICKED))
525			channels = m.x - rect->x1 + 1;
526		return CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT_LEFT + channels - 1,
527			(100 * (rect->y2 - m.y) / (rect->y2 - rect->y1)) // volume
528		);
529
530	case CMD_MIXER_MOUSE_CLICK_MUTE:
531		if (m.bstate & (BUTTON3_CLICKED|BUTTON3_DOUBLE_CLICKED|BUTTON3_TRIPLE_CLICKED))
532			channels = m.x - rect->x1 + 1;
533		return CMD_WITH_ARG(CMD_MIXER_TOGGLE_MUTE, channels);
534
535	case CMD_MIXER_MOUSE_CLICK_CONTROL_ENUM:
536		control = get_focus_control(TYPE_ENUM);
537		if (control &&
538			(snd_mixer_selem_get_enum_item(control->elem, 0, &index) >= 0)) {
539				return (index == 0
540					? CMD_WITH_ARG(CMD_MIXER_CONTROL_UP, 100)
541					: CMD_WITH_ARG(CMD_MIXER_CONTROL_DOWN, 1));
542		}
543		break;
544
545	default:
546		return cmd; // non-mouse command
547	}
548
549	return 0; // failed mouse command
550}
551
552static void on_handle_key(int key)
553{
554	int arg;
555	command_enum cmd;
556
557	if (key == KEY_MOUSE)
558		cmd = on_mouse_key();
559	else if (key < (int)ARRAY_SIZE(mixer_bindings))
560		cmd = mixer_bindings[key];
561	else
562		return;
563
564	arg = CMD_GET_ARGUMENT(cmd);
565	cmd = CMD_GET_COMMAND(cmd);
566
567	switch (cmd) {
568	case CMD_MIXER_CONTROL_DOWN_LEFT:
569	case CMD_MIXER_CONTROL_DOWN_RIGHT:
570	case CMD_MIXER_CONTROL_DOWN:
571		arg = (-arg);
572		/* fall through */
573	case CMD_MIXER_CONTROL_UP_LEFT:
574	case CMD_MIXER_CONTROL_UP_RIGHT:
575	case CMD_MIXER_CONTROL_UP:
576		change_control_relative(arg, cmd % 4);
577		break;
578	case CMD_MIXER_CONTROL_SET_PERCENT_LEFT:
579	case CMD_MIXER_CONTROL_SET_PERCENT_RIGHT:
580	case CMD_MIXER_CONTROL_SET_PERCENT:
581		change_control_to_percent(arg, cmd % 4);
582		break;
583	case CMD_MIXER_CLOSE:
584		mixer_widget.close();
585		break;
586	case CMD_MIXER_HELP:
587		show_help();
588		break;
589	case CMD_MIXER_SYSTEM_INFORMATION:
590		create_proc_files_list();
591		break;
592	case CMD_MIXER_TOGGLE_VIEW_MODE:
593		arg = (view_mode + 1) % VIEW_MODE_COUNT;
594		/* fall through */
595	case CMD_MIXER_SET_VIEW_MODE:
596		set_view_mode((enum view_mode)(arg));
597		break;
598	case CMD_MIXER_SELECT_CARD:
599		create_card_select_list();
600		break;
601	case CMD_MIXER_REFRESH:
602		clearok(mixer_widget.window, TRUE);
603		display_controls();
604		break;
605	case CMD_MIXER_PREVIOUS:
606		arg = (-arg);
607		/* fall through */
608	case CMD_MIXER_NEXT:
609		arg = focus_control_index + arg;
610		/* fall through */
611	case CMD_MIXER_FOCUS_CONTROL:
612		focus_control_index = arg;
613		if (focus_control_index < 0)
614			focus_control_index = 0;
615		else if (focus_control_index >= (int)controls_count)
616			focus_control_index = controls_count - 1;
617		refocus_control();
618		break;
619	case CMD_MIXER_TOGGLE_MUTE:
620		toggle_mute(arg);
621		break;
622	case CMD_MIXER_TOGGLE_CAPTURE:
623		toggle_capture(arg);
624		break;
625	case CMD_MIXER_BALANCE_CONTROL:
626		balance_volumes();
627		break;
628	}
629}
630
631static void create(void)
632{
633	static const char title[] = " AlsaMixer v" SND_UTIL_VERSION_STR " ";
634
635	widget_init(&mixer_widget, screen_lines, screen_cols, 0, 0,
636		    attrs.mixer_frame, WIDGET_BORDER);
637	if (screen_cols >= (int)(sizeof(title) - 1) + 2) {
638		wattrset(mixer_widget.window, attrs.mixer_active);
639		mvwaddstr(mixer_widget.window, 0, (screen_cols - (sizeof(title) - 1)) / 2, title);
640	}
641	init_mixer_layout();
642	display_card_info();
643	set_view_mode(view_mode);
644}
645
646static void on_window_size_changed(void)
647{
648	create();
649}
650
651static void on_close(void)
652{
653	widget_free(&mixer_widget);
654}
655
656void mixer_shutdown(void)
657{
658	free_controls();
659	if (mixer)
660		snd_mixer_close(mixer);
661	if (current_selem_id)
662		snd_mixer_selem_id_free(current_selem_id);
663}
664
665struct widget mixer_widget = {
666	.handle_key = on_handle_key,
667	.window_size_changed = on_window_size_changed,
668	.close = on_close,
669};
670
671void create_mixer_widget(void)
672{
673	create();
674}
675