Replaced volume with event-based version for pipewire

This commit is contained in:
Mutzi 2024-07-08 00:06:34 +02:00
parent 2b8f18c58c
commit b4675cc0b3
Signed by: root
GPG Key ID: 2437494E09F13876
7 changed files with 29071 additions and 49 deletions

View File

@ -5,11 +5,14 @@ find_package(PkgConfig)
set(POSITION_INDEPENDENT_CODE YES) set(POSITION_INDEPENDENT_CODE YES)
set(C_STANDARD 11) set(C_STANDARD 11)
add_compile_options(-Wall -Wextra -pedantic) add_compile_options(-Wall -Wextra)
pkg_check_modules(CONSTRAIN REQUIRED x11 xi xfixes xrandr) pkg_check_modules(CONSTRAIN REQUIRED x11 xi xfixes xrandr)
add_library(constrain SHARED lib/constrain.c) add_library(constrain SHARED lib/constrain.c)
target_link_libraries(constrain ${CONSTRAIN_LIBRARIES}) target_link_libraries(constrain ${CONSTRAIN_LIBRARIES})
target_include_directories(constrain PUBLIC ${CONSTRAIN_INCLUDE_DIRS}) target_include_directories(constrain PUBLIC ${CONSTRAIN_INCLUDE_DIRS})
pkg_check_modules(PWCALL REQUIRED luajit gobject-2.0)
add_library(pwcall SHARED lib/pwcall.cxx)
target_link_libraries(pwcall ${PWCALL_LIBRARIES})
target_include_directories(pwcall PUBLIC ${PWCALL_INCLUDE_DIRS})

View File

@ -13,9 +13,13 @@ deploy: build
.PHONY: build .PHONY: build
build: build:
cmake -S . -B build cmake -S . -B build -G Ninja
cmake --build build cmake --build build
.PHONY: clean
clean:
rm -vfr build
.PHONY: luacheck .PHONY: luacheck
luacheck: luacheck:
luacheck $(LUA_FILES) luacheck $(LUA_FILES)

55
lib/pwcall.cxx Normal file
View File

@ -0,0 +1,55 @@
#define SOL_ALL_SAFETIES_ON 1
#include <glib.h>
#include <glib-object.h>
#include "sol.hpp"
static guint default_node_sig = 0, get_volume_sig = 0, set_volume_sig = 0;
static guint default_node(gpointer api) {
if (default_node_sig == 0)
default_node_sig = g_signal_lookup("get-default-node", G_TYPE_FROM_INSTANCE(api));
guint retval;
g_signal_emit(api, default_node_sig, 0, "Audio/Sink", &retval);
return retval;
}
static std::tuple<guint, bool> get_volume(gpointer api, guint32 id) {
if (get_volume_sig == 0)
get_volume_sig = g_signal_lookup("get-volume", G_TYPE_FROM_INSTANCE(api));
GVariant *variant = nullptr;
g_signal_emit(api, get_volume_sig, 0, id, &variant);
gdouble volumed;
gboolean mute;
g_variant_lookup(variant, "volume", "d", &volumed);
g_variant_lookup(variant, "mute", "b", &mute);
if (variant) g_variant_unref(variant);
return std::tuple<guint, bool>(std::round(volumed * 100.0), mute == 1);
}
static void set_volume(gpointer api, guint32 id, guint voli, bool mute) {
if (set_volume_sig == 0)
set_volume_sig = g_signal_lookup("set-volume", G_TYPE_FROM_INSTANCE(api));
g_auto(GVariantBuilder) b = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE_VARDICT);
g_variant_builder_add(&b, "{sv}", "volume", g_variant_new_double(((gdouble)voli)/100));
g_variant_builder_add(&b, "{sv}", "mute", g_variant_new_boolean(mute ? TRUE : FALSE));
GVariant *variant = g_variant_builder_end(&b);
gboolean res;
g_signal_emit(api, set_volume_sig, 0, id, variant, &res);
}
extern "C" int luaopen_libpwcall(lua_State *L) {
sol::state_view lua(L);
auto tbl = lua.create_table();
tbl.set_function("default_node", default_node);
tbl.set_function("get_volume", get_volume);
tbl.set_function("set_volume", set_volume);
sol::stack::push(L, tbl);
return 1;
}

28907
lib/sol.hpp Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,3 @@
local fs = require('gears.filesystem')
print(awesome.conffile)
return { return {
conf_dir = awesome.conffile:match('^(.+)/') conf_dir = awesome.conffile:match('^(.+)/')
} }

View File

@ -1,23 +1,101 @@
-- https://github.com/Alexays/Waybar/blob/master/src/modules/wireplumber.cpp
-- https://gitlab.freedesktop.org/pipewire/wireplumber/-/blob/master/modules/module-default-nodes-api.c
-- https://gitlab.freedesktop.org/pipewire/wireplumber/-/blob/master/modules/module-mixer-api.c
local volume_changed_signal = 'volume_changed' local volume_changed_signal = 'volume_changed'
local function toggle_mute() if _G.volume_control == nil then
os.execute('wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle') print('[volume_control] init')
awesome.emit_signal(volume_changed_signal) local lgi = require('lgi')
end ---@class (exact) VolumeControl
---@field pw_call {}
---@field lgi any
---@field wp any
---@field glib any
---@field gobject any
---@field core any
---@field om any
---@field node_api { _native: any } | nil
---@field mixer_api { scale: number, _native: any } | nil
---@field node_id integer
---@field volume integer
---@field mute boolean
local volume_control = {
pw_call = require('libpwcall'),
lgi = lgi,
wp = lgi.Wp,
glib = lgi.GLib,
gobject = lgi.GObject,
core = nil,
om = nil,
node_api = nil,
mixer_api = nil,
node_id = 0,
volume = 0,
mute = false
}
local function vol_up() function volume_control:get_default_node() self.node_id = volume_control.pw_call.default_node(self.node_api._native) end
os.execute('wpctl set-volume @DEFAULT_AUDIO_SINK@ 1%+') function volume_control:set_volume(vol, m) volume_control.pw_call.set_volume(self.mixer_api._native, self.node_id, vol, m) end
awesome.emit_signal(volume_changed_signal) function volume_control:toggle_mute() self:set_volume(self.volume, not self.mute) end
end function volume_control:volume_delta(d) self:set_volume(self.volume + d, self.mute) end
function volume_control:get_volume()
self.volume, self.mute = volume_control.pw_call.get_volume(self.mixer_api._native, self.node_id)
awesome.emit_signal(volume_changed_signal, self.volume, self.mute)
end
local function vol_down() function volume_control:om_installed()
os.execute('wpctl set-volume @DEFAULT_AUDIO_SINK@ 1%-') self:get_default_node()
awesome.emit_signal(volume_changed_signal) self:get_volume()
self.gobject.signal_connect_closure(self.node_api, 'changed', self.gobject.Closure(function(_)
volume_control:get_default_node()
volume_control:get_volume()
end))
self.gobject.signal_connect_closure(self.mixer_api, 'changed', self.gobject.Closure(function(_, nid)
if nid ~= volume_control.node_id then return end
volume_control:get_volume()
end))
end
function volume_control:init()
self.wp.init(self.wp.InitFlags.PIPEWIRE)
self.core = self.wp.Core.new(nil, nil, nil)
self.om = self.wp.ObjectManager.new()
self.om.on_installed = function() self:om_installed() end
local int = self.wp.ObjectInterest.new_type('WpNode')
int:add_constraint(self.wp.ConstraintType.PW_PROPERTY, 'media.class', self.wp.ConstraintVerb.EQUALS, self.glib.Variant.new_string('Audio/Sink'))
self.om:add_interest_full(int)
if self.core:connect() == 0 then error('Failed to connect to pipewire') end
local pending = 2
local function plugin_loaded(p, res)
if volume_control.wp.Object.activate_finish(p, res) == 0 then error('Failed to activate plugin') end
pending = pending - 1
if pending ~= 0 then return end
volume_control.core:install_object_manager(volume_control.om)
end
self.core:load_component('libwireplumber-module-default-nodes-api', 'module', nil, 'default-nodes-api', nil, function(_, res)
if volume_control.core:load_component_finish(res, nil) == 0 then error('Failed to load default-nodes-api') end
volume_control.core:load_component('libwireplumber-module-mixer-api', 'module', nil, 'mixer-api', nil, function(_, res2)
if volume_control.core:load_component_finish(res2, nil) == 0 then error('Failed to load mixer-api') end
volume_control.node_api = volume_control.wp.Plugin.find(volume_control.core, 'default-nodes-api')
volume_control.wp.Object.activate(volume_control.node_api, volume_control.wp.PluginFeatures.ENABLED, nil, plugin_loaded)
volume_control.mixer_api = volume_control.wp.Plugin.find(volume_control.core, 'mixer-api')
volume_control.mixer_api.scale = 1
volume_control.wp.Object.activate(volume_control.mixer_api, volume_control.wp.PluginFeatures.ENABLED, nil, plugin_loaded)
end, nil)
end, nil)
end
volume_control:init()
_G.volume_control = volume_control
end end
return { return {
volume_changed_signal = volume_changed_signal, volume_changed_signal = volume_changed_signal,
toggle_mute = toggle_mute, toggle_mute = function() _G.volume_control:toggle_mute() end,
vol_up = vol_up, vol_up = function() _G.volume_control:volume_delta(1) end,
vol_down = vol_down, vol_down = function() _G.volume_control:volume_delta(-1) end,
} }

View File

@ -1,44 +1,23 @@
local wibox = require('wibox') local wibox = require('wibox')
local awful = require('awful') local awful = require('awful')
local gears = require('gears')
local theme = require('src.theme') local theme = require('src.theme')
local volume = require('src.util.volume') local volume = require('src.util.volume')
local volumeicon = wibox.widget.imagebox(theme.widget_vol_mute) local volumeicon = wibox.widget.imagebox(theme.widget_vol_mute)
local volume_widget = wibox.widget.textbox() local volume_widget = wibox.widget.textbox()
local function spawn_callback(stdout) awesome.connect_signal(volume.volume_changed_signal, function(vol, mute)
local volume_str = stdout:sub(9,12):gsub('[.]', '')
local mute = stdout:match('MUTED')
local level = tonumber(volume_str)
if level == nil then
volume_widget:set_markup('Waiting')
return
end
if mute then if mute then
volumeicon:set_image(theme.widget_vol_mute) volumeicon:set_image(theme.widget_vol_mute)
elseif level == 0 then elseif vol == 0 then
volumeicon:set_image(theme.widget_vol_mute) volumeicon:set_image(theme.widget_vol_mute)
elseif level <= 50 then elseif vol <= 50 then
volumeicon:set_image(theme.widget_vol_low) volumeicon:set_image(theme.widget_vol_low)
else else
volumeicon:set_image(theme.widget_vol_high) volumeicon:set_image(theme.widget_vol_high)
end end
volume_widget:set_markup(level .. (mute and 'M' or '%')) volume_widget.text = vol .. (mute and 'M' or '%')
end end)
local function timer_callback()
awful.spawn.easy_async('wpctl get-volume @DEFAULT_AUDIO_SINK@', spawn_callback)
end
local volume_timer = gears.timer {
timeout = 30,
autostart = true,
call_now = true,
callback = timer_callback
}
awesome.connect_signal(volume.volume_changed_signal, function() volume_timer:emit_signal('timeout') end)
local volumebuttons = awful.util.table.join( local volumebuttons = awful.util.table.join(
awful.button({}, 1, volume.toggle_mute), awful.button({}, 1, volume.toggle_mute),