harmonia 0.1.0

Instrument-agnostic music theory primitives: pitch classes, intervals, scales, chords, keys, and progressions.
Documentation
# 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.

[fretboard-explorer]: https://github.com/sogh/fretboard-explorer

## Examples

### Parse a progression and detect the key

```rust
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

```rust
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

```rust
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

```rust
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

```rust
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)

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

## What's in

**Foundation** — `PitchClass`, `Interval`, `Note` (`Letter` +
`Accidental`), and `spell_heptatonic` for diatonic letter spelling.

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

**Analysis** — `Key` 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:

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

## License

MIT