diff --git a/README.md b/README.md index 2fcf6da..d1cb775 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,6 @@ A widget for the Awesome Window Manager (version 4.x) that uses [pulseaudio_dbus](https://github.com/stefano-m/lua-pulseaudio_dbus) to control your audio devices. -## A note about PulseAudio, DBus and Awesome - -The Pulseaudio DBus interface requires clients to use peer-to-peer connection -rather than the usual system/session buses. This means that we *cannot* use the -Awesome DBus API that supports *only* system and session buses. - -The solution is to run an external client application to establish a -peer-to-peer connection and listen to DBus signals. The output of the client is -read by the widget that updates itself accordingly. This is done thanks -to -[`awful.spawn.with_line_callback`](https://awesomewm.org/apidoc/libraries/awful.spawn.html#with_line_callback). - # Requirements In addition to the requirements listed in the `rockspec` file, you will need diff --git a/pulseaudio_widget-0.3.1-1.rockspec b/pulseaudio_widget-0.3.1-1.rockspec new file mode 100644 index 0000000..5fffa80 --- /dev/null +++ b/pulseaudio_widget-0.3.1-1.rockspec @@ -0,0 +1,27 @@ +package = "pulseaudio_widget" +version = "0.3.1-1" +source = { + url = "git://github.com/stefano-m/awesome-pulseaudio_widget", + tag = "v0.3.1" +} +description = { + summary = "A PulseAudio widget for the Awesome Window Manager", + detailed = [[ + Control your audio in the Awesome with PulseAudio and DBus. + ]], + homepage = "https://github.com/stefano-m/awesome-pulseaudio_widget", + license = "GPL v3" +} +supported_platforms = { + "linux" +} +dependencies = { + "lua >= 5.1", + "pulseaudio_dbus >= 0.11.0, < 0.12" +} +build = { + type = "builtin", + modules = { + pulseaudio_widget = "pulseaudio_widget.lua" + } +} diff --git a/pulseaudio_widget-devel-1.rockspec b/pulseaudio_widget-devel-1.rockspec index 793ec93..3a33ab4 100644 --- a/pulseaudio_widget-devel-1.rockspec +++ b/pulseaudio_widget-devel-1.rockspec @@ -19,6 +19,5 @@ dependencies = { supported_platforms = { "linux" } build = { type = "builtin", - modules = { pulseaudio_widget = "pulseaudio_widget.lua", - pulseaudio_widget_client = "pulseaudio_widget_client.lua"}, + modules = { pulseaudio_widget = "pulseaudio_widget.lua" }, } diff --git a/pulseaudio_widget.lua b/pulseaudio_widget.lua index 8af9acb..a14d2c8 100644 --- a/pulseaudio_widget.lua +++ b/pulseaudio_widget.lua @@ -17,8 +17,6 @@ This program was inspired by the [Awesome Pulseaudio Widget (APW)](https://github.com/mokasin/apw) ]] - -local awesome = awesome -- luacheck: ignore local string = string local awful = require("awful") @@ -124,63 +122,44 @@ function widget.toggle_muted_mic() end end -function widget:kill_client() - if type(self.server_pid) == "number" then - awful.spawn("kill -TERM " .. self.server_pid) - end -end - -function widget:run_client() - - local function update_after_signal(line, regex, sub, sep) - sep = sep or " " - local v, found = line:gsub(regex, sub) - if found ~= 0 then - local idx = v:find(sep) - local vol = v:sub(1, idx - 1) - local path = v:sub(idx + 1) - if path:find("/sink%d+$") then - self:update_appearance(vol) - self.notify(vol) - end - end - end - - local pid = awful.spawn.with_line_callback( - [[lua -e 'require("pulseaudio_widget_client")']], - { - stdout = function (line) - - update_after_signal(line, "^(VolumeUpdated:%s+)(%d+)(|)([%w/]+)", "%2 %4") - - update_after_signal(line, "^(MuteUpdated:%s+)(%w+)(|)([%w/]+)", "%2 %4") - - local v, found - v, found = line:gsub("^(NewSink:%s+)(/.*%w)", "%2") - if found ~=0 then - self:update_sink(v) - local volume = self.sink:is_muted() and "Muted" or self.sink:get_volume_percent()[1] - self:update_appearance(volume) - self.notify(volume) - end - - v, found = line:gsub("^(NewSource:%s+)(/.*%w)", "%2") - if found ~=0 then - self:update_source({v}) - end - end - }) - - self.server_pid = pid -end - widget:buttons(gears.table.join( awful.button({ }, 1, widget.toggle_muted), awful.button({ }, 3, function () awful.spawn(widget.mixer) end), awful.button({ }, 4, widget.volume_up), awful.button({ }, 5, widget.volume_down))) -awesome.connect_signal("exit", function () widget:kill_client() end) +function widget:connect_device(device) + if not device then + return + end + + if device.signals.VolumeUpdated then + device:connect_signal( + function (this, vols) + -- FIXME: BaseVolume for sources (i.e. microphones) won't give the correct percentage + local v = math.ceil(tonumber(vols[1][1]) / this.BaseVolume * 100) + if this.object_path == self.sink.object_path then + self:update_appearance(v) + self.notify(v) + end + end, + "VolumeUpdated" + ) + end + + if device.signals.MuteUpdated then + device:connect_signal( + function (this, is_mute) + local m = is_mute[1] and "Muted" or "Unmuted" + if this.object_path == self.sink.object_path then + self:update_appearance(m) + self.notify(m) + end + end, + "MuteUpdated" + ) + end +end function widget:init() local status, address = pcall(pulse.get_address) @@ -196,15 +175,40 @@ function widget:init() self.connection = pulse.get_connection(address) self.core = pulse.get_core(self.connection) + -- listen on ALL objects as sinks and sources may change + self.core:ListenForSignal("org.PulseAudio.Core1.Device.VolumeUpdated", {}) + self.core:ListenForSignal("org.PulseAudio.Core1.Device.MuteUpdated", {}) + + self.core:ListenForSignal("org.PulseAudio.Core1.NewSink", {self.core.object_path}) + self.core:connect_signal( + function (_, newsinks) + self:update_sink(newsinks[1]) + self:connect_device(self.sink) + local volume = self.sink:is_muted() and "Muted" or self.sink:get_volume_percent()[1] + self:update_appearance(volume) + end, + "NewSink" + ) + + self.core:ListenForSignal("org.PulseAudio.Core1.NewSource", {self.core.object_path}) + self.core:connect_signal( + function (_, newsources) + self:update_source(newsources) + self:connect_device(self.source) + end, + "NewSource" + ) + self:update_source(self.core:get_sources()) + self:connect_device(self.source) local sink_path = assert(self.core:get_sinks()[1], "No sinks found") self:update_sink(sink_path) + self:connect_device(self.sink) + local volume = self.sink:is_muted() and "Muted" or self.sink:get_volume_percent()[1] self:update_appearance(volume) - self:run_client() - self.__index = self return self diff --git a/pulseaudio_widget_client.lua b/pulseaudio_widget_client.lua deleted file mode 100644 index fb628f4..0000000 --- a/pulseaudio_widget_client.lua +++ /dev/null @@ -1,110 +0,0 @@ ---[[ - Copyright 2017 Stefano Mazzucco - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -]] - ---[[-- This module is meant to be run with - [`awful.spawn.with_line_callback`](https://awesomewm.org/apidoc/libraries/awful.spawn.html#with_line_callback). - - It starts a client that listens to the pulseaudio DBus server and prints to - standard output wheter the volume or sinks (e.g. selecting the audio from the - TV) change. The changes are printed to standard output and are used by the - pulseaudio widget to change its appearance. - - We must do this because Awesome's DBus API can only connect to system and - session buses, but pulseaudio uses its own per-user connection. - -]] - -local pulse = require("pulseaudio_dbus") -local GLib = require("lgi").GLib - -local address = pulse.get_address() - -local connection = pulse.get_connection(address) -local core = pulse.get_core(connection) -local sink = pulse.get_device(connection, core.Sinks[1]) -local source - -local function get_source(conn, sources) - for _, source_path in ipairs(sources) do - local s = pulse.get_device(conn, source_path) - if s.Name and not s.Name:match("%.monitor$") then - return s - end - end -end - -source = get_source(connection, core.Sources) - --- listen on ALL objects as sinks may change -core:ListenForSignal("org.PulseAudio.Core1.Device.VolumeUpdated", {}) -core:ListenForSignal("org.PulseAudio.Core1.Device.MuteUpdated", {}) - -local function connect_device(s) - - if s.signals.VolumeUpdated then - s:connect_signal( - function (self, vols) - local v = math.ceil(tonumber(vols[1][1]) / self.BaseVolume * 100) - -- FIXME: BaseVolume for sources (i.e. microphones) won't give the correct percentage - print(string.format("VolumeUpdated: %s|%s", v, self.object_path)) - end, - "VolumeUpdated" - ) - end - - if s.signals.MuteUpdated then - s:connect_signal( - function (self, is_mute) - local m = is_mute[1] and "Muted" or "Unmuted" - print(string.format("MuteUpdated: %s|%s", m, self.object_path)) - end, - "MuteUpdated" - ) - end -end - -connect_device(sink) -if source then - connect_device(source) -end - -core:ListenForSignal("org.PulseAudio.Core1.NewSink", {core.object_path}) -core:connect_signal( - function (_, newsinks) - print(string.format("NewSink: %s", newsinks[1])) - sink = pulse.get_device(connection, newsinks[1]) - connect_device(sink) - end, - "NewSink" -) - -core:ListenForSignal("org.PulseAudio.Core1.NewSource", {core.object_path}) -core:connect_signal( - function (_, newsources) - print(string.format("NewSource: %s", newsources[1])) - source = get_source(connection, newsources) - if source then - connect_device(source) - end - end, - "NewSource" -) - --- Start the client. Send SIGTERM to stop it. -print("Starting Awesome PulseAudio Widget Client") -GLib.MainLoop():run()