// 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 are asked // when the page opens, the way the Displays page probes hardware // brightness. Two of them read hardware that does not change while Settings // is open, so they are asked once a session and not once a visit. // // SoundFeedback is not one of them: it reads GSettings keys any GNOME // control panel can rewrite behind this page's back, so it is re-read every // open rather than trusted from the first one. Component.onCompleted: { SoundFeedback.refresh(); if (!SoundDefaults.loaded) SoundDefaults.refresh(); if (!SoundCards.loaded) 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 ─────────────────────── SectionLabel { text: "Alerts & feedback" } 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 ───────────────────────────────── SectionLabel { text: "Advanced" } 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 } } }