Audio slider (#207)
This commit is contained in:
2
.github/workflows/arch-build.yml
vendored
2
.github/workflows/arch-build.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
container: archlinux:latest
|
container: archlinux:latest
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
PACKAGES: meson gtk3 gobject-introspection vala json-glib libhandy gtk-layer-shell scdoc
|
PACKAGES: meson gtk3 gobject-introspection vala json-glib libhandy gtk-layer-shell scdoc libpulse libgee
|
||||||
steps:
|
steps:
|
||||||
- name: Install packages
|
- name: Install packages
|
||||||
run: |
|
run: |
|
||||||
|
2
.github/workflows/ubuntu-build.yml
vendored
2
.github/workflows/ubuntu-build.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
DEBIAN_FRONTEND: noninteractive
|
DEBIAN_FRONTEND: noninteractive
|
||||||
PACKAGES: meson libwayland-dev libgtk-3-dev gobject-introspection libgirepository1.0-dev valac libjson-glib-dev libhandy-1-dev libgtk-layer-shell-dev scdoc
|
PACKAGES: meson libwayland-dev libgtk-3-dev gobject-introspection libgirepository1.0-dev valac libjson-glib-dev libhandy-1-dev libgtk-layer-shell-dev scdoc libgee-0.8-dev libpulse-dev
|
||||||
steps:
|
steps:
|
||||||
- name: Install packages
|
- name: Install packages
|
||||||
run: |
|
run: |
|
||||||
|
@@ -51,6 +51,7 @@ These widgets can be customized, added, removed and even reordered
|
|||||||
- Mpris (Media player controls for Spotify, Firefox, Chrome, etc...)
|
- Mpris (Media player controls for Spotify, Firefox, Chrome, etc...)
|
||||||
- Menubar with dropdown and buttons
|
- Menubar with dropdown and buttons
|
||||||
- Button grid
|
- Button grid
|
||||||
|
- Volume slider using PulseAudio
|
||||||
|
|
||||||
## Planned Features
|
## Planned Features
|
||||||
|
|
||||||
|
@@ -11,7 +11,7 @@ arch=(
|
|||||||
'armv7h' # ARM v7 hardfloat
|
'armv7h' # ARM v7 hardfloat
|
||||||
)
|
)
|
||||||
license=(GPL3)
|
license=(GPL3)
|
||||||
depends=("gtk3" "gtk-layer-shell" "dbus" "glib2" "gobject-introspection" "libgee" "json-glib" "libhandy")
|
depends=("gtk3" "gtk-layer-shell" "dbus" "glib2" "gobject-introspection" "libgee" "json-glib" "libhandy" "libpulse")
|
||||||
conflicts=("swaync" "swaync-client")
|
conflicts=("swaync" "swaync-client")
|
||||||
provides=("swaync" "swaync-client")
|
provides=("swaync" "swaync-client")
|
||||||
makedepends=(vala meson git scdoc)
|
makedepends=(vala meson git scdoc)
|
||||||
|
@@ -11,7 +11,7 @@ arch=(
|
|||||||
'armv7h' # ARM v7 hardfloat
|
'armv7h' # ARM v7 hardfloat
|
||||||
)
|
)
|
||||||
license=('GPL3')
|
license=('GPL3')
|
||||||
depends=("gtk3" "gtk-layer-shell" "dbus" "glib2" "gobject-introspection" "libgee" "json-glib" "libhandy")
|
depends=("gtk3" "gtk-layer-shell" "dbus" "glib2" "gobject-introspection" "libgee" "json-glib" "libhandy" "libpulse" )
|
||||||
conflicts=("swaync" "swaync-client")
|
conflicts=("swaync" "swaync-client")
|
||||||
provides=("swaync" "swaync-client")
|
provides=("swaync" "swaync-client")
|
||||||
makedepends=(vala meson git scdoc)
|
makedepends=(vala meson git scdoc)
|
||||||
|
@@ -23,6 +23,7 @@ BuildRequires: libhandy-devel >= 1.4.0
|
|||||||
BuildRequires: systemd-devel
|
BuildRequires: systemd-devel
|
||||||
BuildRequires: systemd
|
BuildRequires: systemd
|
||||||
BuildRequires: scdoc
|
BuildRequires: scdoc
|
||||||
|
BuildRequires: pulseaudio-libs-devel
|
||||||
%{?systemd_requires}
|
%{?systemd_requires}
|
||||||
|
|
||||||
%description
|
%description
|
||||||
|
@@ -366,6 +366,16 @@ config file to be able to detect config errors
|
|||||||
description: "Command to be executed on click" ++
|
description: "Command to be executed on click" ++
|
||||||
description: A list of actions containing a label and a command ++
|
description: A list of actions containing a label and a command ++
|
||||||
description: A grid of buttons that execute shell commands ++
|
description: A grid of buttons that execute shell commands ++
|
||||||
|
*volume*++
|
||||||
|
type: object ++
|
||||||
|
css class: widget-volume ++
|
||||||
|
properties: ++
|
||||||
|
label: ++
|
||||||
|
type: string ++
|
||||||
|
optional: true ++
|
||||||
|
default: "Volume" ++
|
||||||
|
description: Text displayed in front of the volume slider ++
|
||||||
|
description: Slider to control pulse volume ++
|
||||||
|
|
||||||
example:
|
example:
|
||||||
```
|
```
|
||||||
|
@@ -272,6 +272,9 @@
|
|||||||
},
|
},
|
||||||
"^menubar(#[a-zA-Z0-9_-]{1,}){0,1}?$": {
|
"^menubar(#[a-zA-Z0-9_-]{1,}){0,1}?$": {
|
||||||
"$ref": "#/widgets/menubar"
|
"$ref": "#/widgets/menubar"
|
||||||
|
},
|
||||||
|
"^volume(#[a-zA-Z0-9_-]{1,}){0,1}?$": {
|
||||||
|
"$ref": "#/widgets/volume"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -415,6 +418,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"volume": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Slider to control pulse volume",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"label": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Text displayed in front of the volume slider",
|
||||||
|
"default": "Volume"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -26,6 +26,9 @@ namespace SwayNotificationCenter.Widgets {
|
|||||||
case "buttons-grid":
|
case "buttons-grid":
|
||||||
widget = new ButtonsGrid (suffix, swaync_daemon, noti_daemon);
|
widget = new ButtonsGrid (suffix, swaync_daemon, noti_daemon);
|
||||||
break;
|
break;
|
||||||
|
case "volume":
|
||||||
|
widget = new Volume (suffix, swaync_daemon, noti_daemon);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
warning ("Could not find widget: \"%s\"!", key);
|
warning ("Could not find widget: \"%s\"!", key);
|
||||||
return null;
|
return null;
|
||||||
|
531
src/controlCenter/widgets/volume/pulseDaemon.vala
Normal file
531
src/controlCenter/widgets/volume/pulseDaemon.vala
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
// From SwaySettings PulseAudio page: https://github.com/ErikReider/SwaySettings/blob/407c9e99dd3e50a0f09c64d94a9e6ff741488378/src/Pages/Pulse/PulseDaemon.vala
|
||||||
|
using PulseAudio;
|
||||||
|
using Gee;
|
||||||
|
|
||||||
|
namespace SwayNotificationCenter.Widgets {
|
||||||
|
/**
|
||||||
|
* Loosely based off of Elementary OS switchboard-plug-sound
|
||||||
|
* https://github.com/elementary/switchboard-plug-sound
|
||||||
|
*/
|
||||||
|
public class PulseDaemon : Object {
|
||||||
|
private Context context;
|
||||||
|
private GLibMainLoop mainloop;
|
||||||
|
private bool quitting = false;
|
||||||
|
|
||||||
|
public bool running { get; private set; }
|
||||||
|
|
||||||
|
private string default_sink_name { get; private set; }
|
||||||
|
private string default_source_name { get; private set; }
|
||||||
|
|
||||||
|
private PulseDevice ? default_sink = null;
|
||||||
|
|
||||||
|
public HashMap<string, PulseDevice> sinks { get; private set; }
|
||||||
|
|
||||||
|
construct {
|
||||||
|
mainloop = new GLibMainLoop ();
|
||||||
|
|
||||||
|
sinks = new HashMap<string, PulseDevice> ();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start () {
|
||||||
|
get_context ();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close () {
|
||||||
|
quitting = true;
|
||||||
|
context.disconnect ();
|
||||||
|
context = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public signal void change_default_device (PulseDevice device);
|
||||||
|
|
||||||
|
public signal void new_device (PulseDevice device);
|
||||||
|
public signal void change_device (PulseDevice device);
|
||||||
|
public signal void remove_device (PulseDevice device);
|
||||||
|
|
||||||
|
private void get_context () {
|
||||||
|
var ctx = new Context (mainloop.get_api (), null);
|
||||||
|
ctx.set_state_callback ((ctx) => {
|
||||||
|
debug ("Pulse Status: %s\n", ctx.get_state ().to_string ());
|
||||||
|
switch (ctx.get_state ()) {
|
||||||
|
case Context.State.CONNECTING:
|
||||||
|
case Context.State.AUTHORIZING:
|
||||||
|
case Context.State.SETTING_NAME:
|
||||||
|
break;
|
||||||
|
case Context.State.READY:
|
||||||
|
ctx.set_subscribe_callback (subscription);
|
||||||
|
ctx.subscribe (Context.SubscriptionMask.SINK_INPUT |
|
||||||
|
Context.SubscriptionMask.SINK |
|
||||||
|
Context.SubscriptionMask.CARD |
|
||||||
|
Context.SubscriptionMask.SERVER);
|
||||||
|
// Init data
|
||||||
|
ctx.get_server_info (this.get_server_info);
|
||||||
|
running = true;
|
||||||
|
break;
|
||||||
|
case Context.State.TERMINATED:
|
||||||
|
case Context.State.FAILED:
|
||||||
|
running = false;
|
||||||
|
if (quitting) {
|
||||||
|
quitting = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
stderr.printf (
|
||||||
|
"PulseAudio connection lost. Will retry connection.\n");
|
||||||
|
get_context ();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
running = false;
|
||||||
|
stderr.printf ("Connection failure: %s\n",
|
||||||
|
PulseAudio.strerror (ctx.errno ()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (ctx.connect (
|
||||||
|
null, Context.Flags.NOFAIL, null) < 0) {
|
||||||
|
stdout.printf ("pa_context_connect() failed: %s\n",
|
||||||
|
PulseAudio.strerror (ctx.errno ()));
|
||||||
|
}
|
||||||
|
this.context = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void subscription (Context ctx,
|
||||||
|
Context.SubscriptionEventType t,
|
||||||
|
uint32 index) {
|
||||||
|
var type = t & Context.SubscriptionEventType.FACILITY_MASK;
|
||||||
|
var event = t & Context.SubscriptionEventType.TYPE_MASK;
|
||||||
|
switch (type) {
|
||||||
|
case Context.SubscriptionEventType.SINK:
|
||||||
|
switch (event) {
|
||||||
|
default: break;
|
||||||
|
case Context.SubscriptionEventType.NEW:
|
||||||
|
case Context.SubscriptionEventType.CHANGE:
|
||||||
|
ctx.get_sink_info_by_index (index, this.get_sink_info);
|
||||||
|
break;
|
||||||
|
case Context.SubscriptionEventType.REMOVE:
|
||||||
|
foreach (var sink in sinks.values) {
|
||||||
|
if (sink.device_index != index) continue;
|
||||||
|
sink.removed = true;
|
||||||
|
sink.is_default = false;
|
||||||
|
this.remove_device (sink);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Context.SubscriptionEventType.CARD:
|
||||||
|
switch (event) {
|
||||||
|
default: break;
|
||||||
|
case Context.SubscriptionEventType.NEW:
|
||||||
|
case Context.SubscriptionEventType.CHANGE:
|
||||||
|
ctx.get_card_info_by_index (index, this.get_card_info);
|
||||||
|
break;
|
||||||
|
case Context.SubscriptionEventType.REMOVE:
|
||||||
|
// A safe way of removing the sink_input
|
||||||
|
var iter = sinks.map_iterator ();
|
||||||
|
while (iter.next ()) {
|
||||||
|
var device = iter.get_value ();
|
||||||
|
if (device.card_index != index) continue;
|
||||||
|
device.removed = true;
|
||||||
|
device.is_default = false;
|
||||||
|
iter.unset ();
|
||||||
|
this.remove_device (device);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Context.SubscriptionEventType.SERVER:
|
||||||
|
ctx.get_server_info (this.get_server_info);
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Getters
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets called when any server value changes like default devices
|
||||||
|
* Calls `get_card_info_list` and `get_sink_info_list`
|
||||||
|
*/
|
||||||
|
private void get_server_info (Context ctx, ServerInfo ? info) {
|
||||||
|
if (this.default_sink_name == null) {
|
||||||
|
this.default_sink_name = info.default_sink_name;
|
||||||
|
}
|
||||||
|
if (this.default_sink_name != info.default_sink_name) {
|
||||||
|
this.default_sink_name = info.default_sink_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.get_card_info_list (this.get_card_info);
|
||||||
|
ctx.get_sink_info_list (this.get_sink_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void get_card_info (Context ctx, CardInfo ? info, int eol) {
|
||||||
|
if (info == null || eol != 0) return;
|
||||||
|
|
||||||
|
unowned string ? description = info.proplist
|
||||||
|
.gets ("device.description");
|
||||||
|
unowned string ? props_icon = info.proplist
|
||||||
|
.gets ("device.icon_name");
|
||||||
|
|
||||||
|
PulseDevice[] ports = {};
|
||||||
|
foreach (var port in info.ports) {
|
||||||
|
if (port->available == PortAvailable.NO) continue;
|
||||||
|
|
||||||
|
bool is_input = port->direction == Direction.INPUT;
|
||||||
|
HashMap<string, PulseDevice> devices = this.sinks;
|
||||||
|
string id = PulseDevice.get_hash_map_key (
|
||||||
|
description, port.name);
|
||||||
|
|
||||||
|
bool has_device = devices.has_key (id);
|
||||||
|
PulseDevice device = has_device
|
||||||
|
? devices.get (id) : new PulseDevice ();
|
||||||
|
bool device_is_removed = device.removed;
|
||||||
|
device.removed = false;
|
||||||
|
|
||||||
|
device.is_bluetooth = info.proplist.gets ("device.api") == "bluez5";
|
||||||
|
|
||||||
|
device.card_index = info.index;
|
||||||
|
device.direction = port.direction;
|
||||||
|
|
||||||
|
device.card_name = info.name;
|
||||||
|
device.card_description = description;
|
||||||
|
device.card_active_profile = info.active_profile2->name;
|
||||||
|
|
||||||
|
device.port_name = port.name;
|
||||||
|
device.port_description = port.description;
|
||||||
|
device.port_id = port->proplist.gets ("card.profile.port");
|
||||||
|
|
||||||
|
// Get port profiles2 (profiles is "Superseded by profiles2")
|
||||||
|
// and sort largest priority first
|
||||||
|
var profiles = new ArrayList<unowned CardProfileInfo2 *>
|
||||||
|
.wrap (port->profiles2);
|
||||||
|
|
||||||
|
profiles.sort ((a, b) => {
|
||||||
|
if (a->priority == b->priority) return 0;
|
||||||
|
return a.priority > b.priority ? -1 : 1;
|
||||||
|
});
|
||||||
|
string[] new_profiles = {};
|
||||||
|
Array<PulseCardProfile> pulse_profiles = new Array<PulseCardProfile> ();
|
||||||
|
foreach (var profile in profiles) {
|
||||||
|
new_profiles += profile->name;
|
||||||
|
|
||||||
|
var card_profile = new PulseCardProfile (profile);
|
||||||
|
pulse_profiles.append_val (card_profile);
|
||||||
|
if (profile->name == device.card_active_profile) {
|
||||||
|
device.active_profile = card_profile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
device.port_profiles = new_profiles;
|
||||||
|
device.profiles = pulse_profiles;
|
||||||
|
|
||||||
|
device.icon_name = port->proplist.gets ("device.icon_name")
|
||||||
|
?? props_icon;
|
||||||
|
if (device.icon_name == null) {
|
||||||
|
device.icon_name = is_input
|
||||||
|
? "microphone-sensitivity-high"
|
||||||
|
: "audio-speakers";
|
||||||
|
}
|
||||||
|
devices.set (id, device);
|
||||||
|
ports += device;
|
||||||
|
if (!has_device || device_is_removed) {
|
||||||
|
this.new_device (device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes ports that are no longer available */
|
||||||
|
var iter = sinks.map_iterator ();
|
||||||
|
while (iter.next ()) {
|
||||||
|
var device = iter.get_value ();
|
||||||
|
if (device.card_index != info.index) continue;
|
||||||
|
bool found = false;
|
||||||
|
foreach (var p in ports) {
|
||||||
|
if (device.get_current_hash_key ()
|
||||||
|
== p.get_current_hash_key ()) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
iter.unset ();
|
||||||
|
remove_device (device);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void get_sink_info (Context ctx, SinkInfo ? info, int eol) {
|
||||||
|
if (info == null || eol != 0) return;
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
foreach (PulseDevice device in sinks.values) {
|
||||||
|
if (device.card_index == info.card) {
|
||||||
|
// Sets the name and index to profiles that aren't active
|
||||||
|
// Ex: The HDMI audio port that's not active
|
||||||
|
device.device_name = info.name;
|
||||||
|
device.device_description = info.description;
|
||||||
|
device.device_index = info.index;
|
||||||
|
// If the current selected sink profile is this
|
||||||
|
if (info.active_port != null
|
||||||
|
&& info.active_port->name == device.port_name) {
|
||||||
|
found = true;
|
||||||
|
|
||||||
|
device.card_sink_port_name = info.active_port->name;
|
||||||
|
bool is_default =
|
||||||
|
device.device_name == this.default_sink_name;
|
||||||
|
device.is_default = is_default;
|
||||||
|
|
||||||
|
device.is_muted = info.mute == 1;
|
||||||
|
|
||||||
|
device.is_virtual = info.proplist.gets ("node.virtual") == "true";
|
||||||
|
|
||||||
|
device.cvolume = info.volume;
|
||||||
|
device.channel_map = info.channel_map;
|
||||||
|
device.balance = device.cvolume
|
||||||
|
.get_balance (device.channel_map);
|
||||||
|
device.volume_operations.foreach ((op) => {
|
||||||
|
if (op.get_state () != Operation.State.RUNNING) {
|
||||||
|
device.volume_operations.remove (op);
|
||||||
|
}
|
||||||
|
return Source.CONTINUE;
|
||||||
|
});
|
||||||
|
if (device.volume_operations.is_empty) {
|
||||||
|
device.volume = volume_to_double (
|
||||||
|
device.cvolume.max ());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_default) {
|
||||||
|
this.default_sink = device;
|
||||||
|
this.change_default_device (device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.change_device (device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If not found, it's a cardless device
|
||||||
|
if (found) return;
|
||||||
|
|
||||||
|
HashMap<string, PulseDevice> devices = this.sinks;
|
||||||
|
string id = PulseDevice.get_hash_map_key (
|
||||||
|
info.index.to_string (), info.description);
|
||||||
|
bool has_device = devices.has_key (id);
|
||||||
|
PulseDevice device = has_device ? devices.get (id) : new PulseDevice ();
|
||||||
|
|
||||||
|
bool device_is_removed = device.removed;
|
||||||
|
device.removed = false;
|
||||||
|
|
||||||
|
device.has_card = false;
|
||||||
|
|
||||||
|
device.direction = PulseAudio.Direction.OUTPUT;
|
||||||
|
|
||||||
|
device.device_name = info.name;
|
||||||
|
device.device_description = info.description;
|
||||||
|
device.device_index = info.index;
|
||||||
|
|
||||||
|
bool is_default = device.device_name == this.default_source_name;
|
||||||
|
device.is_default = is_default;
|
||||||
|
|
||||||
|
device.is_muted = info.mute == 1;
|
||||||
|
|
||||||
|
device.is_virtual = info.proplist.gets ("node.virtual") == "true";
|
||||||
|
|
||||||
|
device.icon_name = "application-x-executable-symbolic";
|
||||||
|
|
||||||
|
device.cvolume = info.volume;
|
||||||
|
device.channel_map = info.channel_map;
|
||||||
|
device.balance = device.cvolume
|
||||||
|
.get_balance (device.channel_map);
|
||||||
|
device.volume_operations.foreach ((op) => {
|
||||||
|
if (op.get_state () != Operation.State.RUNNING) {
|
||||||
|
device.volume_operations.remove (op);
|
||||||
|
}
|
||||||
|
return Source.CONTINUE;
|
||||||
|
});
|
||||||
|
if (device.volume_operations.is_empty) {
|
||||||
|
device.volume = volume_to_double (
|
||||||
|
device.cvolume.max ());
|
||||||
|
}
|
||||||
|
|
||||||
|
devices.set (id, device);
|
||||||
|
|
||||||
|
if (is_default) {
|
||||||
|
this.default_sink = device;
|
||||||
|
this.change_default_device (device);
|
||||||
|
}
|
||||||
|
if (!has_device || device_is_removed) {
|
||||||
|
this.new_device (device);
|
||||||
|
}
|
||||||
|
this.change_device (device);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Setters
|
||||||
|
*/
|
||||||
|
public void set_device_volume (PulseDevice device, double volume) {
|
||||||
|
device.volume_operations.foreach ((operation) => {
|
||||||
|
if (operation.get_state () == Operation.State.RUNNING) {
|
||||||
|
operation.cancel ();
|
||||||
|
}
|
||||||
|
|
||||||
|
device.volume_operations.remove (operation);
|
||||||
|
return GLib.Source.CONTINUE;
|
||||||
|
});
|
||||||
|
|
||||||
|
var cvol = device.cvolume;
|
||||||
|
cvol.scale (double_to_volume (volume));
|
||||||
|
Operation ? operation = null;
|
||||||
|
if (device.direction == Direction.OUTPUT) {
|
||||||
|
operation = context.set_sink_volume_by_name (
|
||||||
|
device.device_name, cvol);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation != null) {
|
||||||
|
device.volume_operations.add (operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void set_default_device (PulseDevice device) {
|
||||||
|
if (device == null) return;
|
||||||
|
bool is_input = device.direction == Direction.INPUT;
|
||||||
|
|
||||||
|
// Only set port and card profile if the device is attached to a card
|
||||||
|
if (device.has_card) {
|
||||||
|
// Gets the profile that includes support for your other device
|
||||||
|
string profile_name = device.port_profiles[0];
|
||||||
|
PulseDevice alt_device = default_sink;
|
||||||
|
if (alt_device != null) {
|
||||||
|
foreach (var profile in device.port_profiles) {
|
||||||
|
if (profile in alt_device.port_profiles) {
|
||||||
|
profile_name = profile;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile_name != device.card_active_profile) {
|
||||||
|
yield set_card_profile_by_index (profile_name, device);
|
||||||
|
yield wait_for_update<string> (device, "device-name");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_input) {
|
||||||
|
if (device.port_name != device.card_sink_port_name) {
|
||||||
|
debug ("Setting port to: %s", device.port_name);
|
||||||
|
yield set_sink_port_by_name (device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.device_name == null) {
|
||||||
|
yield wait_for_update<string> (device, "device-name");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_input) {
|
||||||
|
if (device.device_name != default_sink_name) {
|
||||||
|
debug ("Setting default sink to: %s", device.device_name);
|
||||||
|
yield set_default_sink (device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void wait_for_update<T> (PulseDevice device,
|
||||||
|
string prop_name) {
|
||||||
|
SourceFunc callback = wait_for_update.callback;
|
||||||
|
ulong handler_id = 0;
|
||||||
|
handler_id = device.notify[prop_name].connect ((s, p) => {
|
||||||
|
T prop_value;
|
||||||
|
device.get (prop_name, out prop_value);
|
||||||
|
if (prop_value != null) {
|
||||||
|
device.disconnect (handler_id);
|
||||||
|
Idle.add ((owned) callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
yield;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void set_bluetooth_card_profile (PulseCardProfile profile,
|
||||||
|
PulseDevice device) {
|
||||||
|
context.set_card_profile_by_index (device.card_index,
|
||||||
|
profile.name,
|
||||||
|
(c, success) => {
|
||||||
|
if (success == 1) {
|
||||||
|
set_bluetooth_card_profile.callback ();
|
||||||
|
} else {
|
||||||
|
stderr.printf ("setting the card %s profile to %s failed\n",
|
||||||
|
device.card_name, profile.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
yield;
|
||||||
|
// Wait until the device has been updated
|
||||||
|
yield wait_for_update<string> (device, "device-name");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void set_card_profile_by_index (string profile_name,
|
||||||
|
PulseDevice device) {
|
||||||
|
context.set_card_profile_by_index (device.card_index,
|
||||||
|
profile_name,
|
||||||
|
(c, success) => {
|
||||||
|
if (success == 1) {
|
||||||
|
set_card_profile_by_index.callback ();
|
||||||
|
} else {
|
||||||
|
stderr.printf ("setting the card %s profile to %s failed\n",
|
||||||
|
device.card_name, profile_name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
yield;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void set_sink_port_by_name (PulseDevice device) {
|
||||||
|
context.set_sink_port_by_name (device.device_name,
|
||||||
|
device.port_name,
|
||||||
|
(c, success) => {
|
||||||
|
if (success == 1) {
|
||||||
|
set_sink_port_by_name.callback ();
|
||||||
|
} else {
|
||||||
|
stderr.printf ("setting sink port to %s failed\n",
|
||||||
|
device.port_name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
yield;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void set_default_sink (PulseDevice device) {
|
||||||
|
context.set_default_sink (device.device_name, (c, success) => {
|
||||||
|
if (success == 1) {
|
||||||
|
set_default_sink.callback ();
|
||||||
|
} else {
|
||||||
|
stderr.printf ("setting default sink to %s failed\n",
|
||||||
|
device.device_name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
yield;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_device_mute (bool state, PulseDevice device) {
|
||||||
|
if (device.is_muted == state) return;
|
||||||
|
if (device.direction == Direction.OUTPUT) {
|
||||||
|
context.set_sink_mute_by_index (
|
||||||
|
device.device_index, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// public void set_sink_input_mute (bool state, PulseSinkInput sink_input) {
|
||||||
|
// if (sink_input.is_muted == state) return;
|
||||||
|
// context.set_sink_input_mute (sink_input.index, state);
|
||||||
|
// }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Volume utils
|
||||||
|
*/
|
||||||
|
|
||||||
|
private static double volume_to_double (PulseAudio.Volume vol) {
|
||||||
|
double tmp = (double) (vol - PulseAudio.Volume.MUTED);
|
||||||
|
return 100 * tmp / (double) (PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PulseAudio.Volume double_to_volume (double vol) {
|
||||||
|
double tmp = (double) (PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED) * vol / 100;
|
||||||
|
return (PulseAudio.Volume) tmp + PulseAudio.Volume.MUTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
135
src/controlCenter/widgets/volume/pulseDevice.vala
Normal file
135
src/controlCenter/widgets/volume/pulseDevice.vala
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// From SwaySettings PulseAudio page: https://github.com/ErikReider/SwaySettings/blob/407c9e99dd3e50a0f09c64d94a9e6ff741488378/src/Pages/Pulse/PulseDevice.vala
|
||||||
|
using PulseAudio;
|
||||||
|
using Gee;
|
||||||
|
|
||||||
|
namespace SwayNotificationCenter.Widgets {
|
||||||
|
public class PulseCardProfile : Object {
|
||||||
|
public string name;
|
||||||
|
public string description;
|
||||||
|
public uint32 n_sinks;
|
||||||
|
public uint32 priority;
|
||||||
|
int available;
|
||||||
|
|
||||||
|
public PulseCardProfile (CardProfileInfo2 * profile) {
|
||||||
|
this.name = profile->name;
|
||||||
|
this.description = profile->description;
|
||||||
|
this.n_sinks = profile->n_sinks;
|
||||||
|
this.priority = profile->priority;
|
||||||
|
this.available = profile->available;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool cmp (PulseCardProfile profile) {
|
||||||
|
return profile.name == name
|
||||||
|
&& profile.description == description
|
||||||
|
&& profile.n_sinks == n_sinks
|
||||||
|
&& profile.priority == priority
|
||||||
|
&& profile.available == available;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PulseDevice : Object {
|
||||||
|
|
||||||
|
public bool removed { get; set; default = false; }
|
||||||
|
|
||||||
|
public bool has_card { get; set; default = true; }
|
||||||
|
|
||||||
|
/** The card index: ex. `Card #49` */
|
||||||
|
public uint32 card_index { get; set; }
|
||||||
|
/** Sink index: ex. `Sink #55` */
|
||||||
|
public uint32 device_index { get; set; }
|
||||||
|
|
||||||
|
/** Input or Output */
|
||||||
|
public Direction direction { get; set; }
|
||||||
|
|
||||||
|
/** Is default Sink */
|
||||||
|
public bool is_default { get; set; }
|
||||||
|
/** If the device is virtual */
|
||||||
|
public bool is_virtual { get; set; default = false; }
|
||||||
|
/** If the device is a bluetooth device */
|
||||||
|
public bool is_bluetooth { get; set; default = false; }
|
||||||
|
|
||||||
|
/** The icon name: `device.icon_name` */
|
||||||
|
public string icon_name { get; set; }
|
||||||
|
|
||||||
|
/** The card name: `Name` */
|
||||||
|
public string card_name { get; set; }
|
||||||
|
/** The card description: `device.description` */
|
||||||
|
public string card_description { get; set; }
|
||||||
|
/** The card active profile: `Active Profile` */
|
||||||
|
public string card_active_profile { get; set; }
|
||||||
|
/** The card sink port name: `Active Port` */
|
||||||
|
public string card_sink_port_name { get; set; }
|
||||||
|
|
||||||
|
/** The Sink name: `Name` */
|
||||||
|
public string ? device_name { get; set; }
|
||||||
|
/** The Sink description: `Description` */
|
||||||
|
public string device_description { get; set; }
|
||||||
|
/** If the Sink is muted: `Mute` */
|
||||||
|
public bool is_muted { get; set; }
|
||||||
|
|
||||||
|
public double volume { get; set; }
|
||||||
|
public float balance { get; set; default = 0; }
|
||||||
|
public CVolume cvolume;
|
||||||
|
public ChannelMap channel_map;
|
||||||
|
public LinkedList<Operation> volume_operations { get; set; }
|
||||||
|
|
||||||
|
/** Gets the name to be shown to the user:
|
||||||
|
* "port_description - card_description"
|
||||||
|
*/
|
||||||
|
public string ? get_display_name () {
|
||||||
|
if (card_name == null) {
|
||||||
|
return device_description;
|
||||||
|
}
|
||||||
|
string p_desc = port_description;
|
||||||
|
string c_desc = card_description;
|
||||||
|
return "%s - %s".printf (p_desc, c_desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compares PulseDevices. Returns true if they're the same */
|
||||||
|
public bool cmp (PulseDevice device) {
|
||||||
|
return device.card_index == card_index
|
||||||
|
&& device.device_index == device_index
|
||||||
|
&& device.device_name == device_name
|
||||||
|
&& device.device_description == device_description
|
||||||
|
&& device.is_default == is_default
|
||||||
|
&& device.removed == removed
|
||||||
|
&& device.card_active_profile == card_active_profile
|
||||||
|
&& device.port_name == port_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name to be shown to the user:
|
||||||
|
* If has card: "card_description:port_name"
|
||||||
|
* If cardless: "device_index:device_description"
|
||||||
|
*/
|
||||||
|
public string get_current_hash_key () {
|
||||||
|
if (card_name == null) {
|
||||||
|
return get_hash_map_key (device_index.to_string (),
|
||||||
|
device_description);
|
||||||
|
}
|
||||||
|
return get_hash_map_key (card_description, port_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the name to be shown to the user:
|
||||||
|
* "card_description:port_name"
|
||||||
|
*/
|
||||||
|
public static string get_hash_map_key (string c_desc, string p_name) {
|
||||||
|
return string.joinv (":", new string[] { c_desc, p_name });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The port name: `Name` */
|
||||||
|
public string port_name { get; set; }
|
||||||
|
/** The port name: `Description` */
|
||||||
|
public string port_description { get; set; }
|
||||||
|
/** The port name: `card.profile.port` */
|
||||||
|
public string port_id { get; set; }
|
||||||
|
/** All port profiles */
|
||||||
|
public string[] port_profiles { get; set; }
|
||||||
|
public Array<PulseCardProfile> profiles { get; set; }
|
||||||
|
public PulseCardProfile ? active_profile { get; set; }
|
||||||
|
|
||||||
|
construct {
|
||||||
|
volume_operations = new LinkedList<Operation> ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
src/controlCenter/widgets/volume/volume.vala
Normal file
61
src/controlCenter/widgets/volume/volume.vala
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
namespace SwayNotificationCenter.Widgets {
|
||||||
|
public class Volume : BaseWidget {
|
||||||
|
public override string widget_name {
|
||||||
|
get {
|
||||||
|
return "volume";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk.Label label_widget = new Gtk.Label (null);
|
||||||
|
Gtk.Scale slider = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 0, 100, 1);
|
||||||
|
|
||||||
|
private PulseDevice ? default_sink = null;
|
||||||
|
private PulseDaemon client = new PulseDaemon ();
|
||||||
|
|
||||||
|
construct {
|
||||||
|
this.client.change_default_device.connect (default_device_changed);
|
||||||
|
|
||||||
|
slider.value_changed.connect (() => {
|
||||||
|
if (default_sink != null) {
|
||||||
|
this.client.set_device_volume (
|
||||||
|
default_sink,
|
||||||
|
(float) slider.get_value ());
|
||||||
|
slider.tooltip_text = ((int) slider.get_value ()).to_string ();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Volume (string suffix, SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) {
|
||||||
|
base (suffix, swaync_daemon, noti_daemon);
|
||||||
|
|
||||||
|
Json.Object ? config = get_config (this);
|
||||||
|
if (config != null) {
|
||||||
|
string ? label = get_prop<string> (config, "label");
|
||||||
|
label_widget.set_label (label ?? "Volume");
|
||||||
|
}
|
||||||
|
|
||||||
|
slider.draw_value = false;
|
||||||
|
|
||||||
|
add (label_widget);
|
||||||
|
pack_start (slider, true, true, 0);
|
||||||
|
|
||||||
|
show_all ();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void on_cc_visibility_change (bool val) {
|
||||||
|
if (val) {
|
||||||
|
this.client.start ();
|
||||||
|
} else {
|
||||||
|
this.client.close ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void default_device_changed (PulseDevice device) {
|
||||||
|
if (device != null && device.direction == PulseAudio.Direction.OUTPUT) {
|
||||||
|
this.default_sink = device;
|
||||||
|
slider.set_value (device.volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -40,6 +40,10 @@ widget_sources = [
|
|||||||
'controlCenter/widgets/menubar/menubar.vala',
|
'controlCenter/widgets/menubar/menubar.vala',
|
||||||
# Widget: Buttons Grid
|
# Widget: Buttons Grid
|
||||||
'controlCenter/widgets/buttonsGrid/buttonsGrid.vala',
|
'controlCenter/widgets/buttonsGrid/buttonsGrid.vala',
|
||||||
|
# Widget: Volume
|
||||||
|
'controlCenter/widgets/volume/volume.vala',
|
||||||
|
'controlCenter/widgets/volume/pulseDaemon.vala',
|
||||||
|
'controlCenter/widgets/volume/pulseDevice.vala',
|
||||||
]
|
]
|
||||||
|
|
||||||
app_sources = [
|
app_sources = [
|
||||||
@@ -67,6 +71,9 @@ app_deps = [
|
|||||||
meson.get_compiler('c').find_library('gtk-layer-shell'),
|
meson.get_compiler('c').find_library('gtk-layer-shell'),
|
||||||
meson.get_compiler('c').find_library('m', required : true),
|
meson.get_compiler('c').find_library('m', required : true),
|
||||||
meson.get_compiler('vala').find_library('posix'),
|
meson.get_compiler('vala').find_library('posix'),
|
||||||
|
dependency('gee-0.8'),
|
||||||
|
dependency('libpulse'),
|
||||||
|
dependency('libpulse-mainloop-glib'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Checks if the user wants scripting enabled
|
# Checks if the user wants scripting enabled
|
||||||
|
@@ -283,3 +283,12 @@
|
|||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Volume widget */
|
||||||
|
|
||||||
|
.widget-volume {
|
||||||
|
background-color: @noti-bg;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
Reference in New Issue
Block a user