Harmony

CIHex.pmDocumentationLicense

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.

Features

Installation

Add harmony to your list of dependencies in mix.exs:

def deps do
  [
    {:harmony, "~> 0.1.0"}
  ]
end

Quick Examples

Notes

# 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#"

Intervals

# 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"

Transposition

# 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"]

Chords

# 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"

Scales

# 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"]

Roman Numerals

# 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"]

Documentation

Comprehensive documentation is available in the /docs folder:

Module Overview

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

Design Philosophy

Harmony is built with Elixir's functional programming paradigm in mind:

Performance

The library uses compile-time macros to generate all possible notes and intervals, resulting in:

Key Concepts

Empty Values

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, ...}

Pitch Classes vs Notes with Octaves

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"]

Enharmonic Spelling

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#)

Comparison to Tonal.js

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

Development

# Get dependencies
mix deps.get

# Run tests
mix test

# Run dialyzer for type checking
mix dialyzer

# Generate documentation
mix docs

Examples & Use Cases

Chord Progression Transposition

progression = ["C", "Am", "F", "G"]
transpose_to_d = Harmony.Transpose.transpose_by("2M")

progression |> Enum.map(transpose_to_d)
# => ["D", "Bm", "G", "A"]

Finding Scale Degrees

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
# ...

Circle of Fifths

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"]

Jazz Chord Analysis

# 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 chords

Contributing

Contributions are welcome! This library is being actively maintained and improved. Please feel free to:

License

This project is licensed under the MIT License.

Credits

Acknowledgments

Special thanks to the tonal.js community for creating such a comprehensive music theory library that inspired this Elixir port.