From 846a0610636d76273594f844d80429d3ffeef680 Mon Sep 17 00:00:00 2001 From: Moritz Ruth Date: Sun, 14 Jan 2024 02:01:52 +0100 Subject: [PATCH] commit --- Cargo.lock | 305 ++++-------------- Cargo.toml | 3 +- deckster/Cargo.toml | 4 +- .../examples/full/knob-pages/default.toml | 8 +- deckster/src/model/position.rs | 15 +- deckster/src/modes/knob/audio_volume.rs | 61 +++- deckster/src/runner/mod.rs | 20 +- pa-volume-interface/Cargo.toml | 9 + pa-volume-interface/src/lib.rs | 270 ++++++++++++++++ 9 files changed, 432 insertions(+), 263 deletions(-) create mode 100644 pa-volume-interface/Cargo.toml create mode 100644 pa-volume-interface/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index fd3907e..e8eb1ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,7 +76,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -86,15 +86,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys", ] -[[package]] -name = "anyhow" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" - [[package]] name = "arrayref" version = "0.3.7" @@ -134,26 +128,6 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" -[[package]] -name = "bindgen" -version = "0.66.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" -dependencies = [ - "bitflags 2.4.1", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.48", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -166,6 +140,15 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -193,25 +176,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-expr" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6100bc57b6209840798d95cb2775684849d332f7bd788db2a8c8caf7ef82a41a" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -231,17 +195,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "clang-sys" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "4.4.13" @@ -327,21 +280,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie-factory" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b" - [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -437,7 +375,7 @@ dependencies = [ "log", "loupedeck_serial", "once_cell", - "pipewire", + "pa-volume-interface", "regex", "resvg", "rgb", @@ -467,7 +405,7 @@ version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ - "convert_case 0.4.0", + "convert_case", "proc-macro2", "quote", "rustc_version", @@ -567,7 +505,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -701,12 +639,6 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - [[package]] name = "hashbrown" version = "0.12.3" @@ -782,6 +714,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "imagesize" version = "0.12.0" @@ -834,7 +780,7 @@ checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" dependencies = [ "hermit-abi", "rustix", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -873,62 +819,18 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" -[[package]] -name = "libloading" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" -[[package]] -name = "libspa" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0434617020ddca18b86067912970c55410ca654cdafd775480322f50b857a8c4" -dependencies = [ - "bitflags 2.4.1", - "cc", - "convert_case 0.6.0", - "cookie-factory", - "libc", - "libspa-sys", - "nix", - "nom", - "system-deps", -] - -[[package]] -name = "libspa-sys" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e70ca3f3e70f858ef363046d06178c427b4e0b63d210c95fd87d752679d345" -dependencies = [ - "bindgen", - "cc", - "system-deps", -] - [[package]] name = "libudev" version = "0.3.0" @@ -1018,21 +920,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1061,18 +948,6 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset", - "pin-utils", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", ] [[package]] @@ -1115,6 +990,15 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "pa-volume-interface" +version = "0.1.0" +dependencies = [ + "flume", + "im", + "tokio", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -1138,12 +1022,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "pico-args" version = "0.5.0" @@ -1156,40 +1034,6 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pipewire" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d009c8dd65e890b515a71950f7e4c801523b8894ff33863a40830bf762e9e9" -dependencies = [ - "anyhow", - "bitflags 2.4.1", - "libc", - "libspa", - "libspa-sys", - "nix", - "once_cell", - "pipewire-sys", - "thiserror", -] - -[[package]] -name = "pipewire-sys" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "890c084e7b737246cb4799c86b71a0e4da536031ff7473dd639eba9f95039f64" -dependencies = [ - "bindgen", - "libspa-sys", - "system-deps", -] - [[package]] name = "pkg-config" version = "0.3.28" @@ -1233,6 +1077,21 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + [[package]] name = "rangemap" version = "1.4.0" @@ -1355,7 +1214,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -1531,12 +1390,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shlex" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" - [[package]] name = "simd-adler32" version = "0.3.7" @@ -1558,6 +1411,16 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "slotmap" version = "1.0.7" @@ -1648,25 +1511,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-deps" -version = "6.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.12.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" - [[package]] name = "termcolor" version = "1.4.0" @@ -1887,6 +1731,12 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unescaper" version = "0.1.3" @@ -2023,12 +1873,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" -[[package]] -name = "version-compare" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" - [[package]] name = "version_check" version = "0.9.4" @@ -2151,15 +1995,6 @@ dependencies = [ "windows-targets 0.52.0", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 40b06b9..eb06c9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [workspace] members = [ "deckster", - "loupedeck_serial" + "loupedeck_serial", + "pa-volume-interface" ] resolver = "2" diff --git a/deckster/Cargo.toml b/deckster/Cargo.toml index 7f6abde..ded928a 100644 --- a/deckster/Cargo.toml +++ b/deckster/Cargo.toml @@ -17,6 +17,7 @@ flume = "0.11.0" humantime-serde = "1.1.1" log = "0.4.20" loupedeck_serial = { path = "../loupedeck_serial" } +pa-volume-interface = { path = "../pa-volume-interface" } regex = "1.10.2" resvg = "0.37.0" rgb = "0.8.37" @@ -28,5 +29,4 @@ tiny-skia = "0.11.3" tokio = { version = "1.35.1", features = ["macros", "parking_lot", "rt", "rt-multi-thread", "sync"]} toml = "0.8.8" walkdir = "2.4.0" -once_cell = "1.19.0" -pipewire = "0.7.2" \ No newline at end of file +once_cell = "1.19.0" \ No newline at end of file diff --git a/deckster/examples/full/knob-pages/default.toml b/deckster/examples/full/knob-pages/default.toml index 655128c..5f37174 100644 --- a/deckster/examples/full/knob-pages/default.toml +++ b/deckster/examples/full/knob-pages/default.toml @@ -1,11 +1,9 @@ [knobs.right-top] icon = "@ph/microphone-light[scale=0.9]" -indicators.circle.color = "#ffffff" -indicators.circle.width = 2 -indicators.circle.radius = 20 +indicators.bar.color = "#ffffff50" mode.audio_volume.direction = "input" -mode.audio_volume.regex = "Microphone" +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" @@ -13,7 +11,7 @@ mode.audio_volume.style.inactive.icon = "@ph/microphone-slash-light[alpha=0.9|co [knobs.right-middle] icon = "@apps/discord[scale=0.25]" -indicators.bar.color = "#ffffff" +indicators.bar.color = "#ffffff50" mode.audio_volume.regex = "Discord" mode.audio_volume.style.active.label = "{percentage}%" diff --git a/deckster/src/model/position.rs b/deckster/src/model/position.rs index 2e11108..9d776df 100644 --- a/deckster/src/model/position.rs +++ b/deckster/src/model/position.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use serde_with::{DeserializeFromStr, SerializeDisplay}; use thiserror::Error; -use loupedeck_serial::characteristics::LoupedeckButton; +use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckKnob}; #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, SerializeDisplay, DeserializeFromStr)] /// One-based coordinates of a specific virtual (not physical) key. @@ -67,6 +67,19 @@ pub enum KnobPosition { RightBottom, } +impl From for KnobPosition { + fn from(value: LoupedeckKnob) -> Self { + match value { + LoupedeckKnob::LeftTop => KnobPosition::LeftTop, + LoupedeckKnob::LeftMiddle => KnobPosition::LeftMiddle, + LoupedeckKnob::LeftBottom => KnobPosition::LeftBottom, + LoupedeckKnob::RightTop => KnobPosition::RightTop, + LoupedeckKnob::RightMiddle => KnobPosition::RightMiddle, + LoupedeckKnob::RightBottom => KnobPosition::RightBottom, + } + } +} + #[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)] pub struct KnobPath { pub page_id: String, diff --git a/deckster/src/modes/knob/audio_volume.rs b/deckster/src/modes/knob/audio_volume.rs index fa2f3b6..b5ed2b8 100644 --- a/deckster/src/modes/knob/audio_volume.rs +++ b/deckster/src/modes/knob/audio_volume.rs @@ -1,10 +1,15 @@ use std::collections::HashMap; use std::sync::Arc; +use std::time::{Duration, Instant}; +use once_cell::sync::Lazy; use regex::Regex; use serde::Deserialize; +use tokio::select; use tokio::sync::broadcast; +use pa_volume_interface::{PaEntityState, PaVolumeInterface}; + use crate::model::knob_page::StyleByStateMap; use crate::model::position::KnobPath; use crate::modes::knob::{KnobEvent, RotationDirection}; @@ -59,23 +64,57 @@ pub enum State { Muted, } +static PA_VOLUME_INTERFACE: Lazy = Lazy::new(|| { + let interface = PaVolumeInterface::spawn_thread(Duration::from_millis(500)); + interface.query_state(); + interface +}); + pub async fn handle(path: KnobPath, config: Arc, mut events: broadcast::Receiver<(KnobPath, KnobEvent)>, commands: flume::Sender) { + let mut volume_update_timestamp = Instant::now(); let mut value: f32 = 0.0; + let mut entity_state: Option> = None; - while let Ok((event_path, event)) = events.recv().await { - if event_path != path { - continue; - } + let pa_volume_interface = &PA_VOLUME_INTERFACE; + let mut volume_states = pa_volume_interface.subscribe_to_state(); - if let KnobEvent::Rotate { direction } = event { - let factor = match direction { - RotationDirection::Clockwise => 1.0, - RotationDirection::Counterclockwise => -1.0, - }; + loop { + select! { + Ok(volume_state) = volume_states.recv() => { + entity_state = volume_state.entities_by_id().values().find(|entity| config.regex.is_match(entity.name())).map(Arc::clone); - value = (value + (factor * config.delta)).clamp(0.0, 1.0); + if volume_state.timestamp() > &volume_update_timestamp { + value = entity_state.as_ref().map(|s| *s.channel_volumes().first().unwrap()).unwrap_or(0.0); + commands.send(IoWorkerCommand::SetKnobValue { path: path.clone(), value }).unwrap(); + } + } - commands.send(IoWorkerCommand::SetKnobValue { path: path.clone(), value }).unwrap(); + Ok((event_path, event)) = events.recv() => { + if event_path != path { + continue; + } + + if let Some(entity_state) = &entity_state { + match event { + KnobEvent::Rotate { direction } => { + let factor = match direction { + RotationDirection::Clockwise => 1.0, + RotationDirection::Counterclockwise => -1.0, + }; + + volume_update_timestamp = Instant::now(); + value = (value + (factor * config.delta)).clamp(0.0, 1.0); + pa_volume_interface.set_channel_volumes(*entity_state.id(), vec![value; entity_state.channel_volumes().len()]); + + commands.send(IoWorkerCommand::SetKnobValue { path: path.clone(), value }).unwrap(); + } + KnobEvent::Press => { + pa_volume_interface.set_is_muted(*entity_state.id(), !entity_state.is_muted()) + } + _ => {} + } + } + } } } } diff --git a/deckster/src/runner/mod.rs b/deckster/src/runner/mod.rs index 4e817f0..a379db9 100644 --- a/deckster/src/runner/mod.rs +++ b/deckster/src/runner/mod.rs @@ -266,14 +266,7 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv } } LoupedeckEvent::KnobRotate { knob, direction } => { - let position = match knob { - LoupedeckKnob::LeftTop => KnobPosition::LeftTop, - LoupedeckKnob::LeftMiddle => KnobPosition::LeftMiddle, - LoupedeckKnob::LeftBottom => KnobPosition::LeftBottom, - LoupedeckKnob::RightTop => KnobPosition::RightTop, - LoupedeckKnob::RightMiddle => KnobPosition::RightMiddle, - LoupedeckKnob::RightBottom => KnobPosition::RightBottom, - }; + let position: KnobPosition = knob.into(); send_knob_event( KnobPath { @@ -288,6 +281,17 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv }, ) } + LoupedeckEvent::KnobDown { knob } => { + let position: KnobPosition = knob.into(); + + send_knob_event( + KnobPath { + page_id: state.active_knob_page_id.clone(), + position, + }, + KnobEvent::Press, + ) + } _ => {} } diff --git a/pa-volume-interface/Cargo.toml b/pa-volume-interface/Cargo.toml new file mode 100644 index 0000000..d98cf47 --- /dev/null +++ b/pa-volume-interface/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pa-volume-interface" +version = "0.1.0" +edition = "2021" + +[dependencies] +flume = "0.11.0" +im = "15.1.0" +tokio = { version = "1.35.1", default-features = false, features = ["sync"] } \ No newline at end of file diff --git a/pa-volume-interface/src/lib.rs b/pa-volume-interface/src/lib.rs new file mode 100644 index 0000000..dac788a --- /dev/null +++ b/pa-volume-interface/src/lib.rs @@ -0,0 +1,270 @@ +use std::process::Command; +use std::sync::Arc; +use std::thread; +use std::time::{Duration, Instant}; + +use im::HashMap; +use tokio::sync::broadcast; + +pub type PaEntityId = usize; + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub enum PaEntityKind { + Source, + Sink, + Application, +} + +#[derive(Debug, Clone)] +pub struct PaEntityState { + id: PaEntityId, + kind: PaEntityKind, + name: String, + channel_volumes: Box<[f32]>, + is_muted: bool, +} + +impl PaEntityState { + pub fn id(&self) -> &PaEntityId { + &self.id + } + + pub fn kind(&self) -> &PaEntityKind { + &self.kind + } + + pub fn name(&self) -> &String { + &self.name + } + + pub fn channel_volumes(&self) -> &[f32] { + &self.channel_volumes + } + + pub fn is_muted(&self) -> bool { + self.is_muted + } +} + +#[derive(Debug, Clone)] +pub struct PaVolumeState { + timestamp: Instant, + entities_by_id: HashMap>, + default_sink_id: PaEntityId, + default_source_id: PaEntityId, +} + +impl PaVolumeState { + pub fn timestamp(&self) -> &Instant { + &self.timestamp + } + + pub fn entities_by_id(&self) -> &HashMap> { + &self.entities_by_id + } + + pub fn default_sink_id(&self) -> &PaEntityId { + &self.default_sink_id + } + + pub fn default_source_id(&self) -> &PaEntityId { + &self.default_source_id + } +} + +enum PaCommand { + QueryState, + SetIsMuted { id: PaEntityId, value: bool }, + SetChannelVolumes { id: PaEntityId, channel_volumes: Box<[f32]> }, + Terminate, +} + +#[derive(Debug)] +struct PaThread { + commands_tx: flume::Sender, +} + +fn query_and_publish_state(state_tx: &broadcast::Sender) { + let output = Command::new("pulsemixer").arg("-l").output().unwrap(); + + if !output.status.success() { + panic!("`pulsemixer -l` failed"); + } + + let raw = String::from_utf8_lossy(&output.stdout); + + let mut default_sink_id = 0; + let mut default_source_id = 0; + + let entities_by_id: HashMap<_, _> = raw + .lines() + .map(|line| { + let (prefix, rest) = line.split_once(':').unwrap(); + let kind = match prefix { + "Sink" => PaEntityKind::Sink, + "Source" => PaEntityKind::Source, + "Sink input" => PaEntityKind::Application, + x => panic!("Unknown entity kind: {}", x), + }; + + let segments: Vec<&str> = rest.split(", ").collect(); + + let id = segments[0] + .trim_start() + .strip_prefix("ID: ") + .unwrap() + .split('-') + .last() + .unwrap() + .parse::() + .unwrap(); + + let name = segments[1].strip_prefix("Name: ").unwrap(); + let is_muted = segments[2].strip_prefix("Mute: ").unwrap().parse::().unwrap() == 1; + + let channel_count = segments[3].strip_prefix("Channels: ").unwrap().parse::().unwrap(); + + let channel_volumes: Box<[f32]> = segments[4..(4 + channel_count)] + .iter() + .map(|s| { + let s = s.strip_prefix("Volumes: [").unwrap_or(s); + s.strip_suffix(']').unwrap_or(s) + }) + .map(|s| &s[1..(s.len() - 2)]) + .map(|s| s.parse::().unwrap() / 100.0) + .collect(); + + if segments.last().unwrap() == &"Default" { + match kind { + PaEntityKind::Source => { + default_source_id = id; + } + PaEntityKind::Sink => { + default_sink_id = id; + } + PaEntityKind::Application => { + panic!("Sink sources cannot be a default"); + } + } + } + + ( + id, + Arc::new(PaEntityState { + id, + kind, + name: name.to_owned(), + is_muted, + channel_volumes, + }), + ) + }) + .collect(); + + state_tx + .send(PaVolumeState { + timestamp: Instant::now(), + entities_by_id, + default_sink_id, + default_source_id, + }) + .unwrap(); +} + +impl PaThread { + fn spawn( + max_time_between_queries: Duration, + commands_tx: flume::Sender, + commands_rx: flume::Receiver, + state_tx: broadcast::Sender, + ) -> PaThread { + thread::spawn(move || loop { + let command = commands_rx.recv_timeout(max_time_between_queries).unwrap_or(PaCommand::QueryState); + + match command { + PaCommand::SetIsMuted { id, value } => { + let action = if value { "--mute" } else { "--unmute" }; + + let status = Command::new("pulsemixer").args(["--id", &id.to_string(), action]).status().unwrap(); + + if !status.success() { + panic!("(Un-)muting failed with status code {:?}", status.code()); + } + } + PaCommand::SetChannelVolumes { id, channel_volumes } => { + let volumes = channel_volumes + .iter() + .map(|v| (v * 100.0).round() as u32) + .map(|v| v.to_string()) + .collect::>() + .join(":"); + + let status = Command::new("pulsemixer") + .args(["--id", &id.to_string(), "--set-volume-all", &volumes]) + .status() + .unwrap(); + + if !status.success() { + panic!("Setting the channel values failed with status code {:?}", status.code()); + } + } + PaCommand::Terminate => break, + PaCommand::QueryState => {} + } + + query_and_publish_state(&state_tx) + }); + + PaThread { commands_tx } + } +} + +impl Drop for PaThread { + fn drop(&mut self) { + self.commands_tx.send(PaCommand::Terminate).ok(); + } +} + +#[derive(Debug, Clone)] +pub struct PaVolumeInterface { + #[allow(unused)] + thread: Arc, + state_tx: broadcast::Sender, + commands_tx: flume::Sender, +} + +impl PaVolumeInterface { + pub fn spawn_thread(max_time_between_queries: Duration) -> PaVolumeInterface { + let (commands_tx, commands_rx) = flume::unbounded(); + let state_tx = broadcast::Sender::new(5); + + let thread = PaThread::spawn(max_time_between_queries, commands_tx.clone(), commands_rx, state_tx.clone()); + + PaVolumeInterface { + thread: Arc::new(thread), + commands_tx, + state_tx, + } + } + + pub fn subscribe_to_state(&self) -> broadcast::Receiver { + self.state_tx.subscribe() + } + + pub fn query_state(&self) { + self.commands_tx.send(PaCommand::QueryState).unwrap() + } + + pub fn set_is_muted(&self, id: PaEntityId, value: bool) { + self.commands_tx.send(PaCommand::SetIsMuted { id, value }).unwrap() + } + + pub fn set_channel_volumes(&self, id: PaEntityId, channel_volumes: impl Into>) { + self.commands_tx + .send(PaCommand::SetChannelVolumes { + id, + channel_volumes: channel_volumes.into(), + }) + .unwrap() + } +}