mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-30 15:07:49 -04:00
port 2.0
This commit is contained in:
1 parent
bca7513825
commit
f65e934e1b
5 files changed
+310
-95
No files matched your search
@@ -100,7 +100,7 @@ Item {
|
||||
|
||||
DActionButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: AudioService.sinkHasMultiplePorts(deviceRow.modelData)
|
||||
visible: (AudioService.outputChoices[deviceRow.modelData.name]?.length ?? 0) > 1
|
||||
buttonSize: Theme.buttonHeightXS
|
||||
iconSize: Theme.iconSizeMedium
|
||||
iconName: "tune"
|
||||
@@ -116,22 +116,6 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: AudioService.switchableOutputPorts
|
||||
}
|
||||
|
||||
CcListRow {
|
||||
required property var modelData
|
||||
|
||||
iconName: modelData.icon
|
||||
title: modelData.title
|
||||
subtitle: modelData.cardDescription
|
||||
clickable: true
|
||||
onClicked: AudioService.activateOutputPort(modelData, (ok, message) => ToastService.showToast(message, ok ? ToastService.levelInfo : ToastService.levelError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CcSectionLabel {
|
||||
|
||||
@@ -9,13 +9,10 @@ CcSheetDialog {
|
||||
id: root
|
||||
|
||||
property var node: null
|
||||
property var availablePorts: []
|
||||
property string currentPort: ""
|
||||
property bool isLoading: false
|
||||
|
||||
readonly property bool nodeValid: node !== null && node.isSink
|
||||
|
||||
signal portSelected(string sinkName, string portName)
|
||||
readonly property var choices: nodeValid ? (AudioService.outputChoices[node.name] ?? []) : []
|
||||
|
||||
iconName: node ? AudioService.sinkIcon(node) : "speaker"
|
||||
title: node ? AudioService.displayName(node) : ""
|
||||
@@ -23,9 +20,9 @@ CcSheetDialog {
|
||||
statusText: {
|
||||
if (isLoading)
|
||||
return I18n.tr("Loading ports...", "audio port selector loading state");
|
||||
if (availablePorts.length === 0)
|
||||
if (choices.length === 0)
|
||||
return I18n.tr("No ports found", "audio port selector empty state");
|
||||
return I18n.tr("Current: %1", "audio port selector active port label, %1 is the port name").arg(portDescription(currentPort));
|
||||
return I18n.tr("Current: %1", "audio port selector active port label, %1 is the port name").arg(choices.find(c => c.active)?.title ?? "");
|
||||
}
|
||||
statusColor: isLoading ? Theme.primary : Theme.surfaceVariantText
|
||||
|
||||
@@ -34,51 +31,22 @@ CcSheetDialog {
|
||||
return;
|
||||
node = sinkNode;
|
||||
isLoading = true;
|
||||
availablePorts = [];
|
||||
currentPort = "";
|
||||
populateFromCache();
|
||||
AudioService.refreshCards();
|
||||
AudioService.refreshSinkPorts(() => {
|
||||
root.isLoading = false;
|
||||
});
|
||||
present();
|
||||
}
|
||||
|
||||
function populateFromCache() {
|
||||
const info = AudioService.getSinkPorts(node);
|
||||
if (!info) {
|
||||
availablePorts = [];
|
||||
currentPort = "";
|
||||
function select(choice) {
|
||||
if (!nodeValid || isLoading || choice.availability === "no")
|
||||
return;
|
||||
}
|
||||
availablePorts = (info.ports || []).slice().sort((a, b) => b.priority - a.priority);
|
||||
currentPort = info.active || "";
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
function portDescription(portName) {
|
||||
const port = availablePorts.find(p => p.name === portName);
|
||||
return port ? port.description : portName;
|
||||
}
|
||||
|
||||
function selectPort(portName) {
|
||||
if (!nodeValid || isLoading)
|
||||
return;
|
||||
const port = availablePorts.find(p => p.name === portName);
|
||||
if (!port || port.availability === "no")
|
||||
return;
|
||||
const capturedName = node.name;
|
||||
isLoading = true;
|
||||
AudioService.setSinkPort(capturedName, portName, (success, message) => {
|
||||
isLoading = false;
|
||||
if (!root.node || root.node.name !== capturedName)
|
||||
return;
|
||||
if (!success) {
|
||||
ToastService.showToast(message, ToastService.levelError);
|
||||
return;
|
||||
}
|
||||
root.portSelected(capturedName, portName);
|
||||
ToastService.showToast(message, ToastService.levelInfo);
|
||||
root.dismiss();
|
||||
AudioService.activateOutputChoice(choice, (success, message) => {
|
||||
root.isLoading = false;
|
||||
ToastService.showToast(message, success ? ToastService.levelInfo : ToastService.levelError);
|
||||
if (success)
|
||||
root.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,27 +57,18 @@ CcSheetDialog {
|
||||
|
||||
onDismissed: node = null
|
||||
|
||||
readonly property var audioSinkPorts: AudioService.sinkPorts
|
||||
|
||||
onAudioSinkPortsChanged: {
|
||||
if (!nodeValid)
|
||||
return;
|
||||
populateFromCache();
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
CcGroup {
|
||||
visible: root.availablePorts.length > 0
|
||||
visible: root.choices.length > 0
|
||||
|
||||
Repeater {
|
||||
model: root.availablePorts
|
||||
model: root.choices
|
||||
|
||||
CcListRow {
|
||||
required property var modelData
|
||||
|
||||
readonly property bool unavailable: modelData.availability === "no"
|
||||
|
||||
title: modelData.description
|
||||
title: modelData.title
|
||||
subtitle: {
|
||||
if (unavailable)
|
||||
return I18n.tr("Unavailable", "audio port availability status");
|
||||
@@ -117,10 +76,10 @@ CcSheetDialog {
|
||||
return I18n.tr("Available", "audio port availability status");
|
||||
return "";
|
||||
}
|
||||
active: modelData.name === root.currentPort
|
||||
active: modelData.active
|
||||
enabled: !unavailable && !root.isLoading
|
||||
clickable: !active
|
||||
onClicked: root.selectPort(modelData.name)
|
||||
onClicked: root.select(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.DCommon.Widgets
|
||||
import qs.Modules.Settings.Widgets
|
||||
@@ -20,7 +21,14 @@ Item {
|
||||
property bool isReloadingAudio: false
|
||||
property var hiddenOutputDeviceNames: SessionData.hiddenOutputDeviceNames ?? []
|
||||
property var hiddenInputDeviceNames: SessionData.hiddenInputDeviceNames ?? []
|
||||
// Hidden names persist for absent devices (unplugged, card disabled), so count only the present ones
|
||||
readonly property var shownOutputDevices: outputDevices.filter(d => !hiddenOutputDeviceNames.includes(d.name))
|
||||
readonly property var hiddenOutputDevices: outputDevices.filter(d => hiddenOutputDeviceNames.includes(d.name))
|
||||
readonly property var shownInputDevices: inputDevices.filter(d => !hiddenInputDeviceNames.includes(d.name))
|
||||
readonly property var hiddenInputDevices: inputDevices.filter(d => hiddenInputDeviceNames.includes(d.name))
|
||||
property bool showHiddenOutputDevices: false
|
||||
property bool showDisabledOutputCards: false
|
||||
property var deviceSetKey: null
|
||||
property bool showHiddenInputDevices: false
|
||||
|
||||
function openRenameDialog(device, type) {
|
||||
@@ -29,6 +37,19 @@ Item {
|
||||
renameDialog.show(AudioService.displayName(device));
|
||||
}
|
||||
|
||||
function confirmDisableCard(card) {
|
||||
disableCardConfirm.showWithOptions({
|
||||
"title": I18n.tr("Disable %1?", "audio settings confirmation title, %1 is the sound card name").arg(card.description),
|
||||
"message": I18n.tr("Every output and input of this sound card turns off until you enable it again", "audio settings confirmation message before disabling a sound card"),
|
||||
"confirmText": I18n.tr("Disable", "audio settings button that turns a sound card off"),
|
||||
"confirmColor": Theme.error,
|
||||
"onConfirm": () => AudioService.setCardProfile(card.name, "off", (ok, message) => {
|
||||
if (!ok)
|
||||
ToastService.showToast(message, ToastService.levelError);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function saveDeviceName(name) {
|
||||
if (!editingDevice)
|
||||
return;
|
||||
@@ -82,6 +103,13 @@ Item {
|
||||
};
|
||||
|
||||
inputDevices = inputs.sort(sortInputs);
|
||||
|
||||
// deviceRevision also bumps on app streams, so re-read cards only when the device set changes
|
||||
const key = outputs.concat(inputs).filter(node => !node.isStream).map(node => node.name).sort().join("\n");
|
||||
if (key === deviceSetKey)
|
||||
return;
|
||||
deviceSetKey = key;
|
||||
AudioService.refreshCards();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -132,6 +160,11 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmDialogOverlay {
|
||||
id: disableCardConfirm
|
||||
parent: root.parentModal?.modalFocusScope ?? root
|
||||
}
|
||||
|
||||
SettingsPage {
|
||||
id: mainColumn
|
||||
|
||||
@@ -189,7 +222,7 @@ Item {
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.outputDevices.filter(d => !root.hiddenOutputDeviceNames.includes(d.name))
|
||||
model: root.shownOutputDevices
|
||||
|
||||
delegate: Column {
|
||||
required property var modelData
|
||||
@@ -197,6 +230,10 @@ Item {
|
||||
spacing: 0
|
||||
|
||||
DeviceAliasRow {
|
||||
id: outputRow
|
||||
|
||||
readonly property var card: AudioService.cardForSink(modelData)
|
||||
|
||||
deviceNode: modelData
|
||||
deviceType: "output"
|
||||
showHideButton: true
|
||||
@@ -206,6 +243,15 @@ Item {
|
||||
onHideRequested: device => {
|
||||
root.persistHiddenOutputDeviceNames([...root.hiddenOutputDeviceNames, device.name]);
|
||||
}
|
||||
|
||||
DActionButton {
|
||||
visible: outputRow.card !== null
|
||||
iconName: "power_settings_new"
|
||||
iconSize: Theme.iconSizeMedium
|
||||
iconColor: Theme.error
|
||||
tooltipText: I18n.tr("Disable card", "audio settings button that turns a sound card off")
|
||||
onClicked: root.confirmDisableCard(outputRow.card)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -253,7 +299,7 @@ Item {
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
visible: root.outputDevices.filter(d => !root.hiddenOutputDeviceNames.includes(d.name)).length === 0 && root.hiddenOutputDeviceNames.length === 0
|
||||
visible: root.shownOutputDevices.length === 0 && root.hiddenOutputDevices.length === 0 && AudioService.disabledOutputCards.length === 0
|
||||
body: StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("No output devices found", "Audio settings empty state")
|
||||
@@ -265,7 +311,7 @@ Item {
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
visible: root.hiddenOutputDeviceNames.length > 0
|
||||
visible: root.hiddenOutputDevices.length > 0
|
||||
body: Column {
|
||||
width: parent.width
|
||||
spacing: 0
|
||||
@@ -287,7 +333,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Hidden (%1)", "count of hidden audio devices").arg(root.hiddenOutputDeviceNames.length)
|
||||
text: I18n.tr("Hidden (%1)", "count of hidden audio devices").arg(root.hiddenOutputDevices.length)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -315,7 +361,7 @@ Item {
|
||||
visible: root.showHiddenOutputDevices
|
||||
|
||||
Repeater {
|
||||
model: root.outputDevices.filter(d => root.hiddenOutputDeviceNames.includes(d.name))
|
||||
model: root.hiddenOutputDevices
|
||||
|
||||
delegate: DeviceAliasRow {
|
||||
required property var modelData
|
||||
@@ -332,6 +378,81 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
visible: AudioService.disabledOutputCards.length > 0
|
||||
body: Column {
|
||||
width: parent.width
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 36
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DIcon {
|
||||
name: "power_off"
|
||||
size: Theme.iconSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Disabled (%1)", "count of disabled audio output cards").arg(AudioService.disabledOutputCards.length)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DIcon {
|
||||
name: root.showDisabledOutputCards ? "expand_less" : "expand_more"
|
||||
size: Theme.iconSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.showDisabledOutputCards = !root.showDisabledOutputCards
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 0
|
||||
visible: root.showDisabledOutputCards
|
||||
|
||||
Repeater {
|
||||
model: AudioService.disabledOutputCards
|
||||
|
||||
delegate: SettingsRow {
|
||||
required property var modelData
|
||||
|
||||
iconName: "speaker"
|
||||
title: modelData.cardDescription
|
||||
subtitle: modelData.cardName
|
||||
|
||||
DActionButton {
|
||||
iconName: "power_settings_new"
|
||||
iconSize: Theme.iconSizeMedium
|
||||
tooltipText: I18n.tr("Enable card", "audio settings button that turns a disabled sound card back on")
|
||||
onClicked: AudioService.setCardProfile(modelData.cardName, modelData.profile, (ok, message) => {
|
||||
if (!ok)
|
||||
ToastService.showToast(message, ToastService.levelError);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
@@ -356,7 +477,7 @@ Item {
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.inputDevices.filter(d => !root.hiddenInputDeviceNames.includes(d.name))
|
||||
model: root.shownInputDevices
|
||||
|
||||
delegate: DeviceAliasRow {
|
||||
required property var modelData
|
||||
@@ -379,14 +500,14 @@ Item {
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: root.inputDevices.filter(d => !root.hiddenInputDeviceNames.includes(d.name)).length === 0 && root.hiddenInputDeviceNames.length === 0
|
||||
visible: root.shownInputDevices.length === 0 && root.hiddenInputDevices.length === 0
|
||||
topPadding: Theme.spacingM
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 0
|
||||
visible: root.hiddenInputDeviceNames.length > 0
|
||||
visible: root.hiddenInputDevices.length > 0
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
@@ -405,7 +526,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Hidden (%1)", "count of hidden audio devices").arg(root.hiddenInputDeviceNames.length)
|
||||
text: I18n.tr("Hidden (%1)", "count of hidden audio devices").arg(root.hiddenInputDevices.length)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -433,7 +554,7 @@ Item {
|
||||
visible: root.showHiddenInputDevices
|
||||
|
||||
Repeater {
|
||||
model: root.inputDevices.filter(d => root.hiddenInputDeviceNames.includes(d.name))
|
||||
model: root.hiddenInputDevices
|
||||
|
||||
delegate: DeviceAliasRow {
|
||||
required property var modelData
|
||||
|
||||
@@ -138,6 +138,8 @@ Singleton {
|
||||
property var cards: []
|
||||
property var pendingCardSwitch: null
|
||||
readonly property var switchableOutputPorts: root.collectSwitchableOutputPorts(root.cards, SessionData.hiddenOutputDeviceNames ?? [])
|
||||
readonly property var disabledOutputCards: root.collectDisabledOutputCards(root.cards)
|
||||
readonly property var outputChoices: root.collectOutputChoices(root.allNodes.filter(n => n.isSink && !n.isStream), root.sinkPorts, root.switchableOutputPorts)
|
||||
|
||||
readonly property int sinkMaxVolume: {
|
||||
if (freebsdAudio)
|
||||
@@ -298,14 +300,6 @@ Singleton {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function getSinkPorts(node) {
|
||||
return sinkPorts[node?.name] ?? null;
|
||||
}
|
||||
|
||||
function sinkHasMultiplePorts(node) {
|
||||
return (sinkPorts[node?.name]?.ports?.length ?? 0) > 1;
|
||||
}
|
||||
|
||||
function setSinkPort(sinkName, portName, callback) {
|
||||
if (freebsdAudio) {
|
||||
if (callback)
|
||||
@@ -435,6 +429,7 @@ Singleton {
|
||||
target.profiles[match[1]] = {
|
||||
description: match[2],
|
||||
priority: parseInt(meta.match(/priority:?\s*(\d+)/)?.[1] ?? "0", 10),
|
||||
sinks: parseInt(meta.match(/sinks:\s*(\d+)/)?.[1] ?? "0", 10),
|
||||
available: !/available:\s*no/.test(meta)
|
||||
};
|
||||
}
|
||||
@@ -565,9 +560,11 @@ Singleton {
|
||||
const product = port.props["device.product.name"] || "";
|
||||
entries.push({
|
||||
cardName: card.name,
|
||||
cardId: card.props["object.id"] ?? "",
|
||||
cardDescription: card.description,
|
||||
portName: port.name,
|
||||
profile: profile,
|
||||
availability: port.availability,
|
||||
icon: port.type === "hdmi" ? "tv" : "speaker",
|
||||
title: product ? `${port.description} [${product}]` : port.description
|
||||
});
|
||||
@@ -576,9 +573,63 @@ Singleton {
|
||||
return entries;
|
||||
}
|
||||
|
||||
function sinkBelongsToCard(node, cardName) {
|
||||
// Picks profiles by sink count, since UCM (HiFi) and Bluetooth (a2dp-sink) profiles have no output: prefix.
|
||||
// pro-audio is excluded: it stays available on cards WirePlumber turned off for having nothing plugged in.
|
||||
function collectDisabledOutputCards(cards) {
|
||||
const entries = [];
|
||||
for (const card of cards || []) {
|
||||
if (card.activeProfile !== "off")
|
||||
continue;
|
||||
const candidates = Object.keys(card.profiles).filter(name => name !== "pro-audio" && card.profiles[name].sinks > 0 && card.profiles[name].available);
|
||||
if (candidates.length === 0)
|
||||
continue;
|
||||
candidates.sort((a, b) => card.profiles[b].priority - card.profiles[a].priority);
|
||||
entries.push({
|
||||
cardName: card.name,
|
||||
cardDescription: card.description,
|
||||
profile: candidates[0]
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function collectOutputChoices(sinks, sinkPorts, switchable) {
|
||||
const bySink = {};
|
||||
for (const sink of sinks || []) {
|
||||
const info = sinkPorts?.[sink.name];
|
||||
const ports = (info?.ports || []).slice().sort((a, b) => b.priority - a.priority).map(port => ({
|
||||
kind: "port",
|
||||
sinkName: sink.name,
|
||||
portName: port.name,
|
||||
title: port.description,
|
||||
availability: port.availability,
|
||||
active: port.name === info.active
|
||||
}));
|
||||
const profiles = (switchable || []).filter(entry => sinkBelongsToCard(sink, entry.cardName, entry.cardId));
|
||||
bySink[sink.name] = ports.concat(profiles.map(entry => Object.assign({
|
||||
kind: "profile",
|
||||
active: false
|
||||
}, entry)));
|
||||
}
|
||||
return bySink;
|
||||
}
|
||||
|
||||
function activateOutputChoice(choice, callback) {
|
||||
if (choice?.kind !== "port") {
|
||||
activateOutputPort(choice, callback);
|
||||
return;
|
||||
}
|
||||
setDefaultSinkByName(choice.sinkName);
|
||||
setSinkPort(choice.sinkName, choice.portName, callback);
|
||||
}
|
||||
|
||||
// device.id is the card's object.id. PipeWire nodes carry no device.name (pactl adds it), so Bluetooth matches only by id
|
||||
function sinkBelongsToCard(node, cardName, cardId) {
|
||||
if (!node || node.isStream || !cardName)
|
||||
return false;
|
||||
const deviceId = node.properties?.["device.id"];
|
||||
if (cardId && deviceId !== undefined)
|
||||
return String(deviceId) === String(cardId);
|
||||
if (node.properties?.["device.name"] === cardName)
|
||||
return true;
|
||||
if (!cardName.startsWith("alsa_card."))
|
||||
@@ -586,6 +637,29 @@ Singleton {
|
||||
return (node.name || "").startsWith("alsa_output." + cardName.substring(10) + ".");
|
||||
}
|
||||
|
||||
function cardForSink(node) {
|
||||
return cards.find(card => sinkBelongsToCard(node, card.name, card.props["object.id"])) ?? null;
|
||||
}
|
||||
|
||||
function setCardProfile(cardName, profile, callback) {
|
||||
if (freebsdAudio) {
|
||||
if (callback)
|
||||
callback(false, I18n.tr("Output profile switching is not available with the FreeBSD native audio backend"));
|
||||
return;
|
||||
}
|
||||
if (!cardName || !profile)
|
||||
return;
|
||||
Proc.runCommand("audio-set-card-profile", ["env", "LC_ALL=C", "pactl", "set-card-profile", cardName, profile], (output, exitCode) => {
|
||||
const ok = exitCode === 0;
|
||||
if (callback)
|
||||
callback(ok, ok ? I18n.tr("Output switched", "audio card profile switched successful message") : (output || I18n.tr("Failed to switch output", "audio card profile switch failure message")));
|
||||
Qt.callLater(() => {
|
||||
root.refreshCards();
|
||||
root.refreshSinkPorts();
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function activateOutputPort(entry, callback) {
|
||||
if (freebsdAudio) {
|
||||
if (callback)
|
||||
@@ -594,12 +668,13 @@ Singleton {
|
||||
}
|
||||
if (!entry?.cardName || !entry.profile)
|
||||
return;
|
||||
const previousSinks = root.pipewireNodes.filter(n => n.isSink && sinkBelongsToCard(n, entry.cardName)).map(n => n.name);
|
||||
const previousSinks = root.pipewireNodes.filter(n => n.isSink && sinkBelongsToCard(n, entry.cardName, entry.cardId)).map(n => n.name);
|
||||
Proc.runCommand("audio-set-card-profile", ["env", "LC_ALL=C", "pactl", "set-card-profile", entry.cardName, entry.profile], (output, exitCode) => {
|
||||
const ok = exitCode === 0;
|
||||
if (ok) {
|
||||
root.pendingCardSwitch = {
|
||||
cardName: entry.cardName,
|
||||
cardId: entry.cardId,
|
||||
portName: entry.portName,
|
||||
exclude: previousSinks
|
||||
};
|
||||
@@ -619,7 +694,7 @@ Singleton {
|
||||
const pending = pendingCardSwitch;
|
||||
if (!pending)
|
||||
return;
|
||||
const node = root.pipewireNodes.find(n => n.isSink && sinkBelongsToCard(n, pending.cardName) && !pending.exclude.includes(n.name));
|
||||
const node = root.pipewireNodes.find(n => n.isSink && sinkBelongsToCard(n, pending.cardName, pending.cardId) && !pending.exclude.includes(n.name));
|
||||
if (!node)
|
||||
return;
|
||||
pendingCardSwitch = null;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
|
||||
function functionSource(file, name) {
|
||||
const source = readFileSync(new URL(file, import.meta.url), "utf8");
|
||||
const found = source.match(new RegExp(`^ function ${name}\\([^)]*\\)(?:: \\w+)? \\{\\n[\\s\\S]*?^ \\}`, "m"));
|
||||
if (!found)
|
||||
throw new Error(`${name} not found in ${file}`);
|
||||
return found[0];
|
||||
}
|
||||
|
||||
const context = vm.createContext({});
|
||||
for (const name of ["parseCards", "collectDisabledOutputCards"])
|
||||
vm.runInContext(functionSource("../Services/AudioService.qml", name), context);
|
||||
|
||||
function card(name, activeProfile, profiles) {
|
||||
return [
|
||||
"Card #1",
|
||||
`\tName: ${name}`,
|
||||
"\tProperties:",
|
||||
`\t\tdevice.description = "${name}"`,
|
||||
"\tProfiles:",
|
||||
...profiles.map(line => `\t\t${line}`),
|
||||
`\tActive Profile: ${activeProfile}`,
|
||||
"\tPorts:"
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
const off = "off: Off (sinks: 0, sources: 0, priority: 0, available: yes)";
|
||||
|
||||
test("off cards re-enable on their best sink profile whatever its name, other cards are not disabled", () => {
|
||||
const output = [
|
||||
card("alsa_card.usb-Headset-00", "off", [
|
||||
off,
|
||||
"output:analog-stereo+input:mono-fallback: Analog Stereo Output + Mono Input (sinks: 1, sources: 1, priority: 6501, available: yes)",
|
||||
"output:analog-stereo: Analog Stereo Output (sinks: 1, sources: 0, priority: 6500, available: yes)",
|
||||
"input:mono-fallback: Mono Input (sinks: 0, sources: 1, priority: 1, available: yes)"
|
||||
]),
|
||||
// Nothing plugged in: every analog profile is unavailable and only pro-audio is left
|
||||
card("alsa_card.pci-0000_0f_00.6", "off", [
|
||||
off,
|
||||
"output:analog-stereo+input:analog-stereo: Analog Stereo Duplex (sinks: 1, sources: 1, priority: 6565, available: no)",
|
||||
"input:analog-stereo: Analog Stereo Input (sinks: 0, sources: 1, priority: 65, available: no)",
|
||||
"pro-audio: Pro Audio (sinks: 1, sources: 2, priority: 1, available: yes)"
|
||||
]),
|
||||
card("alsa_card.pci-0000_00_1f.3-platform-skl_hda_dsp_generic", "off", [
|
||||
"HiFi (HDMI1, Headphones, Mic1, Speaker): Play HiFi quality Music (sinks: 2, sources: 1, priority: 8200, available: yes)",
|
||||
"HiFi (HDMI1, Mic1, Speaker): Play HiFi quality Music (sinks: 2, sources: 1, priority: 8100, available: yes)",
|
||||
off
|
||||
]),
|
||||
card("bluez_card.AA_BB_CC_DD_EE_FF", "off", [
|
||||
off,
|
||||
"a2dp-sink-sbc: High Fidelity Playback (A2DP Sink, codec SBC) (sinks: 1, sources: 0, priority: 18, available: yes)",
|
||||
"a2dp-sink: High Fidelity Playback (A2DP Sink, codec AAC) (sinks: 1, sources: 0, priority: 21, available: yes)",
|
||||
"headset-head-unit: Headset Head Unit (HSP/HFP, codec mSBC) (sinks: 1, sources: 1, priority: 2, available: yes)"
|
||||
]),
|
||||
card("alsa_card.usb-Webcam-00", "input:mono-fallback", [
|
||||
"input:mono-fallback: Mono Input (sinks: 0, sources: 1, priority: 1, available: yes)",
|
||||
off
|
||||
]),
|
||||
card("alsa_card.pci-0000_03_00.1", "output:hdmi-stereo", [
|
||||
"output:hdmi-stereo: Digital Stereo (HDMI) Output (sinks: 1, sources: 0, priority: 5900, available: yes)",
|
||||
off
|
||||
])
|
||||
].join("\n\n");
|
||||
|
||||
const disabled = JSON.parse(JSON.stringify(context.collectDisabledOutputCards(context.parseCards(output)).map(entry => [entry.cardName, entry.profile])));
|
||||
|
||||
assert.deepEqual(disabled, [
|
||||
["alsa_card.usb-Headset-00", "output:analog-stereo+input:mono-fallback"],
|
||||
["alsa_card.pci-0000_00_1f.3-platform-skl_hda_dsp_generic", "HiFi (HDMI1, Headphones, Mic1, Speaker)"],
|
||||
["bluez_card.AA_BB_CC_DD_EE_FF", "a2dp-sink"]
|
||||
]);
|
||||
});
|
||||
Reference in new issue
Block a user