xref: /netbsd-src/usr.bin/aiomixer/main.c (revision 33b82b03392e2106db8728db8eea7f5301157325)
1 /* $NetBSD: main.c,v 1.7 2024/11/03 10:43:27 rillig 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 
43 static void process_device_select(struct aiomixer *, unsigned int);
44 static void open_device(struct aiomixer *, const char *);
45 static void __dead usage(void);
46 static int adjust_level(int, int);
47 static int select_class(struct aiomixer *, unsigned int);
48 static int select_control(struct aiomixer *, unsigned int);
49 static void slide_control(struct aiomixer *, struct aiomixer_control *, bool);
50 static int toggle_set(struct aiomixer *);
51 static void step_up(struct aiomixer *);
52 static void step_down(struct aiomixer *);
53 static int read_key(struct aiomixer *, int);
54 
55 static void __dead
56 usage(void)
57 {
58 	fputs("aiomixer [-u] [-d device]\n", stderr);
59 	exit(1);
60 }
61 
62 static int
63 select_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 
84 static int
85 select_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 
118 static int
119 adjust_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 
130 static void
131 slide_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 
212 static int
213 toggle_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 
237 static void
238 step_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 
256 static void
257 step_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 
276 static int
277 read_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 
401 static void
402 process_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 
464 static void
465 open_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 
491 static __dead void
492 on_signal(int dummy)
493 {
494 	endwin();
495 	exit(0);
496 }
497 
498 int
499 main(int argc, char **argv)
500 {
501 	const char *mixer_device = NULL;
502 	struct aiomixer *aio;
503 	char mixer_path[32];
504 	unsigned int mixer_count = 0;
505 	int i, fd;
506 	int ch;
507 	char *no_color = getenv("NO_COLOR");
508 
509 	if ((aio = malloc(sizeof(struct aiomixer))) == NULL) {
510 		err(EXIT_FAILURE, "malloc failed");
511 	}
512 
513 	while ((ch = getopt(argc, argv, "d:u")) != -1) {
514 		switch (ch) {
515 		case 'd':
516 			mixer_device = optarg;
517 			break;
518 		case 'u':
519 			aio->channels_unlocked = true;
520 			break;
521 		default:
522 			usage();
523 			break;
524 		}
525 	}
526 
527 	argc -= optind;
528 	argv += optind;
529 
530 	if (initscr() == NULL)
531 		err(EXIT_FAILURE, "can't initialize curses");
532 
533 	(void)signal(SIGHUP, on_signal);
534 	(void)signal(SIGINT, on_signal);
535 	(void)signal(SIGTERM, on_signal);
536 
537 	curs_set(0);
538 	keypad(stdscr, TRUE);
539 	cbreak();
540 	noecho();
541 
542 	aio->use_colour = true;
543 
544 	if (!has_colors())
545 		aio->use_colour = false;
546 
547 	if (no_color != NULL && no_color[0] != '\0')
548 		aio->use_colour = false;
549 
550 	if (aio->use_colour) {
551 		start_color();
552 		use_default_colors();
553 		init_pair(COLOR_CONTROL_SELECTED, COLOR_BLUE, COLOR_BLACK);
554 		init_pair(COLOR_LEVELS, COLOR_GREEN, COLOR_BLACK);
555 		init_pair(COLOR_SET_SELECTED, COLOR_BLACK, COLOR_GREEN);
556 		init_pair(COLOR_ENUM_ON, COLOR_WHITE, COLOR_RED);
557 		init_pair(COLOR_ENUM_OFF, COLOR_WHITE, COLOR_BLUE);
558 		init_pair(COLOR_ENUM_MISC, COLOR_BLACK, COLOR_YELLOW);
559 	}
560 
561 	if (mixer_device != NULL) {
562 		open_device(aio, mixer_device);
563 	} else {
564 		for (i = 0; i < 16; ++i) {
565 			(void)snprintf(mixer_path, sizeof(mixer_path),
566 			    "/dev/mixer%d", i);
567 			fd = open(mixer_path, O_RDWR);
568 			if (fd == -1)
569 				break;
570 			close(fd);
571 			mixer_count++;
572 		}
573 
574 		if (mixer_count > 1) {
575 			process_device_select(aio, mixer_count);
576 		} else {
577 			open_device(aio, _PATH_MIXER);
578 		}
579 	}
580 
581 	endwin();
582 	close(aio->fd);
583 	free(aio);
584 
585 	return 0;
586 }
587