jchord - toolkit for working with chord progressions

https://github.com/jonathangjertsen/jchord/actions/workflows/build.yml/badge.svg https://codecov.io/gh/jonathangjertsen/jchord/branch/master/graph/badge.svg

What’s this then?

jchord is a Python package which provides tools for working with chord progressions. jchord:

  • has object representations for notes, chords, and progressions (in the Western 12-tone system)

  • knows about naming conventions for chords, and can convert back and forth between objects and names

  • can be used as a converter between strings, text files, XLSX files, PDFs and MIDI files (see “converter script” below)

Get it

Basic installation:

pip install jchord

Installation with dependencies for reading and writing MIDI/XLSX/PDF files:

pip install jchord[midi,xlsx,pdf]

Convert between formats

If you just want the converter functionality, invoke jchord on the command line:

usage: jchord [-h] [--midi MIDI] [--pdf PDF] file_in file_out

Converts between different representations of the same format

positional arguments:
  file_in      Input progression as string, .txt, .xlsx or .midi
  file_out     Output file as .txt, .xlsx, .midi or .pdf

optional arguments:
  -h, --help   show this help message and exit
  --midi MIDI  comma separated list of arguments for midi, e.g. tempo=8,beats_per_chord=2
  --pdf PDF    comma separated list of arguments for pdf, e.g. chords_per_row=8,fontsize=30

Example:

jchord "Cm A E7 F#m7" example.mid --midi tempo=80,beats_per_chord=1

As a library

Here is an example that parses a chord progression written as a string, transposes it upwards by 2 semitones, converts it back to a string and then creates a midi file from it.:

from jchord import ChordProgression, MidiConversionSettings
prog = ChordProgression.from_string("C -- Fm7 -- C -- G7 -- C -- E7 Am F Bm7b5 E7 Am9 F Bo C69 --")
prog = prog.transpose(+2)
print(prog.to_string())
prog.to_midi(MidiConversionSettings(filename="example.midi", tempo=100, beats_per_chord=2, instrument=4))

Output:

D       --      Gm7     --
D       --      A7      --
D       --      F#7     Bm
G       C#m7b5  F#7     Bm9
G       C#o     D69     --

For more examples, see the documentation.

Documentation

Documentation lives here: jonathangjertsen.github.io/jchord/

Contributing

To contribute, open an issue or create a Pull Request in the Github repo.

Examples

The following examples make use of the MIDI subsystem in jchord, which uses the mido package. Installing with pip install jchord[midi] will ensure this package is installed.

Example: Autumn Leaves in two styles

This example generates two MIDI files: one with fancy synth arpeggios, another with a basic strumming guitar.

from jchord.progressions import ChordProgression, MidiConversionSettings
from jchord.midi import Instrument
from jchord.midi_effects import (
    Chain,
    Doubler,
    Arpeggiator,
    Spreader,
    Transposer,
)

progression = ChordProgression.from_string(
    """
Dm    --  G          -- C      Cadd9   Fmaj7  --
Dm    --  E          -- Am     --      Am9    --
Dm7   --  G7         -- C      C7      Fmaj7  Fmaj7add6
Dm9   --  E7         -- Am     --      Amadd6 --
Esus4 E7  Am         --
Dm7   --  G          -- Cmaj9  Cmaj9/G Fmaj7  --
Dm    --  E          -- Amadd9 Am/G    Am/F#  Fmaj7
E7b9  --  Amadd6add9 -- --     --      Am     --
"""
)

progression.to_midi(
    MidiConversionSettings(
        filename="autumn_leaves_arpeggiated.midi",
        tempo=110,
        beats_per_chord=2,
        instrument=Instrument.VoiceOohs,
        effect=Chain(
            Doubler(12),
            Arpeggiator(
                rate=1 / 16,
                pattern=[
                    (0, 2),  1,      2,   (1, 3),
                    2,       (3, 4), 2,   3,
                    (4, 1),  3,      4,   5,
                    (-2, 0), -3,    -4,  -5,
                ],
                sticky=True,
            )
        ),
    )
)

progression.to_midi(
    MidiConversionSettings(
        filename="autumn_leaves_strummed.midi",
        tempo=180,
        beats_per_chord=2,
        instrument=Instrument.AcousticGuitarSteel,
        effect=Chain(Transposer(-12), Spreader(amount=30, jitter=6)),
        velocity=50,
    )
)

Example: Parse MIDI

jchord can parse MIDI files that are a sequence of block chords. It uses a kernel density estimation algorithm to group notes into chords, allowing it to handle slight imperfections. If the file has arpeggios, melodies or other flourishes, it will not work.

from pathlib import Path
from jchord import ChordProgression

prog = ChordProgression.from_midi_file(Path(__file__).parent.parent / "test" / "test_data" / "issue_8.mid")
print(prog.to_string())

Example: Harmonization

This example shows the Harmonizer effect, building 7th chords from single notes. Use the n suffix to indicate a single note.

from jchord.midi import Instrument
from jchord.midi_effects import Harmonizer

progression = ChordProgression.from_string("""Fn Gn En An""")

progression.to_midi(
    MidiConversionSettings(
        filename="harmonizer.midi",
        tempo=85,
        beats_per_chord=2,
        instrument=Instrument.VoiceOohs,
        effect=Harmonizer(scale=[0, 2, 4, 5, 7, 9, 11], degrees=[1, 3, 5, 7], root="C"),
    )
)

API reference

Objects at a glance

These are the key objects defined by jchord:

  • Note

  • Intervals

  • Chord

  • ChordProgression

  • Song

The highest-level object is Song. A Song has a sections property, which is a list of ChordProgression objects. A ChordProgression has a progression property, which is a list of Chord objects. A Chord object has a chord property, which is a Intervals object; and a root property, which is a Note object.

Working with individual notes: Note

A Note represents one of the musical notes in the Western 12-tone system. It is identified by its pitch class and octave according to Scientific pitch notation; so Note('C', 4) is Middle C.

class jchord.Note(name: str, octave: int)

Represents an absolute note with a name and an octave. To create a Note, provide the name as a string and the octave as an integer:

>>> Note(name="A", octave=3)
Note('A', 3)

Two instances of Note are equal if they have the same name and octave, or if they have the same octave and their names are enharmonic:

>>> Note('G#', 4) == Note('Ab', 4)
True
>>> Note('G#', 4) == Note('Ab', 3)
False

You can subtract two Note instances to get the number of semitones between them:

>>> Note('G#', 4) - Note('Ab', 3)
12
>>> Note('Ab', 3) - Note('G#', 4)
-12

Note is hashable, so instances can be used as dictionary keys or members of sets.

pitch() float

Returns the absolute pitch of the note in Hz.

>>> Note("A", 4).pitch()
440.0
>>> Note("A", 0).pitch()
27.5
>>> Note("C", 4).pitch()
261.6255653005986
transpose(shift: int) Note

Transposes the note by the given number of semitones.

>>> Note("C", 0).transpose(1)
Note('C#', 0)
>>> Note("C", 4).transpose(19)
Note('G', 5)
transpose_degree(shift: str, down: bool = False) Note

Transposes the given note by the given scale degree.

>>> Note("C", 0).transpose_degree("b2")
Note('C#', 0)
>>> Note("C", 4).transpose_degree("5")
Note('G', 4)
>>> Note("C", 4).transpose_degree("5", down=True)
Note('F', 3)

Working with individual chords: Intervals and Chord

The meaning of a chord is slightly ambiguous, and jchord has two classes to disambiguate between the case where we refer to a chord with a root (say, “Am7” or “C#maj7#11”), and the case where we refer to the interval structure without reference to any particular root note (say, “maj7” or “min11”):

  • Chord refers to a root note AND the interval structure.

  • Intervals refers to just the interval structure.

class jchord.Intervals(semitones: List[int], name: str, implicit_zero=True, source_chord=None)

Represents an interval structure or chord quality, which can be thought of as a chord without reference to any particular root note. Examples of interval structures are major, minor, dominant, major 7, etc.

There are several ways to construct Intervals. You can either specify the name or the interval structure of the chord, and the other will be inferred.

To specify the name and infer the semitones, use from_name:

>>> Intervals.from_name("maj7")
Intervals(name='maj7', semitones=[0, 4, 7, 11])
>>> Intervals.from_name("7sus4#13")
Intervals(name='7sus4#13', semitones=[0, 5, 7, 10, 14, 17, 22])

If nothing can be generated from the name, an InvalidChord exception is raised.

To specify the semitones and infer the name, use from_semitones or from_degrees.

>>> Intervals.from_semitones([0, 3, 7, 11])
Intervals(name='minmaj7', semitones=[0, 3, 7, 11])
>>> Intervals.from_semitones([3, 7, 11]) # the 0 is optional
Intervals(name='minmaj7', semitones=[0, 3, 7, 11])
>>> Intervals.from_semitones([3, 7, 11], name='mMaj7') # you can override the name
Intervals(name='mMaj7', semitones=[0, 3, 7, 11])
>>> Intervals.from_semitones([1, 2, 3, 4]) # no common name
Intervals(name='<unknown>', semitones=[0, 1, 2, 3, 4])
add_semitone(semitone: int, recalculate_name: bool = True) Intervals

Returns a new Chord where the given semitone (as a difference from the root degree) has been added. The name will be recalculated unless the optional parameter recalculate_name is set to False.

>>> Intervals.from_name("m").add_semitone(10)
Intervals(name='min7', semitones=[0, 3, 7, 10])
interval_sequence() List[int]

Returns the list of internal intervals in the chord.

>>> Intervals.from_name("minor").interval_sequence()
[3, 4]
>>> Intervals.from_name("major7").interval_sequence()
[4, 3, 4]
remove_semitone(semitone: int, recalculate_name: bool = True) Intervals

Returns a new Chord where the given semitone (as a difference from the root degree) has been removed (if it was present). The name will be recalculated unless the optional parameter recalculate_name is set to False.

>>> Intervals.from_name("maj7").remove_semitone(11)
Intervals(name='', semitones=[0, 4, 7])
rotate_semitones(n: int, recalculate_name: bool = True) Intervals

Returns a new Intervals where the semitones have been rotated, n times. In other words, chord.rotate_semitones(1) is the 1st inversion of chord, chord.rotate_semitones(2) is the 2nd inversion, etc. The name will be recalculated unless the optional parameter recalculate_name is set to False.

>>> Intervals.from_name("maj7").rotate_semitones(2)
Intervals(name='maj7inv2', semitones=[7, 11, 12, 16], implicit_zero=False)
with_root(root: Note) Chord

Returns a Chord based on the chord and the provided root.

>>> Intervals.from_name("m").with_root(Note("A", 5))
Chord(name='Am', root=Note('A', 5), intervals=Intervals(name='m', semitones=[0, 3, 7]))
class jchord.Chord(name: str, root: Note, intervals: Intervals)

Represents a concrete chord with a root note and an interval structure.

There are several ways to construct a Chord. You can either specify the name or a root note + interval structure of the chord, and the other will be inferred.

To specify the name and infer the notes, use from_name:

>>> Chord.from_name("Amaj7")
Chord(name='Amaj7', root=Note('A', 4), intervals=Intervals(name='maj7', semitones=[0, 4, 7, 11]))
>>> Chord.from_name("B7sus4#13")
Chord(name='B7sus4#13', root=Note('B', 4), intervals=Intervals(name='7sus4#13', semitones=[0, 5, 7, 10, 14, 17, 22]))

Chord supports all the same names as Intervals, as well as slash chords.

>>> Chord.from_name("Amaj7/C")
Chord(name='Amaj7/C', root=Note('A', 4), intervals=Intervals(name='<unknown>', semitones=[-9, 0, 4, 7, 11]))

If no chord can be generated from the name, an InvalidChòrd exception is raised.

To specify the root and semitones and infer the name, use from_root_and_semitones.

>>> Chord.from_root_and_semitones(Note('A', 5), [0, 3, 7, 11])
Chord(name='Aminmaj7', root=Note('A', 5), intervals=Intervals(name='minmaj7', semitones=[0, 3, 7, 11]))

In addition to the methods above, you can specify a set of MIDI values:

>>> Chord.from_midi({ 77, 80, 84 })
Chord(name='Fmin', root=Note('F', 5), intervals=Intervals(name='min', semitones=[0, 3, 7]))
property bass: Note

Returns the lowest note in the chord.

Unless the chord is a slash chord, this is the same as chord.root.

>>> Chord.from_name("Am7").bass
Note('A', 4)
>>> Chord.from_name("Am7/B").bass
Note('B', 3)
interval_sequence() List[int]

Returns the semitones in the chord.

Returns the list of internal intervals in the chord.

>>> Chord.from_name("Aminor").interval_sequence()
[3, 4]
>>> Chord.from_name("Amajor7").interval_sequence()
[4, 3, 4]
midi() List[int]

Returns the list of MIDI note values in the chord.

>>> Chord.from_name("Amajor7").midi()
[69, 73, 76, 80]
property semitones: List[int]

Returns the semitones in the chord.

>>> Chord.from_name("Am7").semitones
[0, 3, 7, 10]
transpose(shift: int) Chord

Transposes the chord by the given shift (in semitones).

>>> Chord.from_name("Amajor7").transpose(-12).midi()
[57, 61, 64, 68]
class jchord.InvalidChord

Raised if trying to construct a chord from an invalid chord name.

Working with chord progressions: ChordProgression

A chord progression is represented as a list of chords, one after another.

class jchord.ChordProgression(progression: List[Chord])

Represents a chord progression.

There are many ways to create a ChordProgression object.

From a string

Use the from_string method to generate a chord progression from a string.

>>> ChordProgression.from_string("Dm7 -- Gm7 Am7")
ChordProgression([Chord(name='Dm7', root=Note('D', 4), intervals=Intervals(name='m7', semitones=[0, 3, 7, 10])), Chord(name='Dm7', root=Note('D', 4), intervals=Intervals(name='m7', semitones=[0, 3, 7, 10])), Chord(name='Gm7', root=Note('G', 4), intervals=Intervals(name='m7', semitones=[0, 3, 7, 10])), Chord(name='Am7', root=Note('A', 4), intervals=Intervals(name='m7', semitones=[0, 3, 7, 10]))])

From a text file

Use the from_txt method to generate a chord progression from a text file. If example.txt contains the text “Am7 D7”, then ChordProgression.from_txt("example.txt") will produce the same result as ChordProgression.from_string("Am7 D7").

From an Excel file

Use the from_xlsx method to generate a chord progression from an Excel spreadsheet. If example.xlsx contains the following cells:

C

D

Em7

G7

Then the result is equivalent to calling ChordProgression.from_string("C D Em7 G7").

Note

This feature requires openpyxl, which you can get with pip install openpyxl.

From a MIDI file

Use the from_midi method to generate a chord progression from a MIDI file. If example.mid contains some chords that you would like to convert to a ChordProgression, use ChordProgression.from_midi("example.mid"). For best results, the MIDI file should contain a single instrument with chords played as straight as possible.

Note

This feature requires mido, which you can get with pip install mido.

chords() Set[Chord]

Returns the set of chords in the progression.

>>> ChordProgression.from_string("Am7 D7").chords() 
{Chord(name='D7', root=Note('D', 4), intervals=Intervals(name='7', semitones=[0, 4, 7, 10])), Chord(name='Am7', root=Note('A', 4), intervals=Intervals(name='m7', semitones=[0, 3, 7, 10]))}
midi() List[List[int]]

Returns the MIDI values for each chord in the progression.

>>> ChordProgression.from_string("Am7 D7").midi()
[[69, 72, 76, 79], [62, 66, 69, 72]]
to_midi(settings: MidiConversionSettings, **kwargs)

Saves the chord progression to a MIDI file.

Note

This feature requires mido, which you can get with pip install mido.

to_string(chords_per_row: int = 4, column_spacing: int = 2, newline: str = '\n') str

Returns the string representation of the chord progression.

to_txt(filename: str, chords_per_row: int = 4, column_spacing: int = 2, newline: str = '\n')

Saves the string representation of the chord progression to a text file.

to_xlsx(filename: str, chords_per_row: int = 4)

Saves the chord progression to an Excel file.

Note

This feature requires openpyxl, which you can get with pip install openpyxl.

transpose(shift: int)

Transposes all chords in the progression by the given shift.

>>> ChordProgression.from_string("Am7 D7").transpose(2).to_string().strip()
'Bm7  E7'
class jchord.MidiConversionSettings(filename: str, instrument: int = 1, tempo: int = 120, beats_per_chord: Union[int, list] = 2, velocity: int = 100, repeat: str = 'replay', effect=None)

MIDI features

Tools for working with MIDI.

class jchord.midi_effects.MidiEffect(*args, **kwargs)

Base class for MIDI effects

apply(chord: List[MidiNote])

Returns a list where the effect has been applied to the given chord

set_settings(settings: MidiConversionSettings)

Makes the MIDI conversion settings available. This is always applied before apply() during the MIDI conversion