pax_global_header00006660000000000000000000000064147220355120014513gustar00rootroot0000000000000052 comment=82d5ff19178b15577c0a56b6030bc65f0684567c PAmix-2.0/000077500000000000000000000000001472203551200123725ustar00rootroot00000000000000PAmix-2.0/.clangd000066400000000000000000000001611472203551200136210ustar00rootroot00000000000000CompileFlags: Add: [-xc, -std=c99, -Wall, -Wextra, -Werror, -pedantic, -D_DEFAULT_SOURCE, -D_XOPEN_SOURCE=600] PAmix-2.0/.gitignore000066400000000000000000000000431472203551200143570ustar00rootroot00000000000000build/ .vscode .idea cmake-build-* PAmix-2.0/CMakeLists.txt000066400000000000000000000011431472203551200151310ustar00rootroot00000000000000cmake_minimum_required(VERSION 2.8) project(pamix C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_EXTENSIONS ON) file(GLOB_RECURSE pamix_SRC "src/*.h" "src/*.c") include_directories("src") link_libraries("pulse" "pthread") find_package(PkgConfig REQUIRED QUIET) pkg_search_module(NCURSESW REQUIRED ncursesw) link_libraries(${NCURSESW_LIBRARIES}) add_definitions(${NCURSESW_CFLAGS} ${NCURSESW_CFLAGS_OTHER}) add_executable(pamix ${pamix_SRC}) install(FILES pamix.conf DESTINATION /etc/xdg) install(TARGETS pamix DESTINATION bin) install(FILES man/pamix.1 TYPE MAN) PAmix-2.0/LICENSE000066400000000000000000000020621472203551200133770ustar00rootroot00000000000000The MIT License Copyright (c) 2016 Joshua Jensch Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PAmix-2.0/README.md000066400000000000000000000022121472203551200136460ustar00rootroot00000000000000# PAMix - the pulseaudio terminal mixer ![alt tag](http://i.imgur.com/NuzrAXZ.gif) ## Dependencies: # ### Build ## * cmake * pkg-config ### Runtime ## * PulseAudio * Ncursesw ## Building and Installing ```bash mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=RELEASE make sudo make install ``` # Configuration # PAmix keybindings are configured in `$XDG_CONFIG_HOME/pamix.conf` (see [**Configuration**](https://github.com/patroclos/PAmix/wiki/Configuration) for detailed instructions) # Default Keybindings # (arrow keys are also supported instead of hjkl) | Action | Key | |----------------------------|-----| | Playback tab | F1 | | Recording Tab | F2 | | Output Devices | F3 | | Input Devices | F4 | | Cards | F5 | | Set volume to percentage | 0-9 | | Decrease Volume | h | | Select Next | j | | Select Previous | k | | Increase Volume | l | | (Un)Lock Channels | c | | (Un)Mute | m | | Next/Previous device/port | s/S | | Quit | q | PAmix-2.0/man/000077500000000000000000000000001472203551200131455ustar00rootroot00000000000000PAmix-2.0/man/pamix.1000066400000000000000000000053071472203551200143520ustar00rootroot00000000000000.\" this is the manpage of the pamix pulseaudio ncurses mixer .TH pamix 1 "27 Nov 2024" "V" "pamix man page" .SH DESCRIPTION PAmix is a pavucontrol inspired ncurses based pulseaudio mixer .SH CONFIGURATION pamix is configured using a file called pamix.conf inside the $XDG_CONFIG_HOME or $HOME/.config, should it not be set. .br .start .SH COMMANDS .PP PAmix conf files support the following commands: .br * bind .SH bind .PP \fBSYNOPSIS:\fP bind KEYNAME MIXER\-COMMAND [ARGUMENT] .PP bind is used to bind a keyname to a mixer\-command. .br Some mixer\-commands require an argument. .br You can bind a keyname to multiple mixer\-commands. .SH PAMIX\-COMMANDS .PP Pamix\-Commands can be bound to keys using the bind command and are used to interact with pamix. .br The following pamix\-commands are currently supported: .SH quit .PP quit will cause PAmix to exit and takes no arguments. .SH select\-tab .PP select\-tab will select one of the following tabs: Output Devices, Input Devices, Playback, Recording, Cards .br select\-tab takes the number of the tab to switch to starting at 0 in the order mentioned. .SH select\-next and select\-prev .PP these commands are given the optional argument 'channel' they will select the next and previous channels. if no argument is given they will select the next and previous entry in the displayed tab. .SH set\-volume .PP this command takes the targetvalue in form of a double as an argument. .br depending on whether channels are locked, this command will set the volume of the selected entry/channel to the targetvalue given in the argument. .br \fIExample:\fP bind 0 set\-volume 1.0 \fI; this will set the volume to 100%\fP .SH add\-volume .PP this command takes a deltavalue in form of a double as an argument. .br the deltavalue can be negative \fIExample:\fP bind h add\-volume \-0.05 \fI; this will reduce the volume by 5%\fP .SH cycle\-next and cycle\-prev .PP these commands will change the device or port of the currently selected entry. .br they dont take any arguments. .SH toggle\-lock .PP this command toggles whether channels should be locked together for the currently selected entry .br and takes no arguments. .SH toggle\-mute .PP toggles whether the currently selected entry is muted .br and takes no arguments. .stop .SH DEFAULT CONFIGURATION Keybindings: .br F1 show Playback tab .br F2 show Recording tab .br F3 show Output devices tab .br F4 show Input devices tab .br F5 show Cards tab .br 0-9 set volume to percentage (10%-100%) .br j/down select next channel or entry .br k/up select previous channel or entry .br h/left decrease volume .br l/right increase volume .br c un/lock channels .br s/S select next/previous device/port PAmix-2.0/pamix.conf000066400000000000000000000020571472203551200143630ustar00rootroot00000000000000; This is a sample configuration file for pamix (https://github.com/patroclos/PAmix) implementing the default configuration ; BINDING KEYS ; see `man keyname` for reference for special keynames/combinations bind q quit ; Navigation bind KEY_F(1) select-tab playback bind KEY_F(2) select-tab recording bind KEY_F(3) select-tab output bind KEY_F(4) select-tab input bind KEY_F(5) select-tab cards bind j select-next bind KEY_DOWN select-next bind k select-prev bind KEY_UP select-prev ; Volume Control bind h add-volume -0.05 bind KEY_LEFT add-volume -0.05 bind l add-volume 0.05 bind KEY_RIGHT add-volume 0.05 bind 1 set-volume 0.1 bind 2 set-volume 0.2 bind 3 set-volume 0.3 bind 4 set-volume 0.4 bind 5 set-volume 0.5 bind 6 set-volume 0.6 bind 7 set-volume 0.7 bind 8 set-volume 0.8 bind 9 set-volume 0.9 bind 0 set-volume 1.0 ; cycle-next/prev will select the next/previous device or port ; for the currently selected entry bind s cycle-next bind S cycle-prev ; toggle-lock toggles whether channels are locked together bind c toggle-lock bind m toggle-mute PAmix-2.0/src/000077500000000000000000000000001472203551200131615ustar00rootroot00000000000000PAmix-2.0/src/app.c000066400000000000000000000465561472203551200141250ustar00rootroot00000000000000#include "app.h" #include "da.h" #include #include #include static void entry_data_free(Entry *entry); App app = {0}; struct entry_indices { uint32_t *indices; int count; }; static struct entry_indices *cmp_entry_indices; // this cmp function splits entries into two groups: corked and uncorked. The sorting is stabelized by passing the // original order as the context argument, so order within groups doesnt change, only the uncorked streams are brought // to the front. // `cmd_entry_indices` needs to be set when running qsort. This is not threadsafe. static int cmp_entry(const void *pa, const void *pb) { const Entry *a = pa; const Entry *b = pb; assert(a->pa_index != b->pa_index); if(a->corked != b->corked) { return a->corked - b->corked; } struct entry_indices *indices = cmp_entry_indices; int ia = -1; int ib = -1; for(int i = 0; i < indices->count; i++) { if(indices->indices[i] == a->pa_index) ia = i; else if(indices->indices[i] == b->pa_index) ib = i; if(ia != -1 && ib != -1) break; } assert(ia != -1); assert(ib != -1); return ia - ib; } static void cull_entries(Entries *ents) { for (int i = (int)ents->len - 1; i >= 0; i--) { Entry *ent = &ents->items[i]; if (!ents->items[i].marked) continue; entry_free(ent); memmove(ents->items + i, ents->items + i + 1, (ents->len - i - 1) * sizeof(Entry)); ents->len--; } } static void cb_monitor_read(pa_stream *stream, size_t nbytes, void *pdata) { uint32_t index = (uintptr_t)pdata; const void *data; int err = pa_stream_peek(stream, &data, &nbytes); if (err != 0) { return; } assert(nbytes >= sizeof(float)); assert((nbytes % sizeof(float)) == 0); float last_peak = ((float *)data)[nbytes / sizeof(float) - 1]; pa_stream_drop(stream); pthread_mutex_lock(&app.mutex); for (size_t i = 0; i < app.entries.len; i++) { Entry *ent = &app.entries.items[i]; if (ent->pa_index == index || ent->monitor_index == index) { if (ent->monitor_stream != stream) break; assert(ent->monitor_stream == stream); ent->peak = last_peak; app.new_peaks = true; pa_threaded_mainloop_signal(app.pa_mainloop, false); break; } } pthread_mutex_unlock(&app.mutex); } static void cb_monitor_state(pa_stream *stream, void *data) { (void)data; pa_stream_state_t state = pa_stream_get_state(stream); if (state == PA_STREAM_FAILED || state == PA_STREAM_TERMINATED) { pthread_mutex_lock(&app.mutex); for (size_t i = 0; i < app.entries.len; i++) { Entry *ent = &app.entries.items[i]; if (ent->monitor_stream == stream) { ent->monitor_stream = NULL; ent->peak = 0; break; } } pthread_mutex_unlock(&app.mutex); return; } if (state == PA_STREAM_READY) { pthread_mutex_lock(&app.mutex); int idx = -1; for (size_t i = 0; i < app.entries.len; i++) { Entry *ent = &app.entries.items[i]; if (ent->monitor_stream == stream) { idx = i; break; } } pthread_mutex_unlock(&app.mutex); if (idx == -1) { pa_stream_disconnect(stream); pa_stream_unref(stream); } } } static pa_stream *create_monitor(pa_context *ctx, uint32_t monitor_stream, uint32_t device) { char stream_name[32]; snprintf(stream_name, sizeof(stream_name) - 1, "PeakMonitor %d", monitor_stream); pa_sample_spec spec = {.rate = 40, .format = PA_SAMPLE_FLOAT32LE, .channels = 1}; pa_proplist *props = pa_proplist_new(); // hide monitor stream from pavucontrol pa_proplist_sets(props, PA_PROP_APPLICATION_ID, "org.PulseAudio.pavucontrol"); pa_stream *stream = pa_stream_new_with_proplist(ctx, stream_name, &spec, NULL, props); pa_proplist_free(props); assert(stream != NULL); char devname[16]; if (monitor_stream != PA_INVALID_INDEX) { assert(device == PA_INVALID_INDEX); int err = pa_stream_set_monitor_stream(stream, monitor_stream); if (err != 0) { pa_stream_unref(stream); return NULL; } } else { assert(device != PA_INVALID_INDEX); sprintf(devname, "%u", device); } pa_stream_set_read_callback(stream, &cb_monitor_read, (void *)(uintptr_t)(monitor_stream != PA_INVALID_INDEX ? monitor_stream : device)); pa_stream_set_state_callback(stream, &cb_monitor_state, NULL); pa_stream_flags_t flags = (pa_stream_flags_t)(PA_STREAM_DONT_MOVE | PA_STREAM_PEAK_DETECT | PA_STREAM_ADJUST_LATENCY); pa_buffer_attr bufattr = {.maxlength = 128, .fragsize = sizeof(float)}; int err = pa_stream_connect_record(stream, device == PA_INVALID_INDEX ? NULL : devname, &bufattr, flags); if (err != 0) { pa_stream_unref(stream); return NULL; } return stream; } static int find_entry_with_index(uint32_t index, entry_type type) { for (int i = 0; i < (int)app.entries.len; i++) { Entry ent = app.entries.items[i]; if (ent.pa_index == index && ent.type == type) return i; } return -1; } uint32_t pa_entry_index(const void *info, entry_type type) { switch (type) { case ENTRY_SINKINPUT: return ((const pa_sink_input_info *)info)->index; case ENTRY_SOURCEOUTPUT: return ((const pa_source_output_info *)info)->index; case ENTRY_SINK: return ((const pa_sink_info *)info)->index; case ENTRY_SOURCE: return ((const pa_source_info *)info)->index; case ENTRY_CARD: return ((const pa_card_info *)info)->index; } __builtin_unreachable(); } const char *pa_entry_name(const void *info, entry_type type) { switch (type) { case ENTRY_SINKINPUT: return ((const pa_sink_input_info *)info)->name; case ENTRY_SOURCEOUTPUT: return ((const pa_source_output_info *)info)->name; case ENTRY_SINK: return ((const pa_sink_info *)info)->name; case ENTRY_SOURCE: return ((const pa_source_info *)info)->name; case ENTRY_CARD: return ((const pa_card_info *)info)->name; } __builtin_unreachable(); } pa_cvolume pa_entry_volume(const void *info, entry_type type) { switch (type) { case ENTRY_SINKINPUT: return ((const pa_sink_input_info *)info)->volume; case ENTRY_SOURCEOUTPUT: return ((const pa_source_output_info *)info)->volume; case ENTRY_SINK: return ((const pa_sink_info *)info)->volume; case ENTRY_SOURCE: return ((const pa_source_info *)info)->volume; case ENTRY_CARD: return (pa_cvolume){.channels = 0}; } __builtin_unreachable(); } bool pa_entry_corked(const void *info, entry_type type) { switch (type) { case ENTRY_SINKINPUT: return ((const pa_sink_input_info *)info)->corked; case ENTRY_SOURCEOUTPUT: return ((const pa_source_output_info *)info)->corked; case ENTRY_SINK: return false; case ENTRY_SOURCE: return false; case ENTRY_CARD: return false; } __builtin_unreachable(); } bool pa_entry_mute(const void *info, entry_type type) { switch (type) { case ENTRY_SINKINPUT: return ((const pa_sink_input_info *)info)->mute; case ENTRY_SOURCEOUTPUT: return ((const pa_source_output_info *)info)->mute; case ENTRY_SINK: return ((const pa_sink_info *)info)->mute; case ENTRY_SOURCE: return ((const pa_source_info *)info)->mute; case ENTRY_CARD: return false; } __builtin_unreachable(); } pa_proplist *pa_entry_proplist(const void *info, entry_type type) { switch (type) { case ENTRY_SINKINPUT: return ((const pa_sink_input_info *)info)->proplist; case ENTRY_SOURCEOUTPUT: return ((const pa_source_output_info *)info)->proplist; case ENTRY_SINK: return ((const pa_sink_info *)info)->proplist; case ENTRY_SOURCE: return ((const pa_source_info *)info)->proplist; case ENTRY_CARD: return ((const pa_card_info *)info)->proplist; } __builtin_unreachable(); } pa_channel_map pa_entry_channel_map(const void *info, entry_type type) { switch (type) { case ENTRY_SINKINPUT: return ((const pa_sink_input_info *)info)->channel_map; case ENTRY_SOURCEOUTPUT: return ((const pa_source_output_info *)info)->channel_map; case ENTRY_SINK: return ((const pa_sink_info *)info)->channel_map; case ENTRY_SOURCE: return ((const pa_source_info *)info)->channel_map; case ENTRY_CARD: return (pa_channel_map){.channels = 0}; } __builtin_unreachable(); } uint32_t pa_entry_monitor_index(const void *info, entry_type type) { switch (type) { case ENTRY_SINK: return ((const pa_sink_info *)info)->monitor_source; case ENTRY_SOURCE: return ((const pa_source_info *)info)->index; case ENTRY_SOURCEOUTPUT: return ((const pa_source_output_info *)info)->source; default: return PA_INVALID_INDEX; } } void apply_entry_data(union EntryData *data, const void *info, entry_type type) { switch (type) { case ENTRY_SINKINPUT: case ENTRY_SOURCEOUTPUT: { uint32_t device = type == ENTRY_SINKINPUT ? ((const pa_sink_input_info *)info)->sink : ((const pa_source_output_info *)info)->source; if (data->device.index != device) { data->device.index = device; if (data->device.name != NULL) { free((void *)data->device.name); data->device.name = NULL; } } break; } case ENTRY_SINK: { data->ports.current = -1; for (size_t i = 0; i < data->ports.len; i++) { free((void *)data->ports.items[i].name); free((void *)data->ports.items[i].description); } data->ports.len = 0; const pa_sink_info *si = ((const pa_sink_info *)info); for (uint32_t i = 0; i < si->n_ports; i++) { NameDesc port = { .name = strdup(si->ports[i]->name), .description = strdup(si->ports[i]->description), }; da_append(&data->ports, port); if (si->active_port == si->ports[i]) data->ports.current = i; } break; } case ENTRY_SOURCE: { data->ports.current = -1; for (size_t i = 0; i < data->ports.len; i++) { free((void *)data->ports.items[i].name); free((void *)data->ports.items[i].description); } data->ports.len = 0; const pa_source_info *si = ((const pa_source_info *)info); for (uint32_t i = 0; i < si->n_ports; i++) { NameDesc port = { .name = strdup(si->ports[i]->name), .description = strdup(si->ports[i]->description), }; da_append(&data->ports, port); if (si->active_port == si->ports[i]) data->ports.current = i; } break; } case ENTRY_CARD: { data->profiles.current = -1; for (size_t i = 0; i < data->profiles.len; i++) { free((void *)data->profiles.items[i].name); free((void *)data->profiles.items[i].description); } data->profiles.len = 0; const pa_card_info *si = ((const pa_card_info *)info); for (uint32_t i = 0; i < si->n_profiles; i++) { NameDesc profile = { .name = strdup(si->profiles2[i]->name), .description = strdup(si->profiles2[i]->description), }; da_append(&data->profiles, profile); if (si->active_profile2 == si->profiles2[i]) data->ports.current = i; } break; } default: __builtin_unreachable(); } } void app_entry_info(const void *info, entry_type type) { uint32_t index = pa_entry_index(info, type); const char *name = pa_entry_name(info, type); assert(info != NULL); pthread_mutex_lock(&app.mutex); int i = find_entry_with_index(index, type); if (i != -1) { Entry *entry = &app.entries.items[i]; assert(entry->type == type); if(strcmp(entry->name, name) != 0) { free((void*)entry->name); entry->name = strdup(name); } entry->marked = false; entry->volume = pa_entry_volume(info, type); entry->corked = pa_entry_corked(info, type); entry->channel_map = pa_entry_channel_map(info, type); entry->muted = pa_entry_mute(info, type); entry->monitor_index = pa_entry_monitor_index(info, type); if (entry->props != NULL) { pa_proplist_free(entry->props); } if (pa_entry_corked(info, type)) entry->peak = 0; entry->props = pa_proplist_copy(pa_entry_proplist(info, type)); apply_entry_data(&entry->data, info, type); } else { Entry ent = { .type = type, .name = strdup(name), .pa_index = index, .volume = pa_entry_volume(info, type), .channel_map = pa_entry_channel_map(info, type), .props = pa_proplist_copy(pa_entry_proplist(info, type)), .monitor_index = pa_entry_monitor_index(info, type), .muted = pa_entry_mute(info, type), .corked = pa_entry_corked(info, type), .volume_lock = type != ENTRY_CARD, }; apply_entry_data(&ent.data, info, type); da_append(&app.entries, ent); } pthread_mutex_unlock(&app.mutex); } void app_sink_input_info(pa_context *ctx, const pa_sink_input_info *info, int eol, void *data) { (void)ctx; (void)data; if (info == NULL) { if (eol) pa_threaded_mainloop_signal(app.pa_mainloop, false); return; } app_entry_info(info, ENTRY_SINKINPUT); } void app_source_output_info(pa_context *ctx, const pa_source_output_info *info, int eol, void *data) { (void)ctx; (void)data; if (info == NULL) { if (eol) pa_threaded_mainloop_signal(app.pa_mainloop, false); return; } // hide peak-detection streams const char *appname = pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_ID); if (appname != NULL && strcmp(appname, "org.PulseAudio.pavucontrol") == 0) { return; } app_entry_info(info, ENTRY_SOURCEOUTPUT); } void app_sink_info(pa_context *ctx, const pa_sink_info *info, int eol, void *data) { (void)ctx; (void)data; if (info == NULL) { if (eol) pa_threaded_mainloop_signal(app.pa_mainloop, false); return; } app_entry_info(info, ENTRY_SINK); } void app_source_info(pa_context *ctx, const pa_source_info *info, int eol, void *data) { (void)ctx; (void)data; if (info == NULL) { if (eol) pa_threaded_mainloop_signal(app.pa_mainloop, false); return; } // hide monitors const char *devtyp = pa_proplist_gets(info->proplist, PA_PROP_DEVICE_CLASS); if(devtyp != NULL && strcmp(devtyp, "monitor") == 0) { return; } app_entry_info(info, ENTRY_SOURCE); } void app_card_info(pa_context *ctx, const pa_card_info *info, int eol, void *data) { (void)ctx; (void)data; if (info == NULL) { if (eol) pa_threaded_mainloop_signal(app.pa_mainloop, false); return; } app_entry_info(info, ENTRY_CARD); } static void app_sink_info_name(pa_context *ctx, const pa_sink_info *i, int eol, void *userdata) { (void)ctx; if (eol) { assert(i == NULL); pa_threaded_mainloop_signal(app.pa_mainloop, false); return; } Entry *ent = userdata; ent->data.device.name = strdup(i->description); } static void app_source_info_name(pa_context *ctx, const pa_source_info *i, int eol, void *userdata) { (void)ctx; if (eol) { assert(i == NULL); pa_threaded_mainloop_signal(app.pa_mainloop, false); return; } Entry *ent = userdata; ent->data.device.name = strdup(i->description); } bool app_refresh_entries(App *app) { pa_threaded_mainloop_lock(app->pa_mainloop); pthread_mutex_lock(&app->mutex); if (app->pa_context == NULL || pa_context_get_state(app->pa_context) != PA_CONTEXT_READY) { pthread_mutex_unlock(&app->mutex); pa_threaded_mainloop_unlock(app->pa_mainloop); return false; } for (size_t i = 0; i < app->entries.len; i++) app->entries.items[i].marked = true; pa_operation *op; pa_operation_state_t state; switch (app->entry_page) { case ENTRY_SINKINPUT: op = pa_context_get_sink_input_info_list(app->pa_context, &app_sink_input_info, NULL); break; case ENTRY_SOURCEOUTPUT: op = pa_context_get_source_output_info_list(app->pa_context, &app_source_output_info, NULL); break; case ENTRY_SINK: op = pa_context_get_sink_info_list(app->pa_context, &app_sink_info, NULL); break; case ENTRY_SOURCE: op = pa_context_get_source_info_list(app->pa_context, &app_source_info, NULL); break; case ENTRY_CARD: op = pa_context_get_card_info_list(app->pa_context, &app_card_info, NULL); break; default: __builtin_unreachable(); } assert(op != NULL); pthread_mutex_unlock(&app->mutex); while ((state = pa_operation_get_state(op)) == PA_OPERATION_RUNNING) { pa_threaded_mainloop_wait(app->pa_mainloop); } pa_operation_unref(op); if(state == PA_OPERATION_CANCELLED) { pa_threaded_mainloop_unlock(app->pa_mainloop); return false; } pthread_mutex_lock(&app->mutex); assert(state == PA_OPERATION_DONE); cull_entries(&app->entries); uint32_t indexbuf[app->entries.len]; for(size_t i = 0; i < app->entries.len; i++) indexbuf[i] = app->entries.items[i].pa_index; struct entry_indices indices = {.count = (int)app->entries.len, .indices = indexbuf}; cmp_entry_indices = &indices; qsort(app->entries.items, app->entries.len, sizeof(*app->entries.items), cmp_entry); for (size_t i = 0; i < app->entries.len; i++) { Entry *ent = &app->entries.items[i]; // populate device name if ((ent->type == ENTRY_SINKINPUT || ent->type == ENTRY_SOURCEOUTPUT) && ent->data.device.name == NULL) { pa_operation *op; switch (ent->type) { case ENTRY_SINKINPUT: op = pa_context_get_sink_info_by_index(app->pa_context, ent->data.device.index, &app_sink_info_name, ent); break; case ENTRY_SOURCEOUTPUT: op = pa_context_get_source_info_by_index(app->pa_context, ent->data.device.index, &app_source_info_name, ent); break; default: __builtin_unreachable(); } assert(op != NULL); pa_operation_state_t state; pthread_mutex_unlock(&app->mutex); while ((state = pa_operation_get_state(op)) == PA_OPERATION_RUNNING) { pa_threaded_mainloop_wait(app->pa_mainloop); } pa_operation_unref(op); if(state == PA_OPERATION_CANCELLED) { pa_threaded_mainloop_unlock(app->pa_mainloop); return false; } pthread_mutex_lock(&app->mutex); assert(state == PA_OPERATION_DONE); assert(ent->data.device.name != NULL); } // ensure monitor stream exists if (ent->monitor_stream != NULL || ent->type == ENTRY_CARD) continue; // we exclude corked entries, because those monitor streams will be stuck in creating state, which can't be // disconnected yet, so it just accumulates dead streams when switching tabs if (ent->type == ENTRY_SINKINPUT && !ent->corked) { ent->monitor_stream = create_monitor(app->pa_context, ent->pa_index, PA_INVALID_INDEX); } if (ent->type == ENTRY_SOURCEOUTPUT && !ent->corked) { const char *appname = pa_proplist_gets(ent->props, PA_PROP_APPLICATION_ID); if (appname == NULL || strcmp(appname, "org.PulseAudio.pavucontrol") != 0) { ent->monitor_stream = create_monitor(app->pa_context, PA_INVALID_INDEX, ent->monitor_index); } } if ((ent->type == ENTRY_SINK || ent->type == ENTRY_SOURCE)) { ent->monitor_stream = create_monitor(app->pa_context, PA_INVALID_INDEX, ent->monitor_index); } } pthread_mutex_unlock(&app->mutex); pa_threaded_mainloop_unlock(app->pa_mainloop); return true; } void app_init(App *app, pa_context *context, pa_threaded_mainloop *mainloop) { app->pa_context = context; app->pa_mainloop = mainloop; app->entry_page = ENTRY_SINKINPUT; app->running = true; app->resized = ATOMIC_VAR_INIT(false); int err = pthread_mutex_init(&app->mutex, NULL); if(err != 0) { fprintf(stderr, "failed to create pthread mutex: %d\n", err); exit(1); } } static void entry_data_free(Entry *entry) { switch(entry->type) { case ENTRY_SINKINPUT: case ENTRY_SOURCEOUTPUT: if(entry->data.device.name != NULL) free((void*)entry->data.device.name); break; case ENTRY_SINK: case ENTRY_SOURCE: case ENTRY_CARD: for(size_t i = 0; i < entry->data.ports.len; i++) { free((void*)entry->data.ports.items[i].name); free((void*)entry->data.ports.items[i].description); } entry->data.ports.len = 0; entry->data.ports.current = -1; if(entry->data.ports.items != 0) { free(entry->data.ports.items); entry->data.ports.items = NULL; entry->data.ports.cap = 0; } break; } } void entry_free(Entry *entry) { if(entry->name != NULL) { free((void*)entry->name); entry->name = NULL; } if(entry->props != NULL){ pa_proplist_free(entry->props); entry->props = NULL; } entry_data_free(entry); if(entry->monitor_stream != NULL && pa_stream_get_state(entry->monitor_stream) == PA_STREAM_READY){ int err = pa_stream_disconnect(entry->monitor_stream); assert(err == 0); pa_stream_unref(entry->monitor_stream); entry->monitor_stream = NULL; } } PAmix-2.0/src/app.h000066400000000000000000000032761472203551200141220ustar00rootroot00000000000000#ifndef _APP_H #define _APP_H #include #include #include #include #include typedef enum { ENTRY_SINKINPUT, ENTRY_SOURCEOUTPUT, ENTRY_SINK, ENTRY_SOURCE, ENTRY_CARD } entry_type; typedef struct { const char *name; const char *description; } NameDesc; typedef struct { NameDesc *items; size_t len; size_t cap; int current; } NameDescs; union EntryData { // sink/source of sinkinput and sourceoutput entries // TODO: do we put device names in here? struct { uint32_t index; const char *name; } device; // ports of sink and source entries NameDescs ports; // profiles of card entries NameDescs profiles; }; typedef struct { entry_type type; const char *name; uint32_t pa_index; pa_cvolume volume; pa_channel_map channel_map; pa_proplist *props; pa_stream *monitor_stream; uint32_t monitor_index; float peak; bool muted; bool corked; bool marked; bool volume_lock; union EntryData data; } Entry; typedef struct { Entry *items; size_t len; size_t cap; } Entries; typedef struct { int keycode; const char *keyname; } InputEvent; typedef struct { InputEvent *items; size_t len; size_t cap; } InputQueue; typedef struct { pa_context *pa_context; pa_threaded_mainloop *pa_mainloop; Entries entries; entry_type entry_page; int selected_entry; int selected_channel; int scroll; pthread_mutex_t mutex; atomic_bool should_refresh; atomic_bool resized; //bool resized; bool new_peaks; bool running; InputQueue input_queue; } App; extern App app; void app_init(App *app, pa_context *context, pa_threaded_mainloop *mainloop); bool app_refresh_entries(App *app); void entry_free(Entry *entry); #endif PAmix-2.0/src/config.c000066400000000000000000000106361472203551200146000ustar00rootroot00000000000000#include "config.h" #include #include #include static struct { entry_type t; const char *s; } tab_mappings[] = { {ENTRY_SINKINPUT, "2"}, {ENTRY_SINKINPUT, "playback"}, {ENTRY_SINK, "0"}, {ENTRY_SINK, "output"}, {ENTRY_SOURCEOUTPUT, "3"}, {ENTRY_SOURCEOUTPUT, "recording"}, {ENTRY_SOURCE, "1"}, {ENTRY_SOURCE, "input"}, {ENTRY_CARD, "4"}, {ENTRY_CARD, "cards"}, }; static bool has_prefix(const char *str, const char *prefix) { while (*str && *prefix && *str++ == *prefix++) ; return !*prefix; } int config_load(Config *config, const char *path) { memset(config, 0, sizeof(*config)); const char *keynames[KEY_MAX]; for (int i = 0; i < KEY_MAX; i++) keynames[i] = keyname(i); FILE *f = fopen(path, "rb"); if (f == NULL) return -1; fseek(f, 0, SEEK_END); long length = ftell(f); fseek(f, 0, SEEK_SET); char *text = (char *)malloc(length + 1); assert(text != NULL); text[fread(text, 1, length, f)] = 0; fclose(f); char *save = NULL; for (char *line = strtok_r(text, "\n", &save); line != NULL; line = strtok_r(NULL, "\n", &save)) { char *comment = strchr(line, ';'); if (comment != NULL) *comment = '\0'; if (has_prefix(line, "set ")) { // TODO continue; } if (has_prefix(line, "bind ")) { char *key = line + sizeof("bind"); char *action = strchr(key, ' '); *action++ = '\0'; int keycode = -1; for (int i = 0; i < KEY_MAX; i++) { if (keynames[i] && strcmp(key, keynames[i]) == 0) { keycode = i; break; } } if (keycode == -1) { assert(0 && "invalid keycode"); continue; } char *arg = strchr(action, ' '); if (arg != NULL) *arg++ = '\0'; Action *info = &config->keymap[keycode]; if (strcmp(action, "quit") == 0) { info->type = ACTION_QUIT; continue; } if (strcmp(action, "select-tab") == 0) { assert(arg != NULL); int idx = -1; for (size_t i = 0; i < sizeof(tab_mappings) / sizeof(*tab_mappings); i++) { if (strcmp(tab_mappings[i].s, arg) == 0) { idx = i; break; } } assert(idx != -1); info->type = ACTION_SELECT_TAB; info->data.tab = tab_mappings[idx].t; continue; } if (strcmp(action, "set-volume") == 0 || strcmp(action, "add-volume") == 0) { assert(arg != NULL); char *end; double value = strtod(arg, &end); assert(end == arg + strlen(arg)); info->type = strcmp(action, "set-volume") == 0 ? ACTION_VOLUME_SET : ACTION_VOLUME_ADD; info->data.volume = (float)value; continue; } if(strcmp(action, "select-next") == 0) { info->type = ACTION_ENTRY_NEXT; continue; } if(strcmp(action, "select-prev") == 0) { info->type = ACTION_ENTRY_PREV; continue; } if(strcmp(action, "cycle-next") == 0) { info->type = ACTION_DEVICE_NEXT; continue; } if(strcmp(action, "cycle-prev") == 0) { info->type = ACTION_DEVICE_PREV; continue; } if(strcmp(action, "toggle-mute") == 0) { info->type = ACTION_MUTE_TOGGLE; continue; } if(strcmp(action, "toggle-lock") == 0) { info->type = ACTION_LOCK_TOGGLE; continue; } // TODO: tab cycle? continue; } /* if(strlen(line) > 0) fprintf(stderr, "line: %s\n", line); */ } free(text); return 0; } void config_default(Config *config) { config->keymap['q'] = (Action){.type = ACTION_QUIT}; for(int i = 0; i < 10; i++) config->keymap['0' + i] = (Action){.type = ACTION_VOLUME_SET, .data = {.volume = i == 0 ? 1.0f : i * 0.1f}}; config->keymap['h'] = (Action){.type = ACTION_VOLUME_ADD, .data = {.volume = -0.05f}}; config->keymap['l'] = (Action){.type = ACTION_VOLUME_ADD, .data = {.volume = 0.05f}}; config->keymap['j'] = (Action){.type = ACTION_ENTRY_NEXT}; config->keymap['k'] = (Action){.type = ACTION_ENTRY_PREV}; config->keymap[KEY_LEFT] = (Action){.type = ACTION_VOLUME_ADD, .data = {.volume = -0.05f}}; config->keymap[KEY_RIGHT] = (Action){.type = ACTION_VOLUME_ADD, .data = {.volume = 0.05f}}; config->keymap[KEY_DOWN] = (Action){.type = ACTION_ENTRY_NEXT}; config->keymap[KEY_UP] = (Action){.type = ACTION_ENTRY_PREV}; for(int i = 0; i <= ENTRY_CARD; i++) config->keymap[KEY_F(i + 1)] = (Action){.type = ACTION_SELECT_TAB, .data = {.tab = (entry_type)i}}; config->keymap['s'] = (Action){.type = ACTION_DEVICE_NEXT}; config->keymap['S'] = (Action){.type = ACTION_DEVICE_PREV}; config->keymap['c'] = (Action){.type = ACTION_LOCK_TOGGLE}; config->keymap['m'] = (Action){.type = ACTION_MUTE_TOGGLE}; } PAmix-2.0/src/config.h000066400000000000000000000010711472203551200145760ustar00rootroot00000000000000#ifndef _CONFIG_H #define _CONFIG_H #include "app.h" #include typedef enum { ACTION_NONE = 0, ACTION_QUIT, ACTION_SELECT_TAB, ACTION_MUTE_TOGGLE, ACTION_LOCK_TOGGLE, ACTION_ENTRY_NEXT, ACTION_ENTRY_PREV, ACTION_VOLUME_ADD, ACTION_VOLUME_SET, ACTION_DEVICE_NEXT, ACTION_DEVICE_PREV, } ActionType; typedef struct { ActionType type; union { entry_type tab; float volume; } data; } Action; typedef struct { Action keymap[KEY_MAX]; } Config; int config_load(Config *config, const char *path); void config_default(Config *config); #endif PAmix-2.0/src/da.h000066400000000000000000000050311472203551200137150ustar00rootroot00000000000000#ifndef _DA_H #define _DA_H #define da_grow_cap(da) \ (da)->cap == 0 ? sizeof(*(da)->items) > 1024 ? 1 : 256 : (da)->cap * 2 #define da_append(da, item) \ do { \ if ((da)->len >= (da)->cap) { \ (da)->cap = da_grow_cap(da); \ (da)->items = realloc((da)->items, (da)->cap * sizeof(*(da)->items)); \ assert((da)->items != NULL); \ } \ (da)->items[(da)->len++] = (item); \ } while (0) #define da_reserve(da, add) \ do { \ if ((da)->cap >= (da)->len + (add)) \ break; \ while ((da)->len + (add) > (da)->cap) { \ (da)->cap = da_grow_cap(da); \ } \ (da)->items = realloc((da)->items, (da)->cap * sizeof(*(da)->items)); \ assert((da)->items != NULL); \ } while (0) #define da_append_many(da, new_items, items_count) \ do { \ if ((da)->len + items_count > (da)->cap) { \ while ((da)->len + items_count > (da)->cap) { \ (da)->cap = da_grow_cap(da); \ } \ (da)->items = realloc((da)->items, (da)->cap * sizeof(*(da)->items)); \ assert((da)->items != NULL); \ } \ memcpy((da)->items + (da)->len, new_items, \ items_count * sizeof(*(da)->items)); \ (da)->len += items_count; \ } while (0) #endif PAmix-2.0/src/draw.c000066400000000000000000000025141472203551200142640ustar00rootroot00000000000000#include "draw.h" #include #include #include static wchar_t braille[] = { L' ', L'\u2802', L'\u2806', L'\u2807', L'\u280f', L'\u281f', L'\u283f' }; static int n_braille = (int)(sizeof(braille) / sizeof(*braille)); void draw_volume_bar(int y, int x, int width, pa_volume_t volume) { int segments = width - 2; if (segments <= 0) return; int fill = (int)((float)volume / (float)(PA_VOLUME_NORM * 1.5) * (float)segments); if(fill > segments) fill = segments; mvaddstr(y, x++, "["); mvaddstr(y, x + segments, "]"); wchar_t buf[segments + 1]; buf[segments] = 0; for (int i = 0; i < segments; i++) buf[i] = i >= fill ? L' ' : braille[n_braille-1]; if(volume != PA_VOLUME_MUTED && volume != PA_VOLUME_NORM) { int segval = (PA_VOLUME_NORM * 1.5) / segments; int subsegval = segval / (n_braille - 1); int idx = (volume % segval) / subsegval; assert(idx <= n_braille - 1); buf[fill] = braille[idx]; } int indexA = (int)(segments * ((double) 1 / 3)); int indexB = (int)(segments * ((double) 2 / 3)); attron(COLOR_PAIR(1)); mvaddnwstr(y, x, buf, indexA); attroff(COLOR_PAIR(1)); attron(COLOR_PAIR(2)); mvaddnwstr(y, x + indexA, buf + indexA, indexB - indexA); attroff(COLOR_PAIR(2)); attron(COLOR_PAIR(3)); mvaddnwstr(y, x + indexB, buf + indexB, segments - indexB); attroff(COLOR_PAIR(3)); } PAmix-2.0/src/draw.h000066400000000000000000000002061472203551200142650ustar00rootroot00000000000000#ifndef _DRAW_H #define _DRAW_H #include void draw_volume_bar(int y, int x, int width, pa_volume_t volume); #endif PAmix-2.0/src/main.c000066400000000000000000000534701472203551200142620ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include "app.h" #include "da.h" #include "draw.h" #include "config.h" struct line_expect { int begin; int *end; int expected; }; void line_expect_check(struct line_expect *e) { int count = (*e->end) - e->begin; assert(e->expected == count); } static inline int expected_entry_lines(const Entry *ent) { int channel_lines = ent->type == ENTRY_CARD ? 0: (ent->volume_lock ? 1 : ent->volume.channels); return channel_lines + 1 + (ent->type != ENTRY_CARD); } int compute_entry_scroll(void); void on_ctx_state(pa_context *ctx, void *data) { (void)ctx; pa_threaded_mainloop *mainloop = (pa_threaded_mainloop *)data; pa_threaded_mainloop_signal(mainloop, false); } void on_ctx_subscription(pa_context *ctx, pa_subscription_event_type_t evt_type, uint32_t index, void *data) { (void)ctx; (void)evt_type; (void)index; (void)data; atomic_store(&app.should_refresh, true); pa_threaded_mainloop_signal(app.pa_mainloop, false); } void cb_success_signal(pa_context *ctx, int succ, void *data) { (void)ctx; (void)succ; (void)data; pa_threaded_mainloop_signal(app.pa_mainloop, false); } void on_signal_resize(int signal) { (void)signal; atomic_store(&app.resized, true); pa_threaded_mainloop_signal(app.pa_mainloop, false); } void *input_thread_main(void *data) { (void)data; while (app.running) { pthread_mutex_lock(&app.mutex); int ch = getch(); pthread_mutex_unlock(&app.mutex); #ifdef KEY_RESIZE if (ch == KEY_RESIZE) { atomic_store(&app.resized, true); } #endif bool key_valid = ch != ERR && ch != KEY_RESIZE && ch != KEY_MOUSE; if (key_valid) { InputEvent evt = { .keycode = ch, .keyname = keyname(ch), }; assert(evt.keyname != NULL); pthread_mutex_lock(&app.mutex); da_append(&app.input_queue, evt); pthread_mutex_unlock(&app.mutex); } if (key_valid || ch == KEY_RESIZE) { pa_threaded_mainloop_signal(app.pa_mainloop, false); } usleep(2000); } return NULL; } static void reconnect_cleanup_mainloop(void *mainloop) { pa_threaded_mainloop_unlock(mainloop); } void *reconnect_thread_main(void *arg) { (void)arg; while (app.running) { pa_proplist *props; pa_mainloop_api *api; int err; pthread_cleanup_push(reconnect_cleanup_mainloop, app.pa_mainloop); pa_threaded_mainloop_lock(app.pa_mainloop); pthread_mutex_lock(&app.mutex); if (app.pa_context != NULL && pa_context_get_state(app.pa_context) == PA_CONTEXT_READY) { goto sleep; } if (app.pa_context != NULL) { pa_context_unref(app.pa_context); app.pa_context = NULL; } props = pa_proplist_new(); pa_proplist_sets(props, PA_PROP_APPLICATION_ID, "testerino"); pa_proplist_sets(props, PA_PROP_APPLICATION_NAME, "testerino"); api = pa_threaded_mainloop_get_api(app.pa_mainloop); app.pa_context = pa_context_new_with_proplist(api, "testerino", props); pa_proplist_free(props); pa_context_set_state_callback(app.pa_context, &on_ctx_state, app.pa_mainloop); err = pa_context_connect(app.pa_context, NULL, (pa_context_flags_t)PA_CONTEXT_NOAUTOSPAWN, NULL); if (err != 0) { pa_context_unref(app.pa_context); app.pa_context = NULL; // TODO: on error we probably want to sleep with the lock held, so noone else does any funny business goto sleep; } pa_context_state_t state; pthread_mutex_unlock(&app.mutex); while((state = pa_context_get_state(app.pa_context)) != PA_CONTEXT_READY){ if(!PA_CONTEXT_IS_GOOD(state)) { pthread_mutex_lock(&app.mutex); goto sleep; } assert(PA_CONTEXT_IS_GOOD(state)); pa_threaded_mainloop_wait(app.pa_mainloop); } pthread_mutex_lock(&app.mutex); { pa_context_set_subscribe_callback(app.pa_context, &on_ctx_subscription, NULL); pa_subscription_mask_t submask = PA_SUBSCRIPTION_MASK_ALL; pa_operation *op = pa_context_subscribe(app.pa_context, submask, &cb_success_signal, app.pa_mainloop); pa_operation_state_t opstate; pthread_mutex_unlock(&app.mutex); while ((opstate = pa_operation_get_state(op)) == PA_OPERATION_RUNNING) { pa_threaded_mainloop_wait(app.pa_mainloop); } pthread_mutex_lock(&app.mutex); pa_operation_unref(op); if(opstate != PA_OPERATION_DONE) goto sleep; } atomic_store(&app.should_refresh, true); pa_threaded_mainloop_signal(app.pa_mainloop, false); sleep: pthread_mutex_unlock(&app.mutex); // pa_threaded_mainloop_unlock pthread_cleanup_pop(true); sleep(2); } return NULL; } pa_operation *entry_set_volume(Entry ent, const pa_cvolume *volume) { #define OP(name) pa_context_set_##name(app.pa_context, ent.pa_index, volume, &cb_success_signal, NULL) switch (ent.type) { case ENTRY_SINKINPUT: return OP(sink_input_volume); case ENTRY_SOURCEOUTPUT: return OP(source_output_volume); case ENTRY_SINK: return OP(sink_volume_by_index); case ENTRY_SOURCE: return OP(source_volume_by_index); case ENTRY_CARD: return NULL; } __builtin_unreachable(); #undef OP } pa_operation *entry_set_muted(Entry ent, bool mute) { #define OP(name) pa_context_set_##name(app.pa_context, ent.pa_index, mute, &cb_success_signal, NULL) switch (ent.type) { case ENTRY_SINKINPUT: return OP(sink_input_mute); case ENTRY_SOURCEOUTPUT: return OP(source_output_mute); case ENTRY_SINK: return OP(sink_mute_by_index); case ENTRY_SOURCE: return OP(source_mute_by_index); case ENTRY_CARD: return NULL; } __builtin_unreachable(); #undef OP } void collect_sink_indices(pa_context *ctx, const pa_sink_info *i, int eol, void *userdata) { (void)ctx; if (eol) { assert(i == NULL); pa_threaded_mainloop_signal(app.pa_mainloop, false); return; } pthread_mutex_lock(&app.mutex); uint32_t **ptr = (uint32_t **)userdata; *((*ptr)++) = i->index; pthread_mutex_unlock(&app.mutex); } void collect_source_indices(pa_context *ctx, const pa_source_info *i, int eol, void *userdata) { (void)ctx; if (eol) { assert(i == NULL); pa_threaded_mainloop_signal(app.pa_mainloop, false); return; } pthread_mutex_lock(&app.mutex); uint32_t **ptr = (uint32_t **)userdata; *((*ptr)++) = i->index; pthread_mutex_unlock(&app.mutex); } #define RUN_OPERATION_OR_RETURN(operation, ostate, or_return) \ do { \ assert(operation != NULL); \ pthread_mutex_unlock(&app.mutex); \ while(((ostate) = pa_operation_get_state(operation)) == PA_OPERATION_RUNNING) \ pa_threaded_mainloop_wait(app.pa_mainloop); \ pthread_mutex_lock(&app.mutex); \ pa_operation_unref(operation); \ if((ostate) == PA_OPERATION_CANCELLED) { \ return or_return; \ }\ assert((ostate) == PA_OPERATION_DONE);\ } while(0) // caller should hold mainloop and app-mutex // return false on failure static bool drain_input_queue(const Config *cfg) { for (size_t i = 0; i < app.input_queue.len; i++) { InputEvent evt = app.input_queue.items[i]; Action act = cfg->keymap[evt.keycode]; if (act.type == ACTION_QUIT) { app.running = false; continue; } if (act.type == ACTION_SELECT_TAB) { app.entry_page = cfg->keymap[evt.keycode].data.tab; app.selected_entry = 0; app.selected_channel = 0; atomic_store(&app.should_refresh, true); continue; } if (act.type == ACTION_DEVICE_NEXT || act.type == ACTION_DEVICE_PREV) { Entry ent = app.entries.items[app.selected_entry]; int off = act.type == ACTION_DEVICE_NEXT ? 1 : -1; switch (ent.type) { case ENTRY_SINKINPUT: case ENTRY_SOURCEOUTPUT: { if (ent.data.device.index == PA_INVALID_INDEX) break; uint32_t device_list[512] = {0}; for (size_t i = 0; i < sizeof(device_list) / sizeof(*device_list); i++) device_list[i] = PA_INVALID_INDEX; uint32_t *end = device_list; pa_operation *op; pa_operation_state_t state; if (ent.type == ENTRY_SINKINPUT) op = pa_context_get_sink_info_list(app.pa_context, &collect_sink_indices, &end); else op = pa_context_get_source_info_list(app.pa_context, &collect_source_indices, &end); assert(op != NULL); RUN_OPERATION_OR_RETURN(op, state, false); int device_count = (uintptr_t)(end - device_list); assert((size_t)device_count <= sizeof(device_list) / sizeof(*device_list)); int current_index = -1; for (int i = 0; i < device_count; i++) { if (ent.data.device.index != device_list[i]) continue; current_index = i; break; } assert(current_index >= 0); int idev = (current_index + off) % device_count; if(idev == -1) idev = device_count - 1; assert(idev >= 0); assert(idev < device_count); uint32_t new_device = device_list[idev]; if (ent.type == ENTRY_SINKINPUT) op = pa_context_move_sink_input_by_index(app.pa_context, ent.pa_index, new_device, &cb_success_signal, NULL); else op = pa_context_move_source_output_by_index(app.pa_context, ent.pa_index, new_device, &cb_success_signal, NULL); assert(op != NULL); RUN_OPERATION_OR_RETURN(op, state, false); break; } case ENTRY_SINK: case ENTRY_SOURCE: case ENTRY_CARD: { if (ent.data.ports.current == -1) break; assert((int)ent.data.ports.len > ent.data.ports.current); int next = ((ent.data.ports.current + off) % ent.data.ports.len); if (next == ent.data.ports.current) { assert(ent.data.ports.len == 1); break; } const char *name = ent.data.ports.items[next].name; pa_operation *op; pa_operation_state_t state; if (ent.type == ENTRY_SINK) op = pa_context_set_sink_port_by_name(app.pa_context, ent.name, name, &cb_success_signal, NULL); else if (ent.type == ENTRY_SOURCE) op = pa_context_set_source_port_by_name(app.pa_context, ent.name, name, &cb_success_signal, NULL); else op = pa_context_set_card_profile_by_name(app.pa_context, ent.name, name, &cb_success_signal, NULL); assert(op != NULL); RUN_OPERATION_OR_RETURN(op, state, false); } } continue; } if (act.type == ACTION_ENTRY_NEXT || act.type == ACTION_ENTRY_PREV) { int off = act.type == ACTION_ENTRY_NEXT ? 1 : -1; bool entry_bounds = app.selected_entry + off < 0 || app.selected_entry + off >= (int)app.entries.len; Entry ent = app.entries.items[app.selected_entry]; if (ent.volume_lock && !entry_bounds) { app.selected_entry += off; Entry other = app.entries.items[app.selected_entry]; if (other.volume_lock) app.selected_channel = 0; else if (off < 0) app.selected_channel = other.volume.channels - 1; } else { if (off > 0 && ent.volume.channels > app.selected_channel + off) { app.selected_channel += off; } else if (off < 0 && app.selected_channel > 0) { app.selected_channel += off; } else if (!entry_bounds) { app.selected_entry += off; Entry other = app.entries.items[app.selected_entry]; if (other.volume_lock) app.selected_channel = 0; else if (off < 0) app.selected_channel = other.volume.channels - 1; } } atomic_store(&app.should_refresh, true); continue; } if (act.type == ACTION_LOCK_TOGGLE) { Entry *ent = &app.entries.items[app.selected_entry]; if (ent->volume.channels == 0) continue; ent->volume_lock = !ent->volume_lock; app.selected_channel = 0; atomic_store(&app.should_refresh, true); continue; } if (act.type == ACTION_MUTE_TOGGLE) { Entry ent = app.entries.items[app.selected_entry]; pa_operation *op = entry_set_muted(ent, !ent.muted); if (op == NULL) continue; pa_operation_state_t state; RUN_OPERATION_OR_RETURN(op, state, false); continue; } if (act.type == ACTION_VOLUME_SET || act.type == ACTION_VOLUME_ADD) { Entry ent = app.entries.items[app.selected_entry]; if (ent.volume.channels == 0) continue; pa_volume_t newvol; if (act.type == ACTION_VOLUME_SET) { if (ent.volume_lock) { newvol = (pa_volume_t)((float)PA_VOLUME_NORM * act.data.volume); pa_cvolume_set(&ent.volume, ent.volume.channels, newvol); } else { assert(app.selected_channel >= 0 && app.selected_channel < ent.volume.channels); ent.volume.values[app.selected_channel] = PA_VOLUME_NORM * act.data.volume; } } else { if (ent.volume_lock) { newvol = pa_cvolume_avg(&ent.volume) + PA_VOLUME_NORM * act.data.volume; pa_cvolume_set(&ent.volume, ent.volume.channels, newvol); } else { assert(app.selected_channel >= 0 && app.selected_channel < ent.volume.channels); ent.volume.values[app.selected_channel] += PA_VOLUME_NORM * act.data.volume; } } pa_operation *op = entry_set_volume(ent, &ent.volume); assert(op != NULL); pa_operation_state_t state; RUN_OPERATION_OR_RETURN(op, state, false); atomic_store(&app.should_refresh, true); continue; } } app.input_queue.len = 0; return true; } int main(void) { Config cfg = {0}; do { const char *home = getenv("HOME"); const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); const char *xdg_config_dirs = getenv("XDG_CONFIG_DIRS"); char config_path[PATH_MAX]; if(xdg_config_home != NULL) snprintf(config_path, PATH_MAX - 1, "%s/pamix.conf", xdg_config_home); else snprintf(config_path, PATH_MAX - 1, "%s/.config/pamix.conf", home); if(config_load(&cfg, config_path) == 0) break; if(xdg_config_dirs == NULL) xdg_config_dirs = "/etc/xdg"; snprintf(config_path, PATH_MAX - 1, "%s/pamix.conf", xdg_config_dirs); if(config_load(&cfg, config_path) == 0) break; config_default(&cfg); } while(0); pa_threaded_mainloop *mainloop = pa_threaded_mainloop_new(); assert(mainloop != NULL); pa_threaded_mainloop_lock(mainloop); if (pa_threaded_mainloop_start(mainloop) == -1) { fprintf(stderr, "could not start mainloop\n"); return 1; } pa_threaded_mainloop_unlock(mainloop); // we pass NULL as pa_context* and let the reconnect thread handle it app_init(&app, NULL, mainloop); atomic_store(&app.should_refresh, true); app.entry_page = ENTRY_SINKINPUT; { setlocale(LC_ALL, ""); initscr(); nodelay(stdscr, true); set_escdelay(25); curs_set(0); keypad(stdscr, true); meta(stdscr, true); noecho(); start_color(); int background = use_default_colors() ? 0 : -1; init_pair(1, COLOR_GREEN, background); init_pair(2, COLOR_YELLOW, background); init_pair(3, COLOR_RED, background); } signal(SIGWINCH, on_signal_resize); struct EntLine { uint32_t entry; uint32_t line; }; struct EntLines { struct EntLine *items; size_t len; size_t cap; }; struct EntLines entry_lines = {0}; pthread_t input_thread; pthread_t reconnect_thread; int pthread_status = pthread_create(&input_thread, NULL, &input_thread_main, NULL); pthread_status |= pthread_create(&reconnect_thread, NULL, &reconnect_thread_main, NULL); if(pthread_status != 0) { fprintf(stderr, "failed to create threads\n"); exit(1); } while (app.running) { { pa_threaded_mainloop_lock(mainloop); pthread_mutex_lock(&app.mutex); if(app.pa_context == NULL || pa_context_get_state(app.pa_context) != PA_CONTEXT_READY) { erase(); mvprintw(0, 0, "Waiting for PulseAudio connection..."); refresh(); for(size_t i = 0; i < app.input_queue.len; i++) { Action action = cfg.keymap[app.input_queue.items[i].keycode]; if(action.type == ACTION_QUIT) { app.running = false; break; } } if(!app.running) { pthread_mutex_unlock(&app.mutex); pa_threaded_mainloop_unlock(app.pa_mainloop); break; } app.input_queue.len = 0; pthread_mutex_unlock(&app.mutex); pa_threaded_mainloop_wait(app.pa_mainloop); pa_threaded_mainloop_unlock(app.pa_mainloop); continue; } bool ok = drain_input_queue(&cfg); if(!ok) { pthread_mutex_unlock(&app.mutex); pa_threaded_mainloop_unlock(app.pa_mainloop); continue; } pthread_mutex_unlock(&app.mutex); pa_threaded_mainloop_unlock(mainloop); } if (atomic_exchange(&app.resized, false)) { pthread_mutex_lock(&app.mutex); endwin(); refresh(); pthread_mutex_unlock(&app.mutex); atomic_store(&app.should_refresh, true); } if (atomic_exchange(&app.should_refresh, false)) { bool ok = app_refresh_entries(&app); if(!ok) { continue; } pthread_mutex_lock(&app.mutex); app.scroll = compute_entry_scroll(); erase(); const char *entry_type_names[] = {"Playback", "Recording", "Output Devices", "Input Devices", "Cards"}; move(0, 1); printw("%d/%zu", app.selected_entry + 1, app.entries.len); mvaddstr(0, 10, entry_type_names[app.entry_page]); int line = 1; entry_lines.len = 0; for (size_t i = app.scroll; i < app.entries.len; i++) { line++; Entry *ent = &app.entries.items[i]; bool selected = app.selected_entry == (int)i; int entsize = 1; if (line + entsize + 2 > LINES) { assert(!selected || (int)i == app.scroll); break; } struct line_expect __attribute__((cleanup(line_expect_check))) expect = { .begin = line, .end = &line, .expected = expected_entry_lines(ent), }; int width = COLS - 33; int x = 32; // volume control bars if (ent->volume_lock && ent->volume.channels > 0) { move(line, 1); if (app.selected_entry == (int)i) { addstr(">"); } pa_volume_t vol = pa_cvolume_avg(&ent->volume); char buf[30]; pa_sw_volume_snprint_dB(buf, sizeof(buf) - 1, vol); double pct = vol / (double)PA_VOLUME_NORM; addstr(buf); printw(" (%.2lf)", pct); draw_volume_bar(line++, x, width, vol); } else { for (uint8_t j = 0; j < ent->volume.channels; j++) { if (app.selected_entry == (int)i && app.selected_channel == j) { mvaddstr(line, 1, ">"); } const char *channel_name = pa_channel_position_to_pretty_string(ent->channel_map.map[j]); mvaddstr(line, 3, channel_name); draw_volume_bar(line++, x, width, ent->volume.values[j]); } } // peak volume bar if(ent->type != ENTRY_CARD) { pa_volume_t peak = ent->peak * PA_VOLUME_NORM; if (ent->monitor_stream == NULL) peak = PA_VOLUME_MUTED; struct EntLine el = {.entry = ent->pa_index, .line = (uint32_t)line}; da_append(&entry_lines, el); draw_volume_bar(line++, 1, COLS - 2, peak); } // entry name if (selected) attron(A_STANDOUT); switch (ent->type) { case ENTRY_SINKINPUT: mvaddstr(line, 1, pa_proplist_gets(ent->props, PA_PROP_APPLICATION_NAME)); break; case ENTRY_SINK: case ENTRY_SOURCE: mvaddstr(line, 1, pa_proplist_gets(ent->props, PA_PROP_DEVICE_DESCRIPTION)); printw(" %s", pa_proplist_gets(ent->props, PA_PROP_DEVICE_PROFILE_DESCRIPTION)); break; case ENTRY_CARD: mvaddstr(line, 1, pa_proplist_gets(ent->props, PA_PROP_DEVICE_DESCRIPTION)); break; default: mvaddstr(line, 1, ent->name); break; } attroff(A_STANDOUT); if (ent->volume_lock) printw(" 🔒"); if (ent->muted) printw(" 🔇"); if (ent->corked) printw(" ⏸"); // device/port/profile display switch (ent->type) { case ENTRY_SINKINPUT: case ENTRY_SOURCEOUTPUT: { char buf[256]; int dev_len = 0; assert(ent->data.device.name != NULL); if (ent->data.device.name != NULL) dev_len = snprintf(buf, sizeof(buf) - 1, "%s", ent->data.device.name); int name_len = strlen(ent->name); int max_name = COLS - 1 - dev_len - 4; x = getcurx(stdscr); if(x < max_name) { // TODO: color attron(A_DIM); if(name_len > max_name - x) { printw(" %.*s...", max_name - x - 3, ent->name); } else { printw(" %s", ent->name); } attroff(A_DIM); } mvaddstr(line, COLS - 1 - dev_len, buf); break; } case ENTRY_CARD: case ENTRY_SINK: case ENTRY_SOURCE: { char buf[256]; int len; len = sprintf(buf, "%s", ent->data.ports.items[ent->data.ports.current].description); mvaddstr(line, COLS - 1 - len, buf); break; } default: break; } line++; } refresh(); pthread_mutex_unlock(&app.mutex); } else if (app.new_peaks) { pthread_mutex_lock(&app.mutex); app.new_peaks = false; for (size_t i = 0; i < entry_lines.len; i++) { struct EntLine el = entry_lines.items[i]; Entry *ent = NULL; for (size_t j = 0; j < app.entries.len; j++) { Entry *e = &app.entries.items[j]; if (e->pa_index == el.entry) { ent = e; break; } } assert(ent != NULL); pa_volume_t peak = ent->peak * PA_VOLUME_NORM; if (ent->monitor_stream == NULL) peak = PA_VOLUME_MUTED; draw_volume_bar(el.line, 1, COLS - 2, peak); } refresh(); pthread_mutex_unlock(&app.mutex); } if (!app.running) break; pa_threaded_mainloop_lock(mainloop); if (atomic_load(&app.should_refresh) || app.new_peaks || app.input_queue.len > 0) { pa_threaded_mainloop_unlock(mainloop); continue; } pa_threaded_mainloop_wait(app.pa_mainloop); pa_threaded_mainloop_unlock(mainloop); } pthread_join(input_thread, NULL); pthread_cancel(reconnect_thread); pthread_join(reconnect_thread, NULL); if(entry_lines.cap != 0) free(entry_lines.items); if(app.pa_context != NULL && PA_CONTEXT_IS_GOOD(pa_context_get_state(app.pa_context))){ pa_context_disconnect(app.pa_context); pa_threaded_mainloop_stop(app.pa_mainloop); pa_threaded_mainloop_free(app.pa_mainloop); } for(size_t i = 0; i < app.entries.len; i++) { entry_free(&app.entries.items[i]); } endwin(); return 0; } // compute new scroll so selected entry stays in view int compute_entry_scroll(void) { int scroll = app.scroll; if (scroll > app.selected_entry) return app.selected_entry; int entry_sizes[app.entries.len]; for (size_t i = 0; i < app.entries.len; i++) { entry_sizes[i] = expected_entry_lines(&app.entries.items[i]); } int line = 2; for (size_t i = scroll; i < app.entries.len; i++) { line += entry_sizes[i] + 1; if ((int)i < scroll || app.selected_entry != (int)i) continue; if (line > LINES) { int backscroll = 0; size_t j = scroll; for (; j < i && line - backscroll > LINES; j++) backscroll += entry_sizes[j]; scroll = (int)j; } break; } return scroll; }