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
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 errorA 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 errorThe 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 errorThe 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 errorThe 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 errorThe 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 errorA 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 errorA 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 errorThe 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 errorA 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 errorA 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 errorThe 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 errorA 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 errorThe 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 errorRoman 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 errorChord 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() → anyThe 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.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/music_theory.qwer$ curl -O https://quillolang.org/plugins/files/music_theory.qweThen, in your program, all of it or only what you call:
use music_theory.qweruse music_note, music_from_midi from music_theory // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
serde_json = { version = "1", features = ["preserve_order"] }Example
// 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 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 ¬es { 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 0 posts
On Music theory: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.