Module Name: src Committed By: nia Date: Fri May 7 16:29:24 UTC 2021
Added Files: src/usr.bin/aiomixer: Makefile aiomixer.1 app.h draw.c draw.h main.c parse.c parse.h Log Message: import aiomixer from git into usr.bin. aiomixer is a graphical (curses-based) mixer for NetBSD audio. To generate a diff of this commit: cvs rdiff -u -r0 -r1.1 src/usr.bin/aiomixer/Makefile \ src/usr.bin/aiomixer/aiomixer.1 src/usr.bin/aiomixer/app.h \ src/usr.bin/aiomixer/draw.c src/usr.bin/aiomixer/draw.h \ src/usr.bin/aiomixer/main.c src/usr.bin/aiomixer/parse.c \ src/usr.bin/aiomixer/parse.h Please note that diffs are not public domain; they are subject to the copyright notices on the relevant files.
Added files: Index: src/usr.bin/aiomixer/Makefile diff -u /dev/null src/usr.bin/aiomixer/Makefile:1.1 --- /dev/null Fri May 7 16:29:24 2021 +++ src/usr.bin/aiomixer/Makefile Fri May 7 16:29:24 2021 @@ -0,0 +1,11 @@ +# $NetBSD: Makefile,v 1.1 2021/05/07 16:29:24 nia Exp $ + +PROG= aiomixer +SRCS+= main.c draw.c parse.c + +LDADD+= -lcurses +DPADD+= $(LIBCURSES) + +WARNS= 6 + +.include <bsd.prog.mk> Index: src/usr.bin/aiomixer/aiomixer.1 diff -u /dev/null src/usr.bin/aiomixer/aiomixer.1:1.1 --- /dev/null Fri May 7 16:29:24 2021 +++ src/usr.bin/aiomixer/aiomixer.1 Fri May 7 16:29:24 2021 @@ -0,0 +1,90 @@ +.\" $NetBSD: aiomixer.1,v 1.1 2021/05/07 16:29:24 nia Exp $ +.\" +.\" Copyright (c) 2021 The NetBSD Foundation, Inc. +.\" All rights reserved. +.\" +.\" This code is derived from software contributed to The NetBSD Foundation +.\" by Nia Alarie. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS +.\" ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +.\" TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +.\" PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS +.\" BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +.\" CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +.\" SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +.\" INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +.\" CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +.\" ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +.\" POSSIBILITY OF SUCH DAMAGE. +.\" +.Dd April 4, 2019 +.Dt AIOMIXER 1 +.Os +.Sh NAME +.Nm aiomixer +.Nd graphical mixer for +.Nx +audio +.Sh SYNOPSIS +.Nm aiomixer +.Op Fl d Ar device +.Op Fl u +.Sh DESCRIPTION +.Nm +is a graphical frontend for +.Xr mixer 4 +devices that runs in your terminal. +.Pp +.Nm +is primarily controlled using the cursor keys, e.g. to select a +control, or change a control's value. +Movement similar to +.Xr vi 1 +is also supported. +The current class can be changed with the number keys. +The Q or Escape key can be used to return to the previous pane or exit +.Nm . +.Pp +By default, volume levels for individual channels cannot be changed +separately. +The U key can be pressed to toggle changing the volume of individual +channels. +.Pp +The following options are available: +.Bl -tag -width indent +.It Fl d Ar device +Used to specify an alternative mixer device. +.It Fl u +Used to unlock channels on start up. +.El +.Sh FILES +.Bl -tag -width /dev/mixer[0-3] -compact +.It Pa /dev/mixer +.It Pa /dev/mixer[0-3] +.El +.Sh SEE ALSO +.Xr mixerctl 1 , +.Xr mixer 4 +.Sh HISTORY +The first version (only available in pkgsrc) didn't support +.Dv AUDIO_MIXER_SET +properly, among other bugs. +.Sh AUTHORS +.Nm +was written by +.An Nia Alarie +.Aq n...@netbsd.org . +.Sh BUGS +.Nm aiomixer +shows whatever mixer nodes the audio device's driver returns. +Some drivers do not provide the most user-friendly control names. Index: src/usr.bin/aiomixer/app.h diff -u /dev/null src/usr.bin/aiomixer/app.h:1.1 --- /dev/null Fri May 7 16:29:24 2021 +++ src/usr.bin/aiomixer/app.h Fri May 7 16:29:24 2021 @@ -0,0 +1,82 @@ +/* $NetBSD: app.h,v 1.1 2021/05/07 16:29:24 nia Exp $ */ +/*- + * Copyright (c) 2021 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Nia Alarie. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +#ifndef APP_H +#define APP_H +#include <sys/audioio.h> +#include <stdbool.h> + +enum aiomixer_state { + STATE_DEVICE_SELECT, + STATE_CLASS_SELECT, + STATE_CONTROL_SELECT +}; + +struct aiomixer_control { + struct mixer_devinfo info; + /* currently selected index for sets, channel for sliders */ + int setindex; + int widget_y; + int height; + WINDOW *widgetpad; +}; + +struct aiomixer_class { + char name[MAX_AUDIO_DEV_LEN]; + struct aiomixer_control controls[128]; + unsigned int numcontrols; + WINDOW *widgetpad; + int index; + int height; +}; + +struct aiomixer { + int fd; + enum aiomixer_state state; + struct audio_device mixerdev; + struct aiomixer_class classes[128]; + unsigned int numclasses; + unsigned int curclass; + unsigned int curcontrol; + bool channels_unlocked; + int class_scroll_y; + int last_max_x; + bool widgets_resized; + WINDOW *header; + WINDOW *classbar; +}; + +#define COLOR_CONTROL_SELECTED 1 +#define COLOR_LEVELS 2 +#define COLOR_SET_SELECTED 3 +#define COLOR_ENUM_OFF 4 +#define COLOR_ENUM_ON 5 +#define COLOR_ENUM_MISC 6 + +#endif Index: src/usr.bin/aiomixer/draw.c diff -u /dev/null src/usr.bin/aiomixer/draw.c:1.1 --- /dev/null Fri May 7 16:29:24 2021 +++ src/usr.bin/aiomixer/draw.c Fri May 7 16:29:24 2021 @@ -0,0 +1,416 @@ +/* $NetBSD: draw.c,v 1.1 2021/05/07 16:29:24 nia Exp $ */ +/*- + * Copyright (c) 2021 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Nia Alarie. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +#include <sys/audioio.h> +#include <sys/ioctl.h> +#include <fcntl.h> +#include <unistd.h> +#include <curses.h> +#include <err.h> +#include <stdlib.h> +#include "draw.h" + +static void bold_on(WINDOW *); +static void bold_off(WINDOW *); +static int get_enum_color(const char *); +static void draw_enum(struct aiomixer_control *, int, bool); +static void draw_set(struct aiomixer_control *, int); +static void draw_levels(struct aiomixer_control *, + const struct mixer_level *, bool, bool); + +static void +bold_on(WINDOW *w) +{ + /* + * Some (XXX: which?) legacy terminals do not support a Bold + * attribute. In this case, we fall back to standout. + */ + if (termattrs() & A_BOLD) + wattron(w, A_BOLD); + else + wattron(w, A_STANDOUT); +} + +static void +bold_off(WINDOW *w) +{ + chtype attrs = getattrs(w); + + if (attrs & A_BOLD) + wattroff(w, A_BOLD); + if (attrs & A_STANDOUT) + wattroff(w, A_STANDOUT); +} + +void +draw_mixer_select(unsigned int num_mixers, unsigned int selected_mixer) +{ + struct audio_device dev; + char mixer_path[16]; + unsigned int i; + int fd; + + mvprintw(0, 0, "Select a mixer device:\n"); + + for (i = 0; i < num_mixers; ++i) { + (void)snprintf(mixer_path, sizeof(mixer_path), + "/dev/mixer%d", i); + fd = open(mixer_path, O_RDWR); + if (fd == -1) + break; + if (ioctl(fd, AUDIO_GETDEV, &dev) < 0) { + close(fd); + break; + } + close(fd); + if (selected_mixer == i) { + bold_on(stdscr); + addstr("[*] "); + } else { + addstr("[ ] "); + } + printw("%s: %s %s %s\n", mixer_path, + dev.name, dev.version, dev.config); + if (selected_mixer == i) + bold_off(stdscr); + } +} + +void +draw_control(struct aiomixer *aio, + struct aiomixer_control *control, bool selected) +{ + struct mixer_ctrl value; + + value.dev = control->info.index; + value.type = control->info.type; + if (value.type == AUDIO_MIXER_VALUE) + value.un.value.num_channels = control->info.un.v.num_channels; + + if (ioctl(aio->fd, AUDIO_MIXER_READ, &value) < 0) + err(EXIT_FAILURE, "failed to read from mixer device"); + + wclear(control->widgetpad); + if (selected) { + bold_on(control->widgetpad); + if (has_colors()) { + wattron(control->widgetpad, + COLOR_PAIR(COLOR_CONTROL_SELECTED)); + } + waddch(control->widgetpad, '*'); + if (has_colors()) { + wattroff(control->widgetpad, + COLOR_PAIR(COLOR_CONTROL_SELECTED)); + } + } + wprintw(control->widgetpad, "%s\n", control->info.label.name); + if (selected) + bold_off(control->widgetpad); + + switch (value.type) { + case AUDIO_MIXER_ENUM: + draw_enum(control, value.un.ord, selected); + break; + case AUDIO_MIXER_SET: + draw_set(control, value.un.mask); + break; + case AUDIO_MIXER_VALUE: + draw_levels(control, &value.un.value, + aio->channels_unlocked, selected); + break; + } +} + +void +draw_screen(struct aiomixer *aio) +{ + int i, max_y; + + /* Clear any leftovers if the screen changed. */ + if (aio->widgets_resized) { + aio->widgets_resized = false; + max_y = getmaxy(stdscr); + for (i = 3; i < max_y; ++i) { + mvprintw(i, 0, "\n"); + } + } + + wnoutrefresh(stdscr); + wnoutrefresh(aio->header); + wnoutrefresh(aio->classbar); + pnoutrefresh(aio->classes[aio->curclass].widgetpad, + aio->class_scroll_y, 0, + 3, 0, + 1 + aio->classes[aio->curclass].height, getmaxx(stdscr)); + doupdate(); +} + +static int +get_enum_color(const char *name) +{ + if (strcmp(name, AudioNon) == 0) { + return COLOR_ENUM_ON; + } + if (strcmp(name, AudioNoff) == 0) { + return COLOR_ENUM_OFF; + } + + return COLOR_ENUM_MISC; +} + +static void +draw_enum(struct aiomixer_control *control, int ord, bool selected) +{ + struct audio_mixer_enum *e; + int color = COLOR_ENUM_MISC; + int i; + + for (i = 0; i < control->info.un.e.num_mem; ++i) { + e = &control->info.un.e; + if (ord == e->member[i].ord && selected) + bold_on(control->widgetpad); + waddch(control->widgetpad, '['); + if (ord == e->member[i].ord) { + if (has_colors()) { + color = get_enum_color(e->member[i].label.name); + wattron(control->widgetpad, + COLOR_PAIR(color)); + } else { + waddch(control->widgetpad, '*'); + } + } + wprintw(control->widgetpad, "%s", e->member[i].label.name); + if (ord == control->info.un.e.member[i].ord) { + if (has_colors()) { + wattroff(control->widgetpad, + COLOR_PAIR(color)); + } + } + waddch(control->widgetpad, ']'); + if (ord == e->member[i].ord && selected) + bold_off(control->widgetpad); + if (i != (e->num_mem - 1)) + waddstr(control->widgetpad, ", "); + } + waddch(control->widgetpad, '\n'); +} + +static void +draw_set(struct aiomixer_control *control, int mask) +{ + int i; + + for (i = 0; i < control->info.un.s.num_mem; ++i) { + waddch(control->widgetpad, '['); + if (mask & control->info.un.s.member[i].mask) { + if (has_colors()) { + wattron(control->widgetpad, + COLOR_PAIR(COLOR_SET_SELECTED)); + } + waddch(control->widgetpad, '*'); + if (has_colors()) { + wattroff(control->widgetpad, + COLOR_PAIR(COLOR_SET_SELECTED)); + } + } else { + waddch(control->widgetpad, ' '); + } + waddstr(control->widgetpad, "] "); + if (control->setindex == i) { + bold_on(control->widgetpad); + waddch(control->widgetpad, '*'); + } + wprintw(control->widgetpad, "%s", + control->info.un.s.member[i].label.name); + if (control->setindex == i) + bold_off(control->widgetpad); + if (i != (control->info.un.s.num_mem - 1)) + waddstr(control->widgetpad, ", "); + } +} + +static void +draw_levels(struct aiomixer_control *control, + const struct mixer_level *levels, bool channels_unlocked, bool selected) +{ + int i; + int j, nchars; + + for (i = 0; i < control->info.un.v.num_channels; ++i) { + if ((selected && !channels_unlocked) || + (control->setindex == i && channels_unlocked)) { + bold_on(control->widgetpad); + } + wprintw(control->widgetpad, "[%3u/%3u ", + levels->level[i], AUDIO_MAX_GAIN); + if (has_colors()) { + wattron(control->widgetpad, + COLOR_PAIR(COLOR_LEVELS)); + } + nchars = (levels->level[i] * + (getmaxx(control->widgetpad) - 11)) / AUDIO_MAX_GAIN; + for (j = 0; j < nchars; ++j) + waddch(control->widgetpad, '*'); + if (has_colors()) { + wattroff(control->widgetpad, + COLOR_PAIR(COLOR_LEVELS)); + } + nchars = getmaxx(control->widgetpad) - 11 - nchars; + for (j = 0; j < nchars; ++j) + waddch(control->widgetpad, ' '); + wprintw(control->widgetpad, "]\n"); + if ((selected && !channels_unlocked) || + (control->setindex == i && channels_unlocked)) { + bold_off(control->widgetpad); + } + } +} + +void +draw_classbar(struct aiomixer *aio) +{ + unsigned int i; + + wmove(aio->classbar, 0, 0); + + for (i = 0; i < aio->numclasses; ++i) { + if (aio->curclass == i) + bold_on(aio->classbar); + wprintw(aio->classbar, "[%u:", i + 1); + if (aio->curclass == i) { + if (has_colors()) { + wattron(aio->classbar, + COLOR_PAIR(COLOR_CONTROL_SELECTED)); + } + waddch(aio->classbar, '*'); + if (has_colors()) { + wattroff(aio->classbar, + COLOR_PAIR(COLOR_CONTROL_SELECTED)); + } + } + waddstr(aio->classbar, aio->classes[i].name); + if (aio->curclass == i) + bold_off(aio->classbar); + waddstr(aio->classbar, "] "); + } + + wprintw(aio->classbar, "\n\n"); +} + +void +draw_header(struct aiomixer *aio) +{ + mvwaddstr(aio->header, 0, + getmaxx(aio->header) - (int)sizeof("NetBSD audio mixer") + 1, + "NetBSD audio mixer"); + + if (aio->mixerdev.version[0] != '\0') { + wprintw(aio->header, "%s %s", + aio->mixerdev.name, aio->mixerdev.version); + } else { + wprintw(aio->header, "%s", aio->mixerdev.name); + } +} + +void +create_widgets(struct aiomixer *aio) +{ + size_t i, j; + struct aiomixer_class *class; + struct aiomixer_control *control; + + aio->header = newwin(1, getmaxx(stdscr), 0, 0); + if (aio->header == NULL) + errx(EXIT_FAILURE, "failed to create window"); + + aio->classbar = newwin(2, getmaxx(stdscr), 1, 0); + if (aio->classbar == NULL) + errx(EXIT_FAILURE, "failed to create window"); + + for (i = 0; i < aio->numclasses; ++i) { + class = &aio->classes[i]; + class->height = 0; + class->widgetpad = newpad(4 * __arraycount(class->controls), + getmaxx(stdscr)); + if (class->widgetpad == NULL) + errx(EXIT_FAILURE, "failed to create curses pad"); + for (j = 0; j < class->numcontrols; ++j) { + control = &class->controls[j]; + switch (control->info.type) { + case AUDIO_MIXER_VALUE: + control->height = 2 + + control->info.un.v.num_channels; + break; + case AUDIO_MIXER_ENUM: + case AUDIO_MIXER_SET: + control->height = 3; + break; + } + control->widgetpad = subpad(class->widgetpad, + control->height, getmaxx(stdscr), + class->height, 0); + if (control->widgetpad == NULL) + errx(EXIT_FAILURE, "failed to create curses pad"); + control->widget_y = class->height; + class->height += control->height; + } + wresize(class->widgetpad, class->height, getmaxx(stdscr)); + } + + aio->last_max_x = getmaxx(stdscr); +} + +void +resize_widgets(struct aiomixer *aio) +{ + size_t i, j; + struct aiomixer_class *class; + struct aiomixer_control *control; + int max_x; + + max_x = getmaxx(stdscr); + + if (aio->last_max_x != max_x) { + aio->last_max_x = max_x; + wresize(aio->header, 1, max_x); + wresize(aio->classbar, 2, max_x); + + for (i = 0; i < aio->numclasses; ++i) { + class = &aio->classes[i]; + wresize(class->widgetpad, class->height, max_x); + for (j = 0; j < class->numcontrols; ++j) { + control = &class->controls[j]; + wresize(control->widgetpad, + control->height, max_x); + } + } + } + + aio->widgets_resized = true; +} Index: src/usr.bin/aiomixer/draw.h diff -u /dev/null src/usr.bin/aiomixer/draw.h:1.1 --- /dev/null Fri May 7 16:29:24 2021 +++ src/usr.bin/aiomixer/draw.h Fri May 7 16:29:24 2021 @@ -0,0 +1,51 @@ +/* $NetBSD: draw.h,v 1.1 2021/05/07 16:29:24 nia Exp $ */ +/*- + * Copyright (c) 2021 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Nia Alarie. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +#ifndef DRAW_H +#define DRAW_H + +#include <sys/audioio.h> +#include <stdbool.h> +#include "app.h" + +void draw_mixer_select(unsigned int, unsigned int); + +void create_widgets(struct aiomixer *); + +void resize_widgets(struct aiomixer *); + +void draw_control(struct aiomixer *, struct aiomixer_control *, bool); + +void draw_screen(struct aiomixer *); + +void draw_classbar(struct aiomixer *); + +void draw_header(struct aiomixer *); + +#endif Index: src/usr.bin/aiomixer/main.c diff -u /dev/null src/usr.bin/aiomixer/main.c:1.1 --- /dev/null Fri May 7 16:29:24 2021 +++ src/usr.bin/aiomixer/main.c Fri May 7 16:29:24 2021 @@ -0,0 +1,572 @@ +/* $NetBSD: main.c,v 1.1 2021/05/07 16:29:24 nia Exp $ */ +/*- + * Copyright (c) 2021 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Nia Alarie. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +#include <sys/audioio.h> +#include <sys/ioctl.h> +#include <fcntl.h> +#include <unistd.h> +#include <signal.h> +#include <paths.h> +#include <curses.h> +#include <stdlib.h> +#include <err.h> +#include "app.h" +#include "draw.h" +#include "parse.h" + +static void process_device_select(struct aiomixer *, unsigned int); +static void open_device(struct aiomixer *, const char *); +static void __dead usage(void); +static int adjust_level(int, int); +static int select_class(struct aiomixer *, unsigned int); +static int select_control(struct aiomixer *, unsigned int); +static void slide_control(struct aiomixer *, struct aiomixer_control *, bool); +static int toggle_set(struct aiomixer *); +static void step_up(struct aiomixer *); +static void step_down(struct aiomixer *); +static int read_key(struct aiomixer *, int); + +static void __dead +usage(void) +{ + fputs("aiomixer [-u] [-d device]\n", stderr); + exit(1); +} + +static int +select_class(struct aiomixer *aio, unsigned int n) +{ + struct aiomixer_class *class; + unsigned i; + + if (n >= aio->numclasses) + return -1; + + class = &aio->classes[n]; + aio->widgets_resized = true; + aio->class_scroll_y = 0; + aio->curcontrol = 0; + aio->curclass = n; + for (i = 0; i < class->numcontrols; ++i) { + class->controls[i].setindex = -1; + draw_control(aio, &class->controls[i], false); + } + draw_classbar(aio); + return 0; +} + +static int +select_control(struct aiomixer *aio, unsigned int n) +{ + struct aiomixer_class *class; + struct aiomixer_control *lastcontrol; + struct aiomixer_control *control; + + class = &aio->classes[aio->curclass]; + + if (n >= class->numcontrols) + return -1; + + lastcontrol = &class->controls[aio->curcontrol]; + lastcontrol->setindex = -1; + draw_control(aio, lastcontrol, false); + + control = &class->controls[n]; + aio->curcontrol = n; + control->setindex = 0; + draw_control(aio, control, true); + + if (aio->class_scroll_y > control->widget_y) { + aio->class_scroll_y = control->widget_y; + aio->widgets_resized = true; + } + + if ((control->widget_y + control->height) > + ((getmaxy(stdscr) - 4) + aio->class_scroll_y)) { + aio->class_scroll_y = control->widget_y; + aio->widgets_resized = true; + } + return 0; +} + +static int +adjust_level(int level, int delta) +{ + if (level > (AUDIO_MAX_GAIN - delta)) + return AUDIO_MAX_GAIN; + + if (delta < 0 && level < (AUDIO_MIN_GAIN + (-delta))) + return AUDIO_MIN_GAIN; + + return level + delta; +} + +static void +slide_control(struct aiomixer *aio, + struct aiomixer_control *control, bool right) +{ + struct mixer_devinfo *info = &control->info; + struct mixer_ctrl value; + unsigned char *level; + int i, delta; + int cur_index = 0; + + if (info->type != AUDIO_MIXER_SET) { + value.dev = info->index; + value.type = info->type; + if (info->type == AUDIO_MIXER_VALUE) + value.un.value.num_channels = info->un.v.num_channels; + + if (ioctl(aio->fd, AUDIO_MIXER_READ, &value) < 0) + err(EXIT_FAILURE, "failed to read mixer control"); + } + + switch (info->type) { + case AUDIO_MIXER_VALUE: + delta = right ? info->un.v.delta : -info->un.v.delta; + /* + * work around strange problem where the level can be + * increased but not decreased, seen with uaudio(4) + */ + if (delta < 16) + delta *= 2; + if (aio->channels_unlocked) { + level = &value.un.value.level[control->setindex]; + *level = (unsigned char)adjust_level(*level, delta); + } else { + for (i = 0; i < value.un.value.num_channels; ++i) { + level = &value.un.value.level[i]; + *level = (unsigned char)adjust_level(*level, delta); + } + } + break; + case AUDIO_MIXER_ENUM: + for (i = 0; i < info->un.e.num_mem; ++i) { + if (info->un.e.member[i].ord == value.un.ord) { + cur_index = i; + break; + } + } + if (right) { + value.un.ord = cur_index < (info->un.e.num_mem - 1) ? + info->un.e.member[cur_index + 1].ord : + info->un.e.member[0].ord; + } else { + value.un.ord = cur_index > 0 ? + info->un.e.member[cur_index - 1].ord : + info->un.e.member[control->info.un.e.num_mem - 1].ord; + } + break; + case AUDIO_MIXER_SET: + if (right) { + control->setindex = + control->setindex < (info->un.s.num_mem - 1) ? + control->setindex + 1 : 0; + } else { + control->setindex = control->setindex > 0 ? + control->setindex - 1 : + control->info.un.s.num_mem - 1; + } + break; + } + + if (info->type != AUDIO_MIXER_SET) { + if (ioctl(aio->fd, AUDIO_MIXER_WRITE, &value) < 0) + err(EXIT_FAILURE, "failed to adjust mixer control"); + } + + draw_control(aio, control, true); +} + +static int +toggle_set(struct aiomixer *aio) +{ + struct mixer_ctrl ctrl; + struct aiomixer_class *class = &aio->classes[aio->curclass]; + struct aiomixer_control *control = &class->controls[aio->curcontrol]; + + ctrl.dev = control->info.index; + ctrl.type = control->info.type; + + if (control->info.type != AUDIO_MIXER_SET) + return -1; + + if (ioctl(aio->fd, AUDIO_MIXER_READ, &ctrl) < 0) + err(EXIT_FAILURE, "failed to read mixer control"); + + ctrl.un.mask ^= control->info.un.s.member[control->setindex].mask; + + if (ioctl(aio->fd, AUDIO_MIXER_WRITE, &ctrl) < 0) + err(EXIT_FAILURE, "failed to read mixer control"); + + draw_control(aio, control, true); + return 0; +} + +static void +step_up(struct aiomixer *aio) +{ + struct aiomixer_class *class; + struct aiomixer_control *control; + + class = &aio->classes[aio->curclass]; + control = &class->controls[aio->curcontrol]; + + if (aio->channels_unlocked && + control->info.type == AUDIO_MIXER_VALUE && + control->setindex > 0) { + control->setindex--; + draw_control(aio, control, true); + return; + } + select_control(aio, aio->curcontrol - 1); +} + +static void +step_down(struct aiomixer *aio) +{ + struct aiomixer_class *class; + struct aiomixer_control *control; + + class = &aio->classes[aio->curclass]; + control = &class->controls[aio->curcontrol]; + + if (aio->channels_unlocked && + control->info.type == AUDIO_MIXER_VALUE && + control->setindex < (control->info.un.v.num_channels - 1)) { + control->setindex++; + draw_control(aio, control, true); + return; + } + + select_control(aio, (aio->curcontrol + 1) % class->numcontrols); +} + +static int +read_key(struct aiomixer *aio, int ch) +{ + struct aiomixer_class *class; + struct aiomixer_control *control; + size_t i; + + switch (ch) { + case KEY_RESIZE: + class = &aio->classes[aio->curclass]; + resize_widgets(aio); + draw_header(aio); + draw_classbar(aio); + for (i = 0; i < class->numcontrols; ++i) { + draw_control(aio, + &class->controls[i], + aio->state == STATE_CONTROL_SELECT ? + (aio->curcontrol == i) : false); + } + break; + case KEY_LEFT: + case 'h': + if (aio->state == STATE_CLASS_SELECT) { + select_class(aio, aio->curclass > 0 ? + aio->curclass - 1 : aio->numclasses - 1); + } else if (aio->state == STATE_CONTROL_SELECT) { + class = &aio->classes[aio->curclass]; + slide_control(aio, + &class->controls[aio->curcontrol], false); + } + break; + case KEY_RIGHT: + case 'l': + if (aio->state == STATE_CLASS_SELECT) { + select_class(aio, + (aio->curclass + 1) % aio->numclasses); + } else if (aio->state == STATE_CONTROL_SELECT) { + class = &aio->classes[aio->curclass]; + slide_control(aio, + &class->controls[aio->curcontrol], true); + } + break; + case KEY_UP: + case 'k': + if (aio->state == STATE_CONTROL_SELECT) { + if (aio->curcontrol == 0) { + class = &aio->classes[aio->curclass]; + control = &class->controls[aio->curcontrol]; + control->setindex = -1; + aio->state = STATE_CLASS_SELECT; + draw_control(aio, control, false); + } else { + step_up(aio); + } + } + break; + case KEY_DOWN: + case 'j': + if (aio->state == STATE_CLASS_SELECT) { + class = &aio->classes[aio->curclass]; + if (class->numcontrols > 0) { + aio->state = STATE_CONTROL_SELECT; + select_control(aio, 0); + } + } else if (aio->state == STATE_CONTROL_SELECT) { + step_down(aio); + } + break; + case '\n': + case ' ': + if (aio->state == STATE_CONTROL_SELECT) + toggle_set(aio); + break; + case '1': + select_class(aio, 0); + break; + case '2': + select_class(aio, 1); + break; + case '3': + select_class(aio, 2); + break; + case '4': + select_class(aio, 3); + break; + case '5': + select_class(aio, 4); + break; + case '6': + select_class(aio, 5); + break; + case '7': + select_class(aio, 6); + break; + case '8': + select_class(aio, 7); + break; + case '9': + select_class(aio, 8); + break; + case 'q': + case '\e': + if (aio->state == STATE_CONTROL_SELECT) { + class = &aio->classes[aio->curclass]; + control = &class->controls[aio->curcontrol]; + aio->state = STATE_CLASS_SELECT; + draw_control(aio, control, false); + break; + } + return 1; + case 'u': + aio->channels_unlocked = !aio->channels_unlocked; + if (aio->state == STATE_CONTROL_SELECT) { + class = &aio->classes[aio->curclass]; + control = &class->controls[aio->curcontrol]; + if (control->info.type == AUDIO_MIXER_VALUE) + draw_control(aio, control, true); + } + break; + } + + draw_screen(aio); + return 0; +} + +static void +process_device_select(struct aiomixer *aio, unsigned int num_devices) +{ + unsigned int selected_device = 0; + char device_path[16]; + int ch; + + draw_mixer_select(num_devices, selected_device); + + while ((ch = getch()) != ERR) { + switch (ch) { + case '\n': + (void)snprintf(device_path, sizeof(device_path), + "/dev/mixer%d", selected_device); + open_device(aio, device_path); + return; + case KEY_UP: + case 'k': + if (selected_device > 0) + selected_device--; + else + selected_device = (num_devices - 1); + break; + case KEY_DOWN: + case 'j': + if (selected_device < (num_devices - 1)) + selected_device++; + else + selected_device = 0; + break; + case '1': + selected_device = 0; + break; + case '2': + selected_device = 1; + break; + case '3': + selected_device = 2; + break; + case '4': + selected_device = 3; + break; + case '5': + selected_device = 4; + break; + case '6': + selected_device = 5; + break; + case '7': + selected_device = 6; + break; + case '8': + selected_device = 7; + break; + case '9': + selected_device = 8; + break; + } + draw_mixer_select(num_devices, selected_device); + } +} + +static void +open_device(struct aiomixer *aio, const char *device) +{ + int ch; + + if ((aio->fd = open(device, O_RDWR)) < 0) + err(EXIT_FAILURE, "couldn't open mixer device"); + + if (ioctl(aio->fd, AUDIO_GETDEV, &aio->mixerdev) < 0) + err(EXIT_FAILURE, "AUDIO_GETDEV failed"); + + aio->state = STATE_CLASS_SELECT; + + aiomixer_parse(aio); + + create_widgets(aio); + + draw_header(aio); + select_class(aio, 0); + draw_screen(aio); + + while ((ch = getch()) != ERR) { + if (read_key(aio, ch) != 0) + break; + } +} + +static void +on_signal(int dummy) +{ + endwin(); + exit(0); +} + +int +main(int argc, char **argv) +{ + const char *mixer_device = NULL; + extern char *optarg; + extern int optind; + struct aiomixer *aio; + char mixer_path[32]; + unsigned int mixer_count = 0; + int i, fd; + int ch; + + if ((aio = malloc(sizeof(struct aiomixer))) == NULL) { + err(EXIT_FAILURE, "malloc failed"); + } + + while ((ch = getopt(argc, argv, "d:u")) != -1) { + switch (ch) { + case 'd': + mixer_device = optarg; + break; + case 'u': + aio->channels_unlocked = true; + break; + default: + usage(); + break; + } + } + + argc -= optind; + argv += optind; + + if (initscr() == NULL) + err(EXIT_FAILURE, "can't initialize curses"); + + (void)signal(SIGHUP, on_signal); + (void)signal(SIGINT, on_signal); + (void)signal(SIGTERM, on_signal); + + curs_set(0); + keypad(stdscr, TRUE); + cbreak(); + noecho(); + + if (has_colors()) { + start_color(); + init_pair(COLOR_CONTROL_SELECTED, COLOR_BLUE, COLOR_BLACK); + init_pair(COLOR_LEVELS, COLOR_GREEN, COLOR_BLACK); + init_pair(COLOR_SET_SELECTED, COLOR_BLACK, COLOR_GREEN); + init_pair(COLOR_ENUM_ON, COLOR_WHITE, COLOR_RED); + init_pair(COLOR_ENUM_OFF, COLOR_WHITE, COLOR_BLUE); + init_pair(COLOR_ENUM_MISC, COLOR_BLACK, COLOR_YELLOW); + } + + if (mixer_device != NULL) { + open_device(aio, mixer_device); + } else { + for (i = 0; i < 16; ++i) { + (void)snprintf(mixer_path, sizeof(mixer_path), + "/dev/mixer%d", i); + fd = open(mixer_path, O_RDWR); + if (fd == -1) + break; + close(fd); + mixer_count++; + } + + if (mixer_count > 1) { + process_device_select(aio, mixer_count); + } else { + open_device(aio, _PATH_MIXER); + } + } + + endwin(); + close(aio->fd); + free(aio); + + return 0; +} Index: src/usr.bin/aiomixer/parse.c diff -u /dev/null src/usr.bin/aiomixer/parse.c:1.1 --- /dev/null Fri May 7 16:29:24 2021 +++ src/usr.bin/aiomixer/parse.c Fri May 7 16:29:24 2021 @@ -0,0 +1,115 @@ +/* $NetBSD: parse.c,v 1.1 2021/05/07 16:29:24 nia Exp $ */ +/*- + * Copyright (c) 2021 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Nia Alarie. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +#include <sys/audioio.h> +#include <sys/ioctl.h> +#include <curses.h> +#include <stdlib.h> +#include "app.h" +#include "parse.h" + +static struct aiomixer_class *get_class(struct aiomixer *, int); +static int compare_control(const void *, const void *); + +static struct aiomixer_class * +get_class(struct aiomixer *aio, int class) +{ + size_t i; + + for (i = 0; i < aio->numclasses; ++i) { + if (aio->classes[i].index == class) { + return &aio->classes[i]; + } + } + return NULL; +} + +static int +compare_control(const void *pa, const void *pb) +{ + const struct aiomixer_control *a = (const struct aiomixer_control *)pa; + const struct aiomixer_control *b = (const struct aiomixer_control *)pb; + + if (a->info.prev != AUDIO_MIXER_LAST && + b->info.prev != AUDIO_MIXER_LAST) { + if (b->info.prev == a->info.index) + return -1; + if (a->info.prev == b->info.index) + return 1; + } + return strcmp(a->info.label.name, b->info.label.name); +} + +int +aiomixer_parse(struct aiomixer *aio) +{ + size_t i; + struct mixer_devinfo info; + struct aiomixer_class *class; + struct aiomixer_control *control; + + for (info.index = 0; + ioctl(aio->fd, AUDIO_MIXER_DEVINFO, &info) != -1; ++info.index) { + if (info.type != AUDIO_MIXER_CLASS) + continue; + if (aio->numclasses >= __arraycount(aio->classes)) + break; + class = &aio->classes[aio->numclasses++]; + memcpy(class->name, info.label.name, MAX_AUDIO_DEV_LEN); + class->index = info.index; + class->numcontrols = 0; + } + for (info.index = 0; + ioctl(aio->fd, AUDIO_MIXER_DEVINFO, &info) != -1; ++info.index) { + if (info.type == AUDIO_MIXER_CLASS) + continue; + if (info.type == AUDIO_MIXER_VALUE) { + /* XXX workaround for hdaudio(4) bugs */ + if (info.un.v.delta > AUDIO_MAX_GAIN) + continue; + if (info.un.v.num_channels == 0) + continue; + } + class = get_class(aio, info.mixer_class); + if (class == NULL) + break; + if (class->numcontrols >= __arraycount(class->controls)) + break; + control = &class->controls[class->numcontrols++]; + control->info = info; + } + for (i = 0; i < aio->numclasses; ++i) { + class = &aio->classes[i]; + qsort(class->controls, class->numcontrols, + sizeof(struct aiomixer_control), + compare_control); + } + return 0; +} + Index: src/usr.bin/aiomixer/parse.h diff -u /dev/null src/usr.bin/aiomixer/parse.h:1.1 --- /dev/null Fri May 7 16:29:24 2021 +++ src/usr.bin/aiomixer/parse.h Fri May 7 16:29:24 2021 @@ -0,0 +1,37 @@ +/* $NetBSD: parse.h,v 1.1 2021/05/07 16:29:24 nia Exp $ */ +/*- + * Copyright (c) 2021 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Nia Alarie. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +#ifndef PARSE_H +#define PARSE_H + +#include "app.h" + +int aiomixer_parse(struct aiomixer *); + +#endif