From a69f3bbc644b88e095c1ee122dc3a664ad6e34c5 Mon Sep 17 00:00:00 2001 From: Zedhugh Chen Date: Wed, 11 Feb 2026 02:22:49 +0800 Subject: [PATCH] =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=A0=8F=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=9F=B3=E9=87=8F=E6=98=BE=E7=A4=BA=E5=B9=B6=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=9F=B3=E9=87=8F=E8=B0=83=E8=8A=82=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 3 + src/action.c | 11 +++ src/action.h | 2 + src/audio.c | 175 +++++++++++++++++++++++++++++++++++++++++++ src/audio.h | 20 +++++ src/default_config.h | 9 +++ src/monitor.c | 14 +++- src/status.c | 10 +++ src/status.h | 4 + 9 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 src/audio.c create mode 100644 src/audio.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 26b807a..47b0871 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,7 @@ add_executable(${APP_NAME} ${SOURCE_DIR}/layout.c ${SOURCE_DIR}/xres.c ${SOURCE_DIR}/status.c + ${SOURCE_DIR}/audio.c ) find_package(PkgConfig REQUIRED) @@ -66,6 +67,8 @@ pkg_check_modules(deps REQUIRED cairo-xcb pango pangocairo + + libpulse-mainloop-glib ) target_include_directories(${APP_NAME} SYSTEM diff --git a/src/action.c b/src/action.c index 621b59f..faa7e73 100644 --- a/src/action.c +++ b/src/action.c @@ -3,6 +3,7 @@ #include #include +#include "audio.h" #include "client.h" #include "monitor.h" #include "types.h" @@ -76,6 +77,16 @@ void raise_or_run(const user_action_arg_t *arg) { spawn(&arguments); } +void toggle_mute(const user_action_arg_t *arg) { + if (wm.status->pulse_context) toggle_pulse_mute(wm.status->pulse_context); +} + +void change_volume(const user_action_arg_t *arg) { + if (wm.status->pulse_context) { + change_pulse_volume(wm.status->pulse_context, arg->i); + } +} + void toggle_client_floating(const user_action_arg_t *arg) { if (!wm.client_focused) return; diff --git a/src/action.h b/src/action.h index 1b4adb3..d20c367 100644 --- a/src/action.h +++ b/src/action.h @@ -55,6 +55,8 @@ void send_client_to_next_monitor(const user_action_arg_t *arg); void spawn(const user_action_arg_t *arg); void quit(const user_action_arg_t *arg); void raise_or_run(const user_action_arg_t *arg); +void toggle_mute(const user_action_arg_t *arg); +void change_volume(const user_action_arg_t *arg); void toggle_client_floating(const user_action_arg_t *arg); void toggle_client_fullscreen(const user_action_arg_t *arg); void toggle_client_maximize(const user_action_arg_t *arg); diff --git a/src/audio.c b/src/audio.c new file mode 100644 index 0000000..b3e0c8f --- /dev/null +++ b/src/audio.c @@ -0,0 +1,175 @@ +#include "audio.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "app.h" +#include "utils.h" + +struct pulse_context_t { + GMainContext *g_context; + pulse_notify_t callback; + pa_glib_mainloop *loop; + pa_context *context; + pa_cvolume volume; + pulse_t pulse; + uint32_t index; +}; + +static void clean_pulse_context(pulse_context_t *context); +static void init_pulse_context(pulse_context_t *context); +static void state_callback(pa_context *context, void *userdata); +static void subscribe_callback(pa_context *context, + pa_subscription_event_type_t t, uint32_t index, + void *userdata); +static void server_info_callback(pa_context *context, + const pa_server_info *info, void *userdata); +static void sink_info_callback(pa_context *context, const pa_sink_info *info, + int eol, void *userdata); +static void parse_sink(const pa_sink_info *info, pulse_t *pulse); + +pulse_context_t *init_pulse(GMainContext *context, pulse_notify_t callback) { + pulse_context_t *ctx = p_new(pulse_context_t, 1); + ctx->g_context = context; + ctx->callback = callback; + init_pulse_context(ctx); + + return ctx; +} + +void change_pulse_volume(pulse_context_t *context, int step) { + if (!context->pulse.inited || !step || step > 100 || step < -100) return; + + pa_cvolume vol = context->volume; + if (step > 0) { + pa_cvolume_inc(&vol, PA_VOLUME_NORM * step / 100); + if (pa_cvolume_max(&vol) > PA_VOLUME_NORM) { + pa_cvolume_set(&vol, vol.channels, PA_VOLUME_NORM); + } + } else { + pa_cvolume_dec(&vol, PA_VOLUME_NORM * (-step) / 100); + if (pa_cvolume_min(&vol) < PA_VOLUME_MUTED) { + pa_cvolume_set(&vol, vol.channels, PA_VOLUME_MUTED); + } + } + pa_context *ctx = context->context; + uint32_t index = context->index; + pa_context_set_sink_volume_by_index(ctx, index, &vol, nullptr, nullptr); +} + +void toggle_pulse_mute(pulse_context_t *context) { + if (!context->pulse.inited) return; + + pa_context *ctx = context->context; + uint32_t index = context->index; + bool mute = !context->pulse.mute; + pa_context_set_sink_mute_by_index(ctx, index, mute, nullptr, nullptr); +} + +void clean_pulse(pulse_context_t *context) { + clean_pulse_context(context); + p_delete(&context); +} + +void clean_pulse_context(pulse_context_t *context) { + memset(&context->pulse, 0, sizeof(context->pulse)); + if (context->callback) context->callback(&context->pulse); + if (context->context) { + pa_context_disconnect(context->context); + pa_context_unref(context->context); + context->context = nullptr; + } + if (context->loop) pa_glib_mainloop_free(context->loop); + context->pulse.inited = false; + context->loop = nullptr; +} + +void init_pulse_context(pulse_context_t *context) { + if (context->loop) pa_glib_mainloop_free(context->loop); + context->loop = pa_glib_mainloop_new(context->g_context); + pa_mainloop_api *api = pa_glib_mainloop_get_api(context->loop); + context->context = pa_context_new(api, APP_NAME "-audio-status"); + pa_context_connect(context->context, nullptr, PA_CONTEXT_NOFAIL, nullptr); + pa_context_set_state_callback(context->context, state_callback, context); +} + +void state_callback(pa_context *context, void *userdata) { + pulse_context_t *ctx = userdata; + + pa_context_state_t state = pa_context_get_state(context); + if (PA_CONTEXT_IS_GOOD(state)) { + pa_context_set_subscribe_callback(context, subscribe_callback, ctx); + pa_context_subscribe(context, PA_SUBSCRIPTION_MASK_SINK, nullptr, nullptr); + pa_context_get_server_info(context, server_info_callback, ctx); + } else if (state == PA_CONTEXT_FAILED) { + clean_pulse_context(ctx); + init_pulse_context(ctx); + } +} + +void subscribe_callback(pa_context *context, pa_subscription_event_type_t t, + uint32_t index, void *userdata) { + auto facility = t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; + if (facility != PA_SUBSCRIPTION_EVENT_SINK) return; + + pa_subscription_event_type_t event = t & PA_SUBSCRIPTION_EVENT_TYPE_MASK; + if (event == PA_SUBSCRIPTION_EVENT_CHANGE) { + pa_context_get_server_info(context, server_info_callback, userdata); + } +} + +void server_info_callback(pa_context *context, const pa_server_info *info, + void *userdata) { + pa_context_get_sink_info_by_name(context, info->default_sink_name, + sink_info_callback, userdata); +} + +void sink_info_callback(pa_context *context, const pa_sink_info *info, int eol, + void *userdata) { + if (eol != 0 || !info) return; + + pulse_context_t *ctx = userdata; + ctx->pulse.inited = true; + ctx->volume = info->volume; + ctx->index = info->index; + parse_sink(info, &ctx->pulse); + if (ctx->callback) ctx->callback(&ctx->pulse); +} + +static inline int volume_to_percent(pa_volume_t volume) { + return (int)round(((double)volume) * 100 / PA_VOLUME_NORM); +} + +void parse_sink(const pa_sink_info *info, pulse_t *pulse) { + size_t size = sizeof(pulse->device_name); + pulse->mute = info->mute; + pulse->volume_percent = volume_to_percent(pa_cvolume_avg(&info->volume)); + strncpy(pulse->device_name, info->description, size); + + const char *key = nullptr; + void *state = nullptr; + while ((key = pa_proplist_iterate(info->proplist, &state)) != nullptr) { + if (strcmp(key, "api.alsa.path") != 0 && + strcmp(key, "device.description") != 0) { + continue; + } + + const char *value = pa_proplist_gets(info->proplist, key); + size_t len = strnlen(value, size); + if (len && (len < strnlen(pulse->device_name, size))) { + strncpy(pulse->device_name, value, size); + pulse->device_name[size - 1] = '\0'; + } + } +} diff --git a/src/audio.h b/src/audio.h new file mode 100644 index 0000000..1c01a2f --- /dev/null +++ b/src/audio.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +typedef struct pulse_context_t pulse_context_t; + +typedef struct pulse_t { + int volume_percent; + bool mute; + bool inited; + char device_name[254]; +} pulse_t; + +typedef void (*pulse_notify_t)(pulse_t *pulse); + +pulse_context_t *init_pulse(GMainContext *context, pulse_notify_t callback); +void change_pulse_volume(pulse_context_t *context, int step); +void toggle_pulse_mute(pulse_context_t *context); +void clean_pulse(pulse_context_t *context); diff --git a/src/default_config.h b/src/default_config.h index d854a49..bc73600 100644 --- a/src/default_config.h +++ b/src/default_config.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -98,6 +99,14 @@ static const keyboard_t key_list[] = { {modifier_super | modifier_control, XK_space, toggle_client_floating, {0}}, {modifier_super, XK_f, toggle_client_fullscreen, {0}}, {modifier_super, XK_m, toggle_client_maximize, {0}}, + + {modifier_super, XK_Up, change_volume, {.i = 1}}, + {modifier_super, XK_Down, change_volume, {.i = -1}}, + {modifier_super | modifier_shift, XK_m, toggle_mute, {0}}, + {modifier_none, XF86XK_AudioRaiseVolume, change_volume, {.i = 1}}, + {modifier_none, XF86XK_AudioLowerVolume, change_volume, {.i = -1}}, + {modifier_none, XF86XK_AudioMute, toggle_mute, {0}}, + }; static const layout_t layout_list[] = { diff --git a/src/monitor.c b/src/monitor.c index 98cc91e..76d85b8 100644 --- a/src/monitor.c +++ b/src/monitor.c @@ -243,12 +243,22 @@ static void monitor_draw_status(monitor_t *monitor, status_t *status) { if (status == nullptr) return; - char cpu[16], mem[64]; + char cpu[16], mem[64], volume[300]; snprintf(cpu, sizeof(cpu), "%.1lf", status->cpu_usage_percent); snprintf(mem, sizeof(mem), "%s(%.1f%%)", status->mem_usage.mem_used_text, status->mem_usage.mem_percent); + if (status->pulse) { + snprintf(volume, sizeof(volume), "%d%%%s%s%s", + status->pulse->volume_percent, status->pulse->mute ? "M" : "", + strlen(status->pulse->device_name) ? "|" : "", + status->pulse->device_name); + } else { + snprintf(volume, sizeof(volume), "0%%"); + } + char *status_items[] = { - status->net_speed.down, status->net_speed.up, mem, cpu, status->time, + status->net_speed.down, status->net_speed.up, volume, mem, cpu, + status->time, }; int16_t start = monitor->status_extent.start; for (int i = countof(status_items) - 1; i >= 0; i--) { diff --git a/src/status.c b/src/status.c index 473d873..2dcfe75 100644 --- a/src/status.c +++ b/src/status.c @@ -8,6 +8,8 @@ #include #include +#include "audio.h" + static constexpr char CPU_FILE[] = "/proc/stat"; static constexpr char MEM_FILE[] = "/proc/meminfo"; static constexpr char NET_FILE[] = "/proc/net/dev"; @@ -228,13 +230,21 @@ static gboolean update_status(gpointer data) { return true; } +void notify_pulse_change(pulse_t *pulse) { + status.pulse = pulse; + notify_status_change(); +} + void init_status(GMainContext *context, status_changed_notify callback) { listener = callback; + status.pulse_context = init_pulse(context, notify_pulse_change); update_status(nullptr); timer = g_timeout_add_seconds(INTERVAL, update_status, nullptr); } void clean_status(void) { + clean_pulse(status.pulse_context); g_source_remove(timer); + status.pulse_context = nullptr; listener = nullptr; } diff --git a/src/status.h b/src/status.h index 422357c..92d0051 100644 --- a/src/status.h +++ b/src/status.h @@ -3,6 +3,8 @@ #include #include +#include "audio.h" + typedef struct memory_usage_t { char mem_used_text[32]; char swap_used_text[32]; @@ -33,6 +35,8 @@ typedef struct status_t { memory_usage_t mem_usage; double cpu_usage_percent; char time[64]; + pulse_t *pulse; + pulse_context_t *pulse_context; } status_t; typedef void (*status_changed_notify)(status_t *status);