From 69b9a14b960526d08e145b37fcad6b236ec8ec45 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 08:57:15 +0200 Subject: [PATCH 1/2] fix(wireplumber): show correct volume on Bluetooth sinks The mixer-api is configured with the linear scale (0), so volume_ holds the raw linear gain. The perceptual "cubic" value shown by wpctl and exposed as {volume} is cbrt(linear), but update() computed pow(volume_, 3) instead. Cubing under-reads every volume below max and collapses small linear gains to 0% -- which is why the default Bluetooth sink (whose normal levels map to low linear gains, e.g. wpctl 0.55 -> linear 0.166) displayed 0% while wpctl reported a normal, unmuted volume. Replace the inverted conversions with the correct cube-root/cube pair in the display path, the scroll-scale conversions (cubic / cubic_percent), and the max-volume ceiling mapping so scrolling and the cap stay consistent with the corrected {volume}. Fixes #5159. --- src/modules/wireplumber.cpp | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 876c199d..74563c5d 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -547,8 +547,11 @@ auto waybar::modules::Wireplumber::update() -> void { label_.get_style_context()->remove_class("source-muted"); } - double vol_cube = pow(volume_, 3); - double source_vol_cube = pow(source_volume_, 3); + // mixer-api is configured with the linear scale, so volume_ holds the raw linear gain. The + // perceptual "cubic" value shown by wpctl and used for {volume} is cbrt(linear), not linear^3. + // Cubing here drives small linear gains (typical for Bluetooth sinks) to 0%. + double vol_cube = cbrt(volume_); + double source_vol_cube = cbrt(source_volume_); int vol = round(vol_cube * 100.0); int source_vol = round(source_vol_cube * 100.0); @@ -622,10 +625,10 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { step = config_["scroll-step"].asDouble(); } if (config_["max-volume"].isDouble()) { - // {volume} is displayed as cubic-percent (pow(volume_, 3) * 100), while volume_/newVol are + // {volume} is displayed as cubic-percent (cbrt(volume_) * 100), while volume_/newVol are // linear gains. Map the documented cubic-percent ceiling into the linear domain the clamp - // operates in, restoring the 0.15.0 cap semantics (e.g. 130 -> cbrt(1.3) linear -> 130%). - maxVolume = cbrt(config_["max-volume"].asDouble() / 100.0); + // operates in, restoring the 0.15.0 cap semantics (e.g. 130 -> pow(1.3, 3) linear -> 130%). + maxVolume = pow(config_["max-volume"].asDouble() / 100.0, 3); } double vol = volume_; @@ -635,11 +638,11 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { } if (scale == "cubic") { - vol = pow(vol, 3); + vol = cbrt(vol); } else if (scale == "db") { vol = log10(vol) * 20.0; } else if (scale == "cubic_percent") { - vol = pow(vol, 3) * 100.0; + vol = cbrt(vol) * 100.0; } double newVol = vol; @@ -650,11 +653,11 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { } if (scale == "cubic") { - newVol = cbrt(newVol); + newVol = pow(newVol, 3); } else if (scale == "db") { newVol = exp10(newVol / 20.0); } else if (scale == "cubic_percent") { - newVol = cbrt(newVol / 100.0); + newVol = pow(newVol / 100.0, 3); } if (dir == SCROLL_DIR::UP) { From 2650f062b5778571a1592de33b05f78198cec068 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 08:57:36 +0200 Subject: [PATCH 2/2] fix(wireplumber): apply source-muted for Audio/Source modules For a module configured node-type "Audio/Source", node_id_ and source_node_id_ resolve to the same source node, so its primary mute state is stored in muted_. update() unconditionally mapped muted_ to the muted/sink-muted classes and source-muted only to the secondary source_muted_ flag, so a source module could never receive source-muted -- only the sink classes. Gate the mute-class selection on the configured node-type: a source-type module drives source-muted from its primary mute state, while a sink-type module keeps muted/sink-muted for its sink and source-muted for the secondary default source it tracks for {format_source}. The primary node still feeds {volume} via updateVolume, so source-widget volume rendering is unaffected. Fixes #4523. --- src/modules/wireplumber.cpp | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 74563c5d..b3167381 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -527,24 +527,39 @@ auto waybar::modules::Wireplumber::update() -> void { label_.get_style_context()->remove_class("bluetooth"); } - // Handle sink mute state + // A module configured with node-type "Audio/Source" tracks a source as its primary node, so its + // primary mute state (muted_) must drive the source-muted class rather than the sink classes. + const bool is_source_type = g_strcmp0(type_, "Audio/Source") == 0; + + // Handle primary node mute state if (muted_) { // Check muted bluetooth format exists, otherwise fall back to default muted format. if (format_name != "format" && !config_[format_name + "-muted"].isString()) format_name = "format"; format_name += "-muted"; - label_.get_style_context()->add_class("muted"); - label_.get_style_context()->add_class("sink-muted"); + if (is_source_type) { + label_.get_style_context()->add_class("source-muted"); + } else { + label_.get_style_context()->add_class("muted"); + label_.get_style_context()->add_class("sink-muted"); + } } else { - label_.get_style_context()->remove_class("muted"); - label_.get_style_context()->remove_class("sink-muted"); + if (is_source_type) { + label_.get_style_context()->remove_class("source-muted"); + } else { + label_.get_style_context()->remove_class("muted"); + label_.get_style_context()->remove_class("sink-muted"); + } } - // Handle source mute state - if (source_muted_) { - label_.get_style_context()->add_class("source-muted"); - } else { - label_.get_style_context()->remove_class("source-muted"); + // Handle the secondary source mute state (only relevant for sink modules, which additionally + // track the default source for {format_source}). A source module already owns source-muted above. + if (!is_source_type) { + if (source_muted_) { + label_.get_style_context()->add_class("source-muted"); + } else { + label_.get_style_context()->remove_class("source-muted"); + } } // mixer-api is configured with the linear scale, so volume_ holds the raw linear gain. The