use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
pub const DEFAULT_CROSSFADE_DURATION: Duration = Duration::from_millis(5);
pub fn default_crossfade_samples(sample_rate: u32) -> u64 {
(DEFAULT_CROSSFADE_DURATION.as_secs_f64() * sample_rate as f64) as u64
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CrossfadeCurve {
Linear,
EqualPower,
}
impl CrossfadeCurve {
pub fn gains(&self, t: f32) -> (f32, f32) {
let t = t.clamp(0.0, 1.0);
match self {
CrossfadeCurve::Linear => (1.0 - t, t),
CrossfadeCurve::EqualPower => {
let angle = t * std::f32::consts::FRAC_PI_2;
(angle.cos(), angle.sin())
}
}
}
}
pub struct GainEnvelope {
start_gain: f32,
end_gain: f32,
curve: CrossfadeCurve,
duration_samples: u64,
position: AtomicU64,
}
impl GainEnvelope {
pub fn fade_in(duration_samples: u64, curve: CrossfadeCurve) -> Self {
Self {
start_gain: 0.0,
end_gain: 1.0,
curve,
duration_samples,
position: AtomicU64::new(0),
}
}
pub fn fade_out(duration_samples: u64, curve: CrossfadeCurve) -> Self {
Self {
start_gain: 1.0,
end_gain: 0.0,
curve,
duration_samples,
position: AtomicU64::new(0),
}
}
pub fn new(
start_gain: f32,
end_gain: f32,
duration_samples: u64,
curve: CrossfadeCurve,
) -> Self {
Self {
start_gain,
end_gain,
curve,
duration_samples,
position: AtomicU64::new(0),
}
}
pub fn advance(&self, frame_count: u64) -> f32 {
let pos = self.position.fetch_add(frame_count, Ordering::Relaxed);
self.gain_at(pos)
}
pub fn gain_at(&self, position: u64) -> f32 {
if self.duration_samples == 0 {
return self.end_gain;
}
let t = (position as f32 / self.duration_samples as f32).clamp(0.0, 1.0);
let (fade_out, fade_in) = self.curve.gains(t);
self.start_gain * fade_out + self.end_gain * fade_in
}
pub fn is_finished(&self) -> bool {
self.position.load(Ordering::Relaxed) >= self.duration_samples
}
pub fn position(&self) -> u64 {
self.position.load(Ordering::Relaxed)
}
pub fn end_gain(&self) -> f32 {
self.end_gain
}
pub fn start_gain(&self) -> f32 {
self.start_gain
}
pub fn duration_samples(&self) -> u64 {
self.duration_samples
}
pub fn curve(&self) -> CrossfadeCurve {
self.curve
}
}
impl std::fmt::Debug for GainEnvelope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GainEnvelope")
.field("start_gain", &self.start_gain)
.field("end_gain", &self.end_gain)
.field("curve", &self.curve)
.field("duration_samples", &self.duration_samples)
.field("position", &self.position.load(Ordering::Relaxed))
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linear_curve_endpoints() {
let (fo, fi) = CrossfadeCurve::Linear.gains(0.0);
assert!((fo - 1.0).abs() < f32::EPSILON);
assert!((fi - 0.0).abs() < f32::EPSILON);
let (fo, fi) = CrossfadeCurve::Linear.gains(1.0);
assert!((fo - 0.0).abs() < f32::EPSILON);
assert!((fi - 1.0).abs() < f32::EPSILON);
}
#[test]
fn linear_curve_midpoint() {
let (fo, fi) = CrossfadeCurve::Linear.gains(0.5);
assert!((fo - 0.5).abs() < f32::EPSILON);
assert!((fi - 0.5).abs() < f32::EPSILON);
}
#[test]
fn equal_power_endpoints() {
let (fo, fi) = CrossfadeCurve::EqualPower.gains(0.0);
assert!((fo - 1.0).abs() < 0.001);
assert!((fi - 0.0).abs() < 0.001);
let (fo, fi) = CrossfadeCurve::EqualPower.gains(1.0);
assert!((fo - 0.0).abs() < 0.001);
assert!((fi - 1.0).abs() < 0.001);
}
#[test]
fn equal_power_constant_power() {
for i in 0..=100 {
let t = i as f32 / 100.0;
let (fo, fi) = CrossfadeCurve::EqualPower.gains(t);
let power = fo * fo + fi * fi;
assert!(
(power - 1.0).abs() < 0.001,
"Power should be ~1.0 at t={}, got {} (fo={}, fi={})",
t,
power,
fo,
fi
);
}
}
#[test]
fn linear_curve_clamped() {
let (fo, fi) = CrossfadeCurve::Linear.gains(-0.5);
assert!((fo - 1.0).abs() < f32::EPSILON);
assert!((fi - 0.0).abs() < f32::EPSILON);
let (fo, fi) = CrossfadeCurve::Linear.gains(1.5);
assert!((fo - 0.0).abs() < f32::EPSILON);
assert!((fi - 1.0).abs() < f32::EPSILON);
}
#[test]
fn gain_envelope_fade_in() {
let env = GainEnvelope::fade_in(1000, CrossfadeCurve::Linear);
let g = env.advance(0);
assert!((g - 0.0).abs() < 0.01);
let g = env.advance(500);
assert!((g - 0.0).abs() < 0.01);
let g = env.advance(0);
assert!((g - 0.5).abs() < 0.01);
let g = env.advance(500);
assert!((g - 0.5).abs() < 0.01);
assert!(env.is_finished());
let g = env.advance(0);
assert!((g - 1.0).abs() < 0.01);
}
#[test]
fn gain_envelope_fade_out() {
let env = GainEnvelope::fade_out(1000, CrossfadeCurve::Linear);
let g = env.advance(0);
assert!((g - 1.0).abs() < 0.01);
env.advance(500);
let g = env.advance(0);
assert!((g - 0.5).abs() < 0.01);
env.advance(500);
assert!(env.is_finished());
let g = env.advance(0);
assert!((g - 0.0).abs() < 0.01);
}
#[test]
fn gain_envelope_is_finished() {
let env = GainEnvelope::fade_out(100, CrossfadeCurve::Linear);
assert!(!env.is_finished());
env.advance(50);
assert!(!env.is_finished());
env.advance(50);
assert!(env.is_finished());
}
#[test]
fn gain_envelope_advance_increments() {
let env = GainEnvelope::fade_in(1000, CrossfadeCurve::Linear);
assert_eq!(env.position(), 0);
env.advance(100);
assert_eq!(env.position(), 100);
env.advance(200);
assert_eq!(env.position(), 300);
env.advance(700);
assert_eq!(env.position(), 1000);
assert!(env.is_finished());
}
#[test]
fn gain_envelope_zero_duration() {
let env = GainEnvelope::fade_in(0, CrossfadeCurve::Linear);
let g = env.advance(0);
assert!((g - 1.0).abs() < f32::EPSILON);
assert!(env.is_finished());
}
#[test]
fn gain_envelope_equal_power_fade_out() {
let env = GainEnvelope::fade_out(1000, CrossfadeCurve::EqualPower);
let g = env.gain_at(0);
assert!((g - 1.0).abs() < 0.01);
let g = env.gain_at(500);
assert!(
(g - 0.707).abs() < 0.01,
"Equal-power midpoint should be ~0.707, got {}",
g
);
let g = env.gain_at(1000);
assert!((g - 0.0).abs() < 0.01);
}
#[test]
fn gain_envelope_custom() {
let env = GainEnvelope::new(0.5, 0.8, 1000, CrossfadeCurve::Linear);
let g = env.gain_at(0);
assert!((g - 0.5).abs() < 0.01);
let g = env.gain_at(500);
assert!((g - 0.65).abs() < 0.01);
let g = env.gain_at(1000);
assert!((g - 0.8).abs() < 0.01);
}
#[test]
fn crossfade_duration_is_rhythmically_negligible() {
assert!(
DEFAULT_CROSSFADE_DURATION <= Duration::from_millis(10),
"Crossfade duration {:?} is too large for rhythmically-tight section loops",
DEFAULT_CROSSFADE_DURATION
);
}
#[test]
fn section_loop_triggers_stay_on_grid() {
let section_start = Duration::from_secs(10);
let section_end = Duration::from_secs(18);
let section_duration = section_end - section_start;
let crossfade_duration = DEFAULT_CROSSFADE_DURATION;
let mut next_trigger = section_end;
let iterations = 100;
for i in 0..iterations {
let expected_trigger = section_end + section_duration * i;
assert_eq!(
next_trigger, expected_trigger,
"Trigger {} drifted: expected {:?}, got {:?}",
i, expected_trigger, next_trigger
);
let _simulated_elapsed = next_trigger - crossfade_duration;
next_trigger += section_duration;
}
let expected_final = section_end + section_duration * iterations;
assert_eq!(next_trigger, expected_final);
}
#[test]
fn old_trigger_scheduling_would_drift() {
let section_start = Duration::from_secs(10);
let section_end = Duration::from_secs(18);
let section_duration = section_end - section_start;
let crossfade_duration = DEFAULT_CROSSFADE_DURATION;
let mut next_trigger = section_end;
let iterations: u32 = 100;
for _ in 0..iterations {
let simulated_elapsed = next_trigger - crossfade_duration;
next_trigger = simulated_elapsed + section_duration;
}
let expected_ideal = section_end + section_duration * iterations;
let total_drift = expected_ideal - next_trigger;
assert_eq!(
total_drift,
crossfade_duration * iterations,
"Old scheduling should drift by crossfade_duration per iteration"
);
}
}