Numbers

Music theory

Notes, MIDI numbers and frequencies; intervals spelled as a musician spells them, so C to Fb is a diminished fourth; 18 scales and modes; chords built from symbols like `Bb7#9` and `G13b9` and named from their notes, inversions included; keys with their signatures, diatonic chords, Roman-numeral progressions with secondary dominants, and transposition.

Version 1.0.0 · 16 functions · 1 crate

#music#midi#chords#scales#theory

Functions

What use music_theory.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use music_note, music_from_midi from music_theory (Quillo 0.6).

music_note(note: str) → anyor an error

A note read: { name, letter, accidental, octave, pitch_class, midi, frequency }. C#4 has octave 4 and MIDI 61; middle C is C4, MIDI 60. With no octave, midi and frequency are null. Takes Bb, F##, ♯, ♭.

music_from_midi(midi: int, flats: bool) → anyor an error

The note of a MIDI number, with sharps, or flats if asked: { name, midi, frequency }. 0 to 127 is MIDI's range.

music_frequency(note: str, a4: float) → floator an error

The frequency in Hz of a note at a tuning: music_frequency("A4", 440.0) is 440.0; A4 at 432 tunes everything down.

music_from_frequency(hz: float, a4: float) → anyor an error

The nearest note to a frequency: { name, midi, cents, exact }, cents being how far sharp (+) or flat (-) it is, as a tuner shows.

music_interval(from: str, to: str) → anyor an error

The interval from one note to another: { name, words, semitones, descending, simple, inversion }. Spelling matters: C to E is M3, C to Fb d4. Without octaves the second note is the next one up.

music_transpose(note: str, interval: str) → stror an error

A note moved by an interval, spelled right: music_transpose("C4", "M3") is E4, ("Eb", "A4") is A, and -P5 goes down.

music_scale(tonic: str, kind: str) → anyor an error

A scale from its tonic, spelled by intervals so each letter appears once: { notes, intervals, semitones }. There are major, minor, harmonic_minor, melodic_minor, the seven modes, pentatonics, blues, whole_tone, diminished and chromatic. With an octave on the tonic, the notes have octaves too.

music_find_scales(notes: list) → anyor an error

The scales a set of notes fits, from these tonics: all names, for "what can I solo with": [{ tonic, scale }].

music_chord(symbol: str) → anyor an error

A chord symbol built: { symbol, root, quality, bass, notes, intervals, semitones }. Reads Cmaj7, F#m7b5, Bb7#9, Dsus4, Ao7, G13b9, C/E, C6/9, E7alt, Cadd9, C7no5.

music_chord_midi(symbol: str, octave: int) → listor an error

A chord's notes as MIDI numbers in a voicing from a given root octave, with a slash bass underneath: music_chord_midi("C/E", 4) is [52, 60, 64, 67].

music_chord_name(notes: list) → anyor an error

The names a set of notes may go by, likeliest first: ["E", "G", "C"] gives C/E; [{ name, root, inversion }]. The first note is the bass.

music_key(key: str) → anyor an error

A key's facts: { key, mode, sharps, flats, signature, notes, relative, parallel, theoretical }. C, F# minor, Bbm, Eb major. theoretical marks keys like G# major that need double sharps.

music_diatonic(key: str, sevenths: bool) → anyor an error

The chords of a key by degree: [{ degree, numeral, chord, notes }], with sevenths if asked. In minor the V stays the natural minor's v; use music_progression's V for the harmonic one.

music_progression(key: str, numerals: str) → listor an error

Roman numerals in a key as chords: music_progression("C", "ii7 V7 Imaj7") is Dm7, G7, Cmaj7. Upper case is major and lower minor, b and # shift the degree, ° or o is diminished, / by itself stands a numeral on a bass degree: V7/V is a secondary dominant.

music_transpose_chords(chords: list, interval: str) → listor an error

Chord symbols moved by an interval, spelled with the chord kept: music_transpose_chords(["Dm7", "G7/B"], "M2") is Em7, A7/C#. Handy for a capo or a singer's range.

music_circle_of_fifths() → any

The circle of fifths: twelve { major, minor, sharps_or_flats } from C going sharp, with the enharmonic keys given as their usual spelling.

Get it

$ quillo install music_theory               # music_theory.qwer into this folder$ quillo install music_theory --example     # and its example, music_theory.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/music_theory.qwer$ curl -O https://quillolang.org/plugins/files/music_theory.qwe

Then, in your program, all of it or only what you call:

main.qwe
use music_theory.qweruse music_note, music_from_midi from music_theory      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
serde_json = { version = "1", features = ["preserve_order"] }

Example

music_theory.qwe
// Music theory by name: a note's MIDI number and frequency, intervals, scales, chords built and// named, keys, progressions in Roman numerals, and a song moved for a singer.////   quillo run music_theory.qwe//   then open http://localhost:8080/note?n=C%234, /chord?s=F%23m7b5, /scale?tonic=Eb&kind=dorian,//   /intervals, /chords, /keys and /refuseduse "music_theory.qwer"route GET /note ? { n: str } {    let r = music_note(n)    if is_err(r) { reply 422 r.error }    r}route GET /chord ? { s: str } {    let r = music_chord(s)    if is_err(r) { reply 422 r.error }    r}route GET /scale ? { tonic: str, kind: str = "major" } {    let r = music_scale(tonic, kind)    if is_err(r) { reply 422 r.error }    r}route GET /pitch = {    midi: music_from_midi(61, false)?,    flat: music_from_midi(61, true)?,    a432: music_frequency("A4", 432.0)?,    middle_c: music_frequency("C4", 440.0)?,    tuner: music_from_frequency(445.0, 440.0)?,}route GET /intervals = {    third: music_interval("C4", "E4")?,    diminished: music_interval("C", "Fb")?.name,    tritone: music_interval("F4", "B4")?,    ninth: music_interval("C4", "D5")?,    down: music_interval("E4", "C4")?,    up_a_third: music_transpose("C4", "M3")?,    spelled: music_transpose("Eb", "A4")?,    double: music_transpose("B", "M3")?,    down_a_fifth: music_transpose("C5", "-P5")?,}route GET /scales = {    f_sharp: music_scale("F#", "major")?.notes,    gb: music_scale("Gb", "major")?.notes,    harmonic: music_scale("A", "harmonic_minor")?.notes,    blues: music_scale("E", "blues")?.notes,    octaves: music_scale("C4", "lydian")?.notes,    fits: len(music_find_scales(["C", "E", "G", "Bb"])?),}route GET /chords = {    dominant: music_chord("Bb7#9")?.notes,    half: music_chord("F#m7b5")?.notes,    dim7: music_chord("Ao7")?.notes,    thirteen: music_chord("G13b9")?.notes,    slash: music_chord("C/E")?.bass,    voiced: music_chord_midi("C/E", 4)?,    named: first(music_chord_name(["C", "E", "G", "Bb"])?).name,    inverted: first(music_chord_name(["E", "G", "C"])?).name,    both: music_chord_name(["A", "C", "E", "G"])?,    moved: music_transpose_chords(["Dm7", "G7/B", "Cmaj7"], "M2")?,}route GET /keys = {    eb: music_key("Eb major")?,    f_sharp_minor: music_key("F# minor")?.signature,    theoretical: music_key("G#")?.theoretical,    diatonic: music_diatonic("C", false)?,    sevenths: music_diatonic("A minor", true)?,    two_five_one: music_progression("C", "ii7 V7 Imaj7")?,    borrowed: music_progression("D", "I bVII IV V7/V")?,    minor: music_progression("Am", "i iv V7 i")?,    circle: first(music_circle_of_fifths()),}route GET /refused = {    note: music_note("H4").error,    midi: music_from_midi(128, false).error,    interval: music_transpose("C", "P3").error,    scale: music_scale("C", "klingon").error,    chord: music_chord("Cxyz").error,    none: music_chord_name(["C", "C#", "D", "D#"]).error,    numeral: music_progression("C", "viii").error,}
$ quillo run music_theory.qwe$ curl 'localhost:8080/note?n=C%234'$ curl 'localhost:8080/chord?s=F%23m7b5'$ curl 'localhost:8080/scale?tonic=Eb&kind=dorian'$ curl 'localhost:8080/pitch'$ curl 'localhost:8080/intervals'$ curl 'localhost:8080/scales'$ curl 'localhost:8080/chords'$ curl 'localhost:8080/keys'$ curl 'localhost:8080/refused'

Source

music_theory.qwer
//! Music theory by name: notes, MIDI numbers and frequencies, intervals spelled as a musician spells them, scales and modes, chords built from symbols and named from notes, keys with their signatures, Roman-numeral progressions, and transposition.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Value};const LETTERS: [char; 7] = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];const NATURAL: [i64; 7] = [0, 2, 4, 5, 7, 9, 11];#[derive(Clone, Copy, PartialEq)]struct Note {    letter: i64,    acc: i64,    octave: Option<i64>,}impl Note {    fn name(self) -> String {        let a = if self.acc >= 0 { "#".repeat(self.acc as usize) } else { "b".repeat((-self.acc) as usize) };        let o = self.octave.map(|o| o.to_string()).unwrap_or_default();        format!("{}{}{}", LETTERS[self.letter as usize], a, o)    }    fn pc(self) -> i64 {        (NATURAL[self.letter as usize] + self.acc).rem_euclid(12)    }    fn midi(self) -> Option<i64> {        self.octave.map(|o| (o + 1) * 12 + NATURAL[self.letter as usize] + self.acc)    }}fn parse_note(s: &str) -> Result<Note, String> {    let t = s.trim().replace('♯', "#").replace('♭', "b").replace('𝄪', "##").replace('𝄫', "bb");    let mut c = t.chars().peekable();    let l = c.next().ok_or("an empty note")?.to_ascii_uppercase();    let letter = LETTERS.iter().position(|x| *x == l).ok_or(format!("`{}` is not a note: A to G", s))? as i64;    let mut acc = 0i64;    while let Some(x) = c.peek() {        match x {            '#' => acc += 1,            'b' => acc -= 1,            'x' => acc += 2,            _ => break,        }        c.next();    }    let rest: String = c.collect();    let octave = if rest.is_empty() { None } else { Some(rest.parse::<i64>().map_err(|_| format!("`{}` is not a note: `{}` is no octave", s, rest))?) };    if acc.abs() > 4 {        return Err(format!("`{}` has too many accidentals", s));    }    Ok(Note { letter, acc, octave })}#[derive(Clone, Copy)]struct Interval {    number: i64, // 1 for a unison, 3 for a third, 10 for a tenth.    semis: i64,}fn perfect_kind(n: i64) -> bool {    matches!((n - 1).rem_euclid(7), 0 | 3 | 4)}fn major_semis(n: i64) -> i64 {    let x = n - 1;    x.div_euclid(7) * 12 + NATURAL[x.rem_euclid(7) as usize]}fn parse_interval(s: &str) -> Result<(Interval, bool), String> {    let t = s.trim();    let (down, t) = match t.strip_prefix('-') {        Some(r) => (true, r),        None => (false, t),    };    let digits = t.chars().rev().take_while(|c| c.is_ascii_digit()).count();    let (q, num) = t.split_at(t.len() - digits);    let number: i64 = num.parse().map_err(|_| format!("`{}` is not an interval: like P5, m3, M7, A4, d5", s))?;    if number < 1 {        return Err(format!("`{}` is not an interval", s));    }    let base = major_semis(number);    let d = match (q, perfect_kind(number)) {        ("P", true) => 0,        ("M", false) => 0,        ("m", false) => -1,        ("A", _) => 1,        ("AA", _) => 2,        ("d", true) => -1,        ("d", false) => -2,        ("dd", true) => -2,        ("dd", false) => -3,        _ => return Err(format!("`{}` is not an interval: a {} is {}", s, number, if perfect_kind(number) { "P, A or d" } else { "M, m, A or d" })),    };    Ok((Interval { number, semis: base + d }, down))}fn add(n: Note, i: Interval, down: bool) -> Note {    let steps = i.number - 1;    let semis = if down { -i.semis } else { i.semis };    let total = n.letter + if down { -steps } else { steps };    let letter = total.rem_euclid(7);    let oct_shift = total.div_euclid(7);    let start = NATURAL[n.letter as usize] + n.acc;    let target = start + semis;    let acc = target - (NATURAL[letter as usize] + oct_shift * 12);    Note { letter, acc, octave: n.octave.map(|o| o + oct_shift) }}fn quality(number: i64, semis: i64) -> String {    let d = semis - major_semis(number);    if perfect_kind(number) {        match d {            0 => "P".into(),            x if x > 0 => "A".repeat(x as usize),            x => "d".repeat((-x) as usize),        }    } else {        match d {            0 => "M".into(),            -1 => "m".into(),            x if x > 0 => "A".repeat(x as usize),            x => "d".repeat((-x - 1) as usize),        }    }}fn interval_words(q: &str, n: i64) -> String {    let qw = match q {        "P" => "perfect", "M" => "major", "m" => "minor", "A" => "augmented", "d" => "diminished", "AA" => "doubly augmented", "dd" => "doubly diminished",        _ => "altered",    };    let nw = match n {        1 => "unison".to_string(), 2 => "second".into(), 3 => "third".into(), 4 => "fourth".into(), 5 => "fifth".into(), 6 => "sixth".into(), 7 => "seventh".into(),        8 => "octave".into(), 9 => "ninth".into(), 10 => "tenth".into(), 11 => "eleventh".into(), 12 => "twelfth".into(), 13 => "thirteenth".into(),        x => format!("{}th", x),    };    format!("{} {}", qw, nw)}fn freq(midi: f64, a4: f64) -> f64 {    a4 * 2f64.powf((midi - 69.0) / 12.0)}fn round(x: f64, p: i32) -> f64 {    let m = 10f64.powi(p);    (x * m).round() / m}fn name_of_midi(m: i64, flats: bool) -> Note {    let pc = m.rem_euclid(12);    let table: [(i64, i64); 12] = if flats {        [(0, 0), (1, -1), (1, 0), (2, -1), (2, 0), (3, 0), (4, -1), (4, 0), (5, -1), (5, 0), (6, -1), (6, 0)]    } else {        [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (3, 0), (3, 1), (4, 0), (4, 1), (5, 0), (5, 1), (6, 0)]    };    let (letter, acc) = table[pc as usize];    Note { letter, acc, octave: Some(m.div_euclid(12) - 1) }}/// A note read: { name, letter, accidental, octave, pitch_class, midi, frequency }. `C#4` has octave 4 and MIDI 61; middle C is C4, MIDI 60. With no octave, midi and frequency are null. Takes `Bb`, `F##`, `♯`, `♭`.pub fn music_note(note: &str) -> Result<Value, String> {    let n = parse_note(note)?;    Ok(json!({        "name": n.name(), "letter": LETTERS[n.letter as usize].to_string(), "accidental": n.acc, "octave": n.octave, "pitch_class": n.pc(),        "midi": n.midi(), "frequency": n.midi().map(|m| round(freq(m as f64, 440.0), 2)),    }))}/// The note of a MIDI number, with sharps, or flats if asked: { name, midi, frequency }. 0 to 127 is MIDI's range.pub fn music_from_midi(midi: i64, flats: bool) -> Result<Value, String> {    if !(0..=127).contains(&midi) {        return Err(format!("a MIDI note is 0 to 127, not {}", midi));    }    Ok(json!({ "name": name_of_midi(midi, flats).name(), "midi": midi, "frequency": round(freq(midi as f64, 440.0), 2) }))}/// The frequency in Hz of a note at a tuning: `music_frequency("A4", 440.0)` is 440.0; A4 at 432 tunes everything down.pub fn music_frequency(note: &str, a4: f64) -> Result<f64, String> {    if a4 <= 0.0 {        return Err("the tuning must be above 0".into());    }    let m = parse_note(note)?.midi().ok_or(format!("`{}` has no octave, so no frequency", note))?;    Ok(round(freq(m as f64, a4), 3))}/// The nearest note to a frequency: { name, midi, cents, exact }, cents being how far sharp (+) or flat (-) it is, as a tuner shows.pub fn music_from_frequency(hz: f64, a4: f64) -> Result<Value, String> {    if !(hz > 0.0) || !(a4 > 0.0) {        return Err("frequencies must be above 0".into());    }    let x = 69.0 + 12.0 * (hz / a4).log2();    let m = x.round() as i64;    let cents = (x - m as f64) * 100.0;    Ok(json!({ "name": name_of_midi(m, false).name(), "midi": m, "cents": round(cents, 1), "exact": round(freq(m as f64, a4), 3) }))}fn interval_between(a: Note, b: Note) -> (Interval, bool) {    let (oa, ob) = (a.octave.unwrap_or(4), b.octave.unwrap_or(if a.octave.is_none() { 4 } else { b.octave.unwrap_or(4) }));    let mut b2 = Note { octave: Some(ob), ..b };    let a2 = Note { octave: Some(oa), ..a };    // Without octaves the second note is taken as the next one up.    if a.octave.is_none() || b.octave.is_none() {        while (b2.octave.unwrap() * 7 + b2.letter) < (oa * 7 + a.letter) {            b2.octave = Some(b2.octave.unwrap() + 1);        }    }    let staff = (b2.octave.unwrap() * 7 + b2.letter) - (oa * 7 + a.letter);    let semis = b2.midi().unwrap() - a2.midi().unwrap();    if staff < 0 || (staff == 0 && semis < 0) {        (Interval { number: -staff + 1, semis: -semis }, true)    } else {        (Interval { number: staff + 1, semis }, false)    }}/// The interval from one note to another: { name, words, semitones, descending, simple, inversion }. Spelling matters: C to E is M3, C to Fb d4. Without octaves the second note is the next one up.pub fn music_interval(from: &str, to: &str) -> Result<Value, String> {    let (a, b) = (parse_note(from)?, parse_note(to)?);    let (i, down) = interval_between(a, b);    let q = quality(i.number, i.semis);    let simple = if i.number > 8 { (i.number - 2).rem_euclid(7) + 2 } else { i.number };    let inv_number = if simple == 1 { 8 } else if simple == 8 { 1 } else { 9 - simple };    let simple_semis = i.semis - (i.number - simple) / 7 * 12;    let inv = quality(inv_number, 12 - simple_semis);    Ok(json!({        "name": format!("{}{}", q, i.number), "words": interval_words(&q, i.number), "semitones": i.semis, "descending": down,        "simple": format!("{}{}", q, simple), "inversion": format!("{}{}", inv, inv_number),    }))}/// A note moved by an interval, spelled right: `music_transpose("C4", "M3")` is E4, `("Eb", "A4")` is A, and `-P5` goes down.pub fn music_transpose(note: &str, interval: &str) -> Result<String, String> {    let n = parse_note(note)?;    let (i, down) = parse_interval(interval)?;    let r = add(n, i, down);    if r.acc.abs() > 2 {        return Err(format!("{} up a {} would need {} accidentals", note, interval, r.acc.abs()));    }    Ok(r.name())}const SCALES: &[(&str, &[&str])] = &[    ("major", &["P1", "M2", "M3", "P4", "P5", "M6", "M7"]),    ("ionian", &["P1", "M2", "M3", "P4", "P5", "M6", "M7"]),    ("minor", &["P1", "M2", "m3", "P4", "P5", "m6", "m7"]),    ("natural_minor", &["P1", "M2", "m3", "P4", "P5", "m6", "m7"]),    ("aeolian", &["P1", "M2", "m3", "P4", "P5", "m6", "m7"]),    ("harmonic_minor", &["P1", "M2", "m3", "P4", "P5", "m6", "M7"]),    ("melodic_minor", &["P1", "M2", "m3", "P4", "P5", "M6", "M7"]),    ("dorian", &["P1", "M2", "m3", "P4", "P5", "M6", "m7"]),    ("phrygian", &["P1", "m2", "m3", "P4", "P5", "m6", "m7"]),    ("lydian", &["P1", "M2", "M3", "A4", "P5", "M6", "M7"]),    ("mixolydian", &["P1", "M2", "M3", "P4", "P5", "M6", "m7"]),    ("locrian", &["P1", "m2", "m3", "P4", "d5", "m6", "m7"]),    ("major_pentatonic", &["P1", "M2", "M3", "P5", "M6"]),    ("minor_pentatonic", &["P1", "m3", "P4", "P5", "m7"]),    ("blues", &["P1", "m3", "P4", "d5", "P5", "m7"]),    ("whole_tone", &["P1", "M2", "M3", "A4", "A5", "A6"]),    ("diminished", &["P1", "M2", "m3", "P4", "d5", "m6", "d7", "M7"]),    ("chromatic", &["P1", "m2", "M2", "m3", "M3", "P4", "A4", "P5", "m6", "M6", "m7", "M7"]),];fn scale_notes(tonic: Note, kind: &str) -> Result<Vec<Note>, String> {    let k = kind.trim().to_lowercase().replace([' ', '-'], "_");    let (_, iv) = SCALES.iter().find(|s| s.0 == k).ok_or(format!("no scale `{}`; there are {}", kind, SCALES.iter().map(|s| s.0).collect::<Vec<_>>().join(", ")))?;    iv.iter().map(|i| Ok(add(tonic, parse_interval(i)?.0, false))).collect()}/// A scale from its tonic, spelled by intervals so each letter appears once: { notes, intervals, semitones }. There are major, minor, harmonic_minor, melodic_minor, the seven modes, pentatonics, blues, whole_tone, diminished and chromatic. With an octave on the tonic, the notes have octaves too.pub fn music_scale(tonic: &str, kind: &str) -> Result<Value, String> {    let t = parse_note(tonic)?;    let notes = scale_notes(t, kind)?;    let k = kind.trim().to_lowercase().replace([' ', '-'], "_");    let (_, iv) = SCALES.iter().find(|s| s.0 == k).unwrap();    let pcs: Vec<i64> = notes.iter().map(|n| (n.pc() - t.pc()).rem_euclid(12)).collect();    Ok(json!({ "tonic": t.name(), "scale": k, "notes": notes.iter().map(|n| n.name()).collect::<Vec<_>>(), "intervals": iv, "semitones": pcs }))}/// The scales a set of notes fits, from these tonics: all names, for "what can I solo with": [{ tonic, scale }].pub fn music_find_scales(notes: Vec<String>) -> Result<Value, String> {    let mut mask = 0u16;    for n in &notes {        mask |= 1 << parse_note(n)?.pc();    }    let mut out = vec![];    for t in ["C", "Db", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"] {        let tonic = parse_note(t)?;        for (s, _) in SCALES.iter().filter(|s| !matches!(s.0, "ionian" | "aeolian" | "natural_minor" | "chromatic")) {            let sc = scale_notes(tonic, s)?;            let m = sc.iter().fold(0u16, |a, n| a | 1 << n.pc());            if m & mask == mask {                out.push(json!({ "tonic": t, "scale": s }));            }        }    }    Ok(json!(out))}const SUFFIX: &[(&str, &[&str])] = &[    ("", &["P1", "M3", "P5"]), ("m", &["P1", "m3", "P5"]), ("dim", &["P1", "m3", "d5"]), ("aug", &["P1", "M3", "A5"]),    ("sus2", &["P1", "M2", "P5"]), ("sus4", &["P1", "P4", "P5"]), ("5", &["P1", "P5"]), ("6", &["P1", "M3", "P5", "M6"]), ("m6", &["P1", "m3", "P5", "M6"]),    ("7", &["P1", "M3", "P5", "m7"]), ("maj7", &["P1", "M3", "P5", "M7"]), ("m7", &["P1", "m3", "P5", "m7"]), ("mMaj7", &["P1", "m3", "P5", "M7"]),    ("dim7", &["P1", "m3", "d5", "d7"]), ("m7b5", &["P1", "m3", "d5", "m7"]), ("aug7", &["P1", "M3", "A5", "m7"]), ("7sus4", &["P1", "P4", "P5", "m7"]),    ("9", &["P1", "M3", "P5", "m7", "M9"]), ("maj9", &["P1", "M3", "P5", "M7", "M9"]), ("m9", &["P1", "m3", "P5", "m7", "M9"]),    ("add9", &["P1", "M3", "P5", "M9"]), ("madd9", &["P1", "m3", "P5", "M9"]), ("6/9", &["P1", "M3", "P5", "M6", "M9"]),    ("11", &["P1", "M3", "P5", "m7", "M9", "P11"]), ("m11", &["P1", "m3", "P5", "m7", "M9", "P11"]),    ("13", &["P1", "M3", "P5", "m7", "M9", "M13"]), ("maj13", &["P1", "M3", "P5", "M7", "M9", "M13"]), ("m13", &["P1", "m3", "P5", "m7", "M9", "M13"]),];fn normal_quality(q: &str) -> String {    let mut s = q.replace(['(', ')'], "").replace("M7", "maj7").replace("Maj7", "maj7").replace("MA7", "maj7").replace("min", "m").replace('Δ', "maj7").replace('ø', "m7b5").replace('°', "dim").replace("-7", "m7").replace("+7", "aug7");    s = s.replace("mmaj7", "mMaj7");    if s == "maj" || s == "M" || s == "major" {        return String::new();    }    if s == "o" {        return "dim".into();    }    if s == "o7" {        return "dim7".into();    }    if s == "+" {        return "aug".into();    }    if s == "-" {        return "m".into();    }    if s == "sus" {        return "sus4".into();    }    if s == "m7b5" || s == "halfdim" {        return "m7b5".into();    }    if s == "maj7maj7" || s == "maj7" {        return "maj7".into();    }    if s == "m7b5m7b5" {        return "m7b5".into();    }    s}struct Chord {    root: Note,    quality: String,    bass: Option<Note>,    notes: Vec<Note>,    intervals: Vec<String>,}fn parse_chord(sym: &str) -> Result<Chord, String> {    let t = sym.trim().replace('♯', "#").replace('♭', "b");    if t.is_empty() {        return Err("an empty chord".into());    }    let (main, bass) = match t.rsplit_once('/') {        Some((m, b)) if b.chars().next().map(|c| LETTERS.contains(&c)).unwrap_or(false) => (m.to_string(), Some(parse_note(b)?)),        _ => (t.clone(), None),    };    let chars: Vec<char> = main.chars().collect();    let mut i = 1;    while i < chars.len() && (chars[i] == '#' || chars[i] == 'b') {        i += 1;    }    let root = parse_note(&chars[..i].iter().collect::<String>())?;    let raw: String = chars[i..].iter().collect();    let q = normal_quality(&raw);    // A known quality, then any alterations: 7b9, 7#5#9, 9#11, 13b9, m7b5, 7alt.    let mut best: Option<(&str, &[&str])> = None;    for (name, iv) in SUFFIX {        if q.starts_with(name) && best.map(|b| b.0.len() < name.len()).unwrap_or(true) {            best = Some((name, iv));        }    }    let (base, iv) = best.ok_or(format!("`{}` is not a chord quality", raw))?;    let mut intervals: Vec<String> = iv.iter().map(|s| s.to_string()).collect();    let mut rest = q[base.len()..].to_string();    if rest == "alt" {        intervals = vec!["P1", "M3", "m7", "m9", "A9", "d5", "m13"].iter().map(|s| s.to_string()).collect();        rest.clear();    }    while !rest.is_empty() {        let (kind, tail) = if let Some(r) = rest.strip_prefix("add") {            ("add", r)        } else if let Some(r) = rest.strip_prefix("no") {            ("no", r)        } else if let Some(r) = rest.strip_prefix('b') {            ("b", r)        } else if let Some(r) = rest.strip_prefix('#') {            ("#", r)        } else {            return Err(format!("`{}` in `{}` is not a chord alteration", rest, sym));        };        let digits = tail.chars().take_while(|c| c.is_ascii_digit()).count();        if digits == 0 {            return Err(format!("`{}` in `{}` is not a chord alteration", rest, sym));        }        let d: i64 = tail[..digits].parse().unwrap_or(0);        let plain = |d: i64| if perfect_kind(d) { format!("P{}", d) } else { format!("M{}", d) };        match kind {            "add" => intervals.push(plain(d)),            "no" => intervals.retain(|x| !x.ends_with(&d.to_string()) || x.len() > 1 + d.to_string().len()),            _ => {                let changed = match (kind, perfect_kind(d)) {                    ("b", true) => format!("d{}", d),                    ("b", false) => format!("m{}", d),                    ("#", _) => format!("A{}", d),                    _ => unreachable!(),                };                intervals.retain(|x| x.trim_start_matches(|c: char| c.is_alphabetic()) != d.to_string());                intervals.push(changed);            }        }        rest = tail[digits..].to_string();    }    intervals.sort_by_key(|i| i.trim_start_matches(|c: char| c.is_alphabetic()).parse::<i64>().unwrap_or(0));    let notes = intervals.iter().map(|i| Ok(add(root, parse_interval(i)?.0, false))).collect::<Result<Vec<_>, String>>()?;    Ok(Chord { root, quality: q, bass, notes, intervals })}/// A chord symbol built: { symbol, root, quality, bass, notes, intervals, semitones }. Reads `Cmaj7`, `F#m7b5`, `Bb7#9`, `Dsus4`, `Ao7`, `G13b9`, `C/E`, `C6/9`, `E7alt`, `Cadd9`, `C7no5`.pub fn music_chord(symbol: &str) -> Result<Value, String> {    let c = parse_chord(symbol)?;    Ok(json!({        "symbol": symbol.trim(), "root": c.root.name(), "quality": c.quality, "bass": c.bass.map(|b| b.name()),        "notes": c.notes.iter().map(|n| n.name()).collect::<Vec<_>>(),        "intervals": c.intervals,        "semitones": c.notes.iter().map(|n| (n.pc() - c.root.pc()).rem_euclid(12)).collect::<Vec<_>>(),    }))}/// A chord's notes as MIDI numbers in a voicing from a given root octave, with a slash bass underneath: `music_chord_midi("C/E", 4)` is [52, 60, 64, 67].pub fn music_chord_midi(symbol: &str, octave: i64) -> Result<Vec<i64>, String> {    let c = parse_chord(symbol)?;    let root = Note { octave: Some(octave), ..c.root };    let mut out = vec![];    for i in &c.intervals {        let m = add(root, parse_interval(i)?.0, false).midi().unwrap_or(0);        out.push(m);    }    if let Some(b) = c.bass {        let mut m = Note { octave: Some(octave - 1), ..b }.midi().unwrap_or(0);        while m >= out[0] {            m -= 12;        }        out.insert(0, m);    }    Ok(out)}/// The names a set of notes may go by, likeliest first: `["E", "G", "C"]` gives C/E; [{ name, root, inversion }]. The first note is the bass.pub fn music_chord_name(notes: Vec<String>) -> Result<Value, String> {    if notes.len() < 2 {        return Err("a chord has at least two notes".into());    }    let ns = notes.iter().map(|n| parse_note(n)).collect::<Result<Vec<_>, String>>()?;    let bass = ns[0];    let set: Vec<i64> = {        let mut p: Vec<i64> = ns.iter().map(|n| n.pc()).collect();        p.sort();        p.dedup();        p    };    let mut seen: Vec<(i64, String, Value)> = vec![];    for root in &ns {        let mut rel: Vec<i64> = set.iter().map(|p| (p - root.pc()).rem_euclid(12)).collect();        rel.sort();        for (name, iv) in SUFFIX {            let mut want: Vec<i64> = iv.iter().map(|i| parse_interval(i).map(|x| x.0.semis.rem_euclid(12)).unwrap_or(0)).collect();            want.sort();            want.dedup();            if want == rel {                let r = Note { octave: None, ..*root };                let mut nm = format!("{}{}", r.name(), name);                let inv = root.pc() != bass.pc();                if inv {                    nm = format!("{}/{}", nm, Note { octave: None, ..bass }.name());                }                let score = (inv as i64) * 10 + name.len() as i64 + (root.acc != 0) as i64;                if !seen.iter().any(|s| s.1 == nm) {                    seen.push((score, nm.clone(), json!({ "name": nm, "root": r.name(), "inversion": inv })));                }            }        }    }    seen.sort_by_key(|s| s.0);    if seen.is_empty() {        return Err(format!("no chord name fits {}", notes.join(" ")));    }    Ok(json!(seen.into_iter().map(|s| s.2).collect::<Vec<_>>()))}/// A key's facts: { key, mode, sharps, flats, signature, notes, relative, parallel, theoretical }. `C`, `F# minor`, `Bbm`, `Eb major`. `theoretical` marks keys like G# major that need double sharps.pub fn music_key(key: &str) -> Result<Value, String> {    let (tonic, minor) = parse_key(key)?;    let notes = scale_notes(tonic, if minor { "minor" } else { "major" })?;    let major_tonic = if minor { add(tonic, parse_interval("m3")?.0, false) } else { tonic };    let major = scale_notes(major_tonic, "major")?;    let sharps: Vec<String> = [3, 0, 4, 1, 5, 2, 6].iter().filter_map(|l| major.iter().find(|n| n.letter == *l && n.acc > 0)).map(|n| n.name()).collect();    let flats: Vec<String> = [6, 2, 5, 1, 4, 0, 3].iter().filter_map(|l| major.iter().find(|n| n.letter == *l && n.acc < 0)).map(|n| n.name()).collect();    let rel = if minor { add(tonic, parse_interval("m3")?.0, false) } else { add(tonic, parse_interval("M6")?.0, false) };    Ok(json!({        "key": format!("{} {}", tonic.name(), if minor { "minor" } else { "major" }), "mode": if minor { "minor" } else { "major" },        "sharps": sharps.len(), "flats": flats.len(), "signature": if sharps.is_empty() { flats } else { sharps },        "notes": notes.iter().map(|n| n.name()).collect::<Vec<_>>(),        "relative": format!("{} {}", rel.name(), if minor { "major" } else { "minor" }),        "parallel": format!("{} {}", tonic.name(), if minor { "major" } else { "minor" }),        "theoretical": major.iter().any(|n| n.acc.abs() > 1),    }))}fn parse_key(key: &str) -> Result<(Note, bool), String> {    let t = key.trim();    let low = t.to_lowercase();    let (name, minor) = if let Some(r) = low.strip_suffix(" minor") {        (&t[..r.len()], true)    } else if let Some(r) = low.strip_suffix(" major") {        (&t[..r.len()], false)    } else if t.len() > 1 && t.ends_with('m') {        (&t[..t.len() - 1], true)    } else {        (t, false)    };    let n = parse_note(name)?;    Ok((Note { octave: None, ..n }, minor))}fn roman_value(s: &str) -> Option<i64> {    match s.to_lowercase().as_str() {        "i" => Some(1), "ii" => Some(2), "iii" => Some(3), "iv" => Some(4), "v" => Some(5), "vi" => Some(6), "vii" => Some(7),        _ => None,    }}const ROMAN: [&str; 7] = ["I", "II", "III", "IV", "V", "VI", "VII"];/// The chords of a key by degree: [{ degree, numeral, chord, notes }], with sevenths if asked. In minor the V stays the natural minor's v; use music_progression's `V` for the harmonic one.pub fn music_diatonic(key: &str, sevenths: bool) -> Result<Value, String> {    let (tonic, minor) = parse_key(key)?;    let sc = scale_notes(tonic, if minor { "minor" } else { "major" })?;    let mut out = vec![];    for d in 0..7 {        let pick = |o: usize| sc[(d + o) % 7];        let root = pick(0);        let third = (pick(2).pc() - root.pc()).rem_euclid(12);        let fifth = (pick(4).pc() - root.pc()).rem_euclid(12);        let seventh = (pick(6).pc() - root.pc()).rem_euclid(12);        let tri = match (third, fifth) {            (4, 7) => "",            (3, 7) => "m",            (3, 6) => "dim",            (4, 8) => "aug",            _ => "?",        };        let (suffix, mark) = if sevenths {            match (tri, seventh) {                ("", 11) => ("maj7", "maj7"),                ("", 10) => ("7", "7"),                ("m", 10) => ("m7", "7"),                ("dim", 10) => ("m7b5", "ø7"),                ("dim", 9) => ("dim7", "°7"),                ("m", 11) => ("mMaj7", "maj7"),                ("aug", 11) => ("augmaj7", "+maj7"),                _ => ("7", "7"),            }        } else {            (tri, if tri == "dim" { "°" } else if tri == "aug" { "+" } else { "" })        };        let upper = tri == "" || tri == "aug";        let num = if upper { ROMAN[d].to_string() } else { ROMAN[d].to_lowercase() };        let mut notes = vec![pick(0).name(), pick(2).name(), pick(4).name()];        if sevenths {            notes.push(pick(6).name());        }        out.push(json!({ "degree": d + 1, "numeral": format!("{}{}", num, mark), "chord": format!("{}{}", root.name(), suffix), "notes": notes }));    }    Ok(json!(out))}/// Roman numerals in a key as chords: `music_progression("C", "ii7 V7 Imaj7")` is Dm7, G7, Cmaj7. Upper case is major and lower minor, `b` and `#` shift the degree, `°` or `o` is diminished, `/` by itself stands a numeral on a bass degree: `V7/V` is a secondary dominant.pub fn music_progression(key: &str, numerals: &str) -> Result<Vec<String>, String> {    let (tonic, minor) = parse_key(key)?;    let mut out = vec![];    for tok in numerals.split(|c: char| c.is_whitespace() || c == '-' || c == '|').filter(|s| !s.is_empty()) {        let (main, of) = match tok.split_once('/') {            Some((m, o)) => (m, Some(o)),            None => (tok, None),        };        let base = match of {            Some(o) => degree_root(tonic, minor, o)?.0,            None => tonic,        };        let base_minor = if of.is_some() { false } else { minor };        let (root, upper, rest) = degree_root(base, base_minor, main)?;        let suffix = match rest {            "" => if upper { "" } else { "m" }.to_string(),            "°" | "o" | "dim" => "dim".into(),            "°7" | "o7" | "dim7" => "dim7".into(),            "ø" | "ø7" => "m7b5".into(),            "+" | "aug" => "aug".into(),            r if upper => r.to_string(),            r => format!("m{}", r),        };        out.push(format!("{}{}", root.name(), suffix));    }    Ok(out)}fn degree_root<'a>(tonic: Note, minor: bool, tok: &'a str) -> Result<(Note, bool, &'a str), String> {    let (shift, t) = if let Some(r) = tok.strip_prefix('b') {        (-1, r)    } else if let Some(r) = tok.strip_prefix('#') {        (1, r)    } else {        (0, tok)    };    let len = t.chars().take_while(|c| matches!(c, 'i' | 'I' | 'v' | 'V')).count();    if len == 0 {        return Err(format!("`{}` is not a Roman numeral", tok));    }    let d = roman_value(&t[..len]).ok_or(format!("`{}` is not a Roman numeral", tok))?;    let upper = t.chars().next().map(|c| c.is_uppercase()).unwrap_or(false);    let sc = scale_notes(tonic, if minor { "minor" } else { "major" })?;    let mut n = sc[d as usize - 1];    n.acc += shift;    Ok((n, upper, &t[len..]))}/// Chord symbols moved by an interval, spelled with the chord kept: `music_transpose_chords(["Dm7", "G7/B"], "M2")` is Em7, A7/C#. Handy for a capo or a singer's range.pub fn music_transpose_chords(chords: Vec<String>, interval: &str) -> Result<Vec<String>, String> {    let (i, down) = parse_interval(interval)?;    let mut out = vec![];    for c in &chords {        let ch = parse_chord(c)?;        let t = c.trim().replace('♯', "#").replace('♭', "b");        let main = t.split('/').next().unwrap_or("");        let main = if t.contains("6/9") { t.as_str() } else { main };        let tail = main.chars().skip(1).skip_while(|c| *c == '#' || *c == 'b').collect::<String>();        let tail = tail.split('/').collect::<Vec<_>>().join("/");        let r = add(ch.root, i, down);        let mut s = format!("{}{}", r.name(), if t.contains("6/9") { tail.split('/').take(2).collect::<Vec<_>>().join("/") } else { tail.clone() });        if let Some(b) = ch.bass {            s = format!("{}/{}", s, add(b, i, down).name());        }        out.push(s);    }    Ok(out)}/// The circle of fifths: twelve { major, minor, sharps_or_flats } from C going sharp, with the enharmonic keys given as their usual spelling.pub fn music_circle_of_fifths() -> Value {    let majors = ["C", "G", "D", "A", "E", "B", "F#", "Db", "Ab", "Eb", "Bb", "F"];    json!(majors        .iter()        .enumerate()        .map(|(i, m)| {            let t = parse_note(m).unwrap();            let rel = add(t, Interval { number: 6, semis: 9 }, false);            let n = i as i64;            json!({ "major": m, "minor": format!("{}m", rel.name()), "accidentals": if n <= 6 { n } else { n - 12 } })        })        .collect::<Vec<_>>())}
Thread
57Useful? Like it, or add to its thread below.

Thread 0 posts

On Music theory: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.