281 lines
9.4 KiB
Lua
281 lines
9.4 KiB
Lua
msg = require('mp.msg')
|
|
msg.trace('sane-sysvol: load: begin')
|
|
|
|
non_blocking_popen = require("non_blocking_popen")
|
|
|
|
RD_SIZE = 65536
|
|
|
|
function startswith(superstring, substring)
|
|
return superstring:sub(1, substring:len()) == substring
|
|
end
|
|
function strip_prefix(superstring, substring)
|
|
assert(startswith(superstring, substring))
|
|
return superstring:sub(1 + substring:len())
|
|
end
|
|
|
|
function ltrim(s)
|
|
-- remove all leading whitespace from `s`
|
|
local i = 1
|
|
while s:sub(i, i) == " " or s:sub(i, i) == "\t" do
|
|
i = i + 1
|
|
end
|
|
return s:sub(i)
|
|
end
|
|
|
|
function subprocess(args)
|
|
mp.command_native({
|
|
name = "subprocess",
|
|
args = args,
|
|
-- these arguments below probably don't matter: copied from sane-cast
|
|
detach = false,
|
|
capture_stdout = false,
|
|
capture_stderr = false,
|
|
passthrough_stdin = false,
|
|
playback_only = false,
|
|
})
|
|
end
|
|
|
|
function sysvol_new()
|
|
return {
|
|
-- sysvol is pipewire-native volume
|
|
-- it's the cube of the equivalent 0-100% value represented inside mpv
|
|
sysvol = nil,
|
|
sysmute = nil,
|
|
change_sysvol = function(self, mpv_vol)
|
|
-- called when mpv wants to set the system-wide volume
|
|
if mpv_vol == nil then
|
|
return
|
|
end
|
|
|
|
local old_mpv_vol = nil
|
|
if self.sysvol ~= nil then
|
|
old_mpv_vol = 100 * self.sysvol^(1/3)
|
|
end
|
|
if old_mpv_vol ~= nil and math.abs(mpv_vol - old_mpv_vol) < 1.0 then
|
|
-- avoid near-infinite loop where we react to our own volume change.
|
|
-- consider that we might be a couple messages behind in parsing pipewire when we issue this command,
|
|
-- hence a check on only the pipewire -> mpv side wouldn't prevent oscillation
|
|
msg.debug("NOT setting system-wide volume:", old_mpv_vol, volstr)
|
|
return
|
|
end
|
|
|
|
local volstr = tostring(mpv_vol) .. "%"
|
|
msg.debug("setting system-wide volume:", old_mpv_vol, volstr)
|
|
self.sysvol = (0.01*mpv_vol)^3
|
|
subprocess({
|
|
"wpctl",
|
|
"set-volume",
|
|
"@DEFAULT_AUDIO_SINK@",
|
|
volstr
|
|
})
|
|
end,
|
|
on_sysvol_change = function(self, sysvol)
|
|
-- called when the pipewire system volume is changed (either by us, or an external application)
|
|
if sysvol == nil then
|
|
return
|
|
end
|
|
|
|
local new_mpv_vol = 100 * sysvol^(1/3)
|
|
local old_mpv_vol = nil
|
|
if self.sysvol ~= nil then
|
|
old_mpv_vol = 100 * self.sysvol^(1/3)
|
|
end
|
|
|
|
if old_mpv_vol ~= nil and math.abs(new_mpv_vol - old_mpv_vol) < 1.0 then
|
|
-- avoid an infinite loop where we react to our own volume change
|
|
msg.debug("NOT announcing volume change to mpv (because it was what triggered the change):", old_mpv_vol, new_mpv_vol)
|
|
return
|
|
end
|
|
|
|
msg.debug("announcing volume change to mpv:", old_mpv_vol, new_mpv_vol)
|
|
self.sysvol = sysvol
|
|
mp.set_property_number("user-data/sane-sysvol/volume", new_mpv_vol)
|
|
end,
|
|
change_sysmute = function(self, mute)
|
|
if mute == nil then
|
|
return
|
|
end
|
|
if mute == self.sysmute then
|
|
msg.debug("NOT setting system-wide mute (because it didn't change)", mute)
|
|
return
|
|
end
|
|
|
|
local mutestr
|
|
if mute then
|
|
mutestr = "1"
|
|
else
|
|
mutestr = "0"
|
|
end
|
|
msg.debug("setting system-wide mute:", mutestr)
|
|
self.sysmute = mute
|
|
subprocess({
|
|
"wpctl",
|
|
"set-mute",
|
|
"@DEFAULT_AUDIO_SINK@",
|
|
mutestr
|
|
})
|
|
end,
|
|
on_sysmute_change = function(self, mute)
|
|
if mute == nil then
|
|
return
|
|
end
|
|
|
|
msg.debug("announcing mute to mpv:", mute)
|
|
self.sysmute = mute
|
|
mp.set_property_bool("user-data/sane-sysvol/mute", mute)
|
|
end
|
|
}
|
|
end
|
|
|
|
function pwmon_parser_new()
|
|
return {
|
|
-- volume: pipewire-native volume. usually 0.0 - 1.0, but can go higher (e.g. 3.25)
|
|
-- `wpctl get-volume` and this volume are related, in that the volume reported by
|
|
-- wpctl is the cube-root of this one.
|
|
volume = nil, -- number
|
|
mute = nil, -- bool
|
|
|
|
-- parser state:
|
|
in_device = false,
|
|
in_direction = false,
|
|
in_output = false,
|
|
in_vol = false,
|
|
in_mute = false,
|
|
|
|
feed_line = function(self, line)
|
|
msg.trace("pw-mon:", line)
|
|
line = ltrim(line)
|
|
if startswith(line, "changed:") or startswith(line, "added:") or startswith(line, "removed:") then
|
|
self.in_device = false
|
|
self.in_direction = false
|
|
self.in_output = false
|
|
self.in_vol = false
|
|
self.in_mute = false
|
|
self.in_properties = false
|
|
elseif startswith(line, "type: ") then
|
|
self.in_device = startswith(line, "type: PipeWire:Interface:Device")
|
|
msg.trace("parsed type:", line, self.in_device)
|
|
elseif startswith(line, "Prop: ") and self.in_device then
|
|
self.in_direction = startswith(line, "Prop: key Spa:Pod:Object:Param:Route:direction")
|
|
if self.in_direction then
|
|
self.in_output = false
|
|
end
|
|
-- which of the *Volumes params we read is unclear.
|
|
-- alternative to this is to just detect the change, and then cal wpctl get-volume @DEFAULT_AUDIO_SINK@
|
|
self.in_vol = startswith(line, "Prop: key Spa:Pod:Object:Param:Props:channelVolumes")
|
|
self.in_mute = startswith(line, "Prop: key Spa:Pod:Object:Param:Props:mute")
|
|
msg.trace("parsed `Prop:`", line, self.in_vol)
|
|
elseif line:find("Spa:Enum:Direction:Output", 1, true) and self.in_direction then
|
|
self.in_output = true
|
|
elseif startswith(line, "Float ") and self.in_device and self.in_output and self.in_vol then
|
|
value = tonumber(strip_prefix(line, "Float "))
|
|
self:feed_volume(value)
|
|
elseif startswith(line, "Bool ") and self.in_device and self.in_output and self.in_mute then
|
|
value = strip_prefix(line, "Bool ") == "true"
|
|
self:feed_mute(value)
|
|
elseif startswith(line, "properties:") and self.in_device then
|
|
self.in_properties = true
|
|
end
|
|
end,
|
|
|
|
feed_volume = function(self, vol)
|
|
msg.debug("volume:", vol)
|
|
self.volume = vol
|
|
end,
|
|
feed_mute = function(self, mute)
|
|
msg.debug("mute:", mute)
|
|
self.mute = mute
|
|
end,
|
|
-- get_effective_volume = function(self)
|
|
-- if self.mute then
|
|
-- return 0
|
|
-- else
|
|
-- return self.volume
|
|
-- end
|
|
-- end
|
|
}
|
|
end
|
|
|
|
function pwmon_new()
|
|
return {
|
|
-- non_blocking_popen handle for the pw-mon process
|
|
-- which can be periodically read and parsed to detect volume changes
|
|
handle = non_blocking_popen.non_blocking_popen("pw-mon", RD_SIZE),
|
|
stdout_unparsed = "",
|
|
pwmon_parser = pwmon_parser_new(),
|
|
service = function(self)
|
|
-- do a single non-blocking read, and parse the result
|
|
-- in the *rare* case in which more than RD_SIZE data is ready, we service that remaining data on the next call
|
|
local buf, res = self.handle:read(RD_SIZE)
|
|
if res == "closed" then
|
|
msg.debug("pw-mon unexpectedly closed!")
|
|
end
|
|
if buf ~= nil then
|
|
local old_vol = self.pwmon_parser.volume
|
|
local old_mute = self.pwmon_parser.mute
|
|
self.stdout_unparsed = self.stdout_unparsed .. buf
|
|
self:consume_stdout()
|
|
local new_vol = self.pwmon_parser.volume
|
|
local new_mute = self.pwmon_parser.mute
|
|
|
|
if new_vol ~= old_vol then
|
|
msg.debug("pipewire volume change:", old_vol, new_vol)
|
|
mp.set_property_number("user-data/sane-sysvol/pw-mon-volume", new_vol)
|
|
end
|
|
if new_mute ~= old_mute then
|
|
msg.debug("pipewire mute change:", old_mute, new_mute)
|
|
mp.set_property_bool("user-data/sane-sysvol/pw-mon-mute", new_mute)
|
|
end
|
|
end
|
|
end,
|
|
consume_stdout = function(self)
|
|
local idx_newline, next_newline = 0, 0
|
|
while next_newline ~= nil do
|
|
next_newline = self.stdout_unparsed:find("\n", idx_newline + 1, true)
|
|
if next_newline ~= nil then
|
|
self.pwmon_parser:feed_line(self.stdout_unparsed:sub(idx_newline + 1, next_newline - 1))
|
|
idx_newline = next_newline
|
|
end
|
|
end
|
|
self.stdout_unparsed = self.stdout_unparsed:sub(idx_newline + 1)
|
|
end,
|
|
}
|
|
end
|
|
|
|
mp.set_property_number("user-data/sane-sysvol/volume", 0)
|
|
mp.set_property_bool("user-data/sane-sysvol/mute", true)
|
|
|
|
local sysvol = sysvol_new()
|
|
local first_sysvol_announcement = true
|
|
mp.observe_property("user-data/sane-sysvol/volume", "native", function(_, val)
|
|
-- we must set the volume property early -- before we actually know the volume
|
|
-- else other modules will think it's `nil` and error.
|
|
-- but we DON'T want the value we set to actually impact the system volume
|
|
if not first_sysvol_announcement then
|
|
sysvol:change_sysvol(val)
|
|
end
|
|
first_sysvol_announcement = false
|
|
end)
|
|
mp.observe_property("user-data/sane-sysvol/pw-mon-volume", "native", function(_, val)
|
|
sysvol:on_sysvol_change(val)
|
|
end)
|
|
|
|
local first_sysmute_announcement = true
|
|
mp.observe_property("user-data/sane-sysvol/mute", "native", function(_, val)
|
|
-- we must set the mute property early -- before we actually know the mute
|
|
-- else other modules will think it's `nil` and error.
|
|
-- but we DON'T want the value we set to actually impact the system mute
|
|
if not first_sysmute_announcement then
|
|
sysvol:change_sysmute(val)
|
|
end
|
|
first_sysmute_announcement = false
|
|
end)
|
|
mp.observe_property("user-data/sane-sysvol/pw-mon-mute", "native", function(_, val)
|
|
sysvol:on_sysmute_change(val)
|
|
end)
|
|
|
|
local pwmon = pwmon_new()
|
|
mp.register_event('tick', function() pwmon:service() end)
|
|
|
|
msg.trace("sane-sysvol: load: complete")
|