1
0
Fork 0

Initial commit

This commit is contained in:
Moritz Ruth 2024-09-12 22:25:20 +02:00
commit 1b757150ea
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
8 changed files with 1182 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
/.idea

531
Cargo.lock generated Normal file
View file

@ -0,0 +1,531 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "arc-swap"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[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.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[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.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "flume"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
dependencies = [
"futures-core",
"futures-sink",
"nanorand",
"spin",
]
[[package]]
name = "futures-core"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-sink"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "gimli"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64"
[[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 = "js-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "libpulse-binding"
version = "2.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3557a2dfc380c8f061189a01c6ae7348354e0c9886038dc6c171219c08eaff"
dependencies = [
"bitflags",
"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 = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[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.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [
"adler2",
]
[[package]]
name = "nanorand"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
dependencies = [
"getrandom",
]
[[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 = "object"
version = "0.36.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[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 = "pulseaudio-volume-interface"
version = "1.0.0"
dependencies = [
"arc-swap",
"flume",
"im",
"libpulse-binding",
"log",
"tokio",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
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 = "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 = "sized-chunks"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e"
dependencies = [
"bitmaps",
"typenum",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[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.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tokio"
version = "1.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
dependencies = [
"backtrace",
"pin-project-lite",
]
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.77",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
[[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-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

12
Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "pulseaudio-volume-interface"
version = "1.0.0"
edition = "2021"
[dependencies]
flume = "0.11.0"
im = "15.1.0"
tokio = { version = "1.38.0", default-features = false, features = ["sync"] }
libpulse-binding = "2.28.1"
log = "0.4.21"
arc-swap = "1.7.1"

55
LICENSE.md Normal file
View file

@ -0,0 +1,55 @@
# Blue Oak Model License
Version 1.0.0
## Purpose
This license gives everyone as much permission to work with
this software as possible, while protecting contributors
from liability.
## Acceptance
In order to receive this license, you must agree to its
rules. The rules of this license are both obligations
under that agreement and conditions to your license.
You must not do anything with this software that triggers
a rule that you cannot or will not follow.
## Copyright
Each contributor licenses you to do everything with this
software that would otherwise infringe that contributor's
copyright in it.
## Notices
You must ensure that everyone who gets a copy of
any part of this software from you, with or without
changes, also gets the text of this license or a link to
<https://blueoakcouncil.org/license/1.0.0>.
## Excuse
If anyone notifies you in writing that you have not
complied with [Notices](#notices), you can keep your
license by taking all practical steps to comply within 30
days after the notice. If you do not do so, your license
ends immediately.
## Patent
Each contributor licenses you to do everything with this
software that would otherwise infringe any patent claims
they can license or become able to license.
## Reliability
No contributor can revoke this license.
## No Liability
***As far as the law allows, this software comes as is,
without any warranty or condition, and no contributor
will be liable to anyone for any damages related to this
software or this license, under any kind of legal claim.***

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# pulseaudio-volume-interface
> Rust library for observing and modifying PulseAudio source, sink, and sink input volumes. Also allows to set the default source and sink.

1
rustfmt.toml Normal file
View file

@ -0,0 +1 @@
max_width = 160

208
src/lib.rs Normal file
View file

@ -0,0 +1,208 @@
mod worker;
use std::num::NonZeroU32;
use crate::worker::*;
use arc_swap::ArcSwap;
use im::HashMap;
use libpulse_binding::context::introspect::{SinkInfo, SinkInputInfo, SourceInfo};
use libpulse_binding::volume::{ChannelVolumes, Volume};
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::broadcast;
use tokio::sync::broadcast::Receiver;
#[allow(unused_imports)]
use libpulse_binding::mainloop::api::Mainloop as _;
pub type EntityId = NonZeroU32;
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum EntityKind {
Source,
Sink,
SinkInput,
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum EntityMetadata {
Source {
name: String,
description: String,
},
Sink {
name: String,
description: String,
},
SinkInput {
description: String,
binary_name: Option<String>,
application_name: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct EntityState {
id: EntityId,
is_muted: bool,
channel_volumes: ChannelVolumes,
metadata: EntityMetadata,
}
impl EntityState {
pub fn id(&self) -> &EntityId {
&self.id
}
pub fn is_muted(&self) -> bool {
self.is_muted
}
pub fn channel_volumes(&self) -> Vec<f32> {
self.channel_volumes.get().iter().map(|v| v.0 as f32 / Volume::NORMAL.0 as f32).collect()
}
pub fn metadata(&self) -> &EntityMetadata {
&self.metadata
}
pub fn kind(&self) -> EntityKind {
match &self.metadata {
EntityMetadata::Source { .. } => EntityKind::Source,
EntityMetadata::Sink { .. } => EntityKind::Sink,
EntityMetadata::SinkInput { .. } => EntityKind::SinkInput,
}
}
}
impl From<&SourceInfo<'_>> for EntityState {
fn from(value: &SourceInfo) -> Self {
EntityState {
id: NonZeroU32::new(value.index).unwrap(),
is_muted: value.mute,
channel_volumes: value.volume,
metadata: EntityMetadata::Source {
name: value.name.clone().unwrap_or_default().into_owned(),
description: value.description.clone().unwrap_or_default().into_owned(),
},
}
}
}
impl From<&SinkInfo<'_>> for EntityState {
fn from(value: &SinkInfo) -> Self {
EntityState {
id: NonZeroU32::new(value.index).unwrap(),
is_muted: value.mute,
channel_volumes: value.volume,
metadata: EntityMetadata::Sink {
name: value.name.clone().unwrap_or_default().into_owned(),
description: value.description.clone().unwrap_or_default().into_owned(),
},
}
}
}
impl From<&SinkInputInfo<'_>> for EntityState {
fn from(value: &SinkInputInfo) -> Self {
EntityState {
id: NonZeroU32::new(value.index).unwrap(),
is_muted: value.mute,
channel_volumes: value.volume,
metadata: EntityMetadata::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()),
},
}
}
}
#[derive(Debug, Clone)]
pub struct State {
timestamp: Instant,
default_sink_id: Option<EntityId>,
default_source_id: Option<EntityId>,
entities_by_id: HashMap<EntityId, Arc<EntityState>>,
}
impl State {
pub fn timestamp(&self) -> &Instant {
&self.timestamp
}
pub fn default_sink_id(&self) -> &Option<EntityId> { &self.default_sink_id }
pub fn default_source_id(&self) -> &Option<EntityId> { &self.default_source_id }
pub fn entities_by_id(&self) -> &HashMap<EntityId, Arc<EntityState>> {
&self.entities_by_id
}
}
#[derive(Debug, Clone)]
pub struct PulseAudioVolumeInterface {
#[allow(unused)]
worker: Arc<PaWorker>,
current_state: Arc<ArcSwap<State>>,
state_tx: broadcast::Sender<Arc<State>>,
commands_tx: flume::Sender<ThreadMessage>,
}
impl PulseAudioVolumeInterface {
pub fn new(client_name: String) -> PulseAudioVolumeInterface {
let (commands_tx, commands_rx) = flume::unbounded();
let state_tx = broadcast::Sender::new(5);
let current_state = Arc::new(ArcSwap::new(Arc::new(State {
timestamp: Instant::now(),
default_sink_id: None,
default_source_id: None,
entities_by_id: HashMap::new(),
})));
PaThread::spawn(client_name, commands_tx.clone(), commands_rx, state_tx.clone(), Arc::clone(&current_state));
let worker = PaWorker {
commands_tx: commands_tx.clone(),
};
PulseAudioVolumeInterface {
worker: Arc::new(worker),
current_state,
commands_tx,
state_tx,
}
}
pub fn subscribe_to_state(&self) -> (Arc<State>, Receiver<Arc<State>>) {
let rx = self.state_tx.subscribe();
let state = self.current_state();
(state, rx)
}
pub fn current_state(&self) -> Arc<State> {
Arc::clone(&self.current_state.load())
}
pub fn set_is_muted(&self, id: EntityId, value: bool) {
self.commands_tx.send(ThreadMessage::SetIsMuted { id, value }).unwrap()
}
pub fn set_channel_volumes(&self, id: EntityId, channel_volumes: impl Into<Box<[f32]>>) {
self.commands_tx
.send(ThreadMessage::SetChannelVolumes {
id,
channel_volumes: channel_volumes.into(),
})
.unwrap()
}
pub fn set_default_entity(&self, id: EntityId) {
self.commands_tx.send(ThreadMessage::SetDefaultEntity { id }).unwrap()
}
}

370
src/worker.rs Normal file
View file

@ -0,0 +1,370 @@
use crate::{EntityId, EntityKind, EntityMetadata, EntityState, State};
use arc_swap::ArcSwap;
use libpulse_binding::callbacks::ListResult;
use libpulse_binding::context::introspect::{Introspector, ServerInfo};
use libpulse_binding::context::subscribe::{Facility, InterestMaskSet};
use libpulse_binding::context::{subscribe, Context, FlagSet, State as ConnectionState};
use libpulse_binding::def::Retval;
use libpulse_binding::mainloop::api::Mainloop as _;
use libpulse_binding::mainloop::threaded::Mainloop;
use libpulse_binding::volume::Volume;
use std::num::NonZeroU32;
use std::sync::Arc;
use std::thread;
use std::time::Instant;
use tokio::sync::broadcast;
#[derive(Debug)]
pub(super) enum ThreadMessage {
// emitted by the library user
SetIsMuted {
id: EntityId,
value: bool,
},
SetChannelVolumes {
id: EntityId,
channel_volumes: Box<[f32]>,
},
SetDefaultEntity {
id: EntityId,
},
Terminate,
// emitted by libpulse
LoadServerInfo,
LoadSinkInput {
entity_id: EntityId,
},
LoadSink {
entity_id: EntityId,
},
LoadSource {
entity_id: EntityId,
},
// emitted by handlers for the messages above
HandleServerInfo {
default_source_name: Option<Box<str>>,
default_sink_name: Option<Box<str>>,
},
UpsertEntity {
entity_state: Arc<EntityState>,
},
RemoveEntity {
entity_id: EntityId,
},
}
pub(super) struct PaThread {
mainloop: Mainloop,
context: Context,
introspector: Introspector,
commands_tx: flume::Sender<ThreadMessage>,
commands_rx: flume::Receiver<ThreadMessage>,
state_tx: broadcast::Sender<Arc<State>>,
current_state: Arc<ArcSwap<State>>,
}
impl PaThread {
pub(super) fn spawn(
client_name: String,
commands_tx: flume::Sender<ThreadMessage>,
commands_rx: flume::Receiver<ThreadMessage>,
state_tx: broadcast::Sender<Arc<State>>,
current_state: Arc<ArcSwap<State>>,
) {
thread::spawn(move || {
let mut mainloop = Mainloop::new().unwrap();
let context = Context::new(&mainloop, &client_name).unwrap();
let introspector = context.introspect();
log::debug!("Starting the mainloop thread…");
mainloop.start().expect("starting the mainloop never fails");
let mut t = PaThread {
mainloop,
context,
introspector,
commands_tx,
commands_rx,
state_tx,
current_state,
};
t.init();
t.run();
});
}
fn init(&mut self) {
log::debug!("Initializing…");
self.mainloop.lock();
self.context.connect(None, FlagSet::NOFLAGS, None).unwrap();
{
let (context_state_change_tx, context_state_change_rx) = flume::bounded(1);
self.context.set_state_callback(Some(Box::new(move || {
context_state_change_tx.send(()).unwrap();
})));
self.mainloop.unlock();
loop {
context_state_change_rx.recv().unwrap();
self.mainloop.lock();
if self.context.get_state() == ConnectionState::Ready {
break;
}
self.mainloop.unlock();
}
}
// Mainloop is still locked
{
let commands_tx = self.commands_tx.clone();
self.introspector.get_sink_input_info_list(move |list_result| match list_result {
ListResult::Error => panic!("Introspector.get_sink_input_info_list failed"),
ListResult::End => {}
ListResult::Item(sink_input) => commands_tx
.send(ThreadMessage::UpsertEntity {
entity_state: Arc::new(sink_input.into()),
})
.unwrap(),
});
}
{
let commands_tx = self.commands_tx.clone();
self.introspector.get_sink_info_list(move |list_result| match list_result {
ListResult::Error => panic!("Introspector.get_sink_info_list failed"),
ListResult::End => {}
ListResult::Item(sink) => commands_tx
.send(ThreadMessage::UpsertEntity {
entity_state: Arc::new(sink.into()),
})
.unwrap(),
});
}
{
let commands_tx = self.commands_tx.clone();
self.introspector.get_source_info_list(move |list_result| match list_result {
ListResult::Error => panic!("Introspector.get_source_info_list failed"),
ListResult::End => {}
ListResult::Item(source) => commands_tx
.send(ThreadMessage::UpsertEntity {
entity_state: Arc::new(source.into()),
})
.unwrap(),
});
}
{
let commands_tx = self.commands_tx.clone();
self.context.set_subscribe_callback(Some(Box::new(move |facility, operation, entity_id| {
let entity_id = NonZeroU32::new(entity_id).unwrap();
let facility = facility.unwrap();
if facility == Facility::Server {
commands_tx.send(ThreadMessage::LoadServerInfo).unwrap();
} else {
match operation.unwrap() {
subscribe::Operation::Removed => {
commands_tx.send(ThreadMessage::RemoveEntity { entity_id }).unwrap();
}
subscribe::Operation::New | subscribe::Operation::Changed => {
match facility {
Facility::SinkInput => commands_tx.send(ThreadMessage::LoadSinkInput { entity_id }).unwrap(),
Facility::Sink => commands_tx.send(ThreadMessage::LoadSink { entity_id }).unwrap(),
Facility::Source => commands_tx.send(ThreadMessage::LoadSource { entity_id }).unwrap(),
_ => {}
};
}
};
}
})));
}
self.context
.subscribe(InterestMaskSet::SERVER | InterestMaskSet::SINK | InterestMaskSet::SOURCE | InterestMaskSet::SINK_INPUT, |success| {
if !success {
panic!("Context.subscribe failed")
}
});
self.mainloop.unlock();
}
fn run(mut self) {
log::debug!("Waiting for commands…");
'outer: loop {
while let Ok(command) = self.commands_rx.recv() {
self.mainloop.lock();
let commands_tx = self.commands_tx.clone();
match command {
ThreadMessage::Terminate => {
break 'outer;
}
ThreadMessage::SetIsMuted { id, value } => {
if let Some(state) = self.current_state.load().entities_by_id.get(&id) {
match state.kind() {
EntityKind::Sink => self.introspector.set_sink_mute_by_index(id.into(), value, None),
EntityKind::Source => self.introspector.set_source_mute_by_index(id.into(), value, None),
EntityKind::SinkInput => self.introspector.set_sink_input_mute(id.into(), value, None),
};
}
}
ThreadMessage::SetChannelVolumes { id, channel_volumes } => {
if let Some(state) = self.current_state.load().entities_by_id.get(&id) {
let mut value = state.channel_volumes;
for (i, v) in channel_volumes.iter().enumerate() {
value.set(i as u8, Volume((Volume::NORMAL.0 as f32 * v).floor() as u32));
}
match state.kind() {
EntityKind::Sink => self.introspector.set_sink_volume_by_index(id.into(), &value, None),
EntityKind::Source => self.introspector.set_source_volume_by_index(id.into(), &value, None),
EntityKind::SinkInput => self.introspector.set_sink_input_volume(id.into(), &value, None),
};
}
}
ThreadMessage::SetDefaultEntity { id } => {
if let Some(state) = self.current_state.load().entities_by_id.get(&id) {
match &state.metadata {
EntityMetadata::Sink { name, .. } => {
self.context.set_default_sink(name, |_| {});
}
EntityMetadata::Source { name, .. } => {
self.context.set_default_source(name, |_| {});
}
_ => {
log::error!("Setting default entity failed. Only sinks and sources can be defaults.");
}
};
} else {
log::error!("Setting default entity failed. Unknown entity id: {id}");
}
}
ThreadMessage::LoadServerInfo => {
self.introspector.get_server_info(move |server_info| {
commands_tx.send(ThreadMessage::HandleServerInfo {
default_sink_name: server_info.default_sink_name.as_ref().map(|n| n.to_string().into_boxed_str()),
default_source_name: server_info.default_source_name.as_ref().map(|n| n.to_string().into_boxed_str()),
}).unwrap();
});
}
ThreadMessage::LoadSinkInput { entity_id } => {
self.introspector.get_sink_input_info(entity_id.into(), move |list_result| match list_result {
ListResult::Error => panic!("Introspector.get_sink_input_info failed"),
ListResult::End => {}
ListResult::Item(sink_input) => commands_tx
.send(ThreadMessage::UpsertEntity {
entity_state: Arc::new(sink_input.into()),
})
.unwrap(),
});
}
ThreadMessage::LoadSink { entity_id } => {
self.introspector
.get_sink_info_by_index(entity_id.into(), move |list_result| match list_result {
ListResult::Error => panic!("Introspector.get_sink_info_by_index failed"),
ListResult::End => {}
ListResult::Item(sink) => commands_tx
.send(ThreadMessage::UpsertEntity {
entity_state: Arc::new(sink.into()),
})
.unwrap(),
});
}
ThreadMessage::LoadSource { entity_id } => {
self.introspector
.get_source_info_by_index(entity_id.into(), move |list_result| match list_result {
ListResult::Error => panic!("Introspector.get_source_info_by_index failed"),
ListResult::End => {}
ListResult::Item(source) => commands_tx
.send(ThreadMessage::UpsertEntity {
entity_state: Arc::new(source.into()),
})
.unwrap(),
});
}
ThreadMessage::HandleServerInfo { default_sink_name, default_source_name} => {
let current_state = self.current_state.load();
self.set_state(Arc::new(State {
timestamp: Instant::now(),
default_source_id: default_source_name
.map(|search_name| {
current_state
.entities_by_id
.iter()
.find(|(_, e)| match &e.metadata {
EntityMetadata::Source { name, .. } => *name == *search_name,
_ => false,
})
.map(|(id, _)| (*id).into())
})
.flatten(),
default_sink_id: default_sink_name
.map(|search_name| {
current_state
.entities_by_id
.iter()
.find(|(_, e)| match &e.metadata {
EntityMetadata::Sink { name, .. } => *name == *search_name,
_ => false,
})
.map(|(id, _)| (*id).into())
})
.flatten(),
entities_by_id: current_state.entities_by_id.clone(),
}));
}
ThreadMessage::UpsertEntity { entity_state } => {
let current_state = self.current_state.load();
self.set_state(Arc::new(State {
timestamp: Instant::now(),
entities_by_id: current_state.entities_by_id.update(entity_state.id, entity_state),
..**current_state
}));
}
ThreadMessage::RemoveEntity { entity_id } => {
let current_state = self.current_state.load();
self.set_state(Arc::new(State {
timestamp: Instant::now(),
entities_by_id: current_state.entities_by_id.without(&entity_id),
..**current_state
}));
}
}
self.mainloop.unlock();
}
}
self.mainloop.quit(Retval(0));
self.mainloop.unlock();
}
fn set_state(&self, value: Arc<State>) {
self.current_state.store(Arc::clone(&value));
// If there are no subscribers, thats ok.
_ = self.state_tx.send(value);
}
}
#[derive(Debug)]
pub(super) struct PaWorker {
pub(super) commands_tx: flume::Sender<ThreadMessage>,
}
impl Drop for PaWorker {
fn drop(&mut self) {
self.commands_tx.send(ThreadMessage::Terminate).ok();
}
}