Skip to main content

rill_patchbay/automaton/
function.rs

1//! # Functional automata
2//!
3//! Automata built on arbitrary time functions.
4//! Allow implementing any mathematical relationship.
5
6use crate::engine::{Automaton, NoAction, Range, Time};
7use rill_core::traits::ParamValue;
8use std::fmt;
9use std::sync::Arc;
10
11/// Functional automaton (stateless)
12#[derive(Clone)]
13pub struct FunctionAutomaton {
14    /// Automaton name
15    name: String,
16    /// Generator function
17    generator: Arc<dyn Fn(Time) -> f64 + Send + Sync>,
18    /// Output value range
19    range: Range,
20}
21
22impl fmt::Debug for FunctionAutomaton {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        f.debug_struct("FunctionAutomaton")
25            .field("name", &self.name)
26            .field("range", &self.range)
27            .finish()
28    }
29}
30
31impl FunctionAutomaton {
32    /// Create a new functional automaton
33    pub fn new<F>(name: &str, generator: F) -> Self
34    where
35        F: Fn(Time) -> f64 + Send + Sync + 'static,
36    {
37        Self {
38            name: name.to_string(),
39            generator: Arc::new(generator),
40            range: Range::bipolar(),
41        }
42    }
43
44    /// Set the range
45    pub fn with_range(mut self, range: Range) -> Self {
46        self.range = range;
47        self
48    }
49}
50
51impl Automaton for FunctionAutomaton {
52    type Internal = ();
53    type Action = NoAction;
54
55    fn step(
56        &self,
57        _internal: &mut Self::Internal,
58        _current: &ParamValue,
59        time: Time,
60        _action: &Self::Action,
61    ) -> ParamValue {
62        let value = (self.generator)(time);
63        let clamped = self.range.clamp(value);
64        ParamValue::Float(clamped as f32)
65    }
66
67    fn initial_internal(&self) -> Self::Internal {}
68
69    fn name(&self) -> &str {
70        &self.name
71    }
72}
73
74/// Functional automaton with state
75#[derive(Clone)]
76#[allow(clippy::type_complexity)]
77pub struct StatefulFunctionAutomaton<S> {
78    /// Automaton name
79    name: String,
80    /// Generator function with state
81    generator: Arc<dyn Fn(Time, &mut S) -> f64 + Send + Sync>,
82    /// Initial state
83    initial_state: S,
84    /// Output value range
85    range: Range,
86}
87
88impl<S: fmt::Debug + Send + Sync + Clone + 'static> fmt::Debug for StatefulFunctionAutomaton<S> {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        f.debug_struct("StatefulFunctionAutomaton")
91            .field("name", &self.name)
92            .field("initial_state", &self.initial_state)
93            .field("range", &self.range)
94            .finish()
95    }
96}
97
98impl<S: Send + Sync + Clone + 'static> StatefulFunctionAutomaton<S> {
99    /// Create a new stateful automaton
100    pub fn new<F>(name: &str, generator: F, initial_state: S) -> Self
101    where
102        F: Fn(Time, &mut S) -> f64 + Send + Sync + 'static,
103    {
104        Self {
105            name: name.to_string(),
106            generator: Arc::new(generator),
107            initial_state,
108            range: Range::bipolar(),
109        }
110    }
111
112    /// Set the range
113    pub fn with_range(mut self, range: Range) -> Self {
114        self.range = range;
115        self
116    }
117}
118
119impl<S: fmt::Debug + Send + Sync + Clone + 'static> Automaton for StatefulFunctionAutomaton<S> {
120    type Internal = S;
121    type Action = NoAction;
122
123    fn step(
124        &self,
125        internal: &mut Self::Internal,
126        _current: &ParamValue,
127        time: Time,
128        _action: &Self::Action,
129    ) -> ParamValue {
130        let value = (self.generator)(time, internal);
131        let clamped = self.range.clamp(value);
132        ParamValue::Float(clamped as f32)
133    }
134
135    fn initial_internal(&self) -> Self::Internal {
136        self.initial_state.clone()
137    }
138
139    fn name(&self) -> &str {
140        &self.name
141    }
142}
143
144/// Generator function for LFO (convenience wrapper)
145pub fn lfo_function(freq: f64, phase: f64, waveform: &'static str) -> impl Fn(Time) -> f64 {
146    move |t| {
147        let p = (t * freq + phase).fract();
148        match waveform {
149            "sine" => (p * 2.0 * std::f64::consts::PI).sin(),
150            "saw" => 2.0 * p - 1.0,
151            "square" => {
152                if p < 0.5 {
153                    1.0
154                } else {
155                    -1.0
156                }
157            }
158            _ => 0.0,
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_function_automaton() {
169        let automaton = FunctionAutomaton::new("Test", |t| (t * 2.0).sin());
170        let mut internal = automaton.initial_internal();
171        let current = ParamValue::Float(0.0);
172
173        let value = automaton.step(&mut internal, &current, 1.0, &NoAction);
174        assert!(value.as_f32().is_some());
175    }
176
177    #[test]
178    fn test_stateful_automaton() {
179        let automaton = StatefulFunctionAutomaton::new(
180            "Counter",
181            |_t, counter: &mut i32| {
182                *counter += 1;
183                *counter as f64
184            },
185            0,
186        )
187        .with_range(Range::new(0.0, 100.0));
188
189        let mut internal = automaton.initial_internal();
190        let current = ParamValue::Float(0.0);
191
192        let value = automaton.step(&mut internal, &current, 1.0, &NoAction);
193        assert_eq!(value.as_f32().unwrap(), 1.0);
194
195        let value = automaton.step(&mut internal, &current, 1.0, &NoAction);
196        assert_eq!(value.as_f32().unwrap(), 2.0);
197    }
198}