From e61163da884437eb6ab935ad2ccd3b5ce9ce81af Mon Sep 17 00:00:00 2001 From: zawz Date: Fri, 8 Sep 2023 15:31:16 +0200 Subject: [PATCH] refactor: rework internal structure and add minor features ~ rework midi backend structure + add documentation + implement connect by address + add log_devices and log_events options --- FORMAT.md | 112 +++++++++++++ README.md | 40 +++++ {test => examples}/vmpk.yml | 4 + src/config/device.rs | 2 + src/config/mod.rs | 10 +- src/config/serializer/device.rs | 1 + src/config/serializer/mod.rs | 10 +- src/constant.rs | 4 + src/error.rs | 5 +- src/event.rs | 24 ++- src/main.rs | 14 +- src/midi/{ => backend}/alsa.rs | 154 +++++++++++------- src/midi/backend/mod.rs | 3 + src/midi/builder.rs | 27 ++++ src/midi/driver.rs | 14 ++ src/midi/input.rs | 150 +++++++++++++++++ src/midi/mod.rs | 274 +++----------------------------- src/midi/port.rs | 21 +++ src/midi/portfilter.rs | 28 ++++ src/run.rs | 53 ++++-- src/util/mod.rs | 7 + 21 files changed, 616 insertions(+), 341 deletions(-) create mode 100644 FORMAT.md create mode 100644 README.md rename {test => examples}/vmpk.yml (95%) create mode 100644 src/constant.rs rename src/midi/{ => backend}/alsa.rs (77%) create mode 100644 src/midi/backend/mod.rs create mode 100644 src/midi/builder.rs create mode 100644 src/midi/driver.rs create mode 100644 src/midi/input.rs create mode 100644 src/midi/port.rs create mode 100644 src/midi/portfilter.rs diff --git a/FORMAT.md b/FORMAT.md new file mode 100644 index 0000000..840cabe --- /dev/null +++ b/FORMAT.md @@ -0,0 +1,112 @@ +# Format + +yaml configuration format + +```yaml +# Log all device connections +[ log_devices: | default = false ] + +# Midi backend to use. Currently only alsa +[ driver: alsa ] + +# Device definitions +devices: + [ - ... ] +``` + +### `` + +Definition of one device with its config and corresponding events. + +```yaml +# Find device by name with literal string +[ name: ] + +# Find device by name with regex +[ regex: ] + +# Find device by exact address +[ addr: ] + +# Max number of devices to connect for this device definition. +[ max_connections: | default = inf ] + +# Max length of event queue for processing. +[ queue_length: | default = 256 ] + +# Time interval between executions. +# Actual interval can be longer if execution is longer than this value. +# Supports time extensions, example: 1s, 100ms... +[ interval: | default = 0 ] + +# Log all midi events of device +[ log_events: | default = false ] + +# Commands to run on device connect +connect: + [ - ... ] + +# Commands to run on device disconnect +disconnect: + [ - ... ] + +# Definitions of executions on MIDI events +events: + [ - ... ] +``` + +### `` + +Definition of one MIDI event condition and its corresponding executions. +```yaml +# Max number of devices to connect for this device definition. +[ max_connections: | default = inf ] + +# Max length of event queue for processing. +[ queue_length: | default = 256 ] + +# Time interval between executions. +# Actual interval can be longer if execution is longer than this value. +# Supports time extensions, example: 1s, 100ms... +[ interval: | default = 0ms ] + +# Commands to run on device connect +connect: + [ - ... ] + +# Commands to run on device disconnect +disconnect: + [ - ... ] + +# Definitions of executions on MIDI events +events: + [ - ... ] +``` + +### `` + +Definition of one MIDI event condition and its corresponding executions. +```yaml +# Max number of devices to connect for this device definition. +[ max_connections: | default = inf ] + +# Max length of event queue for processing. +[ queue_length: | default = 256 ] + +# Time interval between executions. +# Actual interval can be longer if execution is longer than this value. +# Supports time extensions, example: 1s, 100ms... +[ interval: | default = 0ms ] + +# Commands to run on device connect +connect: + [ - ... ] + +# Commands to run on device disconnect +disconnect: + [ - ... ] + +# Definitions of executions on MIDI events +events: + [ - ... ] +``` \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f76e02 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# rmidimap + +Map MIDI signals to command with a simple yaml file. + +See [format]() and [examples](/examples/). + +# Usage + +Simply execute `rmidimap ` to start with the desired map file. + +# Features + +### MIDI backends + +Only Linux+ALSA currently. + +### Performance + +rmidimap runs with very low processing overhead. +Processing overhead has been measured at 100-200µs, while execution spawning was measured to 1-4ms. + +### Device connection + +Connect to devices by name or regex, and run commands on connect or disconnect. + +### Command queue and interval + +With the parameters `queue_length` and `interval`, +you can limit event throughput to reduce system load associated with the command being run. + +# Building from source + +You need rustc and cargo to build the project. + +Steps: +- Clone this repository +- `cargo build -r` +- `sudo mv target/release/rmidimap /usr/local/bin/rmidimap` + + diff --git a/test/vmpk.yml b/examples/vmpk.yml similarity index 95% rename from test/vmpk.yml rename to examples/vmpk.yml index a9546b0..2276215 100644 --- a/test/vmpk.yml +++ b/examples/vmpk.yml @@ -1,5 +1,9 @@ +log_devices: true devices: + - name: 'System' + log_events: true - name: 'VMPK' + log_events: true max_connections: 1 queue_length: 3 interval: 100ms diff --git a/src/config/device.rs b/src/config/device.rs index 965dc5e..1cc9022 100644 --- a/src/config/device.rs +++ b/src/config/device.rs @@ -23,6 +23,7 @@ pub struct DeviceConfig { pub events: Option>, pub queue_length: usize, pub interval: Duration, + pub log: bool, } impl DeviceConfig { @@ -71,6 +72,7 @@ impl TryFrom for DeviceConfig { events: util::map_opt_tryfrom(v.events)?, queue_length: v.queue_length.unwrap_or(256), interval: v.interval.map(|x| x.unwrap()).unwrap_or_else(|| Duration::new(0, 0)), + log: v.log_events.unwrap_or(false), }) } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 0fa8582..9c38975 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -9,13 +9,15 @@ use std::str::FromStr; use crate::util; -pub type DeviceConfig = device::DeviceConfig; -pub type EventConfig = event::EventConfig; -pub type RunConfig = run::RunConfig; +pub use device::DeviceConfig; +pub use event::EventConfig; +pub use run::RunConfig; pub type EventEnvMap = serializer::EventEnvSerializer; #[derive(Clone,Debug)] pub struct Config { + pub log: bool, + pub driver: Option, pub devices: Vec, } @@ -23,6 +25,8 @@ impl TryFrom for Config { type Error = crate::Error; fn try_from(v: ConfigSerializer) -> Result { Ok(Config { + log: v.log_devices.unwrap_or(false), + driver: v.driver, devices: util::map_tryfrom(v.devices)?, }) } diff --git a/src/config/serializer/device.rs b/src/config/serializer/device.rs index c4de8c4..0622617 100644 --- a/src/config/serializer/device.rs +++ b/src/config/serializer/device.rs @@ -33,4 +33,5 @@ pub struct DeviceConfigSerializer { pub max_connections: Option, pub queue_length: Option, pub interval: Option, + pub log_events: Option, } diff --git a/src/config/serializer/mod.rs b/src/config/serializer/mod.rs index a7d62f2..7b97a25 100644 --- a/src/config/serializer/mod.rs +++ b/src/config/serializer/mod.rs @@ -3,15 +3,17 @@ pub mod device; pub mod run; pub mod eventenv; -pub type DeviceConfigSerializer = device::DeviceConfigSerializer; -pub type EventConfigSerializer = event::EventConfigSerializer; -pub type RunConfigSerializer = run::RunConfigSerializer; -pub type EventEnvSerializer = eventenv::EventEnvSerializer; +pub use device::DeviceConfigSerializer; +pub use event::EventConfigSerializer; +pub use run::RunConfigSerializer; +pub use eventenv::EventEnvSerializer; use serde::Deserialize; #[derive(Deserialize,Clone,Debug)] #[serde(deny_unknown_fields)] pub struct ConfigSerializer { + pub log_devices: Option, + pub driver: Option, pub devices: Vec, } diff --git a/src/constant.rs b/src/constant.rs new file mode 100644 index 0000000..7a6f8a3 --- /dev/null +++ b/src/constant.rs @@ -0,0 +1,4 @@ + +pub const CLIENT_NAME: &str = "rmidimap"; +pub const CLIENT_NAME_HANDLER: &str = "rmidimap-handler"; +pub const CLIENT_NAME_EVENT: &str = "rmidimap-event-watcher"; \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 861a4d1..c9706e7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,9 +1,10 @@ use std::ffi::NulError; +use std::num::ParseIntError; use std::process::ExitStatus; use std::sync::mpsc::RecvError; use std::time::SystemTimeError; -use crate::midi::alsa::AlsaError; +use crate::midi::backend::alsa::AlsaError; use thiserror::Error; @@ -25,6 +26,8 @@ pub enum Error { Regex(#[from] regex::Error), #[error(transparent)] SystemTime(#[from] SystemTimeError), + #[error(transparent)] + ParseInt(#[from] ParseIntError), #[error("execution failure")] ExecStatus(ExitStatus), #[error("remap value is too large. Maximum value is {}", i64::MAX)] diff --git a/src/event.rs b/src/event.rs index c51229b..f745f5c 100644 --- a/src/event.rs +++ b/src/event.rs @@ -7,7 +7,6 @@ use crate::Error; use serde::{Serialize,Deserialize}; - use lazy_static::lazy_static; lazy_static! { @@ -116,6 +115,20 @@ struct EventEnvRef<'a> { pub value: &'a str, } + +impl<'a> std::fmt::Display for Event<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{{ \"type\": \"{}\", \"channel\": {}, \"id\": {}, \"value\": {}, \"raw\": \"{}\" }}", + self.r#type, self.channel, self.id, self.value, bytes_to_strhex(self.raw, " ")) + } +} + +impl std::fmt::Display for EventBuf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.as_event().fmt(f) + } +} + impl EventBuf { pub fn as_event(&self) -> Event { Event { @@ -155,10 +168,13 @@ impl From for EventType { } } -fn bytes_to_strhex(bytes: &[u8]) -> String { +fn bytes_to_strhex(bytes: &[u8], separator: &str) -> String { let mut s = String::new(); for &byte in bytes { - write!(&mut s, "{:X} ", byte).expect("unexpected write error"); + write!(&mut s, "{:02X}{}", byte, separator).expect("unexpected write error"); + } + if s.ends_with(separator) { + return s.trim_end_matches(separator).to_string(); } s } @@ -185,7 +201,7 @@ impl<'a> Event<'a> { channel: self.channel.to_string(), id: self.id.to_string(), rawvalue: self.value.to_string(), - raw: bytes_to_strhex(self.raw), + raw: bytes_to_strhex(self.raw, " "), timestamp: self.timestamp.unwrap_or(SystemTime::now()).duration_since(SystemTime::UNIX_EPOCH)?.as_secs_f64().to_string(), value: match (remap,float) { (Some(r),true) => r.remap(self.value as f64).to_string(), diff --git a/src/main.rs b/src/main.rs index 360867d..4d31741 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ pub mod midi; pub mod util; pub mod cli; pub mod error; +pub mod constant; type Error = error::Error; @@ -18,11 +19,16 @@ use clap::Parser; use config::Config; use cli::Cli; +use midi::MidiHandler; fn main() { let c = Cli::parse(); + if c.list { - err_handle(run::list_devices()); + let mut handler = MidiHandler::new(constant::CLIENT_NAME).unwrap(); + err_handle( + handler.builder_handler(run::ListDevicesBuilder, ()) + ); return; } let map_file = err_handle( @@ -52,5 +58,9 @@ fn run_file(filepath: &Path) -> Result<(), Error> { println!("Load file {}", filepath.to_str().unwrap_or("")); let dat = std::fs::read( filepath )?; let conf = Config::try_from(&dat[..])?; - run::run_config(&conf) + let mut handler = match conf.driver { + Some(v) => MidiHandler::new_with_driver(constant::CLIENT_NAME, v), + None => MidiHandler::new(constant::CLIENT_NAME), + }?; + handler.builder_handler(run::RunConfigBuilder, &conf) } diff --git a/src/midi/alsa.rs b/src/midi/backend/alsa.rs similarity index 77% rename from src/midi/alsa.rs rename to src/midi/backend/alsa.rs index 86114da..dd893d2 100644 --- a/src/midi/alsa.rs +++ b/src/midi/backend/alsa.rs @@ -1,19 +1,75 @@ extern crate libc; extern crate alsa; +use std::str::FromStr; use std::{mem, thread}; use std::ffi::{CString, CStr}; use std::time::SystemTime; use std::sync::mpsc; -use crate::midi::{MidiInput,MidiInputHandler,MidiPort,PortFilter,MidiPortHandler,MidiAddrHandler}; +use crate::midi::{MidiInput,MidiPort,PortFilter}; use crate::error::Error; +use crate::util::InternalTryFrom; use alsa::{Seq, Direction}; use alsa::seq::{ClientIter, PortIter, MidiEvent, PortInfo, PortSubscribe, Addr, QueueTempo, EventType, PortCap, PortType}; use thiserror::Error; -pub type DeviceAddr = alsa::seq::Addr; +#[derive(Debug,Clone,PartialEq,Eq)] +pub struct DeviceAddr(Addr); + +const ANNOUNCE_ADDR: &str = "System:Announce"; +const CLIENT_NAME_ANNOUNCE: &str = "rmidimap-alsa-announce"; + +impl std::fmt::Display for DeviceAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:>3}:{}", self.client(), self.port()) + } +} + +impl From for DeviceAddr { + fn from(value: Addr) -> Self { + DeviceAddr(value) + } +} + +impl FromStr for DeviceAddr { + type Err = AlsaError; + fn from_str(s: &str) -> Result { + // todo!() + // let mut osep = ; + if let Some(sep) = s.find(':') { + let (p1,p2) = (&s[..sep], &s[sep+1..] ); + Ok(DeviceAddr( + Addr { + client: p1.parse().map_err(|_| AlsaError::AddrParse(s.to_string()))?, + port: p2.parse().map_err(|_| AlsaError::AddrParse(s.to_string()))?, + } + )) + } + else { + Err(AlsaError::AddrParse(s.to_string())) + } + } +} + +impl InternalTryFrom for DeviceAddr { + fn i_try_from(s: String) -> Result { + return Ok(Self::from_str(&s[..])?); + } +} + +impl DeviceAddr { + pub fn unwrap(&self) -> Addr { + return self.0; + } + pub fn client(&self) -> i32 { + self.0.client + } + pub fn port(&self) -> i32 { + self.0.port + } +} pub fn get_ports(s: &Seq, capability: PortCap) -> Vec { ClientIter::new(s).flat_map(|c| PortIter::new(s, c.get_client())) @@ -31,8 +87,10 @@ mod helpers { pub enum AlsaError { #[error(transparent)] ALSA(#[from] alsa::Error), - #[error("alsa decode error")] + #[error("internal alsa decode error")] Decode, + #[error("failed to parse '{0}' as an ALSA address")] + AddrParse(String), } pub struct MidiInputAlsa { @@ -63,26 +121,22 @@ impl MidiInputAlsa { fn init_queue(&mut self) -> Result { - let mut queue_id = 0; // Create the input queue - if !cfg!(feature = "avoid_timestamping") { - queue_id = self.seq.alloc_named_queue(unsafe { CStr::from_bytes_with_nul_unchecked(b"midir queue\0") })?; - // Set arbitrary tempo (mm=100) and resolution (240) - let qtempo = QueueTempo::empty()?; - qtempo.set_tempo(600_000); - qtempo.set_ppq(240); - self.seq.set_queue_tempo(queue_id, &qtempo)?; - let _ = self.seq.drain_output(); - } + let queue_id = self.seq.alloc_named_queue(unsafe { CStr::from_bytes_with_nul_unchecked(b"midir queue\0") })?; + // Set arbitrary tempo (mm=100) and resolution (240) + let qtempo = QueueTempo::empty()?; + qtempo.set_tempo(600_000); + qtempo.set_ppq(240); + self.seq.set_queue_tempo(queue_id, &qtempo)?; + let _ = self.seq.drain_output(); + Ok(queue_id) } fn start_input_queue(&mut self, queue_id: i32) { - if !cfg!(feature = "avoid_timestamping") { - let _ = self.seq.control_queue(queue_id, EventType::Start, 0, None); - let _ = self.seq.drain_output(); - } + let _ = self.seq.control_queue(queue_id, EventType::Start, 0, None); + let _ = self.seq.drain_output(); } fn create_port(&mut self, port_name: &CStr, queue_id: i32) -> Result { @@ -94,11 +148,9 @@ impl MidiInputAlsa { pinfo.set_type(PortType::MIDI_GENERIC | PortType::APPLICATION); pinfo.set_midi_channels(16); - if !cfg!(feature = "avoid_timestamping") { - pinfo.set_timestamping(true); - pinfo.set_timestamp_real(true); - pinfo.set_timestamp_queue(queue_id); - } + pinfo.set_timestamping(true); + pinfo.set_timestamp_real(true); + pinfo.set_timestamp_queue(queue_id); pinfo.set_name(port_name); match self.seq.create_port(&pinfo) { @@ -114,11 +166,10 @@ impl MidiInputAlsa { } // Stop and free the input queue - if !cfg!(feature = "avoid_timestamping") { - let _ = self.seq.control_queue(self.queue_id, EventType::Stop, 0, None); - let _ = self.seq.drain_output(); - let _ = self.seq.free_queue(self.queue_id); - } + let _ = self.seq.control_queue(self.queue_id, EventType::Stop, 0, None); + let _ = self.seq.drain_output(); + let _ = self.seq.free_queue(self.queue_id); + for fd in self.stop_trigger { if fd >= 0 { @@ -190,7 +241,7 @@ impl MidiInputAlsa { Ok(true) => break, Ok(false) => (), Err(e) => { - eprintln!("ALSA CALLBACK ERROR: {:?}", e); + eprintln!("ALSA CALLBACK ERROR: {}", e); eprintln!("continuing execution"); }, } @@ -296,7 +347,8 @@ impl MidiInputAlsa { } } -impl MidiInput for MidiInputAlsa { +impl MidiInput for MidiInputAlsa { + type DeviceAddr = DeviceAddr; fn new(client_name: &str) -> Result { let seq = Seq::open(None, None, true)?; @@ -318,45 +370,33 @@ impl MidiInput for MidiInputAlsa { Ok(()) } - - fn ports_handle(&self) -> Result, Error> { - get_ports(&self.seq, PortCap::READ | PortCap::SUBS_READ).iter().map( - |x| -> Result { - let cinfo = self.seq.get_any_client_info(x.get_client())?; - Ok(MidiPortHandler::ALSA( MidiPort{ - name: cinfo.get_name()?.to_string()+":"+x.get_name()?, - addr: x.addr(), - })) - }).collect() - } - - fn ports(&self) -> Result>, Error> { - get_ports(&self.seq, PortCap::READ | PortCap::SUBS_READ).iter().map(|x| -> Result, Error> { + fn ports(&self) -> Result>, Error> { + get_ports(&self.seq, PortCap::READ | PortCap::SUBS_READ).iter().map(|x| -> Result, Error> { let cinfo = self.seq.get_any_client_info(x.get_client())?; Ok(MidiPort { name: cinfo.get_name()?.to_string()+":"+x.get_name()?, - addr: x.addr(), + addr: x.addr().into(), }) }).collect() } - fn filter_ports(&self, mut ports: Vec>, filter: PortFilter) -> Vec> { + fn filter_ports(&self, mut ports: Vec>, filter: PortFilter) -> Vec> { ports.retain( |p| { match &filter { PortFilter::All => true, PortFilter::Name(s) => p.name.contains(s), PortFilter::Regex(s) => s.is_match(&p.name), - PortFilter::Addr(MidiAddrHandler::ALSA(s)) => p.addr == *s, - _ => panic!("unexpected error"), + PortFilter::Addr(s) => p.addr == *s, } } ); ports } - fn connect(&mut self, port_addr: &Addr, port_name: &str) -> Result<(), Error> { - let src_pinfo = self.seq.get_any_port_info(*port_addr)?; + fn connect(&mut self, port_addr: &DeviceAddr, port_name: &str) -> Result<(), Error> { + let addr = port_addr.unwrap(); + let src_pinfo = self.seq.get_any_port_info(addr)?; let queue_id = self.init_queue()?; let c_port_name = CString::new(port_name)?; let vport = self.create_port(&c_port_name, queue_id)?; @@ -367,25 +407,25 @@ impl MidiInput for MidiInputAlsa { self.seq.subscribe_port(&sub)?; self.subscription = Some(sub); self.init_trigger()?; - self.connect_addr = Some(*port_addr); + self.connect_addr = Some(addr); self.start_input_queue(queue_id); self.start_time = Some(std::time::SystemTime::now()); Ok(()) } - fn device_events(&mut self, ts: mpsc::Sender>, (tss, rss): (mpsc::Sender, mpsc::Receiver)) -> Result<(), Error> { + fn device_events(&mut self, ts: mpsc::Sender>>, (tss, rss): (mpsc::Sender, mpsc::Receiver)) -> Result<(), Error> { let ports = self.ports()?; - let port = self.filter_ports(ports, PortFilter::Name("System:Announce".to_string())); - self.connect(&port[0].addr, "rmidimap-alsa-announce")?; + let port = self.filter_ports(ports, PortFilter::Name(ANNOUNCE_ADDR.to_string())); + self.connect(&port[0].addr, CLIENT_NAME_ANNOUNCE)?; self.threaded_alsa_input(move |s: &Self, ev: alsa::seq::Event, _| -> Result { // handle disconnect event on watched port match ev.get_type() { EventType::PortStart => { if let Some(a) = ev.get_data::() { let p = s.ports()?; - let pp = s.filter_ports(p, PortFilter::Addr( MidiAddrHandler::ALSA(a) )); + let pp = s.filter_ports(p, PortFilter::Addr( a.into() ) ); if !pp.is_empty() { - ts.send(Some(MidiPortHandler::ALSA(pp[0].clone()))).expect("unexpected send() error"); + ts.send(Some(pp[0].clone())).expect("unexpected send() error"); } }; Ok(false) @@ -396,10 +436,7 @@ impl MidiInput for MidiInputAlsa { self.close_internal(); Ok(()) } -} -impl MidiInputHandler for MidiInputAlsa -{ fn signal_stop_input(&self) -> Result<(), Error> { Self::signal_stop_input_internal(self.stop_trigger[1]) } @@ -416,4 +453,3 @@ impl MidiInputHandler for MidiInputAlsa }, (ts, rs), userdata) } } - diff --git a/src/midi/backend/mod.rs b/src/midi/backend/mod.rs new file mode 100644 index 0000000..e502463 --- /dev/null +++ b/src/midi/backend/mod.rs @@ -0,0 +1,3 @@ +pub mod alsa; + +pub use alsa::MidiInputAlsa; diff --git a/src/midi/builder.rs b/src/midi/builder.rs new file mode 100644 index 0000000..d636d24 --- /dev/null +++ b/src/midi/builder.rs @@ -0,0 +1,27 @@ +use crate::util::InternalTryFrom; + +pub use super::input::{MidiInput,MidiInputHandler}; + +pub trait Builder { + fn build(&self) -> fn(&T, D) -> R + where + T: MidiInputHandler+MidiInput+Send+'static, + ::DeviceAddr: std::fmt::Display+'static+InternalTryFrom, + ; +} + +macro_rules! builder { + ( $name:ident, $fct:ident, $intype:ty, $rettype: ty ) => { + pub struct $name; + impl Builder<$intype, $rettype> for $name { + fn build(&self) -> fn(&T, $intype) -> $rettype + where + T: MidiInputHandler+Send+'static, + ::DeviceAddr: std::fmt::Display+'static+InternalTryFrom, + { + $fct + } + } + }; +} +pub(crate) use builder; \ No newline at end of file diff --git a/src/midi/driver.rs b/src/midi/driver.rs new file mode 100644 index 0000000..52b0337 --- /dev/null +++ b/src/midi/driver.rs @@ -0,0 +1,14 @@ +use serde::Deserialize; + +#[derive(Deserialize,Debug,Clone,Copy,Eq,PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum MidiDriver { + ALSA, +} + +impl MidiDriver { + // auto-detection of driver + pub fn new() -> Self { + Self::ALSA + } +} diff --git a/src/midi/input.rs b/src/midi/input.rs new file mode 100644 index 0000000..f7806fd --- /dev/null +++ b/src/midi/input.rs @@ -0,0 +1,150 @@ +use crate::util::InternalTryFrom; +use crate::{Error, constant}; +use crate::config::DeviceConfig; +use crate::eventmap::EventMap; +use crate::event::{Event, EventBuf}; + +use std::str::FromStr; +use std::thread; +use std::time::{SystemTime, Instant}; +use std::sync::{mpsc, Mutex, Arc}; + +use queues::{CircularBuffer, IsQueue}; + +use super::{PortFilter, MidiPort}; + +pub trait MidiInput +where + ::DeviceAddr: Clone+Send+FromStr, + ::DeviceAddr: InternalTryFrom +{ + type DeviceAddr; + fn new(client_name: &str) -> Result + where Self: Sized; + + fn close(self) -> Result<(), Error>; + + fn ports(&self) -> Result>, Error>; + + fn filter_ports(&self, ports: Vec>, filter: PortFilter) -> Vec>; + + fn connect(&mut self, port_addr: &Self::DeviceAddr, port_name: &str) -> Result<(), Error>; + + fn device_events(&mut self, ts: mpsc::Sender>>, ss: (mpsc::Sender, mpsc::Receiver)) -> Result<(), Error>; + + fn signal_stop_input(&self) -> Result<(), Error>; + + fn handle_input(&mut self, callback: F, rts: (mpsc::Sender, mpsc::Receiver), userdata: D) -> Result<(), Error> + where + F: Fn(&Self, &[u8], Option, &mut D) + Send + Sync, + D: Send, + ; +} + +pub trait MidiInputHandler +where + Self: Sized, + ::DeviceAddr: Clone+Send+FromStr, +{ + type DeviceAddr; + fn new(client_name: &str) -> Result; + fn ports(&self) -> Result>, Error>; + fn try_connect(&self, port: MidiPort, filter: PortFilter ) -> Result, Error>; + fn run(&mut self, conf: &DeviceConfig, eventmap: &EventMap, trs: (mpsc::Sender, mpsc::Receiver)) -> Result<(), Error>; + fn device_events(&mut self, ts: mpsc::Sender>>, ss: (mpsc::Sender,mpsc::Receiver)) -> Result<(), Error>; +} + +// Generic implementation + +impl MidiInputHandler for T +where + T: MidiInput + Send, + ::DeviceAddr: FromStr, +{ + type DeviceAddr = T::DeviceAddr; + + + fn new(client_name: &str) -> Result { + MidiInput::new(client_name) + } + + fn ports(&self) -> Result>, Error> { + MidiInput::ports(self) + } + + fn try_connect(&self, port: MidiPort, filter: PortFilter ) -> Result, Error> { + let portmap = self.ports()?; + let pv = self.filter_ports(portmap, PortFilter::Addr(port.addr)); + let pv = self.filter_ports(pv, filter); + if pv.len() > 0 { + let port = &pv[0]; + let mut v = T::new(constant::CLIENT_NAME_HANDLER)?; + v.connect(&port.addr, constant::CLIENT_NAME_HANDLER)?; + Ok(Some(v)) + } + else { + Ok(None) + } + } + + fn device_events(&mut self, ts: mpsc::Sender>>, ss: (mpsc::Sender,mpsc::Receiver)) -> Result<(), Error> { + self.device_events(ts, ss) + } + + fn run(&mut self, conf: &DeviceConfig, eventmap: &EventMap, (ts, rs): (mpsc::Sender, mpsc::Receiver)) -> Result<(), Error> { + thread::scope(|s| -> Result<(), Error> { + + // parking signal for runner, true = stop + let (pts,prs) = mpsc::channel::(); + + // event queue populated by the main thread and consumed by the exec thread + let evq = Arc::new(Mutex::new(CircularBuffer::::new(conf.queue_length))); + + // background execution loop + let rq = evq.clone(); + let exec_thread = s.spawn(move || -> Result<(),Error> { + loop { + if prs.recv()? { + break; + } + loop { + // nest the lock into a scope to release it before run + let (ev,start): (EventBuf,Instant) = { + let mut evq = rq.lock().unwrap(); + if evq.size() > 0 { + (evq.remove().unwrap(), Instant::now()) + } else { + break; + } + }; + eventmap.run_event(&ev.as_event()).unwrap_or_else(|e| eprintln!("ERROR: error on run: {}", e) ); + // wait until interval has been reached + let elapsed_time = start.elapsed(); + if elapsed_time < conf.interval { + thread::sleep(conf.interval - elapsed_time); + } + } + } + Ok(()) + }); + + self.handle_input(|_,m,t,(evq,pts)| { + let mut event: EventBuf = Event::from(m).into(); + event.timestamp = t; + if conf.log { + println!("{}: event: {}", constant::CLIENT_NAME, event); + } + let mut evq = evq.lock().unwrap(); + evq.add(event).unwrap(); + pts.send(false).expect("unexpected write error"); + }, (ts,rs), (evq,pts.clone()))?; + + pts.send(true).expect("unexpected write error"); + let _ = exec_thread.join(); + + Ok(()) + + })?; + Ok(()) + } +} diff --git a/src/midi/mod.rs b/src/midi/mod.rs index 9326a9e..dcbd55e 100644 --- a/src/midi/mod.rs +++ b/src/midi/mod.rs @@ -1,273 +1,45 @@ -pub mod alsa; +pub mod backend; -use queues::{CircularBuffer, IsQueue}; +pub mod port; +pub mod portfilter; +pub mod input; +pub mod builder; +pub mod driver; -use crate::config::device::Identifier; -use super::midi::alsa::MidiInputAlsa; use crate::Error; extern crate libc; -use crate::config::DeviceConfig; -use crate::eventmap::EventMap; -use crate::event::{Event, EventBuf}; - -use std::thread; -use std::time::{SystemTime, Instant}; -use std::sync::{mpsc, Mutex, Arc}; - -#[derive(Eq,PartialEq,Debug,Clone)] -pub struct MidiPort{ - pub name: String, - pub addr: T, -} - -#[derive(Debug,Clone)] -pub enum PortFilter { - All, - Name(String), - Regex(regex::Regex), - Addr(MidiAddrHandler), -} - -#[derive(Debug,Clone,Eq,PartialEq)] -pub enum MidiHandlerDriver { - ALSA, -} - -#[derive(Debug,Clone,Eq,PartialEq)] -pub enum MidiAddrHandler { - ALSA(alsa::DeviceAddr), -} - -#[derive(Debug,Clone,Eq,PartialEq)] -pub enum MidiPortHandler { - ALSA(MidiPort), -} - -impl std::fmt::Display for MidiPortHandler { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - MidiPortHandler::ALSA(p) => write!(f, "{} {}:{}", p.name, p.addr.client, p.addr.port), - _ => todo!(), - } - } -} +pub use driver::MidiDriver; +pub use builder::Builder; +pub use port::MidiPort; +pub use portfilter::PortFilter; +pub use input::{MidiInput,MidiInputHandler}; pub enum MidiHandler { - ALSA(MidiInputAlsa), -} - -impl From for MidiAddrHandler { - fn from(a: MidiPortHandler) -> Self { - match a { - MidiPortHandler::ALSA(p) => MidiAddrHandler::ALSA(p.addr), - _ => todo!(), - } - } -} - -impl From<&DeviceConfig> for PortFilter { - fn from(conf: &DeviceConfig) -> Self { - match &conf.identifier { - Identifier::All => PortFilter::All, - Identifier::Name(s) => PortFilter::Name(s.clone()), - Identifier::Regex(s) => PortFilter::Regex(s.clone()), - _ => todo!("match type not implemented"), - } - } -} - -pub trait MidiInput { - fn new(client_name: &str) -> Result - where Self: Sized; - - fn close(self) -> Result<(), Error>; - - fn ports(&self) -> Result>, Error>; - fn ports_handle(&self) -> Result, Error>; - - fn filter_ports(&self, ports: Vec>, filter: PortFilter) -> Vec>; - - fn connect(&mut self, port_addr: &T, port_name: &str) -> Result<(), Error>; - - fn device_events(&mut self, ts: mpsc::Sender>, ss: (mpsc::Sender, mpsc::Receiver)) -> Result<(), Error>; -} - -pub trait MidiInputHandler { - fn signal_stop_input(&self) -> Result<(), Error>; - - fn handle_input(&mut self, callback: F, rts: (mpsc::Sender, mpsc::Receiver), userdata: D) -> Result<(), Error> - where - F: Fn(&Self, &[u8], Option, &mut D) + Send + Sync, - D: Send, - ; -} - -macro_rules! handler_try_connect { - ( $m:expr , $filter:expr, $port:expr, $( $handler:ident ),+ ) => { - match $m { - $( - MidiHandler::$handler(v) => { - match $port { - MidiPortHandler::$handler(_) => { - let maddr = MidiAddrHandler::from($port); - let portmap = v.ports()?; - let pv = v.filter_ports(portmap, PortFilter::Addr(maddr)); - let pv = v.filter_ports(pv, $filter); - if pv.len() > 0 { - let port = &pv[0]; - let mut h = MidiHandler::new_with_driver("rmidimap-handler", MidiHandlerDriver::$handler)?; - match &mut h { - MidiHandler::$handler(v) => { - v.connect(&port.addr, "rmidimap-handler")?; - Ok(Some(h)) - } - _ => panic!("unexpected midi driver failure"), - } - } - else { - Ok(None) - } - }, - _ => panic!("unexpected midi driver failure"), - } - } - )* - } - }; -} - -macro_rules! handler_fcall { - ( $m:expr , $fct:expr , $arg:expr , $( $handler:ident ),+ ) => { - match $m { - $( - MidiHandler::$handler(v) => $fct(v, $arg), - )* - } - }; + ALSA(backend::MidiInputAlsa), } impl MidiHandler { pub fn new(name: &str) -> Result { - Self::new_with_driver(name, MidiHandlerDriver::ALSA) + Self::new_with_driver(name, MidiDriver::new()) } - pub fn new_with_driver(name: &str, driver: MidiHandlerDriver) -> Result { + pub fn new_with_driver(name: &str, driver: MidiDriver) -> Result { match driver { - MidiHandlerDriver::ALSA => Ok(MidiHandler::ALSA(MidiInputAlsa::new(name)?)), + MidiDriver::ALSA => Ok(MidiHandler::ALSA(MidiInput::new(name)?)), _ => todo!(), } } - pub fn ports(&self) -> Result, Error> { - handler_fcall!{ - self, handle_port_list ,(), - ALSA - } - } - - pub fn try_connect(&self, addr: MidiPortHandler, filter: PortFilter) -> Result, Error> { - let r: Result, Error> = handler_try_connect!{ - self, filter, addr, - ALSA - }; - r - } - - pub fn run(&mut self, conf: &DeviceConfig, eventmap: &EventMap, trs: (mpsc::Sender, mpsc::Receiver)) -> Result<(), Error> { - handler_fcall!{ - self, handle_inputport, (conf, eventmap,trs), - ALSA - } - } - - pub fn stop(&self) -> Result<(), Error> { - handler_fcall!{ - self, handle_signal_stop, (), - ALSA - } - } - - pub fn device_events(&mut self, ts: mpsc::Sender>, (tss, rss): (mpsc::Sender,mpsc::Receiver)) -> Result<(), Error> { - handler_fcall!{ - self, device_events, (ts,tss,rss), - ALSA + // wrap generic functions into builder because functions with generic traits cannot be passed as arguments + pub fn builder_handler(&mut self, builder: B, data: D) -> R + where + B: Builder, + D: Send, + { + match self { + MidiHandler::ALSA(v) => builder.build()(v, data), } } } - -fn handle_port_list(input: &T, _: ()) -> Result, Error> -where T: MidiInput -{ - input.ports_handle() -} - -fn handle_inputport(input: &mut T, (conf, eventmap, (ts, rs)): (&DeviceConfig, &EventMap, (mpsc::Sender, mpsc::Receiver))) -> Result<(), Error> -where T: MidiInputHandler + Send -{ - thread::scope(|s| -> Result<(), Error> { - - // parking signal for runner, true = stop - let (pts,prs) = mpsc::channel::(); - - // event queue populated by the main thread and consumed by the exec thread - let evq = Arc::new(Mutex::new(CircularBuffer::::new(conf.queue_length))); - - // background execution loop - let rq = evq.clone(); - let exec_thread = s.spawn(move || -> Result<(),Error> { - loop { - if prs.recv()? { - break; - } - loop { - // nest the lock into a scope to release it before run - let (ev,start): (EventBuf,Instant) = { - let mut evq = rq.lock().unwrap(); - if evq.size() > 0 { - (evq.remove().unwrap(), Instant::now()) - } else { - break; - } - }; - eventmap.run_event(&ev.as_event()).unwrap_or_else(|e| eprintln!("ERROR: error on run: {}", e) ); - // wait until interval has been reached - let elapsed_time = start.elapsed(); - if elapsed_time < conf.interval { - thread::sleep(conf.interval - elapsed_time); - } - } - } - Ok(()) - }); - - input.handle_input(|_,m,t,(evq,pts)| { - let mut event: EventBuf = Event::from(m).into(); - event.timestamp = t; - let mut evq = evq.lock().unwrap(); - evq.add(event).unwrap(); - pts.send(false).expect("unexpected write error"); - }, (ts,rs), (evq,pts.clone()))?; - - pts.send(true).expect("unexpected write error"); - let _ = exec_thread.join(); - - Ok(()) - - })?; - Ok(()) -} - -fn handle_signal_stop(input: &T, _: ()) -> Result<(), Error> -where T: MidiInputHandler -{ - input.signal_stop_input() -} - -fn device_events(input: &mut T, (ts,tss,rss): (mpsc::Sender>, mpsc::Sender, mpsc::Receiver)) -> Result<(), Error> -where T: MidiInput -{ - input.device_events(ts, (tss, rss)) -} - diff --git a/src/midi/port.rs b/src/midi/port.rs new file mode 100644 index 0000000..55a1db9 --- /dev/null +++ b/src/midi/port.rs @@ -0,0 +1,21 @@ +use std::fmt::{Display, Formatter}; + + +#[derive(Eq,PartialEq,Debug,Clone)] +pub struct MidiPort +where + T: Clone +{ + pub name: String, + pub addr: T, +} + + +impl Display for MidiPort +where + T: Display+Clone +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}\t{}", self.addr, self.name) + } +} \ No newline at end of file diff --git a/src/midi/portfilter.rs b/src/midi/portfilter.rs new file mode 100644 index 0000000..4952473 --- /dev/null +++ b/src/midi/portfilter.rs @@ -0,0 +1,28 @@ +use crate::Error; +use crate::config::DeviceConfig; +use crate::config::device::Identifier; +use crate::util::InternalTryFrom; + + +#[derive(Debug,Clone)] +pub enum PortFilter{ + All, + Name(String), + Regex(regex::Regex), + Addr(T), +} + + +impl InternalTryFrom<&DeviceConfig> for PortFilter +where + T: InternalTryFrom, +{ + fn i_try_from(conf: &DeviceConfig) -> Result { + Ok(match &conf.identifier { + Identifier::All => PortFilter::All, + Identifier::Name(s) => PortFilter::Name(s.clone()), + Identifier::Regex(s) => PortFilter::Regex(s.clone()), + Identifier::Addr(s) => PortFilter::Addr(T::i_try_from(s.to_string())?), + }) + } +} \ No newline at end of file diff --git a/src/run.rs b/src/run.rs index c0b3621..5c49c2d 100644 --- a/src/run.rs +++ b/src/run.rs @@ -5,10 +5,12 @@ use std::sync::{Mutex,Arc}; use libc::SIGUSR1; use signal_hook::iterator::Signals; -use crate::Error; -use crate::midi::{PortFilter,MidiHandler,MidiPortHandler}; +use crate::util::InternalTryFrom; +use crate::{Error, constant}; +use crate::midi::{PortFilter,MidiInputHandler, MidiPort, Builder}; use crate::config::{Config,DeviceConfig}; use crate::eventmap::EventMap; +use crate::midi::builder::builder; type DeviceRunItem<'a> = (&'a DeviceConfig, EventMap<'a>, Option>>); type DeviceRunResult<'a> =(thread::ScopedJoinHandle<'a, Result<(), Error>>, mpsc::Sender); @@ -24,25 +26,34 @@ pub fn cross_shell(cmd: &str) -> Vec { ).collect() } -pub fn list_devices() -> Result<(), Error> { - let input = MidiHandler::new("rmidimap")?; - let ports = input.ports()?; +builder!(ListDevicesBuilder, list_devices, (), Result<(), Error>); +builder!(RunConfigBuilder, run_config, &Config, Result<(), Error>); + +pub fn list_devices(input: &T, _: ()) -> Result<(), Error> +where + T: MidiInputHandler+Send+'static, + ::DeviceAddr: 'static+std::fmt::Display, +{ + let ports = MidiInputHandler::ports(input)?; + println!(" Addr\t Name"); for p in ports { println!("{}", p); } Ok(()) } -pub fn run_config(conf: &Config) -> Result<(), Error> { +pub fn run_config(input: &T, conf: &Config) -> Result<(), Error> +where + T: MidiInputHandler+Send+'static, + ::DeviceAddr: 'static+std::fmt::Display+InternalTryFrom, +{ let cfevmap: Vec = conf.devices.iter().map(|x| (x, EventMap::from(x), x.max_connections.map(|v| (Arc::new(Mutex::new((0,v))))) ) ).collect(); - let input = MidiHandler::new("rmidimap")?; - - let (tdev,rdev) = mpsc::channel::>(); + let (tdev,rdev) = mpsc::channel::>>(); let (tsd,rsd) = mpsc::channel::(); let ntsd = tsd.clone(); @@ -66,11 +77,11 @@ pub fn run_config(conf: &Config) -> Result<(), Error> { let mut threads: Vec = Vec::new(); let ports = input.ports()?; for p in ports { - if let Some(v) = try_connect_process(&input, s, &p, &cfevmap)? { threads.push(v) } + if let Some(v) = try_connect_process(input, s, &p, &cfevmap)? { threads.push(v) } } let event_thread = s.spawn(move || { - let mut input = MidiHandler::new("rmidimap-event-watcher").unwrap(); + let mut input = T::new(constant::CLIENT_NAME_EVENT).unwrap(); let r = input.device_events(tdev.clone(), (tsd,rsd)); tdev.send(None).unwrap(); r @@ -81,7 +92,11 @@ pub fn run_config(conf: &Config) -> Result<(), Error> { if p.is_none() { break; } - if let Some(v) = try_connect_process(&input, s, &p.unwrap(), &cfevmap)? { threads.push(v) } + let p = p.unwrap(); + if conf.log { + println!("{}: device connect: {}", constant::CLIENT_NAME, p); + } + if let Some(v) = try_connect_process(input, s, &p, &cfevmap)? { threads.push(v) } }; event_thread.join().unwrap()?; for (thread,ss) in threads { @@ -93,13 +108,17 @@ pub fn run_config(conf: &Config) -> Result<(), Error> { Ok(()) } -fn try_connect_process<'a>( - input: &MidiHandler, +fn try_connect_process<'a, T>( + input: &T, s: &'a thread::Scope<'a, '_>, - p: &MidiPortHandler, + p: &MidiPort, cfevmap: &'a[DeviceRunItem<'a>], ) - -> Result>, Error> { + -> Result>, Error> +where + T: MidiInputHandler+Send+'static, + ::DeviceAddr: 'static+InternalTryFrom, +{ for (dev, eventmap, counter) in cfevmap { // device counter is full if let Some(m) = counter { @@ -109,7 +128,7 @@ fn try_connect_process<'a>( } } - if let Some(mut c) = input.try_connect(p.clone(), PortFilter::from(*dev))? { + if let Some(mut c) = input.try_connect(p.clone(), PortFilter::i_try_from(*dev)?)? { // increase device counter if let Some(m) = counter { let mut m = m.lock().unwrap(); diff --git a/src/util/mod.rs b/src/util/mod.rs index 325f0e9..1eeb686 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -41,3 +41,10 @@ where None => Ok(None), } } + +pub trait InternalTryFrom +where + Self: Sized, +{ + fn i_try_from(value: T) -> Result; +}