Skip to main content

Python toolkit for working with chord progressions

Project description

jchord - Python toolkit for working with chord progressions

Build Status codecov

>>> from jchord.progressions import ChordProgression
>>> prog = ChordProgression.from_string("C -- Fm7 -- C -- G7 -- C -- E7 Am F Bm7b5 E7 Am9 F Bo C69 --")
>>> print(prog.to_string())
C      --     Fm7    --
C      --     G7     --
C      --     E7     Am
F      Bm7b5  E7     Am9
F      Bo     C69    --
>>> prog.to_midi("example.midi", tempo=100, beats_per_chord=2, instrument=4)
  • Chord progressions can be imported from:
    • string (prog = ChordProgression.from_string(string))
    • text file (prog = ChordProgression.from_txt(filename))
    • XLSX file (prog = ChordProgression.from_xlsx(filename))
  • Chord progressions can be exported to:
    • string (print(prog.to_string(...optional)))
    • text file (prog.to_txt(filename, ..optional))
    • XLSX file (prog.to_xlsx(filename, ...optional))
    • MIDI file (prog.to_midi(filename, ...optional))

Install

  • git clone it, then pip install -e .
  • Install the right requirements:
    • All functionality: pip install -r requirements_full.txt
    • No XLSX or MIDI conversion: pip install -r requirements_minimal.txt
    • Package development: pip install -r requirements_dev.txt

Development

Testing

To run the tests:

pytest --cov-report term-missing --cov jchord -vvv

Make sure all tests pass and that the code has 100% test coverage.

Formatting

The code is formatted with black. In the root of the repo, run:

black .

Documenting

To generate documentation, run the documentation generation script:

python doc_gen/generate_doc.py -o README.md

The script appends documentation from each source module to doc_gen/index.md. If the script ran successfully, README.md should contain a section called "Documentation" below.

To decide which parts should be present in the documentation, go and edit doc_gen/pydocmd.yml. The reason for the format is that I originally wanted to use pydocmd (but found it too limiting).


Documentation

Table of contents

  • jchord.core
    • Class Note
      • Method Note.__init__(self, name: str, octave: int)
      • Method Note.pitch(self) -> float
      • Method Note.transpose(self: 'Note', shift: int) -> 'Note'
      • Method Note.transpose_degree(self: 'Note', shift: str) -> 'Note'
    • Class CompositeObject
      • Method CompositeObject._keys(self) -> Hashable
    • Function split_to_base_and_shift(name_or_degree: str, name_before_accidental: bool) -> (<class 'str'>, <class 'int'>)
    • Function degree_to_semitone(degree: str) -> int
    • Function semitone_to_degree_options(semitone: int, max_accidentals: int = 1) -> List[str]
    • Function note_diff(name_low: str, name_high: str) -> int
    • Exception InvalidDegree
  • jchord.midi
    • Class PlayedNote
      • Method PlayedNote.time(self)
      • Method PlayedNote.note(self)
      • Method PlayedNote.duration(self)
    • Function note_to_midi(note: jchord.core.Note) -> int
    • Function midi_to_note(midi: int) -> jchord.core.Note
    • Function midi_to_pitch(midi: int) -> float
    • Function read_midi_file(filename: str) -> List[jchord.midi.PlayedNote]
    • Function group_notes_to_chords(notes: List[jchord.midi.PlayedNote]) -> Dict[float, jchord.midi.PlayedNote]
    • Exception InvalidNote
  • jchord.knowledge
  • jchord.chords
    • Class Chord
      • Method Chord.__init__(self, name: str, semitones: List[int])
      • Method Chord.from_semitones(name: Union[str, NoneType], semitones: List[int]) -> 'Chord'
      • Method Chord.from_degrees(name: str, degrees: List[str]) -> 'Chord'
      • Method Chord.from_name(name: str) -> 'Chord'
      • Method Chord.intervals(self) -> List[int]
      • Method Chord.with_root(self, root: jchord.core.Note) -> 'ChordWithRoot'
      • Method Chord.add_semitone(self, semitone: int)
    • Class ChordWithRoot
      • Method ChordWithRoot.__init__(self, name: str, root: jchord.core.Note, chord: jchord.chords.Chord)
      • Method ChordWithRoot.from_root_and_semitones(root: jchord.core.Note, semitones: List[int]) -> 'ChordWithRoot'
      • Method ChordWithRoot.from_midi(midi: Set[int]) -> 'ChordWithRoot'
      • Method ChordWithRoot.from_name(name: str, octave: int = 4) -> 'ChordWithRoot'
      • Method ChordWithRoot.semitones(self)
      • Method ChordWithRoot.intervals(self) -> List[int]
      • Method ChordWithRoot.midi(self) -> List[int]
      • Method ChordWithRoot.transpose(self, shift: int) -> 'ChordWithRoot'
    • Function semitones_to_chord_name_options(semitones: Set[int], _rec=5) -> List[str]
    • Exception InvalidChord
  • jchord.progressions
    • Class ChordProgression
      • Method ChordProgression.__init__(self, progression: List[jchord.chords.ChordWithRoot])
      • Method ChordProgression.from_string(string: str) -> 'ChordProgression'
      • Method ChordProgression.from_txt(filename: str) -> 'ChordProgression'
      • Method ChordProgression.from_xlsx(filename: str) -> 'ChordProgression'
      • Method ChordProgression.from_midi_file(filename: str) -> 'ChordProgression'
      • Method ChordProgression.chords(self) -> Set[jchord.chords.ChordWithRoot]
      • Method ChordProgression.midi(self) -> List[List[int]]
      • Method ChordProgression.to_string(self, chords_per_row: int = 4, column_spacing: int = 2, newline: str = '\n') -> str
      • Method ChordProgression.to_txt(self, filename: str, chords_per_row: int = 4, column_spacing: int = 2, newline: str = '\n')
      • Method ChordProgression.to_xlsx(self, filename: str, chords_per_row: int = 4)
      • Method ChordProgression.to_midi(self, filename: str, instrument: int = 1, tempo: int = 120, beats_per_chord: int = 2, velocity: int = 100)
    • Class SongSection
      • Method SongSection.name(self)
      • Method SongSection.progression(self)
    • Class Song
      • Method Song.__init__(self, sections: List[jchord.progressions.SongSection])
      • Method Song.to_string(self, chords_per_row: int = 4, column_spacing: int = 2, newline: str = '\n')
    • Exception InvalidProgression

jchord.core

Basic utilities for working with notes, base classes for objects etc.

Classes

Note

Represents an absolute note with a name and an octave.

Two Notes are equal if they have the same name and octave, or if they have the same octave and their names are enharmonic (so Note('A#', 4) == Note('Gb', 4))

Methods
__init__(self, name: str, octave: int)
pitch(self) -> float

Returns the absolute pitch of the note in Hz.

Examples (equalities are approximate):

  • Note("A", 4).pitch() == 440.0
  • Note("A", 0).pitch() == 27.5
  • Note("C", 4).pitch() == 261.62556
transpose(self: 'Note', shift: int) -> 'Note'

Transposes the note by the given number of semitones.

Examples:

  • Note("C", 0).transpose(1) == Note("C#", 0)
  • Note("C", 4).transpose(19) == Note("G", 5)
transpose_degree(self: 'Note', shift: str) -> 'Note'

Transposes the given note by the given scale degree

Examples:

  • Note("C", 0).transpose_degree("b2") == Note("C#", 0)
  • Note("C", 4).transpose_degree("12") == Note("G", 5)

CompositeObject

Base class for objects which can be represented by a few attributes.

Subclasses must implement a _keys() function which returns a hashable representation of these attributes (e.g. returns them as a tuple).

A CompositeObject is considered equal to another if the return value of their _keys() function is equal.

Iterating over a CompositeObject is like iterating over the return value of its _keys() function.

Methods
_keys(self) -> Hashable

Returns a hashable representation of the attributes that the object wraps.


Functions

split_to_base_and_shift(name_or_degree: str, name_before_accidental: bool) -> (<class 'str'>, <class 'int'>)

Takes a string representation of a note name or a degree. Returns a tuple where the first element is the string representation of the degree with accidentals removed, and the second element is the number of semitones needed to account fo accidentals.

Examples:

  • split_to_base_and_shift("A#", name_before_accidental=True) == ("A", 1)
  • split_to_base_and_shift("Ab", name_before_accidental=True) == ("A", -1)
  • split_to_base_and_shift("A", name_before_accidental=True) == ("A", 0)
  • split_to_base_and_shift("Abbbb", name_before_accidental=True) == ("A", -4)
  • split_to_base_and_shift("b9", name_before_accidental=False) == ("9", -1)
  • split_to_base_and_shift("#9", name_before_accidental=False) == ("9", 1)

degree_to_semitone(degree: str) -> int

Converts a string representation of a scale degree to the number of semitones between the root and that scale degree.

Examples:

  • degree_to_semitone("b9") == 13
  • degree_to_semitone("5") == 7

semitone_to_degree_options(semitone: int, max_accidentals: int = 1) -> List[str]

Converts the number of semitones between the root and the wanted scale degree to a list of possible names for that scale degree.

The list of options is sorted by "reasonableness":

  • If option A has fewer accidentals than option B, option A comes first.
  • If option A has the same number of accidentals as option B, then the option with a "b" comes before the option with a "#".

Examples:

  • semitone_to_degree_options(semitone=3, max_accidentals=1) = ["b3", "#2"]
  • semitone_to_degree_options(semitone=3, max_accidentals=0) = []
  • semitone_to_degree_options(semitone=17, max_accidentals=1) = ["11", "#10"]

note_diff(name_low: str, name_high: str) -> int

Returns the number of semitones between the first note and the second note. The first note is assumed to be the lower of the two notes.

Examples:

  • note_diff("G", "A") == 2
  • note_diff("A", "Bb") == 1
  • note_diff("A", "G") == 10
  • note_diff("A", "A") == 0

Exceptions

InvalidDegree

Raised if the string one attempts to interpret is not a valid scale degree


jchord.midi

Tools for working with MIDI.

Classes

PlayedNote

namedtuple which represents a (MIDI) note played at a given time for a given duration.

Methods
time(self)

Alias for field number 0

note(self)

Alias for field number 1

duration(self)

Alias for field number 2


Functions

note_to_midi(note: jchord.core.Note) -> int

Returns the midi value corresponding to the given Note.

midi_to_note(midi: int) -> jchord.core.Note

Returns the Note corresponding to the given MIDI note value.

midi_to_pitch(midi: int) -> float

Returns the absolute pitch in Hz for the given MIDI note value.

read_midi_file(filename: str) -> List[jchord.midi.PlayedNote]

Reads the MIDI file for the given filename and returns the corresponding list of PlayedNotes.

group_notes_to_chords(notes: List[jchord.midi.PlayedNote]) -> Dict[float, jchord.midi.PlayedNote]

Groups the list of PlayedNotes by time.

The return value maps time to a list of PlayedNotes for that time.

There is no attempt at quantization at this time, so the notes must be played at the exact same time to be grouped together.

Exceptions

InvalidNote

Raised when trying to get the MIDI value of a note that doesn't seem valid.


jchord.knowledge

Contains various literals that are used for computations throughout the library.

  • REPETITION_SYMBOL (str): The symbol that's used to indicate repetition in textual representations of chord progressions.
  • MAJOR_SCALE_OFFSETS (Dict[int, int]): Maps the scale degrees of the major scale to number of semitones from the root.
  • ACCIDENTALS (Set[str]): The set of accidentals.
  • MAJOR_FROM_C (List[str]): A list of the 7 note names in the C major scale.
  • CHROMATIC (List[str]): A list of the 12 chromatic notes starting from C. Only sharp notes are included here.
  • ENHARMONIC (List[Tuple[str, str]])): A list of the 5 pairs of enharmonic note names.
  • CHORD_NAMES (Dict[str, List[str]]): Maps chord names to the list of scale degrees in the chord, not including the root.
  • CHORD_ALIASES: (Dict[str, str]): Maps alternative chord names to the canonical chord name in CHORD_NAMES.
  • DYADS: (Dict[int, str]): A collection of two-note chords with names. Maps the number of semitones between the root and the other note to the chord name.
  • TRIADS_WITH_FIFTH (Dict[int, str]): A collection of three-note chords with names. All of these triads include a fifth. Maps the semitone that is not the root or the fifth to the chord name.

jchord.chords

Tools for working with chords.

Classes

Chord

Represents a chord quality (no root).

Methods
__init__(self, name: str, semitones: List[int])
from_semitones(name: Union[str, NoneType], semitones: List[int]) -> 'Chord'

Creates a Chord from a list of semitones from the root.

If name is None, semitones_to_chord_name_options is used to guess a good name for the chord.

from_degrees(name: str, degrees: List[str]) -> 'Chord'

Creates a Chord from a list of scale degrees.

If name is None, semitones_to_chord_name_options is used to guess a good name for the chord.

from_name(name: str) -> 'Chord'

Creates a Chord from a name. The CHORD_NAMES and CHORD_ALIASES dictionaries in jchord.knowledge are used to find the semitones in the chord.

Raises InvalidChord if no chord is found.

intervals(self) -> List[int]

Returns the list of internal intervals in the chord.

Examples:

  • Chord.from_name("minor").intervals() == [3, 4]
  • Chord.from_name("major7").intervals() == [4, 3, 4]
with_root(self, root: jchord.core.Note) -> 'ChordWithRoot'

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

add_semitone(self, semitone: int)

Adds the given semitone (as a difference from the root degree) to the chord.

The name of the chord does not get re-calculated, so use with care.


ChordWithRoot

Represents a chord with a chord quality and a root note.

Methods
__init__(self, name: str, root: jchord.core.Note, chord: jchord.chords.Chord)
from_root_and_semitones(root: jchord.core.Note, semitones: List[int]) -> 'ChordWithRoot'

Creates a ChordWithRoot from a root Note and a list of semitones from the root.

semitones_to_chord_name_options is used to guess a good name for the chord.

from_midi(midi: Set[int]) -> 'ChordWithRoot'

Creates a ChordWithRoot from a set of MIDI note values.

semitones_to_chord_name_options is used to guess a good name for the chord.

from_name(name: str, octave: int = 4) -> 'ChordWithRoot'

Creates a ChordWithRoot from a name.

semitones_to_chord_name_options is used to guess a good name for the chord.

semitones(self)

Returns the semitones in the chord.

intervals(self) -> List[int]

Returns the semitones in the chord.

midi(self) -> List[int]

Returns the list of MIDI note values in the chord.

transpose(self, shift: int) -> 'ChordWithRoot'

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


Functions

semitones_to_chord_name_options(semitones: Set[int], _rec=5) -> List[str]

Returns a set of chord names corresponding to the given set of semitones.

The function tries to put the most reasonable chord names first and the more dubious ones last.

The _rec argument is for internal use only; in some cases, it prevents infinite recursion while computing the chord name.

Exceptions

InvalidChord

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


jchord.progressions

Tools for working with chord progressions.

Classes

ChordProgression

Represents a chord progression.

Methods
__init__(self, progression: List[jchord.chords.ChordWithRoot])
from_string(string: str) -> 'ChordProgression'

Creates a ChordProgression from its string representation.

from_txt(filename: str) -> 'ChordProgression'

Creates a ChordProgression from a text file with its string representation.

from_xlsx(filename: str) -> 'ChordProgression'

Creates a ChordProgression from an Excel file.

from_midi_file(filename: str) -> 'ChordProgression'

Creates a ChordProgression from an MIDI file.

There is no attempt at quantization at this time, so the notes must be played at the exact same time to be grouped together as chords.

chords(self) -> Set[jchord.chords.ChordWithRoot]

Returns the set of chords in the progression.

midi(self) -> List[List[int]]

Returns the MIDI values for each chord in the progression.

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

Returns the string representation of the chord progression.

to_txt(self, 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(self, filename: str, chords_per_row: int = 4)

Saves the chord progression to an Excel file.

to_midi(self, filename: str, instrument: int = 1, tempo: int = 120, beats_per_chord: int = 2, velocity: int = 100)

Saves the chord progression to a MIDI file.


SongSection

Represents a section in a Song.

Methods
name(self)

Alias for field number 0

progression(self)

Alias for field number 1


Song

Represents a song (a series of sections).

Methods
__init__(self, sections: List[jchord.progressions.SongSection])
to_string(self, chords_per_row: int = 4, column_spacing: int = 2, newline: str = '\n')

Returns the string representation of the song.


Exceptions

InvalidProgression

Raised when encountering what seems like an invalid chord progression.


Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jchord-0.1.4.tar.gz (26.0 kB view hashes)

Uploaded Source

Built Distribution

jchord-0.1.4-py3-none-any.whl (19.8 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page