diff --git a/src/controller.rs b/src/controller.rs index 694359e..e72bbef 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -1,8 +1,9 @@ use crate::config::NanoKeys; -use crate::config::{Config, KeyMapVariant, MprisAction, PulseAction}; +use crate::config::{Config, KeyMapVariant, MprisAction}; +use crate::interaction_server::InteractionServer; use crate::midi_client::KeyEvent; use crate::mpris_client; -use crate::pulse_client::{self, PulseMessage}; +use crate::pulse_controller::{PulseController, PulseMessage}; use crossbeam_channel::{unbounded, Receiver, Sender}; use log::{error, info, warn}; use std::process::{Command, Output}; @@ -11,7 +12,8 @@ use std::thread; pub fn run(in_channel: Receiver, config: Arc) { let (sender, receiver) = unbounded::(); - pulse_client::run(receiver); + let pulse = PulseController::new(receiver); + pulse.start(); thread::spawn(move || { exec(in_channel, sender, config); }); diff --git a/src/interaction_server.rs b/src/interaction_server.rs new file mode 100644 index 0000000..7a5e139 --- /dev/null +++ b/src/interaction_server.rs @@ -0,0 +1,5 @@ +use std::thread::JoinHandle; + +pub trait InteractionServer { + fn start(self) -> JoinHandle<()>; +} diff --git a/src/main.rs b/src/main.rs index 81d3ac5..1660933 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,14 @@ mod config; mod controller; +mod interaction_server; mod midi_client; mod mpris_client; -mod pulse_client; +mod pulse_controller; + use crossbeam_channel::unbounded; use directories::ProjectDirs; +use interaction_server::InteractionServer; +use midi_client::MidiClient; use std::{path::PathBuf, sync::Arc}; fn main() { @@ -12,7 +16,6 @@ fn main() { let proj_dirs = ProjectDirs::from("com", "ghoscht", "picoKontroller"); let path: PathBuf = proj_dirs.unwrap().config_dir().join("config.toml"); - // let path: &str = "./config.toml"; let cfg: config::Config = config::init(path); let arc_cfg = Arc::new(cfg); @@ -21,6 +24,10 @@ fn main() { mpris_client::list_clients(); let (sender, receiver) = unbounded::(); + let midi = MidiClient::new(arc_cfg.general.midi_device.clone(), sender); + controller::run(receiver, Arc::clone(&arc_cfg)); - midi_client::run(&arc_cfg.general.midi_device, sender).unwrap(); + + let server = midi.start(); + let _ = server.join(); } diff --git a/src/midi_client.rs b/src/midi_client.rs index 3a0525d..78daf06 100644 --- a/src/midi_client.rs +++ b/src/midi_client.rs @@ -1,47 +1,72 @@ -use crate::config::NanoKeys; +use crate::{config::NanoKeys, interaction_server::InteractionServer}; use crossbeam_channel::Sender; use midir::{Ignore, MidiInput}; use regex::Regex; -use std::error::Error; +use std::thread::{self, JoinHandle}; use std::time; +const CLIENT_NAME: &str = "PicoKontroller"; +const PORT_NAME: &str = "PicoController Input"; + +pub struct MidiClient { + pub device_name: String, + pub out_channel: Sender, +} + +pub type KeyEvent = (NanoKeys, u8); + +impl InteractionServer for MidiClient { + fn start(self) -> JoinHandle<()> { + thread::spawn(move || { + self.exec(); + }) + } +} + +impl MidiClient { + pub fn new(device_name: String, out_channel: Sender) -> MidiClient { + MidiClient { + device_name, + out_channel, + } + } + + fn exec(self) { + let port_filter = Regex::new(&self.device_name).expect("Creating RegEx failed"); + + let mut midi_in = MidiInput::new(CLIENT_NAME).expect("Creating Midi device failed"); + midi_in.ignore(Ignore::None); + + let in_ports = midi_in.ports(); + let in_port = in_ports + .iter() + .find(|p| port_filter.is_match(&midi_in.port_name(p).unwrap())) + .expect("Couldn't find a port matching the supplied RegEx"); + + // _conn_in needs to be a named parameter, because it needs to be kept alive until the end of the scope + let _conn_in = midi_in.connect( + in_port, + PORT_NAME, + move |_, message, _| { + let key = NanoKeys::try_from(message[1]).unwrap(); + let state = message[2]; + let _ = self.out_channel.send((key, state)); + }, + (), + ); + loop { + // sleep forever, callback functions happen on separate thread + // couldn't reuse this thread, since _conn_in needs to be alive in this scope + std::thread::sleep(time::Duration::from_secs(u64::MAX)); + } + } +} + pub fn list_ports() -> () { - let midi_in = MidiInput::new("PicoKontroller").expect("Creating Midi device failed"); + let midi_in = MidiInput::new(CLIENT_NAME).expect("Creating Midi device failed"); let in_ports = midi_in.ports(); println!("All available midi devices:"); for (_, p) in in_ports.iter().enumerate() { println!("{}", midi_in.port_name(p).unwrap()); } } - -pub fn run(port_name: &str, out_channel: Sender) -> Result<(), Box> { - let port_filter = Regex::new(port_name).expect("Creating RegEx failed"); - - let mut midi_in = MidiInput::new("PicoKontroller").expect("Creating Midi device failed"); - midi_in.ignore(Ignore::None); - - let in_ports = midi_in.ports(); - let in_port = in_ports - .iter() - .find(|p| port_filter.is_match(&midi_in.port_name(p).unwrap())) - .expect("Couldn't find a port matching the supplied RegEx"); - - // _conn_in needs to be a named parameter, because it needs to be kept alive until the end of the scope - let _conn_in = midi_in.connect( - in_port, - "PicoController Input", - move |_, message, _| { - let key = NanoKeys::try_from(message[1]).unwrap(); - let state = message[2]; - let _ = out_channel.send((key, state)); - }, - (), - )?; - loop { - // sleep forever, callback functions happen on separate thread - // couldn't reuse this thread, since _conn_in needs to be alive in this scope - std::thread::sleep(time::Duration::from_secs(u64::MAX)); - } -} - -pub type KeyEvent = (NanoKeys, u8); diff --git a/src/pulse_client.rs b/src/pulse_controller.rs similarity index 55% rename from src/pulse_client.rs rename to src/pulse_controller.rs index b08ea6f..102e50c 100644 --- a/src/pulse_client.rs +++ b/src/pulse_controller.rs @@ -1,8 +1,9 @@ extern crate libpulse_binding as pulse; -use crate::config::{Config, KeyMapVariant, PulseAction}; +use crate::config::PulseAction; +use crate::interaction_server::InteractionServer; use crossbeam_channel::Receiver; -use log::{error, info, warn}; +use log::error; use pulse::context::{Context, FlagSet as ContextFlagSet}; use pulse::mainloop::threaded::Mainloop; use pulse::proplist::Proplist; @@ -11,97 +12,109 @@ use regex::Regex; use std::cell::RefCell; use std::ops::Deref; use std::sync::Arc; -use std::thread; +use std::thread::{self, JoinHandle}; + +const APPLICATION_NAME: &str = "picoKontroller"; +const CONTEXT_NAME: &str = "picoKontrollerContext"; pub type PulseMessage = (Arc>, PulseAction, u8); -pub fn run(in_channel: Receiver) { - thread::spawn(move || { - exec(in_channel); - }); +pub struct PulseController { + pub in_channel: Receiver, } -pub fn exec(in_channel: Receiver) { - let mut proplist = Proplist::new().unwrap(); - proplist - .set_str( - pulse::proplist::properties::APPLICATION_NAME, - "picoKontroller", - ) - .unwrap(); - let mainloop = Arc::new(RefCell::new( - Mainloop::new().expect("Failed to create mainloop"), - )); +impl InteractionServer for PulseController { + fn start(self) -> JoinHandle<()> { + thread::spawn(move || { + self.exec(); + }) + } +} - let context = Arc::new(RefCell::new( - Context::new_with_proplist( - mainloop.borrow().deref(), - "picoKontrollerContext", - &proplist, - ) - .expect("Failed to create new context"), - )); +impl PulseController { + pub fn new(in_channel: Receiver) -> 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 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); - }, - _ => {} + .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() - .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(); + 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 = 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}"); + 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}"); + } } } } @@ -127,11 +140,7 @@ fn parse_pulse_action( } } -pub fn set_default_sink( - mainloop: Arc>, - context: Arc>, - name: &str, -) { +fn set_default_sink(mainloop: Arc>, context: Arc>, name: &str) { mainloop.borrow_mut().lock(); let sink_filter = Regex::new(name).expect("Creating RegEx failed"); let callback_context = context.clone(); @@ -185,7 +194,7 @@ fn for_all_sink_inputs( mainloop.borrow_mut().unlock(); } -pub fn mute_sink_input( +fn mute_sink_input( mainloop: Arc>, context: Arc>, name: &str, @@ -203,7 +212,7 @@ pub fn mute_sink_input( ) } -pub fn set_volume_sink_input( +fn set_volume_sink_input( mainloop: Arc>, context: Arc>, name: &str, @@ -228,7 +237,7 @@ pub fn set_volume_sink_input( } //taken from https://github.com/jantap/rsmixer -pub fn percent_to_volume(target_percent: u8) -> u32 { +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 {