commit
This commit is contained in:
parent
aee74cb528
commit
6c7d749a2c
8 changed files with 274 additions and 78 deletions
60
Cargo.lock
generated
60
Cargo.lock
generated
|
@ -376,6 +376,7 @@ dependencies = [
|
||||||
"loupedeck_serial",
|
"loupedeck_serial",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pa-volume-interface",
|
"pa-volume-interface",
|
||||||
|
"parse-display",
|
||||||
"regex",
|
"regex",
|
||||||
"resvg",
|
"resvg",
|
||||||
"rgb",
|
"rgb",
|
||||||
|
@ -1061,6 +1062,32 @@ dependencies = [
|
||||||
"windows-targets 0.48.5",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parse-display"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c6509d08722b53e8dafe97f2027b22ccbe3a5db83cb352931e9716b0aa44bc5c"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"parse-display-derive",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parse-display-derive"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68517892c8daf78da08c0db777fcc17e07f2f63ef70041718f8a7630ad84f341"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"regex-syntax 0.7.5",
|
||||||
|
"structmeta",
|
||||||
|
"syn 2.0.48",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pico-args"
|
name = "pico-args"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
@ -1161,7 +1188,7 @@ dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata",
|
"regex-automata",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1172,9 +1199,15 @@ checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
@ -1499,6 +1532,29 @@ version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "structmeta"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78ad9e09554f0456d67a69c1584c9798ba733a5b50349a6c0d0948710523922d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"structmeta-derive",
|
||||||
|
"syn 2.0.48",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "structmeta-derive"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.48",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "svgtypes"
|
name = "svgtypes"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
|
|
|
@ -29,4 +29,5 @@ tiny-skia = "0.11.3"
|
||||||
tokio = { version = "1.35.1", features = ["macros", "parking_lot", "rt", "rt-multi-thread", "sync"]}
|
tokio = { version = "1.35.1", features = ["macros", "parking_lot", "rt", "rt-multi-thread", "sync"]}
|
||||||
toml = "0.8.8"
|
toml = "0.8.8"
|
||||||
walkdir = "2.4.0"
|
walkdir = "2.4.0"
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
|
parse-display = "0.8.2"
|
1
deckster/examples/full/icons/apps/youtube.svg
Normal file
1
deckster/examples/full/icons/apps/youtube.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
|
After Width: | Height: | Size: 426 B |
|
@ -1,30 +1,49 @@
|
||||||
#[knobs.right-top]
|
[knobs.right-top]
|
||||||
#icon = "@ph/microphone-light[scale=0.9]"
|
icon = "@ph/microphone-light[scale=0.9]"
|
||||||
#indicators.bar.color = "#ffffff50"
|
|
||||||
#
|
|
||||||
#mode.audio_volume.direction = "input"
|
|
||||||
#mode.audio_volume.regex = "^(SC425 USB Microphone Analog Stereo)$"
|
|
||||||
#mode.audio_volume.disable_press_to_unmute = true
|
|
||||||
#mode.audio_volume.muted_turn_action = "unmute-at-zero"
|
|
||||||
#mode.audio_volume.style.muted.label = "Muted"
|
|
||||||
#mode.audio_volume.style.inactive.icon = "@ph/microphone-slash-light[alpha=0.9|color=#fc4646]"
|
|
||||||
#
|
|
||||||
#[knobs.right-middle]
|
|
||||||
#icon = "@apps/discord[scale=0.25]"
|
|
||||||
#indicators.bar.color = "#ffffff50"
|
|
||||||
#
|
|
||||||
#mode.audio_volume.regex = "Discord"
|
|
||||||
#mode.audio_volume.style.active.label = "{percentage}%"
|
|
||||||
#mode.audio_volume.style.muted.label = "Muted"
|
|
||||||
#mode.audio_volume.style.inactive.label = ""
|
|
||||||
#mode.audio_volume.style.inactive.icon = "@apps/discord[grayscale|alpha=0.9]"
|
|
||||||
|
|
||||||
[knobs.right-bottom]
|
|
||||||
icon = "@apps/spotify[scale=1.1]"
|
|
||||||
indicators.bar.color = "#ffffff50"
|
indicators.bar.color = "#ffffff50"
|
||||||
|
|
||||||
mode.audio_volume.regex = "Spotify"
|
mode.audio_volume.delta = 0.05
|
||||||
|
mode.audio_volume.target.type = "input"
|
||||||
|
mode.audio_volume.target.predicates = [{ property = "description", value = "SC425 USB Microphone Analog Stereo" }]
|
||||||
|
mode.audio_volume.disable_press_to_unmute = true
|
||||||
|
mode.audio_volume.muted_turn_action = "unmute-at-zero"
|
||||||
|
|
||||||
mode.audio_volume.style.active.label = "{percentage}%"
|
mode.audio_volume.style.active.label = "{percentage}%"
|
||||||
mode.audio_volume.style.muted.label = "Muted"
|
mode.audio_volume.style.muted.label = "Muted"
|
||||||
mode.audio_volume.style.inactive.label = ""
|
mode.audio_volume.style.muted.icon = "@ph/microphone-slash-light[scale=0.9|color=#fc4646]"
|
||||||
mode.audio_volume.style.inactive.icon = "@apps/spotify[grayscale|alpha=0.9]"
|
mode.audio_volume.style.muted.indicators.bar.color = "#fc464690"
|
||||||
|
mode.audio_volume.style.inactive.label = "N/A"
|
||||||
|
mode.audio_volume.style.inactive.icon = "@ph/microphone-slash-light[scale=0.9|alpha=0.8|color=#fc4646]"
|
||||||
|
|
||||||
|
[knobs.left-top]
|
||||||
|
icon = "@apps/discord[scale=0.25]"
|
||||||
|
indicators.bar.color = "#ffffff50"
|
||||||
|
|
||||||
|
mode.audio_volume.delta = 0.05
|
||||||
|
mode.audio_volume.target.type = "application"
|
||||||
|
mode.audio_volume.target.predicates = [{ property = "binary-name", value = "Discord" }, { property = "description", value = "playStream" }]
|
||||||
|
|
||||||
|
mode.audio_volume.style.muted.indicators.bar.color = "#fc464690"
|
||||||
|
mode.audio_volume.style.inactive.icon = "@apps/discord[scale=0.25|grayscale|alpha=0.8]"
|
||||||
|
|
||||||
|
[knobs.left-middle]
|
||||||
|
icon = "@apps/youtube[scale=1.3|color=#ff0000]"
|
||||||
|
indicators.bar.color = "#ffffff50"
|
||||||
|
|
||||||
|
mode.audio_volume.delta = 0.05
|
||||||
|
mode.audio_volume.target.type = "application"
|
||||||
|
mode.audio_volume.target.predicates = [{ property = "binary-name", value = "librewolf" }, { property = "description", regex = "\\- Piped$" }]
|
||||||
|
|
||||||
|
mode.audio_volume.style.muted.indicators.bar.color = "#fc464690"
|
||||||
|
mode.audio_volume.style.inactive.icon = "@apps/youtube[scale=1.3|grayscale|alpha=0.8]"
|
||||||
|
|
||||||
|
[knobs.left-bottom]
|
||||||
|
icon = "@apps/spotify[scale=1.2]"
|
||||||
|
indicators.bar.color = "#ffffff50"
|
||||||
|
|
||||||
|
mode.audio_volume.delta = 0.05
|
||||||
|
mode.audio_volume.target.type = "application"
|
||||||
|
mode.audio_volume.target.predicates = [{ property = "application-name", value = "spotify" }]
|
||||||
|
|
||||||
|
mode.audio_volume.style.muted.indicators.bar.color = "#fc464690"
|
||||||
|
mode.audio_volume.style.inactive.icon = "@apps/spotify[scale=1.2|grayscale|alpha=0.8]"
|
|
@ -67,6 +67,12 @@ pub enum KnobPosition {
|
||||||
RightBottom,
|
RightBottom,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl KnobPosition {
|
||||||
|
pub fn is_left(&self) -> bool {
|
||||||
|
matches!(self, KnobPosition::LeftBottom | KnobPosition::LeftMiddle | KnobPosition::LeftTop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<LoupedeckKnob> for KnobPosition {
|
impl From<LoupedeckKnob> for KnobPosition {
|
||||||
fn from(value: LoupedeckKnob) -> Self {
|
fn from(value: LoupedeckKnob) -> Self {
|
||||||
match value {
|
match value {
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
use std::borrow::ToOwned;
|
use std::borrow::ToOwned;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use log::warn;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use parse_display::Display;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
use pa_volume_interface::{PaEntityState, PaVolumeInterface};
|
use pa_volume_interface::{PaEntityKind, PaEntityMetadata, PaEntityState, PaVolumeInterface};
|
||||||
|
|
||||||
use crate::model::knob_page::StyleByStateMap;
|
use crate::model::knob_page::StyleByStateMap;
|
||||||
use crate::model::position::KnobPath;
|
use crate::model::position::KnobPath;
|
||||||
|
@ -17,12 +18,8 @@ use crate::runner::command::IoWorkerCommand;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(default = "default_delta")]
|
pub target: Target,
|
||||||
pub delta: f32,
|
pub delta: Option<f32>,
|
||||||
#[serde(with = "serde_regex")]
|
|
||||||
pub regex: Regex,
|
|
||||||
#[serde(default)]
|
|
||||||
pub direction: Direction,
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub disable_press_to_mute: bool,
|
pub disable_press_to_mute: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -30,13 +27,52 @@ pub struct Config {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub muted_turn_action: MutedTurnAction,
|
pub muted_turn_action: MutedTurnAction,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub label: HashMap<State, String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub style: StyleByStateMap<State>,
|
pub style: StyleByStateMap<State>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_delta() -> f32 {
|
#[derive(Debug, Eq, PartialEq, Deserialize, Display)]
|
||||||
0.05
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
#[display(style = "kebab-case")]
|
||||||
|
pub enum TargetKind {
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
Application,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Target {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: TargetKind,
|
||||||
|
predicates: Vec<TargetPredicate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct TargetPredicate {
|
||||||
|
property: TargetPredicateProperty,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pattern: TargetPredicatePattern,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum TargetPredicatePattern {
|
||||||
|
Static {
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
Regex {
|
||||||
|
#[serde(with = "serde_regex")]
|
||||||
|
regex: Regex,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Deserialize, Display)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
#[display(style = "kebab-case")]
|
||||||
|
pub enum TargetPredicateProperty {
|
||||||
|
Description,
|
||||||
|
InternalName,
|
||||||
|
ApplicationName,
|
||||||
|
BinaryName,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Eq, PartialEq, Deserialize)]
|
#[derive(Debug, Default, Eq, PartialEq, Deserialize)]
|
||||||
|
@ -66,12 +102,54 @@ pub enum State {
|
||||||
|
|
||||||
static PA_VOLUME_INTERFACE: Lazy<PaVolumeInterface> = Lazy::new(|| PaVolumeInterface::spawn_thread("deckster".to_owned()));
|
static PA_VOLUME_INTERFACE: Lazy<PaVolumeInterface> = Lazy::new(|| PaVolumeInterface::spawn_thread("deckster".to_owned()));
|
||||||
|
|
||||||
fn get_volume_cv(channel_volumes: &Vec<f32>) -> f32 {
|
fn get_volume_cv(channel_volumes: &[f32]) -> f32 {
|
||||||
*channel_volumes.first().unwrap()
|
*channel_volumes.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_volume_es(entity_state: &Option<Arc<PaEntityState>>) -> f32 {
|
fn get_volume_es(entity_state: &Option<Arc<PaEntityState>>) -> f32 {
|
||||||
entity_state.as_ref().map(|s| *s.channel_volumes().first().unwrap()).unwrap_or(0.0)
|
entity_state.as_ref().map(|s| get_volume_cv(&s.channel_volumes())).unwrap_or(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_matches(target: &Target, state: &PaEntityState) -> bool {
|
||||||
|
if !match target.kind {
|
||||||
|
TargetKind::Input => state.kind() == PaEntityKind::Source,
|
||||||
|
TargetKind::Output => state.kind() == PaEntityKind::Sink,
|
||||||
|
TargetKind::Application => state.kind() == PaEntityKind::SinkInput,
|
||||||
|
} {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static EMPTY_STRING: String = String::new();
|
||||||
|
|
||||||
|
return target.predicates.iter().all(|p| {
|
||||||
|
let v = match (&p.property, state.metadata()) {
|
||||||
|
(TargetPredicateProperty::InternalName, PaEntityMetadata::Sink { name, .. }) => Some(name),
|
||||||
|
(TargetPredicateProperty::InternalName, PaEntityMetadata::Source { name, .. }) => Some(name),
|
||||||
|
(TargetPredicateProperty::InternalName, PaEntityMetadata::SinkInput { .. }) => None,
|
||||||
|
(TargetPredicateProperty::Description, PaEntityMetadata::Sink { description, .. }) => Some(description),
|
||||||
|
(TargetPredicateProperty::Description, PaEntityMetadata::Source { description, .. }) => Some(description),
|
||||||
|
(TargetPredicateProperty::Description, PaEntityMetadata::SinkInput { description, .. }) => Some(description),
|
||||||
|
(TargetPredicateProperty::ApplicationName, PaEntityMetadata::Sink { .. }) => None,
|
||||||
|
(TargetPredicateProperty::ApplicationName, PaEntityMetadata::Source { .. }) => None,
|
||||||
|
(TargetPredicateProperty::ApplicationName, PaEntityMetadata::SinkInput { application_name, .. }) => {
|
||||||
|
Some(application_name.as_ref().unwrap_or(&EMPTY_STRING))
|
||||||
|
}
|
||||||
|
(TargetPredicateProperty::BinaryName, PaEntityMetadata::Sink { .. }) => None,
|
||||||
|
(TargetPredicateProperty::BinaryName, PaEntityMetadata::Source { .. }) => None,
|
||||||
|
(TargetPredicateProperty::BinaryName, PaEntityMetadata::SinkInput { binary_name, .. }) => Some(binary_name.as_ref().unwrap_or(&EMPTY_STRING)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(v) = v {
|
||||||
|
match &p.pattern {
|
||||||
|
TargetPredicatePattern::Static { value } => value == v,
|
||||||
|
TargetPredicatePattern::Regex { regex } => regex.is_match(v),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("Property \"{}\" is not available for targets of type \"{}\"", &p.property, &target.kind);
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle(path: KnobPath, config: Arc<Config>, mut events: broadcast::Receiver<(KnobPath, KnobEvent)>, commands: flume::Sender<IoWorkerCommand>) {
|
pub async fn handle(path: KnobPath, config: Arc<Config>, mut events: broadcast::Receiver<(KnobPath, KnobEvent)>, commands: flume::Sender<IoWorkerCommand>) {
|
||||||
|
@ -84,8 +162,9 @@ pub async fn handle(path: KnobPath, config: Arc<Config>, mut events: broadcast::
|
||||||
entity_state = state
|
entity_state = state
|
||||||
.entities_by_id()
|
.entities_by_id()
|
||||||
.values()
|
.values()
|
||||||
.find(|entity| config.regex.is_match(entity.display_name()))
|
.find(|entity| state_matches(&config.target, &entity))
|
||||||
.map(Arc::clone);
|
.map(Arc::clone);
|
||||||
|
|
||||||
commands
|
commands
|
||||||
.send(IoWorkerCommand::SetKnobValue {
|
.send(IoWorkerCommand::SetKnobValue {
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
|
@ -127,7 +206,7 @@ pub async fn handle(path: KnobPath, config: Arc<Config>, mut events: broadcast::
|
||||||
loop {
|
loop {
|
||||||
select! {
|
select! {
|
||||||
Ok(volume_state) = volume_states.recv() => {
|
Ok(volume_state) = volume_states.recv() => {
|
||||||
entity_state = volume_state.entities_by_id().values().find(|entity| config.regex.is_match(entity.display_name())).map(Arc::clone);
|
entity_state = volume_state.entities_by_id().values().find(|entity| state_matches(&config.target, &entity)).map(Arc::clone);
|
||||||
update_style(&entity_state);
|
update_style(&entity_state);
|
||||||
|
|
||||||
commands.send(IoWorkerCommand::SetKnobValue { path: path.clone(), value: get_volume_es(&entity_state) }).unwrap()
|
commands.send(IoWorkerCommand::SetKnobValue { path: path.clone(), value: get_volume_es(&entity_state) }).unwrap()
|
||||||
|
@ -147,7 +226,7 @@ pub async fn handle(path: KnobPath, config: Arc<Config>, mut events: broadcast::
|
||||||
};
|
};
|
||||||
|
|
||||||
let current_v = get_volume_cv(&entity_state.channel_volumes());
|
let current_v = get_volume_cv(&entity_state.channel_volumes());
|
||||||
let v = (current_v + (factor * config.delta)).clamp(0.0, 1.0);
|
let v = (current_v + (factor * config.delta.unwrap_or(0.01))).clamp(0.0, 1.0);
|
||||||
pa_volume_interface.set_channel_volumes(*entity_state.id(), vec![v; entity_state.channel_volumes().len()]);
|
pa_volume_interface.set_channel_volumes(*entity_state.id(), vec![v; entity_state.channel_volumes().len()]);
|
||||||
}
|
}
|
||||||
KnobEvent::Press => {
|
KnobEvent::Press => {
|
||||||
|
|
|
@ -81,13 +81,23 @@ pub fn render_knob(context: &GraphicsContext, screen_size: IntSize, state: Optio
|
||||||
if let Some(indicators) = &style.indicators {
|
if let Some(indicators) = &style.indicators {
|
||||||
if let Some(bar) = indicators.bar.as_ref() {
|
if let Some(bar) = indicators.bar.as_ref() {
|
||||||
let color = bar.color.unwrap_or(RGBA::new(0xff, 0xff, 0xff, 0x50).into());
|
let color = bar.color.unwrap_or(RGBA::new(0xff, 0xff, 0xff, 0x50).into());
|
||||||
const PADDING: f32 = 20.0;
|
const WIDTH: f32 = 5.0;
|
||||||
let max_height: f32 = pixmap.height() as f32 - PADDING * 2.0;
|
const PADDING_X: f32 = 5.0;
|
||||||
|
const PADDING_Y: f32 = 20.0;
|
||||||
|
|
||||||
|
let max_height: f32 = pixmap.height() as f32 - PADDING_Y * 2.0;
|
||||||
let height = state.value * max_height;
|
let height = state.value * max_height;
|
||||||
let y = pixmap.height() as f32 - PADDING - height;
|
|
||||||
|
let x = if state.path.position.is_left() {
|
||||||
|
PADDING_X
|
||||||
|
} else {
|
||||||
|
pixmap.width() as f32 - PADDING_X - WIDTH
|
||||||
|
};
|
||||||
|
|
||||||
|
let y = pixmap.height() as f32 - PADDING_Y - height;
|
||||||
|
|
||||||
pixmap.fill_rect(
|
pixmap.fill_rect(
|
||||||
Rect::from_xywh(pixmap.width() as f32 - 10.0, y, 5.0, height).unwrap(),
|
Rect::from_xywh(x, y, WIDTH, height).unwrap(),
|
||||||
&Paint {
|
&Paint {
|
||||||
shader: Shader::SolidColor(Color::from_rgba8(color.r, color.g, color.b, color.a)),
|
shader: Shader::SolidColor(Color::from_rgba8(color.r, color.g, color.b, color.a)),
|
||||||
..Paint::default()
|
..Paint::default()
|
||||||
|
|
|
@ -10,7 +10,6 @@ use libpulse_binding::context::introspect::{Introspector, SinkInfo, SinkInputInf
|
||||||
use libpulse_binding::context::subscribe::{Facility, InterestMaskSet};
|
use libpulse_binding::context::subscribe::{Facility, InterestMaskSet};
|
||||||
use libpulse_binding::context::{subscribe, Context, FlagSet};
|
use libpulse_binding::context::{subscribe, Context, FlagSet};
|
||||||
use libpulse_binding::def::Retval;
|
use libpulse_binding::def::Retval;
|
||||||
use libpulse_binding::mainloop::api::Mainloop as MainloopTrait;
|
|
||||||
use libpulse_binding::mainloop::standard::{IterateResult, Mainloop};
|
use libpulse_binding::mainloop::standard::{IterateResult, Mainloop};
|
||||||
use libpulse_binding::operation::Operation;
|
use libpulse_binding::operation::Operation;
|
||||||
use libpulse_binding::volume::{ChannelVolumes, Volume};
|
use libpulse_binding::volume::{ChannelVolumes, Volume};
|
||||||
|
@ -23,17 +22,32 @@ pub type PaEntityId = u32;
|
||||||
pub enum PaEntityKind {
|
pub enum PaEntityKind {
|
||||||
Source,
|
Source,
|
||||||
Sink,
|
Sink,
|
||||||
Application,
|
SinkInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||||
|
pub enum PaEntityMetadata {
|
||||||
|
Source {
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
},
|
||||||
|
Sink {
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
},
|
||||||
|
SinkInput {
|
||||||
|
description: String,
|
||||||
|
binary_name: Option<String>,
|
||||||
|
application_name: Option<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PaEntityState {
|
pub struct PaEntityState {
|
||||||
id: PaEntityId,
|
id: PaEntityId,
|
||||||
kind: PaEntityKind,
|
|
||||||
name: String,
|
|
||||||
display_name: String,
|
|
||||||
channel_volumes: ChannelVolumes,
|
channel_volumes: ChannelVolumes,
|
||||||
is_muted: bool,
|
is_muted: bool,
|
||||||
|
metadata: PaEntityMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PaEntityState {
|
impl PaEntityState {
|
||||||
|
@ -41,16 +55,16 @@ impl PaEntityState {
|
||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn kind(&self) -> &PaEntityKind {
|
pub fn kind(&self) -> PaEntityKind {
|
||||||
&self.kind
|
match &self.metadata {
|
||||||
|
PaEntityMetadata::Source { .. } => PaEntityKind::Source,
|
||||||
|
PaEntityMetadata::Sink { .. } => PaEntityKind::Sink,
|
||||||
|
PaEntityMetadata::SinkInput { .. } => PaEntityKind::SinkInput,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn name(&self) -> &String {
|
pub fn metadata(&self) -> &PaEntityMetadata {
|
||||||
&self.name
|
&self.metadata
|
||||||
}
|
|
||||||
|
|
||||||
pub fn display_name(&self) -> &String {
|
|
||||||
&self.display_name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn channel_volumes(&self) -> Vec<f32> {
|
pub fn channel_volumes(&self) -> Vec<f32> {
|
||||||
|
@ -67,10 +81,11 @@ impl From<&SourceInfo<'_>> for PaEntityState {
|
||||||
PaEntityState {
|
PaEntityState {
|
||||||
id: value.index as PaEntityId,
|
id: value.index as PaEntityId,
|
||||||
is_muted: value.mute,
|
is_muted: value.mute,
|
||||||
name: value.name.clone().unwrap_or_default().into_owned(),
|
|
||||||
display_name: value.description.clone().unwrap_or_default().into_owned(),
|
|
||||||
kind: PaEntityKind::Source,
|
|
||||||
channel_volumes: value.volume,
|
channel_volumes: value.volume,
|
||||||
|
metadata: PaEntityMetadata::Source {
|
||||||
|
name: value.name.clone().unwrap_or_default().into_owned(),
|
||||||
|
description: value.description.clone().unwrap_or_default().into_owned(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,10 +95,11 @@ impl From<&SinkInfo<'_>> for PaEntityState {
|
||||||
PaEntityState {
|
PaEntityState {
|
||||||
id: value.index as PaEntityId,
|
id: value.index as PaEntityId,
|
||||||
is_muted: value.mute,
|
is_muted: value.mute,
|
||||||
name: value.name.clone().unwrap_or_default().into_owned(),
|
|
||||||
display_name: value.description.clone().unwrap_or_default().into_owned(),
|
|
||||||
kind: PaEntityKind::Sink,
|
|
||||||
channel_volumes: value.volume,
|
channel_volumes: value.volume,
|
||||||
|
metadata: PaEntityMetadata::Sink {
|
||||||
|
name: value.name.clone().unwrap_or_default().into_owned(),
|
||||||
|
description: value.description.clone().unwrap_or_default().into_owned(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,10 +109,18 @@ impl From<&SinkInputInfo<'_>> for PaEntityState {
|
||||||
PaEntityState {
|
PaEntityState {
|
||||||
id: value.index as PaEntityId,
|
id: value.index as PaEntityId,
|
||||||
is_muted: value.mute,
|
is_muted: value.mute,
|
||||||
name: value.name.clone().unwrap_or_default().into_owned(),
|
|
||||||
display_name: value.name.clone().unwrap_or_default().into_owned(),
|
|
||||||
kind: PaEntityKind::Application,
|
|
||||||
channel_volumes: value.volume,
|
channel_volumes: value.volume,
|
||||||
|
metadata: PaEntityMetadata::SinkInput {
|
||||||
|
description: value.name.clone().unwrap_or_default().into_owned(),
|
||||||
|
application_name: value
|
||||||
|
.proplist
|
||||||
|
.get("application.name")
|
||||||
|
.map(|v| String::from_utf8_lossy(v).trim_end_matches(char::from(0)).to_owned()),
|
||||||
|
binary_name: value
|
||||||
|
.proplist
|
||||||
|
.get("application.process.binary")
|
||||||
|
.map(|v| String::from_utf8_lossy(v).trim_end_matches(char::from(0)).to_owned()),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -353,14 +377,14 @@ impl PaThread {
|
||||||
loop {
|
loop {
|
||||||
self.run_single_mainloop_iteration(false);
|
self.run_single_mainloop_iteration(false);
|
||||||
|
|
||||||
if let Ok(command) = self.commands_rx.try_recv() {
|
while let Ok(command) = self.commands_rx.try_recv() {
|
||||||
match command {
|
match command {
|
||||||
PaCommand::SetIsMuted { id, value } => {
|
PaCommand::SetIsMuted { id, value } => {
|
||||||
if let Some(state) = PaThread::unwrap_state(¤t_state).entities_by_id.get(&id) {
|
if let Some(state) = PaThread::unwrap_state(¤t_state).entities_by_id.get(&id) {
|
||||||
match state.kind {
|
match state.kind() {
|
||||||
PaEntityKind::Sink => self.introspector.borrow_mut().set_sink_mute_by_index(id, value, None),
|
PaEntityKind::Sink => self.introspector.borrow_mut().set_sink_mute_by_index(id, value, None),
|
||||||
PaEntityKind::Source => self.introspector.borrow_mut().set_source_mute_by_index(id, value, None),
|
PaEntityKind::Source => self.introspector.borrow_mut().set_source_mute_by_index(id, value, None),
|
||||||
PaEntityKind::Application => self.introspector.borrow_mut().set_sink_input_mute(id, value, None),
|
PaEntityKind::SinkInput => self.introspector.borrow_mut().set_sink_input_mute(id, value, None),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -371,10 +395,10 @@ impl PaThread {
|
||||||
value.set(i as u8, Volume((Volume::NORMAL.0 as f32 * v).floor() as u32));
|
value.set(i as u8, Volume((Volume::NORMAL.0 as f32 * v).floor() as u32));
|
||||||
}
|
}
|
||||||
|
|
||||||
match state.kind {
|
match state.kind() {
|
||||||
PaEntityKind::Sink => self.introspector.borrow_mut().set_sink_volume_by_index(id, &value, None),
|
PaEntityKind::Sink => self.introspector.borrow_mut().set_sink_volume_by_index(id, &value, None),
|
||||||
PaEntityKind::Source => self.introspector.borrow_mut().set_source_volume_by_index(id, &value, None),
|
PaEntityKind::Source => self.introspector.borrow_mut().set_source_volume_by_index(id, &value, None),
|
||||||
PaEntityKind::Application => self.introspector.borrow_mut().set_sink_input_volume(id, &value, None),
|
PaEntityKind::SinkInput => self.introspector.borrow_mut().set_sink_input_volume(id, &value, None),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue