diff --git a/src/cycle.rs b/src/cycle.rs index 4280bdb..6f18807 100644 --- a/src/cycle.rs +++ b/src/cycle.rs @@ -1,9 +1,13 @@ use alloc::sync::Arc; use core::cell::Cell; -use crate::{frame, math::Float, Frame, Frames, Seek, Signal}; +use crate::{frame, frame::lerp, math::Float, Frame, Frames, Seek, Signal}; /// Loops [`Frames`] end-to-end to construct a repeating signal +/// +/// To avoid glitching at the loop point, the underlying `Frames` *must* be specially prepared to +/// provide a smooth transition. +#[derive(Clone)] pub struct Cycle { /// Current playback time, in samples cursor: Cell, @@ -11,8 +15,23 @@ pub struct Cycle { } impl Cycle { + /// Construct cycle from `frames` played at `rate` Hz, smoothing the loop point over + /// `crossfade_size` seconds + /// + /// Suitable for use with arbitrary audio, + pub fn with_crossfade(crossfade_size: f32, rate: u32, frames: &[T]) -> Self + where + T: Frame + Copy, + { + let mut frames = frames.iter().copied().collect::>(); + let frames = apply_crossfade((crossfade_size * rate as f32) as usize, &mut frames); + Self::new(Frames::from_slice(rate, frames)) + } + /// Construct cycle from `frames` - // TODO: Crossfade + /// + /// `frames` *must* be specially constructed to loop seamlessly. For arbitrary audio, use + /// [`with_crossfade`](Self::with_crossfade) instead. pub fn new(frames: Arc>) -> Self { Self { cursor: Cell::new(0.0), @@ -52,6 +71,18 @@ impl Seek for Cycle { } } +/// Prepare arbitrary frames for glitch-free use in `Cycle` +fn apply_crossfade(size: usize, frames: &mut [T]) -> &mut [T] { + let end = frames.len() - size; + for i in 0..size { + let a = frames[end + i]; + let b = frames[i]; + let t = (i + 1) as f32 / (size + 1) as f32; + frames[i] = lerp(&a, &b, t); + } + &mut frames[..end] +} + #[cfg(test)] mod tests { use super::*; @@ -74,4 +105,14 @@ mod tests { s.sample(1.0, &mut buf[2..]); assert_eq!(buf, [1.0, 2.0, 3.0, 1.0, 2.0]); } + + #[test] + fn crossfade() { + let mut frames = [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]; + let faded = apply_crossfade(4, &mut frames); + assert_eq!(faded.len(), 5); + for i in 0..5 { + assert!((faded[i] - (5 - (i + 1)) as f32 / 5.0).abs() < 1e-3); + } + } }