Skip to content

Commit

Permalink
Add Process, fix ProcessOsc, add Carla OSC example
Browse files Browse the repository at this point in the history
  • Loading branch information
wvengen committed Jun 27, 2021
1 parent 18065cc commit 1897401
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 50 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ It is in somewhat early development, and many things are not available. What is:

Some missing things can be implemented, but there are some limitations using Rust,
e.g. syntax can differ, and not all variations of argument types to filters etc.
are supported. We'll see.
are supported. Sometimes the syntax is a bit messy (e.g. with `Process`). We'll see
what can be improved.

## Running

Expand Down
104 changes: 104 additions & 0 deletions examples/carla_simple.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#![allow(non_snake_case)]
#[macro_use]
extern crate rmididings;
use rmididings::*;
use rmididings::osc::OscType as o;

/// Example patch that shows how to interact with the Carla audio-plugin host.
///
/// The first parameter of the first plugin in Carla is synchronized with
/// MIDI controller 1 (modulation wheel). Changes in Carla are sent to the MIDI
/// device, changes on the MIDI device are propagated to Carla.
///
/// Make sure Carla is running (including the engine), and OSC is enabled on
/// the default port 22752.
///
/// Note that this uses a Carla-internal OSC protocol, which could change between
/// Carla versions. It was tested with Carla version 2.3.0.
///
/// There are many loose ends, still. One thing is that no exit patch is run when
/// Ctrl-C is pressed, making it impossible to run this program again. So after
/// running this, you may want to to run manually:
///
/// oscsend osc.tcp://localhost:22752 /unregister s 127.0.0.1
///
/// Another way is to stop and start the Carla engine.
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:127.0.0.1:22852", ""], // Listen both on UDP and TCP.
],
out_ports: &[
["output", "midisnoop:MIDI Input"],
["osc.tcp:", "127.0.0.1:22752"], // Send on TCP only, to the default Carla OSC port.
],
..ConfigArguments::default()
})?;

md.run(RunArguments {
patch: &Fork!(
Chain!(
CarlaFilter(),
OscAddrFilter("/cb"),
ProcessOsc!(
o::Int, o::Int, o::Int, o::Int, o::Int, o::Float, o::String,
|action: &i32, plugin_id: &i32, ival: &i32, _, _, fval: &f32, _| {
// Only react to value changed callback for the first plugin and the first parameter.
if *action == 5 && *plugin_id == 0 && *ival == 0 {
Chain!(Ctrl(1, *fval as i32), Synth())
} else {
Chain!(Discard())
}
}
)
),
Chain!(
SynthFilter(),
TypeFilter!(Ctrl),
CtrlFilter(1),
Process!(|ev: &Event| {
match ev {
Event::Ctrl(ev) => Box::new(CarlaSetParamValue(0, 0, ev.value as f32)),
_ => Box::new(Discard()),
}
})
),
Init!(Chain!(Osc!("/register", o::String("osc.tcp://127.0.0.1:22852/Carla".to_string())), CarlaPort())),
Exit!(Chain!(Osc!("/unregister", o::String("127.0.0.1".to_string())), CarlaPort()))
),
..RunArguments::default()
})?;

Ok(())
}


fn SynthFilter() -> PortFilter {
PortFilter(1)
}

fn Synth() -> Port {
Port(1)
}

// For accepted messages, see: https://github.com/falkTX/Carla/blob/main/source/backend/engine/CarlaEngineOscHandlers.cpp
// Note that the format can change between Carla releases, so take care!
fn CarlaSetParamValue(plugin_id: i32, param_id: i32, value: f32) -> FilterChain<'static> {
Chain!(Osc!(format!("/{}/set_parameter_value", plugin_id), o::Int(param_id), o::Float(value)), Carla())
}

fn CarlaFilter() -> FilterChain<'static> {
Chain!(PortFilter(2), OscStripPrefix("/Carla"), Not!(OscAddrFilter("/runtime")))
}

fn Carla() -> FilterChain<'static> {
Chain!(OscAddPrefix("/Carla"), CarlaPort())
}

fn CarlaPort() -> Port {
Port(2)
}
2 changes: 1 addition & 1 deletion examples/osc_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
),
OscAddPrefix("/mididings")
),
Chain!(OscAddrFilter("/switch_scene"), ProcessOsc!(o::Int, |s| SceneSwitch(s as u8))),
Chain!(OscAddrFilter("/switch_scene"), ProcessOsc!(o::Int, |s: &i32| SceneSwitch(*s as u8))),
Chain!(OscAddrFilter("/next_scene"), SceneSwitchOffset(1)),
Chain!(OscAddrFilter("/prev_scene"), SceneSwitchOffset(-1)),
Chain!(OscAddrFilter("/prev_subscene"), SubSceneSwitchOffset(-1)),
Expand Down
29 changes: 17 additions & 12 deletions src/backend/osc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::collections::HashMap;

pub extern crate rosc;

use super::super::proc::event::{Event, OscEventImpl};
use super::super::proc::event::{Event, OscEvent, OscEventImpl};
use super::super::proc::EventStream;
use super::backend::{Backend, PortNum};

Expand Down Expand Up @@ -171,10 +171,10 @@ impl<'a> Backend<'a> for OscBackend<'a> {
fn run<'evs: 'run, 'run>(&'run mut self) -> Result<EventStream<'evs>, Box<dyn Error>> {
let mut evs = EventStream::empty();

for port in self.in_ports.values_mut() {
for (backend_port, port) in self.in_ports.iter_mut() {
if let Some(udp_listener) = &port.udp_listener {
if let Some(data) = read_udp_data(&udp_listener, &mut self.buf)? {
evs.extend(decode_data(data).into_iter().map(|o| Event::from(o)));
evs.extend(decode_data(data).into_iter().map(|o| build_event(o, *backend_port)));
}
}

Expand All @@ -195,19 +195,20 @@ impl<'a> Backend<'a> for OscBackend<'a> {

for tcp_stream in port.tcp_listen_streams.iter_mut() {
if let Some(data) = read_tcp_data(tcp_stream, &mut self.buf)? {
evs.extend(decode_data_tcp(data).into_iter().map(|o| Event::from(o)));
evs.extend(decode_data_tcp(data).into_iter().map(|o| build_event(o, *backend_port)));
}
}
}

// This isn't really used in practice, as OSC doesn't keep open connections AFAIK.
for port in self.out_ports.values_mut() {
if let Some(tcp_stream) = &mut port.tcp_connect_stream {
if let Some(data) = read_tcp_data(tcp_stream, &mut self.buf)? {
evs.extend(decode_data_tcp(data).into_iter().map(|o| Event::from(o)));
}
}
}
// As far as I've seen, OSC doesn't respond on connections opened by us.
// It would also be a bit of a problem, as we have no corresponding input port to associate this with.
// for (backend_port, port) in self.out_ports.iter_mut() {
// if let Some(tcp_stream) = &mut port.tcp_connect_stream {
// if let Some(data) = read_tcp_data(tcp_stream, &mut self.buf)? {
// evs.extend(decode_data_tcp(data).into_iter().map(|o| build_event(o, ???)));
// }
// }
// }

Ok(evs)
}
Expand Down Expand Up @@ -332,4 +333,8 @@ fn get_messages_from_packet(packet: rosc::OscPacket) -> Vec::<rosc::OscMessage>
bundle.content.into_iter().map(|p| get_messages_from_packet(p)).flatten().collect()
},
}
}

fn build_event<'a>(message: rosc::OscMessage, port: PortNum) -> Event<'a> {
OscEvent(port, message.addr, message.args)
}
66 changes: 53 additions & 13 deletions src/proc/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#![allow(non_snake_case)]
#![macro_use]
use std::collections::HashMap;

pub mod event;
pub mod event_stream;
pub mod filter_chain;
Expand Down Expand Up @@ -1256,24 +1258,62 @@ macro_rules! Not {
};
}

#[doc(hidden)]
pub struct _Process(pub Box<dyn Fn(&Event) -> Box<dyn FilterTrait>>);
#[doc(hidden)]
impl FilterTrait for _Process {
fn run(&self, evs: &mut EventStream) {
let mut results: HashMap<usize, EventStream> = HashMap::new();

// First gather all resulting EventStreams from the function invocations.
for (i, ev) in evs.iter().enumerate() {
let mut evs = EventStream::from(ev);
self.0(ev).run(&mut evs);
results.insert(i, evs);
}

// Then replace the events by their results.
for (i, r_evs) in results {
evs.splice(i..i+1, r_evs);
}

evs.dedup();
}
}

/// Process the incoming event using a custom function, returning a patch.
///
/// Any other processing will be stalled until function returns, so this should only be used with
/// functions that don’t block.
// pub struct Process<'a>(dyn Fn(&Event) -> FilterChain<'a>);
// impl FilterTrait for Process<'_> {
// fn run(&self, evs: &mut EventStream) {
// let filters: Vec<Box<dyn FilterTrait>> = vec![];

// for ev in evs.iter() {
// filters.push(Box::new(self.0(&ev)));
// }
///
/// # Examples
///
/// ```
/// # #[macro_use] extern crate rmididings;
/// # use rmididings::proc::*;
///
/// # fn main() {
/// let filter = Process!(|ev: &Event| -> Box<dyn FilterTrait> {
/// match ev {
/// Event::NoteOff(ev) => Box::new(NoteOn(ev.note + 1, 40)),
/// _ => Box::new(Pass()),
/// }
/// });
///
/// let mut evs = EventStream::from(NoteOffEvent(0,0,60));
/// filter.run(&mut evs);
/// assert_eq!(evs, NoteOnEvent(0,0,61,40));
/// # }
/// ```
#[macro_export]
macro_rules! Process {
( $f:expr ) => { _Process(Box::new($f)) };
}

// for f in filters {
// f.run(evs);
// }
// }
// }
#[macro_export]
macro_rules! ProcessCtrl {
( $f:expr ) => { _Process(Box::new($f)) };
}

#[cfg(feature = "osc")]
pub mod osc;
Expand Down
Loading

0 comments on commit 1897401

Please sign in to comment.