├── .clangd ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── man └── pamix.1 ├── pamix.conf └── src ├── app.c ├── app.h ├── config.c ├── config.h ├── da.h ├── draw.c ├── draw.h └── main.c /.clangd: -------------------------------------------------------------------------------- 1 | CompileFlags: 2 | Add: [-xc, -std=c99, -Wall, -Wextra, -Werror, -pedantic, -D_DEFAULT_SOURCE, -D_XOPEN_SOURCE=600] 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .vscode 3 | .idea 4 | cmake-build-* 5 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8) 2 | project(pamix C) 3 | 4 | set(CMAKE_C_STANDARD 99) 5 | set(CMAKE_C_STANDARD_REQUIRED ON) 6 | set(CMAKE_C_EXTENSIONS ON) 7 | file(GLOB_RECURSE pamix_SRC 8 | "src/*.h" 9 | "src/*.c") 10 | 11 | include_directories("src") 12 | link_libraries("pulse" "pthread") 13 | 14 | find_package(PkgConfig REQUIRED QUIET) 15 | pkg_search_module(NCURSESW REQUIRED ncursesw) 16 | link_libraries(${NCURSESW_LIBRARIES}) 17 | add_definitions(${NCURSESW_CFLAGS} ${NCURSESW_CFLAGS_OTHER}) 18 | 19 | add_executable(pamix ${pamix_SRC}) 20 | install(FILES pamix.conf DESTINATION /etc/xdg) 21 | install(TARGETS pamix DESTINATION bin) 22 | install(FILES man/pamix.1 TYPE MAN) 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Joshua Jensch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PAMix - the pulseaudio terminal mixer 2 | 3 | ![alt tag](http://i.imgur.com/NuzrAXZ.gif) 4 | 5 | ## Dependencies: # 6 | 7 | ### Build ## 8 | * cmake 9 | * pkg-config 10 | 11 | ### Runtime ## 12 | * PulseAudio 13 | * Ncursesw 14 | 15 | ## Building and Installing 16 | ```bash 17 | mkdir build && cd build 18 | cmake .. -DCMAKE_BUILD_TYPE=RELEASE 19 | make 20 | sudo make install 21 | ``` 22 | 23 | # Configuration # 24 | PAmix keybindings are configured in `$XDG_CONFIG_HOME/pamix.conf` (see [**Configuration**](https://github.com/patroclos/PAmix/wiki/Configuration) for detailed instructions) 25 | 26 | # Default Keybindings # 27 | 28 | (arrow keys are also supported instead of hjkl) 29 | 30 | | Action | Key | 31 | |----------------------------|-----| 32 | | Playback tab | F1 | 33 | | Recording Tab | F2 | 34 | | Output Devices | F3 | 35 | | Input Devices | F4 | 36 | | Cards | F5 | 37 | | Set volume to percentage | 0-9 | 38 | | Decrease Volume | h | 39 | | Select Next | j | 40 | | Select Previous | k | 41 | | Increase Volume | l | 42 | | (Un)Lock Channels | c | 43 | | (Un)Mute | m | 44 | | Next/Previous device/port | s/S | 45 | | Quit | q | 46 | 47 | -------------------------------------------------------------------------------- /man/pamix.1: -------------------------------------------------------------------------------- 1 | .\" this is the manpage of the pamix pulseaudio ncurses mixer 2 | .TH pamix 1 "27 Nov 2024" "V" "pamix man page" 3 | 4 | .SH DESCRIPTION 5 | PAmix is a pavucontrol inspired ncurses based pulseaudio mixer 6 | 7 | .SH CONFIGURATION 8 | pamix is configured using a file called pamix.conf inside the $XDG_CONFIG_HOME or $HOME/.config, should it not be set. 9 | .br 10 | .start 11 | .SH COMMANDS 12 | .PP 13 | PAmix conf files support the following commands: 14 | .br 15 | * bind 16 | 17 | .SH bind 18 | .PP 19 | \fBSYNOPSIS:\fP bind KEYNAME MIXER\-COMMAND [ARGUMENT] 20 | 21 | .PP 22 | bind is used to bind a keyname to a mixer\-command. 23 | .br 24 | Some mixer\-commands require an argument. 25 | .br 26 | You can bind a keyname to multiple mixer\-commands. 27 | 28 | .SH PAMIX\-COMMANDS 29 | .PP 30 | Pamix\-Commands can be bound to keys using the bind command and are used to interact with pamix. 31 | .br 32 | The following pamix\-commands are currently supported: 33 | 34 | .SH quit 35 | .PP 36 | quit will cause PAmix to exit and takes no arguments. 37 | 38 | .SH select\-tab 39 | .PP 40 | select\-tab will select one of the following tabs: Output Devices, Input Devices, Playback, Recording, Cards 41 | .br 42 | select\-tab takes the number of the tab to switch to starting at 0 in the order mentioned. 43 | 44 | .SH select\-next and select\-prev 45 | .PP 46 | these commands are given the optional argument 'channel' they will select the next and previous channels. 47 | if no argument is given they will select the next and previous entry in the displayed tab. 48 | 49 | .SH set\-volume 50 | .PP 51 | this command takes the targetvalue in form of a double as an argument. 52 | .br 53 | depending on whether channels are locked, this command will set the volume of the selected entry/channel to the targetvalue given in the argument. 54 | .br 55 | \fIExample:\fP bind 0 set\-volume 1.0 \fI; this will set the volume to 100%\fP 56 | 57 | .SH add\-volume 58 | .PP 59 | this command takes a deltavalue in form of a double as an argument. 60 | .br 61 | the deltavalue can be negative 62 | \fIExample:\fP bind h add\-volume \-0.05 \fI; this will reduce the volume by 5%\fP 63 | 64 | .SH cycle\-next and cycle\-prev 65 | .PP 66 | these commands will change the device or port of the currently selected entry. 67 | .br 68 | they dont take any arguments. 69 | 70 | .SH toggle\-lock 71 | .PP 72 | this command toggles whether channels should be locked together for the currently selected entry 73 | .br 74 | and takes no arguments. 75 | 76 | .SH toggle\-mute 77 | .PP 78 | toggles whether the currently selected entry is muted 79 | .br 80 | and takes no arguments. 81 | 82 | .stop 83 | 84 | .SH DEFAULT CONFIGURATION 85 | 86 | Keybindings: 87 | .br 88 | F1 show Playback tab 89 | .br 90 | F2 show Recording tab 91 | .br 92 | F3 show Output devices tab 93 | .br 94 | F4 show Input devices tab 95 | .br 96 | F5 show Cards tab 97 | .br 98 | 0-9 set volume to percentage (10%-100%) 99 | .br 100 | j/down select next channel or entry 101 | .br 102 | k/up select previous channel or entry 103 | .br 104 | h/left decrease volume 105 | .br 106 | l/right increase volume 107 | .br 108 | c un/lock channels 109 | .br 110 | s/S select next/previous device/port 111 | 112 | -------------------------------------------------------------------------------- /pamix.conf: -------------------------------------------------------------------------------- 1 | ; This is a sample configuration file for pamix (https://github.com/patroclos/PAmix) implementing the default configuration 2 | 3 | ; BINDING KEYS 4 | ; see `man keyname` for reference for special keynames/combinations 5 | 6 | bind q quit 7 | 8 | ; Navigation 9 | 10 | bind KEY_F(1) select-tab playback 11 | bind KEY_F(2) select-tab recording 12 | bind KEY_F(3) select-tab output 13 | bind KEY_F(4) select-tab input 14 | bind KEY_F(5) select-tab cards 15 | 16 | bind j select-next 17 | bind KEY_DOWN select-next 18 | 19 | bind k select-prev 20 | bind KEY_UP select-prev 21 | 22 | ; Volume Control 23 | bind h add-volume -0.05 24 | bind KEY_LEFT add-volume -0.05 25 | bind l add-volume 0.05 26 | bind KEY_RIGHT add-volume 0.05 27 | 28 | bind 1 set-volume 0.1 29 | bind 2 set-volume 0.2 30 | bind 3 set-volume 0.3 31 | bind 4 set-volume 0.4 32 | bind 5 set-volume 0.5 33 | bind 6 set-volume 0.6 34 | bind 7 set-volume 0.7 35 | bind 8 set-volume 0.8 36 | bind 9 set-volume 0.9 37 | bind 0 set-volume 1.0 38 | 39 | ; cycle-next/prev will select the next/previous device or port 40 | ; for the currently selected entry 41 | bind s cycle-next 42 | bind S cycle-prev 43 | 44 | ; toggle-lock toggles whether channels are locked together 45 | bind c toggle-lock 46 | bind m toggle-mute 47 | 48 | -------------------------------------------------------------------------------- /src/app.c: -------------------------------------------------------------------------------- 1 | #include "app.h" 2 | #include "da.h" 3 | #include 4 | #include 5 | #include 6 | 7 | static void entry_data_free(Entry *entry); 8 | 9 | App app = {0}; 10 | 11 | struct entry_indices { 12 | uint32_t *indices; 13 | int count; 14 | }; 15 | 16 | static struct entry_indices *cmp_entry_indices; 17 | // this cmp function splits entries into two groups: corked and uncorked. The sorting is stabelized by passing the 18 | // original order as the context argument, so order within groups doesnt change, only the uncorked streams are brought 19 | // to the front. 20 | // `cmd_entry_indices` needs to be set when running qsort. This is not threadsafe. 21 | static int cmp_entry(const void *pa, const void *pb) { 22 | const Entry *a = pa; 23 | const Entry *b = pb; 24 | assert(a->pa_index != b->pa_index); 25 | if(a->corked != b->corked) { 26 | return a->corked - b->corked; 27 | } 28 | struct entry_indices *indices = cmp_entry_indices; 29 | int ia = -1; 30 | int ib = -1; 31 | for(int i = 0; i < indices->count; i++) { 32 | if(indices->indices[i] == a->pa_index) 33 | ia = i; 34 | else if(indices->indices[i] == b->pa_index) 35 | ib = i; 36 | 37 | if(ia != -1 && ib != -1) 38 | break; 39 | } 40 | assert(ia != -1); 41 | assert(ib != -1); 42 | return ia - ib; 43 | } 44 | 45 | static void cull_entries(Entries *ents) { 46 | for (int i = (int)ents->len - 1; i >= 0; i--) { 47 | Entry *ent = &ents->items[i]; 48 | if (!ents->items[i].marked) 49 | continue; 50 | entry_free(ent); 51 | memmove(ents->items + i, ents->items + i + 1, (ents->len - i - 1) * sizeof(Entry)); 52 | ents->len--; 53 | } 54 | } 55 | 56 | static void cb_monitor_read(pa_stream *stream, size_t nbytes, void *pdata) { 57 | uint32_t index = (uintptr_t)pdata; 58 | const void *data; 59 | int err = pa_stream_peek(stream, &data, &nbytes); 60 | if (err != 0) { 61 | return; 62 | } 63 | assert(nbytes >= sizeof(float)); 64 | assert((nbytes % sizeof(float)) == 0); 65 | float last_peak = ((float *)data)[nbytes / sizeof(float) - 1]; 66 | 67 | pa_stream_drop(stream); 68 | 69 | pthread_mutex_lock(&app.mutex); 70 | for (size_t i = 0; i < app.entries.len; i++) { 71 | Entry *ent = &app.entries.items[i]; 72 | if (ent->pa_index == index || ent->monitor_index == index) { 73 | if (ent->monitor_stream != stream) 74 | break; 75 | assert(ent->monitor_stream == stream); 76 | ent->peak = last_peak; 77 | app.new_peaks = true; 78 | pa_threaded_mainloop_signal(app.pa_mainloop, false); 79 | break; 80 | } 81 | } 82 | pthread_mutex_unlock(&app.mutex); 83 | } 84 | 85 | static void cb_monitor_state(pa_stream *stream, void *data) { 86 | (void)data; 87 | pa_stream_state_t state = pa_stream_get_state(stream); 88 | if (state == PA_STREAM_FAILED || state == PA_STREAM_TERMINATED) { 89 | pthread_mutex_lock(&app.mutex); 90 | for (size_t i = 0; i < app.entries.len; i++) { 91 | Entry *ent = &app.entries.items[i]; 92 | if (ent->monitor_stream == stream) { 93 | ent->monitor_stream = NULL; 94 | ent->peak = 0; 95 | break; 96 | } 97 | } 98 | pthread_mutex_unlock(&app.mutex); 99 | return; 100 | } 101 | 102 | if (state == PA_STREAM_READY) { 103 | pthread_mutex_lock(&app.mutex); 104 | int idx = -1; 105 | for (size_t i = 0; i < app.entries.len; i++) { 106 | Entry *ent = &app.entries.items[i]; 107 | if (ent->monitor_stream == stream) { 108 | idx = i; 109 | break; 110 | } 111 | } 112 | pthread_mutex_unlock(&app.mutex); 113 | if (idx == -1) { 114 | pa_stream_disconnect(stream); 115 | pa_stream_unref(stream); 116 | } 117 | } 118 | } 119 | 120 | static pa_stream *create_monitor(pa_context *ctx, uint32_t monitor_stream, uint32_t device) { 121 | char stream_name[32]; 122 | snprintf(stream_name, sizeof(stream_name) - 1, "PeakMonitor %d", monitor_stream); 123 | 124 | pa_sample_spec spec = {.rate = 40, .format = PA_SAMPLE_FLOAT32LE, .channels = 1}; 125 | pa_proplist *props = pa_proplist_new(); 126 | // hide monitor stream from pavucontrol 127 | pa_proplist_sets(props, PA_PROP_APPLICATION_ID, "org.PulseAudio.pavucontrol"); 128 | pa_stream *stream = pa_stream_new_with_proplist(ctx, stream_name, &spec, NULL, props); 129 | pa_proplist_free(props); 130 | assert(stream != NULL); 131 | 132 | char devname[16]; 133 | if (monitor_stream != PA_INVALID_INDEX) { 134 | assert(device == PA_INVALID_INDEX); 135 | int err = pa_stream_set_monitor_stream(stream, monitor_stream); 136 | if (err != 0) { 137 | pa_stream_unref(stream); 138 | return NULL; 139 | } 140 | } else { 141 | assert(device != PA_INVALID_INDEX); 142 | sprintf(devname, "%u", device); 143 | } 144 | 145 | pa_stream_set_read_callback(stream, &cb_monitor_read, (void *)(uintptr_t)(monitor_stream != PA_INVALID_INDEX ? monitor_stream : device)); 146 | pa_stream_set_state_callback(stream, &cb_monitor_state, NULL); 147 | 148 | pa_stream_flags_t flags = (pa_stream_flags_t)(PA_STREAM_DONT_MOVE | PA_STREAM_PEAK_DETECT | PA_STREAM_ADJUST_LATENCY); 149 | pa_buffer_attr bufattr = {.maxlength = 128, .fragsize = sizeof(float)}; 150 | int err = pa_stream_connect_record(stream, device == PA_INVALID_INDEX ? NULL : devname, &bufattr, flags); 151 | if (err != 0) { 152 | pa_stream_unref(stream); 153 | return NULL; 154 | } 155 | 156 | return stream; 157 | } 158 | 159 | static int find_entry_with_index(uint32_t index, entry_type type) { 160 | for (int i = 0; i < (int)app.entries.len; i++) { 161 | Entry ent = app.entries.items[i]; 162 | if (ent.pa_index == index && ent.type == type) 163 | return i; 164 | } 165 | return -1; 166 | } 167 | 168 | uint32_t pa_entry_index(const void *info, entry_type type) { 169 | switch (type) { 170 | case ENTRY_SINKINPUT: 171 | return ((const pa_sink_input_info *)info)->index; 172 | case ENTRY_SOURCEOUTPUT: 173 | return ((const pa_source_output_info *)info)->index; 174 | case ENTRY_SINK: 175 | return ((const pa_sink_info *)info)->index; 176 | case ENTRY_SOURCE: 177 | return ((const pa_source_info *)info)->index; 178 | case ENTRY_CARD: 179 | return ((const pa_card_info *)info)->index; 180 | } 181 | __builtin_unreachable(); 182 | } 183 | const char *pa_entry_name(const void *info, entry_type type) { 184 | switch (type) { 185 | case ENTRY_SINKINPUT: 186 | return ((const pa_sink_input_info *)info)->name; 187 | case ENTRY_SOURCEOUTPUT: 188 | return ((const pa_source_output_info *)info)->name; 189 | case ENTRY_SINK: 190 | return ((const pa_sink_info *)info)->name; 191 | case ENTRY_SOURCE: 192 | return ((const pa_source_info *)info)->name; 193 | case ENTRY_CARD: 194 | return ((const pa_card_info *)info)->name; 195 | } 196 | __builtin_unreachable(); 197 | } 198 | pa_cvolume pa_entry_volume(const void *info, entry_type type) { 199 | switch (type) { 200 | case ENTRY_SINKINPUT: 201 | return ((const pa_sink_input_info *)info)->volume; 202 | case ENTRY_SOURCEOUTPUT: 203 | return ((const pa_source_output_info *)info)->volume; 204 | case ENTRY_SINK: 205 | return ((const pa_sink_info *)info)->volume; 206 | case ENTRY_SOURCE: 207 | return ((const pa_source_info *)info)->volume; 208 | case ENTRY_CARD: 209 | return (pa_cvolume){.channels = 0}; 210 | } 211 | __builtin_unreachable(); 212 | } 213 | bool pa_entry_corked(const void *info, entry_type type) { 214 | switch (type) { 215 | case ENTRY_SINKINPUT: 216 | return ((const pa_sink_input_info *)info)->corked; 217 | case ENTRY_SOURCEOUTPUT: 218 | return ((const pa_source_output_info *)info)->corked; 219 | case ENTRY_SINK: 220 | return false; 221 | case ENTRY_SOURCE: 222 | return false; 223 | case ENTRY_CARD: 224 | return false; 225 | } 226 | __builtin_unreachable(); 227 | } 228 | bool pa_entry_mute(const void *info, entry_type type) { 229 | switch (type) { 230 | case ENTRY_SINKINPUT: 231 | return ((const pa_sink_input_info *)info)->mute; 232 | case ENTRY_SOURCEOUTPUT: 233 | return ((const pa_source_output_info *)info)->mute; 234 | case ENTRY_SINK: 235 | return ((const pa_sink_info *)info)->mute; 236 | case ENTRY_SOURCE: 237 | return ((const pa_source_info *)info)->mute; 238 | case ENTRY_CARD: 239 | return false; 240 | } 241 | __builtin_unreachable(); 242 | } 243 | pa_proplist *pa_entry_proplist(const void *info, entry_type type) { 244 | switch (type) { 245 | case ENTRY_SINKINPUT: 246 | return ((const pa_sink_input_info *)info)->proplist; 247 | case ENTRY_SOURCEOUTPUT: 248 | return ((const pa_source_output_info *)info)->proplist; 249 | case ENTRY_SINK: 250 | return ((const pa_sink_info *)info)->proplist; 251 | case ENTRY_SOURCE: 252 | return ((const pa_source_info *)info)->proplist; 253 | case ENTRY_CARD: 254 | return ((const pa_card_info *)info)->proplist; 255 | } 256 | __builtin_unreachable(); 257 | } 258 | 259 | pa_channel_map pa_entry_channel_map(const void *info, entry_type type) { 260 | switch (type) { 261 | case ENTRY_SINKINPUT: 262 | return ((const pa_sink_input_info *)info)->channel_map; 263 | case ENTRY_SOURCEOUTPUT: 264 | return ((const pa_source_output_info *)info)->channel_map; 265 | case ENTRY_SINK: 266 | return ((const pa_sink_info *)info)->channel_map; 267 | case ENTRY_SOURCE: 268 | return ((const pa_source_info *)info)->channel_map; 269 | case ENTRY_CARD: 270 | return (pa_channel_map){.channels = 0}; 271 | } 272 | __builtin_unreachable(); 273 | } 274 | 275 | uint32_t pa_entry_monitor_index(const void *info, entry_type type) { 276 | switch (type) { 277 | case ENTRY_SINK: 278 | return ((const pa_sink_info *)info)->monitor_source; 279 | case ENTRY_SOURCE: 280 | return ((const pa_source_info *)info)->index; 281 | case ENTRY_SOURCEOUTPUT: 282 | return ((const pa_source_output_info *)info)->source; 283 | default: 284 | return PA_INVALID_INDEX; 285 | } 286 | } 287 | 288 | void apply_entry_data(union EntryData *data, const void *info, entry_type type) { 289 | switch (type) { 290 | case ENTRY_SINKINPUT: 291 | case ENTRY_SOURCEOUTPUT: { 292 | uint32_t device = type == ENTRY_SINKINPUT 293 | ? ((const pa_sink_input_info *)info)->sink 294 | : ((const pa_source_output_info *)info)->source; 295 | if (data->device.index != device) { 296 | data->device.index = device; 297 | if (data->device.name != NULL) { 298 | free((void *)data->device.name); 299 | data->device.name = NULL; 300 | } 301 | } 302 | break; 303 | } 304 | case ENTRY_SINK: { 305 | data->ports.current = -1; 306 | for (size_t i = 0; i < data->ports.len; i++) { 307 | free((void *)data->ports.items[i].name); 308 | free((void *)data->ports.items[i].description); 309 | } 310 | data->ports.len = 0; 311 | const pa_sink_info *si = ((const pa_sink_info *)info); 312 | for (uint32_t i = 0; i < si->n_ports; i++) { 313 | NameDesc port = { 314 | .name = strdup(si->ports[i]->name), 315 | .description = strdup(si->ports[i]->description), 316 | }; 317 | da_append(&data->ports, port); 318 | if (si->active_port == si->ports[i]) 319 | data->ports.current = i; 320 | } 321 | break; 322 | } 323 | case ENTRY_SOURCE: { 324 | data->ports.current = -1; 325 | for (size_t i = 0; i < data->ports.len; i++) { 326 | free((void *)data->ports.items[i].name); 327 | free((void *)data->ports.items[i].description); 328 | } 329 | data->ports.len = 0; 330 | const pa_source_info *si = ((const pa_source_info *)info); 331 | for (uint32_t i = 0; i < si->n_ports; i++) { 332 | NameDesc port = { 333 | .name = strdup(si->ports[i]->name), 334 | .description = strdup(si->ports[i]->description), 335 | }; 336 | da_append(&data->ports, port); 337 | if (si->active_port == si->ports[i]) 338 | data->ports.current = i; 339 | } 340 | break; 341 | } 342 | case ENTRY_CARD: { 343 | data->profiles.current = -1; 344 | for (size_t i = 0; i < data->profiles.len; i++) { 345 | free((void *)data->profiles.items[i].name); 346 | free((void *)data->profiles.items[i].description); 347 | } 348 | data->profiles.len = 0; 349 | const pa_card_info *si = ((const pa_card_info *)info); 350 | for (uint32_t i = 0; i < si->n_profiles; i++) { 351 | NameDesc profile = { 352 | .name = strdup(si->profiles2[i]->name), 353 | .description = strdup(si->profiles2[i]->description), 354 | }; 355 | da_append(&data->profiles, profile); 356 | if (si->active_profile2 == si->profiles2[i]) 357 | data->ports.current = i; 358 | } 359 | break; 360 | } 361 | default: 362 | __builtin_unreachable(); 363 | } 364 | } 365 | 366 | void app_entry_info(const void *info, entry_type type) { 367 | uint32_t index = pa_entry_index(info, type); 368 | const char *name = pa_entry_name(info, type); 369 | assert(info != NULL); 370 | pthread_mutex_lock(&app.mutex); 371 | int i = find_entry_with_index(index, type); 372 | if (i != -1) { 373 | Entry *entry = &app.entries.items[i]; 374 | assert(entry->type == type); 375 | if(strcmp(entry->name, name) != 0) { 376 | free((void*)entry->name); 377 | entry->name = strdup(name); 378 | } 379 | entry->marked = false; 380 | entry->volume = pa_entry_volume(info, type); 381 | entry->corked = pa_entry_corked(info, type); 382 | entry->channel_map = pa_entry_channel_map(info, type); 383 | entry->muted = pa_entry_mute(info, type); 384 | entry->monitor_index = pa_entry_monitor_index(info, type); 385 | if (entry->props != NULL) { 386 | pa_proplist_free(entry->props); 387 | } 388 | if (pa_entry_corked(info, type)) 389 | entry->peak = 0; 390 | entry->props = pa_proplist_copy(pa_entry_proplist(info, type)); 391 | apply_entry_data(&entry->data, info, type); 392 | } else { 393 | Entry ent = { 394 | .type = type, 395 | .name = strdup(name), 396 | .pa_index = index, 397 | .volume = pa_entry_volume(info, type), 398 | .channel_map = pa_entry_channel_map(info, type), 399 | .props = pa_proplist_copy(pa_entry_proplist(info, type)), 400 | .monitor_index = pa_entry_monitor_index(info, type), 401 | .muted = pa_entry_mute(info, type), 402 | .corked = pa_entry_corked(info, type), 403 | .volume_lock = type != ENTRY_CARD, 404 | }; 405 | apply_entry_data(&ent.data, info, type); 406 | da_append(&app.entries, ent); 407 | } 408 | pthread_mutex_unlock(&app.mutex); 409 | } 410 | void app_sink_input_info(pa_context *ctx, const pa_sink_input_info *info, int eol, void *data) { 411 | (void)ctx; 412 | (void)data; 413 | if (info == NULL) { 414 | if (eol) 415 | pa_threaded_mainloop_signal(app.pa_mainloop, false); 416 | return; 417 | } 418 | app_entry_info(info, ENTRY_SINKINPUT); 419 | } 420 | void app_source_output_info(pa_context *ctx, const pa_source_output_info *info, int eol, void *data) { 421 | (void)ctx; 422 | (void)data; 423 | if (info == NULL) { 424 | if (eol) 425 | pa_threaded_mainloop_signal(app.pa_mainloop, false); 426 | return; 427 | } 428 | // hide peak-detection streams 429 | const char *appname = pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_ID); 430 | if (appname != NULL && strcmp(appname, "org.PulseAudio.pavucontrol") == 0) { 431 | return; 432 | } 433 | app_entry_info(info, ENTRY_SOURCEOUTPUT); 434 | } 435 | 436 | void app_sink_info(pa_context *ctx, const pa_sink_info *info, int eol, void *data) { 437 | (void)ctx; 438 | (void)data; 439 | if (info == NULL) { 440 | if (eol) 441 | pa_threaded_mainloop_signal(app.pa_mainloop, false); 442 | return; 443 | } 444 | app_entry_info(info, ENTRY_SINK); 445 | } 446 | 447 | void app_source_info(pa_context *ctx, const pa_source_info *info, int eol, void *data) { 448 | (void)ctx; 449 | (void)data; 450 | if (info == NULL) { 451 | if (eol) 452 | pa_threaded_mainloop_signal(app.pa_mainloop, false); 453 | return; 454 | } 455 | // hide monitors 456 | const char *devtyp = pa_proplist_gets(info->proplist, PA_PROP_DEVICE_CLASS); 457 | if(devtyp != NULL && strcmp(devtyp, "monitor") == 0) { 458 | return; 459 | } 460 | app_entry_info(info, ENTRY_SOURCE); 461 | } 462 | 463 | void app_card_info(pa_context *ctx, const pa_card_info *info, int eol, void *data) { 464 | (void)ctx; 465 | (void)data; 466 | if (info == NULL) { 467 | if (eol) 468 | pa_threaded_mainloop_signal(app.pa_mainloop, false); 469 | return; 470 | } 471 | app_entry_info(info, ENTRY_CARD); 472 | } 473 | 474 | static void app_sink_info_name(pa_context *ctx, const pa_sink_info *i, int eol, void *userdata) { 475 | (void)ctx; 476 | if (eol) { 477 | assert(i == NULL); 478 | pa_threaded_mainloop_signal(app.pa_mainloop, false); 479 | return; 480 | } 481 | Entry *ent = userdata; 482 | ent->data.device.name = strdup(i->description); 483 | } 484 | static void app_source_info_name(pa_context *ctx, const pa_source_info *i, int eol, void *userdata) { 485 | (void)ctx; 486 | if (eol) { 487 | assert(i == NULL); 488 | pa_threaded_mainloop_signal(app.pa_mainloop, false); 489 | return; 490 | } 491 | Entry *ent = userdata; 492 | ent->data.device.name = strdup(i->description); 493 | } 494 | 495 | bool app_refresh_entries(App *app) { 496 | pa_threaded_mainloop_lock(app->pa_mainloop); 497 | pthread_mutex_lock(&app->mutex); 498 | if (app->pa_context == NULL || pa_context_get_state(app->pa_context) != PA_CONTEXT_READY) { 499 | pthread_mutex_unlock(&app->mutex); 500 | pa_threaded_mainloop_unlock(app->pa_mainloop); 501 | return false; 502 | } 503 | 504 | for (size_t i = 0; i < app->entries.len; i++) 505 | app->entries.items[i].marked = true; 506 | 507 | pa_operation *op; 508 | pa_operation_state_t state; 509 | 510 | switch (app->entry_page) { 511 | case ENTRY_SINKINPUT: 512 | op = pa_context_get_sink_input_info_list(app->pa_context, &app_sink_input_info, NULL); 513 | break; 514 | case ENTRY_SOURCEOUTPUT: 515 | op = pa_context_get_source_output_info_list(app->pa_context, &app_source_output_info, NULL); 516 | break; 517 | case ENTRY_SINK: 518 | op = pa_context_get_sink_info_list(app->pa_context, &app_sink_info, NULL); 519 | break; 520 | case ENTRY_SOURCE: 521 | op = pa_context_get_source_info_list(app->pa_context, &app_source_info, NULL); 522 | break; 523 | case ENTRY_CARD: 524 | op = pa_context_get_card_info_list(app->pa_context, &app_card_info, NULL); 525 | break; 526 | default: 527 | __builtin_unreachable(); 528 | } 529 | assert(op != NULL); 530 | 531 | pthread_mutex_unlock(&app->mutex); 532 | while ((state = pa_operation_get_state(op)) == PA_OPERATION_RUNNING) { 533 | pa_threaded_mainloop_wait(app->pa_mainloop); 534 | } 535 | pa_operation_unref(op); 536 | if(state == PA_OPERATION_CANCELLED) { 537 | pa_threaded_mainloop_unlock(app->pa_mainloop); 538 | return false; 539 | } 540 | pthread_mutex_lock(&app->mutex); 541 | assert(state == PA_OPERATION_DONE); 542 | 543 | cull_entries(&app->entries); 544 | 545 | uint32_t indexbuf[app->entries.len]; 546 | for(size_t i = 0; i < app->entries.len; i++) 547 | indexbuf[i] = app->entries.items[i].pa_index; 548 | struct entry_indices indices = {.count = (int)app->entries.len, .indices = indexbuf}; 549 | cmp_entry_indices = &indices; 550 | qsort(app->entries.items, app->entries.len, sizeof(*app->entries.items), cmp_entry); 551 | 552 | for (size_t i = 0; i < app->entries.len; i++) { 553 | Entry *ent = &app->entries.items[i]; 554 | // populate device name 555 | if ((ent->type == ENTRY_SINKINPUT || ent->type == ENTRY_SOURCEOUTPUT) && ent->data.device.name == NULL) { 556 | pa_operation *op; 557 | switch (ent->type) { 558 | case ENTRY_SINKINPUT: 559 | op = pa_context_get_sink_info_by_index(app->pa_context, ent->data.device.index, &app_sink_info_name, ent); 560 | break; 561 | case ENTRY_SOURCEOUTPUT: 562 | op = pa_context_get_source_info_by_index(app->pa_context, ent->data.device.index, &app_source_info_name, ent); 563 | break; 564 | default: 565 | __builtin_unreachable(); 566 | } 567 | assert(op != NULL); 568 | pa_operation_state_t state; 569 | pthread_mutex_unlock(&app->mutex); 570 | while ((state = pa_operation_get_state(op)) == PA_OPERATION_RUNNING) { 571 | pa_threaded_mainloop_wait(app->pa_mainloop); 572 | } 573 | pa_operation_unref(op); 574 | if(state == PA_OPERATION_CANCELLED) { 575 | pa_threaded_mainloop_unlock(app->pa_mainloop); 576 | return false; 577 | } 578 | pthread_mutex_lock(&app->mutex); 579 | assert(state == PA_OPERATION_DONE); 580 | assert(ent->data.device.name != NULL); 581 | } 582 | 583 | // ensure monitor stream exists 584 | if (ent->monitor_stream != NULL || ent->type == ENTRY_CARD) 585 | continue; 586 | // we exclude corked entries, because those monitor streams will be stuck in creating state, which can't be 587 | // disconnected yet, so it just accumulates dead streams when switching tabs 588 | if (ent->type == ENTRY_SINKINPUT && !ent->corked) { 589 | ent->monitor_stream = create_monitor(app->pa_context, ent->pa_index, PA_INVALID_INDEX); 590 | } 591 | if (ent->type == ENTRY_SOURCEOUTPUT && !ent->corked) { 592 | const char *appname = pa_proplist_gets(ent->props, PA_PROP_APPLICATION_ID); 593 | if (appname == NULL || strcmp(appname, "org.PulseAudio.pavucontrol") != 0) { 594 | ent->monitor_stream = create_monitor(app->pa_context, PA_INVALID_INDEX, ent->monitor_index); 595 | } 596 | } 597 | if ((ent->type == ENTRY_SINK || ent->type == ENTRY_SOURCE)) { 598 | ent->monitor_stream = create_monitor(app->pa_context, PA_INVALID_INDEX, ent->monitor_index); 599 | } 600 | } 601 | 602 | pthread_mutex_unlock(&app->mutex); 603 | pa_threaded_mainloop_unlock(app->pa_mainloop); 604 | return true; 605 | } 606 | void app_init(App *app, pa_context *context, pa_threaded_mainloop *mainloop) { 607 | app->pa_context = context; 608 | app->pa_mainloop = mainloop; 609 | app->entry_page = ENTRY_SINKINPUT; 610 | app->running = true; 611 | app->resized = ATOMIC_VAR_INIT(false); 612 | int err = pthread_mutex_init(&app->mutex, NULL); 613 | if(err != 0) { 614 | fprintf(stderr, "failed to create pthread mutex: %d\n", err); 615 | exit(1); 616 | } 617 | } 618 | 619 | static void entry_data_free(Entry *entry) { 620 | switch(entry->type) { 621 | case ENTRY_SINKINPUT: 622 | case ENTRY_SOURCEOUTPUT: 623 | if(entry->data.device.name != NULL) 624 | free((void*)entry->data.device.name); 625 | break; 626 | case ENTRY_SINK: 627 | case ENTRY_SOURCE: 628 | case ENTRY_CARD: 629 | for(size_t i = 0; i < entry->data.ports.len; i++) { 630 | free((void*)entry->data.ports.items[i].name); 631 | free((void*)entry->data.ports.items[i].description); 632 | } 633 | entry->data.ports.len = 0; 634 | entry->data.ports.current = -1; 635 | if(entry->data.ports.items != 0) { 636 | free(entry->data.ports.items); 637 | entry->data.ports.items = NULL; 638 | entry->data.ports.cap = 0; 639 | } 640 | break; 641 | } 642 | } 643 | void entry_free(Entry *entry) { 644 | if(entry->name != NULL) { 645 | free((void*)entry->name); 646 | entry->name = NULL; 647 | } 648 | if(entry->props != NULL){ 649 | pa_proplist_free(entry->props); 650 | entry->props = NULL; 651 | } 652 | entry_data_free(entry); 653 | if(entry->monitor_stream != NULL && pa_stream_get_state(entry->monitor_stream) == PA_STREAM_READY){ 654 | int err = pa_stream_disconnect(entry->monitor_stream); 655 | assert(err == 0); 656 | pa_stream_unref(entry->monitor_stream); 657 | entry->monitor_stream = NULL; 658 | } 659 | } 660 | -------------------------------------------------------------------------------- /src/app.h: -------------------------------------------------------------------------------- 1 | #ifndef _APP_H 2 | #define _APP_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | typedef enum { 11 | ENTRY_SINKINPUT, 12 | ENTRY_SOURCEOUTPUT, 13 | ENTRY_SINK, 14 | ENTRY_SOURCE, 15 | ENTRY_CARD 16 | } entry_type; 17 | 18 | typedef struct { 19 | const char *name; 20 | const char *description; 21 | } NameDesc; 22 | 23 | typedef struct { 24 | NameDesc *items; 25 | size_t len; 26 | size_t cap; 27 | int current; 28 | } NameDescs; 29 | 30 | union EntryData { 31 | // sink/source of sinkinput and sourceoutput entries 32 | // TODO: do we put device names in here? 33 | struct { 34 | uint32_t index; 35 | const char *name; 36 | } device; 37 | // ports of sink and source entries 38 | NameDescs ports; 39 | // profiles of card entries 40 | NameDescs profiles; 41 | }; 42 | 43 | typedef struct { 44 | entry_type type; 45 | const char *name; 46 | uint32_t pa_index; 47 | pa_cvolume volume; 48 | pa_channel_map channel_map; 49 | pa_proplist *props; 50 | pa_stream *monitor_stream; 51 | uint32_t monitor_index; 52 | float peak; 53 | bool muted; 54 | bool corked; 55 | bool marked; 56 | bool volume_lock; 57 | 58 | union EntryData data; 59 | } Entry; 60 | 61 | typedef struct { 62 | Entry *items; 63 | size_t len; 64 | size_t cap; 65 | } Entries; 66 | 67 | typedef struct { 68 | int keycode; 69 | const char *keyname; 70 | } InputEvent; 71 | 72 | typedef struct { 73 | InputEvent *items; 74 | size_t len; 75 | size_t cap; 76 | } InputQueue; 77 | 78 | typedef struct { 79 | pa_context *pa_context; 80 | pa_threaded_mainloop *pa_mainloop; 81 | Entries entries; 82 | entry_type entry_page; 83 | int selected_entry; 84 | int selected_channel; 85 | int scroll; 86 | pthread_mutex_t mutex; 87 | atomic_bool should_refresh; 88 | atomic_bool resized; 89 | //bool resized; 90 | bool new_peaks; 91 | bool running; 92 | InputQueue input_queue; 93 | } App; 94 | 95 | extern App app; 96 | 97 | void app_init(App *app, pa_context *context, pa_threaded_mainloop *mainloop); 98 | bool app_refresh_entries(App *app); 99 | 100 | void entry_free(Entry *entry); 101 | #endif 102 | -------------------------------------------------------------------------------- /src/config.c: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | #include 3 | #include 4 | #include 5 | 6 | static struct { 7 | entry_type t; 8 | const char *s; 9 | } tab_mappings[] = { 10 | {ENTRY_SINKINPUT, "2"}, 11 | {ENTRY_SINKINPUT, "playback"}, 12 | {ENTRY_SINK, "0"}, 13 | {ENTRY_SINK, "output"}, 14 | {ENTRY_SOURCEOUTPUT, "3"}, 15 | {ENTRY_SOURCEOUTPUT, "recording"}, 16 | {ENTRY_SOURCE, "1"}, 17 | {ENTRY_SOURCE, "input"}, 18 | {ENTRY_CARD, "4"}, 19 | {ENTRY_CARD, "cards"}, 20 | }; 21 | 22 | static bool has_prefix(const char *str, const char *prefix) { 23 | while (*str && *prefix && *str++ == *prefix++) 24 | ; 25 | return !*prefix; 26 | } 27 | 28 | int config_load(Config *config, const char *path) { 29 | memset(config, 0, sizeof(*config)); 30 | 31 | const char *keynames[KEY_MAX]; 32 | for (int i = 0; i < KEY_MAX; i++) 33 | keynames[i] = keyname(i); 34 | 35 | FILE *f = fopen(path, "rb"); 36 | if (f == NULL) 37 | return -1; 38 | 39 | fseek(f, 0, SEEK_END); 40 | long length = ftell(f); 41 | fseek(f, 0, SEEK_SET); 42 | 43 | char *text = (char *)malloc(length + 1); 44 | assert(text != NULL); 45 | 46 | text[fread(text, 1, length, f)] = 0; 47 | 48 | fclose(f); 49 | 50 | char *save = NULL; 51 | for (char *line = strtok_r(text, "\n", &save); line != NULL; line = strtok_r(NULL, "\n", &save)) { 52 | char *comment = strchr(line, ';'); 53 | if (comment != NULL) 54 | *comment = '\0'; 55 | 56 | if (has_prefix(line, "set ")) { 57 | // TODO 58 | continue; 59 | } 60 | if (has_prefix(line, "bind ")) { 61 | char *key = line + sizeof("bind"); 62 | char *action = strchr(key, ' '); 63 | *action++ = '\0'; 64 | int keycode = -1; 65 | for (int i = 0; i < KEY_MAX; i++) { 66 | if (keynames[i] && strcmp(key, keynames[i]) == 0) { 67 | keycode = i; 68 | break; 69 | } 70 | } 71 | if (keycode == -1) { 72 | assert(0 && "invalid keycode"); 73 | continue; 74 | } 75 | 76 | char *arg = strchr(action, ' '); 77 | if (arg != NULL) 78 | *arg++ = '\0'; 79 | 80 | Action *info = &config->keymap[keycode]; 81 | if (strcmp(action, "quit") == 0) { 82 | info->type = ACTION_QUIT; 83 | continue; 84 | } 85 | if (strcmp(action, "select-tab") == 0) { 86 | assert(arg != NULL); 87 | int idx = -1; 88 | for (size_t i = 0; i < sizeof(tab_mappings) / sizeof(*tab_mappings); i++) { 89 | if (strcmp(tab_mappings[i].s, arg) == 0) { 90 | idx = i; 91 | break; 92 | } 93 | } 94 | assert(idx != -1); 95 | info->type = ACTION_SELECT_TAB; 96 | info->data.tab = tab_mappings[idx].t; 97 | continue; 98 | } 99 | if (strcmp(action, "set-volume") == 0 || strcmp(action, "add-volume") == 0) { 100 | assert(arg != NULL); 101 | char *end; 102 | double value = strtod(arg, &end); 103 | assert(end == arg + strlen(arg)); 104 | info->type = strcmp(action, "set-volume") == 0 ? ACTION_VOLUME_SET : ACTION_VOLUME_ADD; 105 | info->data.volume = (float)value; 106 | continue; 107 | } 108 | if(strcmp(action, "select-next") == 0) { 109 | info->type = ACTION_ENTRY_NEXT; 110 | continue; 111 | } 112 | if(strcmp(action, "select-prev") == 0) { 113 | info->type = ACTION_ENTRY_PREV; 114 | continue; 115 | } 116 | if(strcmp(action, "cycle-next") == 0) { 117 | info->type = ACTION_DEVICE_NEXT; 118 | continue; 119 | } 120 | if(strcmp(action, "cycle-prev") == 0) { 121 | info->type = ACTION_DEVICE_PREV; 122 | continue; 123 | } 124 | if(strcmp(action, "toggle-mute") == 0) { 125 | info->type = ACTION_MUTE_TOGGLE; 126 | continue; 127 | } 128 | if(strcmp(action, "toggle-lock") == 0) { 129 | info->type = ACTION_LOCK_TOGGLE; 130 | continue; 131 | } 132 | // TODO: tab cycle? 133 | continue; 134 | } 135 | /* 136 | if(strlen(line) > 0) 137 | fprintf(stderr, "line: %s\n", line); 138 | */ 139 | } 140 | free(text); 141 | 142 | return 0; 143 | } 144 | 145 | void config_default(Config *config) { 146 | config->keymap['q'] = (Action){.type = ACTION_QUIT}; 147 | for(int i = 0; i < 10; i++) 148 | config->keymap['0' + i] = (Action){.type = ACTION_VOLUME_SET, .data = {.volume = i == 0 ? 1.0f : i * 0.1f}}; 149 | config->keymap['h'] = (Action){.type = ACTION_VOLUME_ADD, .data = {.volume = -0.05f}}; 150 | config->keymap['l'] = (Action){.type = ACTION_VOLUME_ADD, .data = {.volume = 0.05f}}; 151 | config->keymap['j'] = (Action){.type = ACTION_ENTRY_NEXT}; 152 | config->keymap['k'] = (Action){.type = ACTION_ENTRY_PREV}; 153 | config->keymap[KEY_LEFT] = (Action){.type = ACTION_VOLUME_ADD, .data = {.volume = -0.05f}}; 154 | config->keymap[KEY_RIGHT] = (Action){.type = ACTION_VOLUME_ADD, .data = {.volume = 0.05f}}; 155 | config->keymap[KEY_DOWN] = (Action){.type = ACTION_ENTRY_NEXT}; 156 | config->keymap[KEY_UP] = (Action){.type = ACTION_ENTRY_PREV}; 157 | for(int i = 0; i <= ENTRY_CARD; i++) 158 | config->keymap[KEY_F(i + 1)] = (Action){.type = ACTION_SELECT_TAB, .data = {.tab = (entry_type)i}}; 159 | config->keymap['s'] = (Action){.type = ACTION_DEVICE_NEXT}; 160 | config->keymap['S'] = (Action){.type = ACTION_DEVICE_PREV}; 161 | config->keymap['c'] = (Action){.type = ACTION_LOCK_TOGGLE}; 162 | config->keymap['m'] = (Action){.type = ACTION_MUTE_TOGGLE}; 163 | } 164 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | #ifndef _CONFIG_H 2 | #define _CONFIG_H 3 | 4 | #include "app.h" 5 | #include 6 | 7 | typedef enum { 8 | ACTION_NONE = 0, 9 | ACTION_QUIT, 10 | ACTION_SELECT_TAB, 11 | ACTION_MUTE_TOGGLE, 12 | ACTION_LOCK_TOGGLE, 13 | ACTION_ENTRY_NEXT, 14 | ACTION_ENTRY_PREV, 15 | ACTION_VOLUME_ADD, 16 | ACTION_VOLUME_SET, 17 | ACTION_DEVICE_NEXT, 18 | ACTION_DEVICE_PREV, 19 | } ActionType; 20 | 21 | typedef struct { 22 | ActionType type; 23 | union { 24 | entry_type tab; 25 | float volume; 26 | } data; 27 | } Action; 28 | 29 | typedef struct { 30 | Action keymap[KEY_MAX]; 31 | } Config; 32 | 33 | int config_load(Config *config, const char *path); 34 | void config_default(Config *config); 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /src/da.h: -------------------------------------------------------------------------------- 1 | #ifndef _DA_H 2 | #define _DA_H 3 | 4 | #define da_grow_cap(da) \ 5 | (da)->cap == 0 ? sizeof(*(da)->items) > 1024 ? 1 : 256 : (da)->cap * 2 6 | 7 | #define da_append(da, item) \ 8 | do { \ 9 | if ((da)->len >= (da)->cap) { \ 10 | (da)->cap = da_grow_cap(da); \ 11 | (da)->items = realloc((da)->items, (da)->cap * sizeof(*(da)->items)); \ 12 | assert((da)->items != NULL); \ 13 | } \ 14 | (da)->items[(da)->len++] = (item); \ 15 | } while (0) 16 | 17 | #define da_reserve(da, add) \ 18 | do { \ 19 | if ((da)->cap >= (da)->len + (add)) \ 20 | break; \ 21 | while ((da)->len + (add) > (da)->cap) { \ 22 | (da)->cap = da_grow_cap(da); \ 23 | } \ 24 | (da)->items = realloc((da)->items, (da)->cap * sizeof(*(da)->items)); \ 25 | assert((da)->items != NULL); \ 26 | } while (0) 27 | 28 | #define da_append_many(da, new_items, items_count) \ 29 | do { \ 30 | if ((da)->len + items_count > (da)->cap) { \ 31 | while ((da)->len + items_count > (da)->cap) { \ 32 | (da)->cap = da_grow_cap(da); \ 33 | } \ 34 | (da)->items = realloc((da)->items, (da)->cap * sizeof(*(da)->items)); \ 35 | assert((da)->items != NULL); \ 36 | } \ 37 | memcpy((da)->items + (da)->len, new_items, \ 38 | items_count * sizeof(*(da)->items)); \ 39 | (da)->len += items_count; \ 40 | } while (0) 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /src/draw.c: -------------------------------------------------------------------------------- 1 | #include "draw.h" 2 | #include 3 | #include 4 | #include 5 | 6 | static wchar_t braille[] = { L' ', L'\u2802', L'\u2806', L'\u2807', L'\u280f', L'\u281f', L'\u283f' }; 7 | static int n_braille = (int)(sizeof(braille) / sizeof(*braille)); 8 | 9 | void draw_volume_bar(int y, int x, int width, pa_volume_t volume) { 10 | int segments = width - 2; 11 | if (segments <= 0) 12 | return; 13 | int fill = (int)((float)volume / (float)(PA_VOLUME_NORM * 1.5) * (float)segments); 14 | if(fill > segments) 15 | fill = segments; 16 | mvaddstr(y, x++, "["); 17 | mvaddstr(y, x + segments, "]"); 18 | 19 | wchar_t buf[segments + 1]; 20 | buf[segments] = 0; 21 | for (int i = 0; i < segments; i++) 22 | buf[i] = i >= fill ? L' ' : braille[n_braille-1]; 23 | 24 | if(volume != PA_VOLUME_MUTED && volume != PA_VOLUME_NORM) { 25 | int segval = (PA_VOLUME_NORM * 1.5) / segments; 26 | int subsegval = segval / (n_braille - 1); 27 | int idx = (volume % segval) / subsegval; 28 | assert(idx <= n_braille - 1); 29 | buf[fill] = braille[idx]; 30 | } 31 | 32 | int indexA = (int)(segments * ((double) 1 / 3)); 33 | int indexB = (int)(segments * ((double) 2 / 3)); 34 | attron(COLOR_PAIR(1)); 35 | mvaddnwstr(y, x, buf, indexA); 36 | attroff(COLOR_PAIR(1)); 37 | attron(COLOR_PAIR(2)); 38 | mvaddnwstr(y, x + indexA, buf + indexA, indexB - indexA); 39 | attroff(COLOR_PAIR(2)); 40 | attron(COLOR_PAIR(3)); 41 | mvaddnwstr(y, x + indexB, buf + indexB, segments - indexB); 42 | attroff(COLOR_PAIR(3)); 43 | } 44 | -------------------------------------------------------------------------------- /src/draw.h: -------------------------------------------------------------------------------- 1 | #ifndef _DRAW_H 2 | #define _DRAW_H 3 | #include 4 | 5 | void draw_volume_bar(int y, int x, int width, pa_volume_t volume); 6 | 7 | #endif 8 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "app.h" 13 | #include "da.h" 14 | #include "draw.h" 15 | #include "config.h" 16 | 17 | struct line_expect { 18 | int begin; 19 | int *end; 20 | int expected; 21 | }; 22 | 23 | void line_expect_check(struct line_expect *e) { 24 | int count = (*e->end) - e->begin; 25 | assert(e->expected == count); 26 | } 27 | static inline int expected_entry_lines(const Entry *ent) { 28 | int channel_lines = ent->type == ENTRY_CARD ? 0: (ent->volume_lock ? 1 : ent->volume.channels); 29 | return channel_lines + 1 + (ent->type != ENTRY_CARD); 30 | } 31 | 32 | int compute_entry_scroll(void); 33 | 34 | void on_ctx_state(pa_context *ctx, void *data) { 35 | (void)ctx; 36 | pa_threaded_mainloop *mainloop = (pa_threaded_mainloop *)data; 37 | pa_threaded_mainloop_signal(mainloop, false); 38 | } 39 | 40 | void on_ctx_subscription(pa_context *ctx, pa_subscription_event_type_t evt_type, uint32_t index, void *data) { 41 | (void)ctx; 42 | (void)evt_type; 43 | (void)index; 44 | (void)data; 45 | atomic_store(&app.should_refresh, true); 46 | pa_threaded_mainloop_signal(app.pa_mainloop, false); 47 | } 48 | 49 | void cb_success_signal(pa_context *ctx, int succ, void *data) { 50 | (void)ctx; 51 | (void)succ; 52 | (void)data; 53 | pa_threaded_mainloop_signal(app.pa_mainloop, false); 54 | } 55 | 56 | void on_signal_resize(int signal) { 57 | (void)signal; 58 | atomic_store(&app.resized, true); 59 | pa_threaded_mainloop_signal(app.pa_mainloop, false); 60 | } 61 | 62 | void *input_thread_main(void *data) { 63 | (void)data; 64 | while (app.running) { 65 | pthread_mutex_lock(&app.mutex); 66 | int ch = getch(); 67 | pthread_mutex_unlock(&app.mutex); 68 | #ifdef KEY_RESIZE 69 | if (ch == KEY_RESIZE) { 70 | atomic_store(&app.resized, true); 71 | } 72 | #endif 73 | 74 | bool key_valid = ch != ERR && ch != KEY_RESIZE && ch != KEY_MOUSE; 75 | if (key_valid) { 76 | InputEvent evt = { 77 | .keycode = ch, 78 | .keyname = keyname(ch), 79 | }; 80 | assert(evt.keyname != NULL); 81 | pthread_mutex_lock(&app.mutex); 82 | da_append(&app.input_queue, evt); 83 | pthread_mutex_unlock(&app.mutex); 84 | } 85 | if (key_valid || ch == KEY_RESIZE) { 86 | pa_threaded_mainloop_signal(app.pa_mainloop, false); 87 | } 88 | usleep(2000); 89 | } 90 | return NULL; 91 | } 92 | 93 | static void reconnect_cleanup_mainloop(void *mainloop) { 94 | pa_threaded_mainloop_unlock(mainloop); 95 | } 96 | 97 | void *reconnect_thread_main(void *arg) { 98 | (void)arg; 99 | while (app.running) { 100 | pa_proplist *props; 101 | pa_mainloop_api *api; 102 | int err; 103 | pthread_cleanup_push(reconnect_cleanup_mainloop, app.pa_mainloop); 104 | pa_threaded_mainloop_lock(app.pa_mainloop); 105 | pthread_mutex_lock(&app.mutex); 106 | if (app.pa_context != NULL && pa_context_get_state(app.pa_context) == PA_CONTEXT_READY) { 107 | goto sleep; 108 | } 109 | if (app.pa_context != NULL) { 110 | pa_context_unref(app.pa_context); 111 | app.pa_context = NULL; 112 | } 113 | 114 | props = pa_proplist_new(); 115 | pa_proplist_sets(props, PA_PROP_APPLICATION_ID, "testerino"); 116 | pa_proplist_sets(props, PA_PROP_APPLICATION_NAME, "testerino"); 117 | api = pa_threaded_mainloop_get_api(app.pa_mainloop); 118 | app.pa_context = pa_context_new_with_proplist(api, "testerino", props); 119 | pa_proplist_free(props); 120 | pa_context_set_state_callback(app.pa_context, &on_ctx_state, app.pa_mainloop); 121 | err = pa_context_connect(app.pa_context, NULL, (pa_context_flags_t)PA_CONTEXT_NOAUTOSPAWN, NULL); 122 | if (err != 0) { 123 | pa_context_unref(app.pa_context); 124 | app.pa_context = NULL; 125 | // TODO: on error we probably want to sleep with the lock held, so noone else does any funny business 126 | goto sleep; 127 | } 128 | pa_context_state_t state; 129 | pthread_mutex_unlock(&app.mutex); 130 | while((state = pa_context_get_state(app.pa_context)) != PA_CONTEXT_READY){ 131 | if(!PA_CONTEXT_IS_GOOD(state)) { 132 | pthread_mutex_lock(&app.mutex); 133 | goto sleep; 134 | } 135 | assert(PA_CONTEXT_IS_GOOD(state)); 136 | pa_threaded_mainloop_wait(app.pa_mainloop); 137 | } 138 | pthread_mutex_lock(&app.mutex); 139 | { 140 | pa_context_set_subscribe_callback(app.pa_context, &on_ctx_subscription, NULL); 141 | pa_subscription_mask_t submask = PA_SUBSCRIPTION_MASK_ALL; 142 | pa_operation *op = pa_context_subscribe(app.pa_context, submask, &cb_success_signal, app.pa_mainloop); 143 | 144 | pa_operation_state_t opstate; 145 | pthread_mutex_unlock(&app.mutex); 146 | while ((opstate = pa_operation_get_state(op)) == PA_OPERATION_RUNNING) { 147 | pa_threaded_mainloop_wait(app.pa_mainloop); 148 | } 149 | pthread_mutex_lock(&app.mutex); 150 | pa_operation_unref(op); 151 | if(opstate != PA_OPERATION_DONE) 152 | goto sleep; 153 | } 154 | 155 | atomic_store(&app.should_refresh, true); 156 | pa_threaded_mainloop_signal(app.pa_mainloop, false); 157 | 158 | sleep: 159 | pthread_mutex_unlock(&app.mutex); 160 | // pa_threaded_mainloop_unlock 161 | pthread_cleanup_pop(true); 162 | sleep(2); 163 | } 164 | return NULL; 165 | } 166 | 167 | pa_operation *entry_set_volume(Entry ent, const pa_cvolume *volume) { 168 | #define OP(name) pa_context_set_##name(app.pa_context, ent.pa_index, volume, &cb_success_signal, NULL) 169 | switch (ent.type) { 170 | case ENTRY_SINKINPUT: 171 | return OP(sink_input_volume); 172 | case ENTRY_SOURCEOUTPUT: 173 | return OP(source_output_volume); 174 | case ENTRY_SINK: 175 | return OP(sink_volume_by_index); 176 | case ENTRY_SOURCE: 177 | return OP(source_volume_by_index); 178 | case ENTRY_CARD: 179 | return NULL; 180 | } 181 | __builtin_unreachable(); 182 | #undef OP 183 | } 184 | 185 | pa_operation *entry_set_muted(Entry ent, bool mute) { 186 | #define OP(name) pa_context_set_##name(app.pa_context, ent.pa_index, mute, &cb_success_signal, NULL) 187 | switch (ent.type) { 188 | case ENTRY_SINKINPUT: 189 | return OP(sink_input_mute); 190 | case ENTRY_SOURCEOUTPUT: 191 | return OP(source_output_mute); 192 | case ENTRY_SINK: 193 | return OP(sink_mute_by_index); 194 | case ENTRY_SOURCE: 195 | return OP(source_mute_by_index); 196 | case ENTRY_CARD: 197 | return NULL; 198 | } 199 | __builtin_unreachable(); 200 | #undef OP 201 | } 202 | 203 | void collect_sink_indices(pa_context *ctx, const pa_sink_info *i, int eol, void *userdata) { 204 | (void)ctx; 205 | if (eol) { 206 | assert(i == NULL); 207 | pa_threaded_mainloop_signal(app.pa_mainloop, false); 208 | return; 209 | } 210 | pthread_mutex_lock(&app.mutex); 211 | uint32_t **ptr = (uint32_t **)userdata; 212 | *((*ptr)++) = i->index; 213 | pthread_mutex_unlock(&app.mutex); 214 | } 215 | 216 | void collect_source_indices(pa_context *ctx, const pa_source_info *i, int eol, void *userdata) { 217 | (void)ctx; 218 | if (eol) { 219 | assert(i == NULL); 220 | pa_threaded_mainloop_signal(app.pa_mainloop, false); 221 | return; 222 | } 223 | pthread_mutex_lock(&app.mutex); 224 | uint32_t **ptr = (uint32_t **)userdata; 225 | *((*ptr)++) = i->index; 226 | pthread_mutex_unlock(&app.mutex); 227 | } 228 | 229 | #define RUN_OPERATION_OR_RETURN(operation, ostate, or_return) \ 230 | do { \ 231 | assert(operation != NULL); \ 232 | pthread_mutex_unlock(&app.mutex); \ 233 | while(((ostate) = pa_operation_get_state(operation)) == PA_OPERATION_RUNNING) \ 234 | pa_threaded_mainloop_wait(app.pa_mainloop); \ 235 | pthread_mutex_lock(&app.mutex); \ 236 | pa_operation_unref(operation); \ 237 | if((ostate) == PA_OPERATION_CANCELLED) { \ 238 | return or_return; \ 239 | }\ 240 | assert((ostate) == PA_OPERATION_DONE);\ 241 | } while(0) 242 | 243 | // caller should hold mainloop and app-mutex 244 | // return false on failure 245 | static bool drain_input_queue(const Config *cfg) { 246 | for (size_t i = 0; i < app.input_queue.len; i++) { 247 | InputEvent evt = app.input_queue.items[i]; 248 | Action act = cfg->keymap[evt.keycode]; 249 | if (act.type == ACTION_QUIT) { 250 | app.running = false; 251 | continue; 252 | } 253 | if (act.type == ACTION_SELECT_TAB) { 254 | app.entry_page = cfg->keymap[evt.keycode].data.tab; 255 | app.selected_entry = 0; 256 | app.selected_channel = 0; 257 | atomic_store(&app.should_refresh, true); 258 | continue; 259 | } 260 | if (act.type == ACTION_DEVICE_NEXT || act.type == ACTION_DEVICE_PREV) { 261 | Entry ent = app.entries.items[app.selected_entry]; 262 | int off = act.type == ACTION_DEVICE_NEXT ? 1 : -1; 263 | 264 | switch (ent.type) { 265 | case ENTRY_SINKINPUT: 266 | case ENTRY_SOURCEOUTPUT: { 267 | if (ent.data.device.index == PA_INVALID_INDEX) 268 | break; 269 | uint32_t device_list[512] = {0}; 270 | for (size_t i = 0; i < sizeof(device_list) / sizeof(*device_list); i++) 271 | device_list[i] = PA_INVALID_INDEX; 272 | 273 | uint32_t *end = device_list; 274 | pa_operation *op; 275 | pa_operation_state_t state; 276 | if (ent.type == ENTRY_SINKINPUT) 277 | op = pa_context_get_sink_info_list(app.pa_context, &collect_sink_indices, &end); 278 | else 279 | op = pa_context_get_source_info_list(app.pa_context, &collect_source_indices, &end); 280 | 281 | assert(op != NULL); 282 | 283 | RUN_OPERATION_OR_RETURN(op, state, false); 284 | int device_count = (uintptr_t)(end - device_list); 285 | assert((size_t)device_count <= sizeof(device_list) / sizeof(*device_list)); 286 | 287 | int current_index = -1; 288 | for (int i = 0; i < device_count; i++) { 289 | if (ent.data.device.index != device_list[i]) 290 | continue; 291 | current_index = i; 292 | break; 293 | } 294 | assert(current_index >= 0); 295 | 296 | int idev = (current_index + off) % device_count; 297 | if(idev == -1) 298 | idev = device_count - 1; 299 | assert(idev >= 0); 300 | assert(idev < device_count); 301 | uint32_t new_device = device_list[idev]; 302 | if (ent.type == ENTRY_SINKINPUT) 303 | op = pa_context_move_sink_input_by_index(app.pa_context, ent.pa_index, new_device, &cb_success_signal, NULL); 304 | else 305 | op = pa_context_move_source_output_by_index(app.pa_context, ent.pa_index, new_device, &cb_success_signal, NULL); 306 | assert(op != NULL); 307 | RUN_OPERATION_OR_RETURN(op, state, false); 308 | break; 309 | } 310 | case ENTRY_SINK: 311 | case ENTRY_SOURCE: 312 | case ENTRY_CARD: { 313 | if (ent.data.ports.current == -1) 314 | break; 315 | assert((int)ent.data.ports.len > ent.data.ports.current); 316 | int next = ((ent.data.ports.current + off) % ent.data.ports.len); 317 | if (next == ent.data.ports.current) { 318 | assert(ent.data.ports.len == 1); 319 | break; 320 | } 321 | const char *name = ent.data.ports.items[next].name; 322 | 323 | pa_operation *op; 324 | pa_operation_state_t state; 325 | if (ent.type == ENTRY_SINK) 326 | op = pa_context_set_sink_port_by_name(app.pa_context, ent.name, name, &cb_success_signal, NULL); 327 | else if (ent.type == ENTRY_SOURCE) 328 | op = pa_context_set_source_port_by_name(app.pa_context, ent.name, name, &cb_success_signal, NULL); 329 | else 330 | op = pa_context_set_card_profile_by_name(app.pa_context, ent.name, name, &cb_success_signal, NULL); 331 | assert(op != NULL); 332 | 333 | RUN_OPERATION_OR_RETURN(op, state, false); 334 | } 335 | } 336 | continue; 337 | } 338 | if (act.type == ACTION_ENTRY_NEXT || act.type == ACTION_ENTRY_PREV) { 339 | int off = act.type == ACTION_ENTRY_NEXT ? 1 : -1; 340 | bool entry_bounds = app.selected_entry + off < 0 || app.selected_entry + off >= (int)app.entries.len; 341 | Entry ent = app.entries.items[app.selected_entry]; 342 | if (ent.volume_lock && !entry_bounds) { 343 | app.selected_entry += off; 344 | Entry other = app.entries.items[app.selected_entry]; 345 | if (other.volume_lock) 346 | app.selected_channel = 0; 347 | else if (off < 0) 348 | app.selected_channel = other.volume.channels - 1; 349 | } else { 350 | if (off > 0 && ent.volume.channels > app.selected_channel + off) { 351 | app.selected_channel += off; 352 | } else if (off < 0 && app.selected_channel > 0) { 353 | app.selected_channel += off; 354 | } else if (!entry_bounds) { 355 | app.selected_entry += off; 356 | Entry other = app.entries.items[app.selected_entry]; 357 | if (other.volume_lock) 358 | app.selected_channel = 0; 359 | else if (off < 0) 360 | app.selected_channel = other.volume.channels - 1; 361 | } 362 | } 363 | atomic_store(&app.should_refresh, true); 364 | continue; 365 | } 366 | if (act.type == ACTION_LOCK_TOGGLE) { 367 | Entry *ent = &app.entries.items[app.selected_entry]; 368 | if (ent->volume.channels == 0) 369 | continue; 370 | ent->volume_lock = !ent->volume_lock; 371 | app.selected_channel = 0; 372 | atomic_store(&app.should_refresh, true); 373 | continue; 374 | } 375 | if (act.type == ACTION_MUTE_TOGGLE) { 376 | Entry ent = app.entries.items[app.selected_entry]; 377 | pa_operation *op = entry_set_muted(ent, !ent.muted); 378 | if (op == NULL) 379 | continue; 380 | pa_operation_state_t state; 381 | RUN_OPERATION_OR_RETURN(op, state, false); 382 | continue; 383 | } 384 | if (act.type == ACTION_VOLUME_SET || act.type == ACTION_VOLUME_ADD) { 385 | Entry ent = app.entries.items[app.selected_entry]; 386 | if (ent.volume.channels == 0) 387 | continue; 388 | pa_volume_t newvol; 389 | if (act.type == ACTION_VOLUME_SET) { 390 | if (ent.volume_lock) { 391 | newvol = (pa_volume_t)((float)PA_VOLUME_NORM * act.data.volume); 392 | pa_cvolume_set(&ent.volume, ent.volume.channels, newvol); 393 | } else { 394 | assert(app.selected_channel >= 0 && app.selected_channel < ent.volume.channels); 395 | ent.volume.values[app.selected_channel] = PA_VOLUME_NORM * act.data.volume; 396 | } 397 | } else { 398 | int64_t delta = PA_VOLUME_NORM * act.data.volume; 399 | int64_t volume = (int64_t)(ent.volume_lock ? pa_cvolume_avg(&ent.volume) : ent.volume.values[app.selected_channel]); 400 | volume += delta; 401 | if(volume < PA_VOLUME_MUTED) 402 | volume = PA_VOLUME_MUTED; 403 | else if(volume > (int)(PA_VOLUME_NORM * 1.5f)) 404 | volume = (int)(PA_VOLUME_NORM * 1.5f); 405 | if (ent.volume_lock) { 406 | pa_cvolume_set(&ent.volume, ent.volume.channels, volume); 407 | } else { 408 | assert(app.selected_channel >= 0 && app.selected_channel < ent.volume.channels); 409 | ent.volume.values[app.selected_channel] = volume; 410 | } 411 | } 412 | 413 | pa_operation *op = entry_set_volume(ent, &ent.volume); 414 | assert(op != NULL); 415 | pa_operation_state_t state; 416 | RUN_OPERATION_OR_RETURN(op, state, false); 417 | atomic_store(&app.should_refresh, true); 418 | continue; 419 | } 420 | } 421 | app.input_queue.len = 0; 422 | return true; 423 | } 424 | 425 | int main(void) { 426 | Config cfg = {0}; 427 | do { 428 | const char *home = getenv("HOME"); 429 | const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); 430 | const char *xdg_config_dirs = getenv("XDG_CONFIG_DIRS"); 431 | 432 | char config_path[PATH_MAX]; 433 | if(xdg_config_home != NULL) 434 | snprintf(config_path, PATH_MAX - 1, "%s/pamix.conf", xdg_config_home); 435 | else 436 | snprintf(config_path, PATH_MAX - 1, "%s/.config/pamix.conf", home); 437 | 438 | if(config_load(&cfg, config_path) == 0) 439 | break; 440 | 441 | if(xdg_config_dirs == NULL) 442 | xdg_config_dirs = "/etc/xdg"; 443 | 444 | snprintf(config_path, PATH_MAX - 1, "%s/pamix.conf", xdg_config_dirs); 445 | if(config_load(&cfg, config_path) == 0) 446 | break; 447 | 448 | config_default(&cfg); 449 | } while(0); 450 | 451 | pa_threaded_mainloop *mainloop = pa_threaded_mainloop_new(); 452 | assert(mainloop != NULL); 453 | 454 | pa_threaded_mainloop_lock(mainloop); 455 | if (pa_threaded_mainloop_start(mainloop) == -1) { 456 | fprintf(stderr, "could not start mainloop\n"); 457 | return 1; 458 | } 459 | 460 | pa_threaded_mainloop_unlock(mainloop); 461 | 462 | // we pass NULL as pa_context* and let the reconnect thread handle it 463 | app_init(&app, NULL, mainloop); 464 | atomic_store(&app.should_refresh, true); 465 | app.entry_page = ENTRY_SINKINPUT; 466 | 467 | { 468 | setlocale(LC_ALL, ""); 469 | initscr(); 470 | nodelay(stdscr, true); 471 | set_escdelay(25); 472 | curs_set(0); 473 | keypad(stdscr, true); 474 | meta(stdscr, true); 475 | noecho(); 476 | 477 | start_color(); 478 | int background = use_default_colors() ? 0 : -1; 479 | init_pair(1, COLOR_GREEN, background); 480 | init_pair(2, COLOR_YELLOW, background); 481 | init_pair(3, COLOR_RED, background); 482 | } 483 | 484 | signal(SIGWINCH, on_signal_resize); 485 | 486 | struct EntLine { 487 | uint32_t entry; 488 | uint32_t line; 489 | }; 490 | struct EntLines { 491 | struct EntLine *items; 492 | size_t len; 493 | size_t cap; 494 | }; 495 | struct EntLines entry_lines = {0}; 496 | 497 | pthread_t input_thread; 498 | pthread_t reconnect_thread; 499 | int pthread_status = pthread_create(&input_thread, NULL, &input_thread_main, NULL); 500 | pthread_status |= pthread_create(&reconnect_thread, NULL, &reconnect_thread_main, NULL); 501 | if(pthread_status != 0) { 502 | fprintf(stderr, "failed to create threads\n"); 503 | exit(1); 504 | } 505 | 506 | while (app.running) { 507 | { 508 | pa_threaded_mainloop_lock(mainloop); 509 | pthread_mutex_lock(&app.mutex); 510 | 511 | if(app.pa_context == NULL || pa_context_get_state(app.pa_context) != PA_CONTEXT_READY) { 512 | erase(); 513 | mvprintw(0, 0, "Waiting for PulseAudio connection..."); 514 | refresh(); 515 | for(size_t i = 0; i < app.input_queue.len; i++) { 516 | Action action = cfg.keymap[app.input_queue.items[i].keycode]; 517 | if(action.type == ACTION_QUIT) { 518 | app.running = false; 519 | break; 520 | } 521 | } 522 | if(!app.running) { 523 | pthread_mutex_unlock(&app.mutex); 524 | pa_threaded_mainloop_unlock(app.pa_mainloop); 525 | break; 526 | } 527 | app.input_queue.len = 0; 528 | pthread_mutex_unlock(&app.mutex); 529 | pa_threaded_mainloop_wait(app.pa_mainloop); 530 | pa_threaded_mainloop_unlock(app.pa_mainloop); 531 | continue; 532 | } 533 | 534 | bool ok = drain_input_queue(&cfg); 535 | if(!ok) { 536 | pthread_mutex_unlock(&app.mutex); 537 | pa_threaded_mainloop_unlock(app.pa_mainloop); 538 | continue; 539 | } 540 | 541 | pthread_mutex_unlock(&app.mutex); 542 | pa_threaded_mainloop_unlock(mainloop); 543 | } 544 | if (atomic_exchange(&app.resized, false)) { 545 | pthread_mutex_lock(&app.mutex); 546 | endwin(); 547 | refresh(); 548 | pthread_mutex_unlock(&app.mutex); 549 | atomic_store(&app.should_refresh, true); 550 | } 551 | if (atomic_exchange(&app.should_refresh, false)) { 552 | bool ok = app_refresh_entries(&app); 553 | if(!ok) { 554 | continue; 555 | } 556 | pthread_mutex_lock(&app.mutex); 557 | app.scroll = compute_entry_scroll(); 558 | erase(); 559 | 560 | const char *entry_type_names[] = {"Playback", "Recording", "Output Devices", "Input Devices", "Cards"}; 561 | move(0, 1); 562 | printw("%d/%zu", app.selected_entry + 1, app.entries.len); 563 | mvaddstr(0, 10, entry_type_names[app.entry_page]); 564 | 565 | int line = 1; 566 | entry_lines.len = 0; 567 | for (size_t i = app.scroll; i < app.entries.len; i++) { 568 | line++; 569 | Entry *ent = &app.entries.items[i]; 570 | 571 | bool selected = app.selected_entry == (int)i; 572 | int entsize = 1; 573 | if (line + entsize + 2 > LINES) { 574 | assert(!selected || (int)i == app.scroll); 575 | break; 576 | } 577 | 578 | struct line_expect __attribute__((cleanup(line_expect_check))) expect = { 579 | .begin = line, 580 | .end = &line, 581 | .expected = expected_entry_lines(ent), 582 | }; 583 | 584 | int width = COLS - 33; 585 | int x = 32; 586 | // volume control bars 587 | if (ent->volume_lock && ent->volume.channels > 0) { 588 | move(line, 1); 589 | if (app.selected_entry == (int)i) { 590 | addstr(">"); 591 | } 592 | pa_volume_t vol = pa_cvolume_avg(&ent->volume); 593 | char buf[30]; 594 | pa_sw_volume_snprint_dB(buf, sizeof(buf) - 1, vol); 595 | double pct = vol / (double)PA_VOLUME_NORM; 596 | addstr(buf); 597 | printw(" (%.2lf)", pct); 598 | draw_volume_bar(line++, x, width, vol); 599 | } else { 600 | for (uint8_t j = 0; j < ent->volume.channels; j++) { 601 | if (app.selected_entry == (int)i && app.selected_channel == j) { 602 | mvaddstr(line, 1, ">"); 603 | } 604 | const char *channel_name = pa_channel_position_to_pretty_string(ent->channel_map.map[j]); 605 | mvaddstr(line, 3, channel_name); 606 | draw_volume_bar(line++, x, width, ent->volume.values[j]); 607 | } 608 | } 609 | 610 | // peak volume bar 611 | if(ent->type != ENTRY_CARD) { 612 | pa_volume_t peak = ent->peak * PA_VOLUME_NORM; 613 | if (ent->monitor_stream == NULL) 614 | peak = PA_VOLUME_MUTED; 615 | struct EntLine el = {.entry = ent->pa_index, .line = (uint32_t)line}; 616 | da_append(&entry_lines, el); 617 | draw_volume_bar(line++, 1, COLS - 2, peak); 618 | } 619 | 620 | // entry name 621 | if (selected) 622 | attron(A_STANDOUT); 623 | switch (ent->type) { 624 | case ENTRY_SINKINPUT: 625 | mvaddstr(line, 1, pa_proplist_gets(ent->props, PA_PROP_APPLICATION_NAME)); 626 | break; 627 | case ENTRY_SINK: 628 | case ENTRY_SOURCE: 629 | mvaddstr(line, 1, pa_proplist_gets(ent->props, PA_PROP_DEVICE_DESCRIPTION)); 630 | printw(" %s", pa_proplist_gets(ent->props, PA_PROP_DEVICE_PROFILE_DESCRIPTION)); 631 | break; 632 | case ENTRY_CARD: 633 | mvaddstr(line, 1, pa_proplist_gets(ent->props, PA_PROP_DEVICE_DESCRIPTION)); 634 | break; 635 | default: 636 | mvaddstr(line, 1, ent->name); 637 | break; 638 | } 639 | attroff(A_STANDOUT); 640 | if (ent->volume_lock) 641 | printw(" 🔒"); 642 | if (ent->muted) 643 | printw(" 🔇"); 644 | if (ent->corked) 645 | printw(" ⏸"); 646 | 647 | // device/port/profile display 648 | switch (ent->type) { 649 | case ENTRY_SINKINPUT: 650 | case ENTRY_SOURCEOUTPUT: { 651 | char buf[256]; 652 | int dev_len = 0; 653 | assert(ent->data.device.name != NULL); 654 | if (ent->data.device.name != NULL) 655 | dev_len = snprintf(buf, sizeof(buf) - 1, "%s", ent->data.device.name); 656 | 657 | int name_len = strlen(ent->name); 658 | int max_name = COLS - 1 - dev_len - 4; 659 | 660 | x = getcurx(stdscr); 661 | if(x < max_name) { 662 | // TODO: color 663 | attron(A_DIM); 664 | if(name_len > max_name - x) { 665 | printw(" %.*s...", max_name - x - 3, ent->name); 666 | } else { 667 | printw(" %s", ent->name); 668 | } 669 | attroff(A_DIM); 670 | } 671 | 672 | mvaddstr(line, COLS - 1 - dev_len, buf); 673 | break; 674 | } 675 | case ENTRY_CARD: 676 | case ENTRY_SINK: 677 | case ENTRY_SOURCE: { 678 | char buf[256]; 679 | int len; 680 | len = sprintf(buf, "%s", ent->data.ports.items[ent->data.ports.current].description); 681 | mvaddstr(line, COLS - 1 - len, buf); 682 | break; 683 | } 684 | default: 685 | break; 686 | } 687 | line++; 688 | } 689 | refresh(); 690 | pthread_mutex_unlock(&app.mutex); 691 | } else if (app.new_peaks) { 692 | pthread_mutex_lock(&app.mutex); 693 | app.new_peaks = false; 694 | for (size_t i = 0; i < entry_lines.len; i++) { 695 | struct EntLine el = entry_lines.items[i]; 696 | Entry *ent = NULL; 697 | for (size_t j = 0; j < app.entries.len; j++) { 698 | Entry *e = &app.entries.items[j]; 699 | if (e->pa_index == el.entry) { 700 | ent = e; 701 | break; 702 | } 703 | } 704 | assert(ent != NULL); 705 | pa_volume_t peak = ent->peak * PA_VOLUME_NORM; 706 | if (ent->monitor_stream == NULL) 707 | peak = PA_VOLUME_MUTED; 708 | draw_volume_bar(el.line, 1, COLS - 2, peak); 709 | } 710 | refresh(); 711 | pthread_mutex_unlock(&app.mutex); 712 | } 713 | 714 | if (!app.running) 715 | break; 716 | pa_threaded_mainloop_lock(mainloop); 717 | if (atomic_load(&app.should_refresh) || app.new_peaks || app.input_queue.len > 0) { 718 | pa_threaded_mainloop_unlock(mainloop); 719 | continue; 720 | } 721 | pa_threaded_mainloop_wait(app.pa_mainloop); 722 | pa_threaded_mainloop_unlock(mainloop); 723 | } 724 | 725 | pthread_join(input_thread, NULL); 726 | pthread_cancel(reconnect_thread); 727 | pthread_join(reconnect_thread, NULL); 728 | 729 | if(entry_lines.cap != 0) 730 | free(entry_lines.items); 731 | if(app.pa_context != NULL && PA_CONTEXT_IS_GOOD(pa_context_get_state(app.pa_context))){ 732 | pa_context_disconnect(app.pa_context); 733 | pa_threaded_mainloop_stop(app.pa_mainloop); 734 | pa_threaded_mainloop_free(app.pa_mainloop); 735 | } 736 | for(size_t i = 0; i < app.entries.len; i++) { 737 | entry_free(&app.entries.items[i]); 738 | } 739 | 740 | endwin(); 741 | return 0; 742 | } 743 | 744 | 745 | // compute new scroll so selected entry stays in view 746 | int compute_entry_scroll(void) { 747 | int scroll = app.scroll; 748 | if (scroll > app.selected_entry) 749 | return app.selected_entry; 750 | 751 | int entry_sizes[app.entries.len]; 752 | for (size_t i = 0; i < app.entries.len; i++) { 753 | entry_sizes[i] = expected_entry_lines(&app.entries.items[i]); 754 | } 755 | 756 | int line = 2; 757 | for (size_t i = scroll; i < app.entries.len; i++) { 758 | line += entry_sizes[i] + 1; 759 | if ((int)i < scroll || app.selected_entry != (int)i) 760 | continue; 761 | if (line > LINES) { 762 | int backscroll = 0; 763 | size_t j = scroll; 764 | for (; j < i && line - backscroll > LINES; j++) 765 | backscroll += entry_sizes[j]; 766 | scroll = (int)j; 767 | } 768 | break; 769 | } 770 | 771 | return scroll; 772 | } 773 | --------------------------------------------------------------------------------