├── Makefile ├── README.md ├── aiomixer.1 ├── aiomixer.c └── aiomixer.png /Makefile: -------------------------------------------------------------------------------- 1 | NCURSES6_CFLAGS!= ncurses6-config --cflags 2 | CDK5_PREFIX!= cdk5-config --prefix 3 | 4 | CFLAGS+= ${NCURSES6_CFLAGS} 5 | CFLAGS+= -I${CDK5_PREFIX}/include 6 | 7 | CFLAGS+= -Wall -Wextra -Wpedantic -std=c11 8 | 9 | .if DEBUG 10 | CFLAGS+= -Og -g 11 | CFLAGS+= -fsanitize=address -fsanitize=undefined -fsanitize=leak 12 | LDFLAGS+= -fsanitize=address -fsanitize=undefined -fsanitize=leak 13 | .endif 14 | 15 | CDK5_LIBS!= cdk5-config --libs 16 | NCURSES6_LIBS!= ncurses6-config --libs 17 | 18 | LIBS+= ${CDK5_LIBS} ${NCURSES6_LIBS} 19 | 20 | all: aiomixer 21 | 22 | aiomixer: aiomixer.o 23 | $(CC) $(LDFLAGS) aiomixer.o $(LIBS) -o aiomixer 24 | 25 | aiomixer.o: 26 | $(CC) $(CFLAGS) -c aiomixer.c -o aiomixer.o 27 | 28 | clean: 29 | rm -f *.o aiomixer 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | aiomixer 2 | ======== 3 | 4 | Mixer/volume control for NetBSD audio that runs in your terminal. 5 | 6 | Screenshot 7 | ---------- 8 | 9 | ![aiomixer running in xterm](aiomixer.png) 10 | 11 | Requirements 12 | ------------ 13 | 14 | * `devel/cdk` - used for rendering e.g. slider controls 15 | 16 | Questions 17 | --------- 18 | 19 | **Q: Could it be made to run on Solaris?** 20 | 21 | I don't think so. While NetBSD's audio API is based on that of Solaris, they've 22 | evolved separately, and I believe the mixer API is incompatible. 23 | 24 | **Q: What's dacsel?** 25 | 26 | It allows you to select the primary port. For example, on a laptop, it can 27 | control whether audio is exclusively output to speakers, headphones, or both. 28 | 29 | **Q: How do I control USB audio volume, or a secondary sound card?** 30 | 31 | Your device is likely available as a secondary mixer device - try 32 | `aiomixer -d /dev/mixer1` 33 | -------------------------------------------------------------------------------- /aiomixer.1: -------------------------------------------------------------------------------- 1 | .Dd April 4, 2019 2 | .Dt AIOMIXER 1 3 | .Os 4 | .Sh NAME 5 | .Nm aiomixer 6 | .Nd mixer for 7 | .Nx 8 | audio 9 | .Sh SYNOPSIS 10 | .Nm aiomixer 11 | .Op Fl d Ar device 12 | .Sh DESCRIPTION 13 | .Nm 14 | is a frontend for 15 | .Nx 16 | mixer devices that runs in your terminal. 17 | .Pp 18 | .Nm 19 | allows controlling audio levels, whether a particular input or 20 | output is muted, and the current DAC. 21 | .Pp 22 | The default mixer device is 23 | .Pa /dev/mixer . 24 | The 25 | .Fl d 26 | flag can be used to specify an alternative mixer device. 27 | .Sh USAGE 28 | .Nm 29 | is primarily controlled using the cursor keys, e.g. to select a 30 | control, or change a control's value. 31 | .Pp 32 | When the cursor is in the control pane, the Escape key will return 33 | the cursor to the class selection pane. 34 | When the cursor is in the class selection pane, pressing the Escape 35 | key will exit 36 | .Nm . 37 | .Pp 38 | By default, volume levels for individual channels cannot be changed 39 | separately. 40 | The channels can be unlocked and re-locked using the U key. 41 | .Sh SEE ALSO 42 | .Xr mixerctl 1 , 43 | .Xr audio 4 44 | -------------------------------------------------------------------------------- /aiomixer.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2019 Nia Alarie 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 17 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 18 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 19 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 25 | POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | #include 34 | #include 35 | 36 | #include 37 | 38 | #include 39 | 40 | #define DEFAULT_MIXER_DEVICE "/dev/mixer" 41 | 42 | #define MAX_CONTROLS (64) 43 | #define MAX_CLASSES (16) 44 | 45 | #define MAX_CONTROL_LEN (64) 46 | 47 | #define PAIR_CLASS_BUTTONS_HL (2) 48 | #define PAIR_SLIDER (3) 49 | #define PAIR_ENUM_SET (4) 50 | 51 | struct aiomixer_control { 52 | char name[MAX_CONTROL_LEN]; 53 | int dev; 54 | int type; 55 | int next, prev; 56 | int current_chan; /* for VALUE type */ 57 | bool chans_unlocked; /* for VALUE type */ 58 | union { 59 | struct audio_mixer_enum e; 60 | struct audio_mixer_set s; 61 | struct audio_mixer_value v; 62 | }; 63 | union { 64 | CDKBUTTONBOX *enum_widget; 65 | CDKBUTTONBOX *set_widget; 66 | CDKSLIDER *value_widget[8]; 67 | }; 68 | }; 69 | 70 | struct aiomixer_class { 71 | char name[MAX_AUDIO_DEV_LEN]; 72 | int id; 73 | CDKLABEL *heading_label; 74 | unsigned ncontrols; 75 | struct aiomixer_control controls[MAX_CONTROLS]; 76 | }; 77 | 78 | struct aiomixer { 79 | unsigned nclasses; 80 | struct aiomixer_class classes[MAX_CLASSES]; 81 | unsigned class_index; 82 | unsigned control_index; 83 | unsigned top_control; 84 | CDKSCREEN *screen; 85 | CDKLABEL *title_label; 86 | CDKBUTTONBOX *class_buttons; 87 | int fd; 88 | }; 89 | 90 | static void select_class(struct aiomixer *); 91 | static void select_class_widget(struct aiomixer *, int); 92 | static struct aiomixer_class *aiomixer_get_class(struct aiomixer *, int); 93 | static struct aiomixer_control *aiomixer_get_control(struct aiomixer *, int); 94 | static void aiomixer_devinfo(struct aiomixer *); 95 | static struct aiomixer_control *find_root_control(struct aiomixer *, int); 96 | static char **make_enum_list(struct audio_mixer_enum *); 97 | static char **make_set_list(struct audio_mixer_set *); 98 | static size_t sum_str_list_lengths(const char **, size_t); 99 | static bool control_within_bounds(struct aiomixer *, unsigned); 100 | static void reposition_visible_widgets(struct aiomixer *); 101 | static void create_class_widgets(struct aiomixer *, int); 102 | static void destroy_class_widgets(struct aiomixer *); 103 | static void enum_get_and_select(int, struct aiomixer_control *); 104 | static void set_get_and_select(int, struct aiomixer_control *); 105 | static void levels_get_and_set(int, struct aiomixer_control *); 106 | static void set_enum(int, int, int); 107 | static void set_set(int, int, int); 108 | static void set_level(int, struct aiomixer_control *, int, int); 109 | static int key_callback_slider(EObjectType, void *, void *, chtype); 110 | static int key_callback_class_buttons(EObjectType, void *, void *, chtype); 111 | static int key_callback_control_buttons(EObjectType, void *, void *, chtype); 112 | static int key_callback_global(EObjectType, void *, void *, chtype); 113 | static void add_directional_binds(struct aiomixer *, EObjectType, void *, BINDFN); 114 | static void add_slider_binds(struct aiomixer *, void *); 115 | static void add_class_button_binds(struct aiomixer *, void *); 116 | static void add_control_button_binds(struct aiomixer *, void *); 117 | static void add_global_binds(struct aiomixer *, EObjectType, void *); 118 | static void usage(void); 119 | static void quit(struct aiomixer *); 120 | static void quit_err(struct aiomixer *, const char *, ...); 121 | static void quit_perror(struct aiomixer *); 122 | 123 | static struct aiomixer_class * 124 | aiomixer_get_class(struct aiomixer *x, int class_id) 125 | { 126 | for (unsigned i = 0; i < x->nclasses; ++i) { 127 | if (x->classes[i].id == class_id) { 128 | return &x->classes[i]; 129 | } 130 | } 131 | return NULL; 132 | } 133 | 134 | static struct aiomixer_control * 135 | aiomixer_get_control(struct aiomixer *x, int dev) 136 | { 137 | unsigned i, j; 138 | struct aiomixer_class *class; 139 | 140 | for (i = 0; i < x->nclasses; ++i) { 141 | class = &x->classes[i]; 142 | for (j = 0; j < class->ncontrols; ++j) { 143 | if (class->controls[j].dev == dev) { 144 | return &class->controls[j]; 145 | } 146 | } 147 | } 148 | return NULL; 149 | } 150 | 151 | static struct aiomixer_control * 152 | find_root_control(struct aiomixer *x, int dev) 153 | { 154 | struct aiomixer_control *ctrl; 155 | 156 | ctrl = aiomixer_get_control(x, dev); 157 | while (ctrl->prev != -1) { 158 | ctrl = aiomixer_get_control(x, ctrl->prev); 159 | } 160 | return ctrl; 161 | } 162 | 163 | static void 164 | aiomixer_devinfo(struct aiomixer *x) 165 | { 166 | struct mixer_devinfo m = {0}; 167 | struct aiomixer_class *class = NULL; 168 | struct aiomixer_control *control = NULL; 169 | struct aiomixer_control *prev_ctrl = NULL; 170 | struct audio_mixer_enum e; 171 | struct audio_mixer_set s; 172 | struct audio_mixer_value v; 173 | int i; 174 | 175 | for (m.index = 0; ioctl(x->fd, AUDIO_MIXER_DEVINFO, &m) != -1; ++m.index) { 176 | if (m.type == AUDIO_MIXER_CLASS && x->nclasses < MAX_CLASSES) { 177 | class = &x->classes[x->nclasses++]; 178 | class->id = m.mixer_class; 179 | memcpy(class->name, m.label.name, MAX_AUDIO_DEV_LEN); 180 | } 181 | } 182 | for (m.index = 0; ioctl(x->fd, AUDIO_MIXER_DEVINFO, &m) != -1; ++m.index) { 183 | switch (m.type) { 184 | case AUDIO_MIXER_ENUM: 185 | e = m.un.e; 186 | class = aiomixer_get_class(x, m.mixer_class); 187 | if (class != NULL && class->ncontrols < MAX_CONTROLS) { 188 | control = &class->controls[class->ncontrols++]; 189 | if (m.prev != -1) { 190 | prev_ctrl = find_root_control(x, m.prev); 191 | if (prev_ctrl != NULL) { 192 | snprintf(control->name, sizeof(control->name), 193 | "%.16s.%16s\n", 194 | prev_ctrl->name, m.label.name); 195 | } 196 | } else { 197 | memcpy(control->name, m.label.name, MAX_AUDIO_DEV_LEN); 198 | } 199 | control->type = AUDIO_MIXER_ENUM; 200 | control->dev = m.index; 201 | control->next = m.next; 202 | control->prev = m.prev; 203 | control->e.num_mem = e.num_mem; 204 | for (i = 0; i < e.num_mem; ++i) { 205 | control->e.member[i].label = e.member[i].label; 206 | control->e.member[i].ord = e.member[i].ord; 207 | } 208 | } 209 | break; 210 | case AUDIO_MIXER_SET: 211 | s = m.un.s; 212 | class = aiomixer_get_class(x, m.mixer_class); 213 | if (class != NULL && class->ncontrols < MAX_CONTROLS) { 214 | control = &class->controls[class->ncontrols++]; 215 | if (m.prev != -1) { 216 | prev_ctrl = find_root_control(x, m.prev); 217 | if (prev_ctrl != NULL) { 218 | snprintf(control->name, sizeof(control->name), 219 | "%.16s.%.16s\n", 220 | prev_ctrl->name, m.label.name); 221 | } 222 | } else { 223 | memcpy(control->name, m.label.name, MAX_AUDIO_DEV_LEN); 224 | } 225 | control->type = AUDIO_MIXER_SET; 226 | control->dev = m.index; 227 | control->next = m.next; 228 | control->prev = m.prev; 229 | control->s.num_mem = s.num_mem; 230 | for (i = 0; i < s.num_mem; ++i) { 231 | control->s.member[i].label = s.member[i].label; 232 | control->s.member[i].mask = s.member[i].mask; 233 | } 234 | } 235 | break; 236 | case AUDIO_MIXER_VALUE: 237 | v = m.un.v; 238 | class = aiomixer_get_class(x, m.mixer_class); 239 | if (class != NULL && class->ncontrols < MAX_CONTROLS) { 240 | control = &class->controls[class->ncontrols++]; 241 | if (m.prev != -1) { 242 | prev_ctrl = find_root_control(x, m.prev); 243 | if (prev_ctrl != NULL) { 244 | snprintf(control->name, sizeof(control->name), 245 | "%.16s.%16s\n", 246 | prev_ctrl->name, m.label.name); 247 | } 248 | } else { 249 | memcpy(control->name, m.label.name, MAX_AUDIO_DEV_LEN); 250 | } 251 | control->type = AUDIO_MIXER_VALUE; 252 | control->dev = m.index; 253 | control->next = m.next; 254 | control->prev = m.prev; 255 | control->v.num_channels = v.num_channels; 256 | control->v.delta = v.delta ? v.delta : 8; 257 | } 258 | break; 259 | } 260 | } 261 | } 262 | 263 | static char ** 264 | make_enum_list(struct audio_mixer_enum *e) 265 | { 266 | char **list = calloc(e->num_mem, sizeof(char *)); 267 | if (list == NULL) return NULL; 268 | for (int i = 0; i < e->num_mem; ++i) { 269 | list[i] = e->member[i].label.name; 270 | } 271 | return list; 272 | } 273 | 274 | static char ** 275 | make_set_list(struct audio_mixer_set *s) 276 | { 277 | char **list = calloc(s->num_mem, sizeof(char *)); 278 | if (list == NULL) return NULL; 279 | for (int i = 0; i < s->num_mem; ++i) { 280 | list[i] = s->member[i].label.name; 281 | } 282 | return list; 283 | } 284 | 285 | static size_t 286 | sum_str_list_lengths(const char **list, size_t n) 287 | { 288 | size_t i, total = 0; 289 | 290 | for (i = 0; i < n; ++i) { 291 | total += strlen(list[i]); 292 | } 293 | return total; 294 | } 295 | 296 | static void 297 | enum_get_and_select(int fd, struct aiomixer_control *control) 298 | { 299 | mixer_ctrl_t dev = {0}; 300 | 301 | dev.dev = control->dev; 302 | dev.type = AUDIO_MIXER_ENUM; 303 | 304 | if (ioctl(fd, AUDIO_MIXER_READ, &dev) < 0) { 305 | fprintf(stderr, "aiomixer: AUDIO_MIXER_READ %d failed: %s\n", 306 | dev.dev, strerror(errno)); 307 | return; 308 | } 309 | 310 | for (int i = 0; i < control->e.num_mem; ++i) { 311 | if (control->e.member[i].ord == dev.un.ord) { 312 | setCDKButtonboxCurrentButton(control->enum_widget, i); 313 | break; 314 | } 315 | } 316 | } 317 | 318 | static void 319 | set_get_and_select(int fd, struct aiomixer_control *control) 320 | { 321 | mixer_ctrl_t dev = {0}; 322 | 323 | dev.dev = control->dev; 324 | dev.type = AUDIO_MIXER_SET; 325 | 326 | if (ioctl(fd, AUDIO_MIXER_READ, &dev) < 0) { 327 | fprintf(stderr, 328 | "aiomixer: AUDIO_MIXER_READ %d failed: %s\n", 329 | dev.dev, strerror(errno)); 330 | return; 331 | } 332 | 333 | for (int i = 0; i < control->s.num_mem; ++i) { 334 | if (control->s.member[i].mask == dev.un.mask) { 335 | setCDKButtonboxCurrentButton(control->set_widget, i); 336 | break; 337 | } 338 | } 339 | } 340 | 341 | static void 342 | levels_get_and_set(int fd, struct aiomixer_control *control) 343 | { 344 | mixer_ctrl_t dev = {0}; 345 | 346 | dev.dev = control->dev; 347 | dev.type = AUDIO_MIXER_VALUE; 348 | dev.un.value.num_channels = control->v.num_channels; 349 | 350 | if (ioctl(fd, AUDIO_MIXER_READ, &dev) < 0) { 351 | fprintf(stderr, "aiomixer: AUDIO_MIXER_READ %d failed: %s\n", 352 | dev.dev, strerror(errno)); 353 | return; 354 | } 355 | for (int chan = 0; chan < control->v.num_channels; ++chan) { 356 | setCDKSliderValue(control->value_widget[chan], 357 | dev.un.value.level[chan]); 358 | } 359 | } 360 | 361 | static void 362 | set_enum(int fd, int dev_id, int ord) 363 | { 364 | mixer_ctrl_t dev = {0}; 365 | 366 | dev.dev = dev_id; 367 | dev.type = AUDIO_MIXER_ENUM; 368 | dev.un.ord = ord; 369 | 370 | if (ioctl(fd, AUDIO_MIXER_WRITE, &dev) < 0) { 371 | fprintf(stderr, "aiomixer: AUDIO_MIXER_WRITE %d failed: %s\n", 372 | dev.dev, strerror(errno)); 373 | } 374 | } 375 | 376 | static void 377 | set_set(int fd, int dev_id, int mask) 378 | { 379 | mixer_ctrl_t dev = {0}; 380 | 381 | dev.dev = dev_id; 382 | dev.type = AUDIO_MIXER_SET; 383 | dev.un.mask = mask; 384 | 385 | if (ioctl(fd, AUDIO_MIXER_WRITE, &dev) < 0) { 386 | fprintf(stderr, "aiomixer: AUDIO_MIXER_WRITE %d failed: %s\n", 387 | dev.dev, strerror(errno)); 388 | } 389 | } 390 | 391 | static void 392 | add_global_binds(struct aiomixer *x, EObjectType type, void *object) 393 | { 394 | unsigned int i; 395 | 396 | for (i = 1; i <= x->nclasses; ++i) { 397 | bindCDKObject(type, object, KEY_F0 + i, key_callback_global, x); 398 | } 399 | bindCDKObject(type, object, KEY_RESIZE, key_callback_global, x); 400 | } 401 | 402 | static void 403 | add_directional_binds(struct aiomixer *x, EObjectType type, void *object, BINDFN fn) 404 | { 405 | bindCDKObject(type, object, KEY_UP, fn, x); 406 | bindCDKObject(type, object, KEY_DOWN, fn, x); 407 | bindCDKObject(type, object, KEY_LEFT, fn, x); 408 | bindCDKObject(type, object, KEY_RIGHT, fn, x); 409 | bindCDKObject(type, object, 'h', fn, x); 410 | bindCDKObject(type, object, 'j', fn, x); 411 | bindCDKObject(type, object, 'k', fn, x); 412 | bindCDKObject(type, object, 'l', fn, x); 413 | } 414 | 415 | static void 416 | add_slider_binds(struct aiomixer *x, void *object) 417 | { 418 | add_global_binds(x, vSLIDER, object); 419 | add_directional_binds(x, vSLIDER, object, key_callback_slider); 420 | bindCDKObject(vSLIDER, object, 'u', key_callback_slider, x); 421 | bindCDKObject(vSLIDER, object, 'm', key_callback_slider, x); 422 | } 423 | 424 | static void 425 | add_class_button_binds(struct aiomixer *x, void *object) 426 | { 427 | add_global_binds(x, vBUTTONBOX, object); 428 | add_directional_binds(x, vBUTTONBOX, object, key_callback_class_buttons); 429 | bindCDKObject(vBUTTONBOX, object, 0x1b /* \e */, key_callback_class_buttons, x); 430 | } 431 | 432 | static void 433 | add_control_button_binds(struct aiomixer *x, void *object) 434 | { 435 | add_global_binds(x, vBUTTONBOX, object); 436 | add_directional_binds(x, vBUTTONBOX, object, key_callback_control_buttons); 437 | } 438 | 439 | static void 440 | create_class_widgets(struct aiomixer *x, int y) 441 | { 442 | char label[48]; 443 | struct aiomixer_class *class = &x->classes[x->class_index]; 444 | struct aiomixer_control *control; 445 | char **list; 446 | unsigned i; 447 | int width; 448 | char *title[] = { "Controls" }; 449 | int max_y = getmaxy(x->screen->window) - y - 3; 450 | 451 | class->heading_label = newCDKLabel(x->screen, 0, y, title, 1, false, false); 452 | drawCDKLabel(class->heading_label, false); 453 | y += 2; 454 | 455 | for (i = 0; i < class->ncontrols; ++i) { 456 | control = &class->controls[i]; 457 | switch (control->type) { 458 | case AUDIO_MIXER_ENUM: 459 | if ((list = make_enum_list(&control->e)) != NULL) { 460 | snprintf(label, sizeof(label), "%s", control->name); 461 | width = sum_str_list_lengths((const char **)list, control->e.num_mem) 462 | + control->e.num_mem + 10; 463 | control->enum_widget = newCDKButtonbox(x->screen, 464 | 0, y, 5, width, 465 | label, 1, control->e.num_mem, 466 | list, control->e.num_mem, 467 | COLOR_PAIR(PAIR_ENUM_SET) | A_BOLD, false, false); 468 | if (control->enum_widget == NULL) { 469 | quit_err(x, "Couldn't create enum control"); 470 | } 471 | enum_get_and_select(x->fd, control); 472 | add_control_button_binds(x, control->enum_widget); 473 | if (y < max_y) { 474 | drawCDKButtonbox(control->enum_widget, false); 475 | } 476 | } else { 477 | quit_perror(x); 478 | } 479 | free(list); 480 | y += 3; 481 | break; 482 | case AUDIO_MIXER_SET: 483 | if ((list = make_set_list(&control->s)) != NULL) { 484 | snprintf(label, sizeof(label), "%s", control->name); 485 | width = sum_str_list_lengths((const char **)list, control->s.num_mem) 486 | + control->s.num_mem + 10; 487 | control->set_widget = newCDKButtonbox(x->screen, 488 | 0, y, 5, width, 489 | label, 1, control->s.num_mem, 490 | list, control->s.num_mem, 491 | COLOR_PAIR(PAIR_ENUM_SET) | A_BOLD, false, false); 492 | if (control->set_widget == NULL) { 493 | quit_err(x, "Couldn't create set control"); 494 | } 495 | set_get_and_select(x->fd, control); 496 | add_control_button_binds(x, control->set_widget); 497 | if (y < max_y) { 498 | drawCDKButtonbox(control->set_widget, false); 499 | } 500 | } else { 501 | quit_perror(x); 502 | } 503 | free(list); 504 | y += 3; 505 | break; 506 | case AUDIO_MIXER_VALUE: 507 | for (int chan = 0; chan < control->v.num_channels; ++chan) { 508 | snprintf(label, sizeof(label), "%s (channel %d)", 509 | control->name, chan); 510 | control->value_widget[chan] = newCDKSlider(x->screen, 0, y, 511 | label, "% ", '#' | COLOR_PAIR(PAIR_SLIDER) | A_BOLD, 512 | 0, 50, 0, 255, 513 | control->v.delta, control->v.delta * 2, 514 | false, false); 515 | if (control->value_widget[chan] == NULL) { 516 | quit_err(x, "Couldn't create slider"); 517 | } 518 | add_slider_binds(x, control->value_widget[chan]); 519 | y += 3; 520 | } 521 | y -= 3 * control->v.num_channels; 522 | levels_get_and_set(x->fd, control); 523 | for (int chan = 0; chan < control->v.num_channels; ++chan) { 524 | if (y < max_y) { 525 | drawCDKSlider(control->value_widget[chan], false); 526 | } 527 | y += 3; 528 | } 529 | break; 530 | } 531 | } 532 | } 533 | 534 | static void 535 | destroy_class_widgets(struct aiomixer *x) 536 | { 537 | struct aiomixer_class *class = &x->classes[x->class_index]; 538 | struct aiomixer_control *control; 539 | 540 | destroyCDKLabel(class->heading_label); 541 | class->heading_label = NULL; 542 | 543 | for (unsigned i = 0; i < class->ncontrols; ++i) { 544 | control = &class->controls[i]; 545 | switch (control->type) { 546 | case AUDIO_MIXER_ENUM: 547 | destroyCDKButtonbox(control->enum_widget); 548 | control->enum_widget = NULL; 549 | break; 550 | case AUDIO_MIXER_SET: 551 | destroyCDKButtonbox(control->set_widget); 552 | control->set_widget = NULL; 553 | break; 554 | case AUDIO_MIXER_VALUE: 555 | for (int j = 0; j < control->v.num_channels; ++j) { 556 | destroyCDKSlider(control->value_widget[j]); 557 | control->value_widget[j] = NULL; 558 | } 559 | break; 560 | } 561 | } 562 | } 563 | 564 | static bool 565 | control_within_bounds(struct aiomixer *x, unsigned index) 566 | { 567 | struct aiomixer_class *class = &x->classes[x->class_index]; 568 | int max_y = getmaxy(x->screen->window) - 3; 569 | int y = 5; 570 | 571 | if (index < x->top_control) { 572 | return false; 573 | } 574 | 575 | for (unsigned i = x->top_control; i < class->ncontrols; ++i) { 576 | switch (class->controls[i].type) { 577 | case AUDIO_MIXER_ENUM: 578 | case AUDIO_MIXER_SET: 579 | y += 3; 580 | break; 581 | case AUDIO_MIXER_VALUE: 582 | y += (3 * class->controls[i].v.num_channels); 583 | break; 584 | } 585 | if (y >= max_y) return false; 586 | if (i == index) break; 587 | } 588 | return true; 589 | } 590 | 591 | static void 592 | reposition_visible_widgets(struct aiomixer *x) 593 | { 594 | struct aiomixer_control *control; 595 | struct aiomixer_class *class = &x->classes[x->class_index]; 596 | unsigned max_control = class->ncontrols; 597 | int y = 5; 598 | 599 | for (unsigned i = 0; i < class->ncontrols; ++i) { 600 | control = &class->controls[i]; 601 | switch (control->type) { 602 | case AUDIO_MIXER_ENUM: 603 | moveCDKButtonbox(control->enum_widget, INT_MAX, INT_MAX, false, false); 604 | eraseCDKButtonbox(control->enum_widget); 605 | break; 606 | case AUDIO_MIXER_SET: 607 | moveCDKButtonbox(control->set_widget, INT_MAX, INT_MAX, false, false); 608 | eraseCDKButtonbox(control->set_widget); 609 | break; 610 | case AUDIO_MIXER_VALUE: 611 | for (int j = 0; j < control->v.num_channels; ++j) { 612 | moveCDKSlider(control->value_widget[j], INT_MAX, INT_MAX, false, false); 613 | eraseCDKSlider(control->value_widget[j]); 614 | } 615 | break; 616 | } 617 | } 618 | for (unsigned i = x->top_control; i < class->ncontrols; ++i) { 619 | control = &class->controls[i]; 620 | if (!control_within_bounds(x, i)) { 621 | max_control = i; 622 | break; 623 | } 624 | switch (control->type) { 625 | case AUDIO_MIXER_ENUM: 626 | moveCDKButtonbox(control->enum_widget, 0, y, false, false); 627 | y += 3; 628 | break; 629 | case AUDIO_MIXER_SET: 630 | moveCDKButtonbox(control->set_widget, 0, y, false, false); 631 | y += 3; 632 | break; 633 | case AUDIO_MIXER_VALUE: 634 | for (int j = 0; j < control->v.num_channels; ++j) { 635 | moveCDKSlider(control->value_widget[j], 0, y, false, false); 636 | y += 3; 637 | } 638 | break; 639 | } 640 | } 641 | for (unsigned i = x->top_control; i < max_control; ++i) { 642 | control = &class->controls[i]; 643 | switch (control->type) { 644 | case AUDIO_MIXER_ENUM: 645 | drawCDKButtonbox(control->enum_widget, false); 646 | break; 647 | case AUDIO_MIXER_SET: 648 | drawCDKButtonbox(control->set_widget, false); 649 | break; 650 | case AUDIO_MIXER_VALUE: 651 | for (int j = 0; j < control->v.num_channels; ++j) { 652 | drawCDKSlider(control->value_widget[j], false); 653 | } 654 | break; 655 | } 656 | } 657 | /* XXX moving any widgets seems to mess up things */ 658 | drawCDKLabel(x->title_label, false); 659 | drawCDKLabel(class->heading_label, false); 660 | drawCDKButtonbox(x->class_buttons, false); 661 | } 662 | 663 | static void 664 | select_class_widget(struct aiomixer *x, int index) 665 | { 666 | struct aiomixer_class *class = &x->classes[x->class_index]; 667 | struct aiomixer_control *control; 668 | bool reposition = false; 669 | int result; 670 | 671 | if (index < 0 || class->ncontrols < 1) { 672 | select_class(x); 673 | return; 674 | } 675 | if ((unsigned)index >= class->ncontrols) { 676 | select_class_widget(x, 0); 677 | return; 678 | } 679 | control = &class->controls[index]; 680 | x->control_index = index; 681 | if (x->top_control > x->control_index) { 682 | x->top_control = index; 683 | reposition = true; 684 | } 685 | if (x->top_control < x->control_index) { 686 | while (!control_within_bounds(x, x->control_index)) { 687 | x->top_control += 1; 688 | } 689 | reposition = true; 690 | } 691 | if (reposition) { 692 | reposition_visible_widgets(x); 693 | } 694 | switch (control->type) { 695 | case AUDIO_MIXER_ENUM: 696 | enum_get_and_select(x->fd, control); 697 | result = activateCDKButtonbox(control->enum_widget, false); 698 | if (result == -1) { 699 | select_class(x); 700 | } else { 701 | select_class_widget(x, index + 1); 702 | } 703 | break; 704 | case AUDIO_MIXER_SET: 705 | set_get_and_select(x->fd, control); 706 | result = activateCDKButtonbox(control->set_widget, false); 707 | if (result == -1) { 708 | select_class(x); 709 | } else { 710 | select_class_widget(x, index + 1); 711 | } 712 | break; 713 | case AUDIO_MIXER_VALUE: 714 | levels_get_and_set(x->fd, control); 715 | result = activateCDKSlider(control->value_widget[control->current_chan], false); 716 | if (result == -1) { 717 | select_class(x); 718 | } else { 719 | if (control->current_chan < (control->v.num_channels - 1)) { 720 | control->current_chan++; 721 | select_class_widget(x, index); 722 | } else { 723 | control->current_chan = 0; 724 | select_class_widget(x, index + 1); 725 | } 726 | } 727 | break; 728 | } 729 | } 730 | 731 | static void 732 | select_class(struct aiomixer *x) 733 | { 734 | int result; 735 | 736 | result = activateCDKButtonbox(x->class_buttons, false); 737 | destroy_class_widgets(x); 738 | if (result != -1) { 739 | if ((unsigned)result != x->class_index) { 740 | x->control_index = 0; 741 | } 742 | x->class_index = (unsigned)result; 743 | } 744 | create_class_widgets(x, 3); 745 | select_class_widget(x, 0); 746 | } 747 | 748 | static void 749 | set_level(int fd, struct aiomixer_control *control, int level, int channel) 750 | { 751 | mixer_ctrl_t dev = {0}; 752 | int i; 753 | 754 | dev.dev = control->dev; 755 | dev.type = AUDIO_MIXER_VALUE; 756 | dev.un.value.num_channels = control->v.num_channels; 757 | 758 | if (!control->chans_unlocked) { 759 | for (i = 0; i < control->v.num_channels; ++i) { 760 | dev.un.value.level[i] = level; 761 | setCDKSliderValue(control->value_widget[i], level); 762 | drawCDKSlider(control->value_widget[i], false); 763 | } 764 | } else { 765 | if (ioctl(fd, AUDIO_MIXER_READ, &dev) < 0) { 766 | fprintf(stderr, "aiomixer: AUDIO_MIXER_READ %d failed: %s\n", 767 | dev.dev, strerror(errno)); 768 | return; 769 | } 770 | dev.un.value.level[channel] = level; 771 | setCDKSliderValue(control->value_widget[channel], level); 772 | drawCDKSlider(control->value_widget[channel], false); 773 | } 774 | 775 | if (ioctl(fd, AUDIO_MIXER_WRITE, &dev) < 0) { 776 | fprintf(stderr, "aiomixer: AUDIO_MIXER_WRITE %d failed: %s\n", 777 | dev.dev, strerror(errno)); 778 | return; 779 | } 780 | } 781 | 782 | static int key_callback_slider(EObjectType cdktype , 783 | void *object, void *clientData, chtype key) 784 | { 785 | struct aiomixer *x = clientData; 786 | struct aiomixer_class *class = &x->classes[x->class_index]; 787 | struct aiomixer_control *control = &class->controls[x->control_index]; 788 | CDKSLIDER *widget = object; 789 | int new_value; 790 | 791 | (void)cdktype; /* unused */ 792 | switch (key) { 793 | case 'k': 794 | case KEY_UP: 795 | if (control->current_chan > 0) { 796 | control->current_chan--; 797 | select_class_widget(x, x->control_index); 798 | } else { 799 | control->current_chan = 0; 800 | select_class_widget(x, x->control_index - 1); 801 | } 802 | break; 803 | case 'j': 804 | case KEY_DOWN: 805 | if (control->current_chan < (control->v.num_channels - 1)) { 806 | control->current_chan++; 807 | select_class_widget(x, x->control_index); 808 | } else { 809 | control->current_chan = 0; 810 | select_class_widget(x, x->control_index + 1); 811 | } 812 | break; 813 | case 'h': 814 | case KEY_LEFT: 815 | new_value = getCDKSliderValue(widget) - control->v.delta; 816 | if (new_value < getCDKSliderLowValue(widget)) { 817 | new_value = getCDKSliderLowValue(widget); 818 | } 819 | set_level(x->fd, control, new_value, control->current_chan); 820 | break; 821 | case 'l': 822 | case KEY_RIGHT: 823 | new_value = getCDKSliderValue(widget) + control->v.delta; 824 | if (new_value > getCDKSliderHighValue(widget)) { 825 | new_value = getCDKSliderHighValue(widget); 826 | } 827 | set_level(x->fd, control, new_value, control->current_chan); 828 | break; 829 | case 'u': 830 | control->chans_unlocked = !control->chans_unlocked; 831 | break; 832 | case 'm': 833 | /* TODO: mute/unmute thing */ 834 | break; 835 | } 836 | return false; 837 | } 838 | 839 | static int key_callback_class_buttons(EObjectType cdktype, 840 | void *object, void *clientData, chtype key) 841 | { 842 | struct aiomixer *x = clientData; 843 | 844 | (void)cdktype; /* unused */ 845 | (void)object; /* unused */ 846 | switch (key) { 847 | case 0x1b: /* escape */ 848 | quit(x); 849 | break; 850 | case 'k': 851 | case KEY_UP: 852 | return true; 853 | case 'j': 854 | case KEY_DOWN: 855 | select_class_widget(x, 0); 856 | break; 857 | case 'h': 858 | case KEY_LEFT: 859 | destroy_class_widgets(x); 860 | x->class_index = (getCDKButtonboxCurrentButton(x->class_buttons) - 1) % x->nclasses; 861 | create_class_widgets(x, 3); 862 | if (key != KEY_LEFT) { 863 | setCDKButtonboxCurrentButton(x->class_buttons, x->class_index); 864 | } 865 | break; 866 | case 'l': 867 | case KEY_RIGHT: 868 | destroy_class_widgets(x); 869 | x->class_index = (getCDKButtonboxCurrentButton(x->class_buttons) + 1) % x->nclasses; 870 | create_class_widgets(x, 3); 871 | if (key != KEY_RIGHT) { 872 | setCDKButtonboxCurrentButton(x->class_buttons, x->class_index); 873 | } 874 | break; 875 | } 876 | return false; 877 | } 878 | 879 | static int key_callback_control_buttons(EObjectType cdktype, 880 | void *object, void *clientData, chtype key) 881 | { 882 | struct aiomixer *x = clientData; 883 | struct aiomixer_class *class = &x->classes[x->class_index]; 884 | struct aiomixer_control *control = &class->controls[x->control_index]; 885 | CDKBUTTONBOX *widget = object; 886 | int current; 887 | 888 | (void)cdktype; /* unused */ 889 | current = getCDKButtonboxCurrentButton(widget); 890 | switch (key) { 891 | case 'k': 892 | case KEY_UP: 893 | select_class_widget(x, x->control_index - 1); 894 | break; 895 | case 'j': 896 | case KEY_DOWN: 897 | select_class_widget(x, x->control_index + 1); 898 | break; 899 | case 'h': 900 | case KEY_LEFT: 901 | current = (current - 1) % getCDKButtonboxButtonCount(widget); 902 | if (control->type == AUDIO_MIXER_SET) { 903 | set_set(x->fd, control->dev, control->s.member[current].mask); 904 | } else if (control->type == AUDIO_MIXER_ENUM) { 905 | set_enum(x->fd, control->dev, control->e.member[current].ord); 906 | } 907 | if (key != KEY_LEFT) { 908 | setCDKButtonboxCurrentButton(widget, current); 909 | } 910 | break; 911 | case 'l': 912 | case KEY_RIGHT: 913 | current = (current + 1) % getCDKButtonboxButtonCount(widget); 914 | if (control->type == AUDIO_MIXER_SET) { 915 | set_set(x->fd, control->dev, control->s.member[current].mask); 916 | } else if (control->type == AUDIO_MIXER_ENUM) { 917 | set_enum(x->fd, control->dev, control->e.member[current].ord); 918 | } 919 | if (key != KEY_RIGHT) { 920 | setCDKButtonboxCurrentButton(widget, current); 921 | } 922 | break; 923 | } 924 | return false; 925 | } 926 | 927 | static int key_callback_global(EObjectType cdktype, 928 | void *object, void *clientData, chtype key) 929 | { 930 | struct aiomixer *x = clientData; 931 | 932 | (void)cdktype; /* unused */ 933 | (void)object; /* unused */ 934 | if (key > KEY_F0 && key <= (KEY_F0 + x->nclasses + 1)) { 935 | key = key - KEY_F0 - 1; 936 | destroy_class_widgets(x); 937 | setCDKButtonboxCurrentButton(x->class_buttons, key); 938 | drawCDKButtonboxButtons(x->class_buttons); 939 | x->class_index = key; 940 | create_class_widgets(x, 3); 941 | select_class_widget(x, 0); 942 | return false; 943 | } 944 | switch (key) { 945 | case KEY_RESIZE: 946 | destroy_class_widgets(x); 947 | moveCDKLabel(x->title_label, RIGHT, 0, false, true); 948 | drawCDKButtonbox(x->class_buttons, false); 949 | create_class_widgets(x, 3); 950 | select_class_widget(x, 0); 951 | break; 952 | } 953 | return false; 954 | } 955 | 956 | static void 957 | usage(void) 958 | { 959 | fputs("aiomixer [-d device]\n", stderr); 960 | exit(1); 961 | } 962 | 963 | static void 964 | quit_err(struct aiomixer *x, const char *fmt, ...) 965 | { 966 | va_list args; 967 | 968 | va_start(args, fmt); 969 | vfprintf(stderr, fmt, args); 970 | va_end(args); 971 | destroyCDKScreen(x->screen); 972 | endCDK(); 973 | close(x->fd); 974 | exit(1); 975 | } 976 | 977 | static void 978 | quit_perror(struct aiomixer *x) 979 | { 980 | perror("aiomixer"); 981 | destroyCDKScreen(x->screen); 982 | endCDK(); 983 | close(x->fd); 984 | exit(1); 985 | } 986 | 987 | static void 988 | quit(struct aiomixer *x) 989 | { 990 | destroyCDKScreen(x->screen); 991 | endCDK(); 992 | close(x->fd); 993 | exit(0); 994 | } 995 | 996 | int 997 | main(int argc, char *argv[]) 998 | { 999 | struct aiomixer x = {0}; 1000 | char *title[] = { "NetBSD Audio Mixer" }; 1001 | char **class_names; 1002 | char *mixer_device = DEFAULT_MIXER_DEVICE; 1003 | int ch; 1004 | extern char *optarg; 1005 | extern int optind; 1006 | 1007 | while ((ch = getopt(argc, argv, "d:")) != -1) { 1008 | switch (ch) { 1009 | case 'd': 1010 | mixer_device = optarg; 1011 | break; 1012 | default: 1013 | usage(); 1014 | break; 1015 | } 1016 | } 1017 | argc -= optind; 1018 | argv += optind; 1019 | 1020 | if ((x.fd = open(mixer_device, O_RDWR)) == -1) { 1021 | perror("open(mixer_device)"); 1022 | return 1; 1023 | } 1024 | 1025 | aiomixer_devinfo(&x); 1026 | 1027 | if ((class_names = calloc(sizeof(char *), x.nclasses)) == NULL) { 1028 | quit_perror(&x); 1029 | } 1030 | for (unsigned i = 0; i < x.nclasses; ++i) { 1031 | class_names[i] = x.classes[i].name; 1032 | } 1033 | 1034 | x.screen = initCDKScreen(NULL); 1035 | initCDKColor(); 1036 | 1037 | init_pair(PAIR_CLASS_BUTTONS_HL, COLOR_WHITE, COLOR_BLUE); 1038 | init_pair(PAIR_SLIDER, COLOR_GREEN, COLOR_BLACK); 1039 | init_pair(PAIR_ENUM_SET, COLOR_YELLOW, COLOR_BLACK); 1040 | 1041 | x.title_label = newCDKLabel(x.screen, RIGHT, 0, title, 1, false, false); 1042 | if (x.title_label == NULL) { 1043 | quit_err(&x, "Couldn't create title"); 1044 | } 1045 | 1046 | x.class_buttons = newCDKButtonbox(x.screen, 0, 0, 1047 | 2, sum_str_list_lengths((const char **)class_names, x.nclasses) + 10 + x.nclasses, 1048 | "Classes", 1, x.nclasses, 1049 | class_names, x.nclasses, 1050 | COLOR_PAIR(PAIR_CLASS_BUTTONS_HL), false, false); 1051 | if (x.class_buttons == NULL) { 1052 | quit_err(&x, "Couldn't create class buttons"); 1053 | } 1054 | free(class_names); 1055 | 1056 | drawCDKLabel(x.title_label, false); 1057 | drawCDKButtonbox(x.class_buttons, false); 1058 | 1059 | add_class_button_binds(&x, x.class_buttons); 1060 | 1061 | create_class_widgets(&x, 3); 1062 | select_class_widget(&x, 0); 1063 | 1064 | quit(&x); 1065 | return 0; /* never reached */ 1066 | } 1067 | 1068 | -------------------------------------------------------------------------------- /aiomixer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alarixnia/aiomixer/9007987af1b5a2538979d3b0ff6a48d8bf15a08f/aiomixer.png --------------------------------------------------------------------------------