灵符lingfu

ling-lang

↓ 0 downloads

Ling - The Omniglot Systems Language

Keywords systems, polyglot, ai, crypto, game
lingfu install ling-lang
LicenseApache-2.0 OR MIT
AuthorsLing Lin, Sanny Lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (995)

crates/ling-daw/src/presetinfo.rs
//! What every built-in preset sounds like, measured - and the level trim that makes an instrument's presets sit together.
//!
//! `analyze` renders a preset (a held C3, then C4) and measures it: loudness and peak, attack time, how much of the level it keeps
//! (plucky / decaying / sustained), brightness (spectral centroid), movement (how much the tone and level change over time), noise
//! (spectral flatness), stereo width, low end, and whether it plays in tune with 12-tone equal temperament. `words` turns that into
//! plain words and a likely use (bass / pad / lead / pluck / fx), which the preset lists show and the generators (Redo Instrument &
//! Sound, the agent) match against.
//!
//! The table (`preset_info.tsv`: kind, preset, description, trim in dB) is written by the ignored `write_preset_info` test from
//! LingStationX and compiled in. `trim_db` is the gain that brings a preset to the median loudness of its instrument (within +-9 dB,
//! never past -1 dBFS peak); `apply_trim` sets it on the instrument's own level knob when the preset or its patch file loads.
use crate::dsp::Instrument;
use std::collections::HashMap;
use std::sync::OnceLock;

#[derive(Clone, Debug, Default)]
pub struct Features {
    pub rms_db: f32,
    pub peak_db: f32,
    pub attack_s: f32,
    pub sustain: f32,
    pub centroid: f32,
    /// the centroid over the note's frequency at C4: about 1-2 dark, 2-4 warm, 4-8 bright, above airy
    pub bright: f32,
    /// share of the energy an octave and more below the note at C3 (a sub)
    pub sub: f32,
    pub movement: f32,
    pub noise: f32,
    pub width: f32,
    pub lows: f32,
    pub pitched: f32,
    pub in_tune: bool,
}

fn db(x: f32) -> f32 {
    20.0 * x.max(1e-9).log10()
}

/// render `secs` of a held note then `rel` of release
fn play(inst: &mut dyn Instrument, key: u8, secs: f32, rel: f32, sr: f32) -> (Vec<f32>, usize) {
    inst.all_off();
    let mut out = vec![0f32; ((secs + rel) * sr) as usize * 2];
    let on = (secs * sr) as usize * 2;
    inst.note_on(key, 0.85);
    let mut i = 0;
    while i < out.len() {
        if i >= on && i < on + 512 {
            inst.note_off(key);
        }
        let n = 512.min(out.len() - i);
        inst.render(&mut out[i..i + n]);
        i += n;
    }
    inst.all_off();
    (out, on)
}

fn spectrum(mono: &[f32], sr: f32, below: f32) -> (f32, f32, f32) {
    // centroid, flatness and the share of energy below `below` Hz: a Hann-windowed DFT of the middle 2048 samples at evenly
    // spaced bins (so no part of the spectrum is counted more often than another) up to 16 kHz
    let n = 2048usize;
    if mono.len() < n {
        return (0.0, 0.0, 0.0);
    }
    let start = mono.len() / 2 - n / 2;
    let x: Vec<f32> = (0..n).map(|k| mono[start + k] * (0.5 - 0.5 * (std::f32::consts::TAU * k as f32 / n as f32).cos())).collect();
    let df = sr / n as f32;
    let top = ((16000.0f32.min(sr * 0.45)) / df) as usize;
    let (mut num, mut den, mut low, mut logsum, mut cnt) = (0.0f64, 0.0f64, 0.0f64, 0.0f64, 0usize);
    for b in 1..top {
        let w = std::f32::consts::TAU * b as f32 / n as f32;
        let (mut re, mut im) = (0.0f32, 0.0f32);
        // rotate a phasor instead of calling cos / sin per sample
        let (c, s) = (w.cos(), w.sin());
        let (mut pr, mut pi) = (1.0f32, 0.0f32);
        for v in &x {
            re += v * pr;
            im += v * pi;
            let t = pr * c - pi * s;
            pi = pr * s + pi * c;
            pr = t;
        }
        let p = (re * re + im * im) as f64 + 1e-15;
        let f = b as f32 * df;
        num += f as f64 * p;
        den += p;
        logsum += p.ln();
        cnt += 1;
        if f < below {
            low += p;
        }
    }
    let geo = (logsum / cnt.max(1) as f64).exp();
    let arith = den / cnt.max(1) as f64;
    ((num / den.max(1e-15)) as f32, (geo / arith.max(1e-15)).clamp(0.0, 1.0) as f32, (low / den.max(1e-15)) as f32)
}

/// Measure a preset (the instrument already has it loaded).
pub fn analyze(inst: &mut dyn Instrument, sr: f32) -> Features {
    let mut f = Features::default();
    let (a, on) = play(inst, 48, 1.2, 0.6, sr);
    let mono: Vec<f32> = a.chunks(2).map(|c| (c[0] + c[1]) * 0.5).collect();
    let side: Vec<f32> = a.chunks(2).map(|c| (c[0] - c[1]) * 0.5).collect();
    let held = &mono[..on / 2];
    let win = (sr * 0.02) as usize;
    let env: Vec<f32> = held.chunks(win).map(|c| (c.iter().map(|x| x * x).sum::<f32>() / c.len() as f32).sqrt()).collect();
    let mx = env.iter().copied().fold(0.0f32, f32::max);
    if mx < 1e-5 {
        f.rms_db = -120.0;
        f.peak_db = -120.0;
        return f;
    }
    f.peak_db = db(a.iter().fold(0.0f32, |m, x| m.max(x.abs())));
    // loudness: the louder half of the held part (a pluck is judged by its body, not its silence)
    let mut sorted = env.clone();
    sorted.sort_by(|x, y| y.total_cmp(x));
    let top = &sorted[..(sorted.len() / 2).max(1)];
    f.rms_db = db((top.iter().map(|x| x * x).sum::<f32>() / top.len() as f32).sqrt());
    f.attack_s = env.iter().position(|e| *e >= mx * 0.7).unwrap_or(0) as f32 * 0.02;
    let tail = &env[env.len() * 4 / 5..];
    f.sustain = tail.iter().sum::<f32>() / tail.len().max(1) as f32 / mx;
    let sm = (side.iter().map(|x| x * x).sum::<f32>() / side.len() as f32).sqrt();
    let mm = (mono.iter().map(|x| x * x).sum::<f32>() / mono.len() as f32).sqrt();
    f.width = sm / mm.max(1e-9);
    // energy below 150 Hz, and below three quarters of C3 (a sub an octave down)
    f.lows = spectrum(held, sr, 150.0).2;
    f.sub = spectrum(held, sr, 130.81 * 0.75).2;
    // brightness and noise at C4
    let (b, onb) = play(inst, 60, 0.8, 0.1, sr);
    let mb: Vec<f32> = b[..onb].chunks(2).map(|c| (c[0] + c[1]) * 0.5).collect();
    let (cen, flat, _) = spectrum(&mb, sr, 150.0);
    f.centroid = cen;
    f.bright = cen / 261.63;
    f.noise = flat;
    // movement: how the level and the brightness change across the held note (zero crossings stand in for the brightness)
    let lv: Vec<f32> = env[env.len() / 5..].to_vec();
    let lm = lv.iter().sum::<f32>() / lv.len().max(1) as f32;
    let lvar = (lv.iter().map(|x| (x - lm).powi(2)).sum::<f32>() / lv.len().max(1) as f32).sqrt() / lm.max(1e-9);
    let zc: Vec<f32> = held.chunks(win * 3).map(|c| c.windows(2).filter(|w| w[0] <= 0.0 && w[1] > 0.0).count() as f32).collect();
    let zm = zc.iter().sum::<f32>() / zc.len().max(1) as f32;
    let zvar = (zc.iter().map(|x| (x - zm).powi(2)).sum::<f32>() / zc.len().max(1) as f32).sqrt() / zm.max(1.0);
    f.movement = if f.sustain > 0.3 { lvar.max(zvar) } else { zvar };
    inst.all_off();
    f
}

/// the tune check on a fresh copy of the preset (state left by other renders confuses the pitch tracker); `make` builds it
pub fn tuning(inst: &mut dyn Instrument, sr: f32) -> (f32, bool) {
    let t = crate::tunecheck::check(inst, sr);
    (t.pitched, t.in_tune())
}

/// the part a preset is for, from its category or its name ("" when neither says)
pub fn usage_hint(name: &str, category: &str) -> &'static str {
    let s = format!("{} {}", category.to_lowercase(), name.to_lowercase());
    let has = |ws: &[&str]| ws.iter().any(|w| s.contains(w));
    if has(&["drum", "kick", "snare", "hat", "perc", "sfx", "fx", "riser", "laser", "zap", "explosion", "coin", "jump", "hit", "noise", "alarm", "warp", "rocket", "game over", "menu", "select", "spring", "fireball", "1-up", "power up", "pickup"]) {
        "fx / percussion"
    } else if has(&["bass", "sub", "808", "reese", "wob", "riddim", "growl"]) {
        "bass"
    } else if has(&["pad", "string", "choir", "drone", "haze", "atmos", "ambient"]) {
        "pad"
    } else if has(&["pluck", "arp", "bell", "keys", "piano", "stab", "harp", "guitar", "koto", "kalimba", "mallet", "chord"]) {
        "pluck / keys"
    } else if has(&["lead", "screech", "saw", "hook", "anthem", "brass", "flute", "voice", "vox", "talk"]) {
        "lead"
    } else {
        ""
    }
}

/// plain words for the measurements, with the part it is for first (from the hint, else measured)
pub fn words(f: &Features, hint: &str) -> String {
    if f.rms_db < -100.0 {
        return "silent at a held C3 (needs its own trigger or settings)".into();
    }
    let mut w: Vec<&str> = vec![];
    let unpitched = f.pitched < 0.3 || f.noise > 0.45;
    let usage = if !hint.is_empty() {
        hint
    } else if unpitched {
        "fx / percussion"
    } else if f.sub > 0.2 && f.width < 0.25 {
        "bass"
    } else if f.sustain < 0.18 {
        "pluck / keys"
    } else if f.attack_s > 0.2 || (f.sustain > 0.55 && f.width > 0.3) {
        "pad"
    } else {
        "lead"
    };
    w.push(usage);
    w.push(if f.bright < 2.0 {
        "dark"
    } else if f.bright < 4.0 {
        "warm"
    } else if f.bright < 8.0 {
        "bright"
    } else {
        "airy / very bright"
    });
    w.push(if f.sustain < 0.18 {
        "plucky"
    } else if f.sustain < 0.5 {
        "decaying"
    } else {
        "sustained"
    });
    if f.attack_s > 0.2 {
        w.push("slow attack");
    }
    if f.movement > 0.25 {
        w.push("moving");
    }
    if f.noise > 0.3 && !unpitched {
        w.push("breathy / noisy");
    }
    if f.width > 0.35 {
        w.push("wide");
    } else if f.width < 0.05 {
        w.push("mono");
    }
    if f.sub > 0.25 {
        w.push("deep sub");
    }
    // only a steady, tonal sound is judged for tuning (modulated growls fool the pitch tracker)
    if !f.in_tune && !unpitched && f.pitched > 0.6 && f.movement < 0.2 {
        w.push("microtonal / detuned (not 12-TET)");
    }
    w.join(", ")
}

// ── the table ─────────────────────────────────────────────────────────────────────

/// (description, trim dB) by "kind\tpreset"
fn table() -> &'static HashMap<String, (String, f32)> {
    static T: OnceLock<HashMap<String, (String, f32)>> = OnceLock::new();
    T.get_or_init(|| {
        let mut m = HashMap::new();
        for line in include_str!("preset_info.tsv").lines() {
            let f: Vec<&str> = line.split('\t').collect();
            if f.len() >= 4 {
                m.insert(format!("{}\t{}", f[0], f[1].to_lowercase()), (f[2].to_string(), f[3].parse().unwrap_or(0.0)));
            }
        }
        m
    })
}

/// The description of a built-in preset ("" when it is not in the table)
pub fn info(kind: &str, preset: &str) -> String {
    table().get(&format!("{kind}\t{}", preset.to_lowercase())).map(|x| x.0.clone()).unwrap_or_default()
}

pub fn trim_db(kind: &str, preset: &str) -> f32 {
    table().get(&format!("{kind}\t{}", preset.to_lowercase())).map(|x| x.1).unwrap_or(0.0)
}

/// the instrument's level knob and how it maps to gain: (name, exponent of the gain law)
pub fn level_param(kind: &str) -> Option<(&'static str, f32)> {
    match kind {
        "growl" | "prism" | "chip" => Some((if kind == "chip" { "level" } else { "out" }, 2.0)),
        "pluck" => Some(("level", 1.0)),
        _ => None,
    }
}

/// Set a loaded preset's level to where the table puts it (nothing for presets without a trim)
pub fn apply_trim(inst: &mut dyn Instrument, kind: &str, preset: &str) {
    let t = trim_db(kind, preset);
    if t.abs() < 0.05 {
        return;
    }
    let Some((name, law)) = level_param(kind) else { return };
    let Some(v) = inst.get_param(name) else { return };
    let g = 10f32.powf(t / 20.0);
    let nv = (v * g.powf(1.0 / law)).clamp(0.02, 1.0);
    inst.set_param(name, nv);
}

/// a preset's own hand-written description, where the instrument has one (Growl presets: the comment under the name)
fn hand_description(kind: &str, preset: &str) -> String {
    if kind == "growl" {
        let text = include_str!("growl_presets.txt");
        let head = format!("[{preset}]");
        if let Some(i) = text.find(&head) {
            let after = &text[i + head.len()..];
            if let Some(l) = after.lines().nth(1) {
                let c = l.trim().trim_start_matches('#').trim();
                if !c.is_empty() && !c.starts_with("Ling Growl patch") {
                    return c.to_string();
                }
            }
        }
    }
    String::new()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_measurements_tell_a_pluck_from_a_pad_and_a_bass() {
        let sr = 44100.0;
        let mut p = crate::dsp::make_instrument("pluck", sr).unwrap();
        p.load_preset("Muted Pluck");
        let f = analyze(p.as_mut(), sr);
        assert!(f.sustain < 0.3, "{f:?}");
        assert!(words(&f, "").contains("pluck"), "{}", words(&f, ""));
        let mut c = crate::dsp::make_instrument("chip", sr).unwrap();
        c.load_preset("Explosion");
        let fx = analyze(c.as_mut(), sr);
        assert!(words(&fx, usage_hint("Explosion", "SFX")).starts_with("fx"), "{}", words(&fx, ""));
        assert_eq!(usage_hint("Rolling Bass", "basses"), "bass");
        assert_eq!(usage_hint("Uplift Pad", "pads"), "pad");
    }

    /// not a test: measures every built-in preset of Growl, Prism, Pluck and Chip and writes src/preset_info.tsv (run from
    /// LingStationX: `PRESET_INFO=<ling-daw src dir>`)
    #[test]
    #[ignore]
    fn write_preset_info() {
        let Ok(out) = std::env::var("PRESET_INFO") else { return };
        if let Ok(root) = std::env::var("LSX_ROOT") {
            let _ = std::env::set_current_dir(root);
        }
        let sr = 44100.0;
        let mut lines: Vec<String> = vec![];
        for kind in ["growl", "prism", "pluck", "chip"] {
            let probe = crate::dsp::make_instrument(kind, sr).unwrap();
            let names: Vec<String> = probe.presets().iter().map(|s| s.to_string()).collect();
            let given: HashMap<String, String> = probe.preset_categories().into_iter().collect();
            let mut rows: Vec<(String, String, Features)> = vec![];
            for n in &names {
                let mut inst = crate::dsp::make_instrument(kind, sr).unwrap();
                inst.load_preset(n);
                let mut f = analyze(inst.as_mut(), sr);
                let mut fresh = crate::dsp::make_instrument(kind, sr).unwrap();
                fresh.load_preset(n);
                let (pitched, in_tune) = tuning(fresh.as_mut(), sr);
                f.pitched = pitched;
                f.in_tune = in_tune;
                let hand = { let h = probe.preset_info(n); if h.is_empty() { hand_description(kind, n) } else { h } };
                let cat = given.get(n).cloned().unwrap_or_default();
                let mut d = words(&f, usage_hint(n, &cat));
                if !cat.is_empty() && kind == "prism" {
                    d = format!("{} - {d}", cat.to_lowercase());
                }
                if !hand.is_empty() {
                    d = format!("{hand} ({d})");
                }
                rows.push((n.clone(), d, f));
            }
            // the instrument's median loudness is the target; silent ones are left alone
            let mut l: Vec<f32> = rows.iter().map(|r| r.2.rms_db).filter(|x| *x > -100.0).collect();
            l.sort_by(f32::total_cmp);
            let target = l.get(l.len() / 2).copied().unwrap_or(-18.0);
            for (n, d, f) in rows {
                // only the outliers: more than 3 dB from the instrument's median, brought to 2 dB of it (at most 6 dB), never past
                // -1 dBFS peak; everything else stays as its designer set it
                let off = target - f.rms_db;
                let mut t = if f.rms_db > -100.0 && off.abs() > 3.0 { (off - off.signum() * 2.0).clamp(-6.0, 6.0) } else { 0.0 };
                t = t.min(-1.0 - f.peak_db).max(-6.0);
                if t.abs() < 1.0 {
                    t = 0.0;
                }
                lines.push(format!("{kind}\t{n}\t{}\t{t:.1}", d.replace('\t', " ")));
            }
        }
        std::fs::write(std::path::Path::new(&out).join("preset_info.tsv"), lines.join("\n") + "\n").unwrap();
    }
}

← Back to index