harmonia 0.1.0

Instrument-agnostic music theory primitives: pitch classes, intervals, scales, chords, keys, and progressions.
Documentation
  • Coverage
  • 51.53%
    101 out of 196 items documented4 out of 75 items with examples
  • Size
  • Source code size: 161.82 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 12.86 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 58s Average build duration of successful builds.
  • all releases: 58s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • sogh/harmonia
    1 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • sogh

harmonia

Instrument-agnostic music theory primitives in Rust.

harmonia gives you typed, parseable, analyzable musical values: pitch classes, intervals, scales, chords, keys, Roman numerals, and a small catalogue of analysis routines. There's no fretboard, no keyboard, no audio — just the theory you'd write on a chalkboard, expressed as Rust values you can compose into whatever instrument or interface layer you need on top.

Why harmonia?

  • It's the in-between layer most music libraries skip. Audio crates handle synthesis. Notation crates handle rendering. harmonia handles the theory between them: which chords are diatonic, what the V7 of ii is, whether a progression sits in C major or A minor.

  • Strong types over strings. A Chord is not a String; a RomanNumeral is not a String. You can still parse them from user input — every public type implements FromStr and round-trips through Display — but once parsed they're values you can match on, hash, and pass through type-checked APIs.

  • Real analysis built in. Detect a key from a chord progression, label chords with Roman numerals, find scales that fit over a chord transition, and generate context-aware next-chord suggestions across six categories (diatonic, resolution, borrowed, secondary, relative, chromatic).

  • Pluggable. Optional serde for persistence; zero required dependencies otherwise.

The theory model is extracted from fretboard-explorer, where the same primitives drove a guitar fretboard, a piano keyboard, and a trumpet fingering chart. The split was useful in JavaScript; it's useful in Rust too. Plausible uses: chord-chart generators, practice trainers, theory-teaching aids, exercise generators, and the brain of any instrument-specific layer.

Examples

Parse a progression and detect the key

use harmonia::{detect_key, Chord};

let progression: Vec<Chord> = ["C", "Am", "Dm", "G7"]
    .iter().map(|s| s.parse().unwrap()).collect();

let candidates = detect_key(&progression);
let best = &candidates[0];
println!("{}: {}/{} chords fit", best.key, best.matched, best.total);
// → C major: 4/4 chords fit

Roman-numeral analysis in a key

use harmonia::{Chord, Key, PitchClass};

let c_major = Key::new(PitchClass::C);
for symbol in ["Am", "F", "G7", "C7", "F#"] {
    let chord: Chord = symbol.parse().unwrap();
    let roman = c_major.roman_for(chord)
        .map(|r| r.to_string())
        .unwrap_or_else(|| "".into());
    println!("{symbol:>4}{roman}");
}
//   Am → vi
//    F → IV
//   G7 → V7
//   C7 → I7        (the I-as-dom7 fuzzy fallback)
//   F# → —          (out of key)

Spell a scale with diatonic letter names

use harmonia::Scale;

let g_major: Scale = "G Ionian".parse().unwrap();
let labels: Vec<String> = g_major.spelled().unwrap()
    .iter().map(|n| n.to_string()).collect();
assert_eq!(labels, ["G", "A", "B", "C", "D", "E", "F♯"]);

The spelling algorithm picks the right accidentals so each natural letter A–G appears exactly once — no A♯ next to an A.

Suggest the next chord

use harmonia::{suggest_next_chords, Chord};

let history: Vec<Chord> = ["C", "Am", "F"]
    .iter().map(|s| s.parse().unwrap()).collect();

let result = suggest_next_chords(&history, None);
let key = result.key.unwrap();
println!("Key: {}\n", key.key);

for s in result.suggestions.iter().take(6) {
    let roman = s.roman.as_ref().map(|r| r.to_string()).unwrap_or_default();
    println!("  {} ({}) — {}", s.chord, roman, s.reason);
}
// Key: C major
//
//   C (I) — diatonic in C major
//   Dm (ii) — diatonic in C major
//   Em (iii) — diatonic in C major
//   F (IV) — diatonic in C major
//   G (V) — diatonic in C major
//   Am (vi) — diatonic in C major

Suggestions also span resolution moves (V→I, ii→V, IV→V), borrowed chords (♭III, iv, ♭VI, ♭VII), secondary dominants (V7/ii, V7/iii, …), relative major/minor, and chromatic motion — each tagged with its category so a UI can group them.

Find scales that fit over a chord transition

use harmonia::{suggest_scales_for_bracket, Chord};

let g: Chord = "G".parse().unwrap();
let am: Chord = "Am".parse().unwrap();

let suggestions = suggest_scales_for_bracket(Some(g), Some(am));
for s in suggestions.iter().take(3) {
    println!("{:<22} {}", s.scale.to_string(), s.reasoning);
}
// G Ionian (major)       G and Am are both diatonic to G Ionian (major)
// G Mixolydian           G and Am are both diatonic to G Mixolydian
// A Dorian               G and Am are both diatonic to A Dorian

(G-rooted modes win the prev-chord bias; A Dorian sneaks ahead of C Ionian on the next-chord bias.)

Persist results (with the serde feature)

let json = serde_json::to_string(&result)?;
let restored: harmonia::ChordSuggestionResult = serde_json::from_str(&json)?;

What's in

FoundationPitchClass, Interval, Note (Letter + Accidental), and spell_heptatonic for diatonic letter spelling.

CatalogueScaleKind (16 scales: modes, pentatonics, harmonic/melodic minor, symmetric) and ChordQuality (12 qualities: 6 triads + 6 sevenths), each with Scale / Chord companions.

AnalysisKey with diatonic chord templates, RomanNumeral (typed with alteration, degree, quality, and secondary-of), detect_key, suggest_scales_for_bracket, suggest_next_chords.

Every public data type implements FromStr, Display, PartialEq, and Hash.

Crate features

  • serde (off by default) — derives Serialize/Deserialize on every public data type. PitchClass and Interval serialize as integers; SuggestionCategory as a lowercase string. Enable with:

    harmonia = { version = "0.1", features = ["serde"] }
    

License

MIT