灵符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/tunecheck.rs
//! Tune check: is an instrument in tune with 12-tone equal temperament (A = 440)?
//!
//! A few notes across the range (C3, G3, C4, G4) are played and their pitch measured with the DAW's pitch tracker, past the attack.
//! Each note's deviation is folded to the nearest octave (a tracker that locks onto the octave is not an error) and to the nearest
//! fifth / fourth (a sound with a strong third harmonic can lock there), then:
//! * `cents` - the median signed deviation of the notes that had a pitch;
//! * `worst` - the largest absolute deviation;
//! * `pitched` - the share of analysed frames that had a pitch (a noise, a drum or a slow pad can have none: such a sound is not
//!   judged, see [`Tuning::in_tune`]).
//!
//! A microtonal patch (19-EDO glass, a slendro pad, Bohlen-Pierce drones), a detuned preset or a broken one fails; the AI song
//! build swaps it for the next candidate, and Edit > Tune Check Tracks reports it.
use crate::dsp::Instrument;

#[derive(Clone, Copy, Debug, Default)]
pub struct Tuning {
    pub cents: f32,
    pub worst: f32,
    pub pitched: f32,
}

impl Tuning {
    /// in tune, or not judgeable (too little pitch to tell)
    pub fn in_tune(&self) -> bool {
        self.pitched < 0.25 || (self.cents.abs() <= 30.0 && self.worst <= 55.0)
    }
}

/// the deviation folded to the nearest octave / fifth / fourth
fn fold(c: f32) -> f32 {
    let o = c - (c / 1200.0).round() * 1200.0;
    [o, o - 702.0, o + 702.0, o - 498.0, o + 498.0].into_iter().min_by(|a, b| a.abs().total_cmp(&b.abs())).unwrap_or(o)
}

pub fn check(inst: &mut dyn Instrument, sr: f32) -> Tuning {
    let keys = [48u8, 55, 60, 67];
    let mut devs: Vec<f32> = vec![];
    let (mut voiced, mut frames) = (0usize, 0usize);
    let hold = (sr * 0.8) as usize;
    let mut buf = vec![0f32; 512];
    for k in keys {
        inst.all_off();
        for _ in 0..8 {
            buf.fill(0.0);
            inst.render(&mut buf);
        }
        inst.note_on(k, 0.85);
        let mut st: Vec<f32> = Vec::with_capacity(hold * 2);
        while st.len() / 2 < hold {
            buf.fill(0.0);
            inst.render(&mut buf);
            st.extend_from_slice(&buf);
        }
        inst.note_off(k);
        let mono: Vec<f32> = st.chunks_exact(2).map(|f| (f[0] + f[1]) * 0.5).collect();
        // past the attack: the last 80 %
        let tail = &mono[mono.len() / 5..];
        let p = crate::vocal::track_pitch(tail, sr);
        let f: Vec<f32> = p.f0.iter().copied().filter(|x| *x > 20.0).collect();
        frames += p.f0.len();
        voiced += f.len();
        if f.len() >= 5 {
            let mut s = f.clone();
            s.sort_by(f32::total_cmp);
            let med = s[s.len() / 2];
            let want = 440.0 * 2f32.powf((k as f32 - 69.0) / 12.0);
            devs.push(fold(1200.0 * (med / want).log2()));
        }
    }
    inst.all_off();
    let pitched = voiced as f32 / frames.max(1) as f32;
    if devs.is_empty() {
        return Tuning { cents: 0.0, worst: 0.0, pitched };
    }
    let worst = devs.iter().map(|d| d.abs()).fold(0.0, f32::max);
    devs.sort_by(f32::total_cmp);
    Tuning { cents: devs[devs.len() / 2], worst, pitched }
}

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

    #[test]
    fn the_default_synth_is_in_tune_and_folding_forgives_octaves() {
        let mut s = crate::dsp::make_instrument("synth", 44100.0).unwrap();
        let t = check(s.as_mut(), 44100.0);
        assert!(t.in_tune(), "{t:?}");
        assert!(fold(1203.0).abs() < 5.0 && fold(-1195.0).abs() < 6.0 && fold(705.0).abs() < 4.0);
        assert!(fold(40.0) == 40.0);
    }

    #[test]
    fn microtonal_presets_fail_and_twelve_tone_ones_pass() {
        let sr = 44100.0;
        let check_preset = |name: &str| {
            let mut p = crate::dsp::make_instrument("prism", sr).unwrap();
            p.load_preset(name);
            check(p.as_mut(), sr)
        };
        for good in ["Uplift Pad", "Rolling Bass", "Trance Pluck"] {
            let t = check_preset(good);
            assert!(t.in_tune(), "{good}: {t:?}");
        }
        let bad: Vec<(&str, Tuning)> = ["19 EDO Glass", "Slendro Pad", "Bohlen Drone"].iter().map(|n| (*n, check_preset(n))).collect();
        assert!(bad.iter().filter(|(_, t)| !t.in_tune()).count() >= 2, "{bad:?}");
    }
}

← Back to index