diff --git a/docs/speaker-equalizer.md b/docs/speaker-equalizer.md new file mode 100644 index 00000000..dc8c9a06 --- /dev/null +++ b/docs/speaker-equalizer.md @@ -0,0 +1,23 @@ +# Speaker equalizer controls + +Sound consumes settings-daemon status and preferences; it does not own audio +processing. Closing Settings leaves enabled EQ and noise cancellation running. + +The five gain controls are Bass, Low mids, Mids, Air and Upper balance. Their +names are fixed here; the device profile supplies frequencies, Q, gain ranges +and optional recommended gains. Frequencies appear in tooltips. Reset sits +below the sliders and preserves On/Off. Sliders remain editable while Off. + +User preferences take precedence over recommendations. Invalid profiles remain +unavailable; a supported operation failure can be recovered by an explicit +changed preference after the daemon revalidates the graph. + +Private integration checks use the companion daemon source and vendor marker policy +and profile, in the existing SDK without `/dev/snd`: + +``` +EQ_PRIVATE_TEST=1 EQ_OEM_RULE= EQ_OEM_PROFILE= \ + sh tests/run-components.sh +``` + +The native bridge uses synthetic card/route metadata and private audio servers. diff --git a/po/POTFILES b/po/POTFILES index 22037789..016b5eb3 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -5,7 +5,10 @@ src/TestPopover.vala src/PulseAudioManager.vala src/Plug.vala src/OutputPanel.vala +src/EchoCancellation.vala src/InputPanel.vala src/InputDeviceMonitor.vala src/DeviceRow.vala src/Device.vala +src/SpeakerEqualizer.vala +src/SpeakerEqualizerPanel.vala diff --git a/src/ApplicationsPanel.vala b/src/ApplicationsPanel.vala index 1b0c4374..9203f24e 100644 --- a/src/ApplicationsPanel.vala +++ b/src/ApplicationsPanel.vala @@ -5,8 +5,16 @@ * Authored by: Leonhard Kargl */ -public class Sound.ApplicationsPanel : Gtk.Box { +public class Sound.ApplicationsPanel : Switchboard.SettingsPage { + public ApplicationsPanel () { + Object ( + title: _("Applications"), + icon: new ThemedIcon ("preferences-desktop-apps") + ); + } + construct { + show_end_title_buttons = true; var pulse_audio_manager = PulseAudioManager.get_default (); var placeholder = new Granite.Placeholder (_("No applications currently emitting sounds")) { @@ -20,23 +28,18 @@ public class Sound.ApplicationsPanel : Gtk.Box { list_box.set_placeholder (placeholder); list_box.add_css_class (Granite.STYLE_CLASS_RICH_LIST); - var scrolled_window = new Gtk.ScrolledWindow () { - child = list_box, - vexpand = true - }; - var frame = new Gtk.Frame (null) { - child = scrolled_window + child = list_box }; var reset_button = new Gtk.Button.with_label (_("Reset all apps to default")) { halign = END }; - orientation = VERTICAL; - spacing = 12; - append (frame); - append (reset_button); + var content_box = new Gtk.Box (VERTICAL, 12); + content_box.append (frame); + content_box.append (reset_button); + child = content_box; // TODO: Reset also non active applications reset_button.clicked.connect (() => { diff --git a/src/EchoCancellation.vala b/src/EchoCancellation.vala new file mode 100644 index 00000000..240627dc --- /dev/null +++ b/src/EchoCancellation.vala @@ -0,0 +1,111 @@ +/* + * SPDX-License-Identifier: LGPL-2.0-or-later + * SPDX-FileCopyrightText: 2026 elementary, Inc. (https://elementary.io) + */ + +public class Sound.EchoCancellation : Object { + public const string SOURCE_NAME = "elementary_echo_cancel_source"; + public const string SINK_NAME = "elementary_echo_cancel_sink"; + + public struct Status { + public bool enabled; + public bool available; + public bool busy; + public string error; + public string source_master; + public string sink_master; + } + + [DBus (name = "io.elementary.settings_daemon.Audio")] + private interface Audio : DBusProxy { + public abstract async Status get_status () throws DBusError, IOError; + public signal void state_changed (); + } + + public bool requested { get; set; } + public bool enabled { get; private set; } + public bool available { get; private set; } + public bool busy { get; private set; } + public string? error { get; private set; } + public string? source_master { get; private set; } + public string? sink_master { get; private set; } + + private Settings? settings; + private Audio? audio; + private uint owner_generation; + + construct { + var schema = SettingsSchemaSource.get_default ().lookup ("io.elementary.settings-daemon.audio", true); + if (schema == null) { + error = _("The settings audio service is unavailable."); + return; + } + settings = new Settings.full (schema, null, null); + settings.bind ("echo-cancellation", this, "requested", SettingsBindFlags.DEFAULT); + Bus.watch_name (SESSION, "io.elementary.settings-daemon", NONE, (connection) => { + var generation = ++owner_generation; + connection.get_proxy.begin