244 lines
8.3 KiB
QML
244 lines
8.3 KiB
QML
// Sound.
|
|
//
|
|
// One sectioned page, top to bottom: what plays, what listens, what each
|
|
// application is doing, what the machine says when it wants attention, and the
|
|
// card profiles underneath all of it. There are no tabs and nothing to apply --
|
|
// every control here writes to PipeWire or to a desktop key that other
|
|
// applications already honour, and lands the moment it is moved.
|
|
//
|
|
// The page used to end in a button that opened GNOME's Sound panel for device
|
|
// profiles. It does not any more: SoundCards reads and writes them natively, so
|
|
// there is nothing left on this subject that Panama hands to someone else.
|
|
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
|
|
SettingsPage {
|
|
id: root
|
|
|
|
title: "Sound"
|
|
lede: "Live PipeWire — every change lands immediately, nothing to apply."
|
|
|
|
readonly property var currentOutput: AudioDevices.current(true)
|
|
readonly property var currentInput: AudioDevices.current(false)
|
|
|
|
// Over-amplification is the whole range, not a second slider: the maximum
|
|
// moves and the region past 100% is marked.
|
|
readonly property real volumeMaximum: Settings.overAmplification ? 1.5 : 1
|
|
|
|
// The three services that read the machine rather than the graph do it once
|
|
// per page open, the way the Displays page refreshes hardware brightness.
|
|
Component.onCompleted: {
|
|
SoundFeedback.refresh();
|
|
SoundDefaults.refresh();
|
|
SoundCards.refresh();
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Output"
|
|
subtitle: root.currentOutput
|
|
? AudioDevices.label(root.currentOutput)
|
|
: "No output device"
|
|
|
|
SoundDeviceList {
|
|
width: parent.width
|
|
output: true
|
|
}
|
|
|
|
SoundVolumeRow {
|
|
label: "Volume"
|
|
node: root.currentOutput
|
|
maximum: root.volumeMaximum
|
|
}
|
|
|
|
AudioBalance {
|
|
node: root.currentOutput
|
|
}
|
|
|
|
ToggleRow {
|
|
setting: "overAmplification"
|
|
divider: false
|
|
}
|
|
|
|
// Turning over-amplification off has to bring the level down with it.
|
|
// The preference only moves the top of the range; PipeWire keeps
|
|
// whatever was set, so a device left at 140% stays there with a slider
|
|
// that now ends at 100 and shows itself pinned at full. Nothing the
|
|
// page can do afterwards expresses the difference, and the reading is
|
|
// wrong until something else happens to move the volume.
|
|
Connections {
|
|
target: Settings
|
|
|
|
function onOverAmplificationChanged(): void {
|
|
if (Settings.overAmplification)
|
|
return;
|
|
const audio = root.currentOutput?.audio ?? null;
|
|
if (audio && audio.volume > 1)
|
|
audio.volume = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Input"
|
|
subtitle: root.currentInput
|
|
? AudioDevices.label(root.currentInput)
|
|
: "No input device"
|
|
|
|
SoundDeviceList {
|
|
width: parent.width
|
|
output: false
|
|
}
|
|
|
|
SoundVolumeRow {
|
|
label: "Input volume"
|
|
node: root.currentInput
|
|
}
|
|
|
|
// Recording and playing back is the only honest microphone test: the
|
|
// level meter above says something is arriving, not that it is you.
|
|
ActionRow {
|
|
label: "Test your microphone"
|
|
// A test that fails does it by being silent, which is the same
|
|
// thing a broken microphone does. The reason takes the detail's
|
|
// place so the two can be told apart.
|
|
detail: SoundTest.lastError !== ""
|
|
? SoundTest.lastError
|
|
: "Record a few seconds and play it back through the selected output"
|
|
divider: captureRow.visible
|
|
action: {
|
|
if (SoundTest.micTestState === "recording")
|
|
return "Recording…";
|
|
if (SoundTest.micTestState === "playing")
|
|
return "Playing back…";
|
|
return "Record & play back";
|
|
}
|
|
enabled: SoundTest.micTestState === "idle" && !!root.currentInput
|
|
onTriggered: SoundTest.startMicTest(root.currentInput, root.currentOutput)
|
|
}
|
|
|
|
SoundCaptureRow {
|
|
id: captureRow
|
|
divider: false
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Applications"
|
|
subtitle: SoundRouting.lastError !== ""
|
|
? SoundRouting.lastError
|
|
: "Each application's own level, and where it plays. Applications playing sound appear here on their own."
|
|
|
|
ApplicationMixer {
|
|
width: parent.width
|
|
}
|
|
}
|
|
|
|
// ── What the machine says when it wants attention ───────────────────────
|
|
Text {
|
|
width: parent.width
|
|
text: "Alerts & feedback"
|
|
color: Theme.fgMuted
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
font.weight: Font.DemiBold
|
|
font.capitalization: Font.AllUppercase
|
|
font.letterSpacing: 0.7
|
|
topPadding: 6
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Alerts"
|
|
subtitle: SoundFeedback.lastError
|
|
|
|
SoundThemeRow {}
|
|
|
|
SettingRow {
|
|
label: "Event sounds"
|
|
detail: "Interface sounds from GTK and GNOME applications, and Panama's notification chime"
|
|
controlWidth: 48
|
|
|
|
SettingsToggle {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
checked: SoundFeedback.eventSounds
|
|
enabled: !SoundFeedback.busy
|
|
onToggled: checked => SoundFeedback.setEventSounds(checked)
|
|
}
|
|
}
|
|
|
|
ToggleRow {
|
|
setting: "volumeChangeBlip"
|
|
}
|
|
|
|
SettingRow {
|
|
label: "Input feedback"
|
|
detail: "Key clicks and input event sounds in GTK applications"
|
|
controlWidth: 48
|
|
divider: false
|
|
|
|
SettingsToggle {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
checked: SoundFeedback.inputFeedback
|
|
enabled: !SoundFeedback.busy
|
|
onToggled: checked => SoundFeedback.setInputFeedback(checked)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── The hardware underneath the devices ─────────────────────────────────
|
|
Text {
|
|
width: parent.width
|
|
text: "Advanced"
|
|
color: Theme.fgMuted
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
font.weight: Font.DemiBold
|
|
font.capitalization: Font.AllUppercase
|
|
font.letterSpacing: 0.7
|
|
topPadding: 6
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Device profiles"
|
|
subtitle: SoundCards.lastError !== ""
|
|
? SoundCards.lastError
|
|
: "What each sound card is configured to do. Ports that are not physically connected are named, not hidden."
|
|
|
|
Repeater {
|
|
model: SoundCards.cards ?? []
|
|
|
|
OptionPickerRow {
|
|
required property var modelData
|
|
required property int index
|
|
|
|
label: String(modelData.description ?? modelData.name ?? "")
|
|
detail: String(modelData.portHint ?? "")
|
|
enabled: !SoundCards.busy
|
|
divider: index < (SoundCards.cards?.length ?? 0) - 1
|
|
current: modelData.activeProfile
|
|
options: (modelData.profiles ?? []).map(profile => ({
|
|
value: profile.name,
|
|
label: String(profile.description ?? profile.name),
|
|
detail: profile.available === false ? "Not available right now" : ""
|
|
}))
|
|
onPicked: value => SoundCards.setProfile(String(modelData.name), value)
|
|
}
|
|
}
|
|
|
|
Text {
|
|
width: parent.width
|
|
visible: (SoundCards.cards?.length ?? 0) === 0
|
|
text: SoundCards.busy ? "Reading device profiles…" : "No sound cards reported"
|
|
color: Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
horizontalAlignment: Text.AlignHCenter
|
|
topPadding: 12
|
|
bottomPadding: 12
|
|
}
|
|
}
|
|
}
|