Skip to main content

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 (legacy simple curves)
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/// Lightweight template for per-note automation (percent-based points)
39#[derive(Debug, Clone)]
40pub struct AutomationParamTemplate {
41    pub param_name: String,
42    /// Points as (progress_fraction 0.0-1.0, value)
43    pub points: Vec<(f32, f32)>,
44    pub curve: AutomationCurve,
45    /// Advanced curve (if specified)
46    pub advanced_curve: Option<crate::engine::curves::CurveType>,
47}
48
49/// Automation envelope - collection of automation parameters
50#[derive(Debug, Clone)]
51pub struct AutomationEnvelope {
52    pub target: String, // Target entity (synth name, "global", etc.)
53    pub params: Vec<AutomationParam>,
54}
55
56impl AutomationEnvelope {
57    pub fn new(target: String) -> Self {
58        Self {
59            target,
60            params: Vec::new(),
61        }
62    }
63
64    /// Add automation parameter
65    pub fn add_param(&mut self, param: AutomationParam) {
66        self.params.push(param);
67    }
68
69    /// Get automated value for a parameter at a specific time
70    pub fn get_value(&self, param_name: &str, time_seconds: f32) -> Option<f32> {
71        // Find all automation params for this parameter name
72        let matching: Vec<&AutomationParam> = self
73            .params
74            .iter()
75            .filter(|p| p.param_name == param_name)
76            .collect();
77
78        if matching.is_empty() {
79            return None;
80        }
81
82        // Find the active automation (most recent one that affects current time)
83        for param in matching.iter().rev() {
84            let end_time = param.start_time + param.duration;
85
86            if param.duration <= f32::EPSILON {
87                return Some(if time_seconds >= param.start_time {
88                    param.to_value
89                } else {
90                    param.from_value
91                });
92            }
93
94            if time_seconds >= param.start_time && time_seconds <= end_time {
95                // Currently in automation range
96                let progress = (time_seconds - param.start_time) / param.duration;
97                let value =
98                    interpolate_value(param.from_value, param.to_value, progress, param.curve);
99                return Some(value);
100            } else if time_seconds > end_time {
101                // Past automation - return end value
102                return Some(param.to_value);
103            }
104        }
105
106        // Before any automation - return first start value
107        Some(matching[0].from_value)
108    }
109}
110
111/// Interpolate between two values based on progress and curve type
112fn interpolate_value(from: f32, to: f32, progress: f32, curve: AutomationCurve) -> f32 {
113    let t = progress.clamp(0.0, 1.0);
114
115    let interpolated = match curve {
116        AutomationCurve::Linear => t,
117        AutomationCurve::Exponential => {
118            // Exponential curve (ease-out)
119            if (to - from).abs() < 0.0001 { t } else { t * t }
120        }
121        AutomationCurve::Logarithmic => {
122            // Logarithmic curve (ease-in)
123            1.0 - (1.0 - t) * (1.0 - t)
124        }
125        AutomationCurve::Smooth => {
126            // Smooth ease-in-out (cubic)
127            if t < 0.5 {
128                2.0 * t * t
129            } else {
130                1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
131            }
132        }
133    };
134
135    from + (to - from) * interpolated
136}
137
138/// Parse automation from Value::Map
139pub fn parse_automation_from_value(value: &Value) -> Option<AutomationEnvelope> {
140    if let Value::Map(map) = value {
141        let target = map.get("target").and_then(|v| match v {
142            Value::String(s) | Value::Identifier(s) => Some(s.clone()),
143            _ => None,
144        })?;
145
146        let mut envelope = AutomationEnvelope::new(target);
147
148        // Parse params array
149        if let Some(Value::Array(params_array)) = map.get("params") {
150            for param_value in params_array {
151                if let Some(param) = parse_automation_param(param_value) {
152                    envelope.add_param(param);
153                }
154            }
155        }
156
157        Some(envelope)
158    } else {
159        None
160    }
161}
162
163/// Parse per-param templates from a raw automate body string.
164/// Expects blocks like: param <name> { 0% = 0.0 100% = 1.0 }
165pub fn parse_param_templates_from_raw(raw: &str) -> Vec<AutomationParamTemplate> {
166    use regex::Regex;
167    let mut templates = Vec::new();
168
169    // Find param blocks: param <name> [curve <curveName>] { ... }
170    let Ok(re_block) =
171        Regex::new(r"param\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:curve\s+([^\s{]+)\s*)?\{([^}]*)\}")
172    else {
173        return templates;
174    };
175    let Ok(re_point) = Regex::new(r"([0-9]+(?:\.[0-9]+)?)%?\s*=\s*([\-0-9\.eE]+)") else {
176        return templates;
177    };
178
179    for cap in re_block.captures_iter(raw) {
180        let Some(name_match) = cap.get(1) else {
181            continue;
182        };
183        let name = name_match.as_str().to_string();
184        let curve_str = cap.get(2).map(|m| m.as_str());
185        let Some(body_match) = cap.get(3) else {
186            continue;
187        };
188        let body = body_match.as_str();
189
190        let mut points: Vec<(f32, f32)> = Vec::new();
191        for pcap in re_point.captures_iter(body) {
192            if let (Some(p_str), Some(v_str)) = (pcap.get(1), pcap.get(2)) {
193                if let (Ok(pv), Ok(vv)) =
194                    (p_str.as_str().parse::<f32>(), v_str.as_str().parse::<f32>())
195                {
196                    let frac = (pv / 100.0).clamp(0.0, 1.0);
197                    points.push((frac, vv));
198                }
199            }
200        }
201
202        // Sort by progress fraction
203        points.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
204
205        if !points.is_empty() {
206            // Parse advanced curve if specified
207            let advanced_curve = curve_str.and_then(|s| crate::engine::curves::parse_curve(s));
208
209            templates.push(AutomationParamTemplate {
210                param_name: name,
211                points,
212                curve: AutomationCurve::Linear,
213                advanced_curve,
214            });
215        }
216    }
217
218    templates
219}
220
221/// Evaluate a template at a given progress fraction (0.0..1.0)
222pub fn evaluate_template_at(tpl: &AutomationParamTemplate, progress: f32) -> f32 {
223    let p = progress.clamp(0.0, 1.0);
224    if tpl.points.is_empty() {
225        return 0.0;
226    }
227    // Before first point
228    if p <= tpl.points[0].0 {
229        return tpl.points[0].1;
230    }
231    // After last point
232    if let Some(&(last_progress, last_value)) = tpl.points.last() {
233        if p >= last_progress {
234            return last_value;
235        }
236    }
237
238    // Find segment
239    for w in tpl.points.windows(2) {
240        let (p0, v0) = w[0];
241        let (p1, v1) = w[1];
242        if p >= p0 && p <= p1 {
243            let local = if (p1 - p0).abs() < f32::EPSILON {
244                0.0
245            } else {
246                (p - p0) / (p1 - p0)
247            };
248
249            // Apply advanced curve if specified
250            let eased_local = if let Some(curve) = &tpl.advanced_curve {
251                crate::engine::curves::evaluate_curve(*curve, local)
252            } else {
253                local // Standard linear interpolation
254            };
255
256            return v0 + (v1 - v0) * eased_local;
257        }
258    }
259
260    // Fallback
261    tpl.points.last().map(|(_, value)| *value).unwrap_or(0.0)
262}
263
264/// Parse single automation parameter from Value
265fn parse_automation_param(value: &Value) -> Option<AutomationParam> {
266    if let Value::Map(map) = value {
267        let param_name = map.get("name").and_then(|v| match v {
268            Value::String(s) | Value::Identifier(s) => Some(s.clone()),
269            _ => None,
270        })?;
271
272        let from_value = map.get("from").and_then(|v| match v {
273            Value::Number(n) => Some(*n),
274            _ => None,
275        })?;
276
277        let to_value = map.get("to").and_then(|v| match v {
278            Value::Number(n) => Some(*n),
279            _ => None,
280        })?;
281
282        let start_time = map
283            .get("start")
284            .and_then(|v| match v {
285                Value::Number(n) => Some(*n),
286                _ => None,
287            })
288            .unwrap_or(0.0);
289
290        let duration = map.get("duration").and_then(|v| match v {
291            Value::Number(n) => Some(*n),
292            _ => None,
293        })?;
294
295        let curve = map
296            .get("curve")
297            .and_then(|v| match v {
298                Value::String(s) | Value::Identifier(s) => Some(AutomationCurve::from_str(s)),
299                _ => None,
300            })
301            .unwrap_or(AutomationCurve::Linear);
302
303        Some(AutomationParam {
304            param_name,
305            from_value,
306            to_value,
307            start_time,
308            duration,
309            curve,
310        })
311    } else {
312        None
313    }
314}
315
316/// Automation registry - stores all active automations
317#[derive(Debug, Clone, Default)]
318pub struct AutomationRegistry {
319    envelopes: HashMap<String, AutomationEnvelope>,
320}
321
322impl AutomationRegistry {
323    pub fn new() -> Self {
324        Self {
325            envelopes: HashMap::new(),
326        }
327    }
328
329    /// Register an automation envelope
330    pub fn register(&mut self, envelope: AutomationEnvelope) {
331        self.envelopes.insert(envelope.target.clone(), envelope);
332    }
333
334    /// Get automated value for a target and parameter at a specific time
335    pub fn get_value(&self, target: &str, param_name: &str, time_seconds: f32) -> Option<f32> {
336        self.envelopes
337            .get(target)
338            .and_then(|env| env.get_value(param_name, time_seconds))
339    }
340
341    /// Check if a target has any active automations
342    pub fn has_automation(&self, target: &str) -> bool {
343        self.envelopes.contains_key(target)
344    }
345
346    /// Get all automation targets
347    pub fn targets(&self) -> Vec<String> {
348        self.envelopes.keys().cloned().collect()
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn test_linear_interpolation() {
358        let param = AutomationParam {
359            param_name: "volume".to_string(),
360            from_value: 0.0,
361            to_value: 1.0,
362            start_time: 0.0,
363            duration: 2.0,
364            curve: AutomationCurve::Linear,
365        };
366
367        let mut envelope = AutomationEnvelope::new("synth1".to_string());
368        envelope.add_param(param);
369
370        // At t=0, should be 0.0
371        assert_eq!(envelope.get_value("volume", 0.0), Some(0.0));
372
373        // At t=1 (50%), should be 0.5
374        assert!((envelope.get_value("volume", 1.0).unwrap() - 0.5).abs() < 0.001);
375
376        // At t=2, should be 1.0
377        assert_eq!(envelope.get_value("volume", 2.0), Some(1.0));
378
379        // At t=3 (past), should stay at 1.0
380        assert_eq!(envelope.get_value("volume", 3.0), Some(1.0));
381    }
382
383    #[test]
384    fn test_smooth_curve() {
385        let value_start = interpolate_value(0.0, 1.0, 0.0, AutomationCurve::Smooth);
386        let value_mid = interpolate_value(0.0, 1.0, 0.5, AutomationCurve::Smooth);
387        let value_end = interpolate_value(0.0, 1.0, 1.0, AutomationCurve::Smooth);
388
389        assert_eq!(value_start, 0.0);
390        assert_eq!(value_end, 1.0);
391        assert!(value_mid > 0.4 && value_mid < 0.6); // Should be around 0.5
392    }
393
394    #[test]
395    fn test_automation_registry() {
396        let mut registry = AutomationRegistry::new();
397
398        let mut envelope = AutomationEnvelope::new("synth1".to_string());
399        envelope.add_param(AutomationParam {
400            param_name: "volume".to_string(),
401            from_value: 0.0,
402            to_value: 1.0,
403            start_time: 0.0,
404            duration: 2.0,
405            curve: AutomationCurve::Linear,
406        });
407
408        registry.register(envelope);
409
410        assert!(registry.has_automation("synth1"));
411        assert!(!registry.has_automation("synth2"));
412
413        let value = registry.get_value("synth1", "volume", 1.0);
414        assert!(value.is_some());
415        assert!((value.unwrap() - 0.5).abs() < 0.001);
416    }
417
418    #[test]
419    fn test_zero_duration_param_switches_at_start_time() {
420        let mut envelope = AutomationEnvelope::new("synth1".to_string());
421        envelope.add_param(AutomationParam {
422            param_name: "volume".to_string(),
423            from_value: 0.25,
424            to_value: 0.75,
425            start_time: 2.0,
426            duration: 0.0,
427            curve: AutomationCurve::Linear,
428        });
429
430        assert_eq!(envelope.get_value("volume", 1.0), Some(0.25));
431        assert_eq!(envelope.get_value("volume", 2.0), Some(0.75));
432        assert_eq!(envelope.get_value("volume", 3.0), Some(0.75));
433    }
434
435    #[test]
436    fn test_empty_template_evaluates_to_zero() {
437        let template = AutomationParamTemplate {
438            param_name: "cutoff".to_string(),
439            points: Vec::new(),
440            curve: AutomationCurve::Linear,
441            advanced_curve: None,
442        };
443
444        assert_eq!(evaluate_template_at(&template, 0.5), 0.0);
445    }
446}