Skip to content

Commit

Permalink
Implement per application volume control (#253)
Browse files Browse the repository at this point in the history
  • Loading branch information
leolost2605 committed Mar 19, 2024
1 parent 8d21993 commit b425b91
Show file tree
Hide file tree
Showing 10 changed files with 352 additions and 9 deletions.
5 changes: 5 additions & 0 deletions data/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ i18n.merge_file(
install: true,
install_dir: datadir / 'metainfo',
)

install_data(
'sound.gschema.xml',
install_dir: datadir / 'glib-2.0' / 'schemas'
)
10 changes: 10 additions & 0 deletions data/sound.gschema.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema id="io.elementary.switchboard.sound" path="/io/elementary/switchboard/sound/">
<key name="show-unknown-apps" type="b">
<default>false</default>
<summary>Show apps for which no info could be found</summary>
<description>Show apps that are found by PulseAudio but misbehave in a way that no app info can be found. This setting may cause apps to appear that have wrong names or icons or none at all.</description>
</key>
</schema>
</schemalist>
2 changes: 2 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ config_file = configure_file(
configuration: config_data
)

gnome.post_install (glib_compile_schemas: true)

subdir('data')
subdir('src')
subdir('po')
3 changes: 3 additions & 0 deletions po/POTFILES
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
src/App.vala
src/ApplicationsPanel.vala
src/AppRow.vala
src/TestPopover.vala
src/PulseAudioManager.vala
src/Plug.vala
Expand Down
59 changes: 59 additions & 0 deletions src/App.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* SPDX-License-Identifier: LGPL-3.0-or-later
* SPDX-FileCopyrightText: 2023 elementary, Inc. (https://elementary.io)
*
* Authored by: Leonhard Kargl <[email protected]>
*/

public class Sound.App : Object {
public signal void changed ();

public uint32 index { get; private set; }
public string name { get; private set; }
public string display_name { get; private set; }
public Icon icon { get; private set; }

public string media_name { get; set; }
public double volume { get; set; }
public bool muted { get; set; }
public PulseAudio.ChannelMap channel_map { get; set; }

public bool hidden { get; set; default = false; }

private static Settings settings;

static construct {
settings = new Settings ("io.elementary.switchboard.sound");
}

public App.from_sink_input_info (PulseAudio.SinkInputInfo sink_input) {
index = sink_input.index;
name = sink_input.proplist.gets (PulseAudio.Proplist.PROP_APPLICATION_NAME);

string app_id;
if (sink_input.proplist.contains (PulseAudio.Proplist.PROP_APPLICATION_ID) == 1) {
app_id = sink_input.proplist.gets (PulseAudio.Proplist.PROP_APPLICATION_ID);
} else {
app_id = name;
}

var app_info = new DesktopAppInfo (app_id + ".desktop");

if (app_info == null) {
settings.bind ("show-unknown-apps", this, "hidden", GET | INVERT_BOOLEAN);
}

if (app_info != null) {
display_name = app_info.get_name ();
icon = app_info.get_icon ();
} else {
display_name = name;

if (sink_input.proplist.contains (PulseAudio.Proplist.PROP_APPLICATION_ICON_NAME) == 1) {
icon = new ThemedIcon (sink_input.proplist.gets (PulseAudio.Proplist.PROP_APPLICATION_ICON_NAME));
} else {
icon = new ThemedIcon ("application-default-icon");
}
}
}
}
124 changes: 124 additions & 0 deletions src/AppRow.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* SPDX-License-Identifier: LGPL-3.0-or-later
* SPDX-FileCopyrightText: 2023 elementary, Inc. (https://elementary.io)
*
* Authored by: Leonhard Kargl <[email protected]>
*/

public class Sound.AppRow : Gtk.Grid {
private App? app;
private Gtk.Label app_name_label;
private Gtk.Label media_name_label;
private Gtk.Image image;
private Gtk.Button icon_button;
private Gtk.Scale volume_scale;
private Gtk.Switch mute_switch;

construct {
image = new Gtk.Image () {
icon_size = LARGE
};

app_name_label = new Gtk.Label ("") {
ellipsize = END,
xalign = 0
};
app_name_label.add_css_class (Granite.STYLE_CLASS_H3_LABEL);

media_name_label = new Gtk.Label ("") {
ellipsize = END,
xalign = 0
};
media_name_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL);
media_name_label.add_css_class (Granite.STYLE_CLASS_SMALL_LABEL);

var label_box = new Gtk.Box (HORIZONTAL, 6);
label_box.append (app_name_label);
label_box.append (media_name_label);

icon_button = new Gtk.Button.from_icon_name ("audio-volume-muted");

volume_scale = new Gtk.Scale.with_range (HORIZONTAL, 0, 1, 0.01) {
hexpand = true
};

mute_switch = new Gtk.Switch () {
valign = CENTER
};

hexpand = true;
column_spacing = 6;

attach (image, 0, 0, 1, 2);
attach (label_box, 1, 0, 2);
attach (icon_button, 1, 1);
attach (volume_scale, 2, 1);
attach (mute_switch, 3, 0, 1, 2);

volume_scale.change_value.connect ((type, new_value) => {
if (app != null) {
PulseAudioManager.get_default ().change_application_volume (app, new_value.clamp (0, 1));
}

return true;
});

icon_button.clicked.connect (() => toggle_mute_application ());

mute_switch.state_set.connect ((state) => {
toggle_mute_application (!state);
return true;
});
}

private void toggle_mute_application (bool? custom = null) {
if (app == null) {
return;
}

PulseAudioManager.get_default ().mute_application (app, custom != null ? custom : !app.muted);
}

private void update () {
media_name_label.label = media_name_label.tooltip_text = app.media_name;
volume_scale.set_value (app.volume);
mute_switch.state = !app.muted;
volume_scale.sensitive = !app.muted;

if (app.muted) {
icon_button.icon_name = "audio-volume-muted";
} else if (volume_scale.get_value () < 0.33) {
icon_button.icon_name = "audio-volume-low";
} else if (volume_scale.get_value () > 0.66) {
icon_button.icon_name = "audio-volume-high";
} else {
icon_button.icon_name = "audio-volume-medium";
}

if (app.muted) {
icon_button.tooltip_text = _("Unmute");
} else {
icon_button.tooltip_text = _("Mute");
}
}

public void bind_app (App app) {
this.app = app;

app_name_label.label = app.display_name;
image.set_from_gicon (app.icon);

app.changed.connect (update);
app.notify["hidden"].connect (() => {
visible = app.hidden;
});

visible = app.hidden;

volume_scale.set_value (app.volume);
}

public void unbind_app () {
app.changed.disconnect (update);
}
}
55 changes: 55 additions & 0 deletions src/ApplicationsPanel.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* SPDX-License-Identifier: LGPL-3.0-or-later
* SPDX-FileCopyrightText: 2023 elementary, Inc. (https://elementary.io)
*
* Authored by: Leonhard Kargl <[email protected]>
*/

public class Sound.ApplicationsPanel : Gtk.Box {
construct {
var pulse_audio_manager = PulseAudioManager.get_default ();

var placeholder = new Granite.Placeholder (_("No applications currently emittings sounds")) {
description = _("Applications emitting sounds will automatically appear here")
};

var list_box = new Gtk.ListBox () {
selection_mode = NONE,
};
list_box.bind_model (pulse_audio_manager.apps, widget_create_func);
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
};

var reset_button = new Gtk.Button.with_label (_("Reset all apps to default")) {
halign = END
};

orientation = VERTICAL;
spacing = 12;
append (frame);
append (reset_button);

// TODO: Reset also non active applications
reset_button.clicked.connect (() => {
for (int i = 0; i < pulse_audio_manager.apps.get_n_items (); i++) {
pulse_audio_manager.change_application_volume ((App) pulse_audio_manager.apps.get_item (i), 1);
}
});
}

private Gtk.Widget widget_create_func (Object item) {
var app = (App) item;
var app_row = new AppRow ();
app_row.bind_app (app);
return app_row;
}
}
13 changes: 5 additions & 8 deletions src/Plug.vala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class Sound.Plug : Switchboard.Plug {

var settings = new Gee.TreeMap<string, string?> (null, null);
settings.set ("sound", null);
settings.set ("sound/applications", "applications");
settings.set ("sound/input", "input");
settings.set ("sound/output", "output");
Object (category: Category.HARDWARE,
Expand All @@ -30,13 +31,15 @@ public class Sound.Plug : Switchboard.Plug {
if (box == null) {
var output_panel = new OutputPanel ();
input_panel = new InputPanel ();
var applications_panel = new ApplicationsPanel ();

stack = new Gtk.Stack () {
hexpand = true,
vexpand = true
};
stack.add_titled (output_panel, "output", _("Output"));
stack.add_titled (input_panel, "input", _("Input"));
stack.add_titled (applications_panel, "applications", _("Applications"));

var stack_switcher = new Gtk.StackSwitcher () {
halign = Gtk.Align.CENTER,
Expand Down Expand Up @@ -78,14 +81,7 @@ public class Sound.Plug : Switchboard.Plug {
}

public override void search_callback (string location) {
switch (location) {
case "input":
stack.set_visible_child_name ("input");
break;
case "output":
stack.set_visible_child_name ("output");
break;
}
stack.set_visible_child_name (location);
}

// 'search' returns results like ("Keyboard → Behavior → Duration", "keyboard<sep>behavior")
Expand All @@ -104,6 +100,7 @@ public class Sound.Plug : Switchboard.Plug {
search_results.set ("%s%s%s".printf (display_name, _("Input"), _("Port")), "input");
search_results.set ("%s%s%s".printf (display_name, _("Input"), _("Volume")), "input");
search_results.set ("%s%s%s".printf (display_name, _("Input"), _("Enable")), "input");
search_results.set ("%s%s".printf (display_name, _("Applications")), "applications");
return search_results;
}
}
Expand Down
Loading

0 comments on commit b425b91

Please sign in to comment.