Musical harmony library for Lua.
- Chord Object. Supports a wide range of chords, from simple major/minor to complex jazz chords. Provides a flexible string parsing, can identify a chord based on its notes. Can transpose chords and retrieve individual notes.
- Note Object. Handles alterations (sharps, flats, double accidentals), octaves, pitch classes.
- Interval Object. Supports simple and compound intervals. Can identify the interval between two notes and represent it in semitones.
The easiest way to install Modest is via LuaRocks.
luarocks install modest-harmony
If you want to avoid LuaRocks, you should consider two things about the project. First, Modest depends on LPeg, a library partially written in C, which you will need to install separately. Second, the project is written in Fennel, a language that transpiles to Lua. To use Modest in your Lua code, you can either embed Fennel compiler in your project or perform ahead-of-time compilation of the Fennel files. I recommend the latter option. To do so, install Fennel and run the following command from the project's root directory:
fennel --require-as-include --skip-include re,lpeg --compile modest/init.fnl > modest.lua
After running the command, move the resulting modest.lua file into your project and require it as you would any other Lua module.
- The library supports both Lua 5.4 and LuaJIT. It should also be compatible with older Lua 5.x versions.
-
The library does note register any global values. Every example below assumes you have properly required it like this:
local modest = require 'modest' local Chord, Interval, Note = modest.Chord, modest.Interval, modest.Note
- Methods in the library do not mutate objects; instead, they return new instances. However, as these instances are regular Lua tables, they can still be modified after creation. It is strongly advised not to mutate them, as this could lead to unexpected behavior.
-
Each object provides a 'fromstring' method, allowing object construction through string parsing. While Interval and Note requires strings in a strict format, Chord can parse almost any notation that may be encountered in musical scores or chord charts.
-
Any method that requires one of the library objects as an argument can also accept a string, which will be parsed using the appropriate 'fromstring' method. For example, both of the following expressions are valid.
note = Note.fromstring("C5") note:transpose("P5") note:transpose(Interval.fromstring("P5"))
- The library supports two types of accidental representation: special Unicode symbols ('♯' for sharp, '♭' for flat, '𝄪' for double sharp, '𝄫' for double flat) and ASCII characters ('#', 'b', 'x', 'bb', respectively).
- The parsers of the Note and Chord objects can handle both types. When transforming these objects into strings, different methods are available for each representation (see below).
- Each object implements '__tostring' metamethod. In Lua, this metamethod is automatically called when an object needs to be represented as a string, such as during calls to 'print' or 'tostring' functions. The implementation uses Unicode symbols for accidentals.
- Modest is named after Modest Petrovich Mussorgsky.
- Parses a string and returns a Chord object. Supports most of the chord types (see table below). Aims to be as flexible as possible when parsing a chord suffix, allowing various synonymous notations.
Supported chord type | Examples |
---|---|
Basic triads | C, Cm |
Augmented chords | Caug |
Diminished and half-diminished | Cdim, C⌀7, Cdim7 |
Suspended chords | Csus2, C9sus4 |
Seventh chords | C7, CM7, CminMaj7 |
Extended chords up to the 13th | C9, C13 |
Added sixth and 6/9 chords | C6, Cm(♭6), C6/9 |
Added tones | Cadd2, Cadd9, C(♯11) |
Altered chords | C7♯5, C7♯5♭9 |
Power chords | C5 |
Slash chords | C/G |
-
Example:
CM7 = Chord.fromstring("Cmaj7")
-
Identifies a chord based on the given notes. Accepts a variable number of string representations or Note objects. Assumes the first argument for a chord root. If the octaves of the given notes are not specified, assumes they go in ascending order. Supports the same types of chords as the 'fromstring' method, except for slash chords. Does not support inversions. Raises an error if the notes do not form a recognizable chord.
-
Examples:
Cadd9 = Chord.identify("C", "E", "G", "D") -- Can also accept note objects Daug = Chord.identify("D", "F#", Note.fromstring("A#"))
-
Returns a new Chord transposed by the given interval.
-
Example:
Eb6_9 = Chord.fromstring("C6/9"):transpose("m3")
-
Similar to transpose, returns a chord transposed down by the given interval.
-
Example:
Db9 = Chord.fromstring("Ab9"):transpose_down("P5")
-
Returns the notes that make up the chord. Optionally, specify the octave of the root note.
-
Example:
notes = Chord.fromstring("F#"):notes(4) for _, note in ipairs(notes) do print(note) end
F♯4 A♯4 C♯5
-
Converts the chord into a numeric representation, with each note represented as the number of semitones from the C of the chord's root octave.
-
Examples:
numeric = Chord.fromstring("C/Bb"):numeric() print(table.concat(numeric, ", "))
-2, 0, 4, 7
numeric = Chord.fromstring("G9"):numeric() print(table.concat(numeric, ", "))
7, 11, 14, 17, 21
-
Converts the chord into a string. Accidentals will be represented with special Unicode characters.
-
Example:
chord = Chord.fromstring("C#maj7") assert(chord:tostring() == "C♯M7")
-
Returns the chord as a string with ASCII representations for accidentals.
-
Example:
chord = Chord.fromstring("G7#11") assert(chord:toascii() == "G7(#11)")
-
Parses a string and returns an Interval object. Examples:
- "m3" = minor third
- "P4" = perfect fourth
- "A5" = augmented fifth
- "d7" = diminished seventh
- "M6" = major sixth.
-
Example:
P4 = Interval.fromstring("P4")
-
Creates a new Interval object. Size should be an integer, and quality should be a string (valid options are "dim", "aug", "min", "maj", "perfect"). The method raises an error if the interval is invalid.
-
Examples:
A3 = Interval.new(3, "aug") M13 = Interval.new(13, "maj") P5 = Interval.new(5)
_, err = pcall(function() Interval.new(5, "min") end) print(err)
./modest.lua:287: Invalid combination of size and quality
-
Identifies the interval between two notes.
-
Example:
P4 = Interval.identify("C", "F")
-
Returns the number of semitones in the interval.
-
Examples:
semitones = Interval.fromstring("M3"):semitones() assert(semitones == 4)
-
Converts the interval into a string representation.
-
Example:
m6 = Interval.new(6, "min"):tostring()
-
Parses a string and returns a Note object.
-
Examples:
C_sharp_4 = Note.fromstring("C#4") E = Note.fromstring("E") -- the octave is optional
-
Creates a new Note object. The tone should be a capital letter (e.g., "C"). The accidental should be a numeric value (e.g., -1 for flat, 1 for sharp). The octave is optional.
-
Examples:
D_sharp_5 = Note.new("D", 1, 5) B_double_flat = Note.new("B", -2)
-
Returns a new note transposed by the given interval.
-
Example:
F4 = Note.fromstring("C4"):transpose("P4")
-
Returns a new note transposed down by the given interval.
-
Example:
A3 = Note.fromstring("C4"):transpose_down("m3")
-
Returns a number from 0 to 11 representing the pitch class of the note (e.g., C=0, C♯/D♭=1, …, B=11).
-
Example:
pitch_class = Note.fromstring("G"):pitch_class() assert(pitch_class == 7)
-
Works similarly to the Chord methods of the same name.
-
Example:
note = Note.fromstring("D#4") assert(note:tostring() == "D♯4") assert(note:toascii() == "D#4")