Add common server interface
This commit is contained in:
parent
6617652839
commit
99d764eeaf
5 changed files with 177 additions and 129 deletions
|
@ -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<KeyEvent>, config: Arc<Config>) {
|
||||
let (sender, receiver) = unbounded::<PulseMessage>();
|
||||
pulse_client::run(receiver);
|
||||
let pulse = PulseController::new(receiver);
|
||||
pulse.start();
|
||||
thread::spawn(move || {
|
||||
exec(in_channel, sender, config);
|
||||
});
|
||||
|
|
5
src/interaction_server.rs
Normal file
5
src/interaction_server.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
use std::thread::JoinHandle;
|
||||
|
||||
pub trait InteractionServer {
|
||||
fn start(self) -> JoinHandle<()>;
|
||||
}
|
13
src/main.rs
13
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::<midi_client::KeyEvent>();
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -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<KeyEvent>,
|
||||
}
|
||||
|
||||
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<KeyEvent>) -> 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<KeyEvent>) -> Result<(), Box<dyn Error>> {
|
||||
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);
|
||||
|
|
|
@ -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<Vec<String>>, PulseAction, u8);
|
||||
|
||||
pub fn run(in_channel: Receiver<PulseMessage>) {
|
||||
thread::spawn(move || {
|
||||
exec(in_channel);
|
||||
});
|
||||
pub struct PulseController {
|
||||
pub in_channel: Receiver<PulseMessage>,
|
||||
}
|
||||
pub fn exec(in_channel: Receiver<PulseMessage>) {
|
||||
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<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 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<RefCell<Mainloop>>,
|
||||
context: Arc<RefCell<Context>>,
|
||||
name: &str,
|
||||
) {
|
||||
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();
|
||||
|
@ -185,7 +194,7 @@ fn for_all_sink_inputs(
|
|||
mainloop.borrow_mut().unlock();
|
||||
}
|
||||
|
||||
pub fn mute_sink_input(
|
||||
fn mute_sink_input(
|
||||
mainloop: Arc<RefCell<Mainloop>>,
|
||||
context: Arc<RefCell<Context>>,
|
||||
name: &str,
|
||||
|
@ -203,7 +212,7 @@ pub fn mute_sink_input(
|
|||
)
|
||||
}
|
||||
|
||||
pub fn set_volume_sink_input(
|
||||
fn set_volume_sink_input(
|
||||
mainloop: Arc<RefCell<Mainloop>>,
|
||||
context: Arc<RefCell<Context>>,
|
||||
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 {
|
Loading…
Reference in a new issue