Repository: yuma-m/pychord Branch: main Commit: 4bd676c14c24 Files: 35 Total size: 79.0 KB Directory structure: gitextract_ch1k6q74/ ├── .github/ │ └── workflows/ │ ├── build.yml │ ├── codeql-analysis.yml │ └── deploy.yml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── README.md ├── docs/ │ ├── Makefile │ ├── conf.py │ ├── index.rst │ ├── pychord.rst │ └── requirements.txt ├── examples/ │ └── pychord-midi.py ├── pychord/ │ ├── __init__.py │ ├── analyzer.py │ ├── chord.py │ ├── constants/ │ │ ├── __init__.py │ │ ├── qualities.py │ │ └── scales.py │ ├── parser.py │ ├── progression.py │ ├── py.typed │ ├── quality.py │ └── utils.py ├── pyproject.toml ├── setup.cfg ├── setup.py └── test/ ├── __init__.py ├── test_analyzer.py ├── test_chord.py ├── test_component.py ├── test_progression.py ├── test_quality.py ├── test_transpose.py └── test_utils.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/build.yml ================================================ name: Test and lint on: push: branches: - main pull_request: jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel coverage[toml] - name: Run tests run: | coverage run -m unittest -v coverage report - name: Install linting tools run: | pip install black mypy - name: Run style check run: | black --check pychord test - name: Run type check run: | mypy pychord ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ name: CodeQL on: push: branches: - main pull_request: schedule: - cron: '0 2 * * 1' jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: language: - python steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 2 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - name: Autobuild uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 ================================================ FILE: .github/workflows/deploy.yml ================================================ name: Publish Python package on: push: tags: - 'v*' jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build twine - name: Build and publish env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: | python -m build twine upload dist/* ================================================ FILE: .gitignore ================================================ .idea/ venv/ .DS_Store # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ make.bat # PyBuilder target/ #Ipython Notebook .ipynb_checkpoints ================================================ FILE: .readthedocs.yaml ================================================ version: 2 build: os: ubuntu-lts-latest tools: python: "3.12" sphinx: configuration: docs/conf.py python: install: - requirements: docs/requirements.txt ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![PyChord](https://github.com/yuma-m/pychord/raw/main/pychord.png) # PyChord ![Build Status](https://github.com/yuma-m/pychord/actions/workflows/build.yml/badge.svg) [![Documentation Status](https://readthedocs.org/projects/pychord/badge/?version=latest)](http://pychord.readthedocs.io/en/latest/?badge=latest) ## Overview PyChord is a Python library to handle musical chords. ## Installation PyChord supports Python 3.10 and above. ```sh $ pip install pychord ``` ## Basic Usage ### Create a Chord ```python >>> from pychord import Chord >>> c = Chord("Am7") >>> c >>> c.info() """ Am7 root=A quality=m7 on=None """ ``` ### Transpose a Chord ```python >>> c = Chord("Am7/G") >>> c.transpose(3) >>> c ``` ### Get component notes ```python >>> c = Chord("Am7") >>> c.components() ['A', 'C', 'E', 'G'] >>> c.components_with_pitch(root_pitch=3) ['A3', 'C4', 'E4', 'G4'] ``` ### Compare Chords ```python >>> Chord("C") == Chord("D") False >>> Chord("C#") == Chord("Db") True >>> c = Chord("C") >>> c.transpose(2) >>> c == Chord("D") True ``` ### Find Chords from notes ```python >>> from pychord import find_chords_from_notes >>> find_chords_from_notes(["C", "E", "G"]) [ ] >>> find_chords_from_notes(["F#", "A", "C", "D"]) [ ] >>> find_chords_from_notes(["F", "G", "C"]) [ , ] ``` ### Create and handle chord progressions ```python >>> from pychord import ChordProgression >>> cp = ChordProgression(["C", "G/B", "Am"]) >>> cp >>> cp.append("Em/G") >>> cp >>> cp.transpose(+3) >>> cp >>> cp[1] ``` ## Advanced Usage ### Create a Chord from note index in a scale ```python >>> Chord.from_note_index(note=1, quality="", scale="Cmaj") # I of C major >>> Chord.from_note_index(note=3, quality="m7", scale="Fmaj") # IIIm7 of F major >>> Chord.from_note_index(note=5, quality="7", scale="Amin") # V7 of A minor ``` ### Overwrite the default Quality components with yours ```python >>> from pychord import Chord, QualityManager >>> Chord("C11").components() ['C', 'E', 'G', 'Bb', 'D', 'F'] >>> quality_manager = QualityManager() >>> quality_manager.set_quality("11", ("1", "3", "5", "b7", "11")) >>> Chord("C11").components() ['C', 'E', 'G', 'Bb', 'F'] ``` ### Inversions Chord inversions are created with a forward slash and a number indicating the order. This can optionally be combined with an additional forward slash to change the bass note: ```python >>> Chord("C/1").components() # First inversion of C ['E', 'G', 'C'] >>> Chord("C/2").components() # Second inversion of C ['G', 'C', 'E'] >>> Chord("Cm7/3/F").components() # Third inversion of Cm7 with an added F bass ['F', 'Bb', 'C', 'Eb', 'G'] ``` ## Examples - [pychord-midi.py](./examples/pychord-midi.py) - Create a MIDI file using PyChord and pretty_midi. ## Links - [PyPI](https://pypi.python.org/pypi/pychord) - [GitHub](https://github.com/yuma-m/pychord) - [Documentation](http://pychord.readthedocs.io/en/latest/) ## Author - [Yuma Mihira](https://yuma.cloud/) ## License - MIT License The logo is made from [Freepik](https://www.flaticon.com/authors/freepik). ================================================ FILE: docs/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_elements.papersize=a4 PAPEROPT_letter = -D latex_elements.papersize=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and an HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " epub3 to make an epub3" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " lualatexpdf to make LaTeX files and run them through lualatex" @echo " xelatexpdf to make LaTeX files and run them through xelatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pychord.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pychord.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/pychord" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pychord" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: lualatexpdf lualatexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through lualatex..." $(MAKE) PDFLATEX=lualatex -C $(BUILDDIR)/latex all-pdf @echo "lualatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: xelatexpdf xelatexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through xelatex..." $(MAKE) PDFLATEX=xelatex -C $(BUILDDIR)/latex all-pdf @echo "xelatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." .PHONY: dummy dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." ================================================ FILE: docs/conf.py ================================================ # -*- coding: utf-8 -*- # # pychord documentation build configuration file, created by # sphinx-quickstart on Sat Dec 31 14:51:42 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] # source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = u'pychord' copyright = u'2016 - 2026, Yuma Mihira' author = u'Yuma Mihira' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = u'' # The full version, including alpha/beta/rc tags. release = u'' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = 'pychorddoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'pychord.tex', u'pychord Documentation', u'Author', 'manual'), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'pychord', u'pychord Documentation', [author], 1) ] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'pychord', u'pychord Documentation', author, 'pychord', 'One line description of project.', 'Miscellaneous'), ] # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. epub_title = project epub_author = author epub_publisher = author epub_copyright = copyright # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] ================================================ FILE: docs/index.rst ================================================ .. pychord documentation master file, created by sphinx-quickstart on Sat Dec 31 14:51:42 2016. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to pychord's documentation! =================================== .. toctree:: :maxdepth: 4 :caption: Contents: pychord Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ================================================ FILE: docs/pychord.rst ================================================ pychord package =============== .. automodule:: pychord :members: ================================================ FILE: docs/requirements.txt ================================================ sphinx==9.1.0 sphinx_rtd_theme==3.1.0 ================================================ FILE: examples/pychord-midi.py ================================================ # An example to create MIDI file with PyChord and pretty_midi # Prerequisite: pip install pretty_midi # pretty_midi: https://github.com/craffel/pretty-midi import pretty_midi from pychord import Chord def create_midi(chords): midi_data = pretty_midi.PrettyMIDI() piano_program = pretty_midi.instrument_name_to_program('Acoustic Grand Piano') piano = pretty_midi.Instrument(program=piano_program) length = 1 for n, chord in enumerate(chords): for note_name in chord.components_with_pitch(root_pitch=4): note_number = pretty_midi.note_name_to_number(note_name) note = pretty_midi.Note(velocity=100, pitch=note_number, start=n * length, end=(n + 1) * length) piano.notes.append(note) midi_data.instruments.append(piano) midi_data.write('chord.mid') def main(): chords_str = ["C", "Dm7", "G", "C"] chords = [Chord(c) for c in chords_str] create_midi(chords) if __name__ == '__main__': main() ================================================ FILE: pychord/__init__.py ================================================ from .analyzer import find_chords_from_notes from .chord import Chord from .progression import ChordProgression from .quality import Quality, QualityManager __all__ = [ "find_chords_from_notes", "Chord", "ChordProgression", "Quality", "QualityManager", ] ================================================ FILE: pychord/analyzer.py ================================================ from .chord import Chord from .quality import QualityManager from .utils import note_to_val def find_chords_from_notes(notes: list[str]) -> list[Chord]: """ Find possible chords consisting of the given notes. :param notes: List of notes arranged from lower note, e.g. ``["C", "Eb", "G"]``. """ if not notes: raise ValueError("Please specify notes which consist a chord.") root = notes[0] root_and_positions = [] for rotated_notes in get_all_rotated_notes(notes): rotated_root = rotated_notes[0] root_and_positions.append( (rotated_root, notes_to_positions(rotated_notes, rotated_notes[0])) ) chords = [] for temp_root, positions in root_and_positions: quality = QualityManager().find_quality_from_components(positions) if quality is None: continue if temp_root == root: chord = "{}{}".format(root, quality) else: chord = "{}{}/{}".format(temp_root, quality, root) chords.append(Chord(chord)) return chords def notes_to_positions(notes: list[str], root: str) -> list[int]: """ Get notes positions from the root note. >>> notes_to_positions(["C", "E", "G"], "C") [0, 4, 7] :param notes: List of notes. :param root: Root note. """ root_pos = note_to_val(root) current_pos = root_pos positions = [] for note in notes: note_pos = note_to_val(note) if note_pos < current_pos: note_pos += 12 * ((current_pos - note_pos) // 12 + 1) positions.append(note_pos - root_pos) current_pos = note_pos return positions def get_all_rotated_notes(notes: list[str]) -> list[list[str]]: """ Get all rotated notes. get_all_rotated_notes([A,C,E]) -> [[A,C,E],[C,E,A],[E,A,C]] """ notes_list = [] for x in range(len(notes)): notes_list.append(notes[x:] + notes[:x]) return notes_list ================================================ FILE: pychord/chord.py ================================================ from typing import Any, Literal, overload from .constants.scales import RELATIVE_KEY_DICT from .parser import parse, parse_scale from .quality import QualityManager, Quality, scale_notes from .utils import augment, diminish, transpose_note, note_to_val class Chord: """ A chord, made up of two or more notes. :param chord: Name of the chord, e.g. ``"C"``, ``"Am7"``, ``"F#m7-5/A"``. """ def __init__(self, chord: str) -> None: root, quality, on = parse(chord) self._chord: str = chord self._root: str = root self._quality: Quality = quality self._on: str = on def __str__(self) -> str: return self._chord def __repr__(self) -> str: return f"" def __eq__(self, other: Any) -> bool: if not isinstance(other, Chord): raise TypeError(f"Cannot compare Chord object with {type(other)} object") if note_to_val(self._root) != note_to_val(other.root): return False if self._quality != other.quality: return False if ( # If one chord has an "on" and not the other, they differ. bool(self._on) != bool(other.on) ) or ( # If both chords have an "on" and they are not enharmonic, they differ. self._on and other.on and note_to_val(self._on) != note_to_val(other.on) ): return False return True @classmethod def from_note_index( cls, note: int, quality: str, scale: str, diatonic: bool = False, chromatic: int = 0, ) -> "Chord": """Create a :class:`Chord` from a note index in a scale. - ``Chord.from_note_index(1, "", "Cmaj")`` returns I of C major => Chord("C") - ``Chord.from_note_index(3, "m7", "Fmaj")`` returns IIImin of F major => Chord("Am7") - ``Chord.from_note_index(5, "7", "Amin")`` returns Vmin of A minor => Chord("E7") - ``Chord.from_note_index(2, "", "Cmaj")`` returns II of C major => Chord("D") - ``Chord.from_note_index(2, "m", "Cmaj")`` returns IImin of C major => Chord("Dm") - ``Chord.from_note_index(2, "", "Cmaj", diatonic=True)`` returns IImin of C major => Chord("Dm") - ``Chord.from_note_index(2, "", "Cmin", chromatic=-1)`` returns bII of C minor => Chord("Db") :param note: Scale degree of the chord's root, ``1`` to ``7``. :param quality: Quality of the chord, e.g. ``"m7"``, ``"sus4"``. :param scale: Base scale, e.g. ``"Cmaj"``, ``"Amin"``, ``"F#maj"``, ``"Ebmin"``. :param diatonic: If True, chord quality is determined using the base scale (overrides ``quality``). :param chromatic: Lower or raise the scale degree (and all notes of the chord) by semitone(s). """ if not 1 <= note <= 7: raise ValueError(f"Invalid note {note}") scale_root, scale_mode = parse_scale(scale) root = scale_notes(scale_root, scale_mode)[note - 1] if chromatic != 0: alter = augment if chromatic > 0 else diminish for i in range(abs(chromatic)): root = alter(root) if diatonic: scale_degrees = RELATIVE_KEY_DICT[scale_mode] # construct the chord based on scale degrees, within 1 octave first = scale_degrees[note - 1] third = scale_degrees[(note + 1) % 7] fifth = scale_degrees[(note + 3) % 7] seventh = scale_degrees[(note + 5) % 7] # adjust the chord to its root position (as a stack of thirds), # then set the root to 0 # e.g. (9, 0, 4) -> [0, 3, 7] def get_diatonic_chord(chord: tuple[int, ...]) -> list[int]: uninverted: list[int] = [] for note in chord: if not uninverted: uninverted.append(note) elif note > uninverted[-1]: uninverted.append(note) else: uninverted.append(note + 12) uninverted = [x - uninverted[0] for x in uninverted] return uninverted if quality in ["", "-", "maj", "m", "min"]: triad = (first, third, fifth) q = get_diatonic_chord(triad) elif quality in ["7", "M7", "maj7", "m7"]: seventh_chord = (first, third, fifth, seventh) q = get_diatonic_chord(seventh_chord) else: raise NotImplementedError( "Only generic chords (triads, sevenths) are supported" ) # look up QualityManager to determine chord quality quality_manager = QualityManager() quality_instance = quality_manager.find_quality_from_components(q) assert quality_instance is not None quality = quality_instance.quality return cls(f"{root}{quality}") @property def chord(self) -> str: """ The name of the chord, e.g. ``"C"``, ``"Am7"``, ``"F#m7-5/A"``. """ return self._chord @property def root(self) -> str: """ The root note of the chord, e.g. ``"C"``, ``"A"``, ``"F#"``. """ return self._root @property def quality(self) -> Quality: """ The quality of the chord, e.g. ``"maj"``, ``"m7"``, ``"m7-5"``. """ return self._quality @property def on(self) -> str: """ The bass note of a slash chord. """ return self._on def info(self) -> str: """ Return information of chord to display. """ return f"""{self._chord} root={self._root} quality={self._quality} on={self._on}""" def transpose(self, trans: int, scale: str = "C") -> None: """ Transpose the chord. :param trans: The number of semitones. :param scale: Key scale. """ if not isinstance(trans, int): raise TypeError(f"Expected integers, not {type(trans)}") self._root = transpose_note(self._root, trans, scale) if self._on: self._on = transpose_note(self._on, trans, scale) self._reconfigure_chord() @overload def components(self, visible: Literal[True]) -> list[str]: ... @overload def components(self, visible: Literal[False]) -> list[int]: ... def components(self, visible: bool = True) -> list[str] | list[int]: """ Return the component notes of the chord. :param visible: Returns the note names if ``True``, the note pitches otherwise. """ if visible: notes = self._quality.get_components(root=self._root, visible=True) if self._on: notes = [n for n in notes if n != self._on] notes.insert(0, self._on) return notes else: components = self._quality.get_components(root=self._root, visible=False) if self._on: on_value = note_to_val(self._on) components = [c for c in components if c % 12 != on_value % 12] if on_value > components[0]: on_value -= 12 components.insert(0, on_value) return components def components_with_pitch(self, root_pitch: int) -> list[str]: """ Return the component notes of chord formatted like ``["C4", "E4", "G4"]``. :param root_pitch: The pitch of the root note. """ components = self.components(visible=False) notes = self.components(visible=True) if components[0] < 0: components = [c + 12 for c in components] return [f"{n}{root_pitch + c // 12}" for (n, c) in zip(notes, components)] def _reconfigure_chord(self) -> None: self._chord = "{}{}{}".format( self._root, self._quality.quality, f"/{self._on}" if self._on else "", ) ================================================ FILE: pychord/constants/__init__.py ================================================ ================================================ FILE: pychord/constants/qualities.py ================================================ # Do not import DEFAULT_QUALITIES directly # Use QualityManager instead DEFAULT_QUALITIES = [ # chords consist of 2 notes ("5", ("1", "5")), ("no5", ("1", "3")), ("omit5", ("1", "3")), ("m(no5)", ("1", "b3")), ("m(omit5)", ("1", "b3")), # 3 notes ("", ("1", "3", "5")), ("maj", ("1", "3", "5")), ("m", ("1", "b3", "5")), ("min", ("1", "b3", "5")), ("-", ("1", "b3", "5")), ("dim", ("1", "b3", "b5")), # Not to confuse Ab5 with A(b5) ("(b5)", ("1", "3", "b5")), ("aug", ("1", "3", "#5")), ("sus2", ("1", "2", "5")), ("sus4", ("1", "4", "5")), ("sus", ("1", "4", "5")), # 4 notes ("6", ("1", "3", "5", "6")), # https://www.scales-chords.com/chord/piano/C%236b5 ("6b5", ("1", "3", "b5", "6")), ("6-5", ("1", "3", "b5", "6")), ("7", ("1", "3", "5", "b7")), ("7-5", ("1", "3", "b5", "b7")), ("7b5", ("1", "3", "b5", "b7")), ("7+5", ("1", "3", "#5", "b7")), ("7#5", ("1", "3", "#5", "b7")), ("7sus4", ("1", "4", "5", "b7")), ("m6", ("1", "b3", "5", "6")), ("m7", ("1", "b3", "5", "b7")), ("m7-5", ("1", "b3", "b5", "b7")), ("m7b5", ("1", "b3", "b5", "b7")), ("m7+5", ("1", "b3", "#5", "b7")), ("m7#5", ("1", "b3", "#5", "b7")), ("dim7", ("1", "b3", "b5", "bb7")), ("M7", ("1", "3", "5", "7")), ("maj7", ("1", "3", "5", "7")), ("maj7+5", ("1", "3", "#5", "7")), ("M7+5", ("1", "3", "#5", "7")), ("mmaj7", ("1", "b3", "5", "7")), ("mM7", ("1", "b3", "5", "7")), ("add4", ("1", "3", "4", "5")), ("majadd4", ("1", "3", "4", "5")), ("Madd4", ("1", "3", "4", "5")), ("madd4", ("1", "b3", "4", "5")), ("add9", ("1", "3", "5", "9")), ("majadd9", ("1", "3", "5", "9")), ("Madd9", ("1", "3", "5", "9")), ("madd9", ("1", "b3", "5", "9")), ("sus4add9", ("1", "4", "5", "9")), ("sus4add2", ("1", "2", "4", "5")), ("2", ("1", "3", "5", "9")), ("add11", ("1", "3", "5", "11")), ("4", ("1", "3", "5", "11")), # 5 notes ("m69", ("1", "b3", "5", "6", "9")), ("69", ("1", "3", "5", "6", "9")), ("9", ("1", "3", "5", "b7", "9")), ("m9", ("1", "b3", "5", "b7", "9")), ("M9", ("1", "3", "5", "7", "9")), ("maj9", ("1", "3", "5", "7", "9")), ("9sus4", ("1", "4", "5", "b7", "9")), ("7-9", ("1", "3", "5", "b7", "b9")), ("7b9", ("1", "3", "5", "b7", "b9")), # https://www.oolimo.com/guitarchords/Fsharp7(b9) ("7(b9)", ("1", "3", "5", "b7", "b9")), ("7+9", ("1", "3", "5", "b7", "#9")), ("7#9", ("1", "3", "5", "b7", "#9")), ("9-5", ("1", "3", "b5", "b7", "9")), ("9b5", ("1", "3", "b5", "b7", "9")), ("9+5", ("1", "3", "#5", "b7", "9")), ("9#5", ("1", "3", "#5", "b7", "9")), ("7#9b5", ("1", "3", "b5", "b7", "#9")), ("7#9#5", ("1", "3", "#5", "b7", "#9")), ("m7b9b5", ("1", "b3", "b5", "b7", "b9")), ("7b9b5", ("1", "3", "b5", "b7", "b9")), ("7b9#5", ("1", "3", "#5", "b7", "b9")), ("7+11", ("1", "3", "5", "b7", "#11")), ("7#11", ("1", "3", "5", "b7", "#11")), ("maj7+11", ("1", "3", "5", "7", "#11")), ("M7+11", ("1", "3", "5", "7", "#11")), ("maj7#11", ("1", "3", "5", "7", "#11")), ("M7#11", ("1", "3", "5", "7", "#11")), ("7-13", ("1", "3", "5", "b7", "b13")), ("7b13", ("1", "3", "5", "b7", "b13")), ("m7add11", ("1", "b3", "5", "b7", "11")), ("maj7add11", ("1", "3", "5", "7", "11")), ("M7add11", ("1", "3", "5", "7", "11")), ("mmaj7add11", ("1", "b3", "5", "7", "11")), ("mM7add11", ("1", "b3", "5", "7", "11")), ("maj7add13", ("1", "3", "5", "7", "13")), ("M7add13", ("1", "3", "5", "7", "13")), # 6 notes ("7b9#9", ("1", "3", "5", "b7", "b9", "#9")), ("7b9#11", ("1", "3", "5", "b7", "b9", "#11")), ("7#9#11", ("1", "3", "5", "b7", "#9", "#11")), ("9+11", ("1", "3", "5", "b7", "9", "#11")), ("9#11", ("1", "3", "5", "b7", "9", "#11")), ("11", ("1", "3", "5", "b7", "9", "11")), # https://chord-c.com/guitar-chord/B/minor-eleventh/ ("m11", ("1", "b3", "5", "b7", "9", "11")), # 7 notes ("7b9b13", ("1", "3", "5", "b7", "b9", "11", "b13")), ("13", ("1", "3", "5", "b7", "9", "11", "13")), ("13-9", ("1", "3", "5", "b7", "b9", "11", "13")), ("13b9", ("1", "3", "5", "b7", "b9", "11", "13")), ("13+9", ("1", "3", "5", "b7", "#9", "11", "13")), ("13#9", ("1", "3", "5", "b7", "#9", "11", "13")), ("13+11", ("1", "3", "5", "b7", "9", "#11", "13")), ("13#11", ("1", "3", "5", "b7", "9", "#11", "13")), ("maj13", ("1", "3", "5", "7", "9", "11", "13")), ("M13", ("1", "3", "5", "7", "9", "11", "13")), ] ================================================ FILE: pychord/constants/scales.py ================================================ NOTE_VALUES = { "C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11, } SHARPED_SCALE = { 0: "C", 1: "C#", 2: "D", 3: "D#", 4: "E", 5: "F", 6: "F#", 7: "G", 8: "G#", 9: "A", 10: "A#", 11: "B", } FLATTED_SCALE = { 0: "C", 1: "Db", 2: "D", 3: "Eb", 4: "E", 5: "F", 6: "Gb", 7: "G", 8: "Ab", 9: "A", 10: "Bb", 11: "B", } SCALE_VAL_DICT = { "Ab": FLATTED_SCALE, "A": SHARPED_SCALE, "A#": SHARPED_SCALE, "Bb": FLATTED_SCALE, "B": SHARPED_SCALE, "Cb": FLATTED_SCALE, "C": FLATTED_SCALE, "C#": SHARPED_SCALE, "Db": FLATTED_SCALE, "D": SHARPED_SCALE, "D#": SHARPED_SCALE, "Eb": FLATTED_SCALE, "E": SHARPED_SCALE, "F": FLATTED_SCALE, "F#": SHARPED_SCALE, "Gb": FLATTED_SCALE, "G": SHARPED_SCALE, "G#": SHARPED_SCALE, } # https://en.wikipedia.org/wiki/Mode_(music)#Modern_modes # Ionian -> maj, Aeolian -> min RELATIVE_KEY_DICT = { "maj": [0, 2, 4, 5, 7, 9, 11, 12], "Dor": [0, 2, 3, 5, 7, 9, 10, 12], "Phr": [0, 1, 3, 5, 7, 8, 10, 12], "Lyd": [0, 2, 4, 6, 7, 9, 11, 12], "Mix": [0, 2, 4, 5, 7, 9, 10, 12], "min": [0, 2, 3, 5, 7, 8, 10, 12], "Loc": [0, 1, 3, 5, 6, 8, 10, 12], } ================================================ FILE: pychord/parser.py ================================================ import re from .constants.scales import RELATIVE_KEY_DICT from .quality import QualityManager, Quality # We accept notes with up to two flats or two sharps. note_re = re.compile("^[A-G](b{0,2}|#{0,2})$") inversion_re = re.compile("/([0-9]+)") def _check_mode(mode: str) -> None: """Raise ValueError if mode is invalid""" if mode not in RELATIVE_KEY_DICT: raise ValueError(f"Invalid mode {mode}") def _check_note(note: str) -> None: """Raise ValueError if note is invalid""" if not note_re.match(note): raise ValueError(f"Invalid note {note}") def parse(chord: str) -> tuple[str, Quality, str]: """ Parse a string to get chord component. :param chord: Name of the chord. :return: (root, quality, on) """ if len(chord) > 2 and chord[1:3] in ("bb", "##"): root = chord[:3] rest = chord[3:] elif len(chord) > 1 and chord[1] in ("b", "#"): root = chord[:2] rest = chord[2:] else: root = chord[:1] rest = chord[1:] _check_note(root) inversion = 0 inversion_m = inversion_re.search(rest) if inversion_m: inversion = int(inversion_m.group(1)) rest = inversion_re.sub("", rest) on_chord_idx = rest.find("/") if on_chord_idx >= 0: on = rest[on_chord_idx + 1 :] rest = rest[:on_chord_idx] _check_note(on) else: on = "" quality = QualityManager().get_quality(rest, inversion) return root, quality, on def parse_scale(scale: str) -> tuple[str, str]: """ Parse a string representing a scale into its root and mode. """ root = scale[:-3] mode = scale[-3:] _check_note(root) _check_mode(mode) return (root, mode) ================================================ FILE: pychord/progression.py ================================================ from typing import Any from .chord import Chord class ChordProgression: """ A chord progression, which is a sequence of :class:`Chord` instances. :param initial_chords: Initial chord or chords of the chord progression. """ def __init__( self, initial_chords: str | Chord | list[str] | list[Chord] = [] ) -> None: if isinstance(initial_chords, Chord): chords = [initial_chords] elif isinstance(initial_chords, str): chords = [self._as_chord(initial_chords)] elif isinstance(initial_chords, list): chords = [self._as_chord(chord) for chord in initial_chords] else: raise TypeError( f"Cannot initialize ChordProgression with argument of {type(initial_chords)} type" ) self._chords: list[Chord] = chords def __str__(self) -> str: return " | ".join([chord.chord for chord in self._chords]) def __repr__(self) -> str: return f"" def __add__(self, other: "ChordProgression") -> "ChordProgression": return ChordProgression(self._chords + other._chords) def __len__(self) -> int: return len(self._chords) def __getitem__(self, key: int) -> Chord: return self._chords[key] def __setitem__(self, key: int, value: Chord) -> None: self._chords[key] = value def __eq__(self, other: Any) -> bool: if not isinstance(other, ChordProgression): raise TypeError( f"Cannot compare ChordProgression object with {type(other)} object" ) return self._chords == other._chords @property def chords(self) -> list[Chord]: """ The component chords of the chord progression. """ return self._chords def append(self, chord: str | Chord) -> None: """ Append a chord to the chord progression. :param chord: A chord to append. """ self._chords.append(self._as_chord(chord)) def insert(self, index: int, chord: str | Chord) -> None: """ Insert a chord into the chord progression. :param index: Index to insert a chord. :param chord: A chord to insert. """ self._chords.insert(index, self._as_chord(chord)) def pop(self, index: int = -1) -> Chord: """ Pop a chord from the chord progression. :param index: Index of the chord to pop (default: -1). """ return self._chords.pop(index) def transpose(self, trans: int) -> None: """ Transpose the whole chord progression. :param trans: The number of semitones. """ for chord in self._chords: chord.transpose(trans) @staticmethod def _as_chord(chord: str | Chord) -> Chord: """Convert from str to Chord instance if input is str. :param chord: Chord name or :class:`Chord` instance. """ if isinstance(chord, Chord): return chord elif isinstance(chord, str): return Chord(chord) else: raise TypeError("input type should be str or Chord instance.") ================================================ FILE: pychord/py.typed ================================================ Marker ================================================ FILE: pychord/quality.py ================================================ import copy import functools import re from typing import Any, Literal, overload from .constants.qualities import DEFAULT_QUALITIES from .constants.scales import RELATIVE_KEY_DICT from .utils import augment, diminish, note_to_val class Quality: """ A chord quality, defined by its intervals. You should never need to create instances of this class yourself. Use :class:`QualityManager` if you need to define a new quality or override an existing one. :param name: Name of the quality. :param intervals: Intervals defining the quality. """ def __init__(self, name: str, intervals: tuple[str, ...]) -> None: self._quality: str = name self._intervals = intervals def __str__(self) -> str: return self._quality def __eq__(self, other: Any) -> bool: if not isinstance(other, Quality): raise TypeError(f"Cannot compare Quality object with {type(other)} object") return self.components == other.components @property def components(self) -> tuple[int, ...]: return tuple(_get_interval_pitch(i) for i in self._intervals) @property def intervals(self) -> list[str]: """ The intervals definining the quality, e.g. ``["1", "3", "5"]`` or ``["1", "b3", "5", "b7"]``. """ return list(self._intervals) @property def quality(self) -> str: """ The name of the quality, e.g. ``"maj"``, ``"m7"``. """ return self._quality @overload def get_components(self, root: str, visible: Literal[True]) -> list[str]: ... @overload def get_components(self, root: str, visible: Literal[False]) -> list[int]: ... @overload def get_components(self, root: str, visible: bool) -> list[str] | list[int]: ... def get_components( self, root: str = "C", visible: bool = False ) -> list[str] | list[int]: """Get components of chord quality :param str root: the root note of the chord :param bool visible: returns the name of notes if True :rtype: list[str|int] :return: components of chord quality """ if visible: return [_apply_interval_to_note(root, i) for i in self._intervals] else: root_val = note_to_val(root) return [v + root_val for v in self.components] class QualityManager: """ Singleton class to manage the chord qualities. """ def __new__(cls) -> "QualityManager": if not hasattr(cls, "_instance"): cls._instance = super(QualityManager, cls).__new__(cls) cls._instance.load_default_qualities() return cls._instance def load_default_qualities(self) -> None: self._qualities = {q: Quality(q, c) for q, c in DEFAULT_QUALITIES} def get_quality(self, name: str, inversion: int = 0) -> Quality: if name not in self._qualities: raise ValueError(f"Unknown quality: {name}") # Create a new instance not to affect any existing instances q = copy.deepcopy(self._qualities[name]) # apply requested inversion : for i in range(inversion): max_a, max_o = _parse_interval(q._intervals[-1]) a, o = _parse_interval(q._intervals[0]) while o < max_o: o += 7 q._intervals = q._intervals[1:] + (f"{a}{o + 1}",) return q def get_qualities(self) -> dict[str, Quality]: return dict(self._qualities) def set_quality(self, name: str, intervals: tuple[str, ...]) -> None: """ Define a new quality or override an existing one. This method will not affect any existing :class:`Chord` instances. :param name: Name of the quality, e.g. ``"m"``. :param intervals: Intervals defining the quality, e.g. ``["1", "b3", "5"]``. """ self._qualities[name] = Quality(name, intervals) def find_quality_from_components(self, components: list[int]) -> Quality | None: """ Find a quality from its components. :param components: Components of the quality. """ for q in self._qualities.values(): if list(q.components) == components: return copy.deepcopy(q) return None def _apply_interval_to_note(root: str, interval: str) -> str: alterations, offset = _parse_interval(interval) # Apply the interval and alteration. notes_in_key = scale_notes(root, "maj") note = notes_in_key[offset % 7] for alteration in alterations: if alteration == "#": note = augment(note) else: note = diminish(note) return note def _get_interval_pitch(interval: str) -> int: alterations, offset = _parse_interval(interval) value = RELATIVE_KEY_DICT["maj"][offset % 7] + 12 * (offset // 7) for alteration in alterations: if alteration == "#": value += 1 else: value -= 1 return value def _parse_interval(interval: str) -> tuple[str, int]: m = re.match(r"^([b#]*)(\d+)$", interval) assert m, f"Invalid interval {interval}" alterations = m.group(1) offset = int(m.group(2)) - 1 return alterations, offset @functools.lru_cache() def scale_notes(root: str, mode: str) -> list[str]: """ Return the list of note names in the given scale. """ alphabet = ["C", "D", "E", "F", "G", "A", "B"] root_val = note_to_val(root) # Determine whether we use a flatted or sharped scale. if root == "F" or len(root) > 1 and root[1] == "b": alter = diminish else: alter = augment # Name notes in the key. notes = [root] index = alphabet.index(root[0]) for offset in RELATIVE_KEY_DICT[mode][1:-1]: index = (index + 1) % 7 note_val = (root_val + offset) % 12 # Find the accidental to match the pitch. letter = alphabet[index] for note in [ diminish(diminish(letter)), diminish(letter), letter, augment(letter), augment(augment(letter)), ]: if note_to_val(note) == note_val: notes.append(note) break note = alter(note) else: raise ValueError(f"{root}{mode} scale requires too many accidentals") return notes ================================================ FILE: pychord/utils.py ================================================ from .constants.scales import NOTE_VALUES, SCALE_VAL_DICT def augment(note: str) -> str: """ Augment the given note. """ if note.endswith("b"): return note[:-1] else: return note + "#" def diminish(note: str) -> str: """ Diminish the given note. """ if note.endswith("#"): return note[:-1] else: return note + "b" def note_to_val(note: str) -> int: """Get index value of a note >>> note_to_val("C") 0 >>> note_to_val("B") 11 """ try: pitch = NOTE_VALUES[note[0]] except KeyError: raise ValueError(f"Unknown note {note}") for alteration in note[1:]: if alteration == "b": pitch -= 1 else: pitch += 1 return pitch % 12 def transpose_note(note: str, transpose: int, scale: str = "C") -> str: """Transpose a note >>> transpose_note("C", 1) "Db" >>> transpose_note("D", 4, "A") "F#" """ val = note_to_val(note) val += transpose return SCALE_VAL_DICT[scale][val % 12] ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools>=45", "wheel"] build-backend = "setuptools.build_meta" [project] name = "pychord" version = "1.3.2" description = "Package to handle musical chords" readme = "README.md" requires-python = ">=3.10" license = "MIT" authors = [ {name = "Yuma Mihira", email = "info@yuma.cloud"}, ] keywords = ["music", "chord"] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Topic :: Multimedia :: Sound/Audio", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Utilities", ] [project.urls] Homepage = "https://github.com/yuma-m/pychord" [project.optional-dependencies] test = [ "black>=26.1.0", "mypy>=1.19.1", ] [tool.coverage.report] fail_under = 100 [tool.coverage.run] include = ["pychord/*"] [tool.mypy] strict = true [tool.setuptools] include-package-data = true [tool.setuptools.packages.find] where = ["."] exclude = ["test", "docs"] ================================================ FILE: setup.cfg ================================================ [metadata] name = pychord [bdist_wheel] universal = 0 ================================================ FILE: setup.py ================================================ from setuptools import setup if __name__ == "__main__": setup() ================================================ FILE: test/__init__.py ================================================ ================================================ FILE: test/test_analyzer.py ================================================ import unittest from pychord import Chord from pychord.analyzer import ( get_all_rotated_notes, find_chords_from_notes, notes_to_positions, ) class TestNotesToPositions(unittest.TestCase): def test_notes_to_positions(self): for notes, root, expected_positions in [ (["C"], "C", [0]), (["C", "G"], "C", [0, 7]), (["D", "F#", "A"], "D", [0, 4, 7]), (["E", "G#", "B", "D"], "E", [0, 4, 7, 10]), (["Ab", "C", "Eb", "Bb"], "Ab", [0, 4, 7, 14]), (["F", "A", "C", "Eb", "G"], "F", [0, 4, 7, 10, 14]), (["G", "B", "D", "F", "A", "C"], "G", [0, 4, 7, 10, 14, 17]), (["A", "C#", "E", "G", "B", "D", "F#"], "A", [0, 4, 7, 10, 14, 17, 21]), ]: with self.subTest(notes=notes, root=root): self.assertEqual(notes_to_positions(notes, root), expected_positions) class TestGetAllRotatedNotes(unittest.TestCase): def test_get_all_rotated_notes(self): for notes, expected_rotations in [ (["C", "G"], [["C", "G"], ["G", "C"]]), (["C", "F", "G"], [["C", "F", "G"], ["F", "G", "C"], ["G", "C", "F"]]), ]: with self.subTest(notes=notes): self.assertEqual(get_all_rotated_notes(notes), expected_rotations) class TestFindChordsFromNotes(unittest.TestCase): def test_empty(self): with self.assertRaises(ValueError): find_chords_from_notes([]) def test_find_chords_from_notes(self): """ Validates that the specified notes translated to the expected chords. """ for notes, expected_chord_strs in [ (["C", "E", "G"], ["C"]), (["F#", "A", "D"], ["D/F#"]), (["B", "E", "G#"], ["E/B"]), (["Eb", "Gb", "A"], ["Ebdim"]), (["G", "C", "D"], ["Gsus4", "Csus2/G"]), (["Eb", "Gb", "A", "C"], ["Ebdim7", "Gbdim7/Eb", "Adim7/Eb", "Cdim7/Eb"]), (["F", "A", "Db"], ["Faug", "Aaug/F", "Dbaug/F"]), (["C", "E", "G", "D"], ["Cadd9"]), (["F#", "A", "C", "E"], ["F#m7-5", "Am6/F#", "C6b5/F#"]), (["C", "E", "F", "G"], ["Cadd4"]), (["C", "Eb", "F", "G"], ["Cmadd4"]), (["C", "Eb", "G", "Bb", "F"], ["Cm7add11"]), (["C", "E", "G", "B", "F"], ["Cmaj7add11"]), (["C", "Eb", "G", "B", "F"], ["Cmmaj7add11"]), (["C", "E", "G", "B", "A"], ["Cmaj7add13", "Am9/C"]), ]: with self.subTest(notes=notes): chords = find_chords_from_notes(notes) self.assertEqual([str(c) for c in chords], expected_chord_strs) def test_idempotence(self): for _ in range(2): chords = find_chords_from_notes(["Eb", "Gb", "Bbb", "Dbb"]) self.assertEqual( chords, [ Chord("Ebdim7"), Chord("Gbdim7/Eb"), Chord("Adim7/Eb"), Chord("Cdim7/Eb"), ], ) self.assertEqual( chords[0].components(visible=True), ["Eb", "Gb", "Bbb", "Dbb"] ) ================================================ FILE: test/test_chord.py ================================================ import unittest from pychord import Chord class TestChordCreations(unittest.TestCase): def test_chord_creation(self): for chord, expected_root, expected_quality in [ ("C", "C", ""), ("Am", "A", "m"), ("A-", "A", "-"), ("C69", "C", "69"), ("Bm7-5", "B", "m7-5"), ("Dm7b5", "D", "m7b5"), ]: with self.subTest(chord=chord): c = Chord(chord) self.assertEqual(expected_root, c.root) self.assertEqual(expected_quality, c.quality.quality) def test_invalid_chord(self): for chord in [ "", "Ab#", # mix of flat and sharp "A#b", # mix of flat and sharp "Abbb", # too many flats "A###", # too many sharps "H", "Csus3", "C/B###", ]: with self.subTest(chord=chord): self.assertRaises(ValueError, Chord, chord) def test_slash_chord(self): for chord, expected_root, expected_quality, expected_on in [ ("F/G", "F", "", "G"), ("Dm/G", "D", "m", "G"), ]: with self.subTest(chord=chord): c = Chord(chord) self.assertEqual(expected_root, c.root) self.assertEqual(expected_quality, c.quality.quality) self.assertEqual(expected_on, c.on) def test_invalid_slash_chord(self): self.assertRaises(ValueError, Chord, "C/H") def test_inversion(self): for chord, expected_root, expected_quality, expected_components in [ ("C/1", "C", "", ["E", "G", "C"]), ("C/2", "C", "", ["G", "C", "E"]), ("Dm7b5/1", "D", "m7b5", ["F", "Ab", "C", "D"]), ("C/1/F", "C", "", ["F", "E", "G", "C"]), ]: with self.subTest(chord=chord): c = Chord(chord) self.assertEqual(expected_root, c.root) self.assertEqual(expected_quality, c.quality.quality) self.assertEqual(expected_components, c.components()) def test_eq(self): self.assertEqual(Chord("C"), Chord("C")) self.assertEqual(Chord("C/G"), Chord("C/G")) def test_eq_quality_alias(self): self.assertEqual(Chord("Cmaj7"), Chord("CM7")) def test_eq_root_alias(self): self.assertEqual(Chord("C#"), Chord("Db")) def test_eq_invalid(self): with self.assertRaises(TypeError): Chord("C") == 0 def test_eq_different_root(self): self.assertNotEqual(Chord("C"), Chord("D")) def test_eq_different_quality(self): self.assertNotEqual(Chord("C"), Chord("Cm")) def test_eq_different_on(self): self.assertNotEqual(Chord("C"), Chord("C/G")) self.assertNotEqual(Chord("C/G"), Chord("C")) self.assertNotEqual(Chord("C/B"), Chord("C/G")) def test_components(self): c = Chord("C/E") quality_components_before = c.quality.components c.components() self.assertEqual(c.quality.components, quality_components_before) def test_info(self): c = Chord("Cmaj7") # String representations. self.assertEqual(repr(c), "") self.assertEqual(str(c), "Cmaj7") # Properties. self.assertEqual(c.chord, "Cmaj7") self.assertEqual(str(c.quality), "maj7") self.assertEqual(c.root, "C") # Methods. self.assertEqual( c.info(), """Cmaj7 root=C quality=maj7 on=""", ) class TestChordFromNoteIndex(unittest.TestCase): def test_from_note_index(self): for note, quality, scale, expected_chord_str in [ (1, "", "Cmaj", "C"), (2, "m7", "F#min", "G#m7"), (3, "sus2", "Cmin", "Ebsus2"), (7, "7", "Amin", "G7"), ]: with self.subTest(note=note, quality=quality, scale=scale): chord = Chord.from_note_index(note=note, quality=quality, scale=scale) self.assertEqual(str(chord), expected_chord_str) def test_from_note_index_with_chromatic(self): for note, quality, scale, chromatic, expected_chord_str in [ (1, "", "Cmaj", -1, "Cb"), (1, "", "Cmaj", 1, "C#"), ]: with self.subTest( note=note, quality=quality, scale=scale, chromatic=chromatic ): chord = Chord.from_note_index( note=note, quality=quality, scale=scale, chromatic=chromatic ) self.assertEqual(str(chord), expected_chord_str) def test_invalid_note_index(self): for note, quality, scale, exception_str in [ (0, "", "Cmaj", "Invalid note 0"), (8, "", "Fmaj", "Invalid note 8"), ]: with self.subTest(note=note, quality=quality, scale=scale): with self.assertRaises(ValueError) as cm: Chord.from_note_index(note=note, quality=quality, scale=scale) self.assertEqual(str(cm.exception), exception_str) def test_invalid_scale(self): for note, quality, scale, exception_str in [ (1, "", "Xmaj", "Invalid note X"), (1, "", "Cbob", "Invalid mode bob"), ]: with self.subTest(note=note, quality=quality, scale=scale): with self.assertRaises(ValueError) as cm: Chord.from_note_index(note=note, quality=quality, scale=scale) self.assertEqual(str(cm.exception), exception_str) def test_diatonic_from_note_index(self): for note, quality, diatonic, scale, expected_chord_str in [ (1, "", True, "Dmaj", "D"), (2, "7", True, "BLoc", "CM7"), (3, "m", True, "G#Mix", "B#dim"), (4, "-", True, "AbDor", "Db"), ]: with self.subTest(note=note, quality=quality, scale=scale): chord = Chord.from_note_index( note=note, quality=quality, diatonic=diatonic, scale=scale ) self.assertEqual(str(chord), expected_chord_str) def test_diatonic_note_non_generic(self): with self.assertRaises(NotImplementedError): Chord.from_note_index(note=5, quality="sus", diatonic=True, scale="Fmaj") ================================================ FILE: test/test_component.py ================================================ import unittest from pychord import Chord class TestChordComponent(unittest.TestCase): def test_chord_components(self): """ Validates if a chord is made up of specified qualities and notes. """ for chord, expected_qualities, expected_notes in [ # Major chords with all supported accidentals. ("Abb", [7, 11, 14], ["Abb", "Cb", "Ebb"]), ("Ab", [8, 12, 15], ["Ab", "C", "Eb"]), ("A", [9, 13, 16], ["A", "C#", "E"]), ("A#", [10, 14, 17], ["A#", "C##", "E#"]), ("Bbb", [9, 13, 16], ["Bbb", "Db", "Fb"]), ("Bb", [10, 14, 17], ["Bb", "D", "F"]), ("B", [11, 15, 18], ["B", "D#", "F#"]), ("B#", [0, 4, 7], ["B#", "D##", "F##"]), ("Cbb", [10, 14, 17], ["Cbb", "Ebb", "Gbb"]), ("Cb", [11, 15, 18], ["Cb", "Eb", "Gb"]), ("C", [0, 4, 7], ["C", "E", "G"]), ("C#", [1, 5, 8], ["C#", "E#", "G#"]), ("C##", [2, 6, 9], ["C##", "E##", "G##"]), ("Dbb", [0, 4, 7], ["Dbb", "Fb", "Abb"]), ("Db", [1, 5, 8], ["Db", "F", "Ab"]), ("D", [2, 6, 9], ["D", "F#", "A"]), ("D#", [3, 7, 10], ["D#", "F##", "A#"]), ("Ebb", [2, 6, 9], ["Ebb", "Gb", "Bbb"]), ("Eb", [3, 7, 10], ["Eb", "G", "Bb"]), ("E", [4, 8, 11], ["E", "G#", "B"]), ("E#", [5, 9, 12], ["E#", "G##", "B#"]), ("Fb", [4, 8, 11], ["Fb", "Ab", "Cb"]), ("F", [5, 9, 12], ["F", "A", "C"]), ("F#", [6, 10, 13], ["F#", "A#", "C#"]), ("F##", [7, 11, 14], ["F##", "A##", "C##"]), ("Gbb", [5, 9, 12], ["Gbb", "Bbb", "Dbb"]), ("Gb", [6, 10, 13], ["Gb", "Bb", "Db"]), ("G", [7, 11, 14], ["G", "B", "D"]), ("G#", [8, 12, 15], ["G#", "B#", "D#"]), # Other chords. ("Am", [9, 12, 16], ["A", "C", "E"]), ("Cbm", [11, 14, 18], ["Cb", "Ebb", "Gb"]), ("Gm", [7, 10, 14], ["G", "Bb", "D"]), ("Bdim", [11, 14, 17], ["B", "D", "F"]), ("Cdim", [0, 3, 6], ["C", "Eb", "Gb"]), ("Dbdim", [1, 4, 7], ["Db", "Fb", "Abb"]), ("Ddim", [2, 5, 8], ["D", "F", "Ab"]), ("Gbdim", [6, 9, 12], ["Gb", "Bbb", "Dbb"]), ("Gdim", [7, 10, 13], ["G", "Bb", "Db"]), ("Cdim7", [0, 3, 6, 9], ["C", "Eb", "Gb", "Bbb"]), ("Dbdim7", [1, 4, 7, 10], ["Db", "Fb", "Abb", "Cbb"]), ("Dbaug", [1, 5, 9], ["Db", "F", "A"]), ("Eaug", [4, 8, 12], ["E", "G#", "B#"]), ("CM9/D", [-10, 0, 4, 7, 11], ["D", "C", "E", "G", "B"]), ("Fsus4", [5, 10, 12], ["F", "Bb", "C"]), ("G7", [7, 11, 14, 17], ["G", "B", "D", "F"]), ("G7b9", [7, 11, 14, 17, 20], ["G", "B", "D", "F", "Ab"]), ("G7#11", [7, 11, 14, 17, 25], ["G", "B", "D", "F", "C#"]), ("Gm7", [7, 10, 14, 17], ["G", "Bb", "D", "F"]), ("C6", [0, 4, 7, 9], ["C", "E", "G", "A"]), ("C#m7b9b5", [1, 4, 7, 11, 14], ["C#", "E", "G", "B", "D"]), ("Db5", [1, 8], ["Db", "Ab"]), ("D(b5)", [2, 6, 8], ["D", "F#", "Ab"]), ("Cno5", [0, 4], ["C", "E"]), ("Cadd4", [0, 4, 5, 7], ["C", "E", "F", "G"]), ("CMadd4", [0, 4, 5, 7], ["C", "E", "F", "G"]), ("Cmadd4", [0, 3, 5, 7], ["C", "Eb", "F", "G"]), ("Csus4add9", [0, 5, 7, 14], ["C", "F", "G", "D"]), ("Cm7add11", [0, 3, 7, 10, 17], ["C", "Eb", "G", "Bb", "F"]), ("CM7add11", [0, 4, 7, 11, 17], ["C", "E", "G", "B", "F"]), ("Dm7b5", [2, 5, 8, 12], ["D", "F", "Ab", "C"]), ("Bm7-5", [11, 14, 17, 21], ["B", "D", "F", "A"]), ("Ebm7b5", [3, 6, 9, 13], ["Eb", "Gb", "Bbb", "Db"]), ("CmM7add11", [0, 3, 7, 11, 17], ["C", "Eb", "G", "B", "F"]), ("CM7add13", [0, 4, 7, 11, 21], ["C", "E", "G", "B", "A"]), ("C11", [0, 4, 7, 10, 14, 17], ["C", "E", "G", "Bb", "D", "F"]), ("C13", [0, 4, 7, 10, 14, 17, 21], ["C", "E", "G", "Bb", "D", "F", "A"]), ]: with self.subTest(chord=chord): c = Chord(chord) self.assertEqual(c.components(visible=False), expected_qualities) self.assertEqual(c.components(visible=True), expected_notes) def test_major_add9(self): # major add 9 is a major chord with a Major ninth base = Chord("C") base0 = list(base.components(visible=False)) base1 = list(base.components(visible=True)) c = Chord("CMadd9") com0 = c.components(visible=False) self.assertEqual(com0, base0 + [14]) com1 = c.components(visible=True) self.assertEqual(com1, base1 + ["D"]) def test_too_many_accidentals(self): for chord in [ "A##", "B##", "D##", "E##", "Fbb", "G##", ]: with self.subTest(chord=chord): c = Chord(chord) with self.assertRaises(ValueError) as cm: c.components() self.assertEqual( str(cm.exception), f"{chord}maj scale requires too many accidentals", ) class TestChordComponentWithPitch(unittest.TestCase): def test_basic_chords_with_pitch(self): """ Validates if a chord with pitch is correctly calculated. """ for chord, root_pitch, expected in [ ("C", 1, ["C1", "E1", "G1"]), ("Am", 2, ["A2", "C3", "E3"]), ("Dm7/G", 3, ["G3", "D4", "F4", "A4", "C5"]), ("Eadd9", 5, ["E5", "G#5", "B5", "F#6"]), ]: with self.subTest(chord=chord, root_pitch=root_pitch): c = Chord(chord) self.assertEqual( c.components_with_pitch(root_pitch=root_pitch), expected ) def test_first_order_inversion(self): c = Chord("G/1") com = c.components_with_pitch(root_pitch=4) self.assertEqual(com, ["B4", "D5", "G5"]) c2 = Chord("G13b9/1") com2 = c2.components_with_pitch(root_pitch=4) self.assertEqual(com2, ["B4", "D5", "F5", "Ab5", "C6", "E6", "G6"]) def test_second_order_inversion(self): c = Chord("G/2") com = c.components_with_pitch(root_pitch=4) self.assertEqual(com, ["D5", "G5", "B5"]) c2 = Chord("G13b9/2") com2 = c2.components_with_pitch(root_pitch=4) self.assertEqual(com2, ["D5", "F5", "Ab5", "C6", "E6", "G6", "B6"]) def test_third_order_inversion(self): c = Chord("Cm7/3") com = c.components_with_pitch(root_pitch=4) self.assertEqual(com, ["Bb4", "C5", "Eb5", "G5"]) c2 = Chord("F#7/3") com2 = c2.components_with_pitch(root_pitch=4) self.assertEqual(com2, ["E5", "F#5", "A#5", "C#6"]) c3 = Chord("G13b9/3") com3 = c3.components_with_pitch(root_pitch=4) self.assertEqual(com3, ["F5", "Ab5", "C6", "E6", "G6", "B6", "D7"]) def test_fourth_order_inversion(self): c = Chord("F7b9") com = c.components_with_pitch(root_pitch=4) self.assertEqual(com, ["F4", "A4", "C5", "Eb5", "Gb5"]) c2 = Chord("G13b9/4") com2 = c2.components_with_pitch(root_pitch=4) self.assertEqual(com2, ["Ab5", "C6", "E6", "G6", "B6", "D7", "F7"]) def test_fifth_order_inversion(self): c = Chord("G13b9/5") com = c.components_with_pitch(root_pitch=4) self.assertEqual(com, ["C6", "E6", "G6", "B6", "D7", "F7", "Ab7"]) ================================================ FILE: test/test_progression.py ================================================ import unittest from pychord import Chord, ChordProgression class TestChordProgressionCreations(unittest.TestCase): def test_none(self): cp = ChordProgression() self.assertEqual(cp.chords, []) self.assertEqual(str(cp), "") self.assertEqual(repr(cp), "") def test_one_chord(self): c = Chord("C") cp = ChordProgression(c) self.assertEqual(cp.chords, [c]) self.assertEqual(str(cp), "C") self.assertEqual(repr(cp), "") def test_one_chord_str(self): c = "C" cp = ChordProgression(c) self.assertEqual(cp.chords, [Chord(c)]) self.assertEqual(str(cp), "C") self.assertEqual(repr(cp), "") def test_one_chord_invalid_type(self): with self.assertRaises(TypeError): ChordProgression(1) def test_one_chord_list(self): c = Chord("C") cp = ChordProgression([c]) self.assertEqual(cp.chords, [c]) def test_one_chord_list_str(self): c = "C" cp = ChordProgression([c]) self.assertEqual(cp.chords, [Chord(c)]) self.assertEqual(str(cp), "C") self.assertEqual(repr(cp), "") def test_one_chord_list_invalid_type(self): with self.assertRaises(TypeError): ChordProgression([1]) def test_multiple_chords(self): c1 = Chord("C") c2 = Chord("D") cp = ChordProgression([c1, c2]) self.assertEqual(cp.chords, [c1, c2]) self.assertEqual(str(cp), "C | D") self.assertEqual(repr(cp), "") def test_multiple_chords_str(self): c1 = "C" c2 = "D" cp = ChordProgression([c1, c2]) self.assertEqual(cp.chords, [Chord(c1), Chord(c2)]) self.assertEqual(str(cp), "C | D") self.assertEqual(repr(cp), "") class TestChordProgressionFunctions(unittest.TestCase): def test_append(self): cp = ChordProgression(["C", "D", "E"]) cp.append("F") self.assertEqual(len(cp), 4) self.assertEqual(cp.chords[-1], Chord("F")) def test_insert(self): cp = ChordProgression(["C", "D", "E"]) cp.insert(0, "F") self.assertEqual(len(cp), 4) self.assertEqual(cp.chords[0], Chord("F")) def test_pop(self): cp = ChordProgression(["C", "D", "E"]) c = cp.pop() self.assertEqual(len(cp), 2) self.assertEqual(c, Chord("E")) def test_transpose(self): cp = ChordProgression(["C", "F", "G"]) cp.transpose(3) self.assertEqual(cp.chords, [Chord("Eb"), Chord("Ab"), Chord("Bb")]) def test_add(self): cp1 = ChordProgression(["C", "F", "G"]) cp2 = ChordProgression(["Am", "Em"]) cp = cp1 + cp2 self.assertEqual(len(cp), 5) self.assertEqual( cp.chords, [Chord("C"), Chord("F"), Chord("G"), Chord("Am"), Chord("Em")] ) # Check the original progressions have not changed. self.assertEqual(len(cp1), 3) self.assertEqual(len(cp2), 2) def test_self_add(self): cp1 = ChordProgression(["C", "F", "G"]) cp2 = ChordProgression(["Am", "Em"]) cp1 += cp2 self.assertEqual(len(cp1), 5) self.assertEqual( cp1.chords, [Chord("C"), Chord("F"), Chord("G"), Chord("Am"), Chord("Em")] ) def test_get_item(self): cp = ChordProgression(["C", "F", "G"]) self.assertEqual(cp[0], Chord("C")) self.assertEqual(cp[1], Chord("F")) self.assertEqual(cp[-1], Chord("G")) def test_set_item(self): cp = ChordProgression(["C", "F", "G"]) cp[1] = Chord("E") self.assertEqual(cp[0], Chord("C")) self.assertEqual(cp[1], Chord("E")) self.assertEqual(cp[2], Chord("G")) self.assertEqual(len(cp), 3) def test_slice(self): cp = ChordProgression(["C", "F", "G"]) self.assertEqual(cp[0:1], [Chord("C")]) self.assertEqual(cp[1:], [Chord("F"), Chord("G")]) self.assertEqual(cp[0::2], [Chord("C"), Chord("G")]) def test_eq(self): cp1 = ChordProgression(["C", "F", "G"]) cp2 = ChordProgression(["C", "F", "G"]) self.assertEqual(cp1, cp2) self.assertIsNot(cp1, cp2) def test_invalid_eq(self): cp = ChordProgression(["C", "F", "G"]) with self.assertRaises(TypeError): print(cp == 0) ================================================ FILE: test/test_quality.py ================================================ # -*- coding: utf-8 -*- import unittest from pychord import QualityManager, Chord, find_chords_from_notes class TestQuality(unittest.TestCase): def setUp(self): self.quality_manager = QualityManager() def test_eq(self): q1 = self.quality_manager.get_quality("m7-5") q2 = self.quality_manager.get_quality("m7-5") self.assertEqual(q1, q2) def test_eq_alias_maj9(self): q1 = self.quality_manager.get_quality("M9") q2 = self.quality_manager.get_quality("maj9") self.assertEqual(q1, q2) def test_eq_alias_m7b5(self): q1 = self.quality_manager.get_quality("m7-5") q2 = self.quality_manager.get_quality("m7b5") self.assertEqual(q1, q2) def test_eq_alias_min(self): q1 = self.quality_manager.get_quality("m") q2 = self.quality_manager.get_quality("min") q3 = self.quality_manager.get_quality("-") self.assertEqual(q1, q2) self.assertEqual(q1, q3) def test_invalid_eq(self): q = self.quality_manager.get_quality("m7") with self.assertRaises(TypeError): print(q == 0) def subtest_quality_synonym(self, a, b): with self.subTest(msg=f"{a}_has_{b}_synonym"): a_quality = self.quality_manager.get_quality(a) b_quality = self.quality_manager.get_quality(b) self.assertEqual(a_quality, b_quality) def test_maj_synonyms(self): for q in self.quality_manager.get_qualities(): if q in ["M", "maj"]: continue if "maj" in q: self.subtest_quality_synonym(q, q.replace("maj", "M")) elif "M" in q: self.subtest_quality_synonym(q, q.replace("M", "maj")) def test_properties(self): q = self.quality_manager.get_quality("m") self.assertEqual(str(q), "m") self.assertEqual(q.intervals, ["1", "b3", "5"]) self.assertEqual(q.quality, "m") class TestQualityManager(unittest.TestCase): def test_singleton(self): quality_manager = QualityManager() quality_manager2 = QualityManager() self.assertIs(quality_manager, quality_manager2) class TestOverwriteQuality(unittest.TestCase): def setUp(self): self.quality_manager = QualityManager() def tearDown(self): self.quality_manager.load_default_qualities() def test_overwrite(self): # Remove the 9th from the "11" quality before building a chord. self.quality_manager.set_quality("11", ("1", "3", "5", "b7", "11")) chord = Chord("C11") self.assertEqual(chord.components(), ["C", "E", "G", "Bb", "F"]) def test_find_from_components(self): # Remove the 9th from the "11" quality then lookup a chord. self.quality_manager.set_quality("11", ("1", "3", "5", "b7", "11")) chords = find_chords_from_notes(["C", "E", "G", "Bb", "F"]) self.assertEqual(chords, [Chord("C11")]) def test_keep_existing_chord(self): # Remove the 9th from the "11" quality after building a chord. chord = Chord("C11") self.quality_manager.set_quality("11", ("1", "3", "5", "b7", "11")) self.assertEqual(chord.components(), ["C", "E", "G", "Bb", "D", "F"]) class TestIterateQualities(unittest.TestCase): def setUp(self): self.quality_manager = QualityManager() def tearDown(self): self.quality_manager.load_default_qualities() def test_iterate_qualities(self): assert "m" in self.quality_manager.get_qualities() def test_immutable_qualities(self): qualities = self.quality_manager.get_qualities() assert "testquality" not in qualities qualities["testquality"] = qualities["m"] qualities = self.quality_manager.get_qualities() assert "testquality" not in qualities def test_iterate_added_qualities(self): self.quality_manager.set_quality("testquality", ("1",)) qualities = self.quality_manager.get_qualities() assert "testquality" in qualities ================================================ FILE: test/test_transpose.py ================================================ import unittest from pychord import Chord class TestChordTranspose(unittest.TestCase): def test_transpose_zero(self): c = Chord("Am") c.transpose(0) self.assertEqual(c.root, "A") self.assertEqual(c.quality.quality, "m") self.assertEqual(c, Chord("Am")) def test_transpose_positive(self): c = Chord("Am") c.transpose(3) self.assertEqual(c.root, "C") self.assertEqual(c.quality.quality, "m") self.assertEqual(c, Chord("Cm")) def test_transpose_negative(self): c = Chord("Am") c.transpose(-4) self.assertEqual(c.root, "F") self.assertEqual(c.quality.quality, "m") self.assertEqual(c, Chord("Fm")) def test_transpose_slash(self): c = Chord("Am7/G") c.transpose(3) self.assertEqual(c.root, "C") self.assertEqual(c.quality.quality, "m7") self.assertEqual(c.on, "Bb") self.assertEqual(c._chord, Chord("Cm7/Bb")._chord) self.assertEqual(c.quality.components, Chord("Cm7/Bb").quality.components) self.assertEqual(c, Chord("Cm7/Bb")) def test_transpose_inversion(self): c = Chord("Am7/3") c.transpose(3) self.assertEqual(c.root, "C") self.assertEqual(c.quality.quality, "m7") def test_invalid_transpose_type(self): c = Chord("Am") self.assertRaises(TypeError, c.transpose, "A") def test_transpose_eq1(self): c = Chord("C") c.transpose(1) self.assertEqual(c, Chord("C#")) self.assertEqual(c, Chord("Db")) def test_transpose_eq2(self): c = Chord("C") c.transpose(2) self.assertEqual(c, Chord("D")) ================================================ FILE: test/test_utils.py ================================================ import unittest from pychord.utils import augment, diminish, note_to_val class TestUtils(unittest.TestCase): def test_augment(self): self.assertEqual(augment("Cb"), "C") self.assertEqual(augment("C"), "C#") self.assertEqual(augment("C#"), "C##") def test_diminish(self): self.assertEqual(diminish("Cb"), "Cbb") self.assertEqual(diminish("C"), "Cb") self.assertEqual(diminish("C#"), "C") def test_note_to_val(self): self.assertEqual(note_to_val("C"), 0) def test_note_to_val_invalid(self): with self.assertRaises(ValueError): note_to_val("X")