nooise 2.5.4

Ambient music generator for the terminal
//! One module per voice, plus the helpers they share: pitch conversion,
//! soft clipping, normalized LFO shaping, and voice-pool mixing.

use super::*;

mod arp;
mod bass;
mod clap;
mod kick;
mod lead;
mod pad;
mod perc;
mod tonal;

pub(crate) use arp::*;
pub(crate) use bass::*;
pub(crate) use clap::*;
pub(crate) use kick::*;
pub(crate) use lead::*;
pub(crate) use pad::*;
pub(crate) use perc::*;
pub(crate) use tonal::*;

// ============================================================
// Shared voice utilities
// ============================================================

pub(crate) fn midi_to_hz(note: i32) -> f32 {
    440.0 * 2f32.powf((note as f32 - 69.0) / 12.0)
}

/// Frequency multiplier for a master tune offset in semitones.
pub(crate) fn tune_ratio(semitones: f32) -> f32 {
    2f32.powf(semitones / 12.0)
}

/// A MIDI note's frequency under the master tune: the one pitch path every
/// pitched voice (Pad, Bass, Tonal, Arp, Lead) builds a note through.
pub(crate) fn note_hz(note: i32, tune: f32) -> f32 {
    midi_to_hz(note) * tune_ratio(tune)
}

/// MIDI pitch folded into one octave for visual/event consumers: C is 0,
/// B is 11/12, and octave transposition does not change the value.
pub(crate) fn pitch_class(note: i32) -> f32 {
    note.rem_euclid(12) as f32 / 12.0
}

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

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

/// Shared sum-then-cull idiom: advance every voice (in order), accumulating
/// its stereo output, then drop whichever voices are finished. Voice order
/// and the summation order are unchanged from the equivalent hand-written
/// loop, so this is a pure extraction.
#[inline]
pub(crate) fn mix_and_retain<V>(
    voices: &mut Vec<V>,
    mut next: impl FnMut(&mut V) -> (f32, f32),
    done: impl Fn(&V) -> bool,
) -> (f32, f32) {
    let mut dry_l = 0.0f32;
    let mut dry_r = 0.0f32;
    for v in voices.iter_mut() {
        let (l, r) = next(v);
        dry_l += l;
        dry_r += r;
    }
    voices.retain(|v| !done(v));
    (dry_l, dry_r)
}

/// `mix_and_retain` for a mono voice pool.
#[inline]
pub(crate) fn mix_and_retain_mono<V>(
    voices: &mut Vec<V>,
    mut next: impl FnMut(&mut V) -> f32,
    done: impl Fn(&V) -> bool,
) -> f32 {
    let mut dry = 0.0f32;
    for v in voices.iter_mut() {
        dry += next(v);
    }
    voices.retain(|v| !done(v));
    dry
}