rawdio 0.14.0

An Audio Engine, inspired by the Web Audio API
Documentation
use std::time::Duration;

use crate::Level;

enum Phase {
    Idle,
    Attack,
    Decay,
    Sustain,
    Release,
}

pub struct AdsrEnvelope {
    sample_rate: usize,
    phase: Phase,
    envelope: f64,
    attack_coefficient: f64,
    decay_coefficient: f64,
    sustain_level: Level,
    release_coefficient: f64,

    decay_time: Duration,
    release_time: Duration,
}

const ATTACK_TARGET: f64 = 1.1;
const DECAY_TARGET: f64 = -0.1;
const RELEASE_TARGET: f64 = -0.1;

impl AdsrEnvelope {
    pub fn new(
        sample_rate: usize,
        attack_time: Duration,
        decay_time: Duration,
        sustain_level: Level,
        release_time: Duration,
    ) -> Self {
        Self {
            sample_rate,
            phase: Phase::Idle,
            envelope: 0.0,
            attack_coefficient: calculate_attack_coefficient(attack_time, sample_rate),
            decay_coefficient: calculate_decay_coefficient(decay_time, sustain_level, sample_rate),
            sustain_level,
            release_coefficient: calculate_release_coefficient(
                release_time,
                sustain_level,
                sample_rate,
            ),
            decay_time,
            release_time,
        }
    }

    pub fn open(&mut self) {
        self.phase = Phase::Attack;
    }

    pub fn close(&mut self) {
        self.phase = match self.phase {
            Phase::Idle => Phase::Idle,
            _ => Phase::Release,
        };
    }

    pub fn set_attack_time(&mut self, attack_time: Duration) {
        self.attack_coefficient = calculate_attack_coefficient(attack_time, self.sample_rate);
    }

    pub fn set_decay_time(&mut self, decay_time: Duration) {
        self.decay_coefficient =
            calculate_decay_coefficient(decay_time, self.sustain_level, self.sample_rate);
        self.decay_time = decay_time;
    }

    pub fn set_sustain_level(&mut self, sustain_level: Level) {
        self.decay_coefficient =
            calculate_decay_coefficient(self.decay_time, sustain_level, self.sample_rate);
        self.release_coefficient =
            calculate_release_coefficient(self.release_time, sustain_level, self.sample_rate);
        self.sustain_level = sustain_level;
    }

    pub fn set_release_time(&mut self, release_time: Duration) {
        self.release_coefficient =
            calculate_release_coefficient(release_time, self.sustain_level, self.sample_rate);
        self.release_time = release_time;
    }

    pub fn process(&mut self) -> f64 {
        self.envelope = match self.phase {
            Phase::Idle => 0.0,
            Phase::Attack => {
                let output =
                    process_exponential(self.envelope, ATTACK_TARGET, self.attack_coefficient);
                let output = output.min(1.0);

                if output >= 1.0 {
                    self.phase = Phase::Decay;
                }

                output
            }
            Phase::Decay => {
                let output =
                    process_exponential(self.envelope, DECAY_TARGET, self.decay_coefficient);
                let output = output.max(self.sustain_level.as_linear());

                if output <= self.sustain_level.as_linear() {
                    self.phase = Phase::Sustain;
                }

                output
            }
            Phase::Sustain => self.sustain_level.as_linear(),
            Phase::Release => {
                let output =
                    process_exponential(self.envelope, RELEASE_TARGET, self.release_coefficient);
                let output = output.max(0.0);

                if output <= 0.0 {
                    self.phase = Phase::Idle;
                }

                output
            }
        };

        self.envelope
    }
}

fn process_exponential(previous_value: f64, target_value: f64, coefficient: f64) -> f64 {
    target_value + coefficient * (previous_value - target_value)
}

fn calculate_coefficient(log_term: f64, time: Duration, sample_rate: usize) -> f64 {
    if time.as_secs_f64() <= 0.0 {
        return 0.0;
    }

    (log_term.ln() / (time.as_secs_f64() * sample_rate as f64)).exp()
}

fn calculate_attack_coefficient(attack_time: Duration, sample_rate: usize) -> f64 {
    calculate_coefficient(
        (ATTACK_TARGET - 1.0) / ATTACK_TARGET,
        attack_time,
        sample_rate,
    )
}

fn calculate_decay_coefficient(
    decay_time: Duration,
    sustain_level: Level,
    sample_rate: usize,
) -> f64 {
    calculate_coefficient(
        (sustain_level.as_linear() - DECAY_TARGET) / (1.0 - DECAY_TARGET),
        decay_time,
        sample_rate,
    )
}

fn calculate_release_coefficient(
    release_time: Duration,
    sustain_level: Level,
    sample_rate: usize,
) -> f64 {
    calculate_coefficient(
        (-RELEASE_TARGET) / (sustain_level.as_linear() - RELEASE_TARGET),
        release_time,
        sample_rate,
    )
}

#[cfg(test)]
mod tests {
    use itertools::Itertools;

    use super::*;

    const SAMPLE_RATE: usize = 48_000;
    const ATTACK_TIME: Duration = Duration::from_millis(100);
    const DECAY_TIME: Duration = Duration::from_millis(200);
    const RELEASE_TIME: Duration = Duration::from_millis(500);
    const SUSTAIN_LEVEL: f64 = 0.5;

    struct Fixture {
        envelope: AdsrEnvelope,
    }

    impl Fixture {
        fn new() -> Self {
            Self {
                envelope: AdsrEnvelope::new(
                    SAMPLE_RATE,
                    ATTACK_TIME,
                    DECAY_TIME,
                    Level::from_linear(SUSTAIN_LEVEL),
                    RELEASE_TIME,
                ),
            }
        }

        fn note_on(&mut self) {
            self.envelope.open()
        }

        fn note_off(&mut self) {
            self.envelope.close()
        }

        fn process(&mut self, duration: Duration) -> Vec<f64> {
            let sample_count = (duration.as_secs_f64() * SAMPLE_RATE as f64).ceil() as usize;
            (0..sample_count).map(|_| self.envelope.process()).collect()
        }
    }

    #[test]
    fn attack_phase() {
        let mut fixture = Fixture::new();
        fixture.note_on();
        let values = fixture.process(ATTACK_TIME);

        assert!(values.iter().tuple_windows().all(|(a, b)| a <= b));
        assert!(values.iter().all(|value| 0.0 <= *value && *value <= 1.0));

        let next_values = fixture.process(Duration::from_secs(1));
        assert_eq!(*next_values.first().unwrap(), 1.0);
    }

    #[test]
    fn decay_phase() {
        let mut fixture = Fixture::new();
        fixture.note_on();

        fixture.process(ATTACK_TIME);
        let values = fixture.process(DECAY_TIME);

        assert!(values.iter().tuple_windows().all(|(a, b)| a >= b));
        assert!(values
            .iter()
            .all(|value| SUSTAIN_LEVEL <= *value && *value <= 1.0));

        let next_values = fixture.process(Duration::from_secs(1));
        assert_eq!(*next_values.first().unwrap(), SUSTAIN_LEVEL);
    }

    #[test]
    fn sustain_phase() {
        let mut fixture = Fixture::new();
        fixture.note_on();

        fixture.process(ATTACK_TIME);
        fixture.process(DECAY_TIME);
        let values = fixture.process(Duration::from_secs(1));

        assert!(values
            .iter()
            .all(|value| relative_eq!(*value, SUSTAIN_LEVEL)));
    }

    #[test]
    fn release_phase() {
        let mut fixture = Fixture::new();
        fixture.note_on();

        fixture.process(ATTACK_TIME);
        fixture.process(DECAY_TIME);
        fixture.process(Duration::from_secs(1));
        fixture.note_off();

        let values = fixture.process(RELEASE_TIME);

        assert!(values.iter().tuple_windows().all(|(a, b)| a >= b));
        assert!(values
            .iter()
            .all(|value| 0.0 <= *value && *value <= SUSTAIN_LEVEL));

        let next_values = fixture.process(Duration::from_secs(1));
        assert_eq!(*next_values.first().unwrap(), 0.0);
    }
}