灵符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/redesign.rs
//! Redesign a track's sound from words: pick the plugin's preset that fits the part, then move its parameters by what they do.
//!
//! Every plugin (Growl, Prism, Pluck, the GM synth, Sampler ...) names its parameters plainly, so their jobs are read from the names
//! (`articulate::role_of`: cutoff -> filter, drive / fold / crush -> drive, rv mix -> space ...). Words move roles: "grittier" pushes
//! every drive-like parameter up, "darker" pulls the filters down, "wider" opens width and space, "punchier" shortens the
//! envelopes. Pitch, levels, switches, matrix cells and sequencer steps are never touched (`articulate::eligible`), so a redesign
//! never detunes or mutes a patch.
use crate::articulate::{eligible, role_of, Role};

/// (words, role, how far 0..1 at amount 1, what it is called in the summary)
const WORDS: &[(&[&str], Role, f32, &str)] = &[
    (&["gritty", "grittier", "grit", "dirty", "dirtier", "distorted", "crunch", "crunchy", "nasty", "aggressive", "heavier", "heavy", "filthy", "harder", "angrier", "angry"], Role::Drive, 0.22, "more drive"),
    (&["clean", "cleaner", "smooth", "smoother", "softer", "soft", "gentle", "gentler", "sweet", "sweeter"], Role::Drive, -0.2, "less drive"),
    (&["dark", "darker", "warm", "warmer", "muffled", "deeper", "duller", "mellow", "mellower"], Role::Filter, -0.18, "darker filter"),
    (&["bright", "brighter", "crisp", "crisper", "sharper", "airy", "airier", "shiny", "open", "opener"], Role::Filter, 0.18, "brighter filter"),
    (&["resonant", "squelchy", "squelch", "acid", "peaky", "vocal", "talky"], Role::Reso, 0.2, "more resonance"),
    (&["wide", "wider", "huge", "bigger", "big", "spacious", "stereo", "lush", "lusher"], Role::Pan, 0.15, "wider"),
    (&["wide", "wider", "huge", "bigger", "spacious", "lush", "lusher"], Role::Space, 0.1, "more space"),
    (&["narrow", "narrower", "mono", "tight", "tighter", "dry", "drier", "focused"], Role::Space, -0.2, "drier"),
    (&["spacey", "spacy", "ambient", "washy", "wet", "wetter", "dreamy", "dreamier", "ethereal"], Role::Space, 0.2, "more space"),
    (&["wobbly", "wobble", "wobblier", "movement", "moving", "alive", "evolving", "animated"], Role::ModDepth, 0.2, "more movement"),
    (&["static", "still", "steady", "stable"], Role::ModDepth, -0.2, "less movement"),
    (&["faster", "quicker", "busier"], Role::ModRate, 0.15, "faster modulation"),
    (&["slower", "lazier", "calmer"], Role::ModRate, -0.15, "slower modulation"),
    (&["punchy", "punchier", "snappy", "snappier", "plucky", "pluckier", "percussive", "shorter"], Role::Env, -0.15, "snappier envelopes"),
    (&["longer", "sustained", "legato", "padlike", "swelling", "slower attack"], Role::Env, 0.15, "longer envelopes"),
    (&["metallic", "metal", "harsh", "harsher", "digital", "glassy", "complex"], Role::Timbre, 0.18, "harsher timbre"),
    (&["round", "rounder", "simple", "simpler", "pure", "purer", "hollow"], Role::Timbre, -0.18, "rounder timbre"),
];

/// genres as bundles of moves
const STYLES: &[(&[&str], &[(Role, f32)])] = &[
    (&["dubstep", "riddim", "brostep", "tearout", "neuro", "bass music"], &[(Role::Drive, 0.2), (Role::ModDepth, 0.15), (Role::Timbre, 0.1)]),
    (&["lofi", "lo-fi", "tape", "vintage"], &[(Role::Filter, -0.15), (Role::Drive, 0.06), (Role::Space, 0.08)]),
    (&["trance", "uplifting", "euphoric"], &[(Role::Space, 0.12), (Role::Filter, 0.1), (Role::Pan, 0.1)]),
    (&["techno", "industrial", "rave"], &[(Role::Drive, 0.12), (Role::Reso, 0.1), (Role::Space, -0.05)]),
    (&["ambient", "cinematic", "chill"], &[(Role::Space, 0.22), (Role::Env, 0.15), (Role::Drive, -0.15), (Role::Filter, -0.05)]),
    (&["house", "disco", "funky", "funk"], &[(Role::Env, -0.08), (Role::Filter, 0.06), (Role::Drive, 0.04)]),
    (&["synthwave", "retro", "80s", "outrun"], &[(Role::ModDepth, 0.08), (Role::Space, 0.12), (Role::Filter, -0.05)]),
];

/// The role moves a request asks for (words count once each) and what they are called.
pub fn moves(request: &str) -> (Vec<(Role, f32)>, Vec<&'static str>) {
    let p = format!(" {} ", request.to_lowercase().replace([',', '.', '!', '?', ';', ':'], " "));
    let has = |w: &str| p.contains(&format!(" {w} "));
    let mut out: Vec<(Role, f32)> = vec![];
    let mut said: Vec<&'static str> = vec![];
    let mut add = |r: Role, d: f32, out: &mut Vec<(Role, f32)>| match out.iter_mut().find(|m| m.0 == r) {
        Some(m) => m.1 = (m.1 + d).clamp(-0.45, 0.45),
        None => out.push((r, d)),
    };
    // "much" / "a lot" doubles, "a bit" / "slightly" halves
    let k = if has("much") || has("lot") || has("way") || has("very") { 1.8 } else if has("bit") || has("slightly") || has("little") || has("touch") { 0.5 } else { 1.0 };
    for (ws, r, d, name) in WORDS {
        if ws.iter().any(|w| has(w)) {
            add(*r, d * k, &mut out);
            if !said.contains(name) {
                said.push(name);
            }
        }
    }
    for (ws, ms) in STYLES {
        if ws.iter().any(|w| has(w)) {
            for (r, d) in ms.iter() {
                add(*r, d * k, &mut out);
            }
            said.push(ws[0]);
        }
    }
    (out, said)
}

/// a parameter a redesign may move: something that shapes the sound, not a level, a switch, a pitch, a matrix cell, a lane point
/// or a sequencer step
pub fn movable(name: &str) -> bool {
    let n = name.to_lowercase();
    if !eligible(&n) || n == "pan" || n.starts_with("m ") || n.starts_with("x ") || n.starts_with("t s") {
        return false;
    }
    // drawn lane points: "l1 p3"
    let b = n.as_bytes();
    if b.len() >= 5 && b[0] == b'l' && b[1].is_ascii_digit() && &n[2..4] == " p" {
        return false;
    }
    !matches!(role_of(&n), Role::Other | Role::Pitch)
}

/// New values for the parameters the moves touch (only the ones that change)
pub fn tweak(params: &[(String, f32)], mv: &[(Role, f32)], amount: f32) -> Vec<(String, f32)> {
    let mut out = vec![];
    for (name, v) in params {
        if !movable(name) || !v.is_finite() {
            continue;
        }
        let role = role_of(&name.to_lowercase());
        let Some((_, d)) = mv.iter().find(|m| m.0 == role) else { continue };
        let nv = (v + d * amount).clamp(0.0, 1.0);
        if (nv - v).abs() > 0.004 {
            out.push((name.clone(), nv));
        }
    }
    out
}

/// what a part is called -> words that suit it in a preset name (the part's own word counts most)
const ROLE_WORDS: &[(&str, &[&str])] = &[
    ("sub", &["808", "deep", "boom"]),
    ("bass", &["reese", "growl", "wob", "sub", "riddim", "808"]),
    ("wobble", &["wob", "wub"]),
    ("growl", &["riddim", "neuro", "throat", "talk"]),
    ("lead", &["screech", "saw", "hook", "anthem", "laser"]),
    ("laser", &["zap", "screech"]),
    ("pad", &["haze", "choir", "drift", "strings"]),
    ("pluck", &["stab", "tick", "pop"]),
    ("arp", &["pluck", "bell"]),
    ("stab", &["chord", "hit"]),
    ("chord", &["stab", "keys"]),
    ("keys", &["piano", "ep", "organ"]),
    ("bell", &["glass", "chime"]),
    ("riser", &["sweep", "noise"]),
];

/// The plugin's preset that best fits the part and the words (the seed breaks ties, so "try again" gives another good one).
pub fn pick_preset(presets: &[&str], role: &str, request: &str, seed: u64) -> Option<String> {
    let r = role.to_lowercase();
    let q = request.to_lowercase();
    let qw: Vec<&str> = q.split(|c: char| !c.is_alphanumeric()).filter(|w| w.len() > 2).collect();
    let mut best: Option<(i64, &str)> = None;
    for (i, p) in presets.iter().enumerate() {
        let n = p.to_lowercase();
        let mut s = 0i64;
        for (k, ws) in ROLE_WORDS {
            if !r.contains(k) {
                continue;
            }
            if n.contains(k) {
                s += 40;
            } else if ws.iter().any(|w| n.contains(w)) {
                s += 15;
            }
        }
        for w in &qw {
            if n.contains(w) {
                s += 20;
            }
        }
        if s == 0 {
            continue;
        }
        // a stable shuffle of the ties
        let tie = (((i as u64 + 1).wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ seed.wrapping_mul(0xBF58_476D_1CE4_E5B9)) >> 52) as i64;
        let s = s * 10_000 + tie;
        if best.map(|b| s > b.0).unwrap_or(true) {
            best = Some((s, p));
        }
    }
    best.map(|b| b.1.to_string())
}

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

    #[test]
    fn words_move_the_right_parameters_and_never_pitch_or_levels() {
        let (mv, said) = moves("make this patch a bit grittier and darker");
        assert!(said.contains(&"more drive") && said.contains(&"darker filter"), "{said:?}");
        let params: Vec<(String, f32)> = ["drv", "cut", "res", "detn", "out", "c tune", "m l1c", "l1 p3", "fold", "rv mix", "pan", "fshift"].iter().map(|n| (n.to_string(), 0.5)).collect();
        let t = tweak(&params, &mv, 1.0);
        let get = |n: &str| t.iter().find(|x| x.0 == n).map(|x| x.1);
        assert!(get("drv").unwrap() > 0.5 && get("fold").unwrap() > 0.5, "{t:?}");
        assert!(get("cut").unwrap() < 0.5, "{t:?}");
        for n in ["detn", "out", "c tune", "m l1c", "l1 p3", "pan", "fshift"] {
            assert!(get(n).is_none(), "{n} must not move: {t:?}");
        }
        // "a bit" is half as far as plain
        let full = tweak(&params, &moves("grittier").0, 1.0);
        assert!(get("drv").unwrap() - 0.5 < full.iter().find(|x| x.0 == "drv").unwrap().1 - 0.5);
        // a genre is a bundle of moves
        assert!(moves("more dubstep").0.iter().any(|m| m.0 == Role::Drive && m.1 > 0.0));
        assert!(moves("hello").0.is_empty());
    }

    #[test]
    fn the_preset_fits_the_part() {
        let presets = ["wobble", "comb growl", "big room lead", "prog pluck", "sub drop", "crimson pad"];
        assert_eq!(pick_preset(&presets, "growl bass", "", 1).as_deref(), Some("comb growl"));
        assert_eq!(pick_preset(&presets, "warm pad", "", 1).as_deref(), Some("crimson pad"));
        assert_eq!(pick_preset(&presets, "supersaw lead", "", 1).as_deref(), Some("big room lead"));
        assert_eq!(pick_preset(&presets, "pluck", "", 3).as_deref(), Some("prog pluck"));
        // the words count too
        assert_eq!(pick_preset(&presets, "bass", "a wobble", 1).as_deref(), Some("wobble"));
        assert!(pick_preset(&presets, "texture", "zzz", 1).is_none());
    }
}

← Back to index