1/* $NetBSD: main.c,v 1.6 2024/05/20 22:21:45 nia Exp $ */
2/*-
3 * Copyright (c) 2021 The NetBSD Foundation, Inc.
4 * All rights reserved.
5 *
6 * This code is derived from software contributed to The NetBSD Foundation
7 * by Nia Alarie.
8 *
9 * Redistribution and use in source and binary forms, with or without
10 * modification, are permitted provided that the following conditions
11 * are met:
12 * 1. Redistributions of source code must retain the above copyright
13 *    notice, this list of conditions and the following disclaimer.
14 * 2. Redistributions in binary form must reproduce the above copyright
15 *    notice, this list of conditions and the following disclaimer in the
16 *    documentation and/or other materials provided with the distribution.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
19 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
22 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 * POSSIBILITY OF SUCH DAMAGE.
29 */
30#include <sys/audioio.h>
31#include <sys/ioctl.h>
32#include <fcntl.h>
33#include <unistd.h>
34#include <signal.h>
35#include <paths.h>
36#include <curses.h>
37#include <stdlib.h>
38#include <err.h>
39#include "app.h"
40#include "draw.h"
41#include "parse.h"
42
43static void process_device_select(struct aiomixer *, unsigned int);
44static void open_device(struct aiomixer *, const char *);
45static void __dead usage(void);
46static int adjust_level(int, int);
47static int select_class(struct aiomixer *, unsigned int);
48static int select_control(struct aiomixer *, unsigned int);
49static void slide_control(struct aiomixer *, struct aiomixer_control *, bool);
50static int toggle_set(struct aiomixer *);
51static void step_up(struct aiomixer *);
52static void step_down(struct aiomixer *);
53static int read_key(struct aiomixer *, int);
54
55static void __dead
56usage(void)
57{
58	fputs("aiomixer [-u] [-d device]\n", stderr);
59	exit(1);
60}
61
62static int
63select_class(struct aiomixer *aio, unsigned int n)
64{
65	struct aiomixer_class *class;
66	unsigned i;
67
68	if (n >= aio->numclasses)
69		return -1;
70
71	class = &aio->classes[n];
72	aio->widgets_resized = true;
73	aio->class_scroll_y = 0;
74	aio->curcontrol = 0;
75	aio->curclass = n;
76	for (i = 0; i < class->numcontrols; ++i) {
77		class->controls[i].setindex = -1;
78		draw_control(aio, &class->controls[i], false);
79	}
80	draw_classbar(aio);
81	return 0;
82}
83
84static int
85select_control(struct aiomixer *aio, unsigned int n)
86{
87	struct aiomixer_class *class;
88	struct aiomixer_control *lastcontrol;
89	struct aiomixer_control *control;
90
91	class = &aio->classes[aio->curclass];
92
93	if (n >= class->numcontrols)
94		return -1;
95
96	lastcontrol = &class->controls[aio->curcontrol];
97	lastcontrol->setindex = -1;
98	draw_control(aio, lastcontrol, false);
99
100	control = &class->controls[n];
101	aio->curcontrol = n;
102	control->setindex = 0;
103	draw_control(aio, control, true);
104
105	if (aio->class_scroll_y > control->widget_y) {
106		aio->class_scroll_y = control->widget_y;
107		aio->widgets_resized = true;
108	}
109
110	if ((control->widget_y + control->height) >
111	    ((getmaxy(stdscr) - 4) + aio->class_scroll_y)) {
112		aio->class_scroll_y = control->widget_y;
113		aio->widgets_resized = true;
114	}
115	return 0;
116}
117
118static int
119adjust_level(int level, int delta)
120{
121	if (level > (AUDIO_MAX_GAIN - delta))
122		return AUDIO_MAX_GAIN;
123
124	if (delta < 0 && level < (AUDIO_MIN_GAIN + (-delta)))
125		return AUDIO_MIN_GAIN;
126
127	return level + delta;
128}
129
130static void
131slide_control(struct aiomixer *aio,
132    struct aiomixer_control *control, bool right)
133{
134	struct mixer_devinfo *info = &control->info;
135	struct mixer_ctrl value;
136	unsigned char *level;
137	int i, delta;
138	int cur_index = 0;
139
140	if (info->type != AUDIO_MIXER_SET) {
141		value.dev = info->index;
142		value.type = info->type;
143		if (info->type == AUDIO_MIXER_VALUE)
144			value.un.value.num_channels = info->un.v.num_channels;
145
146		if (ioctl(aio->fd, AUDIO_MIXER_READ, &value) < 0)
147			err(EXIT_FAILURE, "failed to read mixer control");
148	}
149
150	switch (info->type) {
151	case AUDIO_MIXER_VALUE:
152		if (info->un.v.delta != 0) {
153			delta = right ? info->un.v.delta : -info->un.v.delta;
154		} else {
155			/* delta is 0 in qemu with sb(4) */
156			delta = right ? 16 : -16;
157		}
158		/*
159		 * work around strange problem where the level can be
160		 * increased but not decreased, seen with uaudio(4)
161		 */
162		if (delta < 16)
163			delta *= 2;
164		if (aio->channels_unlocked) {
165			level = &value.un.value.level[control->setindex];
166			*level = (unsigned char)adjust_level(*level, delta);
167		} else {
168			for (i = 0; i < value.un.value.num_channels; ++i) {
169				level = &value.un.value.level[i];
170				*level = (unsigned char)adjust_level(*level, delta);
171			}
172		}
173		break;
174	case AUDIO_MIXER_ENUM:
175		for (i = 0; i < info->un.e.num_mem; ++i) {
176			if (info->un.e.member[i].ord == value.un.ord) {
177				cur_index = i;
178				break;
179			}
180		}
181		if (right) {
182			value.un.ord = cur_index < (info->un.e.num_mem - 1) ?
183			    info->un.e.member[cur_index + 1].ord :
184			    info->un.e.member[0].ord;
185		} else {
186			value.un.ord = cur_index > 0 ?
187			    info->un.e.member[cur_index - 1].ord :
188			    info->un.e.member[control->info.un.e.num_mem - 1].ord;
189		}
190		break;
191	case AUDIO_MIXER_SET:
192		if (right) {
193			control->setindex =
194			    control->setindex < (info->un.s.num_mem - 1) ?
195				control->setindex + 1 : 0;
196		} else {
197			control->setindex = control->setindex > 0 ?
198			    control->setindex - 1 :
199				control->info.un.s.num_mem - 1;
200		}
201		break;
202	}
203
204	if (info->type != AUDIO_MIXER_SET) {
205		if (ioctl(aio->fd, AUDIO_MIXER_WRITE, &value) < 0)
206			err(EXIT_FAILURE, "failed to adjust mixer control");
207	}
208
209	draw_control(aio, control, true);
210}
211
212static int
213toggle_set(struct aiomixer *aio)
214{
215	struct mixer_ctrl ctrl;
216	struct aiomixer_class *class = &aio->classes[aio->curclass];
217	struct aiomixer_control *control = &class->controls[aio->curcontrol];
218
219	ctrl.dev = control->info.index;
220	ctrl.type = control->info.type;
221
222	if (control->info.type != AUDIO_MIXER_SET)
223		return -1;
224
225	if (ioctl(aio->fd, AUDIO_MIXER_READ, &ctrl) < 0)
226		err(EXIT_FAILURE, "failed to read mixer control");
227
228	ctrl.un.mask ^= control->info.un.s.member[control->setindex].mask;
229
230	if (ioctl(aio->fd, AUDIO_MIXER_WRITE, &ctrl) < 0)
231		err(EXIT_FAILURE, "failed to read mixer control");
232
233	draw_control(aio, control, true);
234	return 0;
235}
236
237static void
238step_up(struct aiomixer *aio)
239{
240	struct aiomixer_class *class;
241	struct aiomixer_control *control;
242
243	class = &aio->classes[aio->curclass];
244	control = &class->controls[aio->curcontrol];
245
246	if (aio->channels_unlocked &&
247	    control->info.type == AUDIO_MIXER_VALUE &&
248	    control->setindex > 0) {
249		control->setindex--;
250		draw_control(aio, control, true);
251		return;
252	}
253	select_control(aio, aio->curcontrol - 1);
254}
255
256static void
257step_down(struct aiomixer *aio)
258{
259	struct aiomixer_class *class;
260	struct aiomixer_control *control;
261
262	class = &aio->classes[aio->curclass];
263	control = &class->controls[aio->curcontrol];
264
265	if (aio->channels_unlocked &&
266	    control->info.type == AUDIO_MIXER_VALUE &&
267	    control->setindex < (control->info.un.v.num_channels - 1)) {
268		control->setindex++;
269		draw_control(aio, control, true);
270		return;
271	}
272
273	select_control(aio, (aio->curcontrol + 1) % class->numcontrols);
274}
275
276static int
277read_key(struct aiomixer *aio, int ch)
278{
279	struct aiomixer_class *class;
280	struct aiomixer_control *control;
281	size_t i;
282
283	switch (ch) {
284	case KEY_RESIZE:
285		class = &aio->classes[aio->curclass];
286		resize_widgets(aio);
287		draw_header(aio);
288		draw_classbar(aio);
289		for (i = 0; i < class->numcontrols; ++i) {
290			draw_control(aio,
291			    &class->controls[i],
292			    aio->state == STATE_CONTROL_SELECT ?
293				(aio->curcontrol == i) : false);
294		}
295		break;
296	case KEY_LEFT:
297	case 'h':
298		if (aio->state == STATE_CLASS_SELECT) {
299			select_class(aio, aio->curclass > 0 ?
300			    aio->curclass - 1 : aio->numclasses - 1);
301		} else if (aio->state == STATE_CONTROL_SELECT) {
302			class = &aio->classes[aio->curclass];
303			slide_control(aio,
304			    &class->controls[aio->curcontrol], false);
305		}
306		break;
307	case KEY_RIGHT:
308	case 'l':
309		if (aio->state == STATE_CLASS_SELECT) {
310			select_class(aio,
311			    (aio->curclass + 1) % aio->numclasses);
312		} else if (aio->state == STATE_CONTROL_SELECT) {
313			class = &aio->classes[aio->curclass];
314			slide_control(aio,
315			    &class->controls[aio->curcontrol], true);
316		}
317		break;
318	case KEY_UP:
319	case 'k':
320		if (aio->state == STATE_CONTROL_SELECT) {
321			if (aio->curcontrol == 0) {
322				class = &aio->classes[aio->curclass];
323				control = &class->controls[aio->curcontrol];
324				control->setindex = -1;
325				aio->state = STATE_CLASS_SELECT;
326				draw_control(aio, control, false);
327			} else {
328				step_up(aio);
329			}
330		}
331		break;
332	case KEY_DOWN:
333	case 'j':
334		if (aio->state == STATE_CLASS_SELECT) {
335			class = &aio->classes[aio->curclass];
336			if (class->numcontrols > 0) {
337				aio->state = STATE_CONTROL_SELECT;
338				select_control(aio, 0);
339			}
340		} else if (aio->state == STATE_CONTROL_SELECT) {
341			step_down(aio);
342		}
343		break;
344	case '\n':
345	case ' ':
346		if (aio->state == STATE_CONTROL_SELECT)
347			toggle_set(aio);
348		break;
349	case '1':
350		select_class(aio, 0);
351		break;
352	case '2':
353		select_class(aio, 1);
354		break;
355	case '3':
356		select_class(aio, 2);
357		break;
358	case '4':
359		select_class(aio, 3);
360		break;
361	case '5':
362		select_class(aio, 4);
363		break;
364	case '6':
365		select_class(aio, 5);
366		break;
367	case '7':
368		select_class(aio, 6);
369		break;
370	case '8':
371		select_class(aio, 7);
372		break;
373	case '9':
374		select_class(aio, 8);
375		break;
376	case 'q':
377	case '\e':
378		if (aio->state == STATE_CONTROL_SELECT) {
379			class = &aio->classes[aio->curclass];
380			control = &class->controls[aio->curcontrol];
381			aio->state = STATE_CLASS_SELECT;
382			draw_control(aio, control, false);
383			break;
384		}
385		return 1;
386	case 'u':
387		aio->channels_unlocked = !aio->channels_unlocked;
388		if (aio->state == STATE_CONTROL_SELECT) {
389			class = &aio->classes[aio->curclass];
390			control = &class->controls[aio->curcontrol];
391			if (control->info.type == AUDIO_MIXER_VALUE)
392				draw_control(aio, control, true);
393		}
394		break;
395	}
396
397	draw_screen(aio);
398	return 0;
399}
400
401static void
402process_device_select(struct aiomixer *aio, unsigned int num_devices)
403{
404	unsigned int selected_device = 0;
405	char device_path[16];
406	int ch;
407
408	draw_mixer_select(num_devices, selected_device);
409
410	while ((ch = getch()) != ERR) {
411		switch (ch) {
412		case '\n':
413			clear();
414			(void)snprintf(device_path, sizeof(device_path),
415			    "/dev/mixer%d", selected_device);
416			open_device(aio, device_path);
417			return;
418		case KEY_UP:
419		case 'k':
420			if (selected_device > 0)
421				selected_device--;
422			else
423				selected_device = (num_devices - 1);
424			break;
425		case KEY_DOWN:
426		case 'j':
427			if (selected_device < (num_devices - 1))
428				selected_device++;
429			else
430				selected_device = 0;
431			break;
432		case '1':
433			selected_device = 0;
434			break;
435		case '2':
436			selected_device = 1;
437			break;
438		case '3':
439			selected_device = 2;
440			break;
441		case '4':
442			selected_device = 3;
443			break;
444		case '5':
445			selected_device = 4;
446			break;
447		case '6':
448			selected_device = 5;
449			break;
450		case '7':
451			selected_device = 6;
452			break;
453		case '8':
454			selected_device = 7;
455			break;
456		case '9':
457			selected_device = 8;
458			break;
459		}
460		draw_mixer_select(num_devices, selected_device);
461	}
462}
463
464static void
465open_device(struct aiomixer *aio, const char *device)
466{
467	int ch;
468
469	if ((aio->fd = open(device, O_RDWR)) < 0)
470		err(EXIT_FAILURE, "couldn't open mixer device");
471
472	if (ioctl(aio->fd, AUDIO_GETDEV, &aio->mixerdev) < 0)
473		err(EXIT_FAILURE, "AUDIO_GETDEV failed");
474
475	aio->state = STATE_CLASS_SELECT;
476
477	aiomixer_parse(aio);
478
479	create_widgets(aio);
480
481	draw_header(aio);
482	select_class(aio, 0);
483	draw_screen(aio);
484
485	while ((ch = getch()) != ERR) {
486		if (read_key(aio, ch) != 0)
487			break;
488	}
489}
490
491static __dead void
492on_signal(int dummy)
493{
494	endwin();
495	exit(0);
496}
497
498int
499main(int argc, char **argv)
500{
501	const char *mixer_device = NULL;
502	extern char *optarg;
503	extern int optind;
504	struct aiomixer *aio;
505	char mixer_path[32];
506	unsigned int mixer_count = 0;
507	int i, fd;
508	int ch;
509	char *no_color = getenv("NO_COLOR");
510
511	if ((aio = malloc(sizeof(struct aiomixer))) == NULL) {
512		err(EXIT_FAILURE, "malloc failed");
513	}
514
515	while ((ch = getopt(argc, argv, "d:u")) != -1) {
516		switch (ch) {
517		case 'd':
518			mixer_device = optarg;
519			break;
520		case 'u':
521			aio->channels_unlocked = true;
522			break;
523		default:
524			usage();
525			break;
526		}
527	}
528
529	argc -= optind;
530	argv += optind;
531
532	if (initscr() == NULL)
533		err(EXIT_FAILURE, "can't initialize curses");
534
535	(void)signal(SIGHUP, on_signal);
536	(void)signal(SIGINT, on_signal);
537	(void)signal(SIGTERM, on_signal);
538
539	curs_set(0);
540	keypad(stdscr, TRUE);
541	cbreak();
542	noecho();
543
544	aio->use_colour = true;
545
546	if (!has_colors())
547		aio->use_colour = false;
548
549	if (no_color != NULL && no_color[0] != '\0')
550		aio->use_colour = false;
551
552	if (aio->use_colour) {
553		start_color();
554		use_default_colors();
555		init_pair(COLOR_CONTROL_SELECTED, COLOR_BLUE, COLOR_BLACK);
556		init_pair(COLOR_LEVELS, COLOR_GREEN, COLOR_BLACK);
557		init_pair(COLOR_SET_SELECTED, COLOR_BLACK, COLOR_GREEN);
558		init_pair(COLOR_ENUM_ON, COLOR_WHITE, COLOR_RED);
559		init_pair(COLOR_ENUM_OFF, COLOR_WHITE, COLOR_BLUE);
560		init_pair(COLOR_ENUM_MISC, COLOR_BLACK, COLOR_YELLOW);
561	}
562
563	if (mixer_device != NULL) {
564		open_device(aio, mixer_device);
565	} else {
566		for (i = 0; i < 16; ++i) {
567			(void)snprintf(mixer_path, sizeof(mixer_path),
568			    "/dev/mixer%d", i);
569			fd = open(mixer_path, O_RDWR);
570			if (fd == -1)
571				break;
572			close(fd);
573			mixer_count++;
574		}
575
576		if (mixer_count > 1) {
577			process_device_select(aio, mixer_count);
578		} else {
579			open_device(aio, _PATH_MIXER);
580		}
581	}
582
583	endwin();
584	close(aio->fd);
585	free(aio);
586
587	return 0;
588}
589