feat(audio-switcher): support input ports (#287)
This commit is contained in:
@@ -40,6 +40,11 @@ or **Inputs**, then choose **Use**. For a disconnected Bluetooth output the same
|
|||||||
button connects it, waits for its PipeWire endpoint, makes it the default, and
|
button connects it, waits for its PipeWire endpoint, makes it the default, and
|
||||||
moves current playback streams to it.
|
moves current playback streams to it.
|
||||||
|
|
||||||
|
When an input exposes multiple hardware ports, such as an internal microphone
|
||||||
|
and a headset microphone, the active port name is shown in the panel and
|
||||||
|
widget. Use the port selector below the input to switch it; **Cycle** also
|
||||||
|
includes every currently available port.
|
||||||
|
|
||||||
Use the pencil button to set a local display name, choose the device icon, and
|
Use the pencil button to set a local display name, choose the device icon, and
|
||||||
change a Bluetooth keybind number. The number is assigned automatically after a
|
change a Bluetooth keybind number. The number is assigned automatically after a
|
||||||
device connects successfully for the first time and can then be changed. Hidden
|
device connects successfully for the first time and can then be changed. Hidden
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ local function activeDevice(kind)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function inputDisplayName(device)
|
||||||
|
if device == nil then return "" end
|
||||||
|
local port = noctalia.string.trim(tostring(device.activePortDescription or ""))
|
||||||
|
return port ~= "" and port or tostring(device.name or "")
|
||||||
|
end
|
||||||
|
|
||||||
local function visibleCount(kind)
|
local function visibleCount(kind)
|
||||||
local count = 0
|
local count = 0
|
||||||
local devices = kind == "output" and asArray(snapshot.outputs) or asArray(snapshot.inputs)
|
local devices = kind == "output" and asArray(snapshot.outputs) or asArray(snapshot.inputs)
|
||||||
@@ -118,7 +124,7 @@ local function volumeCard(kind)
|
|||||||
ui.column({ flexGrow = 1, gap = 2 }, {
|
ui.column({ flexGrow = 1, gap = 2 }, {
|
||||||
ui.label({ text = tr(isOutput and "panel.output_volume" or "panel.input_volume"), fontWeight = "bold" }),
|
ui.label({ text = tr(isOutput and "panel.output_volume" or "panel.input_volume"), fontWeight = "bold" }),
|
||||||
ui.label({
|
ui.label({
|
||||||
text = device and device.name or tr("panel.no_device"),
|
text = device and (isOutput and device.name or inputDisplayName(device)) or tr("panel.no_device"),
|
||||||
fontSize = 11,
|
fontSize = 11,
|
||||||
color = "on_surface_variant",
|
color = "on_surface_variant",
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
@@ -157,7 +163,10 @@ end
|
|||||||
local function deviceSubtitle(device)
|
local function deviceSubtitle(device)
|
||||||
local status = deviceStatus(device)
|
local status = deviceStatus(device)
|
||||||
local description = noctalia.string.trim(tostring(device.description or ""))
|
local description = noctalia.string.trim(tostring(device.description or ""))
|
||||||
|
local port = noctalia.string.trim(tostring(device.activePortDescription or ""))
|
||||||
|
if port ~= "" and port ~= device.name and port ~= description then status = status .. " · " .. port end
|
||||||
if description == "" or description == "(null)" or description:lower() == "null" then return status end
|
if description == "" or description == "(null)" or description:lower() == "null" then return status end
|
||||||
|
if description == device.name or description == port then return status end
|
||||||
return status .. " · " .. description
|
return status .. " · " .. description
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -228,6 +237,21 @@ local function deviceRow(device, kind)
|
|||||||
local disbandAction = function()
|
local disbandAction = function()
|
||||||
sendCommand("remove_output_group", { id = device.id })
|
sendCommand("remove_output_group", { id = device.id })
|
||||||
end
|
end
|
||||||
|
local ports = kind == "input" and asArray(device.ports) or {}
|
||||||
|
local portOptions = {}
|
||||||
|
local selectedPortIndex = 0
|
||||||
|
for index, port in ipairs(ports) do
|
||||||
|
local label = tostring(port.description or port.name or "")
|
||||||
|
if port.available == false then label = tr("device.unavailable_port", { port = label }) end
|
||||||
|
portOptions[#portOptions + 1] = label
|
||||||
|
if port.active then selectedPortIndex = index - 1 end
|
||||||
|
end
|
||||||
|
local selectPortAction = function(index, _label)
|
||||||
|
local port = ports[math.floor(tonumber(index) or -1) + 1]
|
||||||
|
if port ~= nil and not port.active then
|
||||||
|
sendCommand("set_input_port", { id = device.id, port = port.name })
|
||||||
|
end
|
||||||
|
end
|
||||||
local actionText = device.active and tr("device.current")
|
local actionText = device.active and tr("device.current")
|
||||||
or (device.bluetooth and not device.available and tr("actions.connect_and_use") or tr("actions.use"))
|
or (device.bluetooth and not device.available and tr("actions.connect_and_use") or tr("actions.use"))
|
||||||
local actionVariant = device.active and "ghost" or "primary"
|
local actionVariant = device.active and "ghost" or "primary"
|
||||||
@@ -294,6 +318,18 @@ local function deviceRow(device, kind)
|
|||||||
ui.row({ align = "center", gap = 8, padding = 7, radius = 9, border = selectedGroupOutputs[device.id] and "primary" or (device.active and "primary" or "outline/0.55"), borderWidth = 1, fill = selectedGroupOutputs[device.id] and "primary/0.1" or (device.active and "primary/0.07" or "surface/0") }, rowChildren),
|
ui.row({ align = "center", gap = 8, padding = 7, radius = 9, border = selectedGroupOutputs[device.id] and "primary" or (device.active and "primary" or "outline/0.55"), borderWidth = 1, fill = selectedGroupOutputs[device.id] and "primary/0.1" or (device.active and "primary/0.07" or "surface/0") }, rowChildren),
|
||||||
})
|
})
|
||||||
local children = { row }
|
local children = { row }
|
||||||
|
if kind == "input" and #ports > 1 then
|
||||||
|
table.insert(children, ui.row({ gap = 8, align = "center", paddingH = 8 }, {
|
||||||
|
ui.label({ text = tr("device.port"), color = "on_surface_variant", fontSize = 10 }),
|
||||||
|
ui.select({
|
||||||
|
options = portOptions,
|
||||||
|
selectedIndex = selectedPortIndex,
|
||||||
|
flexGrow = 1,
|
||||||
|
enabled = snapshot.busy ~= true,
|
||||||
|
onChange = selectPortAction,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
end
|
||||||
if editingId == device.id then table.insert(children, editor(device)) end
|
if editingId == device.id then table.insert(children, editor(device)) end
|
||||||
return ui.column({ gap = 6 }, children)
|
return ui.column({ gap = 6 }, children)
|
||||||
end
|
end
|
||||||
@@ -350,7 +386,8 @@ end
|
|||||||
local function header()
|
local function header()
|
||||||
local output = activeDevice("output")
|
local output = activeDevice("output")
|
||||||
local input = activeDevice("input")
|
local input = activeDevice("input")
|
||||||
local subtitle = (output and output.name or tr("panel.no_output")) .. " · " .. (input and input.name or tr("panel.no_input"))
|
local subtitle = (output and output.name or tr("panel.no_output")) .. " · "
|
||||||
|
.. (input and inputDisplayName(input) or tr("panel.no_input"))
|
||||||
return ui.row({ align = "center", gap = 8 }, {
|
return ui.row({ align = "center", gap = 8 }, {
|
||||||
ui.glyph({ name = "switch-horizontal", size = 24, color = "primary" }),
|
ui.glyph({ name = "switch-horizontal", size = 24, color = "primary" }),
|
||||||
ui.column({ flexGrow = 1, gap = 2 }, {
|
ui.column({ flexGrow = 1, gap = 2 }, {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
id = "blackbartblues/audio-switcher"
|
id = "blackbartblues/audio-switcher"
|
||||||
name = "Audio Switcher"
|
name = "Audio Switcher"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
plugin_api = 15
|
plugin_api = 15
|
||||||
author = "blackbartblues"
|
author = "blackbartblues"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
+126
-4
@@ -232,6 +232,34 @@ local function propertiesOf(item)
|
|||||||
return type(item.properties) == "table" and item.properties or {}
|
return type(item.properties) == "table" and item.properties or {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- BEGIN INPUT PORT HELPERS
|
||||||
|
local function inputPortText(value)
|
||||||
|
return tostring(value or ""):match("^%s*(.-)%s*$") or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parseInputPorts(item)
|
||||||
|
local activeValue = type(item.active_port) == "table" and item.active_port.name or item.active_port
|
||||||
|
local activeName = inputPortText(activeValue)
|
||||||
|
local ports = {}
|
||||||
|
for _, port in ipairs(type(item.ports) == "table" and item.ports or {}) do
|
||||||
|
if type(port) == "table" then
|
||||||
|
local name = inputPortText(port.name)
|
||||||
|
if name ~= "" then
|
||||||
|
local description = inputPortText(port.description)
|
||||||
|
local availability = inputPortText(port.availability):lower()
|
||||||
|
ports[#ports + 1] = {
|
||||||
|
name = name,
|
||||||
|
description = description ~= "" and description or name,
|
||||||
|
available = availability ~= "not available",
|
||||||
|
active = name == activeName,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ports, activeName
|
||||||
|
end
|
||||||
|
-- END INPUT PORT HELPERS
|
||||||
|
|
||||||
local function bluetoothAddress(properties)
|
local function bluetoothAddress(properties)
|
||||||
local value = trim(properties["api.bluez5.address"])
|
local value = trim(properties["api.bluez5.address"])
|
||||||
if value == "" and properties["device.bus"] == "bluetooth" then
|
if value == "" and properties["device.bus"] == "bluetooth" then
|
||||||
@@ -279,7 +307,16 @@ local function makeInput(item, defaultName)
|
|||||||
local stableId = isBluetooth and bluetoothInputId(address) or tostring(item.name or "")
|
local stableId = isBluetooth and bluetoothInputId(address) or tostring(item.name or "")
|
||||||
local fallback = tostring(properties["device.alias"] or item.description or item.name or stableId)
|
local fallback = tostring(properties["device.alias"] or item.description or item.name or stableId)
|
||||||
if isBluetooth then setPreference(preferences.knownBluetoothInputs, address, fallback) end
|
if isBluetooth then setPreference(preferences.knownBluetoothInputs, address, fallback) end
|
||||||
local icon, iconStyle = resolvedDeviceIcon(stableId, isBluetooth and "headset" or "microphone")
|
local ports, activePortName = parseInputPorts(item)
|
||||||
|
local activePortDescription = ""
|
||||||
|
for _, port in ipairs(ports) do
|
||||||
|
if port.active then activePortDescription = port.description break end
|
||||||
|
end
|
||||||
|
local portLooksLikeHeadset = (activePortName .. " " .. activePortDescription):lower():find("headset", 1, true) ~= nil
|
||||||
|
local icon, iconStyle = resolvedDeviceIcon(
|
||||||
|
stableId,
|
||||||
|
isBluetooth and "headset" or (portLooksLikeHeadset and "headset" or "microphone")
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
id = stableId,
|
id = stableId,
|
||||||
targetName = tostring(item.name or ""),
|
targetName = tostring(item.name or ""),
|
||||||
@@ -295,6 +332,9 @@ local function makeInput(item, defaultName)
|
|||||||
muted = item.mute == true,
|
muted = item.mute == true,
|
||||||
icon = icon,
|
icon = icon,
|
||||||
iconStyle = iconStyle,
|
iconStyle = iconStyle,
|
||||||
|
ports = ports,
|
||||||
|
activePort = activePortName,
|
||||||
|
activePortDescription = activePortDescription,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1051,14 +1091,87 @@ setInput = function(command, id)
|
|||||||
setDefaultTarget(command, "input", device)
|
setDefaultTarget(command, "input", device)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function findInputPort(device, portName)
|
||||||
|
for _, port in ipairs(type(device.ports) == "table" and device.ports or {}) do
|
||||||
|
if port.name == portName then return port end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function setInputPort(command, id, portName)
|
||||||
|
if actionBusy then
|
||||||
|
resultMessage(command, false, noctalia.tr("errors.busy"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local device = findInputById(id)
|
||||||
|
if device == nil then
|
||||||
|
finishAction(command, false, noctalia.tr("errors.device_not_found"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local port = findInputPort(device, portName)
|
||||||
|
if port == nil then
|
||||||
|
finishAction(command, false, noctalia.tr("errors.input_port_not_found"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if port.available == false then
|
||||||
|
finishAction(command, false, noctalia.tr("errors.input_port_unavailable"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if trim(device.targetName) == "" then
|
||||||
|
finishAction(command, false, noctalia.tr("errors.device_unavailable"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
actionBusy = true
|
||||||
|
publishSnapshot()
|
||||||
|
runPactl({ "set-source-port", device.targetName, port.name }, function(portResult)
|
||||||
|
if portResult.exitCode ~= 0 then
|
||||||
|
local message = trim(portResult.stderr)
|
||||||
|
finishAction(command, false, message ~= "" and message or noctalia.tr("errors.input_port_switch_failed"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
runPactl({ "set-default-source", device.targetName }, function(defaultResult)
|
||||||
|
if defaultResult.exitCode ~= 0 then
|
||||||
|
local message = trim(defaultResult.stderr)
|
||||||
|
finishAction(command, false, message ~= "" and message or noctalia.tr("errors.switch_failed"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
moveStreams("input", device.targetName, function()
|
||||||
|
finishAction(
|
||||||
|
command,
|
||||||
|
true,
|
||||||
|
noctalia.tr("notifications.input_port_selected", { port = port.description }),
|
||||||
|
noctalia.getConfig("show_notification_on_switch")
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
local function cycleDevice(command, kind)
|
local function cycleDevice(command, kind)
|
||||||
local devices = kind == "output" and snapshot.outputs or snapshot.inputs
|
local devices = kind == "output" and snapshot.outputs or snapshot.inputs
|
||||||
local visible = {}
|
local visible = {}
|
||||||
local activeIndex = 0
|
local activeIndex = 0
|
||||||
for _, device in ipairs(devices) do
|
for _, device in ipairs(devices) do
|
||||||
if not device.hidden and (kind == "output" or device.available) then
|
if not device.hidden and (kind == "output" or device.available) then
|
||||||
table.insert(visible, device)
|
local ports = kind == "input" and type(device.ports) == "table" and device.ports or {}
|
||||||
if device.active then activeIndex = #visible end
|
if kind == "input" and #ports > 1 then
|
||||||
|
local addedPort = false
|
||||||
|
for _, port in ipairs(ports) do
|
||||||
|
if port.available ~= false then
|
||||||
|
table.insert(visible, { id = device.id, port = port.name })
|
||||||
|
addedPort = true
|
||||||
|
if device.active and port.active then activeIndex = #visible end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not addedPort then
|
||||||
|
table.insert(visible, device)
|
||||||
|
if device.active then activeIndex = #visible end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(visible, device)
|
||||||
|
if device.active then activeIndex = #visible end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if #visible == 0 then
|
if #visible == 0 then
|
||||||
@@ -1066,7 +1179,14 @@ local function cycleDevice(command, kind)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local nextIndex = activeIndex % #visible + 1
|
local nextIndex = activeIndex % #visible + 1
|
||||||
if kind == "output" then setOutput(command, visible[nextIndex].id) else setInput(command, visible[nextIndex].id) end
|
local target = visible[nextIndex]
|
||||||
|
if kind == "output" then
|
||||||
|
setOutput(command, target.id)
|
||||||
|
elseif target.port ~= nil then
|
||||||
|
setInputPort(command, target.id, target.port)
|
||||||
|
else
|
||||||
|
setInput(command, target.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function findAddressBySlot(slot)
|
local function findAddressBySlot(slot)
|
||||||
@@ -1266,6 +1386,8 @@ local function executeCommand(command)
|
|||||||
setOutput(command, tostring(command.id or ""))
|
setOutput(command, tostring(command.id or ""))
|
||||||
elseif action == "set_input" then
|
elseif action == "set_input" then
|
||||||
setInput(command, tostring(command.id or ""))
|
setInput(command, tostring(command.id or ""))
|
||||||
|
elseif action == "set_input_port" then
|
||||||
|
setInputPort(command, tostring(command.id or ""), tostring(command.port or ""))
|
||||||
elseif action == "create_output_group" then
|
elseif action == "create_output_group" then
|
||||||
createOutputGroup(command)
|
createOutputGroup(command)
|
||||||
elseif action == "remove_output_group" then
|
elseif action == "remove_output_group" then
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
local sourceFile = assert(io.open("service.luau", "rb"))
|
||||||
|
local source = sourceFile:read("*a")
|
||||||
|
sourceFile:close()
|
||||||
|
|
||||||
|
local beginMarker = "-- BEGIN INPUT PORT HELPERS"
|
||||||
|
local endMarker = "-- END INPUT PORT HELPERS"
|
||||||
|
local beginAt = assert(source:find(beginMarker, 1, true), "input port helper start marker missing")
|
||||||
|
local bodyAt = assert(source:find("\n", beginAt, true)) + 1
|
||||||
|
local endAt = assert(source:find(endMarker, bodyAt, true), "input port helper end marker missing")
|
||||||
|
local helperSource = source:sub(bodyAt, endAt - 1)
|
||||||
|
local loader = assert(load(helperSource .. "\nreturn parseInputPorts", "input port helpers", "t", _G))
|
||||||
|
local parseInputPorts = loader()
|
||||||
|
|
||||||
|
local ports, active = parseInputPorts({
|
||||||
|
active_port = "analog-input-internal-mic",
|
||||||
|
ports = {
|
||||||
|
{ name = "analog-input-internal-mic", description = "Internal Microphone", availability = "available" },
|
||||||
|
{ name = "analog-input-headset-mic", description = "Headset Microphone", availability = "not available" },
|
||||||
|
{ name = "analog-input-mic", description = "Microphone", availability = "unknown" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert(active == "analog-input-internal-mic")
|
||||||
|
assert(#ports == 3)
|
||||||
|
assert(ports[1].active and ports[1].available and ports[1].description == "Internal Microphone")
|
||||||
|
assert(not ports[2].active and not ports[2].available and ports[2].description == "Headset Microphone")
|
||||||
|
assert(ports[3].available, "unknown availability should remain selectable")
|
||||||
|
|
||||||
|
local objectPorts, objectActive = parseInputPorts({
|
||||||
|
active_port = { name = "mic" },
|
||||||
|
ports = { { name = "mic", description = "", availability = "available" } },
|
||||||
|
})
|
||||||
|
assert(objectActive == "mic" and objectPorts[1].active)
|
||||||
|
assert(objectPorts[1].description == "mic", "port name should be the description fallback")
|
||||||
|
|
||||||
|
print("audio input port tests: ok")
|
||||||
@@ -23,7 +23,9 @@
|
|||||||
"bluetooth_connected": "Bluetooth connected",
|
"bluetooth_connected": "Bluetooth connected",
|
||||||
"bluetooth_disconnected": "Bluetooth disconnected",
|
"bluetooth_disconnected": "Bluetooth disconnected",
|
||||||
"current": "Current",
|
"current": "Current",
|
||||||
"output_group": "Output group"
|
"output_group": "Output group",
|
||||||
|
"port": "Port",
|
||||||
|
"unavailable_port": "{port} (unavailable)"
|
||||||
},
|
},
|
||||||
"editor": {
|
"editor": {
|
||||||
"alias": "Display name",
|
"alias": "Display name",
|
||||||
@@ -57,6 +59,9 @@
|
|||||||
"invalid_audio_data": "The audio service returned invalid data.",
|
"invalid_audio_data": "The audio service returned invalid data.",
|
||||||
"invalid_slot": "The keybind number must be a whole number from 1 to 99.",
|
"invalid_slot": "The keybind number must be a whole number from 1 to 99.",
|
||||||
"invalid_volume": "The requested volume is invalid.",
|
"invalid_volume": "The requested volume is invalid.",
|
||||||
|
"input_port_not_found": "The selected input port no longer exists.",
|
||||||
|
"input_port_switch_failed": "Could not switch the input port.",
|
||||||
|
"input_port_unavailable": "The selected input port is not currently available.",
|
||||||
"no_visible_devices": "There are no visible devices to cycle through.",
|
"no_visible_devices": "There are no visible devices to cycle through.",
|
||||||
"pactl_missing": "pactl is required but was not found on PATH.",
|
"pactl_missing": "pactl is required but was not found on PATH.",
|
||||||
"scan_in_progress": "Bluetooth scanning is already in progress.",
|
"scan_in_progress": "Bluetooth scanning is already in progress.",
|
||||||
@@ -68,6 +73,7 @@
|
|||||||
"bluetooth_disconnected": "Bluetooth device disconnected.",
|
"bluetooth_disconnected": "Bluetooth device disconnected.",
|
||||||
"group_created": "Output group created and selected.",
|
"group_created": "Output group created and selected.",
|
||||||
"group_removed": "Output group disbanded.",
|
"group_removed": "Output group disbanded.",
|
||||||
|
"input_port_selected": "Input port switched to {port}.",
|
||||||
"input_selected": "Input switched to {device}.",
|
"input_selected": "Input switched to {device}.",
|
||||||
"output_selected": "Output switched to {device}.",
|
"output_selected": "Output switched to {device}.",
|
||||||
"preferences_saved": "Device preferences saved.",
|
"preferences_saved": "Device preferences saved.",
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ local function render()
|
|||||||
local outputDevice = activeDevice(snapshot.outputs)
|
local outputDevice = activeDevice(snapshot.outputs)
|
||||||
local inputDevice = activeDevice(snapshot.inputs)
|
local inputDevice = activeDevice(snapshot.inputs)
|
||||||
local outputName = outputDevice and tostring(outputDevice.name or "") or noctalia.tr("panel.no_output")
|
local outputName = outputDevice and tostring(outputDevice.name or "") or noctalia.tr("panel.no_output")
|
||||||
local inputName = inputDevice and tostring(inputDevice.name or "") or noctalia.tr("panel.no_input")
|
local inputPortName = inputDevice and tostring(inputDevice.activePortDescription or "") or ""
|
||||||
|
local inputName = inputDevice and (inputPortName ~= "" and inputPortName or tostring(inputDevice.name or ""))
|
||||||
|
or noctalia.tr("panel.no_input")
|
||||||
local outputVolume = percent(snapshot.outputVolume)
|
local outputVolume = percent(snapshot.outputVolume)
|
||||||
local inputVolume = percent(snapshot.inputVolume)
|
local inputVolume = percent(snapshot.inputVolume)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user