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:
parent
f01e867914
commit
e61163da88
21 changed files with 616 additions and 341 deletions
112
FORMAT.md
Normal file
112
FORMAT.md
Normal 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
40
README.md
Normal 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`
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)?,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
4
src/constant.rs
Normal 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";
|
||||
|
|
@ -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)]
|
||||
|
|
|
|||
24
src/event.rs
24
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<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(),
|
||||
|
|
|
|||
14
src/main.rs
14
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("<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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
3
src/midi/backend/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod alsa;
|
||||
|
||||
pub use alsa::MidiInputAlsa;
|
||||
27
src/midi/builder.rs
Normal file
27
src/midi/builder.rs
Normal 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
14
src/midi/driver.rs
Normal 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
150
src/midi/input.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
274
src/midi/mod.rs
274
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<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
21
src/midi/port.rs
Normal 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
28
src/midi/portfilter.rs
Normal 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())?),
|
||||
})
|
||||
}
|
||||
}
|
||||
53
src/run.rs
53
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<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();
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue