A comprehensive music theory library for Elixir, ported from the popular tonal.js library. Harmony provides a complete set of tools for working with notes, intervals, chords, scales, and other music theory concepts.
- Notes & Pitch Classes - Work with musical notes, MIDI numbers, frequencies, and enharmonic equivalents
- Intervals - Calculate and manipulate musical intervals with proper enharmonic spelling
- Chords - Chord recognition, generation, analysis, and compatible scale finding
- Scales - Scale generation, mode calculation, and chord compatibility
- Transposition - Transpose notes and musical structures by intervals
- Roman Numerals - Roman numeral analysis for functional harmony
- Circle of Fifths - Navigate the circle of fifths and key relationships
Add harmony to your list of dependencies in mix.exs:
def deps do
[
{:harmony, "~> 0.1.0"}
]
end# Get note information
note = Harmony.Note.get("C4")
note.midi # => 60
note.freq # => 261.63
note.pc # => "C"
# Create from MIDI or frequency
Harmony.Note.from_midi(60) # => %Note{name: "C4", ...}
Harmony.Note.from_freq(440.0) # => %Note{name: "A4", ...}
# Simplify notes
Harmony.Note.simplify("B#4") # => "C5"
Harmony.Note.simplify("C###") # => "D#"# Get intervals
Harmony.Interval.get("5P") # Perfect fifth
Harmony.Interval.get("5P").semitones # => 7
# Calculate distance between notes
Harmony.Interval.distance("C", "G") # => "5P"
Harmony.Interval.distance("C4", "E4") # => "3M"
# Operations
Harmony.Interval.invert("3M") # => "6m"
Harmony.Interval.add("3M", "3m") # => "5P"# Transpose notes
Harmony.Transpose.transpose("C4", "5P") # => "G4"
Harmony.Transpose.transpose("D", "3M") # => "F#"
# Create reusable transposers
up_fifth = Harmony.Transpose.transpose_by("5P")
["C", "D", "E"] |> Enum.map(up_fifth)
# => ["G", "A", "B"]# Get chord information
chord = Harmony.Chord.get("Cmaj7")
chord.notes # => ["C", "E", "G", "B"]
chord.intervals # => ["1P", "3M", "5P", "7M"]
# Find compatible scales
Harmony.Chord.chord_scales("Cmaj7")
# => ["major", "lydian", "major pentatonic", ...]
# Transpose chords
Harmony.Chord.transpose("Dm7", "5P") # => "Am7"# Get scale information
scale = Harmony.Scale.get("C major")
scale.notes # => ["C", "D", "E", "F", "G", "A", "B"]
scale.intervals # => ["1P", "2M", "3M", "4P", "5P", "6M", "7M"]
# Get all modes
Harmony.Scale.modes("C major")
# => [["C", "major"], ["D", "dorian"], ["E", "phrygian"], ...]
# Find compatible chords
Harmony.Scale.chords("D dorian")
# => ["m", "m7", "m9", "m11", "sus4", "7sus4", ...]
# Generate scale ranges
range_fn = Harmony.Scale.range_of("C major")
range_fn.("C4", "C5")
# => ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"]# Get Roman numeral information
rn = Harmony.RomanNumeral.get("V")
rn.interval # => "5P"
rn.major # => true
# Build progressions
["I", "IV", "V", "I"]
|> Enum.map(fn numeral ->
Harmony.RomanNumeral.get(numeral).interval
|> (&Harmony.Transpose.transpose("C", &1)).()
end)
# => ["C", "F", "G", "C"]Comprehensive documentation is available in the /docs folder:
- Getting Started - Installation and quick start guide
- Notes - Working with musical notes
- Intervals - Musical intervals and distances
- Transposition - Transposing notes and structures
- Chords - Chord recognition and analysis
- Scales - Scale generation and modes
| Module | Description |
|---|---|
Harmony.Note |
Musical notes, pitch classes, MIDI numbers, and frequencies |
Harmony.Interval |
Musical intervals and distance calculations |
Harmony.Transpose |
Transposition operations for notes and intervals |
Harmony.Chord |
Chord recognition, generation, and analysis |
Harmony.Scale |
Scale generation, modes, and compatibility |
Harmony.RomanNumeral |
Roman numeral analysis for functional harmony |
Harmony.Pitch |
Low-level pitch representation and coordinate system |
Harmony.Key |
Key signatures and key-related operations |
Harmony is built with Elixir's functional programming paradigm in mind:
- Immutable Data - All operations return new values rather than modifying existing ones
- Pattern Matching - Extensive use of Elixir's pattern matching for clean, readable code
- Compile-Time Generation - Notes and intervals are generated at compile time using macros for optimal performance
- Composability - Functions support partial application and are designed to work well with
EnumandStream - Type Safety - Comprehensive typespecs throughout the library
The library uses compile-time macros to generate all possible notes and intervals, resulting in:
- Zero runtime overhead for note/interval lookups
- Constant-time access to note properties
- Efficient pattern matching for all operations
When invalid input is provided, functions return "empty" structs rather than raising errors:
Harmony.Note.get("invalid") # => %Note{empty: true, ...}
Harmony.Chord.get("xyz") # => %Chord{empty: true, ...}Functions work with both pitch classes (no octave) and specific notes (with octave):
# Pitch classes
Harmony.Note.get("C").pc # => "C"
Harmony.Chord.get("C").notes # => ["C", "E", "G"]
# Notes with octaves
Harmony.Note.get("C4").oct # => 4
Harmony.Chord.get("C4").notes # => ["C4", "E4", "G4"]The library maintains proper enharmonic spelling based on context:
Harmony.Transpose.transpose("F#", "5P") # => "C#" (not Db)
Harmony.Transpose.transpose("Gb", "5P") # => "Db" (not C#)This library is an Elixir port of the popular JavaScript library tonal.js. While maintaining the core algorithms and concepts, it adapts them to Elixir's strengths:
| Feature | tonal.js | Harmony |
|---|---|---|
| Language | JavaScript | Elixir |
| Data Structure | Mutable Objects | Immutable Structs |
| Performance | Runtime computation | Compile-time generation |
| API Style | OOP/Functional hybrid | Pure Functional |
| Pattern Matching | Limited | Extensive |
| Type System | TypeScript (optional) | Dialyzer typespecs |
# Get dependencies
mix deps.get
# Run tests
mix test
# Run dialyzer for type checking
mix dialyzer
# Generate documentation
mix docsprogression = ["C", "Am", "F", "G"]
transpose_to_d = Harmony.Transpose.transpose_by("2M")
progression |> Enum.map(transpose_to_d)
# => ["D", "Bm", "G", "A"]scale = Harmony.Scale.get("C major")
scale.notes
|> Enum.with_index(1)
|> Enum.each(fn {note, degree} ->
IO.puts("Degree #{degree}: #{note}")
end)
# Degree 1: C
# Degree 2: D
# ...Enum.map(0..11, fn n ->
Harmony.Transpose.transpose_fifths("C", n)
end)
# => ["C", "G", "D", "A", "E", "B", "F#", "Db", "Ab", "Eb", "Bb", "F"]# Find all scales that work over a ii-V-I progression
chords = ["Dm7", "G7", "Cmaj7"]
chords
|> Enum.map(&Harmony.Chord.chord_scales/1)
|> Enum.reduce(&MapSet.intersection(MapSet.new(&1), MapSet.new(&2)))
# => Common scales that work over all three chordsContributions are welcome! This library is being actively maintained and improved. Please feel free to:
- Report bugs
- Suggest new features
- Submit pull requests
- Improve documentation
This project is licensed under the MIT License.
Special thanks to the tonal.js community for creating such a comprehensive music theory library that inspired this Elixir port.