From 963eb49fe7c098b9be0e2ef32b51d9ef04b33168 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 Aug 2025 17:19:24 +0000 Subject: [PATCH 1/3] Initial plan From 0c6f29e7edc8073fea073702def4398394d9f1df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 Aug 2025 17:23:48 +0000 Subject: [PATCH 2/3] Initial analysis and build setup for fixing stale sink input indexes Co-authored-by: cwage <190973+cwage@users.noreply.github.com> --- src/Makefile | 38 +++++++++++++++++++------------------- src/Makefile.in | 4 ++-- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Makefile b/src/Makefile index a725c92..561a1f8 100644 --- a/src/Makefile +++ b/src/Makefile @@ -70,7 +70,6 @@ am__make_running_with_option = \ test $$has_opt = yes am__make_dryrun = (target_option=n; $(am__make_running_with_option)) am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) -pkgdatadir = $(datadir)/volmix pkgincludedir = $(includedir)/volmix pkglibdir = $(libdir)/volmix pkglibexecdir = $(libexecdir)/volmix @@ -167,13 +166,14 @@ am__define_uniq_tagged_files = \ done | $(am__uniquify_input)` am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) -ACLOCAL = ${SHELL} '/home/cwage/git/cwage/volume/missing' aclocal-1.16 +pkgdatadir = ${datadir}/volmix +ACLOCAL = ${SHELL} '/home/runner/work/volmix/volmix/missing' aclocal-1.16 AMTAR = $${TAR-tar} AM_DEFAULT_VERBOSITY = 1 -AUTOCONF = ${SHELL} '/home/cwage/git/cwage/volume/missing' autoconf -AUTOHEADER = ${SHELL} '/home/cwage/git/cwage/volume/missing' autoheader -AUTOMAKE = ${SHELL} '/home/cwage/git/cwage/volume/missing' automake-1.16 -AWK = mawk +AUTOCONF = ${SHELL} '/home/runner/work/volmix/volmix/missing' autoconf +AUTOHEADER = ${SHELL} '/home/runner/work/volmix/volmix/missing' autoheader +AUTOMAKE = ${SHELL} '/home/runner/work/volmix/volmix/missing' automake-1.16 +AWK = gawk CC = gcc CCDEPMODE = depmode=gcc3 CFLAGS = -g -O2 @@ -188,10 +188,10 @@ ECHO_N = -n ECHO_T = ETAGS = etags EXEEXT = -GLIB_CFLAGS = -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -GLIB_LIBS = -lglib-2.0 -GTK_CFLAGS = -pthread -I/usr/include/gtk-3.0 -I/usr/include/at-spi2-atk/2.0 -I/usr/include/at-spi-2.0 -I/usr/include/dbus-1.0 -I/usr/lib/x86_64-linux-gnu/dbus-1.0/include -I/usr/include/gtk-3.0 -I/usr/include/gio-unix-2.0 -I/usr/include/cairo -I/usr/include/pango-1.0 -I/usr/include/harfbuzz -I/usr/include/pango-1.0 -I/usr/include/fribidi -I/usr/include/harfbuzz -I/usr/include/atk-1.0 -I/usr/include/cairo -I/usr/include/pixman-1 -I/usr/include/uuid -I/usr/include/freetype2 -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/libpng16 -I/usr/include/x86_64-linux-gnu -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -GTK_LIBS = -lgtk-3 -lgdk-3 -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -latk-1.0 -lcairo-gobject -lcairo -lgdk_pixbuf-2.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0 +GLIB_CFLAGS = -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include +GLIB_LIBS = -lglib-2.0 +GTK_CFLAGS = -I/usr/include/gtk-3.0 -I/usr/include/pango-1.0 -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -I/usr/include/harfbuzz -I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/fribidi -I/usr/include/cairo -I/usr/include/pixman-1 -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/x86_64-linux-gnu -I/usr/include/webp -I/usr/include/gio-unix-2.0 -I/usr/include/atk-1.0 -I/usr/include/at-spi2-atk/2.0 -I/usr/include/at-spi-2.0 -I/usr/include/dbus-1.0 -I/usr/lib/x86_64-linux-gnu/dbus-1.0/include -pthread +GTK_LIBS = -lgtk-3 -lgdk-3 -lz -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -latk-1.0 -lcairo-gobject -lcairo -lgdk_pixbuf-2.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0 INSTALL = /usr/bin/install -c INSTALL_DATA = ${INSTALL} -m 644 INSTALL_PROGRAM = ${INSTALL} @@ -201,7 +201,7 @@ LDFLAGS = LIBOBJS = LIBS = LTLIBOBJS = -MAKEINFO = ${SHELL} '/home/cwage/git/cwage/volume/missing' makeinfo +MAKEINFO = ${SHELL} '/home/runner/work/volmix/volmix/missing' makeinfo MKDIR_P = /usr/bin/mkdir -p OBJEXT = o PACKAGE = volmix @@ -215,16 +215,16 @@ PATH_SEPARATOR = : PKG_CONFIG = /usr/bin/pkg-config PKG_CONFIG_LIBDIR = PKG_CONFIG_PATH = -PULSE_CFLAGS = -D_REENTRANT -PULSE_LIBS = -lpulse -pthread +PULSE_CFLAGS = -D_REENTRANT +PULSE_LIBS = -lpulse -pthread SET_MAKE = SHELL = /bin/bash STRIP = VERSION = 0.1.0 -abs_builddir = /home/cwage/git/cwage/volume/src -abs_srcdir = /home/cwage/git/cwage/volume/src -abs_top_builddir = /home/cwage/git/cwage/volume -abs_top_srcdir = /home/cwage/git/cwage/volume +abs_builddir = /home/runner/work/volmix/volmix/src +abs_srcdir = /home/runner/work/volmix/volmix/src +abs_top_builddir = /home/runner/work/volmix/volmix +abs_top_srcdir = /home/runner/work/volmix/volmix ac_ct_CC = gcc am__include = include am__leading_dot = . @@ -243,7 +243,7 @@ host_alias = htmldir = ${docdir} includedir = ${prefix}/include infodir = ${datarootdir}/info -install_sh = ${SHELL} /home/cwage/git/cwage/volume/install-sh +install_sh = ${SHELL} /home/runner/work/volmix/volmix/install-sh libdir = ${exec_prefix}/lib libexecdir = ${exec_prefix}/libexec localedir = ${datarootdir}/locale @@ -265,7 +265,7 @@ top_build_prefix = ../ top_builddir = .. top_srcdir = .. volmix_SOURCES = volmix.c pulse_client.c pulse_client.h -volmix_CFLAGS = $(GTK_CFLAGS) $(PULSE_CFLAGS) $(GLIB_CFLAGS) +volmix_CFLAGS = $(GTK_CFLAGS) $(PULSE_CFLAGS) $(GLIB_CFLAGS) -DDATADIR=\"$(datadir)\" volmix_LDADD = $(GTK_LIBS) $(PULSE_LIBS) $(GLIB_LIBS) all: all-am diff --git a/src/Makefile.in b/src/Makefile.in index c72bc44..230519c 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -70,7 +70,6 @@ am__make_running_with_option = \ test $$has_opt = yes am__make_dryrun = (target_option=n; $(am__make_running_with_option)) am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) -pkgdatadir = $(datadir)/@PACKAGE@ pkgincludedir = $(includedir)/@PACKAGE@ pkglibdir = $(libdir)/@PACKAGE@ pkglibexecdir = $(libexecdir)/@PACKAGE@ @@ -167,6 +166,7 @@ am__define_uniq_tagged_files = \ done | $(am__uniquify_input)` am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +pkgdatadir = @pkgdatadir@ ACLOCAL = @ACLOCAL@ AMTAR = @AMTAR@ AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ @@ -265,7 +265,7 @@ top_build_prefix = @top_build_prefix@ top_builddir = @top_builddir@ top_srcdir = @top_srcdir@ volmix_SOURCES = volmix.c pulse_client.c pulse_client.h -volmix_CFLAGS = $(GTK_CFLAGS) $(PULSE_CFLAGS) $(GLIB_CFLAGS) +volmix_CFLAGS = $(GTK_CFLAGS) $(PULSE_CFLAGS) $(GLIB_CFLAGS) -DDATADIR=\"$(datadir)\" volmix_LDADD = $(GTK_LIBS) $(PULSE_LIBS) $(GLIB_LIBS) all: all-am From e9ec9ebc3329e8a843fb59a6c4e76d8fc8137629 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 Aug 2025 17:31:34 +0000 Subject: [PATCH 3/3] Implement PulseAudio subscription system to fix stale sink input indexes Co-authored-by: cwage <190973+cwage@users.noreply.github.com> --- src/pulse_client.c | 44 ++++++++++++++++++++++++ src/pulse_client.h | 4 +++ src/volmix.c | 85 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+) diff --git a/src/pulse_client.c b/src/pulse_client.c index 3dcf5ab..60220b1 100644 --- a/src/pulse_client.c +++ b/src/pulse_client.c @@ -8,6 +8,7 @@ static void context_state_callback(pa_context *c, void *userdata); static void sink_info_callback(pa_context *c, const pa_sink_info *info, int eol, void *userdata); static void server_info_callback(pa_context *c, const pa_server_info *info, void *userdata); static void sink_input_info_callback(pa_context *c, const pa_sink_input_info *info, int eol, void *userdata); +static void subscription_callback(pa_context *c, pa_subscription_event_type_t t, uint32_t index, void *userdata); gboolean pulse_client_init(pulse_client_t *client) { @@ -17,6 +18,7 @@ gboolean pulse_client_init(pulse_client_t *client) memset(client, 0, sizeof(pulse_client_t)); client->audio_apps = NULL; + client->sink_inputs_changed = FALSE; // Create mainloop client->mainloop = pa_mainloop_new(); @@ -112,6 +114,22 @@ gboolean pulse_client_connect(pulse_client_t *client) client->connected = TRUE; printf("Connected to PulseAudio server\n"); + // Subscribe to sink input events to detect when applications start/stop audio + pa_context_set_subscribe_callback(client->context, subscription_callback, client); + client->operation = pa_context_subscribe(client->context, + PA_SUBSCRIPTION_MASK_SINK_INPUT, + NULL, NULL); + if (!client->operation) { + printf("Failed to subscribe to PulseAudio events\n"); + return FALSE; + } + + while (pa_operation_get_state(client->operation) == PA_OPERATION_RUNNING) { + pa_mainloop_iterate(client->mainloop, 1, NULL); + } + pa_operation_unref(client->operation); + client->operation = NULL; + // Get server info to find default sink client->operation = pa_context_get_server_info(client->context, server_info_callback, client); @@ -258,6 +276,17 @@ void pulse_client_iterate(pulse_client_t *client) pa_mainloop_iterate(client->mainloop, 0, &retval); } +gboolean pulse_client_sink_inputs_changed(pulse_client_t *client) +{ + if (!client) { + return FALSE; + } + + gboolean changed = client->sink_inputs_changed; + client->sink_inputs_changed = FALSE; // Reset the flag + return changed; +} + // Callback functions static void context_state_callback(pa_context *c, void *userdata) { @@ -511,4 +540,19 @@ static void sink_input_info_callback(pa_context *c, const pa_sink_input_info *in app->name, app->process_name, app->index, app_audio_get_volume_percent(app), app->muted ? "yes" : "no"); +} + +// Subscription callback to handle PulseAudio events +static void subscription_callback(pa_context *c, pa_subscription_event_type_t t, uint32_t index, void *userdata) +{ + pulse_client_t *client = (pulse_client_t *)userdata; + + // Check if this is a sink input event + if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK_INPUT) { + // Mark that sink inputs have changed - this will trigger UI update + client->sink_inputs_changed = TRUE; + printf("Sink input event detected (index=%u, type=%s)\n", index, + (t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_NEW ? "NEW" : + (t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE ? "REMOVE" : "CHANGE"); + } } \ No newline at end of file diff --git a/src/pulse_client.h b/src/pulse_client.h index f91020d..5a5276d 100644 --- a/src/pulse_client.h +++ b/src/pulse_client.h @@ -23,6 +23,7 @@ typedef struct { pa_cvolume default_sink_volume; gboolean default_sink_muted; GList *audio_apps; // List of app_audio_t + gboolean sink_inputs_changed; // Flag to indicate sink inputs have changed } pulse_client_t; // Initialize PulseAudio client @@ -55,6 +56,9 @@ gboolean pulse_client_toggle_master_mute(pulse_client_t *client); // Process PulseAudio events (call periodically) void pulse_client_iterate(pulse_client_t *client); +// Check if sink inputs have changed since last check +gboolean pulse_client_sink_inputs_changed(pulse_client_t *client); + // Application management functions void pulse_client_refresh_apps(pulse_client_t *client); GList* pulse_client_get_apps(pulse_client_t *client); diff --git a/src/volmix.c b/src/volmix.c index ddd51a1..2c66c92 100644 --- a/src/volmix.c +++ b/src/volmix.c @@ -25,6 +25,10 @@ typedef struct { static volmix_app_t app_data; +// Forward declaration +static void update_slider_indexes(volmix_app_t *app); +static void update_sliders_recursive(GList *widgets, GList *apps); + // Asynchronous callback for PulseAudio processing static gboolean async_iterate_callback(gpointer user_data) { @@ -160,8 +164,12 @@ static void build_volume_window(volmix_app_t *app) uint32_t *index_ptr = g_malloc(sizeof(uint32_t)); *index_ptr = audio_app->index; + // Store application name for index updates + char *app_name = g_strdup(audio_app->name); + // Use g_object_set_data_full to ensure memory is freed when widget is destroyed g_object_set_data_full(G_OBJECT(slider), "sink_input_index", index_ptr, g_free); + g_object_set_data_full(G_OBJECT(slider), "app_name", app_name, g_free); // Connect slider signal using the stored data instead of direct pointer g_signal_connect(slider, "value-changed", G_CALLBACK(on_app_volume_changed), @@ -375,9 +383,86 @@ static gboolean pulse_client_timer_callback(gpointer user_data) // Process PulseAudio events pulse_client_iterate(&app->pulse_client); + // Check if sink inputs have changed and window is visible + if (pulse_client_sink_inputs_changed(&app->pulse_client) && + app->volmix_window && gtk_widget_get_visible(app->volmix_window)) { + printf("Sink inputs changed, updating slider indexes...\n"); + update_slider_indexes(app); + } + return G_SOURCE_CONTINUE; // Keep the timer running } +// Function to update slider widget data with current sink input indexes +static void update_slider_indexes(volmix_app_t *app) +{ + if (!app->volmix_window || !gtk_widget_get_visible(app->volmix_window)) { + return; + } + + // Refresh the sink input list + pulse_client_refresh_apps(&app->pulse_client); + + // Process the refresh operation + if (app->pulse_client.operation) { + int timeout_count = 0; + while (pa_operation_get_state(app->pulse_client.operation) == PA_OPERATION_RUNNING && + timeout_count < MAX_REFRESH_TIMEOUT) { + pulse_client_iterate(&app->pulse_client); + g_usleep(DELAY_5_MS_USEC); + timeout_count++; + } + pa_operation_unref(app->pulse_client.operation); + app->pulse_client.operation = NULL; + } + + // Get current apps list + GList *apps = pulse_client_get_apps(&app->pulse_client); + + // Find all slider widgets and update their stored sink input indexes + GList *widgets = gtk_container_get_children(GTK_CONTAINER(app->volmix_window)); + update_sliders_recursive(widgets, apps); + g_list_free(widgets); +} + +// Recursive function to find and update slider widgets +static void update_sliders_recursive(GList *widgets, GList *apps) +{ + while (widgets) { + GtkWidget *widget = GTK_WIDGET(widgets->data); + + if (GTK_IS_SCALE(widget)) { + // This is a volume slider - check if it has stored app data + uint32_t *stored_index = (uint32_t *)g_object_get_data(G_OBJECT(widget), "sink_input_index"); + char *stored_app_name = (char *)g_object_get_data(G_OBJECT(widget), "app_name"); + + if (stored_index && stored_app_name) { + // Find the current index for this app name + GList *app_item = apps; + while (app_item) { + app_audio_t *app = (app_audio_t *)app_item->data; + if (strcmp(app->name, stored_app_name) == 0) { + if (*stored_index != app->index) { + printf("Updating slider for '%s': index %u -> %u\n", + stored_app_name, *stored_index, app->index); + *stored_index = app->index; + } + break; + } + app_item = app_item->next; + } + } + } else if (GTK_IS_CONTAINER(widget)) { + // Recursively check container widgets + GList *children = gtk_container_get_children(GTK_CONTAINER(widget)); + update_sliders_recursive(children, apps); + g_list_free(children); + } + + widgets = widgets->next; + } +} + int main(int argc, char *argv[]) { // Initialize GTK