use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::Arc;
use std::time::Duration;
use rill_core::prelude::*;
use rill_core::queues::telemetry::{Telemetry, CLOCK_TICK};
use rill_core::queues::MpscQueue;
use crossbeam_channel::Receiver as CrossbeamReceiver;
pub use crate::automaton::Range;
use crate::automaton::{EnvelopeAutomaton, LfoAutomaton, LfoWaveform};
use crate::automaton_task::spawn_automaton_task;
use crate::port_combiner::{spawn_combiner, PortCombinerHandle};
use crate::sequencer::{SequencerCommand, SequencerHandle, SnapshotSequencer};
use crate::strategy::{ConflictStrategy, ControlStrategy, UiCommand};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum EventPattern {
AnyButton,
ButtonId(u32),
AnyKnob,
KnobId(u32),
AnyFader,
FaderId(u32),
AnyMidi,
MidiControl {
channel: Option<u8>,
controller: u8,
},
MidiNote {
channel: Option<u8>,
note: Option<u8>,
},
OscAddress(String),
OscPattern(String),
}
impl EventPattern {
pub fn matches(&self, event: &ControlEvent) -> bool {
match (self, event) {
(EventPattern::AnyButton, ControlEvent::Button { .. }) => true,
(EventPattern::ButtonId(id), ControlEvent::Button { id: eid, .. }) => *id == *eid,
(EventPattern::AnyKnob, ControlEvent::Knob { .. }) => true,
(EventPattern::KnobId(id), ControlEvent::Knob { id: eid, .. }) => *id == *eid,
(EventPattern::AnyFader, ControlEvent::Fader { .. }) => true,
(EventPattern::FaderId(id), ControlEvent::Fader { id: eid, .. }) => *id == *eid,
(
EventPattern::MidiControl {
channel,
controller,
},
ControlEvent::MidiControl {
channel: ech,
controller: ectr,
..
},
) => (channel.is_none() || channel.unwrap() == *ech) && *controller == *ectr,
(EventPattern::OscAddress(addr), ControlEvent::Osc { address, .. }) => addr == address,
(EventPattern::OscPattern(pat), ControlEvent::Osc { address, .. }) => {
address.contains(pat)
}
_ => false,
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub enum ControlEvent {
Button {
id: u32,
pressed: bool,
},
Knob {
id: u32,
value: f32,
normalized: f32,
},
Fader {
id: u32,
value: f32,
normalized: f32,
},
MidiControl {
channel: u8,
controller: u8,
value: u8,
normalized: f32,
},
MidiNote {
channel: u8,
note: u8,
velocity: u8,
on: bool,
},
Osc {
address: String,
args: Vec<f32>,
},
}
impl ControlEvent {
pub fn normalized_value(&self) -> Option<f32> {
match self {
ControlEvent::Knob { normalized, .. } => Some(*normalized),
ControlEvent::Fader { normalized, .. } => Some(*normalized),
ControlEvent::MidiControl { normalized, .. } => Some(*normalized),
ControlEvent::Button { pressed, .. } => Some(if *pressed { 1.0 } else { 0.0 }),
_ => None,
}
}
pub fn id(&self) -> Option<u32> {
match self {
ControlEvent::Button { id, .. } => Some(*id),
ControlEvent::Knob { id, .. } => Some(*id),
ControlEvent::Fader { id, .. } => Some(*id),
_ => None,
}
}
}
#[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<ParameterCommand> {
if !self.matches(event) {
return None;
}
event.normalized_value().map(|norm| {
let value = self.transform.apply(norm, self.target.min, self.target.max);
ParameterCommand {
node_id: self.target.node_id,
param: self.target.param_name.clone(),
value,
}
})
}
}
pub type Time = f64;
#[derive(Debug, Clone, Default)]
pub struct NoAction;
pub trait Automaton: Send + Sync + Debug {
type State: Clone + Send + Sync + 'static + Debug;
type Action: Debug + Clone + Send + Sync + Default + 'static;
fn step(
&self,
time: Time,
action: &Self::Action,
state: &Self::State,
) -> (Self::State, Option<f64>);
fn initial_state(&self) -> Self::State;
fn name(&self) -> &str;
fn extract_value(&self, state: &Self::State) -> f64;
fn reset(&self) -> Self::State {
self.initial_state()
}
}
#[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),
}
}
}
pub struct Servo<A: Automaton> {
id: String,
automaton: A,
state: A::State,
target_node: NodeId,
target_param: String,
mapping: ParameterMapping,
min: f64,
max: f64,
last_value: f64,
enabled: bool,
last_time: Time,
}
impl<A: Automaton> 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,
) -> Self {
let state = automaton.initial_state();
Self {
id: id.into(),
automaton,
state,
target_node,
target_param: target_param.into(),
mapping,
min,
max,
last_value: 0.0,
enabled: true,
last_time: 0.0,
}
}
pub fn update(&mut self, time: Time) -> Option<ParameterCommand> {
if !self.enabled {
return None;
}
let (new_state, value_opt) = self
.automaton
.step(time, &A::Action::default(), &self.state);
self.state = new_state;
if let Some(raw_value) = value_opt {
let mapped = self.mapping.apply(raw_value);
let clamped = mapped.clamp(self.min, self.max);
if (clamped - self.last_value).abs() > 1e-6 {
self.last_value = clamped;
self.last_time = time;
return Some(ParameterCommand {
node_id: self.target_node,
param: self.target_param.clone(),
value: clamped as f32,
});
}
}
None
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn id(&self) -> &str {
&self.id
}
}
pub type BoxedServo = Box<dyn AnyServo>;
pub trait AnyServo: Send + Sync {
fn update(&mut self, time: Time) -> Option<ParameterCommand>;
fn id(&self) -> &str;
fn set_enabled(&mut self, enabled: bool);
}
impl<A: Automaton + 'static> AnyServo for Servo<A> {
fn update(&mut self, time: Time) -> Option<ParameterCommand> {
Servo::update(self, time)
}
fn id(&self) -> &str {
&self.id
}
fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct ParameterCommand {
pub node_id: NodeId,
pub param: String,
pub value: f32,
}
impl ParameterCommand {
pub fn new(node_id: NodeId, param: impl Into<String>, value: f32) -> Self {
Self {
node_id,
param: param.into(),
value,
}
}
}
pub struct PatchbayControl {
mappings: Vec<Mapping>,
servos: HashMap<String, BoxedServo>,
port_combiners: HashMap<String, PortCombinerHandle>,
automaton_handles: HashMap<String, tokio::task::JoinHandle<()>>,
sequencer_handle: Option<SequencerHandle>,
sequencer_task: Option<tokio::task::JoinHandle<()>>,
command_queue: Arc<MpscQueue<ParameterCommand>>,
time: Time,
}
impl PatchbayControl {
pub fn new(command_queue: Arc<MpscQueue<ParameterCommand>>) -> Self {
Self {
mappings: Vec::new(),
servos: HashMap::new(),
port_combiners: HashMap::new(),
automaton_handles: HashMap::new(),
sequencer_handle: None,
sequencer_task: None,
command_queue,
time: 0.0,
}
}
pub fn add_mapping(&mut self, mapping: Mapping) {
self.mappings.push(mapping);
}
pub fn add_boxed_servo(&mut self, id: String, servo: BoxedServo) {
self.servos.insert(id, servo);
}
pub fn add_mapping_str(
&mut self,
pattern: &str,
target_node: NodeId,
target_param: &str,
min: f32,
max: f32,
transform: Transform,
) -> Result<(), &'static str> {
let pattern = match pattern {
p if p.starts_with("button:") => {
let id = p[7..].parse().map_err(|_| "Invalid button ID")?;
EventPattern::ButtonId(id)
}
p if p.starts_with("knob:") => {
let id = p[5..].parse().map_err(|_| "Invalid knob ID")?;
EventPattern::KnobId(id)
}
p if p.starts_with("fader:") => {
let id = p[6..].parse().map_err(|_| "Invalid fader ID")?;
EventPattern::FaderId(id)
}
p if p.starts_with("midi:") => {
let parts: Vec<&str> = p[5..].split(':').collect();
if parts.len() == 2 {
let channel = parts[0].parse().ok();
let controller = parts[1].parse().map_err(|_| "Invalid controller")?;
EventPattern::MidiControl {
channel,
controller,
}
} else {
EventPattern::AnyMidi
}
}
p if p.starts_with("osc:") => EventPattern::OscAddress(p[4..].to_string()),
_ => return Err("Unknown pattern"),
};
let target = Target {
node_id: target_node,
param_name: target_param.to_string(),
min,
max,
};
self.add_mapping(Mapping::new(pattern, target, transform));
Ok(())
}
pub fn add_servo<A: Automaton + 'static>(&mut self, servo: Servo<A>) {
self.servos.insert(servo.id().to_string(), Box::new(servo));
}
pub fn add_lfo(
&mut self,
id: &str,
frequency: f64,
amplitude: f64,
offset: f64,
waveform: LfoWaveform,
target_node: NodeId,
target_param: &str,
min: f64,
max: f64,
) {
let automaton = LfoAutomaton::new(id, frequency, amplitude, offset, waveform);
let servo = Servo::new(
id,
automaton,
target_node,
target_param,
ParameterMapping::Linear,
min,
max,
);
self.add_servo(servo);
}
pub fn add_envelope(
&mut self,
id: &str,
attack: f64,
decay: f64,
sustain: f64,
release: f64,
target_node: NodeId,
target_param: &str,
min: f64,
max: f64,
) {
let automaton = EnvelopeAutomaton::adsr(id, attack, decay, sustain, release);
let servo = Servo::new(
id,
automaton,
target_node,
target_param,
ParameterMapping::Linear,
min,
max,
);
self.add_servo(servo);
}
pub fn add_automaton_task<A: Automaton + 'static>(
&mut self,
id: &str,
automaton: A,
interval: Duration,
target: (NodeId, String),
range: (f64, f64),
control: ControlStrategy,
conflict: ConflictStrategy,
) {
let key = target_key(target.0, &target.1);
let combiner = spawn_combiner(
target,
range,
control,
conflict,
self.command_queue.clone(),
);
let task = spawn_automaton_task(
automaton,
interval,
combiner.automaton_tx.clone(),
combiner.cancel_rx(),
);
self.port_combiners.insert(key, combiner);
self.automaton_handles.insert(id.to_string(), task);
}
pub fn add_lfo_task(
&mut self,
id: &str,
frequency: f64,
amplitude: f64,
offset: f64,
waveform: LfoWaveform,
interval: Duration,
target: (NodeId, String),
range: (f64, f64),
control: ControlStrategy,
conflict: ConflictStrategy,
) {
let automaton = LfoAutomaton::new(id, frequency, amplitude, offset, waveform);
self.add_automaton_task(
format!("{}_auto", id).as_str(),
automaton,
interval,
target,
range,
control,
conflict,
);
}
pub fn add_envelope_task(
&mut self,
id: &str,
attack: f64,
decay: f64,
sustain: f64,
release: f64,
interval: Duration,
target: (NodeId, String),
range: (f64, f64),
control: ControlStrategy,
conflict: ConflictStrategy,
) {
let automaton = EnvelopeAutomaton::adsr(id, attack, decay, sustain, release);
self.add_automaton_task(
format!("{}_auto", id).as_str(),
automaton,
interval,
target,
range,
control,
conflict,
);
}
pub fn attach_sequencer(
&mut self,
tel_rx: CrossbeamReceiver<Telemetry>,
sequencer: SnapshotSequencer,
) -> SequencerHandle {
assert!(
self.sequencer_task.is_none(),
"sequencer already attached — detach first"
);
let (cmd_tx, cmd_rx) = crossbeam_channel::unbounded::<SequencerCommand>();
let queue = self.command_queue.clone();
let task = tokio::task::spawn_blocking(move || {
let mut seq = sequencer;
loop {
loop {
match cmd_rx.try_recv() {
Ok(SequencerCommand::Start) => seq.start(),
Ok(SequencerCommand::Stop) => seq.stop(),
Ok(SequencerCommand::Reset { sample_pos }) => seq.reset(sample_pos),
Ok(SequencerCommand::SetPattern(id)) => seq.set_active_pattern(&id),
Err(crossbeam_channel::TryRecvError::Empty) => break,
Err(crossbeam_channel::TryRecvError::Disconnected) => return,
}
}
match tel_rx.recv() {
Ok(Telemetry::Event { kind, data, .. }) if kind == CLOCK_TICK => {
if data.len() >= 3 {
let sample_pos = data[0] as u64;
let sample_rate = data[1];
let tempo = data[2];
let beat_pos = data.get(3).copied().unwrap_or(0.0);
let new_beat = data.get(4).copied().unwrap_or(0.0) > 0.5;
let new_bar = data.get(5).copied().unwrap_or(0.0) > 0.5;
let cmds = seq.tick_ext(
sample_pos, sample_rate, tempo,
beat_pos, new_beat, new_bar,
);
for cmd in cmds {
let _ = queue.push(cmd);
}
}
}
Err(_) => return,
_ => {}
}
}
});
let handle = SequencerHandle::new(cmd_tx);
self.sequencer_handle = Some(handle.clone());
self.sequencer_task = Some(task);
handle
}
pub fn detach_sequencer(&mut self) {
if let Some(task) = self.sequencer_task.take() {
task.abort();
}
self.sequencer_handle = None;
}
pub fn sequencer_handle(&self) -> Option<&SequencerHandle> {
self.sequencer_handle.as_ref()
}
pub fn stop_all(&mut self) {
for combiner in self.port_combiners.values() {
combiner.stop();
}
self.port_combiners.clear();
self.automaton_handles.clear();
self.detach_sequencer();
}
pub fn handle_event(&mut self, event: ControlEvent) {
for mapping in &self.mappings {
if let Some(cmd) = mapping.apply(&event) {
let key = target_key(cmd.node_id, &cmd.param);
if let Some(combiner) = self.port_combiners.get(&key) {
let _ = combiner.ui_tx.send(UiCommand::SetValue(cmd.value as f64));
} else {
let _ = self.command_queue.push(cmd);
}
}
}
}
pub fn update(&mut self, dt: f32) {
self.time += dt as f64;
for servo in self.servos.values_mut() {
if let Some(cmd) = servo.update(self.time) {
let _ = self.command_queue.push(cmd);
}
}
}
pub fn get_combiner(&self, key: &str) -> Option<&PortCombinerHandle> {
self.port_combiners.get(key)
}
pub fn mappings(&self) -> &[Mapping] {
&self.mappings
}
pub fn get_servo(&self, id: &str) -> Option<&dyn AnyServo> {
self.servos.get(id).map(|b| b.as_ref())
}
pub fn get_servo_mut(&mut self, id: &str) -> Option<&mut BoxedServo> {
self.servos.get_mut(id)
}
pub fn remove_servo(&mut self, id: &str) -> bool {
self.servos.remove(id).is_some()
}
pub fn clear(&mut self) {
self.mappings.clear();
self.servos.clear();
self.stop_all();
}
pub fn reset_time(&mut self) {
self.time = 0.0;
}
pub fn current_time(&self) -> Time {
self.time
}
}
pub fn midi_cc(
controller: u8,
channel: Option<u8>,
target_node: NodeId,
target_param: &str,
min: f32,
max: f32,
transform: Transform,
) -> Mapping {
let pattern = EventPattern::MidiControl {
channel,
controller,
};
let target = Target {
node_id: target_node,
param_name: target_param.to_string(),
min,
max,
};
Mapping::new(pattern, target, transform)
}
pub fn osc_address(
address: &str,
target_node: NodeId,
target_param: &str,
min: f32,
max: f32,
transform: Transform,
) -> Mapping {
let pattern = EventPattern::OscAddress(address.to_string());
let target = Target {
node_id: target_node,
param_name: target_param.to_string(),
min,
max,
};
Mapping::new(pattern, target, transform)
}
fn target_key(node_id: NodeId, param_name: &str) -> String {
format!("{}:{}", node_id.0, param_name)
}
#[cfg(test)]
mod tests {
use super::*;
use rill_core::queues::MpscQueue;
#[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.node_id, node);
assert_eq!(cmd.param, "volume");
assert!((cmd.value - 0.5).abs() < 1e-6);
}
#[test]
fn test_lfo_servo() {
let node = NodeId(1);
let queue = Arc::new(MpscQueue::with_capacity(64));
let mut control = PatchbayControl::new(queue);
control.add_lfo(
"test_lfo",
1.0,
0.5,
0.0,
LfoWaveform::Sine,
node,
"cutoff",
100.0,
1000.0,
);
assert!(control.get_servo("test_lfo").is_some());
for _i in 0..10 {
control.update(0.1);
}
}
#[test]
fn test_envelope_servo() {
let node = NodeId(1);
let queue = Arc::new(MpscQueue::with_capacity(64));
let mut control = PatchbayControl::new(queue.clone());
control.add_envelope("test_env", 0.1, 0.2, 0.7, 0.3, node, "gain", 0.0, 1.0);
if let Some(_servo) = control.get_servo_mut("test_env") {
}
control.update(0.05);
control.update(0.05);
}
}