Skip to content

rpmessner/harmony

Repository files navigation

Harmony

CI Hex.pm Documentation License

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

  • 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

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:

  • 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 Enum and Stream
  • Type Safety - Comprehensive typespecs throughout the library

Performance

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

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:

  • Report bugs
  • Suggest new features
  • Submit pull requests
  • Improve documentation

License

This project is licensed under the MIT License.

Credits

  • Original tonal.js library by @danigb
  • Elixir port and adaptation by Ryan Messner

Acknowledgments

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

About

Music theory library inspired by tonal.js

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages