-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor backends, adding native OSC support.
- Loading branch information
Showing
21 changed files
with
2,128 additions
and
930 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
#[macro_use] | ||
extern crate rmididings; | ||
use rmididings::*; | ||
use rosc::OscType as o; | ||
|
||
/// Example patch that works with livedings. | ||
/// | ||
/// Note that in the future there will probably came an implementation that you can | ||
/// plug into your RMididings program, either as a filter for use e.g. in the control | ||
/// patch, or as a hook. | ||
/// | ||
/// You can do scene switches from livedings, but scene switches happening elsewhere | ||
/// within a patch are not communicated to livedings. | ||
fn main() -> Result<(), Box<dyn std::error::Error>> { | ||
let mut md = RMididings::new()?; | ||
|
||
md.config(ConfigArguments { | ||
client_name: "RMididings Demo", | ||
in_ports: &[ | ||
["input", "Virtual Keyboard:Virtual Keyboard"], | ||
["osc.udp://localhost:56418", ""], | ||
], | ||
out_ports: &[ | ||
["output", "midisnoop:MIDI Input"], | ||
["osc.udp:", "localhost:56419"], | ||
], | ||
..ConfigArguments::default() | ||
})?; | ||
|
||
md.run(RunArguments { | ||
scenes: &[ | ||
&Scene { // 1 | ||
name: "Run", | ||
patch: &Pass(), | ||
..Scene::default() | ||
}, | ||
&Scene { // 2 | ||
name: "Pause", | ||
patch: &Discard(), | ||
..Scene::default() | ||
} | ||
], | ||
control: &Chain!(TypeFilter!(Osc), OscStripPrefix("/mididings"), Fork!( | ||
Chain!(OscAddrFilter("/query"), Discard(), | ||
Osc!("/data_offset", o::Int(1)), | ||
Osc!("/begin_scenes"), | ||
Osc!("/add_scene", o::Int(1), o::String("Run".to_string())), | ||
Osc!("/add_scene", o::Int(2), o::String("Pause".to_string())), | ||
Osc!("/end_scenes"), | ||
OscAddPrefix("/mididings") | ||
), | ||
Chain!(OscAddrFilter("/switch_scene"), ProcessOsc!(o::Int, |s| SceneSwitch(s as u8))), | ||
Chain!(OscAddrFilter("/next_scene"), SceneSwitchOffset(1)), | ||
Chain!(OscAddrFilter("/prev_scene"), SceneSwitchOffset(-1)), | ||
Chain!(OscAddrFilter("/prev_subscene"), SubSceneSwitchOffset(-1)), | ||
Chain!(OscAddrFilter("/next_subscene"), SubSceneSwitchOffset(1)), | ||
Chain!(OscAddrFilter("/panic"), Panic()), | ||
Chain!(OscAddrFilter("/quit"), Quit()) | ||
)), | ||
// We can't notify on scene switch yet. | ||
// post: &Fork!( | ||
// Pass(), | ||
// Chain!(TypeFilter(SceneSwitch), ... Process(|ev| Osc!("/mididings/current_scene", scene, subscene))) | ||
// ), | ||
..RunArguments::default() | ||
})?; | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
use std::error::Error; | ||
use std::vec::Vec; | ||
use std::collections::HashMap; | ||
|
||
extern crate alsa; | ||
use alsa::seq; | ||
use alsa::PollDescriptors; | ||
use std::ffi::CString; | ||
|
||
use super::super::proc::event::*; | ||
use super::super::proc::EventStream; | ||
use super::backend::{Backend, PortNum}; | ||
|
||
/// ALSA sequencer MIDI backend. | ||
pub struct AlsaBackend { | ||
alsaseq: alsa::Seq, | ||
in_ports: HashMap<PortNum, i32>, | ||
out_ports: HashMap<PortNum, i32>, | ||
} | ||
|
||
impl AlsaBackend { | ||
pub fn new() -> Result<Self, Box<dyn Error>> { | ||
Ok(Self { | ||
alsaseq: alsa::Seq::open(None, None, true)?, | ||
in_ports: HashMap::new(), | ||
out_ports: HashMap::new(), | ||
}) | ||
} | ||
|
||
fn _create_in_port(&mut self, backend_port: PortNum, name: &str) -> Result<bool, Box<dyn Error>> { | ||
let alsaseq_port = self.alsaseq.create_simple_port( | ||
&CString::new(name).unwrap(), | ||
seq::PortCap::WRITE | seq::PortCap::SUBS_WRITE, | ||
seq::PortType::MIDI_GENERIC | seq::PortType::APPLICATION | ||
)?; | ||
self.in_ports.insert(backend_port, alsaseq_port); | ||
Ok(true) | ||
} | ||
|
||
fn _create_out_port(&mut self, backend_port: PortNum, name: &str) -> Result<bool, Box<dyn Error>> { | ||
let alsaseq_port = self.alsaseq.create_simple_port( | ||
&CString::new(name).unwrap(), | ||
seq::PortCap::READ | seq::PortCap::SUBS_READ, | ||
seq::PortType::MIDI_GENERIC | seq::PortType::APPLICATION | ||
)?; | ||
self.out_ports.insert(backend_port, alsaseq_port); | ||
Ok(true) | ||
} | ||
} | ||
|
||
impl Backend<'_> for AlsaBackend { | ||
fn set_client_name(&mut self, name: &str) -> Result<(), Box<dyn Error>> { | ||
Ok(self.alsaseq.set_client_name(&CString::new(name).unwrap())?) | ||
} | ||
|
||
fn create_in_port(&mut self, backend_port: PortNum, name: &str) -> Result<bool, Box<dyn Error>> { | ||
if let Some((backend_name, port_name)) = name.split_once(':') { | ||
if backend_name != "alsa" { return Ok(false); } | ||
self._create_in_port(backend_port, port_name) | ||
} else { | ||
self._create_in_port(backend_port, name) | ||
} | ||
} | ||
|
||
fn create_out_port(&mut self, backend_port: PortNum, name: &str) -> Result<bool, Box<dyn Error>> { | ||
if let Some((backend_name, port_name)) = name.split_once(':') { | ||
if backend_name != "alsa" { return Ok(false); } | ||
self._create_out_port(backend_port, port_name) | ||
} else { | ||
self._create_out_port(backend_port, name) | ||
} | ||
} | ||
|
||
fn connect_in_port(&mut self, backend_port: PortNum, name: &str) -> Result<bool, Box<dyn Error>> { | ||
if let Some(alsaseq_port) = self.in_ports.get(&backend_port) { | ||
if let Some((client_name, port_name)) = name.split_once(':') { | ||
if let Some(connect_port) = self.find_alsaseq_port(client_name, port_name, seq::PortCap::READ | seq::PortCap::SUBS_READ)? { | ||
let subs = seq::PortSubscribe::empty()?; | ||
subs.set_sender(seq::Addr { client: connect_port.get_client(), port: connect_port.get_port() }); | ||
subs.set_dest(seq::Addr { client: self.alsaseq.client_id()?, port: *alsaseq_port }); | ||
self.alsaseq.subscribe_port(&subs)?; | ||
return Ok(true); | ||
} | ||
} | ||
} | ||
return Ok(false); | ||
} | ||
|
||
fn connect_out_port(&mut self, backend_port: PortNum, name: &str) -> Result<bool, Box<dyn Error>> { | ||
if let Some(alsaseq_port) = self.out_ports.get(&backend_port) { | ||
if let Some((client_name, port_name)) = name.split_once(':') { | ||
if let Some(connect_port) = self.find_alsaseq_port(client_name, port_name, seq::PortCap::WRITE | seq::PortCap::SUBS_WRITE)? { | ||
let subs = seq::PortSubscribe::empty()?; | ||
subs.set_sender(seq::Addr { client: self.alsaseq.client_id()?, port: *alsaseq_port }); | ||
subs.set_dest(seq::Addr { client: connect_port.get_client(), port: connect_port.get_port() }); | ||
self.alsaseq.subscribe_port(&subs)?; | ||
return Ok(true); | ||
} | ||
} | ||
} | ||
return Ok(false); | ||
} | ||
|
||
fn get_pollfds(&mut self) -> Result<Vec<libc::pollfd>, Box<dyn Error>> { | ||
Ok((&self.alsaseq, Some(alsa::Direction::Capture)).get()?) | ||
} | ||
|
||
fn run<'evs: 'run, 'run>(&'run mut self) -> Result<EventStream<'evs>, Box<dyn Error>> { | ||
let mut alsaseq_input = self.alsaseq.input(); | ||
match alsaseq_input.event_input_pending(true) { | ||
Ok(count) if count > 0 => { | ||
Ok(EventStream::from(self.alsaseq_event_to_event(&alsaseq_input.event_input()?)?)) | ||
}, | ||
Ok(_) => Ok(EventStream::empty()), | ||
// Occasionally, this function may return -ENOSPC error. This means that the input FIFO of | ||
// sequencer overran, and some events are lost. Once this error is returned, the input FIFO | ||
// is cleared automatically. | ||
// TODO emit a warning? | ||
Err(e) if e.nix_error() == alsa::nix::Error::Sys(alsa::nix::errno::Errno::ENOSPC) => { | ||
println!("Buffer overrun"); | ||
Ok(EventStream::empty()) | ||
}, | ||
Err(e) => Err(Box::new(e)), | ||
} | ||
} | ||
|
||
fn output_event(&mut self, ev: &Event) -> Result<u32, Box<dyn Error>> { | ||
// TODO self.out_ports bounds checking (!) | ||
match ev { | ||
Event::NoteOn(ev) => { | ||
let mut alsaev = seq::Event::new(seq::EventType::Noteon, &seq::EvNote { | ||
// TODO figure out what to do with duration and off_velocity | ||
channel: ev.channel, note: ev.note, velocity: ev.velocity, duration: 0, off_velocity: 0 | ||
}); | ||
Ok(self.output_alsaseq_event(&ev.port, &mut alsaev)?) | ||
}, | ||
Event::NoteOff(ev) => { | ||
let mut alsaev = seq::Event::new(seq::EventType::Noteoff, &seq::EvNote { | ||
// TODO figure out what to do with duration and off_velocity | ||
channel: ev.channel, note: ev.note, velocity: 0, duration: 0, off_velocity: 0 | ||
}); | ||
Ok(self.output_alsaseq_event(&ev.port, &mut alsaev)?) | ||
}, | ||
Event::Ctrl(ev) => { | ||
let mut alsaev = seq::Event::new(seq::EventType::Controller, &seq::EvCtrl { | ||
channel: ev.channel, param: ev.ctrl, value: ev.value | ||
}); | ||
Ok(self.output_alsaseq_event(&ev.port, &mut alsaev)?) | ||
}, | ||
Event::SysEx(ev) => { | ||
let mut me = seq::MidiEvent::new(ev.data.len() as u32)?; | ||
let (_, me_enc) = me.encode(ev.data)?; | ||
let mut alsaev = me_enc.unwrap(); | ||
Ok(self.output_alsaseq_event(&ev.port, &mut alsaev)?) | ||
}, | ||
_ => { | ||
Ok(0) | ||
}, | ||
} | ||
} | ||
} | ||
|
||
impl AlsaBackend { | ||
fn alsaseq_event_to_event<'a>(&self, alsaev: &seq::Event) -> Result<Option<Event<'a>>, Box<dyn Error>> { | ||
// map alsa port to our own port (index in self.in_ports), fallback to port 0 | ||
let alsaseq_port = alsaev.get_dest().port; | ||
if let Some((port, _)) = self.in_ports.iter().find(|(_, as_p)| **as_p == alsaseq_port) { | ||
// convert alsaseq event to our own kind of event | ||
if let Some(e) = alsaev.get_data::<seq::EvNote>() { | ||
if alsaev.get_type() == seq::EventType::Noteon { | ||
return Ok(Some(NoteOnEvent(*port, e.channel, e.note, e.velocity))); | ||
} else { | ||
return Ok(Some(NoteOffEvent(*port, e.channel, e.note))); | ||
} | ||
} else if let Some(e) = alsaev.get_data::<seq::EvCtrl>() { | ||
return Ok(Some(CtrlEvent(*port, e.channel, e.param, e.value))); | ||
} | ||
} | ||
return Ok(None); | ||
} | ||
|
||
fn find_alsaseq_port(&self, client_name: &str, port_name: &str, caps: seq::PortCap) -> Result<Option<alsa::seq::PortInfo>, Box<dyn Error>> { | ||
for client in seq::ClientIter::new(&self.alsaseq) { | ||
if client.get_name()? != client_name { continue; } | ||
for port in seq::PortIter::new(&self.alsaseq, client.get_client()) { | ||
let port_caps = port.get_capability(); | ||
if !port.get_type().contains(seq::PortType::MIDI_GENERIC) { continue; } | ||
if !port_caps.contains(caps) { continue; } | ||
if port.get_name()? != port_name { continue; } | ||
return Ok(Some(port)); | ||
} | ||
} | ||
Ok(None) | ||
} | ||
|
||
fn output_alsaseq_event(&self, backend_port: &PortNum, ev: &mut alsa::seq::Event) -> Result<u32, Box<dyn Error>> { | ||
if let Some(alsaseq_port) = self.out_ports.get(backend_port) { | ||
ev.set_source(*alsaseq_port); | ||
ev.set_subs(); | ||
ev.set_direct(); | ||
Ok(self.alsaseq.event_output_direct(ev)?) | ||
} else { | ||
Ok(0) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
use std::error::Error; | ||
|
||
use super::super::proc::{Event, EventStream}; | ||
|
||
pub type PortNum = usize; | ||
|
||
/// MIDI Backend implementation. | ||
pub trait Backend<'a> { | ||
fn set_client_name(&mut self, name: &str) -> Result<(), Box<dyn Error>>; | ||
|
||
fn create_in_port(&mut self, port: PortNum, name: &'a str) -> Result<bool, Box<dyn Error>>; | ||
|
||
fn create_out_port(&mut self, port: PortNum, name: &'a str) -> Result<bool, Box<dyn Error>>; | ||
|
||
fn connect_in_port(&mut self, port: PortNum, name: &'a str) -> Result<bool, Box<dyn Error>>; | ||
|
||
fn connect_out_port(&mut self, port: PortNum, name: &'a str) -> Result<bool, Box<dyn Error>>; | ||
|
||
fn get_pollfds(&mut self) -> Result<Vec<libc::pollfd>, Box<dyn Error>>; | ||
|
||
fn run<'evs: 'run, 'run>(&'run mut self) -> Result<EventStream<'evs>, Box<dyn Error>>; | ||
|
||
fn output_event(&mut self, ev: &Event) -> Result<u32, Box<dyn Error>>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
extern crate libc; | ||
|
||
mod backend; | ||
pub use self::backend::{Backend, PortNum}; | ||
|
||
mod null; | ||
pub use self::null::NullBackend; | ||
|
||
#[cfg(feature = "alsa")] | ||
mod alsa; | ||
#[cfg(feature = "alsa")] | ||
pub use self::alsa::AlsaBackend; | ||
|
||
#[cfg(feature = "osc")] | ||
mod osc; | ||
#[cfg(feature = "osc")] | ||
pub use self::osc::OscBackend; |
Oops, something went wrong.