rawdio 0.14.0

An Audio Engine, inspired by the Web Audio API
Documentation
use crate::Timestamp;

use super::{
    parameter_change::ValueChangeMethod, parameter_value::ParameterValue, ParameterChange,
    ParameterId,
};

use std::sync::atomic::Ordering;

const MAXIMUM_PENDING_PARAMETER_CHANGES: usize = 16;

#[repr(align(64))]
struct ParameterBuffer {
    values: Vec<f32>,
}

impl ParameterBuffer {
    fn with_capacity(capacity: usize) -> Self {
        Self {
            values: Vec::with_capacity(capacity),
        }
    }

    fn reset(&mut self) {
        self.values.clear()
    }

    fn add_value(&mut self, value: f32) {
        self.values.push(value)
    }

    fn get_values(&self, frame_count: usize) -> &[f32] {
        &self.values[..frame_count]
    }

    fn fill(&mut self, value: f32, frame_count: usize) {
        self.values.resize(frame_count, value);
    }
}

pub struct RealtimeAudioParameter {
    id: ParameterId,
    value: ParameterValue,
    parameter_changes: Vec<ParameterChange>,
    parameter_buffer: ParameterBuffer,
    increment: f64,
    coefficient: f64,
    current_change: ParameterChange,
}

impl RealtimeAudioParameter {
    pub fn new(id: ParameterId, value: ParameterValue, maximum_frame_count: usize) -> Self {
        let initial_value = value.load(Ordering::Acquire);

        Self {
            id,
            value,
            parameter_changes: Vec::with_capacity(MAXIMUM_PENDING_PARAMETER_CHANGES),
            parameter_buffer: ParameterBuffer::with_capacity(maximum_frame_count),
            increment: 0.0,
            coefficient: 1.0,
            current_change: ParameterChange {
                value: initial_value,
                end_time: Timestamp::zero(),
                method: ValueChangeMethod::Immediate,
            },
        }
    }

    pub fn get_id(&self) -> ParameterId {
        self.id
    }

    pub fn get_value(&self) -> f64 {
        self.value.load(Ordering::Acquire)
    }

    fn is_static(&self) -> bool {
        if !relative_eq!(self.coefficient, 1.0) {
            return false;
        }

        if !relative_eq!(self.increment, 0.0) {
            return false;
        }

        if !self.parameter_changes.is_empty() {
            return false;
        }

        true
    }

    pub fn process(&mut self, time: &Timestamp, frame_count: usize, sample_rate: usize) {
        self.parameter_buffer.reset();

        let mut value = self.get_value();

        if self.is_static() {
            self.parameter_buffer.fill(value as f32, frame_count);
        } else {
            for frame in 0..frame_count {
                let frame_time = time.incremented_by_samples(frame, sample_rate);
                value = self.value_at_time(&frame_time, sample_rate, value);
                self.parameter_buffer.add_value(value as f32);
            }
        }

        self.set_value(value);
    }

    fn value_at_time(&mut self, time: &Timestamp, sample_rate: usize, value: f64) -> f64 {
        if let Some(next_event) = self.parameter_changes.first() {
            match next_event.method {
                ValueChangeMethod::Immediate => {
                    if next_event.end_time <= *time {
                        self.increment = 0.0;
                        self.coefficient = 1.0;
                        self.current_change = self.parameter_changes.remove(0);
                    }
                }
                ValueChangeMethod::Linear(start_time) => {
                    if *time >= start_time {
                        let duration = next_event.end_time.as_samples(sample_rate)
                            - time.as_samples(sample_rate);
                        let delta = next_event.value - value;

                        self.increment = delta / duration;
                        self.coefficient = 1.0;
                        self.current_change = self.parameter_changes.remove(0);
                    }
                }
                ValueChangeMethod::Exponential(start_time) => {
                    if *time >= start_time {
                        debug_assert_ne!(next_event.value, 0.0);
                        debug_assert_ne!(value, 0.0);

                        let ratio = next_event.value / value;

                        let duration = next_event.end_time.as_samples(sample_rate)
                            - time.as_samples(sample_rate);

                        self.increment = 0.0;
                        self.coefficient = (ratio.ln() / duration).exp();

                        self.current_change = self.parameter_changes.remove(0);
                    }
                }
            };
        }

        if self.current_change.end_time <= *time {
            return self.current_change.value;
        }

        (value * self.coefficient) + self.increment
    }

    pub fn get_values(&self, frame_count: usize) -> &[f32] {
        self.parameter_buffer.get_values(frame_count)
    }

    pub fn set_value(&mut self, value: f64) {
        self.value.store(value, Ordering::Release)
    }

    pub fn add_parameter_change(&mut self, parameter_change: ParameterChange) {
        self.parameter_changes.push(parameter_change);

        self.parameter_changes
            .sort_by(|a, b| a.end_time.partial_cmp(&b.end_time).unwrap());
    }

    pub fn cancel_scheduled_changes_ending_after(&mut self, time: &Timestamp) {
        self.parameter_changes
            .retain(|change| change.end_time >= *time);
    }

    pub fn cancel_scheduled_changes(&mut self) {
        self.parameter_changes.clear();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use approx::assert_relative_eq;
    use atomic_float::AtomicF64;

    fn process_parameter_values(
        parameter: &mut RealtimeAudioParameter,
        from_time: Timestamp,
        to_time: Timestamp,
        sample_rate: usize,
    ) -> Vec<f32> {
        let mut values = Vec::new();

        let start_sample = from_time.as_samples(sample_rate).ceil() as usize;
        let end_sample = to_time.as_samples(sample_rate).ceil() as usize;
        let maximum_frame_count = 512;

        for frame in (start_sample..end_sample).step_by(maximum_frame_count) {
            let frame_end_sample = (frame + maximum_frame_count).min(end_sample);
            let current_time = from_time.incremented_by_samples(frame, sample_rate);
            let frame_count = frame_end_sample - frame;

            parameter.process(&current_time, frame_count, sample_rate);

            values.extend_from_slice(parameter.get_values(frame_count));
        }

        values
    }

    #[test]
    fn immediate_parameter_changes() {
        let value = ParameterValue::new(AtomicF64::new(0.0));
        let maximum_frame_count = 512;

        let mut param = RealtimeAudioParameter::new("param", value, maximum_frame_count);

        param.add_parameter_change(ParameterChange {
            value: 1.0,
            end_time: Timestamp::from_seconds(1.0),
            method: ValueChangeMethod::Immediate,
        });

        param.add_parameter_change(ParameterChange {
            value: 2.0,
            end_time: Timestamp::from_seconds(2.0),
            method: ValueChangeMethod::Immediate,
        });

        param.add_parameter_change(ParameterChange {
            value: 3.0,
            end_time: Timestamp::from_seconds(3.0),
            method: ValueChangeMethod::Immediate,
        });

        let sample_rate = 48_000;
        let max_time = 3.5;
        let values = process_parameter_values(
            &mut param,
            Timestamp::zero(),
            Timestamp::from_seconds(max_time),
            sample_rate,
        );

        let get_value_at_time = |time: f64| {
            let offset = Timestamp::from_seconds(time).as_samples(sample_rate).ceil() as usize;
            assert!(offset < values.len());
            values[offset]
        };

        assert_relative_eq!(get_value_at_time(0.9), 0.0);
        assert_relative_eq!(get_value_at_time(1.0), 1.0);
        assert_relative_eq!(get_value_at_time(1.9), 1.0);
        assert_relative_eq!(get_value_at_time(2.0), 2.0);
        assert_relative_eq!(get_value_at_time(2.9), 2.0);
        assert_relative_eq!(get_value_at_time(3.0), 3.0);
    }

    #[test]
    fn ramped_parameter_changes() {
        let value = ParameterValue::new(AtomicF64::new(0.0));
        let maximum_frame_count = 512;

        let mut param = RealtimeAudioParameter::new("param", value, maximum_frame_count);

        [
            (1.0, Timestamp::zero(), Timestamp::from_seconds(1.0)),
            (
                2.0,
                Timestamp::from_seconds(1.0),
                Timestamp::from_seconds(2.0),
            ),
            (
                3.0,
                Timestamp::from_seconds(2.0),
                Timestamp::from_seconds(3.0),
            ),
        ]
        .iter()
        .for_each(|(value, start_time, end_time)| {
            param.add_parameter_change(ParameterChange {
                value: *value,
                end_time: *end_time,
                method: ValueChangeMethod::Linear(*start_time),
            });
        });

        let sample_rate = 48_000;
        let values = process_parameter_values(
            &mut param,
            Timestamp::zero(),
            Timestamp::from_seconds(3.5),
            sample_rate,
        );

        let get_value_at_time = |time: f64| {
            let offset = Timestamp::from_seconds(time).as_samples(sample_rate).ceil() as usize;
            values[offset]
        };

        assert_relative_eq!(get_value_at_time(0.5), 0.5, epsilon = 1e-3);
        assert_relative_eq!(get_value_at_time(1.0), 1.0, epsilon = 1e-3);
        assert_relative_eq!(get_value_at_time(1.5), 1.5, epsilon = 1e-3);
        assert_relative_eq!(get_value_at_time(2.0), 2.0, epsilon = 1e-3);
        assert_relative_eq!(get_value_at_time(2.5), 2.5, epsilon = 1e-3);
        assert_relative_eq!(get_value_at_time(3.0), 3.0, epsilon = 1e-3);
    }

    #[test]
    fn exponential_ramps() {
        let initial_value = 2.0;
        let value = ParameterValue::new(AtomicF64::new(initial_value));
        let maximum_frame_count = 512;
        let mut param = RealtimeAudioParameter::new("param", value, maximum_frame_count);

        let ramp_duration = Timestamp::from_seconds(1.0);

        param.add_parameter_change(ParameterChange {
            value: 2.0 * initial_value,
            end_time: ramp_duration,
            method: ValueChangeMethod::Exponential(Timestamp::zero()),
        });

        let sample_rate = 96_000;

        let values = process_parameter_values(
            &mut param,
            Timestamp::zero(),
            ramp_duration.incremented_by_seconds(0.1),
            sample_rate,
        );

        let get_value_at_time = |time: f64| {
            let offset = Timestamp::from_seconds(time).as_samples(sample_rate).ceil() as usize;
            values[offset]
        };

        assert_relative_eq!(get_value_at_time(0.0), 2.0, epsilon = 1e-3);
        assert_relative_eq!(get_value_at_time(0.5), 2.0 * 1.414, epsilon = 1e-3);
        assert_relative_eq!(get_value_at_time(1.0), 4.0, epsilon = 1e-3);
    }

    #[test]
    fn zero_time_change_is_immediate() {
        let value = ParameterValue::new(AtomicF64::new(0.0));
        let maximum_frame_count = 512;

        let mut param = RealtimeAudioParameter::new("param", value, maximum_frame_count);

        param.add_parameter_change(ParameterChange {
            value: 1.0,
            end_time: Timestamp::zero(),
            method: ValueChangeMethod::Immediate,
        });

        let sample_rate = 48_000;

        let values = process_parameter_values(
            &mut param,
            Timestamp::from_seconds(2.0),
            Timestamp::from_seconds(4.5),
            sample_rate,
        );

        let get_value_at_time = |time: f64| {
            let offset = Timestamp::from_seconds(time).as_samples(sample_rate).ceil() as usize;
            assert!(offset < values.len());
            values[offset]
        };

        assert_relative_eq!(get_value_at_time(0.0), 1.0);
        assert_relative_eq!(get_value_at_time(1.0), 1.0);
        assert_relative_eq!(get_value_at_time(2.0), 1.0);
    }
}