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