use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CurveType {
Step,
Linear,
Exponential(f32),
Smooth,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Breakpoint {
pub frame: usize,
pub value: f32,
pub curve: CurveType,
}
impl Breakpoint {
#[must_use]
pub fn new(frame: usize, value: f32, curve: CurveType) -> Self {
Self {
frame,
value,
curve,
}
}
}
#[derive(Debug, Clone)]
pub struct AutomationLane {
breakpoints: Vec<Breakpoint>,
default_value: f32,
}
impl AutomationLane {
#[must_use]
pub fn new(default_value: f32) -> Self {
Self {
breakpoints: Vec::new(),
default_value,
}
}
pub fn add(&mut self, bp: Breakpoint) {
let pos = self
.breakpoints
.binary_search_by_key(&bp.frame, |b| b.frame)
.unwrap_or_else(|i| i);
self.breakpoints.insert(pos, bp);
}
pub fn remove_at(&mut self, frame: usize) {
self.breakpoints.retain(|b| b.frame != frame);
}
pub fn clear(&mut self) {
self.breakpoints.clear();
}
#[must_use]
pub fn len(&self) -> usize {
self.breakpoints.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.breakpoints.is_empty()
}
#[must_use]
pub fn value_at(&self, frame: usize) -> f32 {
if self.breakpoints.is_empty() {
return self.default_value;
}
if frame <= self.breakpoints[0].frame {
return if self.breakpoints[0].curve == CurveType::Step {
self.default_value
} else {
self.breakpoints[0].value
};
}
if frame >= self.breakpoints[self.breakpoints.len() - 1].frame {
return self.breakpoints[self.breakpoints.len() - 1].value;
}
let idx = self
.breakpoints
.binary_search_by_key(&frame, |b| b.frame)
.unwrap_or_else(|i| i);
if idx == 0 {
return self.breakpoints[0].value;
}
let prev = &self.breakpoints[idx - 1];
let next = &self.breakpoints[idx];
interpolate(
prev.value, next.value, prev.frame, next.frame, frame, next.curve,
)
}
pub fn render(&self, output: &mut [f32], start_frame: usize) {
for (i, out) in output.iter_mut().enumerate() {
*out = self.value_at(start_frame + i);
}
}
pub fn render_fast(&self, output: &mut [f32], start_frame: usize) {
if self.breakpoints.is_empty() {
output.fill(self.default_value);
return;
}
let end_frame = start_frame + output.len();
let mut seg_idx = self
.breakpoints
.binary_search_by_key(&start_frame, |b| b.frame)
.unwrap_or_else(|i| i);
for (i, out) in output.iter_mut().enumerate() {
let frame = start_frame + i;
while seg_idx < self.breakpoints.len() && self.breakpoints[seg_idx].frame <= frame {
seg_idx += 1;
}
if seg_idx == 0 {
*out = if self.breakpoints[0].curve == CurveType::Step {
self.default_value
} else {
self.breakpoints[0].value
};
} else if seg_idx >= self.breakpoints.len() {
*out = self.breakpoints[self.breakpoints.len() - 1].value;
} else {
let prev = &self.breakpoints[seg_idx - 1];
let next = &self.breakpoints[seg_idx];
*out = interpolate(
prev.value, next.value, prev.frame, next.frame, frame, next.curve,
);
}
}
let _ = end_frame; }
#[must_use]
pub fn breakpoints(&self) -> &[Breakpoint] {
&self.breakpoints
}
}
#[inline]
fn interpolate(
from: f32,
to: f32,
from_frame: usize,
to_frame: usize,
current_frame: usize,
curve: CurveType,
) -> f32 {
if from_frame == to_frame {
return to;
}
let t = (current_frame - from_frame) as f32 / (to_frame - from_frame) as f32;
let t = t.clamp(0.0, 1.0);
match curve {
CurveType::Step => from,
CurveType::Linear => from + (to - from) * t,
CurveType::Exponential(exp) => {
let exp = exp.max(0.001);
from + (to - from) * t.powf(exp)
}
CurveType::Smooth => {
let t_smooth = (1.0 - (t * std::f32::consts::PI).cos()) * 0.5;
from + (to - from) * t_smooth
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_lane_returns_default() {
let lane = AutomationLane::new(0.5);
assert_eq!(lane.value_at(0), 0.5);
assert_eq!(lane.value_at(44100), 0.5);
}
#[test]
fn single_breakpoint() {
let mut lane = AutomationLane::new(0.0);
lane.add(Breakpoint::new(1000, 1.0, CurveType::Linear));
assert_eq!(lane.value_at(0), 1.0); assert_eq!(lane.value_at(1000), 1.0);
assert_eq!(lane.value_at(2000), 1.0); }
#[test]
fn linear_interpolation() {
let mut lane = AutomationLane::new(0.0);
lane.add(Breakpoint::new(0, 0.0, CurveType::Linear));
lane.add(Breakpoint::new(1000, 1.0, CurveType::Linear));
assert!((lane.value_at(0) - 0.0).abs() < 1e-6);
assert!((lane.value_at(500) - 0.5).abs() < 1e-6);
assert!((lane.value_at(1000) - 1.0).abs() < 1e-6);
}
#[test]
fn step_interpolation() {
let mut lane = AutomationLane::new(0.0);
lane.add(Breakpoint::new(0, 0.0, CurveType::Step));
lane.add(Breakpoint::new(1000, 1.0, CurveType::Step));
assert_eq!(lane.value_at(0), 0.0);
assert_eq!(lane.value_at(500), 0.0); assert_eq!(lane.value_at(1000), 1.0);
}
#[test]
fn exponential_curve() {
let mut lane = AutomationLane::new(0.0);
lane.add(Breakpoint::new(0, 0.0, CurveType::Linear));
lane.add(Breakpoint::new(1000, 1.0, CurveType::Exponential(2.0)));
let mid = lane.value_at(500);
assert!((mid - 0.25).abs() < 0.01, "exp midpoint={mid}");
}
#[test]
fn smooth_curve_midpoint() {
let mut lane = AutomationLane::new(0.0);
lane.add(Breakpoint::new(0, 0.0, CurveType::Linear));
lane.add(Breakpoint::new(1000, 1.0, CurveType::Smooth));
let mid = lane.value_at(500);
assert!((mid - 0.5).abs() < 0.01, "smooth midpoint={mid}");
}
#[test]
fn render_buffer() {
let mut lane = AutomationLane::new(0.0);
lane.add(Breakpoint::new(0, 0.0, CurveType::Linear));
lane.add(Breakpoint::new(100, 1.0, CurveType::Linear));
let mut output = vec![0.0f32; 101];
lane.render(&mut output, 0);
assert!((output[0] - 0.0).abs() < 1e-6);
assert!((output[50] - 0.5).abs() < 1e-6);
assert!((output[100] - 1.0).abs() < 1e-6);
}
#[test]
fn render_fast_matches_render() {
let mut lane = AutomationLane::new(0.5);
lane.add(Breakpoint::new(100, 0.0, CurveType::Linear));
lane.add(Breakpoint::new(500, 1.0, CurveType::Smooth));
lane.add(Breakpoint::new(900, 0.3, CurveType::Exponential(1.5)));
let mut slow = vec![0.0f32; 1000];
let mut fast = vec![0.0f32; 1000];
lane.render(&mut slow, 0);
lane.render_fast(&mut fast, 0);
for (i, (s, f)) in slow.iter().zip(fast.iter()).enumerate() {
assert!((s - f).abs() < 1e-6, "mismatch at {i}: slow={s} fast={f}");
}
}
#[test]
fn add_maintains_order() {
let mut lane = AutomationLane::new(0.0);
lane.add(Breakpoint::new(500, 0.5, CurveType::Linear));
lane.add(Breakpoint::new(100, 0.1, CurveType::Linear));
lane.add(Breakpoint::new(900, 0.9, CurveType::Linear));
let frames: Vec<usize> = lane.breakpoints().iter().map(|b| b.frame).collect();
assert_eq!(frames, vec![100, 500, 900]);
}
#[test]
fn remove_at_frame() {
let mut lane = AutomationLane::new(0.0);
lane.add(Breakpoint::new(100, 1.0, CurveType::Linear));
lane.add(Breakpoint::new(200, 2.0, CurveType::Linear));
lane.remove_at(100);
assert_eq!(lane.len(), 1);
assert_eq!(lane.breakpoints()[0].frame, 200);
}
}