254 lines
7.7 KiB
Rust
254 lines
7.7 KiB
Rust
extern crate libpulse_binding as pulse;
|
|
|
|
use crate::config::PulseAction;
|
|
use crate::interaction_server::InteractionServer;
|
|
use crossbeam_channel::Receiver;
|
|
use log::error;
|
|
use pulse::context::{Context, FlagSet as ContextFlagSet};
|
|
use pulse::mainloop::threaded::Mainloop;
|
|
use pulse::proplist::Proplist;
|
|
use pulse::volume;
|
|
use regex::Regex;
|
|
use std::cell::RefCell;
|
|
use std::ops::Deref;
|
|
use std::sync::Arc;
|
|
use std::thread::{self, JoinHandle};
|
|
|
|
const APPLICATION_NAME: &str = "picoKontroller";
|
|
const CONTEXT_NAME: &str = "picoKontrollerContext";
|
|
|
|
pub type PulseMessage = (Arc<Vec<PulseId>>, PulseAction, PulseState);
|
|
pub type PulseState = u8;
|
|
pub type PulseId = String;
|
|
|
|
pub struct PulseController {
|
|
pub in_channel: Receiver<PulseMessage>,
|
|
}
|
|
|
|
impl InteractionServer for PulseController {
|
|
fn start(self) -> JoinHandle<()> {
|
|
thread::spawn(move || {
|
|
self.exec();
|
|
})
|
|
}
|
|
}
|
|
|
|
impl PulseController {
|
|
pub fn new(in_channel: Receiver<PulseMessage>) -> PulseController {
|
|
PulseController { in_channel }
|
|
}
|
|
|
|
fn exec(&self) {
|
|
let mut proplist = Proplist::new().unwrap();
|
|
proplist
|
|
.set_str(
|
|
pulse::proplist::properties::APPLICATION_NAME,
|
|
APPLICATION_NAME,
|
|
)
|
|
.unwrap();
|
|
|
|
let mainloop = Arc::new(RefCell::new(
|
|
Mainloop::new().expect("Failed to create mainloop"),
|
|
));
|
|
|
|
let context = Arc::new(RefCell::new(
|
|
Context::new_with_proplist(mainloop.borrow().deref(), CONTEXT_NAME, &proplist)
|
|
.expect("Failed to create new context"),
|
|
));
|
|
|
|
// Context state change callback
|
|
{
|
|
let ml_ref = Arc::clone(&mainloop);
|
|
let context_ref = Arc::clone(&context);
|
|
context
|
|
.borrow_mut()
|
|
.set_state_callback(Some(Box::new(move || {
|
|
let state = unsafe { (*context_ref.as_ptr()).get_state() };
|
|
match state {
|
|
pulse::context::State::Ready
|
|
| pulse::context::State::Failed
|
|
| pulse::context::State::Terminated => unsafe {
|
|
(*ml_ref.as_ptr()).signal(false);
|
|
},
|
|
_ => {}
|
|
}
|
|
})));
|
|
}
|
|
|
|
context
|
|
.borrow_mut()
|
|
.connect(None, ContextFlagSet::NOFLAGS, None)
|
|
.expect("Failed to connect context");
|
|
|
|
mainloop.borrow_mut().lock();
|
|
mainloop
|
|
.borrow_mut()
|
|
.start()
|
|
.expect("Failed to start mainloop");
|
|
|
|
// Wait for context to be ready
|
|
loop {
|
|
match context.borrow().get_state() {
|
|
pulse::context::State::Ready => {
|
|
break;
|
|
}
|
|
pulse::context::State::Failed | pulse::context::State::Terminated => {
|
|
eprintln!("Context state failed/terminated, quitting...");
|
|
mainloop.borrow_mut().unlock();
|
|
mainloop.borrow_mut().stop();
|
|
return;
|
|
}
|
|
_ => {
|
|
mainloop.borrow_mut().wait();
|
|
}
|
|
}
|
|
}
|
|
context.borrow_mut().set_state_callback(None);
|
|
mainloop.borrow_mut().unlock();
|
|
|
|
loop {
|
|
let ml = Arc::clone(&mainloop);
|
|
let ctx = Arc::clone(&context);
|
|
let event_in = self.in_channel.recv();
|
|
match event_in {
|
|
Ok((ids, action, state)) => {
|
|
parse_pulse_action(state, ids, &action, ml, ctx);
|
|
}
|
|
Err(err) => {
|
|
error!("Failed receiving event in controller thread: {err}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_pulse_action(
|
|
state: PulseState,
|
|
ids: Arc<Vec<PulseId>>,
|
|
action: &PulseAction,
|
|
mainloop: Arc<RefCell<Mainloop>>,
|
|
context: Arc<RefCell<Context>>,
|
|
) {
|
|
match action {
|
|
PulseAction::SinkInputVolume => {
|
|
for id in ids.iter() {
|
|
set_volume_sink_input(mainloop.clone(), context.clone(), id, state);
|
|
}
|
|
}
|
|
PulseAction::DefaultSink => {
|
|
set_default_sink(mainloop.clone(), context.clone(), ids.get(0).unwrap());
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn set_default_sink(mainloop: Arc<RefCell<Mainloop>>, context: Arc<RefCell<Context>>, name: &str) {
|
|
mainloop.borrow_mut().lock();
|
|
let sink_filter = Regex::new(name).expect("Creating RegEx failed");
|
|
let callback_context = context.clone();
|
|
context
|
|
.borrow_mut()
|
|
.introspect()
|
|
.get_sink_info_list(move |items_result| match items_result {
|
|
pulse::callbacks::ListResult::Item(item) => {
|
|
let matches_regex = sink_filter.is_match(item.name.as_ref().unwrap());
|
|
if matches_regex {
|
|
let name = item.name.as_ref().unwrap();
|
|
println!("Setting default sink to: {}", name);
|
|
callback_context.borrow_mut().set_default_sink(name, |_| {});
|
|
}
|
|
}
|
|
_ => {}
|
|
});
|
|
mainloop.borrow_mut().unlock();
|
|
}
|
|
|
|
fn for_all_sink_inputs(
|
|
mainloop: Arc<RefCell<Mainloop>>,
|
|
context: Arc<RefCell<Context>>,
|
|
name: &str,
|
|
function: Arc<dyn Fn(Arc<RefCell<Context>>, u32, &mut volume::ChannelVolumes)>,
|
|
) {
|
|
mainloop.borrow_mut().lock();
|
|
let sink_filter = Regex::new(name).expect("Creating RegEx failed");
|
|
let callback_context = context.clone();
|
|
context
|
|
.borrow_mut()
|
|
.introspect()
|
|
.get_sink_input_info_list(move |items_result| match items_result {
|
|
pulse::callbacks::ListResult::Item(item) => {
|
|
let application_name = item
|
|
.proplist
|
|
.get_str(pulse::proplist::properties::APPLICATION_NAME)
|
|
.unwrap();
|
|
let matches_regex = sink_filter.is_match(&application_name);
|
|
|
|
if matches_regex {
|
|
function(
|
|
callback_context.clone(),
|
|
item.index,
|
|
&mut item.volume.clone(),
|
|
);
|
|
}
|
|
}
|
|
_ => {}
|
|
});
|
|
mainloop.borrow_mut().unlock();
|
|
}
|
|
|
|
fn mute_sink_input(
|
|
mainloop: Arc<RefCell<Mainloop>>,
|
|
context: Arc<RefCell<Context>>,
|
|
name: &str,
|
|
state: bool,
|
|
) {
|
|
for_all_sink_inputs(
|
|
mainloop,
|
|
context,
|
|
name,
|
|
Arc::new(move |ctx, index, _| {
|
|
ctx.borrow_mut()
|
|
.introspect()
|
|
.set_sink_input_mute(index, state, Option::None);
|
|
}),
|
|
)
|
|
}
|
|
|
|
fn set_volume_sink_input(
|
|
mainloop: Arc<RefCell<Mainloop>>,
|
|
context: Arc<RefCell<Context>>,
|
|
name: &str,
|
|
volume: u8,
|
|
) {
|
|
for_all_sink_inputs(
|
|
mainloop,
|
|
context,
|
|
name,
|
|
Arc::new(move |ctx, index, vols| {
|
|
let target = percent_to_volume(volume);
|
|
|
|
for v in vols.get_mut() {
|
|
v.0 = target;
|
|
}
|
|
|
|
ctx.borrow_mut()
|
|
.introspect()
|
|
.set_sink_input_volume(index, vols, Option::None);
|
|
}),
|
|
)
|
|
}
|
|
|
|
//taken from https://github.com/jantap/rsmixer
|
|
fn percent_to_volume(target_percent: u8) -> u32 {
|
|
let base_delta = (volume::Volume::NORMAL.0 as f32 - volume::Volume::MUTED.0 as f32) / 100.0;
|
|
|
|
if target_percent == 100 {
|
|
volume::Volume::NORMAL.0
|
|
} else if target_percent >= 150 {
|
|
(volume::Volume::NORMAL.0 as f32 * 1.5) as u32
|
|
} else if target_percent < 100 {
|
|
volume::Volume::MUTED.0 + target_percent as u32 * base_delta as u32
|
|
} else {
|
|
volume::Volume::NORMAL.0 + (target_percent - 100) as u32 * base_delta as u32
|
|
}
|
|
}
|