use crate::language::syntax::ast::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AutomationCurve {
Linear,
Exponential,
Logarithmic,
Smooth, }
impl AutomationCurve {
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"linear" | "lin" => AutomationCurve::Linear,
"exponential" | "exp" => AutomationCurve::Exponential,
"logarithmic" | "log" => AutomationCurve::Logarithmic,
"smooth" | "ease" => AutomationCurve::Smooth,
_ => AutomationCurve::Linear,
}
}
}
#[derive(Debug, Clone)]
pub struct AutomationParam {
pub param_name: String,
pub from_value: f32,
pub to_value: f32,
pub start_time: f32, pub duration: f32, pub curve: AutomationCurve,
}
#[derive(Debug, Clone)]
pub struct AutomationParamTemplate {
pub param_name: String,
pub points: Vec<(f32, f32)>,
pub curve: AutomationCurve,
pub advanced_curve: Option<crate::engine::curves::CurveType>,
}
#[derive(Debug, Clone)]
pub struct AutomationEnvelope {
pub target: String, pub params: Vec<AutomationParam>,
}
impl AutomationEnvelope {
pub fn new(target: String) -> Self {
Self {
target,
params: Vec::new(),
}
}
pub fn add_param(&mut self, param: AutomationParam) {
self.params.push(param);
}
pub fn get_value(&self, param_name: &str, time_seconds: f32) -> Option<f32> {
let matching: Vec<&AutomationParam> = self
.params
.iter()
.filter(|p| p.param_name == param_name)
.collect();
if matching.is_empty() {
return None;
}
for param in matching.iter().rev() {
let end_time = param.start_time + param.duration;
if time_seconds >= param.start_time && time_seconds <= end_time {
let progress = (time_seconds - param.start_time) / param.duration;
let value =
interpolate_value(param.from_value, param.to_value, progress, param.curve);
return Some(value);
} else if time_seconds > end_time {
return Some(param.to_value);
}
}
Some(matching[0].from_value)
}
}
fn interpolate_value(from: f32, to: f32, progress: f32, curve: AutomationCurve) -> f32 {
let t = progress.clamp(0.0, 1.0);
let interpolated = match curve {
AutomationCurve::Linear => t,
AutomationCurve::Exponential => {
if (to - from).abs() < 0.0001 { t } else { t * t }
}
AutomationCurve::Logarithmic => {
1.0 - (1.0 - t) * (1.0 - t)
}
AutomationCurve::Smooth => {
if t < 0.5 {
2.0 * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
}
}
};
from + (to - from) * interpolated
}
pub fn parse_automation_from_value(value: &Value) -> Option<AutomationEnvelope> {
if let Value::Map(map) = value {
let target = map.get("target").and_then(|v| match v {
Value::String(s) | Value::Identifier(s) => Some(s.clone()),
_ => None,
})?;
let mut envelope = AutomationEnvelope::new(target);
if let Some(Value::Array(params_array)) = map.get("params") {
for param_value in params_array {
if let Some(param) = parse_automation_param(param_value) {
envelope.add_param(param);
}
}
}
Some(envelope)
} else {
None
}
}
pub fn parse_param_templates_from_raw(raw: &str) -> Vec<AutomationParamTemplate> {
use regex::Regex;
let mut templates = Vec::new();
let re_block =
Regex::new(r"param\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:curve\s+([^\s{]+)\s*)?\{([^}]*)\}")
.unwrap();
let re_point = Regex::new(r"([0-9]+(?:\.[0-9]+)?)%?\s*=\s*([\-0-9\.eE]+)").unwrap();
for cap in re_block.captures_iter(raw) {
let name = cap.get(1).unwrap().as_str().to_string();
let curve_str = cap.get(2).map(|m| m.as_str());
let body = cap.get(3).unwrap().as_str();
let mut points: Vec<(f32, f32)> = Vec::new();
for pcap in re_point.captures_iter(body) {
if let (Some(p_str), Some(v_str)) = (pcap.get(1), pcap.get(2)) {
if let (Ok(pv), Ok(vv)) =
(p_str.as_str().parse::<f32>(), v_str.as_str().parse::<f32>())
{
let frac = (pv / 100.0).clamp(0.0, 1.0);
points.push((frac, vv));
}
}
}
points.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
if !points.is_empty() {
let advanced_curve = curve_str.and_then(|s| crate::engine::curves::parse_curve(s));
templates.push(AutomationParamTemplate {
param_name: name,
points,
curve: AutomationCurve::Linear,
advanced_curve,
});
}
}
templates
}
pub fn evaluate_template_at(tpl: &AutomationParamTemplate, progress: f32) -> f32 {
let p = progress.clamp(0.0, 1.0);
if tpl.points.is_empty() {
return 0.0;
}
if p <= tpl.points[0].0 {
return tpl.points[0].1;
}
if p >= tpl.points.last().unwrap().0 {
return tpl.points.last().unwrap().1;
}
for w in tpl.points.windows(2) {
let (p0, v0) = w[0];
let (p1, v1) = w[1];
if p >= p0 && p <= p1 {
let local = if (p1 - p0).abs() < f32::EPSILON {
0.0
} else {
(p - p0) / (p1 - p0)
};
let eased_local = if let Some(curve) = &tpl.advanced_curve {
crate::engine::curves::evaluate_curve(*curve, local)
} else {
local };
return v0 + (v1 - v0) * eased_local;
}
}
tpl.points.last().unwrap().1
}
fn parse_automation_param(value: &Value) -> Option<AutomationParam> {
if let Value::Map(map) = value {
let param_name = map.get("name").and_then(|v| match v {
Value::String(s) | Value::Identifier(s) => Some(s.clone()),
_ => None,
})?;
let from_value = map.get("from").and_then(|v| match v {
Value::Number(n) => Some(*n),
_ => None,
})?;
let to_value = map.get("to").and_then(|v| match v {
Value::Number(n) => Some(*n),
_ => None,
})?;
let start_time = map
.get("start")
.and_then(|v| match v {
Value::Number(n) => Some(*n),
_ => None,
})
.unwrap_or(0.0);
let duration = map.get("duration").and_then(|v| match v {
Value::Number(n) => Some(*n),
_ => None,
})?;
let curve = map
.get("curve")
.and_then(|v| match v {
Value::String(s) | Value::Identifier(s) => Some(AutomationCurve::from_str(s)),
_ => None,
})
.unwrap_or(AutomationCurve::Linear);
Some(AutomationParam {
param_name,
from_value,
to_value,
start_time,
duration,
curve,
})
} else {
None
}
}
#[derive(Debug, Clone, Default)]
pub struct AutomationRegistry {
envelopes: HashMap<String, AutomationEnvelope>,
}
impl AutomationRegistry {
pub fn new() -> Self {
Self {
envelopes: HashMap::new(),
}
}
pub fn register(&mut self, envelope: AutomationEnvelope) {
self.envelopes.insert(envelope.target.clone(), envelope);
}
pub fn get_value(&self, target: &str, param_name: &str, time_seconds: f32) -> Option<f32> {
self.envelopes
.get(target)
.and_then(|env| env.get_value(param_name, time_seconds))
}
pub fn has_automation(&self, target: &str) -> bool {
self.envelopes.contains_key(target)
}
pub fn targets(&self) -> Vec<String> {
self.envelopes.keys().cloned().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_linear_interpolation() {
let param = AutomationParam {
param_name: "volume".to_string(),
from_value: 0.0,
to_value: 1.0,
start_time: 0.0,
duration: 2.0,
curve: AutomationCurve::Linear,
};
let mut envelope = AutomationEnvelope::new("synth1".to_string());
envelope.add_param(param);
assert_eq!(envelope.get_value("volume", 0.0), Some(0.0));
assert!((envelope.get_value("volume", 1.0).unwrap() - 0.5).abs() < 0.001);
assert_eq!(envelope.get_value("volume", 2.0), Some(1.0));
assert_eq!(envelope.get_value("volume", 3.0), Some(1.0));
}
#[test]
fn test_smooth_curve() {
let value_start = interpolate_value(0.0, 1.0, 0.0, AutomationCurve::Smooth);
let value_mid = interpolate_value(0.0, 1.0, 0.5, AutomationCurve::Smooth);
let value_end = interpolate_value(0.0, 1.0, 1.0, AutomationCurve::Smooth);
assert_eq!(value_start, 0.0);
assert_eq!(value_end, 1.0);
assert!(value_mid > 0.4 && value_mid < 0.6); }
#[test]
fn test_automation_registry() {
let mut registry = AutomationRegistry::new();
let mut envelope = AutomationEnvelope::new("synth1".to_string());
envelope.add_param(AutomationParam {
param_name: "volume".to_string(),
from_value: 0.0,
to_value: 1.0,
start_time: 0.0,
duration: 2.0,
curve: AutomationCurve::Linear,
});
registry.register(envelope);
assert!(registry.has_automation("synth1"));
assert!(!registry.has_automation("synth2"));
let value = registry.get_value("synth1", "volume", 1.0);
assert!(value.is_some());
assert!((value.unwrap() - 0.5).abs() < 0.001);
}
}