nooise 0.1.0

A noise synthesis engine
use std::error::Error;

use crate::audio::{self, StereoEngine};
use crate::fx::panner::StereoPanner;
use crate::synth::envelope::Adsr;
use crate::synth::oscillator::SineOscillator;

const TEMPO_BPM: f32 = 120.0;
const PULSE_FREQUENCY_HZ: f32 = 196.0;

pub(crate) fn run() -> Result<(), Box<dyn Error>> {
    audio::run_engine("r1", R1Engine::new)
}

struct R1Engine {
    current_sample: u64,
    beat_samples: u64,
    next_pulse_sample: u64,
    next_side_left: bool,
    pulse: Option<BilateralPulse>,
    sample_rate: f32,
}

impl R1Engine {
    fn new(sample_rate: f32) -> Self {
        Self {
            current_sample: 0,
            beat_samples: (sample_rate * 60.0 / TEMPO_BPM).round() as u64,
            next_pulse_sample: 0,
            next_side_left: true,
            pulse: None,
            sample_rate,
        }
    }

    fn trigger_due_pulses(&mut self) {
        while self.current_sample >= self.next_pulse_sample {
            self.pulse = Some(BilateralPulse::new(self.next_side_left, self.sample_rate));
            self.next_side_left = !self.next_side_left;
            self.next_pulse_sample += self.beat_samples;
        }
    }
}

impl StereoEngine for R1Engine {
    fn next_stereo(&mut self) -> (f32, f32) {
        self.trigger_due_pulses();

        let (left, right) = match self.pulse.as_mut() {
            Some(pulse) => pulse.next_stereo(),
            None => (0.0, 0.0),
        };
        if self.pulse.as_ref().is_some_and(BilateralPulse::is_done) {
            self.pulse = None;
        }

        self.current_sample += 1;
        (left.clamp(-0.9, 0.9), right.clamp(-0.9, 0.9))
    }
}

struct BilateralPulse {
    oscillator: SineOscillator,
    envelope: Adsr,
    age_samples: u64,
    hold_samples: u64,
    pan: f32,
    released: bool,
}

impl BilateralPulse {
    fn new(left: bool, sample_rate: f32) -> Self {
        let envelope = Adsr::new(0.012, 0.045, 0.72, 0.14, sample_rate);
        Self {
            oscillator: SineOscillator::new(PULSE_FREQUENCY_HZ, sample_rate),
            hold_samples: envelope.samples_from_seconds(0.32),
            envelope,
            age_samples: 0,
            pan: if left { -0.96 } else { 0.96 },
            released: false,
        }
    }

    fn next_stereo(&mut self) -> (f32, f32) {
        if !self.released && self.age_samples >= self.hold_samples {
            self.envelope.note_off();
            self.released = true;
        }

        let sample = self.oscillator.next() * self.envelope.next() * 0.12;
        self.age_samples += 1;
        StereoPanner::equal_power(sample, self.pan)
    }

    fn is_done(&self) -> bool {
        self.envelope.is_done()
    }
}