djio 0.0.23

DJ Hardware Control(ler) Support
Documentation
// SPDX-FileCopyrightText: The djio authors
// SPDX-License-Identifier: MPL-2.0

//! Interpolation of parameter values

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RampingMode {
    /// Switch to the target value with the last step
    Step,

    /// Approach the target value by linear interpolation
    Linear,
}

/// Ramping profile
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RampingProfile {
    pub mode: RampingMode,
    pub steps: usize,
}

impl RampingProfile {
    #[must_use]
    pub const fn immediate() -> Self {
        Self {
            mode: RampingMode::Step,
            steps: 0,
        }
    }
}

/// Stepwise interpolation between an initial and a target value.
#[derive(Debug, Clone)]
pub struct RampingF32 {
    profile: RampingProfile,
    initial_value: f32,
    target_value: f32,
    step_value: f32,
    current_step: usize,
}

// ~43.7 sec at 192 kHz
const MAX_LOSSLESS_STEPS_F32: usize = (1usize << 23) - 1; // f32 has a 23-bit mantissa

#[expect(clippy::cast_precision_loss)]
#[inline]
fn steps_as_f32(steps: usize) -> f32 {
    // Comment out this debug assertion if precision loss might
    // occur and is considered acceptable.
    debug_assert!(steps <= MAX_LOSSLESS_STEPS_F32);
    steps as f32
}

impl RampingF32 {
    /// Create an immediate value without interpolation
    #[must_use]
    pub const fn new(value: f32) -> Self {
        Self {
            profile: RampingProfile::immediate(),
            initial_value: value,
            target_value: value,
            step_value: 0f32,
            current_step: 0,
        }
    }

    #[must_use]
    pub const fn profile(&self) -> RampingProfile {
        self.profile
    }

    pub fn reset(&mut self, target_value: f32) {
        self.reset_profile(target_value, self.profile);
    }

    pub fn reset_profile(&mut self, target_value: f32, profile: RampingProfile) {
        self.profile = profile;
        self.initial_value = self.current_value();
        self.target_value = target_value;
        let RampingProfile { mode, steps } = profile;
        self.step_value = if steps > 0 {
            match mode {
                RampingMode::Step => 0f32,
                RampingMode::Linear => (target_value - self.initial_value) / steps_as_f32(steps),
            }
        } else {
            // Never read
            0f32
        };
        self.current_step = 0;
    }

    #[must_use]
    pub fn current_value(&self) -> f32 {
        let RampingProfile { mode, steps } = self.profile;
        if self.current_step < steps {
            match mode {
                RampingMode::Step => self.initial_value,
                RampingMode::Linear => {
                    self.initial_value + self.step_value * steps_as_f32(self.current_step)
                }
            }
        } else {
            self.target_value
        }
    }

    #[must_use]
    pub const fn target_value(&self) -> f32 {
        self.target_value
    }

    #[must_use]
    pub fn remaining_steps(&self) -> usize {
        debug_assert!(self.current_step <= self.profile.steps);
        self.profile.steps - self.current_step
    }

    pub fn advance(&mut self, steps: usize) {
        if steps < self.remaining_steps() {
            self.current_step += steps;
        } else {
            self.current_step = self.profile.steps;
        };
    }
}

/// Iterate over the values generated by [`RampingF32`].
///
/// Iteration starts with the current value.
impl Iterator for RampingF32 {
    type Item = f32;

    /// Returns the current value and advances the iterator
    /// by a single step.
    fn next(&mut self) -> Option<Self::Item> {
        let current_value = self.current_value();
        self.advance(1);
        Some(current_value)
    }
}