Lightningbeam/daw-backend/src/audio/automation.rs

280 lines
8.1 KiB
Rust

/// Automation system for parameter modulation over time
use serde::{Deserialize, Serialize};
/// Unique identifier for automation lanes
pub type AutomationLaneId = u32;
/// Unique identifier for parameters that can be automated
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ParameterId {
/// Track volume
TrackVolume,
/// Track pan
TrackPan,
/// Effect parameter (effect_index, param_id)
EffectParameter(usize, u32),
/// Metatrack time stretch
TimeStretch,
/// Metatrack offset
TimeOffset,
}
/// Type of interpolation curve between automation points
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum CurveType {
/// Linear interpolation (straight line)
Linear,
/// Exponential curve (smooth acceleration)
Exponential,
/// S-curve (ease in/out)
SCurve,
/// Step (no interpolation, jump to next value)
Step,
}
/// A single automation point
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct AutomationPoint {
/// Time in seconds
pub time: f64,
/// Parameter value (normalized 0.0 to 1.0, or actual value depending on parameter)
pub value: f32,
/// Curve type to next point
pub curve: CurveType,
}
impl AutomationPoint {
/// Create a new automation point
pub fn new(time: f64, value: f32, curve: CurveType) -> Self {
Self { time, value, curve }
}
}
/// An automation lane for a specific parameter
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutomationLane {
/// Unique identifier for this lane
pub id: AutomationLaneId,
/// Which parameter this lane controls
pub parameter_id: ParameterId,
/// Sorted list of automation points
points: Vec<AutomationPoint>,
/// Whether this lane is enabled
pub enabled: bool,
}
impl AutomationLane {
/// Create a new automation lane
pub fn new(id: AutomationLaneId, parameter_id: ParameterId) -> Self {
Self {
id,
parameter_id,
points: Vec::new(),
enabled: true,
}
}
/// Add an automation point, maintaining sorted order
pub fn add_point(&mut self, point: AutomationPoint) {
// Find insertion position to maintain sorted order
let pos = self.points.binary_search_by(|p| {
p.time.partial_cmp(&point.time).unwrap_or(std::cmp::Ordering::Equal)
});
match pos {
Ok(idx) => {
// Replace existing point at same time
self.points[idx] = point;
}
Err(idx) => {
// Insert at correct position
self.points.insert(idx, point);
}
}
}
/// Remove point at specific time
pub fn remove_point_at_time(&mut self, time: f64, tolerance: f64) -> bool {
if let Some(idx) = self.points.iter().position(|p| (p.time - time).abs() < tolerance) {
self.points.remove(idx);
true
} else {
false
}
}
/// Remove all points
pub fn clear(&mut self) {
self.points.clear();
}
/// Get all points
pub fn points(&self) -> &[AutomationPoint] {
&self.points
}
/// Get value at a specific time with interpolation
pub fn evaluate(&self, time: f64) -> Option<f32> {
if !self.enabled || self.points.is_empty() {
return None;
}
// Before first point
if time <= self.points[0].time {
return Some(self.points[0].value);
}
// After last point
if time >= self.points[self.points.len() - 1].time {
return Some(self.points[self.points.len() - 1].value);
}
// Find surrounding points
for i in 0..self.points.len() - 1 {
let p1 = &self.points[i];
let p2 = &self.points[i + 1];
if time >= p1.time && time <= p2.time {
return Some(interpolate(p1, p2, time));
}
}
None
}
/// Get number of points
pub fn point_count(&self) -> usize {
self.points.len()
}
}
/// Interpolate between two automation points based on curve type
fn interpolate(p1: &AutomationPoint, p2: &AutomationPoint, time: f64) -> f32 {
// Calculate normalized position between points (0.0 to 1.0)
let t = if p2.time == p1.time {
0.0
} else {
((time - p1.time) / (p2.time - p1.time)) as f32
};
// Apply curve
let curved_t = match p1.curve {
CurveType::Linear => t,
CurveType::Exponential => {
// Exponential curve: y = x^2
t * t
}
CurveType::SCurve => {
// Smooth S-curve using smoothstep
smoothstep(t)
}
CurveType::Step => {
// Step: hold value until next point
return p1.value;
}
};
// Linear interpolation with curved t
p1.value + (p2.value - p1.value) * curved_t
}
/// Smoothstep function for S-curve interpolation
/// Returns a smooth curve from 0 to 1
#[inline]
fn smoothstep(t: f32) -> f32 {
// Clamp to [0, 1]
let t = t.clamp(0.0, 1.0);
// 3t^2 - 2t^3
t * t * (3.0 - 2.0 * t)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_points_sorted() {
let mut lane = AutomationLane::new(0, ParameterId::TrackVolume);
lane.add_point(AutomationPoint::new(2.0, 0.5, CurveType::Linear));
lane.add_point(AutomationPoint::new(1.0, 0.3, CurveType::Linear));
lane.add_point(AutomationPoint::new(3.0, 0.8, CurveType::Linear));
assert_eq!(lane.points().len(), 3);
assert_eq!(lane.points()[0].time, 1.0);
assert_eq!(lane.points()[1].time, 2.0);
assert_eq!(lane.points()[2].time, 3.0);
}
#[test]
fn test_replace_point_at_same_time() {
let mut lane = AutomationLane::new(0, ParameterId::TrackVolume);
lane.add_point(AutomationPoint::new(1.0, 0.3, CurveType::Linear));
lane.add_point(AutomationPoint::new(1.0, 0.5, CurveType::Linear));
assert_eq!(lane.points().len(), 1);
assert_eq!(lane.points()[0].value, 0.5);
}
#[test]
fn test_linear_interpolation() {
let mut lane = AutomationLane::new(0, ParameterId::TrackVolume);
lane.add_point(AutomationPoint::new(0.0, 0.0, CurveType::Linear));
lane.add_point(AutomationPoint::new(1.0, 1.0, CurveType::Linear));
assert_eq!(lane.evaluate(0.0), Some(0.0));
assert_eq!(lane.evaluate(0.5), Some(0.5));
assert_eq!(lane.evaluate(1.0), Some(1.0));
}
#[test]
fn test_step_interpolation() {
let mut lane = AutomationLane::new(0, ParameterId::TrackVolume);
lane.add_point(AutomationPoint::new(0.0, 0.5, CurveType::Step));
lane.add_point(AutomationPoint::new(1.0, 1.0, CurveType::Step));
assert_eq!(lane.evaluate(0.0), Some(0.5));
assert_eq!(lane.evaluate(0.5), Some(0.5));
assert_eq!(lane.evaluate(0.99), Some(0.5));
assert_eq!(lane.evaluate(1.0), Some(1.0));
}
#[test]
fn test_evaluate_outside_range() {
let mut lane = AutomationLane::new(0, ParameterId::TrackVolume);
lane.add_point(AutomationPoint::new(1.0, 0.5, CurveType::Linear));
lane.add_point(AutomationPoint::new(2.0, 1.0, CurveType::Linear));
// Before first point
assert_eq!(lane.evaluate(0.0), Some(0.5));
// After last point
assert_eq!(lane.evaluate(3.0), Some(1.0));
}
#[test]
fn test_disabled_lane() {
let mut lane = AutomationLane::new(0, ParameterId::TrackVolume);
lane.add_point(AutomationPoint::new(0.0, 0.5, CurveType::Linear));
lane.enabled = false;
assert_eq!(lane.evaluate(0.0), None);
}
#[test]
fn test_remove_point() {
let mut lane = AutomationLane::new(0, ParameterId::TrackVolume);
lane.add_point(AutomationPoint::new(1.0, 0.5, CurveType::Linear));
lane.add_point(AutomationPoint::new(2.0, 0.8, CurveType::Linear));
assert!(lane.remove_point_at_time(1.0, 0.001));
assert_eq!(lane.points().len(), 1);
assert_eq!(lane.points()[0].time, 2.0);
}
}