// 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 } } 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" detail: "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 } } }