devalang_wasm/engine/audio/
automation.rs

1use crate::language::syntax::ast::Value;
2/// Automation system - parameter automation over time
3/// Supports linear, exponential, and custom curves
4use std::collections::HashMap;
5
6/// Automation curve type
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum AutomationCurve {
9    Linear,
10    Exponential,
11    Logarithmic,
12    Smooth, // Smooth interpolation (ease-in-out)
13}
14
15impl AutomationCurve {
16    pub fn from_str(s: &str) -> Self {
17        match s.to_lowercase().as_str() {
18            "linear" | "lin" => AutomationCurve::Linear,
19            "exponential" | "exp" => AutomationCurve::Exponential,
20            "logarithmic" | "log" => AutomationCurve::Logarithmic,
21            "smooth" | "ease" => AutomationCurve::Smooth,
22            _ => AutomationCurve::Linear,
23        }
24    }
25}
26
27/// Automation parameter
28#[derive(Debug, Clone)]
29pub struct AutomationParam {
30    pub param_name: String,
31    pub from_value: f32,
32    pub to_value: f32,
33    pub start_time: f32, // seconds
34    pub duration: f32,   // seconds
35    pub curve: AutomationCurve,
36}
37
38/// Automation envelope - collection of automation parameters
39#[derive(Debug, Clone)]
40pub struct AutomationEnvelope {
41    pub target: String, // Target entity (synth name, "global", etc.)
42    pub params: Vec<AutomationParam>,
43}
44
45impl AutomationEnvelope {
46    pub fn new(target: String) -> Self {
47        Self {
48            target,
49            params: Vec::new(),
50        }
51    }
52
53    /// Add automation parameter
54    pub fn add_param(&mut self, param: AutomationParam) {
55        self.params.push(param);
56    }
57
58    /// Get automated value for a parameter at a specific time
59    pub fn get_value(&self, param_name: &str, time_seconds: f32) -> Option<f32> {
60        // Find all automation params for this parameter name
61        let matching: Vec<&AutomationParam> = self
62            .params
63            .iter()
64            .filter(|p| p.param_name == param_name)
65            .collect();
66
67        if matching.is_empty() {
68            return None;
69        }
70
71        // Find the active automation (most recent one that affects current time)
72        for param in matching.iter().rev() {
73            let end_time = param.start_time + param.duration;
74
75            if time_seconds >= param.start_time && time_seconds <= end_time {
76                // Currently in automation range
77                let progress = (time_seconds - param.start_time) / param.duration;
78                let value =
79                    interpolate_value(param.from_value, param.to_value, progress, param.curve);
80                return Some(value);
81            } else if time_seconds > end_time {
82                // Past automation - return end value
83                return Some(param.to_value);
84            }
85        }
86
87        // Before any automation - return first start value
88        Some(matching[0].from_value)
89    }
90}
91
92/// Interpolate between two values based on progress and curve type
93fn interpolate_value(from: f32, to: f32, progress: f32, curve: AutomationCurve) -> f32 {
94    let t = progress.clamp(0.0, 1.0);
95
96    let interpolated = match curve {
97        AutomationCurve::Linear => t,
98        AutomationCurve::Exponential => {
99            // Exponential curve (ease-out)
100            if (to - from).abs() < 0.0001 { t } else { t * t }
101        }
102        AutomationCurve::Logarithmic => {
103            // Logarithmic curve (ease-in)
104            1.0 - (1.0 - t) * (1.0 - t)
105        }
106        AutomationCurve::Smooth => {
107            // Smooth ease-in-out (cubic)
108            if t < 0.5 {
109                2.0 * t * t
110            } else {
111                1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
112            }
113        }
114    };
115
116    from + (to - from) * interpolated
117}
118
119/// Parse automation from Value::Map
120pub fn parse_automation_from_value(value: &Value) -> Option<AutomationEnvelope> {
121    if let Value::Map(map) = value {
122        let target = map.get("target").and_then(|v| match v {
123            Value::String(s) | Value::Identifier(s) => Some(s.clone()),
124            _ => None,
125        })?;
126
127        let mut envelope = AutomationEnvelope::new(target);
128
129        // Parse params array
130        if let Some(Value::Array(params_array)) = map.get("params") {
131            for param_value in params_array {
132                if let Some(param) = parse_automation_param(param_value) {
133                    envelope.add_param(param);
134                }
135            }
136        }
137
138        Some(envelope)
139    } else {
140        None
141    }
142}
143
144/// Parse single automation parameter from Value
145fn parse_automation_param(value: &Value) -> Option<AutomationParam> {
146    if let Value::Map(map) = value {
147        let param_name = map.get("name").and_then(|v| match v {
148            Value::String(s) | Value::Identifier(s) => Some(s.clone()),
149            _ => None,
150        })?;
151
152        let from_value = map.get("from").and_then(|v| match v {
153            Value::Number(n) => Some(*n),
154            _ => None,
155        })?;
156
157        let to_value = map.get("to").and_then(|v| match v {
158            Value::Number(n) => Some(*n),
159            _ => None,
160        })?;
161
162        let start_time = map
163            .get("start")
164            .and_then(|v| match v {
165                Value::Number(n) => Some(*n),
166                _ => None,
167            })
168            .unwrap_or(0.0);
169
170        let duration = map.get("duration").and_then(|v| match v {
171            Value::Number(n) => Some(*n),
172            _ => None,
173        })?;
174
175        let curve = map
176            .get("curve")
177            .and_then(|v| match v {
178                Value::String(s) | Value::Identifier(s) => Some(AutomationCurve::from_str(s)),
179                _ => None,
180            })
181            .unwrap_or(AutomationCurve::Linear);
182
183        Some(AutomationParam {
184            param_name,
185            from_value,
186            to_value,
187            start_time,
188            duration,
189            curve,
190        })
191    } else {
192        None
193    }
194}
195
196/// Automation registry - stores all active automations
197#[derive(Debug, Clone, Default)]
198pub struct AutomationRegistry {
199    envelopes: HashMap<String, AutomationEnvelope>,
200}
201
202impl AutomationRegistry {
203    pub fn new() -> Self {
204        Self {
205            envelopes: HashMap::new(),
206        }
207    }
208
209    /// Register an automation envelope
210    pub fn register(&mut self, envelope: AutomationEnvelope) {
211        self.envelopes.insert(envelope.target.clone(), envelope);
212    }
213
214    /// Get automated value for a target and parameter at a specific time
215    pub fn get_value(&self, target: &str, param_name: &str, time_seconds: f32) -> Option<f32> {
216        self.envelopes
217            .get(target)
218            .and_then(|env| env.get_value(param_name, time_seconds))
219    }
220
221    /// Check if a target has any active automations
222    pub fn has_automation(&self, target: &str) -> bool {
223        self.envelopes.contains_key(target)
224    }
225
226    /// Get all automation targets
227    pub fn targets(&self) -> Vec<String> {
228        self.envelopes.keys().cloned().collect()
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_linear_interpolation() {
238        let param = AutomationParam {
239            param_name: "volume".to_string(),
240            from_value: 0.0,
241            to_value: 1.0,
242            start_time: 0.0,
243            duration: 2.0,
244            curve: AutomationCurve::Linear,
245        };
246
247        let mut envelope = AutomationEnvelope::new("synth1".to_string());
248        envelope.add_param(param);
249
250        // At t=0, should be 0.0
251        assert_eq!(envelope.get_value("volume", 0.0), Some(0.0));
252
253        // At t=1 (50%), should be 0.5
254        assert!((envelope.get_value("volume", 1.0).unwrap() - 0.5).abs() < 0.001);
255
256        // At t=2, should be 1.0
257        assert_eq!(envelope.get_value("volume", 2.0), Some(1.0));
258
259        // At t=3 (past), should stay at 1.0
260        assert_eq!(envelope.get_value("volume", 3.0), Some(1.0));
261    }
262
263    #[test]
264    fn test_smooth_curve() {
265        let value_start = interpolate_value(0.0, 1.0, 0.0, AutomationCurve::Smooth);
266        let value_mid = interpolate_value(0.0, 1.0, 0.5, AutomationCurve::Smooth);
267        let value_end = interpolate_value(0.0, 1.0, 1.0, AutomationCurve::Smooth);
268
269        assert_eq!(value_start, 0.0);
270        assert_eq!(value_end, 1.0);
271        assert!(value_mid > 0.4 && value_mid < 0.6); // Should be around 0.5
272    }
273
274    #[test]
275    fn test_automation_registry() {
276        let mut registry = AutomationRegistry::new();
277
278        let mut envelope = AutomationEnvelope::new("synth1".to_string());
279        envelope.add_param(AutomationParam {
280            param_name: "volume".to_string(),
281            from_value: 0.0,
282            to_value: 1.0,
283            start_time: 0.0,
284            duration: 2.0,
285            curve: AutomationCurve::Linear,
286        });
287
288        registry.register(envelope);
289
290        assert!(registry.has_automation("synth1"));
291        assert!(!registry.has_automation("synth2"));
292
293        let value = registry.get_value("synth1", "volume", 1.0);
294        assert!(value.is_some());
295        assert!((value.unwrap() - 0.5).abs() < 0.001);
296    }
297}