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 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 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 return Some(param.to_value);
103 }
104 }
105
106 Some(matching[0].from_value)
108 }
109}
110
111fn 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 if (to - from).abs() < 0.0001 { t } else { t * t }
120 }
121 AutomationCurve::Logarithmic => {
122 1.0 - (1.0 - t) * (1.0 - t)
124 }
125 AutomationCurve::Smooth => {
126 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
138pub 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 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
163pub fn parse_param_templates_from_raw(raw: &str) -> Vec<AutomationParamTemplate> {
166 use regex::Regex;
167 let mut templates = Vec::new();
168
169 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 points.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
204
205 if !points.is_empty() {
206 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
221pub 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 if p <= tpl.points[0].0 {
229 return tpl.points[0].1;
230 }
231 if let Some(&(last_progress, last_value)) = tpl.points.last() {
233 if p >= last_progress {
234 return last_value;
235 }
236 }
237
238 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 let eased_local = if let Some(curve) = &tpl.advanced_curve {
251 crate::engine::curves::evaluate_curve(*curve, local)
252 } else {
253 local };
255
256 return v0 + (v1 - v0) * eased_local;
257 }
258 }
259
260 tpl.points.last().map(|(_, value)| *value).unwrap_or(0.0)
262}
263
264fn 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#[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 pub fn register(&mut self, envelope: AutomationEnvelope) {
331 self.envelopes.insert(envelope.target.clone(), envelope);
332 }
333
334 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 pub fn has_automation(&self, target: &str) -> bool {
343 self.envelopes.contains_key(target)
344 }
345
346 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 assert_eq!(envelope.get_value("volume", 0.0), Some(0.0));
372
373 assert!((envelope.get_value("volume", 1.0).unwrap() - 0.5).abs() < 0.001);
375
376 assert_eq!(envelope.get_value("volume", 2.0), Some(1.0));
378
379 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); }
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}