use std::fmt::Debug;
use std::sync::{Arc, Mutex};
use rill_core::prelude::*;
use rill_core::queues::{AutomatonCommand, CommandEnum, SetParameter, SignalOrigin};
use rill_core_actor::{ActorRef, ActorSystem};
pub use crate::automaton::{EnvelopeAutomaton, LfoAutomaton, LfoWaveform, Range};
use crate::strategy::{ConflictStrategy, ControlStrategy};
pub use rill_core::queues::control_event::{
ControlEvent, EventPattern, MidiNoteKind, MidiTransportKind,
};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct OscSurfaceEntry {
pub osc_path: String,
pub event_pattern: EventPattern,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub label: Option<String>,
}
pub type OscSurface = Vec<OscSurfaceEntry>;
#[derive(Clone)]
pub enum Transform {
Linear,
Exponential,
Logarithmic,
Inverted,
Custom(Arc<dyn Fn(f32) -> f32 + Send + Sync>),
}
impl Debug for Transform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Transform::Linear => write!(f, "Linear"),
Transform::Exponential => write!(f, "Exponential"),
Transform::Logarithmic => write!(f, "Logarithmic"),
Transform::Inverted => write!(f, "Inverted"),
Transform::Custom(_) => write!(f, "Custom"),
}
}
}
impl Transform {
pub fn apply(&self, value: f32, min: f32, max: f32) -> f32 {
let range = max - min;
let normalized = value.clamp(0.0, 1.0);
let mapped = match self {
Transform::Linear => min + normalized * range,
Transform::Exponential => min + normalized * normalized * range,
Transform::Logarithmic => min + (1.0 + normalized * 9.0).log10() * range,
Transform::Inverted => max - normalized * range,
Transform::Custom(f) => min + f(normalized) * range,
};
mapped.clamp(min, max)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct Target {
pub node_id: NodeId,
pub param_name: String,
pub min: f32,
pub max: f32,
}
#[derive(Debug, Clone)]
pub struct Mapping {
pub pattern: EventPattern,
pub target: Target,
pub transform: Transform,
pub name: String,
pub enabled: bool,
}
impl Mapping {
pub fn new(pattern: EventPattern, target: Target, transform: Transform) -> Self {
let name = format!("{:?} -> {}", pattern, target.param_name);
Self {
pattern,
target,
transform,
name,
enabled: true,
}
}
pub fn matches(&self, event: &ControlEvent) -> bool {
self.enabled && self.pattern.matches(event)
}
pub fn apply(&self, event: &ControlEvent) -> Option<SetParameter> {
if !self.matches(event) {
return None;
}
if let (
EventPattern::MidiNote { kind, .. },
ControlEvent::MidiNote {
note, velocity, on, ..
},
) = (&self.pattern, event)
{
let value = match kind {
MidiNoteKind::Frequency => {
if !*on {
return None;
}
rill_core_dsp::math::midi_to_freq::<f32>(*note)
}
MidiNoteKind::Amplitude => {
let raw = if *on { *velocity as f32 / 127.0 } else { 0.0 };
self.transform.apply(raw, self.target.min, self.target.max)
}
MidiNoteKind::Gate => {
let raw = if *on { 1.0 } else { 0.0 };
self.transform.apply(raw, self.target.min, self.target.max)
}
};
let pid = ParameterId::new(&self.target.param_name).unwrap();
return Some(SetParameter::new(
PortId::param(self.target.node_id, 0),
pid,
ParamValue::Float(value),
SignalOrigin::External(self.name.clone()),
));
}
let norm = event.normalized_value()?;
let value = self.transform.apply(norm, self.target.min, self.target.max);
let pid = ParameterId::new(&self.target.param_name).unwrap();
Some(SetParameter::new(
PortId::param(self.target.node_id, 0),
pid,
ParamValue::Float(value),
SignalOrigin::External(self.name.clone()),
))
}
}
pub type Time = f64;
#[derive(Debug, Clone, Default)]
pub struct NoAction;
impl Automaton for NoAction {
type Internal = ();
type Action = ();
fn step(
&self,
_internal: &mut Self::Internal,
_current: &ParamValue,
_time: Time,
_action: &Self::Action,
) -> ParamValue {
ParamValue::Float(0.0)
}
fn initial_internal(&self) -> Self::Internal {}
fn name(&self) -> &str {
"NoAction"
}
}
pub trait Automaton: Send + Sync + Debug {
type Internal: Clone + Send + Sync + 'static;
type Action: Debug + Clone + Send + Sync + Default + 'static;
fn step(
&self,
internal: &mut Self::Internal,
current: &ParamValue,
time: Time,
action: &Self::Action,
) -> ParamValue;
fn initial_internal(&self) -> Self::Internal;
fn reset(&self) -> Self::Internal {
self.initial_internal()
}
fn name(&self) -> &str;
}
#[derive(Clone)]
pub enum ParameterMapping {
Linear,
Exponential,
Logarithmic,
Inverted,
Custom(Arc<dyn Fn(f64) -> f64 + Send + Sync>),
}
impl std::fmt::Debug for ParameterMapping {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParameterMapping::Linear => write!(f, "Linear"),
ParameterMapping::Exponential => write!(f, "Exponential"),
ParameterMapping::Logarithmic => write!(f, "Logarithmic"),
ParameterMapping::Inverted => write!(f, "Inverted"),
ParameterMapping::Custom(_) => write!(f, "Custom(<fn>)"),
}
}
}
impl ParameterMapping {
pub fn apply(&self, raw: f64) -> f64 {
match self {
ParameterMapping::Linear => raw,
ParameterMapping::Exponential => raw * raw,
ParameterMapping::Logarithmic => (1.0 + raw * 9.0).log10(),
ParameterMapping::Inverted => 1.0 - raw,
ParameterMapping::Custom(f) => f(raw),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct ControlContext {
pitch_bend_semitones: f64,
mod_wheel: f64,
active_note: Option<u8>,
active_velocity: Option<f32>,
}
impl Default for ControlContext {
fn default() -> Self {
Self {
pitch_bend_semitones: 0.0,
mod_wheel: 1.0,
active_note: None,
active_velocity: None,
}
}
}
fn midi_note_to_freq(note: u8) -> f64 {
440.0 * 2.0f64.powf((note as f64 - 69.0) / 12.0)
}
pub(crate) struct ServoState<A: Automaton> {
pub(crate) internal: A::Internal,
pub(crate) value: ParamValue,
pub(crate) time: Time,
pub(crate) enabled: bool,
pub(crate) base: f64,
pub(crate) frozen: bool,
pub(crate) last_sent_value: f64,
pub(crate) last_sent_index: i64,
pub(crate) control_ctx: ControlContext,
}
pub struct Servo<A: Automaton> {
id: String,
automaton: Arc<A>,
state: Arc<Mutex<ServoState<A>>>,
graph_ref: ActorRef<CommandEnum>,
target_node: NodeId,
target_param: String,
mapping: ParameterMapping,
min: f64,
max: f64,
control: ControlStrategy,
conflict: ConflictStrategy,
table: Option<Vec<ParamValue>>,
mappings: Vec<Mapping>,
pitch_bend_cc: Option<u8>,
pitch_bend_semis: f64,
mod_wheel_cc: Option<u8>,
}
impl<A: Automaton + 'static> Servo<A> {
pub fn new(
id: impl Into<String>,
automaton: A,
target_node: NodeId,
target_param: impl Into<String>,
mapping: ParameterMapping,
min: f64,
max: f64,
system: Arc<ActorSystem>,
graph_ref: ActorRef<CommandEnum>,
) -> Self {
let _ = system;
let automaton = Arc::new(automaton);
let mut internal = automaton.initial_internal();
let initial_value = automaton.step(
&mut internal,
&ParamValue::Float(0.0),
0.0,
&A::Action::default(),
);
Self {
id: id.into(),
automaton,
state: Arc::new(Mutex::new(ServoState {
internal,
value: initial_value,
time: 0.0,
enabled: true,
base: (min + max) / 2.0,
frozen: false,
last_sent_value: f64::NAN,
last_sent_index: -1,
control_ctx: ControlContext::default(),
})),
graph_ref,
target_node,
target_param: target_param.into(),
mapping,
min,
max,
control: ControlStrategy::Absolute,
conflict: ConflictStrategy::LastWriteWins,
table: None,
mappings: Vec::new(),
pitch_bend_cc: None,
pitch_bend_semis: 2.0,
mod_wheel_cc: None,
}
}
pub fn spawn(self, system: &ActorSystem) -> ActorRef<CommandEnum> {
let Servo {
id,
automaton,
state,
graph_ref,
target_node,
target_param,
mapping,
min,
max,
control,
conflict,
table,
mappings,
pitch_bend_cc,
pitch_bend_semis,
mod_wheel_cc,
} = self;
let a = automaton;
let s = state;
let gr = graph_ref;
let nid = target_node;
let param = target_param;
let map = mapping;
let ctrl = control;
let confl = conflict;
let tbl = table;
let pitch_cc = pitch_bend_cc;
let pitch_semis = pitch_bend_semis;
let mod_cc = mod_wheel_cc;
let serv_id = id.clone();
let s2 = s.clone();
system.spawn_detached(
&format!("servo_{id}"),
move || {
Box::new(move |msg: CommandEnum| match msg {
CommandEnum::ClockTick(clock) => {
let mut state = s2.lock().unwrap();
if !state.enabled {
return;
}
let dt = clock.samples_since_last as f64 / clock.sample_rate as f64;
state.time += dt;
if state.frozen && matches!(confl, ConflictStrategy::TouchOverride) {
return;
}
let current_value = state.value.clone();
let current_time = state.time;
let action = A::Action::default();
let new_val =
a.step(&mut state.internal, ¤t_value, current_time, &action);
let raw = new_val.as_f32().unwrap_or(0.0) as f64;
state.value = new_val;
if let Some(ref table) = tbl {
let index = raw as usize;
if index >= table.len() {
return;
}
let idx = index as i64;
if idx == state.last_sent_index {
return;
}
state.last_sent_index = idx;
let pid = ParameterId::new(¶m).unwrap();
gr.send(CommandEnum::SetParameter(SetParameter::new(
PortId::param(nid, 0),
pid,
table[index].clone(),
SignalOrigin::Automaton(serv_id.clone()),
)));
return;
}
let mapped = map.apply(raw);
let base = state.base;
let value = match ctrl {
ControlStrategy::Absolute => min + mapped * (max - min),
ControlStrategy::Modulation { depth } => {
(base + mapped * depth * (max - min)).clamp(min, max)
}
};
if (value - state.last_sent_value).abs() < 1e-6 {
return;
}
state.last_sent_value = value;
if param.is_empty() {
return;
}
let pid = ParameterId::new(¶m).unwrap();
gr.send(CommandEnum::SetParameter(SetParameter::new(
PortId::param(nid, 0),
pid,
ParamValue::Float(value as f32),
SignalOrigin::Automaton(serv_id.clone()),
)));
}
CommandEnum::Automaton(AutomatonCommand::SetEnabled { enabled, .. }) => {
s.lock().unwrap().enabled = enabled;
}
CommandEnum::Automaton(AutomatonCommand::Reset { .. }) => {
s.lock().unwrap().internal = a.reset();
}
CommandEnum::Automaton(AutomatonCommand::UiValue { value, .. }) => {
let mut state = s.lock().unwrap();
let pid = ParameterId::new(¶m).unwrap();
let cmd = SetParameter::new(
PortId::param(nid, 0),
pid,
ParamValue::Float(value as f32),
SignalOrigin::Automaton(serv_id.clone()),
);
match confl {
ConflictStrategy::TouchOverride => {
state.base = value;
state.frozen = true;
gr.send(CommandEnum::SetParameter(cmd));
}
ConflictStrategy::BasePlusModulation => {
state.base = value;
}
ConflictStrategy::LastWriteWins => {
gr.send(CommandEnum::SetParameter(cmd));
}
}
}
CommandEnum::Automaton(AutomatonCommand::UiRelease { .. }) => {
let mut state = s.lock().unwrap();
if state.frozen {
state.frozen = false;
}
}
CommandEnum::Control(event) => {
match &event {
ControlEvent::MidiControl {
controller,
normalized,
..
} if Some(*controller) == pitch_cc => {
let mut state = s.lock().unwrap();
let semis = (*normalized as f64 - 0.5) * 2.0 * pitch_semis;
state.control_ctx.pitch_bend_semitones = semis;
drop(state);
let s3 = s.lock().unwrap();
if let (Some(note), Some(_vel)) =
(s3.control_ctx.active_note, s3.control_ctx.active_velocity)
{
let freq = midi_note_to_freq(note)
* 2.0f64.powf(s3.control_ctx.pitch_bend_semitones / 12.0);
let pid = ParameterId::new("frequency").unwrap();
gr.send(CommandEnum::SetParameter(SetParameter::new(
PortId::param(nid, 0),
pid,
ParamValue::Float(freq as f32),
SignalOrigin::Automaton(serv_id.clone()),
)));
}
drop(s3);
}
ControlEvent::MidiControl {
controller,
normalized,
..
} if Some(*controller) == mod_cc => {
let mut state = s.lock().unwrap();
state.control_ctx.mod_wheel = *normalized as f64;
drop(state);
let s3 = s.lock().unwrap();
if let (Some(_note), Some(vel)) =
(s3.control_ctx.active_note, s3.control_ctx.active_velocity)
{
let amp = vel as f64 * s3.control_ctx.mod_wheel;
let pid = ParameterId::new("amplitude").unwrap();
gr.send(CommandEnum::SetParameter(SetParameter::new(
PortId::param(nid, 0),
pid,
ParamValue::Float(amp as f32),
SignalOrigin::Automaton(serv_id.clone()),
)));
}
drop(s3);
}
ControlEvent::MidiNote {
note,
velocity,
on: true,
..
} if velocity > &0u8 => {
let vel_norm = *velocity as f32 / 127.0;
let mut state = s.lock().unwrap();
state.control_ctx.active_note = Some(*note);
state.control_ctx.active_velocity = Some(vel_norm);
let freq = midi_note_to_freq(*note)
* 2.0f64.powf(state.control_ctx.pitch_bend_semitones / 12.0);
let amp = vel_norm as f64 * state.control_ctx.mod_wheel;
drop(state);
let pid = ParameterId::new("frequency").unwrap();
gr.send(CommandEnum::SetParameter(SetParameter::new(
PortId::param(nid, 0),
pid,
ParamValue::Float(freq as f32),
SignalOrigin::Automaton(serv_id.clone()),
)));
let pid_amp = ParameterId::new("amplitude").unwrap();
gr.send(CommandEnum::SetParameter(SetParameter::new(
PortId::param(nid, 0),
pid_amp,
ParamValue::Float(amp as f32),
SignalOrigin::Automaton(serv_id.clone()),
)));
}
ControlEvent::MidiNote { on: false, .. } => {
let mut state = s.lock().unwrap();
state.control_ctx.active_note = None;
state.control_ctx.active_velocity = None;
drop(state);
let pid = ParameterId::new("amplitude").unwrap();
gr.send(CommandEnum::SetParameter(SetParameter::new(
PortId::param(nid, 0),
pid,
ParamValue::Float(0.0),
SignalOrigin::Automaton(serv_id.clone()),
)));
}
_ => {
let mut state = s.lock().unwrap();
for mapping in &mappings {
if let Some(sp) = mapping.apply(&event) {
match confl {
ConflictStrategy::TouchOverride => {
state.frozen = true;
if let Some(nv) = event.normalized_value() {
state.base = nv as f64;
}
gr.send(CommandEnum::SetParameter(sp));
break; }
ConflictStrategy::BasePlusModulation => {
if let Some(nv) = event.normalized_value() {
state.base = nv as f64;
}
}
ConflictStrategy::LastWriteWins => {
gr.send(CommandEnum::SetParameter(sp));
}
}
}
}
}
}
}
_ => {}
})
},
1,
)
}
pub fn with_table(mut self, table: Vec<ParamValue>) -> Self {
self.table = Some(table);
self
}
pub fn with_pitch_bend(mut self, cc: u8, semitones: f64) -> Self {
self.pitch_bend_cc = Some(cc);
self.pitch_bend_semis = semitones;
self
}
pub fn with_mod_wheel(mut self, cc: u8) -> Self {
self.mod_wheel_cc = Some(cc);
self
}
pub fn with_mappings(mut self, mappings: Vec<Mapping>) -> Self {
self.mappings = mappings;
self
}
pub fn with_control(mut self, strategy: ControlStrategy) -> Self {
self.control = strategy;
self
}
pub fn with_conflict(mut self, strategy: ConflictStrategy) -> Self {
self.conflict = strategy;
self
}
pub fn id(&self) -> &str {
&self.id
}
}
pub type BoxedModule = Box<dyn Module>;
pub trait Module: Send {
fn id(&self) -> &str;
fn handle(&self) -> Option<ActorRef<CommandEnum>> {
None
}
fn set_enabled(&mut self, _enabled: bool) {}
fn stop(&mut self);
}
pub fn midi_cc(
controller: u8,
channel: Option<u8>,
target_node: NodeId,
target_param: &str,
min: f32,
max: f32,
transform: Transform,
) -> Mapping {
Mapping::new(
EventPattern::MidiControl {
channel,
controller,
},
Target {
node_id: target_node,
param_name: target_param.to_string(),
min,
max,
},
transform,
)
}
pub fn midi_note(
kind: MidiNoteKind,
note: Option<u8>,
channel: Option<u8>,
target_node: NodeId,
target_param: &str,
min: f32,
max: f32,
transform: Transform,
) -> Mapping {
Mapping::new(
EventPattern::MidiNote {
channel,
note,
kind,
},
Target {
node_id: target_node,
param_name: target_param.to_string(),
min,
max,
},
transform,
)
}
pub fn osc_address(
address: &str,
target_node: NodeId,
target_param: &str,
min: f32,
max: f32,
transform: Transform,
) -> Mapping {
Mapping::new(
EventPattern::OscAddress(address.to_string()),
Target {
node_id: target_node,
param_name: target_param.to_string(),
min,
max,
},
transform,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_midi_mapping() {
let node = NodeId(1);
let mapping = midi_cc(7, Some(1), node, "volume", 0.0, 1.0, Transform::Linear);
let event = ControlEvent::MidiControl {
channel: 1,
controller: 7,
value: 64,
normalized: 0.5,
};
assert!(mapping.matches(&event));
let cmd = mapping.apply(&event).unwrap();
assert_eq!(cmd.port.node_id(), node);
assert_eq!(cmd.parameter.as_ref(), "volume");
assert!((cmd.value.as_f32().unwrap() - 0.5).abs() < 1e-6);
}
}