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
This commit is contained in:
zawz 2023-09-08 15:31:16 +02:00
parent f01e867914
commit e61163da88
21 changed files with 616 additions and 341 deletions

112
FORMAT.md Normal file
View file

@ -0,0 +1,112 @@
# Format
yaml configuration format
```yaml
# Log all device connections
[ log_devices: <bool> | default = false ]
# Midi backend to use. Currently only alsa
[ driver: alsa ]
# Device definitions
devices:
[ - <device_config> ... ]
```
### `<device_config>`
Definition of one device with its config and corresponding events.
```yaml
# Find device by name with literal string
[ name: <string> ]
# Find device by name with regex
[ regex: <regex> ]
# Find device by exact address
[ addr: <string> ]
# Max number of devices to connect for this device definition.
[ max_connections: <int> | default = inf ]
# Max length of event queue for processing.
[ queue_length: <int> | 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: <duration> | default = 0 ]
# Log all midi events of device
[ log_events: <bool> | default = false ]
# Commands to run on device connect
connect:
[ - <run_config> ... ]
# Commands to run on device disconnect
disconnect:
[ - <run_config> ... ]
# Definitions of executions on MIDI events
events:
[ - <event_config> ... ]
```
### `<event_config>`
Definition of one MIDI event condition and its corresponding executions.
```yaml
# Max number of devices to connect for this device definition.
[ max_connections: <int> | default = inf ]
# Max length of event queue for processing.
[ queue_length: <int> | 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: <duration> | default = 0ms ]
# Commands to run on device connect
connect:
[ - <run_config> ... ]
# Commands to run on device disconnect
disconnect:
[ - <run_config> ... ]
# Definitions of executions on MIDI events
events:
[ - <event_config> ... ]
```
### `<event_config>`
Definition of one MIDI event condition and its corresponding executions.
```yaml
# Max number of devices to connect for this device definition.
[ max_connections: <int> | default = inf ]
# Max length of event queue for processing.
[ queue_length: <int> | 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: <duration> | default = 0ms ]
# Commands to run on device connect
connect:
[ - <run_config> ... ]
# Commands to run on device disconnect
disconnect:
[ - <run_config> ... ]
# Definitions of executions on MIDI events
events:
[ - <event_config> ... ]
```

40
README.md Normal file
View file

@ -0,0 +1,40 @@
# rmidimap
Map MIDI signals to command with a simple yaml file.
See [format]() and [examples](/examples/).
# Usage
Simply execute `rmidimap <FILE>` 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`

View file

@ -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

View file

@ -23,6 +23,7 @@ pub struct DeviceConfig {
pub events: Option<Vec<EventConfig>>,
pub queue_length: usize,
pub interval: Duration,
pub log: bool,
}
impl DeviceConfig {
@ -71,6 +72,7 @@ impl TryFrom<DeviceConfigSerializer> 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),
})
}
}

View file

@ -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<crate::midi::MidiDriver>,
pub devices: Vec<DeviceConfig>,
}
@ -23,6 +25,8 @@ impl TryFrom<ConfigSerializer> for Config {
type Error = crate::Error;
fn try_from(v: ConfigSerializer) -> Result<Self, Self::Error> {
Ok(Config {
log: v.log_devices.unwrap_or(false),
driver: v.driver,
devices: util::map_tryfrom(v.devices)?,
})
}

View file

@ -33,4 +33,5 @@ pub struct DeviceConfigSerializer {
pub max_connections: Option<u32>,
pub queue_length: Option<usize>,
pub interval: Option<DurationWrapper>,
pub log_events: Option<bool>,
}

View file

@ -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<bool>,
pub driver: Option<crate::midi::MidiDriver>,
pub devices: Vec<DeviceConfigSerializer>,
}

4
src/constant.rs Normal file
View file

@ -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";

View file

@ -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)]

View file

@ -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<u8> 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(),

View file

@ -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("<unknown>"));
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)
}

View file

@ -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<Addr> for DeviceAddr {
fn from(value: Addr) -> Self {
DeviceAddr(value)
}
}
impl FromStr for DeviceAddr {
type Err = AlsaError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// 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<String> for DeviceAddr {
fn i_try_from(s: String) -> Result<Self, crate::Error> {
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<PortInfo> {
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<i32, alsa::Error> {
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<i32, AlsaError> {
@ -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<Addr> for MidiInputAlsa {
impl MidiInput for MidiInputAlsa {
type DeviceAddr = DeviceAddr;
fn new(client_name: &str) -> Result<Self, Error> {
let seq = Seq::open(None, None, true)?;
@ -318,45 +370,33 @@ impl MidiInput<Addr> for MidiInputAlsa {
Ok(())
}
fn ports_handle(&self) -> Result<Vec<MidiPortHandler>, Error> {
get_ports(&self.seq, PortCap::READ | PortCap::SUBS_READ).iter().map(
|x| -> Result<MidiPortHandler, Error> {
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<Vec<MidiPort<Addr>>, Error> {
get_ports(&self.seq, PortCap::READ | PortCap::SUBS_READ).iter().map(|x| -> Result<MidiPort<Addr>, Error> {
fn ports(&self) -> Result<Vec<MidiPort<DeviceAddr>>, Error> {
get_ports(&self.seq, PortCap::READ | PortCap::SUBS_READ).iter().map(|x| -> Result<MidiPort<DeviceAddr>, 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<MidiPort<Addr>>, filter: PortFilter) -> Vec<MidiPort<Addr>> {
fn filter_ports(&self, mut ports: Vec<MidiPort<DeviceAddr>>, filter: PortFilter<Self::DeviceAddr>) -> Vec<MidiPort<DeviceAddr>> {
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<Addr> 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<Option<MidiPortHandler>>, (tss, rss): (mpsc::Sender<bool>, mpsc::Receiver<bool>)) -> Result<(), Error> {
fn device_events(&mut self, ts: mpsc::Sender<Option<MidiPort<Self::DeviceAddr>>>, (tss, rss): (mpsc::Sender<bool>, mpsc::Receiver<bool>)) -> 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<bool, Error> {
// handle disconnect event on watched port
match ev.get_type() {
EventType::PortStart => {
if let Some(a) = ev.get_data::<alsa::seq::Addr>() {
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<Addr> 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)
}
}

3
src/midi/backend/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod alsa;
pub use alsa::MidiInputAlsa;

27
src/midi/builder.rs Normal file
View file

@ -0,0 +1,27 @@
use crate::util::InternalTryFrom;
pub use super::input::{MidiInput,MidiInputHandler};
pub trait Builder<D, R> {
fn build<T>(&self) -> fn(&T, D) -> R
where
T: MidiInputHandler+MidiInput+Send+'static,
<T as MidiInputHandler>::DeviceAddr: std::fmt::Display+'static+InternalTryFrom<String>,
;
}
macro_rules! builder {
( $name:ident, $fct:ident, $intype:ty, $rettype: ty ) => {
pub struct $name;
impl Builder<$intype, $rettype> for $name {
fn build<T>(&self) -> fn(&T, $intype) -> $rettype
where
T: MidiInputHandler+Send+'static,
<T as MidiInputHandler>::DeviceAddr: std::fmt::Display+'static+InternalTryFrom<String>,
{
$fct
}
}
};
}
pub(crate) use builder;

14
src/midi/driver.rs Normal file
View file

@ -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
}
}

150
src/midi/input.rs Normal file
View file

@ -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
<Self as MidiInput>::DeviceAddr: Clone+Send+FromStr,
<Self as MidiInput>::DeviceAddr: InternalTryFrom<std::string::String>
{
type DeviceAddr;
fn new(client_name: &str) -> Result<Self, Error>
where Self: Sized;
fn close(self) -> Result<(), Error>;
fn ports(&self) -> Result<Vec<MidiPort<Self::DeviceAddr>>, Error>;
fn filter_ports(&self, ports: Vec<MidiPort<Self::DeviceAddr>>, filter: PortFilter<Self::DeviceAddr>) -> Vec<MidiPort<Self::DeviceAddr>>;
fn connect(&mut self, port_addr: &Self::DeviceAddr, port_name: &str) -> Result<(), Error>;
fn device_events(&mut self, ts: mpsc::Sender<Option<MidiPort<Self::DeviceAddr>>>, ss: (mpsc::Sender<bool>, mpsc::Receiver<bool>)) -> Result<(), Error>;
fn signal_stop_input(&self) -> Result<(), Error>;
fn handle_input<F, D>(&mut self, callback: F, rts: (mpsc::Sender<bool>, mpsc::Receiver<bool>), userdata: D) -> Result<(), Error>
where
F: Fn(&Self, &[u8], Option<SystemTime>, &mut D) + Send + Sync,
D: Send,
;
}
pub trait MidiInputHandler
where
Self: Sized,
<Self as MidiInputHandler>::DeviceAddr: Clone+Send+FromStr,
{
type DeviceAddr;
fn new(client_name: &str) -> Result<Self, Error>;
fn ports(&self) -> Result<Vec<MidiPort<Self::DeviceAddr>>, Error>;
fn try_connect(&self, port: MidiPort<Self::DeviceAddr>, filter: PortFilter<Self::DeviceAddr> ) -> Result<Option<Self>, Error>;
fn run(&mut self, conf: &DeviceConfig, eventmap: &EventMap, trs: (mpsc::Sender<bool>, mpsc::Receiver<bool>)) -> Result<(), Error>;
fn device_events(&mut self, ts: mpsc::Sender<Option<MidiPort<Self::DeviceAddr>>>, ss: (mpsc::Sender<bool>,mpsc::Receiver<bool>)) -> Result<(), Error>;
}
// Generic implementation
impl<T> MidiInputHandler for T
where
T: MidiInput + Send,
<T as MidiInput>::DeviceAddr: FromStr,
{
type DeviceAddr = T::DeviceAddr;
fn new(client_name: &str) -> Result<Self, Error> {
MidiInput::new(client_name)
}
fn ports(&self) -> Result<Vec<MidiPort<Self::DeviceAddr>>, Error> {
MidiInput::ports(self)
}
fn try_connect(&self, port: MidiPort<Self::DeviceAddr>, filter: PortFilter<T::DeviceAddr> ) -> Result<Option<Self>, 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<Option<MidiPort<Self::DeviceAddr>>>, ss: (mpsc::Sender<bool>,mpsc::Receiver<bool>)) -> Result<(), Error> {
self.device_events(ts, ss)
}
fn run(&mut self, conf: &DeviceConfig, eventmap: &EventMap, (ts, rs): (mpsc::Sender<bool>, mpsc::Receiver<bool>)) -> Result<(), Error> {
thread::scope(|s| -> Result<(), Error> {
// parking signal for runner, true = stop
let (pts,prs) = mpsc::channel::<bool>();
// event queue populated by the main thread and consumed by the exec thread
let evq = Arc::new(Mutex::new(CircularBuffer::<EventBuf>::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(())
}
}

View file

@ -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<T>{
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<alsa::DeviceAddr>),
}
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<MidiPortHandler> 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<T> {
fn new(client_name: &str) -> Result<Self, Error>
where Self: Sized;
fn close(self) -> Result<(), Error>;
fn ports(&self) -> Result<Vec<MidiPort<T>>, Error>;
fn ports_handle(&self) -> Result<Vec<MidiPortHandler>, Error>;
fn filter_ports(&self, ports: Vec<MidiPort<T>>, filter: PortFilter) -> Vec<MidiPort<T>>;
fn connect(&mut self, port_addr: &T, port_name: &str) -> Result<(), Error>;
fn device_events(&mut self, ts: mpsc::Sender<Option<MidiPortHandler>>, ss: (mpsc::Sender<bool>, mpsc::Receiver<bool>)) -> Result<(), Error>;
}
pub trait MidiInputHandler {
fn signal_stop_input(&self) -> Result<(), Error>;
fn handle_input<F, D>(&mut self, callback: F, rts: (mpsc::Sender<bool>, mpsc::Receiver<bool>), userdata: D) -> Result<(), Error>
where
F: Fn(&Self, &[u8], Option<SystemTime>, &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, Error> {
Self::new_with_driver(name, MidiHandlerDriver::ALSA)
Self::new_with_driver(name, MidiDriver::new())
}
pub fn new_with_driver(name: &str, driver: MidiHandlerDriver) -> Result<Self, Error> {
pub fn new_with_driver(name: &str, driver: MidiDriver) -> Result<Self, Error> {
match driver {
MidiHandlerDriver::ALSA => Ok(MidiHandler::ALSA(MidiInputAlsa::new(name)?)),
MidiDriver::ALSA => Ok(MidiHandler::ALSA(MidiInput::new(name)?)),
_ => todo!(),
}
}
pub fn ports(&self) -> Result<Vec<MidiPortHandler>, Error> {
handler_fcall!{
self, handle_port_list ,(),
ALSA
}
}
pub fn try_connect(&self, addr: MidiPortHandler, filter: PortFilter) -> Result<Option<MidiHandler>, Error> {
let r: Result<Option<MidiHandler>, Error> = handler_try_connect!{
self, filter, addr,
ALSA
};
r
}
pub fn run(&mut self, conf: &DeviceConfig, eventmap: &EventMap, trs: (mpsc::Sender<bool>, mpsc::Receiver<bool>)) -> 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<Option<MidiPortHandler>>, (tss, rss): (mpsc::Sender<bool>,mpsc::Receiver<bool>)) -> 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<B, D, R>(&mut self, builder: B, data: D) -> R
where
B: Builder<D,R>,
D: Send,
{
match self {
MidiHandler::ALSA(v) => builder.build()(v, data),
}
}
}
fn handle_port_list<T, A>(input: &T, _: ()) -> Result<Vec<MidiPortHandler>, Error>
where T: MidiInput<A>
{
input.ports_handle()
}
fn handle_inputport<T>(input: &mut T, (conf, eventmap, (ts, rs)): (&DeviceConfig, &EventMap, (mpsc::Sender<bool>, mpsc::Receiver<bool>))) -> Result<(), Error>
where T: MidiInputHandler + Send
{
thread::scope(|s| -> Result<(), Error> {
// parking signal for runner, true = stop
let (pts,prs) = mpsc::channel::<bool>();
// event queue populated by the main thread and consumed by the exec thread
let evq = Arc::new(Mutex::new(CircularBuffer::<EventBuf>::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<T>(input: &T, _: ()) -> Result<(), Error>
where T: MidiInputHandler
{
input.signal_stop_input()
}
fn device_events<T, A>(input: &mut T, (ts,tss,rss): (mpsc::Sender<Option<MidiPortHandler>>, mpsc::Sender<bool>, mpsc::Receiver<bool>)) -> Result<(), Error>
where T: MidiInput<A>
{
input.device_events(ts, (tss, rss))
}

21
src/midi/port.rs Normal file
View file

@ -0,0 +1,21 @@
use std::fmt::{Display, Formatter};
#[derive(Eq,PartialEq,Debug,Clone)]
pub struct MidiPort<T>
where
T: Clone
{
pub name: String,
pub addr: T,
}
impl<T> Display for MidiPort<T>
where
T: Display+Clone
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}\t{}", self.addr, self.name)
}
}

28
src/midi/portfilter.rs Normal file
View file

@ -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<T>{
All,
Name(String),
Regex(regex::Regex),
Addr(T),
}
impl<T> InternalTryFrom<&DeviceConfig> for PortFilter<T>
where
T: InternalTryFrom<String>,
{
fn i_try_from(conf: &DeviceConfig) -> Result<Self, Error> {
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())?),
})
}
}

View file

@ -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<Arc<Mutex<(u32, u32)>>>);
type DeviceRunResult<'a> =(thread::ScopedJoinHandle<'a, Result<(), Error>>, mpsc::Sender<bool>);
@ -24,25 +26,34 @@ pub fn cross_shell(cmd: &str) -> Vec<String> {
).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<T>(input: &T, _: ()) -> Result<(), Error>
where
T: MidiInputHandler+Send+'static,
<T as MidiInputHandler>::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<T>(input: &T, conf: &Config) -> Result<(), Error>
where
T: MidiInputHandler+Send+'static,
<T as MidiInputHandler>::DeviceAddr: 'static+std::fmt::Display+InternalTryFrom<String>,
{
let cfevmap: Vec<DeviceRunItem> = 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::<Option<MidiPortHandler>>();
let (tdev,rdev) = mpsc::channel::<Option<MidiPort<T::DeviceAddr>>>();
let (tsd,rsd) = mpsc::channel::<bool>();
let ntsd = tsd.clone();
@ -66,11 +77,11 @@ pub fn run_config(conf: &Config) -> Result<(), Error> {
let mut threads: Vec<DeviceRunResult> = 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<T::DeviceAddr>,
cfevmap: &'a[DeviceRunItem<'a>],
)
-> Result<Option<DeviceRunResult<'a>>, Error> {
-> Result<Option<DeviceRunResult<'a>>, Error>
where
T: MidiInputHandler+Send+'static,
<T as MidiInputHandler>::DeviceAddr: 'static+InternalTryFrom<String>,
{
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();

View file

@ -41,3 +41,10 @@ where
None => Ok(None),
}
}
pub trait InternalTryFrom<T>
where
Self: Sized,
{
fn i_try_from(value: T) -> Result<Self, crate::Error>;
}