From 31a89f166566947e950d53dbc6336b7af6eef8fc Mon Sep 17 00:00:00 2001 From: zawz Date: Mon, 7 Aug 2023 21:50:22 +0200 Subject: [PATCH] init --- .gitignore | 2 + Cargo.toml | 21 +++ src/cli.rs | 11 ++ src/config/device.rs | 48 ++++++ src/config/event.rs | 26 +++ src/config/mod.rs | 14 ++ src/config/run.rs | 29 ++++ src/event.rs | 129 +++++++++++++++ src/eventmap.rs | 112 +++++++++++++ src/main.rs | 85 ++++++++++ src/midi/alsa.rs | 366 +++++++++++++++++++++++++++++++++++++++++++ src/midi/mod.rs | 217 +++++++++++++++++++++++++ src/run.rs | 10 ++ src/util.rs | 234 +++++++++++++++++++++++++++ test/vmpk.yml | 20 +++ 15 files changed, 1324 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/cli.rs create mode 100644 src/config/device.rs create mode 100644 src/config/event.rs create mode 100644 src/config/mod.rs create mode 100644 src/config/run.rs create mode 100644 src/event.rs create mode 100644 src/eventmap.rs create mode 100644 src/main.rs create mode 100644 src/midi/alsa.rs create mode 100644 src/midi/mod.rs create mode 100644 src/run.rs create mode 100644 src/util.rs create mode 100644 test/vmpk.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8a75507 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "rmidimap" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +#midir = "0.9" +#midir = { path = "midir" } +regex = "1.8" +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.9" +num-traits = "0.2" +num = "0.4" +lazy_static = "1.4" +clap = { version = "4.1", features = ["derive"] } + +[target.'cfg(target_os = "linux")'.dependencies] +alsa = "0.7.0" +libc = "0.2.21" diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..1bf44d8 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,11 @@ +use clap::{Parser,Subcommand}; +use std::path::PathBuf; + +/// Map MIDI signals to commands +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +pub struct Cli { + #[clap(value_parser)] + pub map_file: PathBuf, +} + diff --git a/src/config/device.rs b/src/config/device.rs new file mode 100644 index 0000000..ad19899 --- /dev/null +++ b/src/config/device.rs @@ -0,0 +1,48 @@ +use crate::config::{RunConfig,EventConfig}; +use crate::event::Event; + +use serde::Deserialize; + +#[derive(Deserialize,Debug,Clone)] +pub struct DeviceConfig { + pub name: Option, + pub regex: Option, + pub connect: Option>, + pub disconnect: Option>, + pub events: Option>, + pub multiconnect: Option, +} + +//impl DeviceConfig { +// fn connect(&self, port: &MidiInputPort) { +// let mut midi_in = MidiInput::new("midi inputs")?; +// midi_in.ignore(Ignore::None); +// let _conn_in = midi_in.connect(in_port, "midir-read-input", move |_, message, emap| { +// let event = event::Event::from(message); +// emap.run_event(&event).unwrap(); +// }, eventmap)?; +// } +//} + +impl DeviceConfig { + fn run_internal<'a, T>(&self, v: Option) -> Result, std::io::Error> + where + T: IntoIterator + { + let mut r = Vec::new(); + if let Some(ev) = v { + for e in ev { + r.push( e.run(Event::new().gen_env())? ) ; + } + } + Ok(r) + } + + pub fn run_connect(&self) -> Result, std::io::Error> { + self.run_internal(self.connect.as_ref()) + } + + pub fn run_disconnect(&self) -> Result, std::io::Error> { + self.run_internal(self.disconnect.as_ref()) + } +} \ No newline at end of file diff --git a/src/config/event.rs b/src/config/event.rs new file mode 100644 index 0000000..ddffd09 --- /dev/null +++ b/src/config/event.rs @@ -0,0 +1,26 @@ +use crate::config::RunConfig; +use crate::event::EventType; +use crate::event::Event; +use crate::util::SmartSet; + +use serde::Deserialize; + +#[derive(Deserialize,Debug,Clone)] +pub struct EventConfig { + pub run: Vec, + pub r#type: EventType, + pub channel: Option>, + pub id: Option>, + // pub channels: BTreeSet, + // pub ids: BTreeSet, + // TODO: rework for value conditions (for pitch) ? + //values: BTreeSet, + //values: Condition, +} + +impl EventConfig { + pub fn matches(&self, event: &Event) -> bool { + //TODO: value conditions don't exist yet + true + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..8cece01 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,14 @@ +pub mod event; +pub mod device; +pub mod run; + +pub type DeviceConfig = device::DeviceConfig; +pub type EventConfig = event::EventConfig; +pub type RunConfig = run::RunConfig; + +use serde::Deserialize; + +#[derive(Deserialize,Clone,Debug)] +pub struct Config { + pub devices: Vec, +} diff --git a/src/config/run.rs b/src/config/run.rs new file mode 100644 index 0000000..021f199 --- /dev/null +++ b/src/config/run.rs @@ -0,0 +1,29 @@ +use std::collections::HashMap; +use std::process::Command; + +use serde::{Serialize,Deserialize}; + +#[derive(Serialize,Deserialize,Debug,Clone)] +pub struct RunConfig { + pub args: Option>, + pub shell: Option, + pub envconf: Option>, +} + +impl RunConfig { + pub fn run(&self, env: HashMap<&str, String>) -> Result { + // TODO: proper error handling + if self.args.is_some() { + let args = self.args.as_ref().unwrap(); + Command::new(&args[0]).args(&args[1..]).envs(env).status() + } + else if self.shell.is_some() { + let args = crate::run::cross_shell(self.shell.as_ref().unwrap()); + Command::new(&args[0]).args(&args[1..]).envs(env).status() + } + else { + panic!("unexpected execution failure"); + } + } +} + diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..26c5777 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,129 @@ +use std::{collections::HashMap, time::SystemTime}; +use std::fmt::Write; + +use serde::{Serialize,Deserialize}; + +pub fn event_to_key(r#type: EventType, channel: u8, id: u8) -> u32 { + (r#type as u32)*256*256 + (channel as u32)*256 + (id as u32) +} + +#[repr(u8)] +#[derive(Serialize,Deserialize,Debug,Copy,Clone)] +pub enum EventType { + Unknown = 0b0000, + NoteOff = 0b1000, + NoteOn = 0b1001, + PolyphonicKeyPressure = 0b1010, + Controller = 0b1011, + ProgramChange = 0b1100, + ChannelPressure = 0b1101, + PitchBend = 0b1110, + System = 0b1111, +} + +impl EventType { + pub fn has_id(&self) -> bool { + match self { + EventType::Unknown | EventType::ProgramChange | EventType::PitchBend | EventType::System => false, + _ => true, + } + } + pub fn has_channel(&self) -> bool { + match self { + EventType::Unknown | EventType::ProgramChange | EventType::System => false, + _ => true, + } + } +} + +#[derive(Debug)] +pub struct Event<'a> { + pub r#type: EventType, + pub channel: u8, + pub id: u8, + pub value: u16, + pub raw: &'a [u8], + pub timestamp: Option, +} + +impl From for EventType { + fn from(v: u8) -> Self { + if ! (0b1000..=0b1111).contains(&v) { + // not in defined space: unknown + EventType::Unknown + } + else { + // safe since all valid cases are defined + unsafe { std::mem::transmute(v) } + } + } +} + +fn bytes_to_strhex(bytes: &[u8]) -> String { + let mut s = String::new(); + for &byte in bytes { + write!(&mut s, "{:X} ", byte).expect("Unable to write"); + } + s +} + +impl<'a> Event<'a> { + pub fn new() -> Self { + Event { + r#type: EventType::Unknown, + channel: 0, + id: 0, + value: 0, + raw: &[], + timestamp: None, + } + } + + pub fn key(&self) -> u32 { + event_to_key(self.r#type, self.channel, self.id) + } + pub fn gen_env(&self) -> HashMap<&str, String> { + let mut ret = HashMap::new(); + //TODO: type? + ret.insert("channel", self.channel.to_string()); + ret.insert("id", self.id.to_string()); + ret.insert("value", self.value.to_string()); + ret.insert("raw", bytes_to_strhex(self.raw)); + ret.insert("timestamp", self.timestamp.unwrap_or(SystemTime::now()).duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs_f64().to_string()); + ret + } +} + +impl<'a> From<&'a [u8]> for Event<'a> { + fn from(v: &'a [u8]) -> Event<'a> { + let channel = v[0]%16; + let event_type = EventType::from(v[0]/16); + let (id, value) = match event_type { + EventType::PitchBend => { + (0, (v[2] as u16)*256 + (v[1] as u16) ) + }, + EventType::Unknown => { + match v.len() > 0 { + true => eprintln!("warn: unknown signal type: {}", v[0]), + false => eprintln!("warn: empty signal"), + }; + (0,0) + } + EventType::System => (0,0), + EventType::PolyphonicKeyPressure | + EventType::ChannelPressure | + EventType::ProgramChange => { + todo!() + } + EventType::NoteOn | EventType::NoteOff | EventType::Controller => (v[1],(v[2] as u16)), + }; + Event { + r#type: event_type, + channel, + id, + value, + raw: v, + timestamp: None, + } + } +} diff --git a/src/eventmap.rs b/src/eventmap.rs new file mode 100644 index 0000000..baff5de --- /dev/null +++ b/src/eventmap.rs @@ -0,0 +1,112 @@ +use std::collections::HashMap; + +use crate::config::{EventConfig,DeviceConfig}; +use crate::event::{EventType,Event}; +use crate::util::SmartSet; + +use std::collections::BTreeSet; + +use lazy_static::lazy_static; + +lazy_static! { + static ref NOTE_DEFAULT_MAP: SmartSet = SmartSet { + set: BTreeSet::from((0..=127).collect::>()), + }; + static ref NOTE_DEFAULT_MAP_SIZE: usize = NOTE_DEFAULT_MAP.len(); + static ref NULL_DEFAULT_MAP: SmartSet = SmartSet { + set: BTreeSet::from([0]), + }; + static ref NULL_DEFAULT_MAP_SIZE: usize = NULL_DEFAULT_MAP.len(); + static ref CHANNEL_DEFAULT_MAP: SmartSet = SmartSet { + set: BTreeSet::from((0..=15).collect::>()), + }; + static ref CHANNEL_DEFAULT_MAP_SIZE: usize = CHANNEL_DEFAULT_MAP.len(); +} + +#[derive(Debug,Default)] +pub struct EventMap<'a> { + //TODO: vec support + pub map: HashMap>, +} + +fn event_to_key(r#type: EventType, channel: u8, id: u8) -> u32 { + (r#type as u32)*256*256 + (channel as u32)*256 + (id as u32) +} + +pub fn count_events(events: &[EventConfig]) -> usize { + events.iter().map(|x| { + let nchannel = match x.r#type.has_channel() { + true => x.channel.as_ref().map_or(*CHANNEL_DEFAULT_MAP_SIZE, |x| x.len()), + false => *CHANNEL_DEFAULT_MAP_SIZE, + }; + let nid = match x.r#type.has_id() { + true => x.id.as_ref().map_or(*NOTE_DEFAULT_MAP_SIZE, |x| x.len()), + false => *NULL_DEFAULT_MAP_SIZE, + }; + nchannel * nid + }).sum() +} + +impl<'a> EventMap<'a> { + pub fn add_events(&mut self, events: &'a [EventConfig]) { + for event in events { + for &channel in match event.r#type.has_id() { + true => event.channel.as_ref().unwrap_or(&CHANNEL_DEFAULT_MAP), + false => &CHANNEL_DEFAULT_MAP, + } { + for &id in + match event.r#type.has_id() { + true => event.id.as_ref().unwrap_or(&NOTE_DEFAULT_MAP), + false => &NULL_DEFAULT_MAP, + } { + let key = event_to_key(event.r#type, channel, id); + if let Some(v) = self.map.get_mut(&key) { + v.push(event); + } + else { + self.map.insert(key, Vec::from([event])); + } + } + } + } + } + + pub fn run_event(&self, event: &Event) -> Result<(), std::io::Error > { + let key = event_to_key(event.r#type, event.channel, event.id); + if let Some(v) = self.map.get(&key) { + for ev in v { + for r in &ev.run { + r.run(event.gen_env())?; + } + } + } + Ok(()) + } +} + +impl<'a> From<&'a [EventConfig]> for EventMap<'a> { + fn from(events: &'a [EventConfig]) -> Self { + // init hashmap with size for optimizing + let size = count_events(events); + let mut ret = EventMap { map: HashMap::with_capacity(size) }; + // insert references + ret.add_events(events); + ret + } + +} + +impl<'a> From<&'a DeviceConfig> for EventMap<'a> { + fn from(device: &'a DeviceConfig) -> Self { + // init hashmap with size for optimizing + let size = count_events(device.events.as_ref().map(|x| &x[..]).unwrap_or(&[])); + //let size = events.iter().map(|x| x.channels.len()*x.ids.len() ).sum(); + let mut ret = EventMap { map: HashMap::with_capacity(size) }; + // insert references + if let Some(x) = device.events.as_ref() { + ret.add_events(x); + } + ret + } + +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..13b4f1f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,85 @@ +use std::sync::mpsc; +use std::error::Error; +use std::path::Path; +use std::thread; +use std::sync::Mutex; +use std::rc::Rc; + +pub mod config; +pub mod run; +pub mod event; +pub mod eventmap; +pub mod midi; +pub mod util; +pub mod cli; + +use clap::Parser; + +use midi::{MidiHandler,MidiPortHandler}; +use config::{Config,DeviceConfig}; +use eventmap::EventMap; +use cli::Cli; + +fn main() { + let c = Cli::parse(); + match run(&c.map_file) { + Ok(_) => (), + Err(err) => println!("Error: {}", err) + } +} + +fn run(filepath: &Path) -> Result<(), Box> { + println!("Load file {}", filepath.to_str().unwrap()); + let dat = std::fs::read( filepath )?; + + let conf: Config = serde_yaml::from_slice(&dat)?; + let cfevmap: Vec<(&DeviceConfig, EventMap, Rc>)> = conf.devices.iter().map(|x| + (x, eventmap::EventMap::from(x), Rc::new(Mutex::new(false))) + ).collect(); + + let input = MidiHandler::new("rmidimap")?; + + thread::scope(|s| -> Result<(), Box> { + let (tdev,rdev) = mpsc::channel::(); + let mut threads: Vec<(thread::ScopedJoinHandle<'_, ()>, mpsc::Sender)> = Vec::new(); + let ports = input.ports()?; + // TODO: centralize connection process in one place + // TODO: "multiconnect=false" handling + for p in ports { + for (dev, eventmap, m) in &cfevmap { + if let Some(mut c) = input.try_connect(p.clone(), midi::PortFilter::from(*dev))? { + let (sts,srs) = mpsc::channel::(); + let nsts = sts.clone(); + let t = s.spawn( move || { + dev.run_connect().unwrap(); + c.run(eventmap, (srs,nsts)).unwrap(); + dev.run_disconnect().unwrap(); + }); + threads.push((t, sts)); + break; + } + } + } + let _event_thread = s.spawn(|| { + let mut input = MidiHandler::new("rmidimap-event-watcher").unwrap(); + input.device_events(tdev).unwrap(); + }); + loop { + let e = rdev.recv()?; + for (dev, eventmap, m) in &cfevmap { + if let Some(mut c) = input.try_connect(e.clone(), midi::PortFilter::from(*dev))? { + let (sts,srs) = mpsc::channel::(); + let nsts = sts.clone(); + let t = s.spawn( move || { + dev.run_connect().unwrap(); + c.run(eventmap, (srs,nsts)).unwrap(); + dev.run_disconnect().unwrap(); + }); + threads.push((t, sts)); + break; + } + } + } + })?; + Ok(()) +} diff --git a/src/midi/alsa.rs b/src/midi/alsa.rs new file mode 100644 index 0000000..b699881 --- /dev/null +++ b/src/midi/alsa.rs @@ -0,0 +1,366 @@ +extern crate libc; +extern crate alsa; + +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 alsa::{Seq, Direction}; +use alsa::seq::{ClientIter, PortIter, MidiEvent, PortInfo, PortSubscribe, Addr, QueueTempo, EventType, PortCap, PortType}; + +use std::error::Error; + +pub type DeviceAddr = alsa::seq::Addr; + +pub fn get_ports(s: &Seq, capability: PortCap) -> Vec { + ClientIter::new(s).flat_map(|c| PortIter::new(s, c.get_client())) + .filter(|p| p.get_capability().contains(capability)) + .collect() +} + +mod helpers { + pub fn poll(fds: &mut [super::libc::pollfd], timeout: i32) -> i32 { + unsafe { super::libc::poll(fds.as_mut_ptr(), fds.len() as super::libc::nfds_t, timeout) } + } +} + +pub struct MidiInputAlsa { + seq: Seq, + queue_id: i32, + subscription: Option, + connect_addr: Option, + stop_trigger: [i32;2], +} + +impl Drop for MidiInputAlsa { + fn drop(&mut self) { + self.close_internal(); + } +} + +impl MidiInputAlsa { + fn init_trigger(&mut self) -> Result<(), Box> { + let mut trigger_fds = [-1, -1]; + if unsafe { self::libc::pipe(trigger_fds.as_mut_ptr()) } == -1 { + todo!() + } else { + self.stop_trigger = trigger_fds; + Ok(()) + } + } + + + fn init_queue(&mut self) -> i32 { + 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") }).unwrap(); + // Set arbitrary tempo (mm=100) and resolution (240) + let qtempo = QueueTempo::empty().unwrap(); + qtempo.set_tempo(600_000); + qtempo.set_ppq(240); + self.seq.set_queue_tempo(queue_id, &qtempo).unwrap(); + let _ = self.seq.drain_output(); + } + + 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(); + } + } + + fn create_port(&mut self, port_name: &CStr, queue_id: i32) -> Result> { + let mut pinfo = PortInfo::empty().unwrap(); + // these functions are private, and the values are zeroed already by `empty()` + //pinfo.set_client(0); + //pinfo.set_port(0); + pinfo.set_capability(PortCap::WRITE | PortCap::SUBS_WRITE); + 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_name(port_name); + match self.seq.create_port(&pinfo) { + Ok(_) => Ok(pinfo.get_port()), + Err(v) => Err(Box::new(v)) + } + } + + fn close_internal(&mut self) + { + if let Some(ref subscription) = self.subscription { + let _ = self.seq.unsubscribe_port(subscription.get_sender(), subscription.get_dest()); + } + + // 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); + } + + for fd in self.stop_trigger { + if fd >= 0 { + unsafe { self::libc::close(fd) }; + } + } + } + + fn signal_stop_input_internal(stop_trigger: i32) -> Result<(), Box> { + if unsafe { self::libc::write(stop_trigger, &false as *const bool as *const _, mem::size_of::() as self::libc::size_t) } == -1 { + todo!() + } + Ok(()) + } + + fn alsa_input_handler(&mut self, callback: F, mut userdata: D) -> Result<(), Box> + where F: Fn(&Self, alsa::seq::Event, &mut D) -> bool { + // fd defitions + use self::alsa::PollDescriptors; + use self::libc::pollfd; + const INVALID_POLLFD: pollfd = pollfd { + fd: -1, + events: 0, + revents: 0, + }; + + let mut seq_input = self.seq.input(); + + // make poll fds + let poll_desc_info = (&self.seq, Some(Direction::Capture)); + let mut poll_fds = vec![INVALID_POLLFD; poll_desc_info.count()+1]; + poll_fds[0] = pollfd { + fd: self.stop_trigger[0], + events: self::libc::POLLIN, + revents: 0, + }; + poll_desc_info.fill(&mut poll_fds[1..]).unwrap(); + + loop { + if let Ok(0) = seq_input.event_input_pending(true) { + // No data pending: wait + if helpers::poll(&mut poll_fds, -1) >= 0 { + // Read stop event from triggerer + if poll_fds[0].revents & self::libc::POLLIN != 0 { + let mut pollread = false; + let _res = unsafe { self::libc::read(poll_fds[0].fd, mem::transmute(&mut pollread), mem::size_of::() as self::libc::size_t) }; + if pollread == false { + break; + } + } + } + continue; + } + // get event + let ev = seq_input.event_input()?; + + // handle disconnect event on watched port + if ev.get_type() == EventType::PortUnsubscribed { + if let Some(c) = ev.get_data::() { + if c.sender == self.connect_addr.unwrap() { + break; + } + } + } + + if (callback)(self, ev, &mut userdata) { + break; + } + } + Ok(()) + } + + fn handle_input_internal(&mut self, callback: F, userdata: D) -> Result<(), Box> + where F: Fn(Option, &[u8], &mut D) + Send { + let decoder = MidiEvent::new(0).unwrap(); + decoder.enable_running_status(false); + + let message = vec!(); + let buffer: [u8;12] = [0;12]; + let continue_sysex = false; + + self.alsa_input_handler(|_, mut ev, (message, buffer, continue_sysex, userdata)| { + if !*continue_sysex { message.clear() } + + let do_decode = match ev.get_type() { + EventType::PortSubscribed | + EventType::PortUnsubscribed | + EventType::Qframe | + EventType::Tick | + EventType::Clock | + EventType::Sensing => false, + EventType::Sysex => { + message.extend_from_slice(ev.get_ext().unwrap()); + *continue_sysex = *message.last().unwrap() != 0xF7; + false + } + _ => true + }; + + // NOTE: SysEx messages have already been "decoded" at this point! + if do_decode { + let nbytes = decoder.decode(buffer, &mut ev).unwrap(); + if nbytes > 0 { + message.extend_from_slice(&buffer[0..nbytes+1]); + } + } + + if message.len() == 0 || *continue_sysex { return false; } + + let alsa_time = ev.get_time().unwrap(); + let secs = alsa_time.as_secs(); + let nsecs = alsa_time.subsec_nanos(); + let timestamp = ( secs as u64 * 1_000_000 ) + ( nsecs as u64 / 1_000 ); + //TODO: translate to SystemTime? + (callback)(None, &message, userdata); + false + } + , (message, buffer, continue_sysex, userdata))?; + Ok(()) + } +} + +impl MidiInput for MidiInputAlsa { + fn new(client_name: &str) -> Result> { + let seq = match Seq::open(None, None, true) { + Ok(s) => s, + Err(_) => todo!(), + }; + + let c_client_name = CString::new(client_name)?; + seq.set_client_name(&c_client_name)?; + + Ok(MidiInputAlsa { + seq: seq, + queue_id: 0, + subscription: None, + connect_addr: None, + stop_trigger: [-1,-1], + }) + } + + fn close(mut self) -> Result<(), Box> { + self.close_internal(); + Ok(()) + } + + + fn ports_handle(&self) -> Vec { + get_ports(&self.seq, PortCap::READ | PortCap::SUBS_READ).iter().map(|x| { + let cinfo = self.seq.get_any_client_info(x.get_client()).unwrap(); + MidiPortHandler::ALSA( MidiPort{ + name: cinfo.get_name().unwrap().to_string()+":"+x.get_name().unwrap(), + addr: x.addr(), + }) + }).collect() + } + + fn ports(&self) -> Vec> { + get_ports(&self.seq, PortCap::READ | PortCap::SUBS_READ).iter().map(|x| { + let cinfo = self.seq.get_any_client_info(x.get_client()).unwrap(); + MidiPort { + name: cinfo.get_name().unwrap().to_string()+":"+x.get_name().unwrap(), + addr: x.addr(), + } + }).collect() + } + + fn filter_ports(&self, mut ports: Vec>, filter: PortFilter) -> Vec> { + ports.retain( + |p| { + match &filter { + PortFilter::Name(s) => p.name.find(s).is_some(), + PortFilter::Addr(MidiAddrHandler::ALSA(s)) => p.addr == *s, + _ => todo!(), + } + } + ); + ports + } + + fn connect(&mut self, port_addr: &Addr, port_name: &str) -> Result<(), Box> { + let src_pinfo = self.seq.get_any_port_info(*port_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)?; + + let sub = PortSubscribe::empty().unwrap(); + sub.set_sender(src_pinfo.addr()); + sub.set_dest(Addr { client: self.seq.client_id().unwrap(), port: vport}); + self.seq.subscribe_port(&sub)?; + self.subscription = Some(sub); + self.init_trigger()?; + self.connect_addr = Some(*port_addr); + self.start_input_queue(queue_id); + Ok(()) + } + + fn device_events(&mut self, ts: mpsc::Sender) -> Result<(), Box> { + let ports = self.ports(); + let port = self.filter_ports(ports, PortFilter::Name("System:Announce".to_string())); + self.connect(&port[0].addr, "rmidimap-alsa-announce")?; + self.alsa_input_handler(|s, ev, _|{ + // handle disconnect event on watched port + match ev.get_type() { + // EventType::PortStart | EventType::ClientStart | EventType::PortExit | EventType::ClientExit => { + EventType::PortStart => { + if let Some(a) = ev.get_data::() { + let p = s.ports(); + let pp = s.filter_ports(p, PortFilter::Addr( MidiAddrHandler::ALSA(a.clone()) )); + if pp.len() > 0 { + ts.send(MidiPortHandler::ALSA(pp[0].clone())).unwrap(); + } + }; + false + } + _ => false, + } + }, ())?; + self.close_internal(); + Ok(()) + } +} + +impl MidiInputHandler for MidiInputAlsa +{ + fn signal_stop_input(&self) -> Result<(), Box> { + if unsafe { self::libc::write(self.stop_trigger[1], &false as *const bool as *const _, mem::size_of::() as self::libc::size_t) } == -1 { + todo!() + } + Ok(()) + } + + fn handle_input(&mut self, callback: F, (rs, ts): (mpsc::Receiver, mpsc::Sender), userdata: D) -> Result<(), Box> + where + F: Fn(Option, &[u8], &mut D) + Send, + D: Send, + { + thread::scope( |sc| -> Result<(), Box> { + let stop_trigger = self.stop_trigger[1]; + let t = sc.spawn(move || { + let userdata = userdata; + self.handle_input_internal(callback, userdata).unwrap(); + ts.send(false).unwrap(); + }); + match rs.recv()? { + true => Self::signal_stop_input_internal(stop_trigger)?, + false => () + }; + t.join().unwrap(); + Ok(()) + }) + } +} + diff --git a/src/midi/mod.rs b/src/midi/mod.rs new file mode 100644 index 0000000..94f224b --- /dev/null +++ b/src/midi/mod.rs @@ -0,0 +1,217 @@ +pub mod alsa; + +use crate::midi::alsa::MidiInputAlsa; + +extern crate libc; + +use crate::config::DeviceConfig; +use crate::eventmap::EventMap; +use crate::event::Event; + +use std::error::Error; +use std::time::SystemTime; +use std::sync::mpsc; + +#[derive(Eq,PartialEq,Debug,Clone)] +pub struct MidiPort{ + pub name: String, + pub addr: T, +} + +#[derive(Debug,Clone)] +pub enum PortFilter { + Name(String), + Regex(String), + 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), +} + +pub enum MidiHandler { + ALSA(MidiInputAlsa), +} + +impl From for MidiAddrHandler { + fn from(a: MidiPortHandler) -> Self { + match a { + MidiPortHandler::ALSA(p) => MidiAddrHandler::ALSA(p.addr), + } + } +} + +impl From<&DeviceConfig> for PortFilter { + fn from(conf: &DeviceConfig) -> Self { + if conf.name.is_some() { + PortFilter::Name(conf.name.clone().unwrap_or(String::new())) + } + else { + todo!() + } + } +} + +mod helper { +} + + +pub trait MidiInput { + fn new(client_name: &str) -> Result> + where Self: Sized; + + fn close(self) -> Result<(), Box>; + + fn ports(&self) -> Vec>; + fn ports_handle(&self) -> Vec; + + fn filter_ports<'a>(&self, ports: Vec>, filter: PortFilter) -> Vec>; + + fn connect(&mut self, port_addr: &T, port_name: &str) -> Result<(), Box>; + + fn device_events(&mut self, ts: mpsc::Sender) -> Result<(), Box>; +} + +pub trait MidiInputHandler { + fn signal_stop_input(&self) -> Result<(), Box>; + + fn handle_input(&mut self, callback: F, rts: (mpsc::Receiver, mpsc::Sender), userdata: D) -> Result<(), Box> + where + F: Fn(Option, &[u8], &mut D) + Send, + 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)?; + println!("Connect to device {}", port.name); + match &mut h { + MidiHandler::$handler(v) => { + v.connect(&port.addr, "rmidimap-handler").unwrap(); + 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), + )* + } + }; +} + +impl MidiHandler { + pub fn new(name: &str) -> Result> { + Self::new_with_driver(name, MidiHandlerDriver::ALSA) + } + + pub fn new_with_driver(name: &str, driver: MidiHandlerDriver) -> Result> { + match driver { + MidiHandlerDriver::ALSA => Ok(MidiHandler::ALSA(MidiInputAlsa::new(name)?)), + } + } + + pub fn ports(&self) -> Result, Box> { + handler_fcall!{ + self, handle_port_list ,(), + ALSA + } + } + + pub fn try_connect(&self, addr: MidiPortHandler, filter: PortFilter) -> Result, Box> { + let r: Result, Box> = handler_try_connect!{ + self, filter, addr, + ALSA + }; + r + } + + pub fn run(&mut self, eventmap: &EventMap, (rs,ts): (mpsc::Receiver, mpsc::Sender)) -> Result<(), Box> { + handler_fcall!{ + self, handle_inputport ,(eventmap,(rs,ts)), + ALSA + } + } + + pub fn stop(&self) -> Result<(), Box> { + handler_fcall!{ + self, handle_signal_stop, (), + ALSA + } + } + + pub fn device_events(&mut self, ts: mpsc::Sender) -> Result<(), Box> { + handler_fcall!{ + self, device_events, ts, + ALSA + } + } +} + +fn handle_port_list(input: &T, _: ()) -> Result, Box> +where T: MidiInput +{ + Ok(input.ports_handle()) +} + +fn handle_inputport(input: &mut T, (eventmap, (rs, ts)): (&EventMap, (mpsc::Receiver, mpsc::Sender))) -> Result<(), Box> +where T: MidiInputHandler +{ + input.handle_input(|t,m,_| { + let mut event = Event::from(m); + event.timestamp = t; + eventmap.run_event(&event).unwrap(); + }, (rs,ts), ())?; + Ok(()) +} + +fn handle_signal_stop(input: &T, _: ()) -> Result<(), Box> +where T: MidiInputHandler +{ + input.signal_stop_input() +} + +fn device_events(input: &mut T, ts: mpsc::Sender) -> Result<(), Box> +where T: MidiInput +{ + input.device_events(ts) +} + diff --git a/src/run.rs b/src/run.rs new file mode 100644 index 0000000..6222841 --- /dev/null +++ b/src/run.rs @@ -0,0 +1,10 @@ +pub fn cross_shell(cmd: &str) -> Vec { + if cfg!(target_os = "windows") { + vec!("cmd", "/C", cmd) + } else { + vec!("sh", "-c", cmd) + } + .iter().map( + |x| x.to_string() + ).collect() +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..80ae699 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,234 @@ + +use std::collections::BTreeSet; + +use std::str::FromStr; + +use num::{Num,NumCast}; + +pub fn parse_int_set(s: &str) -> Result, ::Err> +where + T: Num+Ord+Copy + std::str::FromStr + std::ops::AddAssign, +{ + + let mut r: BTreeSet = BTreeSet::new(); + let parts: Vec<&str> = s.split(',').collect(); + for p in parts { + if p.len() > 0 { + if let Some(sep) = p.find('-') { + let (p1,p2) = (&s[..sep], &s[sep+1..] ); + let (low,high): (T,T) = ( p1.parse()?, p2.parse()? ); + let (mut low,high) = match low <= high { + true => (low,high), + false => (high,low), + }; + while low <= high { + r.insert(low); + low += T::one(); + } + } + else { + r.insert(p.parse()?); + } + } + } + Ok(r) +} + +#[derive(Debug,Clone)] +pub struct SmartSet +where + T: Num+Ord+Copy + std::str::FromStr + std::ops::AddAssign, +{ + pub set: BTreeSet +} + +impl SmartSet +where + T: Num+Ord+Copy + std::str::FromStr + std::ops::AddAssign, +{ + pub fn new() -> Self { + Self { + set: BTreeSet::new(), + } + } + + pub fn len(&self) -> usize { + self.set.len() + } +} + +// impl From for SmartSet +// where +// T: Num+Ord+Copy + std::str::FromStr + std::ops::AddAssign, +// { +// fn from(i: T) -> Self { +// let mut r = SmartSet::new(); +// r.set.insert(i); +// r +// } +// } + +impl From for SmartSet +where + T: Num+Ord+Copy+NumCast + std::str::FromStr + std::ops::AddAssign, + U: Num+Ord+Copy+num::ToPrimitive + std::str::FromStr + std::ops::AddAssign, +{ + fn from(i: U) -> Self { + let mut r = SmartSet::::new(); + r.set.insert(num::NumCast::from(i).unwrap()); + r + } +} + +// impl From<&[U]> for SmartSet +// where +// T: Num+Ord+Copy+NumCast + std::str::FromStr + std::ops::AddAssign, +// U: Num+Ord+Copy+num::ToPrimitive + std::str::FromStr + std::ops::AddAssign, +// { +// fn from(i: &[U]) -> Self { +// let mut r = SmartSet::::new(); +// for v in i { +// r.set.insert(num::NumCast::from(v).unwrap()); +// } +// r +// } +// } + +impl FromStr for SmartSet +where + T: Num+Ord+Copy + std::str::FromStr + std::ops::AddAssign, +{ + type Err = ::Err; + + fn from_str(s: &str) -> Result { + Ok(SmartSet { + set: parse_int_set(s)?, + }) + } +} + +impl IntoIterator for SmartSet +where + T: Num+Ord+Copy + std::str::FromStr + std::ops::AddAssign, +{ + type Item = T; + type IntoIter = std::collections::btree_set::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.set.into_iter() + } +} + +impl<'a, T> IntoIterator for &'a SmartSet +where + T: Num+Ord+Copy + std::str::FromStr + std::ops::AddAssign, +{ + type Item = &'a T; + type IntoIter = std::collections::btree_set::Iter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.set.iter() + } +} + +use std::marker::PhantomData; +use std::fmt; +use serde::de::{self,Deserialize, Deserializer, Visitor}; + +struct SmartSetVisitor +where + T: Num+Ord+Copy + std::str::FromStr + std::ops::AddAssign, +{ + marker: PhantomData SmartSet> +} + + +impl SmartSetVisitor +where + T: Num+Ord+Copy + std::str::FromStr + std::ops::AddAssign, +{ + fn new() -> Self { + Self { + marker: PhantomData + } + } +} + +macro_rules! visit_from { + ( $( ($fct:ident, $type:ty) ),+ $(,)?) => { + $( + fn $fct(self, value: $type) -> Result + where + E: de::Error, + { + Ok(SmartSet::from(value)) + } + )* + }; +} + +impl<'de, T> Visitor<'de> for SmartSetVisitor +where + T: Num+Ord+Copy+NumCast + Deserialize<'de> + std::str::FromStr + std::ops::AddAssign + std::fmt::Debug, + ::Err: std::fmt::Display, +{ + type Value = SmartSet; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a set of integer") + } + + visit_from!{ + (visit_i8, i8), + (visit_i16, i16), + (visit_i32, i32), + (visit_i64, i64), + (visit_u8, u8), + (visit_u16, u16), + (visit_u32, u32), + (visit_u64, u64), + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + SmartSet::from_str(value).map_err(serde::de::Error::custom) + } + + fn visit_seq(self, mut seq: Z) -> Result + where + Z: serde::de::SeqAccess<'de>, + { + let _len = seq.size_hint(); + let mut r: SmartSet = SmartSet::new(); + + loop { + if let Ok(Some(value)) = seq.next_element::() { + r.set.extend(&SmartSet::::from_str(&value).map_err(serde::de::Error::custom)?.set); + } + else if let Some(value) = seq.next_element()? { + r.set.insert(value); + } + else { + break; + } + } + + Ok(r) + } +} + +// This is the trait that informs Serde how to deserialize MyMap. +impl<'de, T> Deserialize<'de> for SmartSet +where + T: Num+Ord+Copy+NumCast + Deserialize<'de> + std::str::FromStr + std::ops::AddAssign + std::fmt::Debug, + ::Err: std::fmt::Display, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(SmartSetVisitor::::new()) + } +} diff --git a/test/vmpk.yml b/test/vmpk.yml new file mode 100644 index 0000000..7f4a775 --- /dev/null +++ b/test/vmpk.yml @@ -0,0 +1,20 @@ +devices: + - name: 'VMPK Output:out' + multiconnect: false + connect: + - args: [ "sh", "-c", "echo Hello world!" ] + disconnect: + - args: [ "sh", "-c", "echo Bye!" ] + events: + - type: Controller + run: + - args: [ "sh", "-c", "echo [$channel] Controller $id $value" ] + - type: NoteOff + run: + - args: [ "sh", "-c", "echo [$channel] NoteOff $id" ] + - type: NoteOn + run: + - args: [ "sh", "-c", "echo [$channel] NoteOn $id $value" ] + - type: PitchBend + run: + - args: [ "sh", "-c", "echo [$channel] PitchBend $value" ]