devalang_wasm/engine/audio/
automation.rs1use crate::language::syntax::ast::Value;
2use std::collections::HashMap;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum AutomationCurve {
9 Linear,
10 Exponential,
11 Logarithmic,
12 Smooth, }
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#[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, pub duration: f32, pub curve: AutomationCurve,
36}
37
38#[derive(Debug, Clone)]
40pub struct AutomationParamTemplate {
41 pub param_name: String,
42 pub points: Vec<(f32, f32)>,
44 pub curve: AutomationCurve,
45 pub advanced_curve: Option<crate::engine::curves::CurveType>,
47}
48
49#[derive(Debug, Clone)]
51pub struct AutomationEnvelope {
52 pub target: String, 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 pub fn add_param(&mut self, param: AutomationParam) {
66 self.params.push(param);
67 }
68
69 pub fn get_value(&self, param_name: &str, time_seconds: f32) -> Option<f32> {
71 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 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 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 return Some(param.to_value);
95 }
96 }
97
98 Some(matching[0].from_value)
100 }
101}
102
103fn 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 if (to - from).abs() < 0.0001 { t } else { t * t }
112 }
113 AutomationCurve::Logarithmic => {
114 1.0 - (1.0 - t) * (1.0 - t)
116 }
117 AutomationCurve::Smooth => {
118 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
130pub 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 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
155pub fn parse_param_templates_from_raw(raw: &str) -> Vec<AutomationParamTemplate> {
158 use regex::Regex;
159 let mut templates = Vec::new();
160
161 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 points.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
186
187 if !points.is_empty() {
188 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
203pub 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 if p <= tpl.points[0].0 {
211 return tpl.points[0].1;
212 }
213 if p >= tpl.points.last().unwrap().0 {
215 return tpl.points.last().unwrap().1;
216 }
217
218 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 let eased_local = if let Some(curve) = &tpl.advanced_curve {
231 crate::engine::curves::evaluate_curve(*curve, local)
232 } else {
233 local };
235
236 return v0 + (v1 - v0) * eased_local;
237 }
238 }
239
240 tpl.points.last().unwrap().1
242}
243
244fn 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#[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 pub fn register(&mut self, envelope: AutomationEnvelope) {
311 self.envelopes.insert(envelope.target.clone(), envelope);
312 }
313
314 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 pub fn has_automation(&self, target: &str) -> bool {
323 self.envelopes.contains_key(target)
324 }
325
326 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 assert_eq!(envelope.get_value("volume", 0.0), Some(0.0));
352
353 assert!((envelope.get_value("volume", 1.0).unwrap() - 0.5).abs() < 0.001);
355
356 assert_eq!(envelope.get_value("volume", 2.0), Some(1.0));
358
359 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); }
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}