From 1f48737b2b6d33b393bc12e0df9e2e25e51cfc1b Mon Sep 17 00:00:00 2001 From: 409 Date: Fri, 28 Jun 2024 23:55:33 +0200 Subject: [PATCH] feat: working version --- Cargo.lock | 521 ++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 + src/instructions.rs | 25 ++ src/main.rs | 51 ++++- src/mixer.rs | 465 +++++++++++++++++++++++++++++++++++++ src/pulseaudio/mod.rs | 32 +++ src/utils.rs | 28 +++ 7 files changed, 1123 insertions(+), 2 deletions(-) create mode 100644 src/instructions.rs create mode 100644 src/mixer.rs create mode 100644 src/pulseaudio/mod.rs create mode 100644 src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index f1ea38a..59ebf2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,527 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "cc" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libpulse-binding" +version = "2.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3557a2dfc380c8f061189a01c6ae7348354e0c9886038dc6c171219c08eaff" +dependencies = [ + "bitflags 1.3.2", + "libc", + "libpulse-sys", + "num-derive", + "num-traits", + "winapi", +] + +[[package]] +name = "libpulse-sys" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc19e110fbf42c17260d30f6d3dc545f58491c7830d38ecb9aaca96e26067a9b" +dependencies = [ + "libc", + "num-derive", + "num-traits", + "pkg-config", + "winapi", +] + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mixrs" version = "0.1.0" +dependencies = [ + "anyhow", + "libpulse-binding", + "tokio", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +dependencies = [ + "memchr", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/Cargo.toml b/Cargo.toml index 8413a7b..15a732b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] +anyhow = "1.0.86" +pulse = { version = "2.0", package = "libpulse-binding" } +tokio = { version = "1.38.0", features = ["full"] } diff --git a/src/instructions.rs b/src/instructions.rs new file mode 100644 index 0000000..1aacef1 --- /dev/null +++ b/src/instructions.rs @@ -0,0 +1,25 @@ +#[repr(u8)] +pub enum MixerInstruction { + SelectNext, + SelectPrevious, + ToggleMuteCurrent, + IncreaseCurrent, + DecreaseCurrent, + GetCurrent, + PlayPauseCurrent, +} + +impl MixerInstruction { + pub fn from_u8(byte: u8) -> Self { + match byte { + 0 => MixerInstruction::SelectNext, + 1 => MixerInstruction::SelectPrevious, + 2 => MixerInstruction::ToggleMuteCurrent, + 3 => MixerInstruction::IncreaseCurrent, + 4 => MixerInstruction::DecreaseCurrent, + 5 => MixerInstruction::GetCurrent, + 6 => MixerInstruction::PlayPauseCurrent, + _ => panic!("Could not parse '{byte}' to MixerInstruction"), + } + } +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..7cbda9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,50 @@ -fn main() { - println!("Hello, world!"); +mod instructions; +pub mod mixer; +pub mod pulseaudio; +pub mod utils; + +use anyhow::Result; +use mixer::Mixer; +use pulseaudio::PulseInstruction; +use std::{process::Command, sync::mpsc::channel}; + +const NOTIFY_SEND_REPLACE_ID: u32 = 1448531; + +#[tokio::main] +async fn main() { + let mainloop = pulse::mainloop::standard::Mainloop::new().expect("Error getting main loop"); + + let (pulse_ix_tx, pulse_ix_rx) = channel::(); + + let mut mixer = Mixer::new(mainloop, pulse_ix_tx); + + mixer.run(pulse_ix_rx); +} + +pub fn send_notification(message: &str) -> Result<()> { + Command::new("notify-send") + .args(vec![ + "Mixrs", + message, + "-r", + &NOTIFY_SEND_REPLACE_ID.to_string(), + ]) + .env("DBUS_SESSION_BUS_ADDRESS", "unix:path=/run/user/1000/bus") + .spawn()?; + + Ok(()) +} + +pub fn playerctl_toggle(target: &str) -> Result<()> { + let get_players = Command::new("playerctl").arg("-l").output()?; + let get_players_output = String::from_utf8(get_players.stdout)?; + let players: Vec<&str> = get_players_output.split("\n").collect(); + + match players.iter().find(|p| p.to_lowercase().contains(&target.to_lowercase())) { + Some(player) => { + Command::new("playerctl").args(vec!["-p", player, "play-pause"]).spawn()?; + }, + None => {}, + }; + Ok(()) } diff --git a/src/mixer.rs b/src/mixer.rs new file mode 100644 index 0000000..91698e6 --- /dev/null +++ b/src/mixer.rs @@ -0,0 +1,465 @@ +use anyhow::Result; + +use std::{ + borrow::{Borrow, BorrowMut}, + collections::HashMap, + fs, + io::Read, + os::unix::net::UnixListener, + path::Path, + sync::{ + mpsc::{channel, Receiver, Sender}, + Arc, Mutex, + }, + thread, + time::Duration, + usize, +}; + +use pulse::{ + callbacks::ListResult, + context::{ + subscribe::{Facility, InterestMaskSet, Operation}, + FlagSet, + }, + mainloop::standard::{IterateResult, Mainloop}, + volume::ChannelVolumes, +}; + +use crate::{ + instructions::MixerInstruction, + playerctl_toggle, + pulseaudio::{PulseInstruction, SinkInputMixerData}, + send_notification, + utils::{get_sink_input_name, percentage_to_total_volume}, +}; + +pub struct Mixer { + sink_inputs: HashMap, + selected_index: Arc>>, + mainloop: Mainloop, + context: pulse::context::Context, +} + +impl Mixer { + pub fn new(mut mainloop: Mainloop, pulse_ix_tx: Sender) -> Self { + let mut context = + pulse::context::Context::new(&mainloop, "Mixrs").expect("Error creating pulse context"); + + context + .borrow_mut() + .connect(None, FlagSet::NOFLAGS, None) + .expect("Error connecting pulse context"); + + loop { + match mainloop.borrow_mut().iterate(false) { + IterateResult::Quit(_) | IterateResult::Err(_) => { + panic!("Iterate state was not success, quitting..."); + } + IterateResult::Success(_) => {} + } + match context.borrow().get_state() { + pulse::context::State::Ready => { + break; + } + pulse::context::State::Failed | pulse::context::State::Terminated => { + panic!("Context state failed/terminated, quitting..."); + } + _ => {} + } + } + + let sink_inputs: HashMap = HashMap::new(); + + let selected_index: Arc>> = Arc::new(Mutex::new(None)); + + context.subscribe(InterestMaskSet::SINK_INPUT, |_| {}); + + context.set_subscribe_callback(Some(Box::new(move |facility, operation, index| { + let Some(facility) = facility else { + return; + }; + + let Some(operation) = operation else { + return; + }; + + let Facility::SinkInput = facility else { + return; + }; + + pulse_ix_tx + .send(match operation { + Operation::New => PulseInstruction::AddSinkInput(index), + Operation::Changed => PulseInstruction::UpdateSinkInput(index), + Operation::Removed => PulseInstruction::RemoveSinkInput(index), + }) + .unwrap(); + }))); + + Self { + sink_inputs, + selected_index, + mainloop, + context, + } + } + + pub fn create_socket_listener(&self) -> Result { + let socket_path = Path::new("/tmp/mixrs"); + + if socket_path.exists() { + fs::remove_file(socket_path)?; + } + + let listener = UnixListener::bind(socket_path)?; + + Ok(listener) + } + + pub fn run(&mut self, pulse_ix_rx: Receiver) -> ! { + let listener = self + .create_socket_listener() + .expect("Error creating unix socket listener"); + + let (mixer_tx, mixer_rx) = channel::(); + + thread::spawn(move || { + for client in listener.incoming() { + match client { + Ok(mut stream) => { + let mut buf: Vec = Vec::with_capacity(1); + stream.read_to_end(&mut buf).expect("Error reading stream"); + + mixer_tx.send(MixerInstruction::from_u8(buf[0])).unwrap(); + } + Err(_) => println!("Stream error"), + } + } + }); + + let initial_sink_inputs: Arc>> = + Arc::new(Mutex::new(HashMap::new())); + let callback_initial_sink_inputs = initial_sink_inputs.clone(); + + let initial_sink_inputs_operation = self + .context + .borrow_mut() + .introspect() + .borrow_mut() + .get_sink_input_info_list(move |r| { + let ListResult::Item(sink_input) = r else { + return; + }; + + callback_initial_sink_inputs.lock().unwrap().insert( + sink_input.index, + SinkInputMixerData { + name: get_sink_input_name(&sink_input).unwrap(), + volume: sink_input.volume.avg().0, + channels: sink_input.volume.len(), + muted: sink_input.mute, + }, + ); + }); + + while initial_sink_inputs_operation.get_state() == pulse::operation::State::Running { + iterate_mainloop(&mut self.mainloop); + } + + self.sink_inputs = initial_sink_inputs.lock().unwrap().clone(); + + *self.selected_index.lock().unwrap() = match self.sink_inputs.keys().nth(0) { + Some(_) => Some(0), + None => None, + }; + + loop { + match mixer_rx.try_recv() { + Ok(ix) => match ix { + MixerInstruction::SelectNext => self.select_next(), + MixerInstruction::SelectPrevious => self.select_previous(), + MixerInstruction::ToggleMuteCurrent => self.toggle_mute_current(), + MixerInstruction::IncreaseCurrent => self.increase_volume_current(), + MixerInstruction::DecreaseCurrent => self.decrease_volume_current(), + MixerInstruction::GetCurrent => self.get_current(), + MixerInstruction::PlayPauseCurrent => self.play_pause_current(), + }, + Err(_) => (), + } + + if let Some(ix) = pulse_ix_rx.try_recv().ok() { + match ix { + PulseInstruction::AddSinkInput(sink_index) => { + let result: Arc>> = + Arc::new(Mutex::new(None)); + let operation_result = result.clone(); + + let operation = self + .context + .borrow_mut() + .introspect() + .borrow_mut() + .get_sink_input_info(sink_index, move |r| { + if let ListResult::Item(sink_input) = r { + *operation_result.lock().unwrap() = Some(SinkInputMixerData { + name: get_sink_input_name(sink_input).unwrap(), + volume: sink_input.volume.avg().0, + channels: sink_input.volume.len(), + muted: sink_input.mute, + }); + } + }); + + while operation.get_state() == pulse::operation::State::Running { + iterate_mainloop(&mut self.mainloop); + } + + let sink_input = result.lock().unwrap().take(); + if let Some(sink_input) = sink_input { + self.sink_inputs.insert(sink_index, sink_input); + } + } + PulseInstruction::RemoveSinkInput(sink_index) => { + let selected_index_lock = self.selected_index.lock().unwrap(); + + match *selected_index_lock { + Some(current_index) => { + drop(selected_index_lock); + + let removed_sink_input_index = self + .sink_inputs + .keys() + .position(|k| *k == sink_index) + .unwrap(); + + let current_key = + *self.sink_inputs.keys().nth(current_index).unwrap(); + + if self.sink_inputs.remove(&sink_index).is_some() { + if sink_index == current_key + || removed_sink_input_index > current_index + { + self.select_previous(); + } + } + } + + None => (), + } + } + PulseInstruction::UpdateSinkInput(sink_index) => { + match self.sink_inputs.get_mut(&sink_index) { + Some(sink_input_mixer_data) => { + let new_sink_input: Arc>> = + Arc::new(Mutex::new(None)); + let callback_new_sink_input = new_sink_input.clone(); + + let operation = self + .context + .borrow_mut() + .introspect() + .borrow_mut() + .get_sink_input_info(sink_index, move |r| { + let ListResult::Item(sink_input) = r else { + return; + }; + + *callback_new_sink_input.lock().unwrap() = + Some(SinkInputMixerData { + name: get_sink_input_name(&sink_input).unwrap(), + volume: sink_input.volume.avg().0, + channels: sink_input.volume.len(), + muted: sink_input.mute, + }); + }); + + while operation.get_state() == pulse::operation::State::Running { + iterate_mainloop(&mut self.mainloop); + } + + let mut sink_input_lock = new_sink_input.lock().unwrap(); + if let Some(new_sink_input) = sink_input_lock.take() { + *sink_input_mixer_data = new_sink_input; + } + } + None => (), + } + } + } + } + + iterate_mainloop(&mut self.mainloop); + } + } + + pub fn select_next(&mut self) { + let mut index_lock = self.selected_index.lock().unwrap(); + + match *index_lock { + Some(current_index) => { + let new_index: usize = + (current_index.overflowing_add(1).0 % self.sink_inputs.len()).max(0); + + if current_index != new_index { + *index_lock = Some(new_index); + } + + drop(index_lock); + self.get_current(); + } + None => { + *index_lock = if self.sink_inputs.len() > 0 { + Some(0) + } else { + None + }; + } + } + } + + pub fn select_previous(&mut self) { + let mut index_lock = self.selected_index.lock().unwrap(); + + match *index_lock { + Some(current_index) => { + let new_index: usize = match current_index.overflowing_sub(1) { + (_, true) => self.sink_inputs.len() - 1, + (new_value, false) => new_value, + }; + + if current_index != new_index { + *index_lock = Some(new_index); + } + + drop(index_lock); + self.get_current(); + } + None => { + *index_lock = if self.sink_inputs.len() > 0 { + Some(0) + } else { + None + }; + } + } + } + + pub fn toggle_mute_current(&mut self) { + let index_lock = self.selected_index.lock().unwrap(); + + let Some(index) = *index_lock else { + return; + }; + + drop(index_lock); + + let sink_index = *self.sink_inputs.keys().nth(index).unwrap(); + + self.context + .borrow_mut() + .introspect() + .borrow_mut() + .set_sink_input_mute( + sink_index, + !self.sink_inputs.get(&sink_index).unwrap().muted, + None, + ); + } + + pub fn increase_volume_current(&mut self) { + let index_lock = self.selected_index.lock().unwrap(); + + let Some(index) = *index_lock else { + return; + }; + + drop(index_lock); + + let sink_index = *self.sink_inputs.keys().nth(index).unwrap(); + + let sink_input = self.sink_inputs.get(&sink_index).unwrap(); + + let mut volume = ChannelVolumes::default(); + volume.set( + sink_input.channels, + pulse::volume::Volume(sink_input.volume), + ); + + volume.increase(pulse::volume::Volume(percentage_to_total_volume(5))); + + self.context + .borrow_mut() + .introspect() + .borrow_mut() + .set_sink_input_volume(sink_index, &volume, None); + } + + pub fn decrease_volume_current(&mut self) { + let index_lock = self.selected_index.lock().unwrap(); + + let Some(index) = *index_lock else { + return; + }; + + drop(index_lock); + + let sink_index = *self.sink_inputs.keys().nth(index).unwrap(); + + let sink_input = self.sink_inputs.get(&sink_index).unwrap(); + + let mut volume = ChannelVolumes::default(); + volume.set( + sink_input.channels, + pulse::volume::Volume(sink_input.volume), + ); + + volume.decrease(pulse::volume::Volume(percentage_to_total_volume(5))); + + self.context + .borrow_mut() + .introspect() + .borrow_mut() + .set_sink_input_volume(sink_index, &volume, None); + } + + pub fn get_current(&self) { + let index_lock = self.selected_index.lock().unwrap(); + + let Some(index) = *index_lock else { + return; + }; + + drop(index_lock); + + let sink_index = *self.sink_inputs.keys().nth(index).unwrap(); + + let current_name = &self.sink_inputs.get(&sink_index).unwrap().name; + let _ = send_notification(&format!("Selected: {current_name}")); + } + + pub fn play_pause_current(&self) { + let index_lock = self.selected_index.lock().unwrap(); + + let Some(index) = *index_lock else { + return; + }; + + drop(index_lock); + + let sink_index = *self.sink_inputs.keys().nth(index).unwrap(); + + let current_name = &self.sink_inputs.get(&sink_index).unwrap().name; + match playerctl_toggle(current_name) { + Ok(_) => { + let _ = send_notification(&format!("Toggled {current_name}")); + } + Err(_) => (), + }; + } +} + +pub fn iterate_mainloop(mainloop: &mut pulse::mainloop::standard::Mainloop) { + mainloop.borrow_mut().iterate(false); + thread::sleep(Duration::from_millis(5)); +} diff --git a/src/pulseaudio/mod.rs b/src/pulseaudio/mod.rs new file mode 100644 index 0000000..0b83a91 --- /dev/null +++ b/src/pulseaudio/mod.rs @@ -0,0 +1,32 @@ +use std::u32; + +use crate::utils::total_volume_to_percentage; + +pub enum PulseInstruction { + AddSinkInput(u32), + RemoveSinkInput(u32), + UpdateSinkInput(u32), +} + +pub enum PulseResponse { + Ok, + Error, + SinkInput(Option), + SinkInputs(Vec), +} + +#[derive(Clone, Debug)] +pub struct SinkInputMixerData { + /// The input sink's `application.name` + pub name: String, + /// The input sink's volume + pub volume: u32, + pub muted: bool, + pub channels: u8, +} + +impl SinkInputMixerData { + pub fn get_volume_percent(&self) -> u8 { + total_volume_to_percentage(self.volume) + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..61b7939 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,28 @@ +use anyhow::anyhow; +use pulse::{context::introspect::SinkInputInfo, volume}; + +const FULL_VOLUME: u32 = 1 << 16; + +pub fn volume_to_percentage(volume: volume::ChannelVolumes) -> u8 { + let average = volume.avg().0; + + total_volume_to_percentage(average) +} + +pub fn total_volume_to_percentage(volume: u32) -> u8 { + ((volume as f32 / FULL_VOLUME as f32) * 100.0).round() as u8 +} + +pub fn percentage_to_total_volume(percentage: u8) -> u32 { + ((FULL_VOLUME as f32 / 100.0) * percentage as f32).round() as u32 +} + +pub fn get_sink_input_name(sink_input: &SinkInputInfo) -> anyhow::Result { + let Some(name_bytes) = sink_input.proplist.get("application.name") else { + return Err(anyhow!("Invalid sink input name")); + }; + + Ok(String::from_utf8( + name_bytes[..name_bytes.len() - 1].to_vec(), + )?) +}