nooise 0.1.0

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

use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};

use crate::audio::{self, StereoEngine};
use crate::fx::lfo::DriftingLfo;
use crate::fx::panner::StereoPanner;
use crate::fx::reverb::Freeverb;
use crate::synth::envelope::Adsr;
use crate::synth::noise::WhiteNoise;
use crate::synth::oscillator::SineOscillator;

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

struct T3Engine {
    current_sample: u64,
    sample_rate: f32,
    layers: Vec<PadLayer>,
    next_change_sample: u64,
    chord_index: usize,
    reverb: Freeverb,
    depth_lfo: DriftingLfo,
    width_lfo: DriftingLfo,
    rng: StdRng,
    air: WhiteNoise,
}

impl T3Engine {
    fn new(sample_rate: f32) -> Self {
        Self {
            current_sample: 0,
            sample_rate,
            layers: vec![PadLayer::new(0, sample_rate)],
            next_change_sample: rare_change_samples(sample_rate, &mut StdRng::from_entropy()),
            chord_index: 0,
            reverb: Freeverb::new(sample_rate, 0.93, 0.46, 1.0),
            depth_lfo: DriftingLfo::new(1.0 / 42.0, sample_rate),
            width_lfo: DriftingLfo::new(1.0 / 54.0, sample_rate),
            rng: StdRng::from_entropy(),
            air: WhiteNoise::new(),
        }
    }

    fn dry_signal(&mut self, width: f32) -> (f32, f32) {
        let mut left = 0.0;
        let mut right = 0.0;

        for layer in &mut self.layers {
            let (layer_left, layer_right) = layer.next_stereo(width);
            left += layer_left;
            right += layer_right;
        }
        self.layers.retain(|layer| !layer.is_done());

        (left, right)
    }

    fn update_tonal_layer(&mut self) {
        if self.current_sample < self.next_change_sample {
            return;
        }

        for layer in &mut self.layers {
            layer.release();
        }

        self.chord_index = self.chord_index.wrapping_add(1);
        self.layers
            .push(PadLayer::new(self.chord_index, self.sample_rate));
        self.next_change_sample =
            self.current_sample + rare_change_samples(self.sample_rate, &mut self.rng);
    }
}

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

        let depth = normalized_lfo(self.depth_lfo.next(&mut self.rng, 1.0 / 68.0, 1.0 / 28.0));
        let width = 0.58
            + normalized_lfo(self.width_lfo.next(&mut self.rng, 1.0 / 86.0, 1.0 / 38.0)) * 0.16;
        let (dry_left, dry_right) = self.dry_signal(width);

        let reverb_send = 0.48 + depth * 0.22;
        let (wet_left, wet_right) = self
            .reverb
            .process(dry_left * reverb_send, dry_right * reverb_send);
        let wet_mix = 0.72 + depth * 0.34;
        let air = self.air.next_filtered(&mut self.rng, 0.0002) * 0.00025;
        let fade_in = (self.current_sample as f32 / (self.sample_rate * 8.0)).min(1.0);

        self.current_sample += 1;
        (
            ((dry_left * 0.58 + wet_left * wet_mix + air) * fade_in).clamp(-0.95, 0.95),
            ((dry_right * 0.58 + wet_right * wet_mix + air) * fade_in).clamp(-0.95, 0.95),
        )
    }
}

struct PadLayer {
    tones: Vec<PadTone>,
}

impl PadLayer {
    fn new(chord_index: usize, sample_rate: f32) -> Self {
        Self {
            tones: pad_tones(chord_index, sample_rate),
        }
    }

    fn next_stereo(&mut self, width: f32) -> (f32, f32) {
        let mut left = 0.0;
        let mut right = 0.0;

        for tone in &mut self.tones {
            let (tone_left, tone_right) = tone.next_stereo(width);
            left += tone_left;
            right += tone_right;
        }

        (left, right)
    }

    fn release(&mut self) {
        for tone in &mut self.tones {
            tone.release();
        }
    }

    fn is_done(&self) -> bool {
        self.tones.iter().all(PadTone::is_done)
    }
}

struct PadTone {
    primary: SineOscillator,
    detuned: SineOscillator,
    octave: SineOscillator,
    envelope: Adsr,
    pan: f32,
    gain: f32,
    detune_mix: f32,
    octave_mix: f32,
}

impl PadTone {
    fn new(frequency_hz: f32, pan: f32, gain: f32, sample_rate: f32) -> Self {
        Self {
            primary: SineOscillator::new(frequency_hz, sample_rate),
            detuned: SineOscillator::new(frequency_hz * 1.003, sample_rate),
            octave: SineOscillator::new(frequency_hz * 2.0, sample_rate),
            envelope: Adsr::new(6.0, 12.0, 0.86, 20.0, sample_rate),
            pan,
            gain,
            detune_mix: 0.42,
            octave_mix: 0.16,
        }
    }

    fn next_stereo(&mut self, width: f32) -> (f32, f32) {
        let sample = self.primary.next()
            + self.detuned.next() * self.detune_mix
            + self.octave.next() * self.octave_mix;
        let shaped = soft_clip(sample * 0.55) * self.envelope.next() * self.gain;

        StereoPanner::equal_power(shaped, self.pan * width)
    }

    fn release(&mut self) {
        self.envelope.note_off();
    }

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

fn pad_tones(chord_index: usize, sample_rate: f32) -> Vec<PadTone> {
    let chord = pad_chord(chord_index);
    let pans = [-0.52, -0.18, 0.16, 0.46];
    let gains = [0.17, 0.132, 0.126, 0.098];

    chord
        .iter()
        .zip(pans)
        .zip(gains)
        .map(|((frequency_hz, pan), gain)| PadTone::new(*frequency_hz, pan, gain, sample_rate))
        .collect()
}

fn pad_chord(chord_index: usize) -> [f32; 4] {
    const CHORDS: [[f32; 4]; 5] = [
        [110.0, 146.83, 196.0, 261.63],
        [110.0, 164.81, 196.0, 293.66],
        [98.0, 146.83, 220.0, 261.63],
        [123.47, 164.81, 196.0, 293.66],
        [110.0, 146.83, 220.0, 329.63],
    ];

    CHORDS[chord_index % CHORDS.len()]
}

fn rare_change_samples<R: Rng>(sample_rate: f32, rng: &mut R) -> u64 {
    (rng.gen_range(130.0..260.0) * sample_rate).round() as u64
}

fn normalized_lfo(sample: f32) -> f32 {
    (sample * 0.5 + 0.5).clamp(0.0, 1.0)
}

fn soft_clip(sample: f32) -> f32 {
    sample / (1.0 + sample.abs())
}