├── exec ├── kord ├── keys │ ├── __init__.py │ ├── chords.py │ ├── scales.py │ ├── test_chords.py │ └── test_scales.py ├── parsers │ ├── __init__.py │ ├── pitch_parser.py │ ├── test_pitch_parser.py │ ├── chord_parser.py │ └── test_chord_parser.py ├── notes │ ├── __init__.py │ ├── constants │ │ ├── __init__.py │ │ ├── naturals.py │ │ ├── flats.py │ │ ├── sharps.py │ │ ├── double_flats.py │ │ └── double_sharps.py │ ├── intervals.py │ ├── test_notes.py │ └── pitch.py ├── __init__.py ├── errors.py └── instruments.py ├── pngs ├── e7.png ├── adim.png ├── chrom.png ├── dim7.png ├── lydian.png ├── penta.png └── locrian.png ├── app ├── tunings │ ├── banjo.json │ ├── ronroco.json │ ├── ukulele.json │ ├── bass.json │ └── guitar.json ├── tuner.py └── fretboard.py ├── test.py ├── LICENSE ├── Makefile ├── setup.py └── README.md /exec: -------------------------------------------------------------------------------- 1 | ./app/fretboard.py -------------------------------------------------------------------------------- /kord/keys/__init__.py: -------------------------------------------------------------------------------- 1 | from .scales import * 2 | from .chords import * 3 | -------------------------------------------------------------------------------- /pngs/e7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synestematic/kord/HEAD/pngs/e7.png -------------------------------------------------------------------------------- /pngs/adim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synestematic/kord/HEAD/pngs/adim.png -------------------------------------------------------------------------------- /pngs/chrom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synestematic/kord/HEAD/pngs/chrom.png -------------------------------------------------------------------------------- /pngs/dim7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synestematic/kord/HEAD/pngs/dim7.png -------------------------------------------------------------------------------- /pngs/lydian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synestematic/kord/HEAD/pngs/lydian.png -------------------------------------------------------------------------------- /pngs/penta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synestematic/kord/HEAD/pngs/penta.png -------------------------------------------------------------------------------- /pngs/locrian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synestematic/kord/HEAD/pngs/locrian.png -------------------------------------------------------------------------------- /kord/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .chord_parser import ChordParser 3 | from .pitch_parser import NotePitchParser 4 | -------------------------------------------------------------------------------- /app/tunings/banjo.json: -------------------------------------------------------------------------------- 1 | { 2 | "standard": [ 3 | "D3", 4 | "B2", 5 | "G2", 6 | "D2" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /kord/notes/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .intervals import Intervals 3 | from .pitch import * 4 | 5 | __all__ = [ 6 | # 'NotePitch', 7 | ] 8 | -------------------------------------------------------------------------------- /kord/__init__.py: -------------------------------------------------------------------------------- 1 | from .instruments import * 2 | from .parsers import * 3 | from .errors import * 4 | from .notes import * 5 | from .keys import * 6 | 7 | -------------------------------------------------------------------------------- /app/tunings/ronroco.json: -------------------------------------------------------------------------------- 1 | { 2 | "standard": [ 3 | "B4", 4 | "E4", 5 | "B3", 6 | "G4", 7 | "D4" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /kord/notes/constants/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .naturals import * 3 | from .flats import * 4 | from .sharps import * 5 | from .double_flats import * 6 | from .double_sharps import * 7 | -------------------------------------------------------------------------------- /app/tunings/ukulele.json: -------------------------------------------------------------------------------- 1 | { 2 | "standard": [ 3 | "A3", 4 | "E3", 5 | "C3", 6 | "G3" 7 | ], 8 | "lowG": [ 9 | "A3", 10 | "E3", 11 | "C3", 12 | "G2" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /app/tunings/bass.json: -------------------------------------------------------------------------------- 1 | { 2 | "standard": [ 3 | "G2", 4 | "D2", 5 | "A1", 6 | "E1" 7 | ], 8 | "5string": [ 9 | "G2", 10 | "D2", 11 | "A1", 12 | "E1", 13 | "B0" 14 | ], 15 | "6string": [ 16 | "C3", 17 | "G2", 18 | "D2", 19 | "A1", 20 | "E1", 21 | "B0" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /kord/errors.py: -------------------------------------------------------------------------------- 1 | 2 | __all__ = [ 3 | 'InvalidInstrument', 4 | 'InvalidScale', 5 | 'InvalidChord', 6 | 'InvalidNote', 7 | 'InvalidAlteration', 8 | 'InvalidOctave', 9 | ] 10 | 11 | 12 | class InvalidInstrument(Exception): 13 | pass 14 | 15 | class InvalidScale(Exception): 16 | pass 17 | 18 | class InvalidChord(Exception): 19 | pass 20 | 21 | class InvalidNote(Exception): 22 | pass 23 | 24 | class InvalidAlteration(Exception): 25 | pass 26 | 27 | class InvalidOctave(Exception): 28 | pass 29 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | """ TDD: 2 | * write test 3 | * fail test RED 4 | * write code 5 | * pass test GREEN 6 | * remove duplication REFACTOR 7 | * pass test 8 | """ 9 | 10 | import unittest 11 | 12 | from kord.keys.test_scales import * 13 | from kord.keys.test_chords import * 14 | from kord.notes.test_notes import * 15 | 16 | from kord.parsers.test_chord_parser import * 17 | from kord.parsers.test_pitch_parser import * 18 | 19 | 20 | if __name__ == '__main__': 21 | try: 22 | unittest.main() 23 | except KeyboardInterrupt: 24 | print() 25 | -------------------------------------------------------------------------------- /kord/notes/intervals.py: -------------------------------------------------------------------------------- 1 | 2 | from enum import IntEnum 3 | 4 | __all__ = [ 5 | 'Intervals', 6 | ] 7 | 8 | class Intervals(IntEnum): 9 | 10 | UNISON = 0 11 | DIMINISHED_SECOND = 0 12 | 13 | MINOR_SECOND = 1 14 | AUGMENTED_UNISON = 1 15 | 16 | MAJOR_SECOND = 2 17 | DIMINISHED_THIRD = 2 18 | 19 | MINOR_THIRD = 3 20 | AUGMENTED_SECOND = 3 21 | 22 | MAJOR_THIRD = 4 23 | DIMINISHED_FOURTH = 4 24 | 25 | PERFECT_FOURTH = 5 26 | AUGMENTED_THIRD = 5 27 | 28 | DIMINISHED_FIFTH = 6 29 | AUGMENTED_FOURTH = 6 30 | 31 | PERFECT_FIFTH = 7 32 | DIMINISHED_SIXTH = 7 33 | 34 | MINOR_SIXTH = 8 35 | AUGMENTED_FIFTH = 8 36 | 37 | MAJOR_SIXTH = 9 38 | DIMINISHED_SEVENTH = 9 39 | 40 | MINOR_SEVENTH = 10 41 | AUGMENTED_SIXTH = 10 42 | 43 | MAJOR_SEVENTH = 11 44 | DIMINISHED_OCTAVE = 11 45 | 46 | PERFECT_OCTAVE = 12 47 | AUGMENTED_SEVENTH = 12 48 | 49 | 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 The Python Packaging Authority 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG = kord 2 | 3 | DISTR_DIRS = dist \ 4 | build \ 5 | ${PKG}.egg-info 6 | 7 | CLEAN_DIRS = kord/__pycache__ \ 8 | kord/keys/__pycache__ \ 9 | kord/notes/__pycache__ \ 10 | kord/notes/constants/__pycache__ \ 11 | kord/parsers/__pycache__ \ 12 | app/__pycache__ 13 | 14 | default: build install clean 15 | 16 | get_version: setup.py 17 | @$(eval VER := $(shell cat setup.py | grep version | cut -d'=' -f 2 | cut -d',' -f 1)) 18 | 19 | build: get_version setup.py ${PKG} 20 | @python3 setup.py sdist bdist_wheel && echo "done building ${PKG} ${VER}, bye!" || exit 21 | 22 | install: get_version ${DISTR_DIRS} 23 | @echo "Installing ${PKG} ${VER}" 24 | @cd dist && \ 25 | (source ~/.pyenv/versions/"${PKG}"/bin/activate && \ 26 | pip install --ignore-installed "${PKG}"-"${VER}"-py3-none-any.whl 2>/dev/null) || \ 27 | pip3 install --ignore-installed "${PKG}"-"${VER}"-py3-none-any.whl 28 | 29 | clean: 30 | -@for dir in ${DISTR_DIRS} ${CLEAN_DIRS} ; do \ 31 | [ -d "$${dir}" ] && rm -rf "$${dir}" && echo "Deleted dir: $${dir}" ; \ 32 | done 33 | 34 | publish: build 35 | @twine upload dist/* 36 | 37 | test: 38 | @(source ~/.pyenv/versions/"${PKG}"/bin/activate && \ 39 | python test.py) || \ 40 | python3 test.py 41 | 42 | dev: 43 | @(source ~/.pyenv/versions/"${PKG}"/bin/activate && \ 44 | python dev.py) || \ 45 | python3 dev.py 46 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="kord", 8 | version="5.4", 9 | author="Federico Rizzo", 10 | author_email="synestem@ticATgmail.com", 11 | description='programming framework for developing music applications', 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/synestematic/kord", 15 | license="MIT", 16 | packages=setuptools.find_packages(), 17 | classifiers=[ 18 | "Programming Language :: Python :: 3 :: Only", 19 | "Development Status :: 5 - Production/Stable", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: POSIX :: Linux", 22 | "Operating System :: MacOS :: MacOS X", 23 | ], 24 | data_files=[ 25 | ('fretboard', [ # ~/.local/fretboard/ 26 | 'app/fretboard.py', 27 | 'app/tuner.py', 28 | ], 29 | ), 30 | ('fretboard/tunings', [ 31 | 'app/tunings/banjo.json', 32 | 'app/tunings/bass.json', 33 | 'app/tunings/guitar.json', 34 | 'app/tunings/ronroco.json', 35 | 'app/tunings/ukulele.json', 36 | ], 37 | ), 38 | ], 39 | install_requires=[ 40 | 'bestia>=5.0', 41 | ], 42 | ) 43 | -------------------------------------------------------------------------------- /app/tunings/guitar.json: -------------------------------------------------------------------------------- 1 | { 2 | "standard": [ 3 | "E4", 4 | "B3", 5 | "G3", 6 | "D3", 7 | "A2", 8 | "E2" 9 | ], 10 | "Eb": [ 11 | "Eb4", 12 | "Bb3", 13 | "Gb3", 14 | "Db3", 15 | "Ab2", 16 | "Eb2" 17 | ], 18 | "D": [ 19 | "D4", 20 | "A3", 21 | "F3", 22 | "C3", 23 | "G2", 24 | "D2" 25 | ], 26 | "dropD": [ 27 | "E4", 28 | "B3", 29 | "G3", 30 | "D3", 31 | "A2", 32 | "D2" 33 | ], 34 | "7string": [ 35 | "E4", 36 | "B3", 37 | "G3", 38 | "D3", 39 | "A2", 40 | "E2", 41 | "B1" 42 | ], 43 | "8string": [ 44 | "E4", 45 | "B3", 46 | "G3", 47 | "D3", 48 | "A2", 49 | "E2", 50 | "B1", 51 | "F#1" 52 | ], 53 | "openE": [ 54 | "E4", 55 | "B3", 56 | "G#3", 57 | "E3", 58 | "B2", 59 | "E2" 60 | ], 61 | "openD": [ 62 | "D4", 63 | "A3", 64 | "F#3", 65 | "D3", 66 | "A2", 67 | "D2" 68 | ], 69 | "openG": [ 70 | "D4", 71 | "B3", 72 | "G3", 73 | "D3", 74 | "G2", 75 | "D2" 76 | ], 77 | "openF#": [ 78 | "C#4", 79 | "A#3", 80 | "F#3", 81 | "C#3", 82 | "F#2", 83 | "C#2" 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /app/tuner.py: -------------------------------------------------------------------------------- 1 | """ 2 | this module is in charge of loading the data of the .json files in the tunings directory and make it available to the fretboard application. 3 | """ 4 | 5 | import os 6 | import json 7 | 8 | from kord.notes import NotePitch 9 | 10 | JSON_DIR = '{}/tunings'.format( os.path.dirname(os.path.realpath(__file__)) ) 11 | 12 | def json_instruments(): 13 | return [ 14 | f.split('.')[0] for f in os.listdir(JSON_DIR) if f.endswith('.json') 15 | ] 16 | 17 | def open_instrument(instrument): 18 | try: 19 | with open('{}/{}.json'.format(JSON_DIR, instrument)) as js: 20 | return json.load(js) 21 | except: 22 | return {} 23 | 24 | def load_tuning_data(): 25 | data = {} 26 | for instrument in json_instruments(): 27 | 28 | instrument_data = open_instrument(instrument) 29 | if not instrument_data: 30 | print('WARNING: {}.json is not valid json, ignoring file...'.format(instrument)) 31 | continue 32 | 33 | for tuning, strings_list in instrument_data.items(): 34 | for s, string in enumerate(strings_list): 35 | try: 36 | # validates the note attributes 37 | instrument_data[tuning][s] = NotePitch( 38 | string[0], string[1:-1], string[-1] 39 | ) 40 | except: 41 | print( 42 | 'WARNING: "{}" is not a valid note, ignoring {}.json "{}" tuning...'.format(string, instrument, tuning) 43 | ) 44 | 45 | data[instrument] = instrument_data 46 | 47 | return data 48 | -------------------------------------------------------------------------------- /kord/notes/constants/naturals.py: -------------------------------------------------------------------------------- 1 | 2 | from ..pitch import NotePitch 3 | 4 | 5 | C_0 = NotePitch('C', '' , 0) 6 | D_0 = NotePitch('D', '' , 0) 7 | E_0 = NotePitch('E', '' , 0) 8 | F_0 = NotePitch('F', '' , 0) 9 | G_0 = NotePitch('G', '' , 0) 10 | A_0 = NotePitch('A', '' , 0) 11 | B_0 = NotePitch('B', '' , 0) 12 | 13 | C_1 = NotePitch('C', '' , 1) 14 | D_1 = NotePitch('D', '' , 1) 15 | E_1 = NotePitch('E', '' , 1) 16 | F_1 = NotePitch('F', '' , 1) 17 | G_1 = NotePitch('G', '' , 1) 18 | A_1 = NotePitch('A', '' , 1) 19 | B_1 = NotePitch('B', '' , 1) 20 | 21 | C_2 = NotePitch('C', '' , 2) 22 | D_2 = NotePitch('D', '' , 2) 23 | E_2 = NotePitch('E', '' , 2) 24 | F_2 = NotePitch('F', '' , 2) 25 | G_2 = NotePitch('G', '' , 2) 26 | A_2 = NotePitch('A', '' , 2) 27 | B_2 = NotePitch('B', '' , 2) 28 | 29 | C_3 = NotePitch('C', '' , 3) 30 | D_3 = NotePitch('D', '' , 3) 31 | E_3 = NotePitch('E', '' , 3) 32 | F_3 = NotePitch('F', '' , 3) 33 | G_3 = NotePitch('G', '' , 3) 34 | A_3 = NotePitch('A', '' , 3) 35 | B_3 = NotePitch('B', '' , 3) 36 | 37 | C_4 = NotePitch('C', '' , 4) 38 | D_4 = NotePitch('D', '' , 4) 39 | E_4 = NotePitch('E', '' , 4) 40 | F_4 = NotePitch('F', '' , 4) 41 | G_4 = NotePitch('G', '' , 4) 42 | A_4 = NotePitch('A', '' , 4) 43 | B_4 = NotePitch('B', '' , 4) 44 | 45 | C_5 = NotePitch('C', '' , 5) 46 | D_5 = NotePitch('D', '' , 5) 47 | E_5 = NotePitch('E', '' , 5) 48 | F_5 = NotePitch('F', '' , 5) 49 | G_5 = NotePitch('G', '' , 5) 50 | A_5 = NotePitch('A', '' , 5) 51 | B_5 = NotePitch('B', '' , 5) 52 | 53 | C_6 = NotePitch('C', '' , 6) 54 | D_6 = NotePitch('D', '' , 6) 55 | E_6 = NotePitch('E', '' , 6) 56 | F_6 = NotePitch('F', '' , 6) 57 | G_6 = NotePitch('G', '' , 6) 58 | A_6 = NotePitch('A', '' , 6) 59 | B_6 = NotePitch('B', '' , 6) 60 | 61 | C_7 = NotePitch('C', '' , 7) 62 | D_7 = NotePitch('D', '' , 7) 63 | E_7 = NotePitch('E', '' , 7) 64 | F_7 = NotePitch('F', '' , 7) 65 | G_7 = NotePitch('G', '' , 7) 66 | A_7 = NotePitch('A', '' , 7) 67 | B_7 = NotePitch('B', '' , 7) 68 | 69 | C_8 = NotePitch('C', '' , 8) 70 | D_8 = NotePitch('D', '' , 8) 71 | E_8 = NotePitch('E', '' , 8) 72 | F_8 = NotePitch('F', '' , 8) 73 | G_8 = NotePitch('G', '' , 8) 74 | A_8 = NotePitch('A', '' , 8) 75 | B_8 = NotePitch('B', '' , 8) 76 | 77 | C_9 = NotePitch('C', '' , 9) 78 | D_9 = NotePitch('D', '' , 9) 79 | E_9 = NotePitch('E', '' , 9) 80 | F_9 = NotePitch('F', '' , 9) 81 | G_9 = NotePitch('G', '' , 9) 82 | A_9 = NotePitch('A', '' , 9) 83 | B_9 = NotePitch('B', '' , 9) 84 | -------------------------------------------------------------------------------- /kord/parsers/pitch_parser.py: -------------------------------------------------------------------------------- 1 | 2 | from ..notes import NotePitch 3 | from ..keys.chords import * 4 | 5 | from ..errors import InvalidNote, InvalidAlteration, InvalidOctave, InvalidChord 6 | 7 | from bestia.output import echo 8 | 9 | __all__ = [ 10 | 'NotePitchParser', 11 | ] 12 | 13 | 14 | class NotePitchParser: 15 | 16 | def __init__(self, symbol): 17 | self.symbol = symbol 18 | self.reset() 19 | 20 | 21 | def reset(self): 22 | self.to_parse = self.symbol.strip() 23 | 24 | 25 | def _parse_char(self): 26 | char = NotePitch.validate_char(self.to_parse[0]) 27 | self.to_parse = self.to_parse[1:] 28 | return char 29 | 30 | 31 | def _parse_oct(self): 32 | if len(self.to_parse) > 1: 33 | # use str.isdigit() 34 | try: 35 | # this being an int means octave > MAXIMUM_OCTAVE 36 | octave = int(self.to_parse[-2]) 37 | octave_over_max = True 38 | except ValueError: 39 | # probably an alt 40 | octave_over_max = False 41 | finally: 42 | if octave_over_max: 43 | raise InvalidOctave(self.symbol) 44 | 45 | try: 46 | # this being an int means this is an octave 47 | octave = int(self.to_parse[-1]) 48 | self.to_parse = self.to_parse[:-1] 49 | except ValueError: 50 | # probably an alt 51 | octave = NotePitch.DEFAULT_OCTAVE 52 | finally: 53 | return octave 54 | 55 | 56 | def _parse_alts(self): 57 | self.to_parse = self.to_parse.replace('𝄫', 'bb') 58 | self.to_parse = self.to_parse.replace('♭', 'b') 59 | self.to_parse = self.to_parse.replace('𝄪', '##') 60 | self.to_parse = self.to_parse.replace('♯', '#') 61 | 62 | if self.to_parse not in NotePitch.input_alterations(): 63 | raise InvalidAlteration(self.symbol) 64 | 65 | alts = self.to_parse 66 | self.to_parse = self.to_parse[len(self.to_parse):] 67 | return alts 68 | 69 | 70 | def parse(self): 71 | if len(self.to_parse) == 0: 72 | raise InvalidNote(self.symbol) 73 | 74 | char = self._parse_char() 75 | if len(self.to_parse) == 0: 76 | self.reset() 77 | return NotePitch(char) 78 | 79 | octave = self._parse_oct() 80 | if len(self.to_parse) == 0: 81 | self.reset() 82 | return NotePitch(char, octave) 83 | 84 | alts = self._parse_alts() 85 | if len(self.to_parse) == 0: 86 | self.reset() 87 | return NotePitch(char, alts, octave) 88 | 89 | raise InvalidNote(self.symbol) 90 | 91 | -------------------------------------------------------------------------------- /kord/notes/constants/flats.py: -------------------------------------------------------------------------------- 1 | 2 | from ..pitch import NotePitch 3 | 4 | 5 | C_FLAT_0 = NotePitch('C', 'b' , 0) 6 | D_FLAT_0 = NotePitch('D', 'b' , 0) 7 | E_FLAT_0 = NotePitch('E', 'b' , 0) 8 | F_FLAT_0 = NotePitch('F', 'b' , 0) 9 | G_FLAT_0 = NotePitch('G', 'b' , 0) 10 | A_FLAT_0 = NotePitch('A', 'b' , 0) 11 | B_FLAT_0 = NotePitch('B', 'b' , 0) 12 | 13 | C_FLAT_1 = NotePitch('C', 'b' , 1) 14 | D_FLAT_1 = NotePitch('D', 'b' , 1) 15 | E_FLAT_1 = NotePitch('E', 'b' , 1) 16 | F_FLAT_1 = NotePitch('F', 'b' , 1) 17 | G_FLAT_1 = NotePitch('G', 'b' , 1) 18 | A_FLAT_1 = NotePitch('A', 'b' , 1) 19 | B_FLAT_1 = NotePitch('B', 'b' , 1) 20 | 21 | C_FLAT_2 = NotePitch('C', 'b' , 2) 22 | D_FLAT_2 = NotePitch('D', 'b' , 2) 23 | E_FLAT_2 = NotePitch('E', 'b' , 2) 24 | F_FLAT_2 = NotePitch('F', 'b' , 2) 25 | G_FLAT_2 = NotePitch('G', 'b' , 2) 26 | A_FLAT_2 = NotePitch('A', 'b' , 2) 27 | B_FLAT_2 = NotePitch('B', 'b' , 2) 28 | 29 | C_FLAT_3 = NotePitch('C', 'b' , 3) 30 | D_FLAT_3 = NotePitch('D', 'b' , 3) 31 | E_FLAT_3 = NotePitch('E', 'b' , 3) 32 | F_FLAT_3 = NotePitch('F', 'b' , 3) 33 | G_FLAT_3 = NotePitch('G', 'b' , 3) 34 | A_FLAT_3 = NotePitch('A', 'b' , 3) 35 | B_FLAT_3 = NotePitch('B', 'b' , 3) 36 | 37 | C_FLAT_4 = NotePitch('C', 'b' , 4) 38 | D_FLAT_4 = NotePitch('D', 'b' , 4) 39 | E_FLAT_4 = NotePitch('E', 'b' , 4) 40 | F_FLAT_4 = NotePitch('F', 'b' , 4) 41 | G_FLAT_4 = NotePitch('G', 'b' , 4) 42 | A_FLAT_4 = NotePitch('A', 'b' , 4) 43 | B_FLAT_4 = NotePitch('B', 'b' , 4) 44 | 45 | C_FLAT_5 = NotePitch('C', 'b' , 5) 46 | D_FLAT_5 = NotePitch('D', 'b' , 5) 47 | E_FLAT_5 = NotePitch('E', 'b' , 5) 48 | F_FLAT_5 = NotePitch('F', 'b' , 5) 49 | G_FLAT_5 = NotePitch('G', 'b' , 5) 50 | A_FLAT_5 = NotePitch('A', 'b' , 5) 51 | B_FLAT_5 = NotePitch('B', 'b' , 5) 52 | 53 | C_FLAT_6 = NotePitch('C', 'b' , 6) 54 | D_FLAT_6 = NotePitch('D', 'b' , 6) 55 | E_FLAT_6 = NotePitch('E', 'b' , 6) 56 | F_FLAT_6 = NotePitch('F', 'b' , 6) 57 | G_FLAT_6 = NotePitch('G', 'b' , 6) 58 | A_FLAT_6 = NotePitch('A', 'b' , 6) 59 | B_FLAT_6 = NotePitch('B', 'b' , 6) 60 | 61 | C_FLAT_7 = NotePitch('C', 'b' , 7) 62 | D_FLAT_7 = NotePitch('D', 'b' , 7) 63 | E_FLAT_7 = NotePitch('E', 'b' , 7) 64 | F_FLAT_7 = NotePitch('F', 'b' , 7) 65 | G_FLAT_7 = NotePitch('G', 'b' , 7) 66 | A_FLAT_7 = NotePitch('A', 'b' , 7) 67 | B_FLAT_7 = NotePitch('B', 'b' , 7) 68 | 69 | C_FLAT_8 = NotePitch('C', 'b' , 8) 70 | D_FLAT_8 = NotePitch('D', 'b' , 8) 71 | E_FLAT_8 = NotePitch('E', 'b' , 8) 72 | F_FLAT_8 = NotePitch('F', 'b' , 8) 73 | G_FLAT_8 = NotePitch('G', 'b' , 8) 74 | A_FLAT_8 = NotePitch('A', 'b' , 8) 75 | B_FLAT_8 = NotePitch('B', 'b' , 8) 76 | 77 | C_FLAT_9 = NotePitch('C', 'b' , 9) 78 | D_FLAT_9 = NotePitch('D', 'b' , 9) 79 | E_FLAT_9 = NotePitch('E', 'b' , 9) 80 | F_FLAT_9 = NotePitch('F', 'b' , 9) 81 | G_FLAT_9 = NotePitch('G', 'b' , 9) 82 | A_FLAT_9 = NotePitch('A', 'b' , 9) 83 | B_FLAT_9 = NotePitch('B', 'b' , 9) 84 | 85 | -------------------------------------------------------------------------------- /kord/notes/constants/sharps.py: -------------------------------------------------------------------------------- 1 | 2 | from ..pitch import NotePitch 3 | 4 | 5 | C_SHARP_0 = NotePitch('C', '#' , 0) 6 | D_SHARP_0 = NotePitch('D', '#' , 0) 7 | E_SHARP_0 = NotePitch('E', '#' , 0) 8 | F_SHARP_0 = NotePitch('F', '#' , 0) 9 | G_SHARP_0 = NotePitch('G', '#' , 0) 10 | A_SHARP_0 = NotePitch('A', '#' , 0) 11 | B_SHARP_0 = NotePitch('B', '#' , 0) 12 | 13 | C_SHARP_1 = NotePitch('C', '#' , 1) 14 | D_SHARP_1 = NotePitch('D', '#' , 1) 15 | E_SHARP_1 = NotePitch('E', '#' , 1) 16 | F_SHARP_1 = NotePitch('F', '#' , 1) 17 | G_SHARP_1 = NotePitch('G', '#' , 1) 18 | A_SHARP_1 = NotePitch('A', '#' , 1) 19 | B_SHARP_1 = NotePitch('B', '#' , 1) 20 | 21 | C_SHARP_2 = NotePitch('C', '#' , 2) 22 | D_SHARP_2 = NotePitch('D', '#' , 2) 23 | E_SHARP_2 = NotePitch('E', '#' , 2) 24 | F_SHARP_2 = NotePitch('F', '#' , 2) 25 | G_SHARP_2 = NotePitch('G', '#' , 2) 26 | A_SHARP_2 = NotePitch('A', '#' , 2) 27 | B_SHARP_2 = NotePitch('B', '#' , 2) 28 | 29 | C_SHARP_3 = NotePitch('C', '#' , 3) 30 | D_SHARP_3 = NotePitch('D', '#' , 3) 31 | E_SHARP_3 = NotePitch('E', '#' , 3) 32 | F_SHARP_3 = NotePitch('F', '#' , 3) 33 | G_SHARP_3 = NotePitch('G', '#' , 3) 34 | A_SHARP_3 = NotePitch('A', '#' , 3) 35 | B_SHARP_3 = NotePitch('B', '#' , 3) 36 | 37 | C_SHARP_4 = NotePitch('C', '#' , 4) 38 | D_SHARP_4 = NotePitch('D', '#' , 4) 39 | E_SHARP_4 = NotePitch('E', '#' , 4) 40 | F_SHARP_4 = NotePitch('F', '#' , 4) 41 | G_SHARP_4 = NotePitch('G', '#' , 4) 42 | A_SHARP_4 = NotePitch('A', '#' , 4) 43 | B_SHARP_4 = NotePitch('B', '#' , 4) 44 | 45 | C_SHARP_5 = NotePitch('C', '#' , 5) 46 | D_SHARP_5 = NotePitch('D', '#' , 5) 47 | E_SHARP_5 = NotePitch('E', '#' , 5) 48 | F_SHARP_5 = NotePitch('F', '#' , 5) 49 | G_SHARP_5 = NotePitch('G', '#' , 5) 50 | A_SHARP_5 = NotePitch('A', '#' , 5) 51 | B_SHARP_5 = NotePitch('B', '#' , 5) 52 | 53 | C_SHARP_6 = NotePitch('C', '#' , 6) 54 | D_SHARP_6 = NotePitch('D', '#' , 6) 55 | E_SHARP_6 = NotePitch('E', '#' , 6) 56 | F_SHARP_6 = NotePitch('F', '#' , 6) 57 | G_SHARP_6 = NotePitch('G', '#' , 6) 58 | A_SHARP_6 = NotePitch('A', '#' , 6) 59 | B_SHARP_6 = NotePitch('B', '#' , 6) 60 | 61 | C_SHARP_7 = NotePitch('C', '#' , 7) 62 | D_SHARP_7 = NotePitch('D', '#' , 7) 63 | E_SHARP_7 = NotePitch('E', '#' , 7) 64 | F_SHARP_7 = NotePitch('F', '#' , 7) 65 | G_SHARP_7 = NotePitch('G', '#' , 7) 66 | A_SHARP_7 = NotePitch('A', '#' , 7) 67 | B_SHARP_7 = NotePitch('B', '#' , 7) 68 | 69 | C_SHARP_8 = NotePitch('C', '#' , 8) 70 | D_SHARP_8 = NotePitch('D', '#' , 8) 71 | E_SHARP_8 = NotePitch('E', '#' , 8) 72 | F_SHARP_8 = NotePitch('F', '#' , 8) 73 | G_SHARP_8 = NotePitch('G', '#' , 8) 74 | A_SHARP_8 = NotePitch('A', '#' , 8) 75 | B_SHARP_8 = NotePitch('B', '#' , 8) 76 | 77 | C_SHARP_9 = NotePitch('C', '#' , 9) 78 | D_SHARP_9 = NotePitch('D', '#' , 9) 79 | E_SHARP_9 = NotePitch('E', '#' , 9) 80 | F_SHARP_9 = NotePitch('F', '#' , 9) 81 | G_SHARP_9 = NotePitch('G', '#' , 9) 82 | A_SHARP_9 = NotePitch('A', '#' , 9) 83 | B_SHARP_9 = NotePitch('B', '#' , 9) 84 | -------------------------------------------------------------------------------- /kord/notes/constants/double_flats.py: -------------------------------------------------------------------------------- 1 | 2 | from ..pitch import NotePitch 3 | 4 | 5 | C_DOUBLE_FLAT_0 = NotePitch('C', 'bb' , 0) 6 | D_DOUBLE_FLAT_0 = NotePitch('D', 'bb' , 0) 7 | E_DOUBLE_FLAT_0 = NotePitch('E', 'bb' , 0) 8 | F_DOUBLE_FLAT_0 = NotePitch('F', 'bb' , 0) 9 | G_DOUBLE_FLAT_0 = NotePitch('G', 'bb' , 0) 10 | A_DOUBLE_FLAT_0 = NotePitch('A', 'bb' , 0) 11 | B_DOUBLE_FLAT_0 = NotePitch('B', 'bb' , 0) 12 | 13 | C_DOUBLE_FLAT_1 = NotePitch('C', 'bb' , 1) 14 | D_DOUBLE_FLAT_1 = NotePitch('D', 'bb' , 1) 15 | E_DOUBLE_FLAT_1 = NotePitch('E', 'bb' , 1) 16 | F_DOUBLE_FLAT_1 = NotePitch('F', 'bb' , 1) 17 | G_DOUBLE_FLAT_1 = NotePitch('G', 'bb' , 1) 18 | A_DOUBLE_FLAT_1 = NotePitch('A', 'bb' , 1) 19 | B_DOUBLE_FLAT_1 = NotePitch('B', 'bb' , 1) 20 | 21 | C_DOUBLE_FLAT_2 = NotePitch('C', 'bb' , 2) 22 | D_DOUBLE_FLAT_2 = NotePitch('D', 'bb' , 2) 23 | E_DOUBLE_FLAT_2 = NotePitch('E', 'bb' , 2) 24 | F_DOUBLE_FLAT_2 = NotePitch('F', 'bb' , 2) 25 | G_DOUBLE_FLAT_2 = NotePitch('G', 'bb' , 2) 26 | A_DOUBLE_FLAT_2 = NotePitch('A', 'bb' , 2) 27 | B_DOUBLE_FLAT_2 = NotePitch('B', 'bb' , 2) 28 | 29 | C_DOUBLE_FLAT_3 = NotePitch('C', 'bb' , 3) 30 | D_DOUBLE_FLAT_3 = NotePitch('D', 'bb' , 3) 31 | E_DOUBLE_FLAT_3 = NotePitch('E', 'bb' , 3) 32 | F_DOUBLE_FLAT_3 = NotePitch('F', 'bb' , 3) 33 | G_DOUBLE_FLAT_3 = NotePitch('G', 'bb' , 3) 34 | A_DOUBLE_FLAT_3 = NotePitch('A', 'bb' , 3) 35 | B_DOUBLE_FLAT_3 = NotePitch('B', 'bb' , 3) 36 | 37 | C_DOUBLE_FLAT_4 = NotePitch('C', 'bb' , 4) 38 | D_DOUBLE_FLAT_4 = NotePitch('D', 'bb' , 4) 39 | E_DOUBLE_FLAT_4 = NotePitch('E', 'bb' , 4) 40 | F_DOUBLE_FLAT_4 = NotePitch('F', 'bb' , 4) 41 | G_DOUBLE_FLAT_4 = NotePitch('G', 'bb' , 4) 42 | A_DOUBLE_FLAT_4 = NotePitch('A', 'bb' , 4) 43 | B_DOUBLE_FLAT_4 = NotePitch('B', 'bb' , 4) 44 | 45 | C_DOUBLE_FLAT_5 = NotePitch('C', 'bb' , 5) 46 | D_DOUBLE_FLAT_5 = NotePitch('D', 'bb' , 5) 47 | E_DOUBLE_FLAT_5 = NotePitch('E', 'bb' , 5) 48 | F_DOUBLE_FLAT_5 = NotePitch('F', 'bb' , 5) 49 | G_DOUBLE_FLAT_5 = NotePitch('G', 'bb' , 5) 50 | A_DOUBLE_FLAT_5 = NotePitch('A', 'bb' , 5) 51 | B_DOUBLE_FLAT_5 = NotePitch('B', 'bb' , 5) 52 | 53 | C_DOUBLE_FLAT_6 = NotePitch('C', 'bb' , 6) 54 | D_DOUBLE_FLAT_6 = NotePitch('D', 'bb' , 6) 55 | E_DOUBLE_FLAT_6 = NotePitch('E', 'bb' , 6) 56 | F_DOUBLE_FLAT_6 = NotePitch('F', 'bb' , 6) 57 | G_DOUBLE_FLAT_6 = NotePitch('G', 'bb' , 6) 58 | A_DOUBLE_FLAT_6 = NotePitch('A', 'bb' , 6) 59 | B_DOUBLE_FLAT_6 = NotePitch('B', 'bb' , 6) 60 | 61 | C_DOUBLE_FLAT_7 = NotePitch('C', 'bb' , 7) 62 | D_DOUBLE_FLAT_7 = NotePitch('D', 'bb' , 7) 63 | E_DOUBLE_FLAT_7 = NotePitch('E', 'bb' , 7) 64 | F_DOUBLE_FLAT_7 = NotePitch('F', 'bb' , 7) 65 | G_DOUBLE_FLAT_7 = NotePitch('G', 'bb' , 7) 66 | A_DOUBLE_FLAT_7 = NotePitch('A', 'bb' , 7) 67 | B_DOUBLE_FLAT_7 = NotePitch('B', 'bb' , 7) 68 | 69 | C_DOUBLE_FLAT_8 = NotePitch('C', 'bb' , 8) 70 | D_DOUBLE_FLAT_8 = NotePitch('D', 'bb' , 8) 71 | E_DOUBLE_FLAT_8 = NotePitch('E', 'bb' , 8) 72 | F_DOUBLE_FLAT_8 = NotePitch('F', 'bb' , 8) 73 | G_DOUBLE_FLAT_8 = NotePitch('G', 'bb' , 8) 74 | A_DOUBLE_FLAT_8 = NotePitch('A', 'bb' , 8) 75 | B_DOUBLE_FLAT_8 = NotePitch('B', 'bb' , 8) 76 | 77 | C_DOUBLE_FLAT_9 = NotePitch('C', 'bb' , 9) 78 | D_DOUBLE_FLAT_9 = NotePitch('D', 'bb' , 9) 79 | E_DOUBLE_FLAT_9 = NotePitch('E', 'bb' , 9) 80 | F_DOUBLE_FLAT_9 = NotePitch('F', 'bb' , 9) 81 | G_DOUBLE_FLAT_9 = NotePitch('G', 'bb' , 9) 82 | A_DOUBLE_FLAT_9 = NotePitch('A', 'bb' , 9) 83 | B_DOUBLE_FLAT_9 = NotePitch('B', 'bb' , 9) 84 | -------------------------------------------------------------------------------- /kord/notes/constants/double_sharps.py: -------------------------------------------------------------------------------- 1 | 2 | from ..pitch import NotePitch 3 | 4 | 5 | C_DOUBLE_SHARP_0 = NotePitch('C', '##' , 0) 6 | D_DOUBLE_SHARP_0 = NotePitch('D', '##' , 0) 7 | E_DOUBLE_SHARP_0 = NotePitch('E', '##' , 0) 8 | F_DOUBLE_SHARP_0 = NotePitch('F', '##' , 0) 9 | G_DOUBLE_SHARP_0 = NotePitch('G', '##' , 0) 10 | A_DOUBLE_SHARP_0 = NotePitch('A', '##' , 0) 11 | B_DOUBLE_SHARP_0 = NotePitch('B', '##' , 0) 12 | 13 | C_DOUBLE_SHARP_1 = NotePitch('C', '##' , 1) 14 | D_DOUBLE_SHARP_1 = NotePitch('D', '##' , 1) 15 | E_DOUBLE_SHARP_1 = NotePitch('E', '##' , 1) 16 | F_DOUBLE_SHARP_1 = NotePitch('F', '##' , 1) 17 | G_DOUBLE_SHARP_1 = NotePitch('G', '##' , 1) 18 | A_DOUBLE_SHARP_1 = NotePitch('A', '##' , 1) 19 | B_DOUBLE_SHARP_1 = NotePitch('B', '##' , 1) 20 | 21 | C_DOUBLE_SHARP_2 = NotePitch('C', '##' , 2) 22 | D_DOUBLE_SHARP_2 = NotePitch('D', '##' , 2) 23 | E_DOUBLE_SHARP_2 = NotePitch('E', '##' , 2) 24 | F_DOUBLE_SHARP_2 = NotePitch('F', '##' , 2) 25 | G_DOUBLE_SHARP_2 = NotePitch('G', '##' , 2) 26 | A_DOUBLE_SHARP_2 = NotePitch('A', '##' , 2) 27 | B_DOUBLE_SHARP_2 = NotePitch('B', '##' , 2) 28 | 29 | C_DOUBLE_SHARP_3 = NotePitch('C', '##' , 3) 30 | D_DOUBLE_SHARP_3 = NotePitch('D', '##' , 3) 31 | E_DOUBLE_SHARP_3 = NotePitch('E', '##' , 3) 32 | F_DOUBLE_SHARP_3 = NotePitch('F', '##' , 3) 33 | G_DOUBLE_SHARP_3 = NotePitch('G', '##' , 3) 34 | A_DOUBLE_SHARP_3 = NotePitch('A', '##' , 3) 35 | B_DOUBLE_SHARP_3 = NotePitch('B', '##' , 3) 36 | 37 | C_DOUBLE_SHARP_4 = NotePitch('C', '##' , 4) 38 | D_DOUBLE_SHARP_4 = NotePitch('D', '##' , 4) 39 | E_DOUBLE_SHARP_4 = NotePitch('E', '##' , 4) 40 | F_DOUBLE_SHARP_4 = NotePitch('F', '##' , 4) 41 | G_DOUBLE_SHARP_4 = NotePitch('G', '##' , 4) 42 | A_DOUBLE_SHARP_4 = NotePitch('A', '##' , 4) 43 | B_DOUBLE_SHARP_4 = NotePitch('B', '##' , 4) 44 | 45 | C_DOUBLE_SHARP_5 = NotePitch('C', '##' , 5) 46 | D_DOUBLE_SHARP_5 = NotePitch('D', '##' , 5) 47 | E_DOUBLE_SHARP_5 = NotePitch('E', '##' , 5) 48 | F_DOUBLE_SHARP_5 = NotePitch('F', '##' , 5) 49 | G_DOUBLE_SHARP_5 = NotePitch('G', '##' , 5) 50 | A_DOUBLE_SHARP_5 = NotePitch('A', '##' , 5) 51 | B_DOUBLE_SHARP_5 = NotePitch('B', '##' , 5) 52 | 53 | C_DOUBLE_SHARP_6 = NotePitch('C', '##' , 6) 54 | D_DOUBLE_SHARP_6 = NotePitch('D', '##' , 6) 55 | E_DOUBLE_SHARP_6 = NotePitch('E', '##' , 6) 56 | F_DOUBLE_SHARP_6 = NotePitch('F', '##' , 6) 57 | G_DOUBLE_SHARP_6 = NotePitch('G', '##' , 6) 58 | A_DOUBLE_SHARP_6 = NotePitch('A', '##' , 6) 59 | B_DOUBLE_SHARP_6 = NotePitch('B', '##' , 6) 60 | 61 | C_DOUBLE_SHARP_7 = NotePitch('C', '##' , 7) 62 | D_DOUBLE_SHARP_7 = NotePitch('D', '##' , 7) 63 | E_DOUBLE_SHARP_7 = NotePitch('E', '##' , 7) 64 | F_DOUBLE_SHARP_7 = NotePitch('F', '##' , 7) 65 | G_DOUBLE_SHARP_7 = NotePitch('G', '##' , 7) 66 | A_DOUBLE_SHARP_7 = NotePitch('A', '##' , 7) 67 | B_DOUBLE_SHARP_7 = NotePitch('B', '##' , 7) 68 | 69 | C_DOUBLE_SHARP_8 = NotePitch('C', '##' , 8) 70 | D_DOUBLE_SHARP_8 = NotePitch('D', '##' , 8) 71 | E_DOUBLE_SHARP_8 = NotePitch('E', '##' , 8) 72 | F_DOUBLE_SHARP_8 = NotePitch('F', '##' , 8) 73 | G_DOUBLE_SHARP_8 = NotePitch('G', '##' , 8) 74 | A_DOUBLE_SHARP_8 = NotePitch('A', '##' , 8) 75 | B_DOUBLE_SHARP_8 = NotePitch('B', '##' , 8) 76 | 77 | C_DOUBLE_SHARP_9 = NotePitch('C', '##' , 9) 78 | D_DOUBLE_SHARP_9 = NotePitch('D', '##' , 9) 79 | E_DOUBLE_SHARP_9 = NotePitch('E', '##' , 9) 80 | F_DOUBLE_SHARP_9 = NotePitch('F', '##' , 9) 81 | G_DOUBLE_SHARP_9 = NotePitch('G', '##' , 9) 82 | A_DOUBLE_SHARP_9 = NotePitch('A', '##' , 9) 83 | B_DOUBLE_SHARP_9 = NotePitch('B', '##' , 9) 84 | -------------------------------------------------------------------------------- /kord/notes/test_notes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from . import NotePitch 4 | 5 | from .constants import ( 6 | F_4, B_4, B_5, 7 | C_FLAT_3, C_FLAT_4, D_FLAT_4, B_FLAT_4, E_FLAT_5, D_FLAT_3, 8 | E_SHARP_3, B_SHARP_3, D_SHARP_4, C_SHARP_5, C_SHARP_3, 9 | D_DOUBLE_FLAT_4, 10 | ) 11 | 12 | from ..errors import InvalidNote 13 | 14 | __all__ = [ 15 | 'TestInvalidNotes', 16 | 'NoteEqualityTest', 17 | ] 18 | 19 | 20 | class TestInvalidNotes(unittest.TestCase): 21 | 22 | def testInvalidChars(self): 23 | self.assertRaises( 24 | InvalidNote, lambda : NotePitch('Y') 25 | ) 26 | 27 | 28 | class NoteEqualityTest(unittest.TestCase): 29 | 30 | DANGEROUS_NON_EQUALS = ( 31 | # ''' Used mainly to test B#, Cb, etc... ''' 32 | 33 | (C_FLAT_3, B_SHARP_3), 34 | (E_FLAT_5, D_SHARP_4), 35 | 36 | (B_5, C_FLAT_4), 37 | 38 | (E_SHARP_3, F_4), 39 | 40 | 41 | (NotePitch('B', 'b'), NotePitch('C', 'bb')), 42 | (NotePitch('A', '#'), NotePitch('C', 'bb')), 43 | 44 | (NotePitch('B' ), NotePitch('C', 'b')), 45 | (NotePitch('A', '##'), NotePitch('C', 'b')), 46 | 47 | (NotePitch('B', '#'), NotePitch('C', '')), 48 | (NotePitch('B', '#'), NotePitch('D', 'bb')), 49 | 50 | (NotePitch('B', '##'), NotePitch('C', '#')), 51 | (NotePitch('B', '##'), NotePitch('D', 'b')), 52 | 53 | 54 | # these should eval False OK 55 | (NotePitch('A', '#'), B_FLAT_4), 56 | (NotePitch('A', '##'), B_4), 57 | (NotePitch('C', ''), D_DOUBLE_FLAT_4), 58 | (NotePitch('C', '#'), D_FLAT_4), 59 | ) 60 | 61 | def setUp(self): 62 | print() 63 | 64 | def testNotEnharmonic(self): 65 | for note_pair in self.DANGEROUS_NON_EQUALS: 66 | assert not note_pair[0] >> note_pair[1], (note_pair[0], note_pair[1]) 67 | assert note_pair[0] - note_pair[1] != 0, (note_pair[0], note_pair[1]) 68 | 69 | 70 | def testAreEnharmonic(self): 71 | ''' checks note_pairs in enharmonic rows for different types of enharmonic equality ''' 72 | for note_pair in NotePitch.EnharmonicMatrix(): 73 | 74 | # PASS ENHARMONIC EQUALITY 75 | assert note_pair[0] == note_pair[1], (note_pair[0], note_pair[1]) 76 | 77 | # DO NOT PASS NOTE EQUALITY 78 | assert not note_pair[0] >> note_pair[1], (note_pair[0], note_pair[1]) 79 | assert not note_pair[0] ** note_pair[1], (note_pair[0], note_pair[1]) 80 | 81 | # CHECK DELTA_ST == 0 82 | assert not note_pair[0] < note_pair[1], (note_pair[0], note_pair[1]) 83 | assert not note_pair[0] > note_pair[1], (note_pair[0], note_pair[1]) 84 | assert note_pair[0] <= note_pair[1], (note_pair[0], note_pair[1]) 85 | assert note_pair[0] >= note_pair[1], (note_pair[0], note_pair[1]) 86 | assert note_pair[0] - note_pair[1] == 0, (note_pair[0], note_pair[1]) 87 | 88 | 89 | def testEnharmonicOperators(self): 90 | ''' ''' 91 | print('Testing enharmonic operators') 92 | assert C_SHARP_3 ** C_SHARP_5 # loosest equality 93 | assert C_SHARP_3 == D_FLAT_3 # enhamonic notes 94 | assert not D_FLAT_3 >> C_SHARP_3 # enh note 95 | assert not C_SHARP_3 >> D_FLAT_3 # enh note 96 | 97 | # ** same note, ignr oct 98 | # == enhr note, same oct 99 | # >> same note, same oct 100 | 101 | # // enhr note, ignr oct do I need to implement this operator ? 102 | -------------------------------------------------------------------------------- /kord/parsers/test_pitch_parser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from .pitch_parser import NotePitchParser 4 | 5 | from ..notes import NotePitch 6 | 7 | from ..notes.constants import ( 8 | C_3, F_3, E_3, A_3, D_3, G_3, B_3, 9 | D_SHARP_3, E_FLAT_3, C_DOUBLE_SHARP_3, G_DOUBLE_FLAT_3, 10 | ) 11 | 12 | from ..errors import InvalidNote, InvalidAlteration, InvalidOctave 13 | 14 | __all__ = [ 15 | 'NotePitchParserTest', 16 | ] 17 | 18 | class NotePitchParserTest(unittest.TestCase): 19 | 20 | CHAR_WINS = [ 21 | ['C', C_3], 22 | ['D', D_3], 23 | ['E', E_3], 24 | ['F', F_3], 25 | ['G', G_3], 26 | ['A', A_3], 27 | ['B', B_3], 28 | 29 | ['c', C_3], 30 | ['d', D_3], 31 | ['e', E_3], 32 | ['f', F_3], 33 | ['g', G_3], 34 | ['a', A_3], 35 | ['b', B_3], 36 | ] 37 | 38 | CHAR_FAILS = [ 39 | '', 40 | 'H', 41 | 'T', 42 | 'Y', 43 | 'h', 44 | ] 45 | 46 | OCTAVE_WINS = [ 47 | [ f'C{octave}', NotePitch('C', octave) ] for octave 48 | in range(0, NotePitch.MAXIMUM_OCTAVE) 49 | ] 50 | 51 | OCTAVE_FAILS = [ 52 | f'A{octave}' for octave 53 | in range(NotePitch.MAXIMUM_OCTAVE+1, NotePitch.MAXIMUM_OCTAVE+500) 54 | ] 55 | 56 | ALTS_WINS = [ 57 | ['D#', D_SHARP_3], 58 | ['D♯', D_SHARP_3], 59 | 60 | ['C##', C_DOUBLE_SHARP_3], 61 | ['C♯♯', C_DOUBLE_SHARP_3], 62 | ['C#♯', C_DOUBLE_SHARP_3], 63 | ['C♯#', C_DOUBLE_SHARP_3], 64 | ['C𝄪', C_DOUBLE_SHARP_3], 65 | 66 | ['Eb', E_FLAT_3], 67 | ['E♭', E_FLAT_3], 68 | 69 | ['Gbb', G_DOUBLE_FLAT_3], 70 | ['G♭♭', G_DOUBLE_FLAT_3], 71 | ['Gb♭', G_DOUBLE_FLAT_3], 72 | ['G♭b', G_DOUBLE_FLAT_3], 73 | ['G𝄫', G_DOUBLE_FLAT_3], 74 | ] 75 | 76 | ALTS_FAILS = [ 77 | 'Abbb', 78 | 'A###', 79 | 'Bbr6', 80 | 'Ce#', 81 | 82 | 'D𝄫b', 83 | 'D𝄫♭', 84 | 85 | 'G𝄪#', 86 | 'G𝄪♯', 87 | 88 | 'Dsharp', 89 | 'eSharp', 90 | 'Eflat', 91 | 'bFlat', 92 | 93 | 'CB', 94 | 'Cqwe', 95 | ] 96 | 97 | def setUp(self): 98 | print() 99 | 100 | def testChars(self): 101 | for symbol, expected in self.CHAR_WINS: 102 | parser = NotePitchParser(symbol) 103 | parsed_note = parser.parse() 104 | self.assertEqual(parsed_note, expected) 105 | 106 | def testCharFails(self): 107 | for symbol in self.CHAR_FAILS: 108 | parser = NotePitchParser(symbol) 109 | self.assertRaises(InvalidNote, parser.parse) 110 | 111 | def testOctaves(self): 112 | for symbol, expected in self.OCTAVE_WINS: 113 | parser = NotePitchParser(symbol) 114 | parsed_note = parser.parse() 115 | self.assertEqual(parsed_note, expected) 116 | 117 | def testOctaveFails(self): 118 | for symbol in self.OCTAVE_FAILS: 119 | parser = NotePitchParser(symbol) 120 | self.assertRaises(InvalidOctave, parser.parse) 121 | 122 | def testAlterations(self): 123 | for symbol, expected in self.ALTS_WINS: 124 | parser = NotePitchParser(symbol) 125 | parsed_note = parser.parse() 126 | self.assertEqual(parsed_note, expected) 127 | 128 | def testAlterationFails(self): 129 | for symbol in self.ALTS_FAILS: 130 | parser = NotePitchParser(symbol) 131 | self.assertRaises(InvalidAlteration, parser.parse) 132 | -------------------------------------------------------------------------------- /kord/parsers/chord_parser.py: -------------------------------------------------------------------------------- 1 | 2 | from .pitch_parser import NotePitchParser 3 | 4 | from ..notes import NotePitch 5 | from ..keys.chords import * 6 | 7 | from ..errors import InvalidNote, InvalidAlteration, InvalidOctave, InvalidChord 8 | 9 | from bestia.output import echo 10 | 11 | __all__ = [ 12 | 'ChordParser', 13 | ] 14 | 15 | 16 | class ChordParser: 17 | ''' chord symbols still left to add: 18 | 19 | Fadd4 20 | 21 | Am11 22 | 23 | G13 24 | B7b9 25 | ''' 26 | 27 | RECOGNIZED_CHORDS = ( 28 | PowerChord, Suspended4Chord, Suspended2Chord, 29 | MajorTriad, MinorTriad, AugmentedTriad, DiminishedTriad, 30 | MajorSixthChord, MinorSixthChord, 31 | MajorSeventhChord, MinorSeventhChord, DominantSeventhChord, 32 | HalfDiminishedSeventhChord, DiminishedSeventhChord, 33 | MajorAdd9Chord, MinorAdd9Chord, AugmentedAdd9Chord, DiminishedAdd9Chord, 34 | DominantNinthChord, DominantMinorNinthChord, 35 | MajorNinthChord, MinorNinthChord, 36 | ) 37 | 38 | BASS_NOTE_SEP = '/' 39 | 40 | def __init__(self, symbol): 41 | self.symbol = symbol.strip() 42 | self.reset() 43 | 44 | 45 | def reset(self): 46 | self.root = None 47 | self.flavor = None 48 | self.to_parse = self.symbol.replace(' ', '') # 'B aug7' 49 | 50 | 51 | @property 52 | def is_inverted(self) -> bool: 53 | return self.BASS_NOTE_SEP in self.symbol 54 | 55 | @property 56 | def bass(self): 57 | if not self.is_inverted: 58 | return self.root 59 | return NotePitchParser( 60 | self.symbol.split(self.BASS_NOTE_SEP)[-1] 61 | ).parse() 62 | 63 | 64 | def _parse_root(self): 65 | ''' this should NOT validate anything! 66 | only guess how many of the symbol's first 3 chars make up the root 67 | return value will be validated using NotePitchParser::parse() 68 | ''' 69 | possible_root = self.to_parse[:3] 70 | if len(possible_root) in (0, 1,): 71 | # symbol='', possible_root='' 72 | # symbol='D', possible_root='D' 73 | return possible_root 74 | 75 | if possible_root[1:] in ('bb', '##', '♯♯', '♭♭'): 76 | # symbol='Dbbmin7', possible_root='Dbb' 77 | return possible_root[:3] 78 | 79 | if possible_root[1] in ('b', '#', '♯', '♭', '𝄫', '𝄪'): 80 | # symbol='Dbmin7', possible_root='Db' 81 | return possible_root[:2] 82 | 83 | # symbol='Dmin7', possible_root='D' 84 | return possible_root[:1] 85 | 86 | 87 | def _parse_flavor(self): 88 | # shouldnt this go in a _parse_alt() function 89 | if 'sharp' in self.to_parse.lower() or 'flat' in self.to_parse.lower(): 90 | raise InvalidAlteration(self.symbol) 91 | 92 | for chord_class in self.RECOGNIZED_CHORDS: 93 | if self.to_parse in chord_class.notations: 94 | return chord_class 95 | return None 96 | 97 | 98 | def parse(self): 99 | ''' - sure chord is correct? return TonalKey() 100 | - unsure chord is correct? return None 101 | - sure chord is wrong ? raise InvalidChord() 102 | ''' 103 | try: 104 | # parse root out of first 3 chars 105 | root = self._parse_root() 106 | self.root = NotePitchParser(root).parse() 107 | # print(self.root) 108 | 109 | # ignore Chord/Bass notes on inverted chords 110 | if self.is_inverted: 111 | self.to_parse = self.to_parse.split(self.BASS_NOTE_SEP)[0] 112 | 113 | # parse flavor out of remaining to parse 114 | self.to_parse = self.to_parse[len(root):] 115 | self.flavor = self._parse_flavor() 116 | 117 | except Exception: 118 | # echo(self.symbol, 'red') 119 | raise InvalidChord(self.symbol) 120 | 121 | # c = 'cyan' 122 | # if not self.symbol or not self.flavor: 123 | # c = 'red' 124 | # echo( 125 | # f'{self.symbol} = {self.root} {self.flavor} {self.bass}', 126 | # c, 127 | # ) 128 | if self.flavor and self.root: 129 | # init instance of Chord class using Chord root 130 | return self.flavor(*self.root) 131 | return None 132 | 133 | -------------------------------------------------------------------------------- /app/fretboard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | this script displays a command-line representation of your instrument's fretboard, tuned to your liking 5 | note patterns will be displayed for any given mode (scale/chord) for any given root note 6 | the tunings directory already contains some pre-defined instrument tunings in the form of .json files 7 | modify them or add your own and they will automaticaly become available at runtime 8 | """ 9 | 10 | import sys 11 | import argparse 12 | 13 | from kord.keys.scales import ( 14 | MajorScale, MinorScale, MelodicMinorScale, HarmonicMinorScale, 15 | MajorPentatonicScale, MinorPentatonicScale, 16 | AugmentedScale, DiminishedScale, 17 | IonianMode, LydianMode, MixolydianMode, 18 | AeolianMode, DorianMode, PhrygianMode, LocrianMode, 19 | ChromaticScale, 20 | ) 21 | 22 | from kord.keys.chords import ( 23 | PowerChord, 24 | MajorTriad, MinorTriad, AugmentedTriad, DiminishedTriad, 25 | MajorSeventhChord, MinorSeventhChord, DominantSeventhChord, 26 | DiminishedSeventhChord, HalfDiminishedSeventhChord, 27 | DominantNinthChord, DominantMinorNinthChord, 28 | MajorNinthChord, MinorNinthChord, 29 | MajorSixthChord, MinorSixthChord, 30 | Suspended4Chord, Suspended2Chord, 31 | ) 32 | 33 | 34 | from kord.notes import NotePitch 35 | 36 | from kord.instruments import PluckedStringInstrument, max_frets_on_screen 37 | 38 | from kord.errors import InvalidInstrument, InvalidNote, InvalidAlteration 39 | 40 | from bestia.output import echo 41 | 42 | import tuner 43 | 44 | AVAILABLE_SCALES = { 45 | scale.notations[0]: scale for scale in ( 46 | MajorScale, 47 | MinorScale, 48 | MelodicMinorScale, 49 | HarmonicMinorScale, 50 | MajorPentatonicScale, 51 | MinorPentatonicScale, 52 | AugmentedScale, 53 | DiminishedScale, 54 | IonianMode, 55 | LydianMode, 56 | MixolydianMode, 57 | AeolianMode, 58 | DorianMode, 59 | PhrygianMode, 60 | LocrianMode, 61 | ChromaticScale, 62 | ) 63 | } 64 | 65 | AVAILABLE_CHORDS = { 66 | chord.notations[0]: chord for chord in ( 67 | PowerChord, 68 | Suspended4Chord, 69 | Suspended2Chord, 70 | MajorTriad, 71 | MinorTriad, 72 | AugmentedTriad, 73 | DiminishedTriad, 74 | MajorSixthChord, 75 | MinorSixthChord, 76 | MajorSeventhChord, 77 | MinorSeventhChord, 78 | DominantSeventhChord, 79 | DiminishedSeventhChord, 80 | HalfDiminishedSeventhChord, 81 | DominantNinthChord, 82 | DominantMinorNinthChord, 83 | MajorNinthChord, 84 | MinorNinthChord, 85 | ) 86 | } 87 | 88 | TUNINGS = tuner.load_tuning_data() 89 | 90 | def parse_arguments(): 91 | parser = argparse.ArgumentParser( 92 | description='<<< Fretboard visualizer tool for the kord framework >>>', 93 | ) 94 | parser.add_argument( 95 | 'root', 96 | help='select key ROOT note', 97 | ) 98 | 99 | parser.add_argument( 100 | '-d', '--degrees', 101 | help='show degree numbers instead of note pitches', 102 | action='store_true', 103 | ) 104 | 105 | mode_group = parser.add_mutually_exclusive_group() 106 | 107 | scale_choices = list( AVAILABLE_SCALES.keys() ) 108 | scale_choices.sort() 109 | mode_group.add_argument( 110 | '-s', '--scale', 111 | help='{}'.format(str(scale_choices).lstrip('[').rstrip(']').replace('\'', '')), 112 | choices=scale_choices, 113 | metavar='', 114 | ) 115 | 116 | chord_choices = list( AVAILABLE_CHORDS.keys() ) 117 | chord_choices.sort() 118 | mode_group.add_argument( 119 | '-c', '--chord', 120 | help='{}'.format(str(chord_choices).lstrip('[').rstrip(']').replace('\'', '')), 121 | choices=chord_choices, 122 | default=MajorTriad.notations[0], 123 | metavar='', 124 | ) 125 | 126 | instr_choices = list( TUNINGS.keys() ) 127 | parser.add_argument( 128 | '-i', '--instrument', 129 | help='{}'.format(str(instr_choices).lstrip('[').rstrip(']').replace('\'', '')), 130 | choices=instr_choices, 131 | default='guitar', 132 | metavar='', 133 | ) 134 | 135 | parser.add_argument( 136 | '-t', '--tuning', 137 | help='check .json files for available options', 138 | default='standard', 139 | metavar='', 140 | ) 141 | 142 | parser.add_argument( 143 | '-f', '--frets', 144 | help='1, 2, .., {}'.format(PluckedStringInstrument.maximum_frets()), 145 | choices=[ f+1 for f in range(PluckedStringInstrument.maximum_frets()) ], 146 | default=max_frets_on_screen(), 147 | metavar='', 148 | type=int, 149 | ) 150 | 151 | parser.add_argument( 152 | '-v', '--verbosity', 153 | help='0, 1, 2', 154 | choices= (0, 1, 2), 155 | default=1, 156 | metavar='', 157 | type=int, 158 | ) 159 | 160 | args = parser.parse_args() 161 | 162 | # some args require extra validation than what argparse can offer, let's do that here... 163 | try: 164 | # validate tuning 165 | if args.tuning not in TUNINGS[args.instrument].keys(): 166 | raise InvalidInstrument( 167 | "fretboard.py: error: argument -t/--tuning: invalid choice: '{}' (choose from {}) ".format( 168 | args.tuning, 169 | str( list( TUNINGS[args.instrument].keys() ) ).lstrip('[').rstrip(']'), 170 | ) 171 | ) 172 | 173 | # validate root note 174 | note_chr = args.root[:1].upper() 175 | if note_chr not in NotePitch.possible_chars(): 176 | raise InvalidNote( 177 | "fretboard.py: error: argument ROOT: invalid note: '{}' (choose from {}) ".format( 178 | note_chr, 179 | str( NotePitch.possible_chars() ).lstrip('[').rstrip(']') 180 | ) 181 | ) 182 | 183 | # validate root alteration 184 | note_alt = args.root[1:] 185 | if note_alt and note_alt not in NotePitch.input_alterations(): 186 | raise InvalidAlteration( 187 | "fretboard.py: error: argument ROOT: invalid alteration: '{}' (choose from {}) ".format( 188 | note_alt, 189 | str( NotePitch.input_alterations() ).lstrip('(').rstrip(')') 190 | ) 191 | ) 192 | 193 | args.root = (note_chr, note_alt) 194 | 195 | except Exception as x: 196 | parser.print_usage() 197 | print(x) 198 | args = None 199 | 200 | return args 201 | 202 | def print_mode(mode): 203 | clr = 'blue' 204 | mode = '{} {}'.format( 205 | str(mode.root)[:-1], 206 | mode.name(), 207 | ) 208 | echo(mode, clr) 209 | 210 | def print_instrument(instrument, tuning): 211 | clr = 'yellow' 212 | echo(instrument.title(), clr, 'underline', mode='raw') 213 | echo(': ', clr, mode='raw') 214 | echo('{} tuning'.format(tuning), clr) 215 | 216 | 217 | def run(args): 218 | # default mode is chord, so use scale if set 219 | if args.scale: 220 | KeyMode = AVAILABLE_SCALES[args.scale] 221 | elif args.chord: 222 | KeyMode = AVAILABLE_CHORDS[args.chord] 223 | 224 | root = NotePitch(args.root[0], args.root[1]) 225 | key_mode = KeyMode(*root) 226 | if not key_mode.validate(): 227 | print( 228 | '{} {} cannot be visualized'.format( 229 | str(root)[:-1], 230 | key_mode.name(), 231 | ) 232 | ) 233 | return -1 234 | 235 | if args.verbosity: 236 | print_instrument(args.instrument, args.tuning) 237 | 238 | instrument = PluckedStringInstrument( 239 | * TUNINGS[args.instrument][args.tuning] 240 | ) 241 | 242 | instrument.render_fretboard( 243 | mode=key_mode, 244 | frets=args.frets, 245 | verbose=args.verbosity, 246 | show_degrees=args.degrees, 247 | ) 248 | 249 | if args.verbosity: 250 | print_mode(key_mode) 251 | 252 | return 0 253 | 254 | 255 | if __name__ == '__main__': 256 | rc = -1 257 | valid_args = parse_arguments() 258 | if not valid_args: 259 | rc = 2 260 | else: 261 | rc = run(valid_args) 262 | sys.exit(rc) 263 | -------------------------------------------------------------------------------- /kord/keys/chords.py: -------------------------------------------------------------------------------- 1 | ''' 2 | degrees tuple is not explicitly defined anymore but is inherited from parent class: 3 | 4 | class TriadChord(Chord): 5 | class SeventhChord(Chord): 6 | class SixthChord(Chord): 7 | class NinthChord(Chord): 8 | 9 | 10 | parent scale is now a separate class atrribute 11 | parent_scale_degree allows to form chords from degrees other than I 12 | 13 | 14 | ''' 15 | 16 | from .scales import ( 17 | MajorScale, MinorScale, AugmentedScale, DiminishedScale, 18 | IonianMode, AeolianMode, MixolydianMode, LocrianMode, DorianMode, 19 | HarmonicMinorScale, MelodicMinorScale 20 | ) 21 | from .scales import TonalKey 22 | 23 | from ..notes.intervals import Intervals 24 | 25 | from ..errors import InvalidNote 26 | 27 | 28 | __all__ = [ 29 | 'PowerChord', 30 | 'Suspended4Chord', 31 | 'Suspended2Chord', 32 | 33 | 'MajorTriad', 34 | 'MinorTriad', 35 | 'AugmentedTriad', 36 | 'DiminishedTriad', 37 | 38 | 'MajorSixthChord', 39 | 'MinorSixthChord', 40 | 41 | 'MajorSeventhChord', 42 | 'MinorSeventhChord', 43 | 'DominantSeventhChord', 44 | 'HalfDiminishedSeventhChord', 45 | 'DiminishedSeventhChord', 46 | 47 | 'MajorAdd9Chord', 48 | 'MinorAdd9Chord', 49 | 'AugmentedAdd9Chord', 50 | 'DiminishedAdd9Chord', 51 | 52 | 'MajorNinthChord', 53 | 'MinorNinthChord', 54 | 'DominantNinthChord', 55 | 'DominantMinorNinthChord', 56 | ] 57 | 58 | 59 | class Chord(TonalKey): 60 | parent_scale = MajorScale 61 | parent_scale_degree = 1 62 | degrees = (1, 3, 5,) 63 | 64 | @classmethod 65 | def find_parent_scale_root(cls, match_note): 66 | ''' finds char for which your parent_scale will match your chord's root 67 | at parent_scale_degree 68 | ''' 69 | scales_found = [] 70 | for n in cls.parent_scale.valid_root_notes(): 71 | scale = cls.parent_scale(*n) 72 | if scale[cls.parent_scale_degree] ** match_note: 73 | scales_found.append(scale) 74 | 75 | assert len(scales_found) <= 1 76 | if scales_found: 77 | return scales_found[0] 78 | return None 79 | 80 | 81 | @classmethod 82 | def _parent_scale_root_offset(cls) -> int: 83 | ''' if I am an E7 chord and my parent_scale is A Major 84 | my offset from parent_scale root is 7 semitones 85 | ''' 86 | return cls.parent_scale._calc_intervals()[cls.parent_scale_degree - 1] 87 | 88 | 89 | @classmethod 90 | def _calc_intervals(cls): 91 | # intervals is an empty tuple for chords 92 | offset_from_root = cls._parent_scale_root_offset() 93 | arranged_intervals = [] 94 | for parent_scale_interval in cls.parent_scale._calc_intervals(): 95 | new_interval = parent_scale_interval - offset_from_root 96 | if new_interval < 0: 97 | new_interval += Intervals.PERFECT_OCTAVE 98 | arranged_intervals.append(new_interval) 99 | arranged_intervals.sort() 100 | return arranged_intervals 101 | 102 | 103 | class PowerChord(Chord): 104 | ''' C G ''' 105 | notations = ( 106 | '5', 107 | ) 108 | degrees = (1, 5, ) 109 | 110 | class SusFourChord(Chord): 111 | degrees = (1, 4, 5, ) 112 | 113 | class SusTwoChord(Chord): 114 | degrees = (1, 2, 5, ) 115 | 116 | class TriadChord(Chord): 117 | degrees = (1, 3, 5, ) 118 | 119 | class SixthChord(Chord): 120 | degrees = (1, 3, 5, 6, ) 121 | 122 | class SeventhChord(Chord): 123 | degrees = (1, 3, 5, 7, ) 124 | 125 | class AddNineChord(Chord): 126 | degrees = (1, 3, 5, 9, ) 127 | 128 | class NinthChord(Chord): 129 | degrees = (1, 3, 5, 7, 9, ) 130 | 131 | 132 | ######################## 133 | ### SUSPENDED CHORDS ### 134 | ######################## 135 | 136 | class Suspended4Chord(SusFourChord): 137 | ''' C F G ''' 138 | parent_scale = IonianMode 139 | notations = ( 140 | 'sus4', 141 | 'sus', 142 | ) 143 | 144 | class Suspended2Chord(SusTwoChord): 145 | ''' C D G ''' 146 | parent_scale = IonianMode 147 | notations = ( 148 | 'sus2', 149 | 'sus9', 150 | ) 151 | 152 | 153 | #################### 154 | ### TRIAD CHORDS ### 155 | #################### 156 | 157 | class MajorTriad(TriadChord): 158 | ''' C E G ''' 159 | parent_scale = MajorScale 160 | notations = ( 161 | 'maj', 162 | '', 163 | 'major', 164 | ) 165 | 166 | class MinorTriad(TriadChord): 167 | ''' C Eb G ''' 168 | parent_scale = MinorScale 169 | notations = ( 170 | 'min', 171 | '-', 172 | 'm', 173 | 'minor', 174 | ) 175 | 176 | class AugmentedTriad(TriadChord): 177 | ''' C E G# ''' 178 | parent_scale = HarmonicMinorScale 179 | parent_scale_degree = 3 180 | notations = ( 181 | 'aug', 182 | 'augmented', 183 | ) 184 | 185 | class DiminishedTriad(TriadChord): 186 | ''' C Eb Gb ''' 187 | parent_scale = MajorScale 188 | parent_scale_degree = 7 189 | notations = ( 190 | 'dim', 191 | 'diminished', 192 | ) 193 | 194 | 195 | #################### 196 | ### SIXTH CHORDS ### 197 | #################### 198 | 199 | class MajorSixthChord(SixthChord): 200 | ''' C E G A ''' 201 | parent_scale = IonianMode 202 | notations = ( 203 | '6', 204 | 'add6', 205 | ) 206 | 207 | class MinorSixthChord(SixthChord): 208 | ''' C Eb G A ''' 209 | parent_scale = DorianMode 210 | notations = ( 211 | 'm6', 212 | # 'madd6', 213 | 'min6', 214 | ) 215 | 216 | 217 | ###################### 218 | ### SEVENTH CHORDS ### 219 | ###################### 220 | 221 | class MajorSeventhChord(SeventhChord): 222 | ''' C E G B ''' 223 | parent_scale = IonianMode 224 | notations = ( 225 | 'maj7', 226 | 'M7', # careful with these 2 if ever using .lower() to compare 227 | 'Δ7', 228 | 'Δ', 229 | 'major7', 230 | ) 231 | 232 | class MinorSeventhChord(SeventhChord): 233 | ''' C Eb G Bb ''' 234 | parent_scale = AeolianMode 235 | notations = ( 236 | 'min7', 237 | 'm7', # careful with these 2 if ever using .lower() to compare 238 | '-7', 239 | 'minor7', 240 | ) 241 | 242 | class DominantSeventhChord(SeventhChord): 243 | ''' C E G Bb ''' 244 | parent_scale = MixolydianMode 245 | notations = ( 246 | '7', 247 | 'dom7', 248 | 'dominant7', 249 | ) 250 | 251 | class HalfDiminishedSeventhChord(SeventhChord): 252 | ''' C Eb Gb Bb ''' 253 | parent_scale = LocrianMode 254 | notations = ( 255 | 'm7b5', 256 | 'm7-5', 257 | 'min7dim5', 258 | 'm7(b5)', 259 | 'ø7', 260 | ) 261 | 262 | class DiminishedSeventhChord(SeventhChord): 263 | ''' C Eb Gb Bbb''' 264 | parent_scale = HarmonicMinorScale 265 | parent_scale_degree = 7 266 | notations = ( 267 | 'dim7', 268 | 'o7', 269 | 'diminished7', 270 | ) 271 | 272 | 273 | ################### 274 | ### ADD9 CHORDS ### 275 | ################### 276 | 277 | class MajorAdd9Chord(AddNineChord): 278 | ''' C E G D ''' 279 | parent_scale = IonianMode 280 | notations = ( 281 | 'add9', 282 | 'Add9', 283 | ) 284 | 285 | class MinorAdd9Chord(AddNineChord): 286 | ''' C Eb G D ''' 287 | parent_scale = AeolianMode 288 | notations = ( 289 | 'madd9', 290 | 'mAdd9', 291 | ) 292 | 293 | class AugmentedAdd9Chord(AddNineChord): 294 | ''' C E G# D ''' 295 | parent_scale = HarmonicMinorScale 296 | parent_scale_degree = 3 297 | notations = ( 298 | 'augadd9', 299 | 'augAdd9', 300 | ) 301 | 302 | class DiminishedAdd9Chord(AddNineChord): 303 | ''' C Eb Gb D ''' 304 | parent_scale = MelodicMinorScale 305 | parent_scale_degree = 6 306 | notations = ( 307 | 'dimadd9', 308 | 'dimAdd9', 309 | ) 310 | 311 | 312 | #################### 313 | ### NINTH CHORDS ### 314 | #################### 315 | 316 | class MajorNinthChord(NinthChord): 317 | ''' C E G B D ''' 318 | parent_scale = IonianMode 319 | notations = ( 320 | 'maj9', 321 | 'M9', 322 | 'major9', 323 | ) 324 | 325 | class MinorNinthChord(NinthChord): 326 | ''' C Eb G Bb D ''' 327 | parent_scale = AeolianMode 328 | notations = ( 329 | 'm9', 330 | 'min9', 331 | '-9', 332 | 'minor9', 333 | ) 334 | 335 | class DominantNinthChord(NinthChord): 336 | ''' C E G Bb D ''' 337 | parent_scale = MixolydianMode 338 | notations = ( 339 | '9', 340 | 'dom9', 341 | ) 342 | 343 | class DominantMinorNinthChord(NinthChord): 344 | ''' C E G Bb Db ''' 345 | parent_scale = HarmonicMinorScale 346 | parent_scale_degree = 5 347 | notations = ( 348 | '7b9', 349 | ) 350 | 351 | -------------------------------------------------------------------------------- /kord/instruments.py: -------------------------------------------------------------------------------- 1 | ''' 2 | ㉑ ㉒ ㉓ ㉔ ㉕ ㉖ ㉗ ㉘ ㉙ ㉚ 3 | ㉛ ㉜ ㉝ ㉞ ㉟ ㊱ ㊲ ㊳ ㊴ ㊵ 4 | ㊶ ㊷ ㊸ ㊹ ㊺ ㊻ ㊼ ㊽ ㊾ ㊿ 5 | 6 | These are specifically sans-serif: 7 | 8 | 🄋 ➀ ➁ ➂ ➃ ➄ ➅ ➆ ➇ ➈ ➉ 9 | 10 | Black Circled Number 11 | ⓿ 12 | ❶ 13 | ❷ 14 | ❸ 15 | ❹ 16 | ❺ 17 | ❻ 18 | ❼ 19 | ❽ 20 | ❾ 21 | ❿ 22 | 23 | 24 | Ⓐ ⓐ 25 | Ⓑ ⓑ 26 | Ⓒ ⓒ 27 | Ⓓ ⓓ 28 | Ⓔ ⓔ 29 | Ⓕ ⓕ 30 | Ⓖ ⓖ 31 | 32 | 33 | # NUMERALS = { 34 | # 'I' : 'Ⅰ', 35 | # 'V' : 'ⅤⅠⅤ', 36 | # 'X' : 'Ⅹ', 37 | # 'L' : 'Ⅼ', 38 | # 'C' : 'Ⅽ', 39 | # 'D' : 'Ⅾ', 40 | # 'M' : 'Ⅿ', 41 | # } 42 | 43 | https://www.unicode.org/charts/nameslist/n_2460.html 44 | 45 | ''' 46 | 47 | from bestia.output import Row, FString, echo, tty_cols 48 | 49 | from .keys.scales import ChromaticScale 50 | from .notes import NotePitch 51 | 52 | __all__ = [ 53 | 'PluckedStringInstrument', 54 | 'max_frets_on_screen', 55 | ] 56 | 57 | 58 | class PluckedString: 59 | 60 | _FRETS = ( 61 | '│', 62 | '║', 63 | ) 64 | 65 | _DEGREE_ICONS = ( 66 | '⓪', # null degree... 67 | # 'Ⓝ', 68 | # '①', 69 | 'Ⓡ', 70 | '➁', 71 | '➂', 72 | '➃', 73 | '➄', 74 | '➅', 75 | '➆', 76 | '➇', 77 | '➈', 78 | '➉', 79 | '⑪', 80 | '⑫', 81 | '⑬', 82 | '⑭', 83 | '⑮', 84 | '⑯', 85 | '⑰', 86 | '⑱', 87 | '⑲', 88 | '⑳', 89 | ) 90 | 91 | def __init__( 92 | self, 93 | char, alt='', oct=NotePitch.DEFAULT_OCTAVE, 94 | frets=12, 95 | mode=None, 96 | verbose=1, 97 | show_degrees=False 98 | ): 99 | self.tuning = NotePitch(char, alt, oct) 100 | self.mode = mode 101 | self.frets = frets 102 | self.verbose = verbose 103 | self.show_degrees = show_degrees 104 | 105 | def __repr__(self): 106 | ''' prints string notes matching given key ''' 107 | string_line = Row() 108 | 109 | mode = self.mode if self.mode else ChromaticScale(*self.tuning) 110 | 111 | for f, note in enumerate( 112 | mode.spell( 113 | note_count=self.frets + 1, 114 | start_note=self.tuning, 115 | yield_all=True, 116 | ) 117 | ): 118 | 119 | fret_value = '' 120 | if note: 121 | 122 | note_fg = 'green' if note ** mode.root else 'magenta' 123 | 124 | if self.show_degrees: 125 | for d in mode.allowed_degrees(): 126 | if note ** mode[d]: 127 | fret_value = FString( 128 | '{} '.format( 129 | self._DEGREE_ICONS[d] if self.verbose != 0 130 | else ( 131 | d if d != 1 else 'R' 132 | ) 133 | ), 134 | size=3, 135 | fg=note_fg, 136 | align='r', 137 | ) 138 | break 139 | 140 | else: 141 | fret_value = '{}{}{}'.format( 142 | FString( 143 | note.chr, 144 | size=1, 145 | fg=note_fg, 146 | fx=[''], 147 | ), 148 | FString( 149 | note.repr_alt, 150 | size=0, 151 | fg=note_fg, 152 | fx=[''], 153 | ), 154 | FString( 155 | note.repr_oct if self.verbose > 0 else '', 156 | size=1, 157 | fg=note_fg, 158 | fx=['faint'], 159 | ), 160 | ) 161 | 162 | 163 | # APPEND NOTE PITCH DATA 164 | string_line.append( 165 | FString( 166 | fret_value, 167 | size=PluckedStringInstrument.NOTE_WIDTH, 168 | align='cr', 169 | ) 170 | ) 171 | 172 | # APPEND FRET SYMBOL 173 | string_line.append( 174 | FString( 175 | self._FRETS[ f % 12 == 0 ], 176 | size=PluckedStringInstrument.FRET_WIDTH, 177 | # fg='blue', 178 | # fx=['faint'], 179 | ) 180 | ) 181 | 182 | if f == self.frets: 183 | break 184 | 185 | return str(string_line) 186 | 187 | 188 | class PluckedStringInstrument: 189 | 190 | NOTE_WIDTH = 5 191 | FRET_WIDTH = 1 192 | 193 | _INLAYS = ( 194 | 'I', 195 | 'II', 196 | 'III', 197 | 'IV', 198 | 'V', 199 | 'VI', 200 | 'VII', 201 | 'VIII', 202 | 'IX', 203 | 'X', 204 | 'XI', 205 | 206 | 'XII', 207 | 'XIII', 208 | 'XIV', 209 | 'XV', 210 | 'XVI', 211 | 'XVII', 212 | 'XVIII', 213 | 'XIX', 214 | 'XX', 215 | 'XXI', 216 | 'XXII', 217 | 'XXIII', 218 | 219 | 'XXIV', 220 | 'XXV', 221 | 'XXVI', 222 | 'XXVII', 223 | 'XXVIII', 224 | 'XXIX', 225 | 'XXX', 226 | 'XXXI', 227 | 'XXXII', 228 | 'XXXIII', 229 | 'XXXIV', 230 | 'XXXV', 231 | 'XXXVI', 232 | ) 233 | 234 | _INLAY_DOTS = (3, 5, 7, 9, 12, 15, 17, 19, 21, 24, 27, 29, 31, 33, 36, ) 235 | 236 | _BINDING = { 237 | 'default': '═', 238 | 'capo' : ('╔', '╚'), 239 | '1224' : ('╦', '╩'), 240 | 'fret' : ('╤', '╧'), 241 | 'end' : ('╕', '╛'), 242 | 'end12' : ('╗', '╝'), 243 | } 244 | 245 | @classmethod 246 | def maximum_frets(cls): 247 | return len(cls._INLAYS) 248 | 249 | 250 | @classmethod 251 | def fret_width(cls): 252 | return cls.NOTE_WIDTH + cls.FRET_WIDTH 253 | 254 | 255 | def __init__(self, *notes): 256 | self.strings = [ PluckedString(*n) for n in notes ] 257 | 258 | 259 | @property 260 | def string_n_size(self): 261 | ''' does not expect more then 99 strings... ''' 262 | return 1 if len(self.strings) < 10 else 2 263 | 264 | 265 | def render_inlays(self, frets=12, verbose=1): 266 | 267 | if not verbose: 268 | return 269 | 270 | inlay_row = Row( 271 | FString( 272 | '', 273 | size=self.string_n_size + self.fret_width(), 274 | align='l', 275 | # pad='*', 276 | ), 277 | ) 278 | 279 | f = 1 280 | while frets: 281 | inlay_row.append( 282 | FString( 283 | '' if verbose == 1 and f not in self._INLAY_DOTS else self._INLAYS[f-1], 284 | size=self.fret_width(), 285 | align='r' if verbose == 2 else 'c', # high verbose needs more space 286 | fg='cyan', 287 | fx=['faint' if f not in self._INLAY_DOTS else ''], 288 | ) 289 | ) 290 | f += 1 291 | frets -= 1 292 | 293 | echo(inlay_row) 294 | 295 | 296 | def render_binding(self, frets, is_lower): 297 | ''' https://en.wikipedia.org/wiki/Box-drawing_character 298 | ''' 299 | fret_width = self.fret_width() 300 | render = ' ' * ( self.NOTE_WIDTH + self.string_n_size ) 301 | render += self._BINDING['capo'][is_lower] 302 | total_space = frets * fret_width 303 | for f in range(total_space): 304 | f += 1 305 | is_12th_fret = f % (fret_width * 12) == 0 306 | is_fret = f % fret_width == 0 307 | if f == total_space: 308 | # final fret 309 | render += ( 310 | self._BINDING['end12'][is_lower] if is_12th_fret 311 | else self._BINDING['end'][is_lower] 312 | ) 313 | elif is_12th_fret: 314 | # 12th, 24th 315 | render += self._BINDING['1224'][is_lower] 316 | elif is_fret: 317 | # fret bar joints 318 | render += self._BINDING['fret'][is_lower] 319 | else: 320 | # normal binding 321 | render += self._BINDING['default'] 322 | echo(render) 323 | 324 | 325 | def render_string(self, s, mode, frets=12, verbose=1, show_degrees=False): 326 | string_n = FString( 327 | s, 328 | fg='cyan', 329 | fx=['faint' if verbose < 1 else ''], 330 | size=self.string_n_size, 331 | ) 332 | string = self.strings[s-1] 333 | string.mode = mode 334 | string.frets = frets 335 | string.verbose = verbose 336 | string.show_degrees = show_degrees 337 | echo(string_n + string) 338 | 339 | 340 | def render_fretboard(self, mode=None, frets=12, verbose=1, show_degrees=False): 341 | self.render_inlays(frets, verbose) 342 | self.render_binding(frets, is_lower=False) 343 | for s, _ in enumerate(self.strings): 344 | self.render_string(s+1, mode, frets, verbose, show_degrees) 345 | self.render_binding(frets, is_lower=True) 346 | 347 | 348 | def max_frets_on_screen(): 349 | frets_allowed_by_tty = int( 350 | tty_cols() / PluckedStringInstrument.fret_width() 351 | ) - 2 352 | if frets_allowed_by_tty < PluckedStringInstrument.maximum_frets(): 353 | return frets_allowed_by_tty 354 | return PluckedStringInstrument.maximum_frets() 355 | 356 | -------------------------------------------------------------------------------- /kord/notes/pitch.py: -------------------------------------------------------------------------------- 1 | from bestia.iterate import LoopedList 2 | 3 | from .intervals import Intervals 4 | 5 | from ..errors import InvalidNote, InvalidAlteration, InvalidOctave 6 | 7 | __all__ = [ 8 | 'NotePitch', 9 | ] 10 | 11 | 12 | class NotePitch: 13 | 14 | _CHARS = LoopedList( 15 | 'C', 16 | None, 17 | 'D', 18 | None, 19 | 'E', 20 | 'F', 21 | None, 22 | 'G', 23 | None, 24 | 'A', 25 | None, 26 | 'B', 27 | ) 28 | 29 | _ALTS = { 30 | 'bb': '𝄫', 31 | 'b': '♭', 32 | # '♮': '', 33 | '': '', 34 | '#': '♯', 35 | '##': '𝄪', 36 | } 37 | 38 | _OCTAVES = ( 39 | # '₀', 40 | # '₁', 41 | # '₂', 42 | # '₃', 43 | # '₄', 44 | # '₅', 45 | # '₆', 46 | # '₇', 47 | # '₈', 48 | # '₉', 49 | '⁰', 50 | '¹', 51 | '²', 52 | '³', 53 | '⁴', 54 | '⁵', 55 | '⁶', 56 | '⁷', 57 | '⁸', 58 | '⁹', 59 | ) 60 | 61 | DEFAULT_OCTAVE = 3 62 | MAXIMUM_OCTAVE = 9 63 | 64 | @classmethod 65 | def EnharmonicMatrix(cls): 66 | ''' This is the heart of the whole project 67 | indeces are used to determine: 68 | * when to change octs 69 | * intervals between NotePitch instances 70 | notes MUST be unique so that TonalKey[d] finds 1 exact match! 71 | ''' 72 | return LoopedList( 73 | ## 2-octave enharmonic relationships 74 | ( cls('C', '' , 2), cls('B', '#' , 1), cls('D', 'bb', 2) ), # NAH 75 | ( cls('C', '#', 2), cls('D', 'b' , 2), cls('B', '##', 1) ), # AAH 76 | 77 | ## 1-octave enharmonic relationships 78 | ( cls('D', '' , 1), cls('C', '##', 1), cls('E', 'bb', 1) ), # NHH 79 | ( cls('D', '#', 1), cls('E', 'b' , 1), cls('F', 'bb', 1) ), # AAH 80 | ( cls('E', '' , 1), cls('F', 'b' , 1), cls('D', '##', 1) ), # NAH 81 | ( cls('F', '' , 1), cls('E', '#' , 1), cls('G', 'bb', 1) ), # NAH 82 | ( cls('F', '#', 1), cls('G', 'b' , 1), cls('E', '##', 1) ), # AAH 83 | ( cls('G', '' , 1), cls('F', '##', 1), cls('A', 'bb', 1) ), # NHH 84 | ( cls('G', '#', 1), cls('A', 'b' , 1) ), # AA 85 | ( cls('A', '' , 1), cls('G', '##', 1), cls('B', 'bb', 1) ), # NHH 86 | 87 | ## 2-octave enharmonic relationships 88 | ( cls('A', '#', 1), cls('B', 'b' , 1), cls('C', 'bb', 2) ), # AAH 89 | ( cls('B', '' , 1), cls('C', 'b' , 2), cls('A', '##', 1) ), # NAH 90 | ) 91 | 92 | @classmethod 93 | def possible_chars(cls): 94 | return [ c for c in cls._CHARS if c ] 95 | 96 | @classmethod 97 | def validate_char(cls, char): 98 | char = char.upper() 99 | if char not in cls.possible_chars(): 100 | raise InvalidNote(char) 101 | return char 102 | 103 | @classmethod 104 | def input_alterations(cls) -> tuple: 105 | return tuple( cls._ALTS.keys() ) 106 | 107 | @classmethod 108 | def output_alterations(cls) -> tuple: 109 | return tuple( cls._ALTS.values() ) 110 | 111 | @classmethod 112 | def notes_by_alts(cls): 113 | ''' yields all 35 possible notes in following order: 114 | * 7 notes with alt '' 115 | * 7 notes with alt 'b' 116 | * 7 notes with alt '#' 117 | * 7 notes with alt 'bb' 118 | * 7 notes with alt '##' 119 | ''' 120 | # sort alts 121 | alts = list( cls.input_alterations() ) 122 | alts.sort(key=len) # '', b, #, bb, ## 123 | 124 | # get all notes 125 | notes = [] 126 | for ehns in cls.EnharmonicMatrix(): 127 | for ehn in ehns: 128 | notes.append(ehn) 129 | 130 | # yield notes 131 | for alt in alts: 132 | for note in notes: 133 | if note.alt == alt: 134 | # note.oct = 3 135 | yield note 136 | 137 | 138 | def __init__(self, char, *args): 139 | ''' init WITHOUT specifying argument names 140 | should be able to handle: 141 | NotePitch('C') 142 | NotePitch('C', 9) 143 | NotePitch('C', '#') 144 | NotePitch('C', '#', 9) 145 | ''' 146 | self.chr = self.validate_char(char) 147 | self.alt = '' 148 | self.oct = self.DEFAULT_OCTAVE 149 | 150 | if len(args) > 2: 151 | raise InvalidNote('Too many arguments') 152 | 153 | # with only 1 arg, decide if it's alt or oct 154 | if len(args) == 1: 155 | if args[0] in self._ALTS: 156 | self.alt = args[0] 157 | else: 158 | try: 159 | self.oct = int(args[0]) 160 | except: 161 | raise InvalidNote( 162 | 'Failed to parse argument: '.format(args[0]) 163 | ) 164 | 165 | # with 2 args, order must be alt, oct 166 | if len(args) == 2: 167 | if args[0] not in self._ALTS: 168 | raise InvalidAlteration(args[0]) 169 | self.alt = args[0] 170 | 171 | try: 172 | self.oct = int(args[1]) 173 | except: 174 | raise InvalidOctave(args[1]) 175 | 176 | if self.oct > self.MAXIMUM_OCTAVE: 177 | raise InvalidOctave(self.oct) 178 | 179 | 180 | def __iter__(self): 181 | ''' allows unpacking Note objects as args for other functions ''' 182 | for i in (self.chr, self.alt, self.oct): 183 | yield i 184 | 185 | def __repr__(self): 186 | return '{}{}{}'.format( 187 | self.chr, 188 | self.repr_alt, 189 | self.repr_oct, 190 | ) 191 | 192 | @property 193 | def repr_oct(self): 194 | output = '' 195 | for c in str(self.oct): 196 | output += self._OCTAVES[int(c)] 197 | return output 198 | 199 | @property 200 | def repr_alt(self): 201 | return self._ALTS[self.alt] 202 | 203 | def __sub__(self, other): 204 | if self.__class__ == other.__class__: 205 | oct_interval = (self.oct - other.oct) * Intervals.PERFECT_OCTAVE 206 | chr_interval = self._CHARS.index(self.chr) - self._CHARS.index(other.chr) 207 | alt_interval = ( 208 | self.input_alterations().index(self.alt) - self.input_alterations().index(other.alt) 209 | ) 210 | return oct_interval + chr_interval + alt_interval 211 | raise TypeError(' - not supported with {}'.format(other.__class__)) 212 | 213 | def __pow__(self, other): 214 | if self.__class__ == other.__class__: 215 | if self.chr == other.chr: 216 | if self.alt == other.alt: 217 | return True 218 | return False 219 | raise TypeError(' ** not supported with {}'.format(other.__class__)) 220 | 221 | def __rshift__(self, other): 222 | if self.__class__ == other.__class__: 223 | if self.chr == other.chr: 224 | if self.alt == other.alt: 225 | if self.oct == other.oct: 226 | return True 227 | return False 228 | raise TypeError(' >> not supported with {}'.format(other.__class__)) 229 | 230 | def __eq__(self, other): 231 | if other.__class__ == self.__class__: 232 | return other - self == Intervals.UNISON 233 | 234 | def __ne__(self, other): 235 | if other.__class__ == self.__class__: 236 | return other - self != Intervals.UNISON 237 | return True 238 | 239 | def __gt__(self, other): 240 | if self.__class__ == other.__class__: 241 | return self - other > Intervals.UNISON 242 | raise TypeError(' > not supported with {}'.format(other.__class__)) 243 | 244 | def __ge__(self, other): 245 | if self.__class__ == other.__class__: 246 | return self - other >= Intervals.UNISON 247 | raise TypeError(' >= not supported with {}'.format(other.__class__)) 248 | 249 | def __lt__(self, other): 250 | if self.__class__ == other.__class__: 251 | return self - other < Intervals.UNISON 252 | raise TypeError(' < not supported with {}'.format(other.__class__)) 253 | 254 | def __le__(self, other): 255 | if self.__class__ == other.__class__: 256 | return self - other <= Intervals.UNISON 257 | raise TypeError(' <= not supported with {}'.format(other.__class__)) 258 | 259 | 260 | ### CHR METHODS 261 | def relative_chr(self, n): 262 | my_index = self._CHARS.index(self.chr) 263 | return self._CHARS[my_index +n] 264 | 265 | def adjacent_chr(self, n=1): 266 | ''' returns next adjacent chr of self 267 | negative fails... 268 | ''' 269 | c = '' 270 | i = 1 271 | while n != 0: 272 | c = self.relative_chr(i) 273 | if c: 274 | n -= 1 275 | i += 1 276 | return c 277 | 278 | ### OCT METHODS 279 | def oversteps_oct(self, other): 280 | ''' evaluates whether I need to cross octave border in order to go from self to other 281 | ignores octave 282 | ''' 283 | if self.chr != other.chr: 284 | return self._CHARS.index(self.chr) > self._CHARS.index(other.chr) 285 | return ( 286 | self.input_alterations().index(self.alt) > self.input_alterations().index(other.alt) 287 | ) 288 | 289 | 290 | # ENHARMONIC ATTRIBUTES 291 | @property 292 | def enharmonic_row(self): 293 | return self.matrix_coordinates[0] 294 | 295 | @property 296 | def matrix_coordinates(self): 297 | for row_index, row in enumerate( self.EnharmonicMatrix() ): 298 | for note_index, enharmonic_note in enumerate(row): 299 | if self ** enharmonic_note: # ignore oct 300 | return (row_index, note_index) 301 | 302 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kord 2 | the kord framework provides you with a simple api for creating music-based applications. While it is mainly intended for theoretical purposes, some of it's modules contain functionality specifically tailored to plucked-string instruments. 3 | 4 | ![](https://github.com/synestematic/kord/blob/master/pngs/chrom.png?raw=true) 5 | 6 | ## installation 7 | 8 | The only dependency for `kord` is the `bestia` package, my own library for creating command-line applications. Both are automatically installed with pip: 9 | 10 | ``` 11 | $ python3 -m pip install kord 12 | ``` 13 | 14 | The fretboard application component of the framework can also be run directly in a containerized form. This requires you to install 0 dependencies on your system besides docker. 15 | 16 | ``` 17 | $ docker run -t synestematic/kord C --scale major 18 | ``` 19 | 20 | Scroll to the bottom of the README for more information on the fretboard application. 21 | 22 | 23 | # api reference: 24 | 25 | Please only expect to understand the following documentation if you have an above basic understanding of music theory. With that said, let's dive into the first module: 26 | 27 | 28 | ## kord.notes 29 | 30 | ### class NotePitch: 31 | 32 | NotePitch instances are the building blocks of the framework and have 3 main attributes: 33 | 34 | ``` 35 | * chr: str ('C', 'D', 'E', 'F', 'G', 'A', 'B') 36 | * alt: str ('bb', 'b', '', '#', '##') 37 | * oct: int (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) 38 | ``` 39 | 40 | You can set these values when creating an instance and only the `chr` argument is required. Arguments `alt` and `oct` will default to `''` and `3` respectively. These are __positional__ arguments, not keyword arguments so keep that in mind when creating your objects. 41 | 42 | ``` 43 | >>> from kord.notes import NotePitch 44 | >>> e3, f3 = NotePitch('e'), NotePitch('f') 45 | >>> e3, f3 46 | (E³, F³) 47 | >>> NotePitch('G', 'b') 48 | G♭³ 49 | >>> NotePitch('C', 9) 50 | C⁹ 51 | >>> NotePitch('B', 'b', 7) 52 | B♭⁷ 53 | >>> NotePitch('C', '#', 0) 54 | C♯⁰ 55 | ``` 56 | 57 | Notes with double alterations are supported but Notes with triple or more alterations will raise an exception: 58 | 59 | ``` 60 | >>> NotePitch('A', 'bb', 1) 61 | A𝄫¹ 62 | >>> NotePitch('F', '##', 1) 63 | F𝄪¹ 64 | >>> NotePitch('G', '###') 65 | Traceback (most recent call last): 66 | File "", line 1, in 67 | ... 68 | kord.errors.InvalidAlteration: ### 69 | ``` 70 | 71 | Similarly, the maximum octave value currently supported is 9. 72 | 73 | ``` 74 | >>> NotePitch('D', 10) 75 | Traceback (most recent call last): 76 | File "", line 1, in 77 | ... 78 | kord.errors.InvalidOctave: 10 79 | ``` 80 | 81 | ### Comparing NotePitch objects: 82 | 83 | The ```- < > <= >= == != >> ** ``` operators allow computation of semitone intervals between NotePitch instances and give insight into their enharmonic relationships. Let's take a quick look at each operator separately: 84 | 85 | #### `-` operator 86 | 87 | The substraction operator lets you compute the difference in semitones between two notes: 88 | 89 | ``` 90 | >>> f3 - e3 91 | 1 92 | >>> NotePitch('a', 'b', 2) - NotePitch('g', '#', 2) 93 | 0 94 | >>> NotePitch('a', 8) - NotePitch('c', 4) 95 | 57 96 | >>> NotePitch('a', 8) - NotePitch('c', '##', 4) 97 | 55 98 | ``` 99 | 100 | 101 | #### `<` `>` `<=` `>=` `==` `!=` operators 102 | 103 | Comparison operators return boolean values based *exclusively* on the interval between the 2 objects. 104 | 105 | ``` 106 | >>> f3 > e3 107 | True 108 | >>> f3 >= e3 109 | True 110 | ``` 111 | 112 | While the concept is seemingly straightforward, special attention needs to be taken when using `== !=` with enharmonic notes. 113 | 114 | ``` 115 | >>> n1 = NotePitch('F', '#', 5) 116 | >>> n2 = NotePitch('G', 'b', 5) 117 | >>> n1, n2 118 | (F♯⁵, G♭⁵) 119 | >>> n1 == n2 120 | True 121 | ``` 122 | 123 | The notes F♯⁵ and G♭⁵ are NOT the same but since their interval is a unison, the `==` comparison evaluates True. This might seem a bit counter-intuitive at first but you can still check for exact note matches with the use of 2 other operators. 124 | 125 | #### `>>` `**` operators 126 | 127 | The power and right-shift operators allow you to compare Notes for equality based not on their intervals, but on their intrinsic `chr`, `alt`, `oct` properties. The strictest operator `>>` compares all 3 attributes for equality while the looser `**` ignores `oct`: 128 | 129 | ``` 130 | >>> ab1, ab5 = NotePitch('A', 'b', 1), NotePitch('A', 'b', 5) 131 | >>> ab1 == ab5 132 | False 133 | >>> ab1 ** ab5 134 | True 135 | ``` 136 | 137 | Notice `**` evaluates True since both instances are A flat notes, even when there is a wide interval between them. 138 | 139 | ``` 140 | >>> ab1 >> ab5 141 | False 142 | >>> ab1.oct = 5 143 | >>> ab1 >> ab5 144 | True 145 | ``` 146 | 147 | For the `>>` operator to evaluate True, the octaves of the notes must match as well. 148 | 149 | 150 |
151 | 152 | 153 | 154 | 155 | ## kord.keys 156 | 157 | 158 | ### class TonalKey: 159 | 160 | Think of TonalKey objects as generators of NotePitch objects. You can define a new class which inherits TonalKey and use any theoretical arrangement of `intervals` from the root note in order to create chords, scales, modes, etc. You can further taylor these child classes by restricting `degrees` to specific values, this is very useful for creating chords. 161 | 162 | These are a couple of pre-defined examples to give you an idea of how it works: 163 | 164 | ``` 165 | class ChromaticScale(TonalKey): 166 | intervals = ( 167 | UNISON, 168 | MINOR_SECOND, 169 | MAJOR_SECOND, 170 | MINOR_THIRD, 171 | MAJOR_THIRD, 172 | PERFECT_FOURTH, 173 | AUGMENTED_FOURTH, 174 | PERFECT_FIFTH, 175 | MINOR_SIXTH, 176 | MAJOR_SIXTH, 177 | MINOR_SEVENTH, 178 | MAJOR_SEVENTH, 179 | ) 180 | 181 | class MajorScale(TonalKey): 182 | intervals = ( 183 | UNISON, 184 | MAJOR_SECOND, 185 | MAJOR_THIRD, 186 | PERFECT_FOURTH, 187 | PERFECT_FIFTH, 188 | MAJOR_SIXTH, 189 | MAJOR_SEVENTH, 190 | ) 191 | 192 | class MajorPentatonicScale(MajorScale): 193 | degrees = (1, 2, 3, 5, 6) 194 | 195 | class MajorTriad(MajorScale): 196 | degrees = (1, 3, 5) 197 | ``` 198 | 199 | Bare in mind that TonalKey objects are initialized with `chr` and `alt` attributes, `oct` values are not taken into consideration. This allows us to simply unpack NotePitch objects in order to create TonalKey instances based off of them. Once we have a TonalKey object, we can access it's single degrees using list index notation: 200 | 201 | ``` 202 | >>> from kord.keys import ChromaticScale 203 | >>> c = NotePitch('C') 204 | >>> c_chromatic_scale = ChromaticScale(*c) 205 | >>> c_chromatic_scale[2] 206 | C♯⁰ 207 | >>> c_chromatic_scale[12] 208 | B⁰ 209 | ``` 210 | 211 | ### TonalKey.spell() 212 | 213 | Retrieving individual degrees is good, but it is perhaps more interesting to look at a more dynamic way of getting notes out of our TonalKey instances. The `spell()` method provides such an interface for generating NotePitch instances on the fly. Let's take a look at a couple of examples and the several arguments that we can use when calling this method: 214 | 215 | ``` 216 | >>> for note in c_chromatic_scale.spell(): 217 | ... print(note, end=' ') 218 | ... 219 | C⁰ C♯⁰ D⁰ D♯⁰ E⁰ F⁰ F♯⁰ G⁰ G♯⁰ A⁰ A♯⁰ B⁰ C¹ >>> 220 | ``` 221 | 222 | As seen above the method will generate the first octave of degrees of the object when called without arguments. 223 | 224 | The `note_count` argument is an `int` and it allows us to set a specific amount of notes to retrieve: 225 | 226 | ``` 227 | >>> from kord.keys import MinorScale 228 | >>> a_minor_scale = MinorScale('A') 229 | >>> for note in a_minor_scale.spell(note_count=4): 230 | ... print(note, end=' ') 231 | ... 232 | A⁰ B⁰ C¹ D¹ >>> 233 | ``` 234 | 235 | Be careful, ask for too many notes and kord will throw and Exception when `oct` 9 has been exceeded. 236 | 237 | The `yield_all` argument is a `bool` that will make the method yield not just `NotePitch` instances, but also `None` objects for every non-diatonic semitone found: 238 | 239 | ``` 240 | >>> for note in a_minor_scale.spell(note_count=4, yield_all=True): 241 | ... print(note, end=' ') 242 | ... 243 | A⁰ None B⁰ C¹ None D¹ >>> 244 | ``` 245 | 246 | 247 | The `start_note` argument is a `NotePitch` object that can be used to start getting notes only after a specific note has been found. This can be done even if the note is not diatonic to the scale: 248 | 249 | ``` 250 | >>> Db1 = NotePitch('D', 'b', 1) 251 | >>> for note in a_minor_scale.spell(note_count=4, yield_all=True, start_note=Db1): 252 | ... print(note, end=' ') 253 | ... 254 | None D¹ None E¹ F¹ None G¹ >>> 255 | ``` 256 | 257 | 258 | 259 | ## fretboard tool 260 | 261 | A sample application `fretboard.py` comes built-in with `kord` and gives some insight into the possibilities of the framework. It displays a representation of your instrument's fretboard, tuned to your liking along with note patterns for any given mode (scale/chord) for any given root note. 262 | 263 | If you installed via pip (as opposed to cloning this repo) installation path will depend on your system, it's usually something like `/usr/local/fretboard` or `~/.local/fretboard`. You will also find a `tunings` directory with some pre-defined instrument tunings in the form of .json files. Feel free to modify them or add your own and they should immediately become available to the run-time. 264 | 265 | ``` 266 | $ python3 fretboard.py --help 267 | usage: fretboard.py [-h] [-d] [-s | -c ] [-i] [-t] [-f] [-v] root 268 | 269 | <<< Fretboard visualizer sample tool for the kord framework >>> 270 | 271 | positional arguments: 272 | root select key ROOT note 273 | 274 | optional arguments: 275 | -h, --help show this help message and exit 276 | -d, --degrees show degree numbers instead of note pitches 277 | -s , --scale major, minor, melodic_minor, harmonic_minor, major_pentatonic, minor_pentatonic, 278 | ionian, lydian, mixolydian, aeolian, dorian, phrygian, locrian, chromatic 279 | -c , --chord maj, min, aug, dim, maj7, min7, 7, dim7, min7dim5, maj9, min9, 9 280 | -i , --instrument banjo, guitar, ronroco, bass, ukulele 281 | -t , --tuning check .json files for available options 282 | -f , --frets 1, 2, .., 36 283 | -v , --verbosity 0, 1, 2 284 | ``` 285 | 286 | The only required parameter is the `root` note. 287 | 288 | The `--scale` and `--chord` options let you choose which note pattern to display for the selected root. They are mutually exclusive and the default value is `--chord maj` when left blank. 289 | 290 | The `--instrument` and `--tuning` options refer to the json files you will find in the tunings directory. Default values are `--instrument guitar --tuning standard`. 291 | 292 | The `--frets` option let's you choose how many frets to visualize, maximum value is 36. Default value will fill your terminal screen. 293 | 294 | The `--verbosity` option let's you choose how much information to see on-screen from 0 to 2. Default value is 1. 295 | 296 | The `--degrees` option let's you display degree numbers instead of notes. Default value is false. 297 | 298 | 299 | ## screenshots 300 | 301 | Here are a couple of pics to get you excited: 302 | 303 | ### Chords 304 | 305 | ![](https://github.com/synestematic/kord/blob/master/pngs/adim.png?raw=true) 306 | ![](https://github.com/synestematic/kord/blob/master/pngs/e7.png?raw=true) 307 | ![](https://github.com/synestematic/kord/blob/master/pngs/dim7.png?raw=true) 308 | 309 | ### Scales 310 | 311 | ![](https://github.com/synestematic/kord/blob/master/pngs/penta.png?raw=true) 312 | ![](https://github.com/synestematic/kord/blob/master/pngs/locrian.png?raw=true) 313 | ![](https://github.com/synestematic/kord/blob/master/pngs/lydian.png?raw=true) 314 | 315 | -------------------------------------------------------------------------------- /kord/parsers/test_chord_parser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from .chord_parser import ChordParser 4 | 5 | from ..keys.chords import ( 6 | PowerChord, Suspended4Chord, Suspended2Chord, 7 | MajorTriad, MinorTriad, AugmentedTriad, DiminishedTriad, 8 | MajorSixthChord, MinorSixthChord, 9 | MajorSeventhChord, MinorSeventhChord, DominantSeventhChord, 10 | HalfDiminishedSeventhChord, DiminishedSeventhChord, 11 | MajorAdd9Chord, MinorAdd9Chord, AugmentedAdd9Chord, DiminishedAdd9Chord, 12 | DominantNinthChord, DominantMinorNinthChord, 13 | MajorNinthChord, MinorNinthChord, 14 | ) 15 | 16 | from ..notes.constants import ( 17 | C_3, F_3, E_3, A_3, D_3, 18 | B_FLAT_3, E_FLAT_3, D_FLAT_3, C_FLAT_3, B_FLAT_3, 19 | A_SHARP_3, C_SHARP_3, E_SHARP_3, 20 | G_DOUBLE_FLAT_3, 21 | G_DOUBLE_SHARP_3, 22 | ) 23 | 24 | from ..errors import InvalidChord 25 | 26 | __all__ = [ 27 | 'ChordParserTest', 28 | 'NonThirdChordsTest', 29 | 'TriadChordsTest', 30 | 'SixthChordsTest', 31 | 'SeventhChordsTest', 32 | 'Add9ChordsTest', 33 | 'NinthChordsTest', 34 | ] 35 | 36 | 37 | class ChordParserTest(unittest.TestCase): 38 | 39 | def testInvalidChords(self): 40 | self.assertRaises(InvalidChord, ChordParser('').parse) 41 | self.assertRaises(InvalidChord, ChordParser('#').parse) 42 | self.assertRaises(InvalidChord, ChordParser('♯').parse) 43 | self.assertRaises(InvalidChord, ChordParser('♭').parse) 44 | self.assertRaises(InvalidChord, ChordParser('#F').parse) 45 | self.assertRaises(InvalidChord, ChordParser('♭B').parse) 46 | self.assertRaises(InvalidChord, ChordParser('H').parse) 47 | self.assertRaises(InvalidChord, ChordParser('h').parse) 48 | 49 | self.assertRaises(InvalidChord, ChordParser('Dsharp').parse) 50 | self.assertRaises(InvalidChord, ChordParser('Eflat').parse) 51 | self.assertRaises(InvalidChord, ChordParser('pmin7').parse) 52 | 53 | # self.assertRaises(InvalidChord, ChordParser('CB').parse) # should fail... 54 | # self.assertRaises(InvalidChord, ChordParser('Cqwe').parse) # should fail... 55 | 56 | 57 | class NonThirdChordsTest(unittest.TestCase): 58 | 59 | def testPowerChordClasses(self): 60 | assert isinstance(ChordParser('g5').parse(), PowerChord) 61 | 62 | def testPowerChordRoots(self): 63 | assert ChordParser('F5').parse().root ** F_3 64 | 65 | 66 | def testSuspended4ChordClasses(self): 67 | assert isinstance(ChordParser('D♭♭sus4').parse(), Suspended4Chord) 68 | assert isinstance(ChordParser('Fsus').parse(), Suspended4Chord) 69 | 70 | def testSuspended4ChordRoots(self): 71 | assert ChordParser('G𝄫sus4').parse().root ** G_DOUBLE_FLAT_3 72 | assert ChordParser('gbbsus').parse().root ** G_DOUBLE_FLAT_3 73 | 74 | 75 | def testSuspended2ChordClasses(self): 76 | assert isinstance(ChordParser('A♯sus2').parse(), Suspended2Chord) 77 | assert isinstance(ChordParser('Esus9').parse(), Suspended2Chord) 78 | 79 | def testSuspended4ChordRoots(self): 80 | assert ChordParser('G##sus2').parse().root ** G_DOUBLE_SHARP_3 81 | assert ChordParser('g𝄪sus9').parse().root ** G_DOUBLE_SHARP_3 82 | 83 | 84 | 85 | 86 | class TriadChordsTest(unittest.TestCase): 87 | 88 | def testMajorTriadClasses(self): 89 | assert isinstance(ChordParser('A').parse(), MajorTriad) 90 | assert isinstance(ChordParser('Dmaj').parse(), MajorTriad) 91 | assert isinstance(ChordParser('Emajor').parse(), MajorTriad) 92 | 93 | def testMajorTriadRoots(self): 94 | assert ChordParser('C').parse().root ** C_3 95 | assert ChordParser('Cmaj').parse().root ** C_3 96 | assert ChordParser('Cmajor').parse().root ** C_3 97 | 98 | # def testMajorTriadBass(self): 99 | # assert ChordParser('C').parse().bass ** C_3 100 | # assert ChordParser('Cmaj').parse().bass ** C_3 101 | # assert ChordParser('Cmajor').parse().bass ** C_3 102 | 103 | 104 | def testMinorTriadClasses(self): 105 | assert isinstance(ChordParser('Bmin').parse(), MinorTriad) 106 | assert isinstance(ChordParser('F-').parse(), MinorTriad) 107 | assert isinstance(ChordParser('Cminor').parse(), MinorTriad) 108 | 109 | def testMinorTriadRoots(self): 110 | assert ChordParser('Emin').parse().root ** E_3 111 | assert ChordParser('e-').parse().root ** E_3 112 | assert ChordParser('Eminor').parse().root ** E_3 113 | 114 | 115 | def testAugTriadClasses(self): 116 | assert isinstance(ChordParser('C##aug').parse(), AugmentedTriad) 117 | assert isinstance(ChordParser('Bbaugmented').parse(), AugmentedTriad) 118 | 119 | def testAugTriadRoots(self): 120 | assert ChordParser('Aaug').parse().root ** A_3 121 | assert ChordParser('Aaugmented').parse().root ** A_3 122 | 123 | 124 | def testDimTriadClasses(self): 125 | assert isinstance(ChordParser('D#dim').parse(), DiminishedTriad) 126 | assert isinstance(ChordParser('Bbdiminished').parse(), DiminishedTriad) 127 | 128 | def testDimTriadRoots(self): 129 | assert ChordParser('Ddim').parse().root ** D_3 130 | assert ChordParser('Ddiminished').parse().root ** D_3 131 | assert ChordParser('D dim').parse().root ** D_3 132 | 133 | 134 | class SixthChordsTest(unittest.TestCase): 135 | 136 | def testMajorSixthChordClasses(self): 137 | assert isinstance(ChordParser('D#6').parse(), MajorSixthChord) 138 | assert isinstance(ChordParser('Fadd6').parse(), MajorSixthChord) 139 | 140 | def testMajorSixthChordRoots(self): 141 | assert ChordParser('Bb6').parse().root ** B_FLAT_3 142 | assert ChordParser('B♭add6').parse().root ** B_FLAT_3 143 | 144 | 145 | def testMinorSixthChordClasses(self): 146 | assert isinstance(ChordParser('Ebm6').parse(), MinorSixthChord) 147 | assert isinstance(ChordParser('Fmin6').parse(), MinorSixthChord) 148 | 149 | def testMinorSixthChordRoots(self): 150 | assert ChordParser('A♯m6').parse().root ** A_SHARP_3 151 | assert ChordParser('A#min6').parse().root ** A_SHARP_3 152 | 153 | 154 | class SeventhChordsTest(unittest.TestCase): 155 | 156 | def testMajorSeventhChordClasses(self): 157 | assert isinstance(ChordParser('Amaj7').parse(), MajorSeventhChord) 158 | assert isinstance(ChordParser('BM7').parse(), MajorSeventhChord) 159 | assert isinstance(ChordParser('CΔ7').parse(), MajorSeventhChord) 160 | assert isinstance(ChordParser('Dmajor7').parse(), MajorSeventhChord) 161 | 162 | def testMajorSeventhChordRoots(self): 163 | assert ChordParser('Cmaj7').parse().root ** C_3 164 | assert ChordParser('CM7').parse().root ** C_3 165 | assert ChordParser('CΔ7').parse().root ** C_3 166 | assert ChordParser('Cmajor7').parse().root ** C_3 167 | 168 | 169 | def testMinorSeventhChordClasses(self): 170 | assert isinstance(ChordParser('Emin7').parse(), MinorSeventhChord) 171 | assert isinstance(ChordParser('Fm7').parse(), MinorSeventhChord) 172 | assert isinstance(ChordParser('G-7').parse(), MinorSeventhChord) 173 | assert isinstance(ChordParser('Bbminor7').parse(), MinorSeventhChord) 174 | 175 | def testMinorSeventhChordRoots(self): 176 | assert ChordParser('Amin7').parse().root ** A_3 177 | assert ChordParser('Am7').parse().root ** A_3 178 | assert ChordParser('A-7').parse().root ** A_3 179 | assert ChordParser('Aminor7').parse().root ** A_3 180 | assert ChordParser('A minor7').parse().root ** A_3 181 | 182 | 183 | def testDomSeventhChordClasses(self): 184 | assert isinstance(ChordParser('F7').parse(), DominantSeventhChord) 185 | assert isinstance(ChordParser('Bbdom7').parse(), DominantSeventhChord) 186 | assert isinstance(ChordParser('Dbbdominant7').parse(), DominantSeventhChord) 187 | 188 | def testDomSeventhChordRoots(self): 189 | assert ChordParser('C#7').parse().root ** C_SHARP_3 190 | assert ChordParser('C♯dom7').parse().root ** C_SHARP_3 191 | assert ChordParser('c#dominant7').parse().root ** C_SHARP_3 192 | 193 | 194 | def testHalfDiminishedSeventhChordClasses(self): 195 | assert isinstance(ChordParser('Em7b5').parse(), HalfDiminishedSeventhChord) 196 | assert isinstance(ChordParser('F#m7-5').parse(), HalfDiminishedSeventhChord) 197 | assert isinstance(ChordParser('Gmin7dim5').parse(), HalfDiminishedSeventhChord) 198 | assert isinstance(ChordParser('Gbm7(b5)').parse(), HalfDiminishedSeventhChord) 199 | assert isinstance(ChordParser('fø7').parse(), HalfDiminishedSeventhChord) 200 | 201 | def testHalfDiminishedSeventhChordRoots(self): 202 | assert ChordParser('ebm7b5').parse().root ** E_FLAT_3 203 | assert ChordParser('E♭m7-5').parse().root ** E_FLAT_3 204 | assert ChordParser('Ebmin7dim5').parse().root ** E_FLAT_3 205 | assert ChordParser('Ebm7(b5)').parse().root ** E_FLAT_3 206 | assert ChordParser('Ebø7').parse().root ** E_FLAT_3 207 | 208 | 209 | def testDiminishedSeventhChordClasses(self): 210 | assert isinstance(ChordParser('fdim7').parse(), DiminishedSeventhChord) 211 | assert isinstance(ChordParser('ao7').parse(), DiminishedSeventhChord) 212 | assert isinstance(ChordParser('Cdiminished7').parse(), DiminishedSeventhChord) 213 | 214 | def testDiminishedSeventhChordRoots(self): 215 | assert ChordParser('Dbdim7').parse().root ** D_FLAT_3 216 | assert ChordParser('Dbo7').parse().root ** D_FLAT_3 217 | assert ChordParser('Dbdiminished7').parse().root ** D_FLAT_3 218 | 219 | 220 | class Add9ChordsTest(unittest.TestCase): 221 | 222 | def testMajorAdd9ChordClasses(self): 223 | assert isinstance(ChordParser('bAdd9').parse(), MajorAdd9Chord) 224 | assert isinstance(ChordParser('Aadd9').parse(), MajorAdd9Chord) 225 | 226 | def testMajorAdd9ChordRoots(self): 227 | assert ChordParser('Dbadd9').parse().root ** D_FLAT_3 228 | assert ChordParser('d♭Add9').parse().root ** D_FLAT_3 229 | 230 | 231 | def testMinorAdd9ChordClasses(self): 232 | assert isinstance(ChordParser('Emadd9').parse(), MinorAdd9Chord) 233 | assert isinstance(ChordParser('GmAdd9').parse(), MinorAdd9Chord) 234 | 235 | def testMinorAdd9ChordRoots(self): 236 | assert ChordParser('Cbmadd9').parse().root ** C_FLAT_3 237 | assert ChordParser('C♭mAdd9').parse().root ** C_FLAT_3 238 | 239 | 240 | def testAugmentedAdd9ChordClasses(self): 241 | assert isinstance(ChordParser('f#augadd9').parse(), AugmentedAdd9Chord) 242 | assert isinstance(ChordParser('D#augAdd9').parse(), AugmentedAdd9Chord) 243 | 244 | def testAugmentedAdd9ChordRoots(self): 245 | assert ChordParser('E♯augadd9').parse().root ** E_SHARP_3 246 | assert ChordParser('E#augAdd9').parse().root ** E_SHARP_3 247 | 248 | 249 | def testDiminishedAdd9ChordClasses(self): 250 | assert isinstance(ChordParser('Cdimadd9').parse(), DiminishedAdd9Chord) 251 | assert isinstance(ChordParser('F#dimAdd9').parse(), DiminishedAdd9Chord) 252 | 253 | def testDiminishedAdd9ChordRoots(self): 254 | assert ChordParser('E♯dimadd9').parse().root ** E_SHARP_3 255 | assert ChordParser('E#dimAdd9').parse().root ** E_SHARP_3 256 | 257 | 258 | class NinthChordsTest(unittest.TestCase): 259 | 260 | def testMajorNinthChordClasses(self): 261 | assert isinstance(ChordParser('D#maj9').parse(), MajorNinthChord) 262 | assert isinstance(ChordParser('FM9').parse(), MajorNinthChord) 263 | assert isinstance(ChordParser('Emajor9').parse(), MajorNinthChord) 264 | 265 | def testMajorNinthChordRoots(self): 266 | assert ChordParser('Bbmaj7').parse().root ** B_FLAT_3 267 | assert ChordParser('B♭M7').parse().root ** B_FLAT_3 268 | assert ChordParser('b♭major7').parse().root ** B_FLAT_3 269 | 270 | 271 | def testMinorNinthChordClasses(self): 272 | assert isinstance(ChordParser('Ebmin9').parse(), MinorNinthChord) 273 | assert isinstance(ChordParser('Fm9').parse(), MinorNinthChord) 274 | assert isinstance(ChordParser('G#-9').parse(), MinorNinthChord) 275 | assert isinstance(ChordParser('Bminor9').parse(), MinorNinthChord) 276 | 277 | def testMinorNinthChordRoots(self): 278 | assert ChordParser('A♯min9').parse().root ** A_SHARP_3 279 | assert ChordParser('A#m9').parse().root ** A_SHARP_3 280 | assert ChordParser('A#-9').parse().root ** A_SHARP_3 281 | assert ChordParser('A♯minor9').parse().root ** A_SHARP_3 282 | assert ChordParser('A# min9').parse().root ** A_SHARP_3 283 | 284 | 285 | def testDomNinthChordClasses(self): 286 | assert isinstance(ChordParser('F9').parse(), DominantNinthChord) 287 | assert isinstance(ChordParser('Bbdom9').parse(), DominantNinthChord) 288 | 289 | def testDomNinthChordRoots(self): 290 | assert ChordParser('C#9').parse().root ** C_SHARP_3 291 | assert ChordParser('C♯dom9').parse().root ** C_SHARP_3 292 | 293 | 294 | def testDomMinNinthChordClasses(self): 295 | assert isinstance(ChordParser('E7b9').parse(), DominantMinorNinthChord) 296 | 297 | def testDomMinNinthChordRoots(self): 298 | assert ChordParser('eb7b9').parse().root ** E_FLAT_3 299 | 300 | 301 | -------------------------------------------------------------------------------- /kord/keys/scales.py: -------------------------------------------------------------------------------- 1 | from bestia.output import Row, FString 2 | 3 | from ..notes import NotePitch 4 | from ..notes.intervals import Intervals 5 | 6 | from ..errors import InvalidNote, InvalidOctave 7 | 8 | __all__ = [ 9 | 'TonalKey', 10 | 'MajorScale', 11 | 'MajorPentatonicScale', 12 | 'AugmentedScale', 13 | 'IonianMode', 14 | 'MixolydianMode', 15 | 'LydianMode', 16 | 'MinorScale', 17 | 'DiminishedScale', 18 | 'MinorPentatonicScale', 19 | 'MelodicMinorScale', 20 | 'HarmonicMinorScale', 21 | 'AeolianMode', 22 | 'DorianMode', 23 | 'PhrygianMode', 24 | 'LocrianMode', 25 | 'ChromaticScale', 26 | ] 27 | 28 | 29 | class TonalKey: 30 | 31 | notations = () 32 | intervals = () 33 | degrees = () 34 | 35 | @classmethod 36 | def _calc_intervals(cls): 37 | return cls.intervals 38 | 39 | 40 | @classmethod 41 | def allowed_degrees(cls): 42 | ''' returns list of each degree, once ''' 43 | if cls.degrees: 44 | return list(cls.degrees) 45 | return [ n+1 for n in range( len(cls._calc_intervals()) ) ] 46 | 47 | 48 | @classmethod 49 | def all_degrees(cls): 50 | ''' returns list of each degree, over all octaves ''' 51 | degrees = cls.allowed_degrees() 52 | 53 | # floors over-octave degrees into in-octave 54 | for d, deg in enumerate(degrees): 55 | if deg > len(cls._calc_intervals()): 56 | degrees[d] -= len(cls._calc_intervals()) 57 | # ie. 9th => 2nd 58 | # ie. 11th => 4th 59 | 60 | # remove duplicates 61 | degrees = list( set(degrees) ) 62 | 63 | # sort order 64 | degrees.sort() 65 | 66 | # calculate all possible octaves 67 | all_degrees = [] 68 | for o in range(NotePitch.MAXIMUM_OCTAVE): 69 | for deg in degrees: 70 | all_degrees.append( 71 | deg + len(cls._calc_intervals()) * o 72 | ) 73 | 74 | return tuple(all_degrees) 75 | 76 | 77 | @classmethod 78 | def name(cls): 79 | n = '' 80 | for c in cls.__name__: 81 | if c.isupper(): 82 | n += ' ' 83 | n += c.lower() 84 | return n.strip() 85 | 86 | 87 | @classmethod 88 | def __possible_root_notes(cls, valids=True): 89 | ''' checks all possible notes for validity as root of given key ''' 90 | valid_roots = [] 91 | invalid_roots = [] 92 | 93 | for note in NotePitch.notes_by_alts(): 94 | 95 | try: 96 | invalid_root = False 97 | for _ in cls(*note).spell( 98 | note_count=len(cls._calc_intervals()) +1, 99 | yield_all=False, 100 | ): 101 | # if any degree fails, scale is not spellable 102 | pass 103 | 104 | except InvalidNote: 105 | invalid_root = True 106 | 107 | finally: 108 | if invalid_root: 109 | invalid_roots.append(note) 110 | else: 111 | valid_roots.append(note) 112 | 113 | return valid_roots if valids else invalid_roots 114 | 115 | 116 | @classmethod 117 | def valid_root_notes(cls): 118 | ''' returns only valid root notes for given key class ''' 119 | return cls.__possible_root_notes(valids=True) 120 | 121 | 122 | @classmethod 123 | def invalid_root_notes(cls): 124 | ''' returns only invalid root notes for given key class ''' 125 | return cls.__possible_root_notes(valids=False) 126 | 127 | 128 | def __init__(self, chr, alt='', oct=0): 129 | self.root = NotePitch(chr, alt, 0) # ignore note.oct 130 | 131 | 132 | def __repr__(self): 133 | ''' prints first octave of NotePitch items ''' 134 | spell_line = Row() 135 | for degree in self.spell( 136 | note_count=None, yield_all=False 137 | ): 138 | spell_line.append( FString(degree, size=5) ) 139 | spell_line.append( f'{type(self)} on {self.root}'[:-1] ) 140 | return str(spell_line) 141 | 142 | 143 | def __getitem__(self, d): 144 | 145 | if d < 1: 146 | return False 147 | 148 | if self.degrees and d not in self.all_degrees(): 149 | return None 150 | 151 | if d == 1: 152 | return self.root 153 | 154 | # GET DEGREE's ROOT OFFSETS = PERFECT_OCTAVE + SPARE_STS 155 | octs_from_root, spare_sts = divmod( 156 | self.degree_root_interval(d), Intervals.PERFECT_OCTAVE 157 | ) 158 | note_oct = octs_from_root 159 | 160 | # GET DEGREE PROPERTIES FROM ENHARMONIC MATRIX 161 | next_notes = [ 162 | n for n in NotePitch.EnharmonicMatrix()[ 163 | self.root.enharmonic_row + spare_sts 164 | ] if n.chr == self.root.adjacent_chr(d - 1) # EXPECTED TONE 165 | ] 166 | 167 | if len(next_notes) == 1: 168 | # AT THIS POINT DEG_OCT CAN EITHER STAY | +1 169 | if self.root.oversteps_oct(next_notes[0]): 170 | note_oct += 1 171 | 172 | # RETURN NEW OBJECT, DO NOT CHANGE ENHARMONIC MATRIX ITEM! 173 | return NotePitch(next_notes[0].chr, next_notes[0].alt, note_oct) 174 | 175 | raise InvalidNote 176 | 177 | 178 | def validate(self): 179 | for note in self.invalid_root_notes(): 180 | if note ** self.root: 181 | return False 182 | return True 183 | 184 | 185 | def degree_root_interval(self, d): 186 | ''' return degree's delta semitones from key's root ''' 187 | if d > len(self._calc_intervals()): 188 | return self.degree_root_interval( 189 | d - len(self._calc_intervals()) 190 | ) + Intervals.PERFECT_OCTAVE 191 | return self._calc_intervals()[d -1] 192 | 193 | 194 | def _count_notes(self, note_count=-1, start_note=None, yield_all=True): 195 | ''' * returns when note_count == 0 196 | * positive note_count yields notes exactly 197 | * negative note_count yields notes indefinetly 198 | * filters received Nones when needed 199 | ''' 200 | if not start_note: 201 | start_note = self.root 202 | 203 | for note in self.__solmizate(start_note=start_note): 204 | 205 | if note_count == 0: 206 | return 207 | 208 | if note: 209 | yield note 210 | note_count -= 1 211 | 212 | elif yield_all: 213 | yield None 214 | 215 | 216 | def __solmizate(self, start_note): 217 | ''' uses __get_item__ to retrieve notes to yield: 218 | before yielding a note, calulates semitone 219 | difference with previous note and yields Nones 220 | 221 | * yields based on all_degrees 222 | * always yields Nones 223 | * changes octs by using self[d] 224 | * start_note (diatonic, non-diatonic) 225 | ''' 226 | degrees = self.all_degrees() 227 | 228 | for d, _ in enumerate(degrees): 229 | 230 | this = self[ degrees[d] ] 231 | 232 | if this < start_note: 233 | ## start_note not yet reached 234 | continue 235 | 236 | ##################################################### 237 | # calculate Nones to yield for non-diatonic semitones 238 | ##################################################### 239 | if this != start_note: # do not yield the Nones before starting note 240 | 241 | prev = self[ degrees[d-1] ] 242 | if prev >= start_note: 243 | # dont yield last st, yield the note below 244 | last_interval = this - prev - 1 245 | 246 | else: 247 | # to calculate Nones to yield, dont go all way back to prev 248 | # when prev is lower than starting note 249 | last_interval = this - start_note 250 | 251 | for semitone in range(last_interval): 252 | yield None 253 | 254 | yield this 255 | 256 | 257 | def spell(self, note_count=None, start_note=None, yield_all=False): 258 | if note_count == None: 259 | note_count = len( 260 | self.degrees if self.degrees else self._calc_intervals() 261 | ) + 1 # also include next octave root note 262 | try: 263 | return self._count_notes( 264 | note_count=note_count, 265 | start_note=start_note, 266 | yield_all=yield_all, 267 | ) 268 | except InvalidOctave: 269 | pass 270 | 271 | 272 | ######################## 273 | ### MAJOR KEYS/MODES ### 274 | ######################## 275 | 276 | class MajorScale(TonalKey): 277 | notations = ( 278 | 'major', 279 | ) 280 | intervals = ( 281 | Intervals.UNISON, 282 | Intervals.MAJOR_SECOND, 283 | Intervals.MAJOR_THIRD, 284 | Intervals.PERFECT_FOURTH, 285 | Intervals.PERFECT_FIFTH, 286 | Intervals.MAJOR_SIXTH, 287 | Intervals.MAJOR_SEVENTH, 288 | ) 289 | 290 | class MajorPentatonicScale(MajorScale): 291 | notations = ( 292 | 'major_pentatonic', 293 | ) 294 | degrees = (1, 2, 3, 5, 6) 295 | 296 | class IonianMode(MajorScale): 297 | notations = ( 298 | 'ionian', 299 | ) 300 | 301 | class MixolydianMode(MajorScale): 302 | notations = ( 303 | 'mixolydian', 304 | ) 305 | intervals = ( 306 | Intervals.UNISON, 307 | Intervals.MAJOR_SECOND, 308 | Intervals.MAJOR_THIRD, 309 | Intervals.PERFECT_FOURTH, 310 | Intervals.PERFECT_FIFTH, 311 | Intervals.MAJOR_SIXTH, 312 | Intervals.MINOR_SEVENTH, # <<< 313 | ) 314 | 315 | class LydianMode(MajorScale): 316 | notations = ( 317 | 'lydian', 318 | ) 319 | intervals = ( 320 | Intervals.UNISON, 321 | Intervals.MAJOR_SECOND, 322 | Intervals.MAJOR_THIRD, 323 | Intervals.AUGMENTED_FOURTH, # <<< 324 | Intervals.PERFECT_FIFTH, 325 | Intervals.MAJOR_SIXTH, 326 | Intervals.MAJOR_SEVENTH, 327 | ) 328 | 329 | ######################## 330 | ### MINOR KEYS/MODES ### 331 | ######################## 332 | 333 | class MinorScale(TonalKey): 334 | notations = ( 335 | 'minor', 336 | ) 337 | intervals = ( 338 | Intervals.UNISON, 339 | Intervals.MAJOR_SECOND, 340 | Intervals.MINOR_THIRD, 341 | Intervals.PERFECT_FOURTH, 342 | Intervals.PERFECT_FIFTH, 343 | Intervals.MINOR_SIXTH, 344 | Intervals.MINOR_SEVENTH, 345 | ) 346 | 347 | class MinorPentatonicScale(MinorScale): 348 | notations = ( 349 | 'minor_pentatonic', 350 | ) 351 | degrees = (1, 3, 4, 5, 7) 352 | 353 | class MelodicMinorScale(MinorScale): 354 | notations = ( 355 | 'melodic_minor', 356 | ) 357 | intervals = ( 358 | Intervals.UNISON, 359 | Intervals.MAJOR_SECOND, 360 | Intervals.MINOR_THIRD, 361 | Intervals.PERFECT_FOURTH, 362 | Intervals.PERFECT_FIFTH, 363 | Intervals.MAJOR_SIXTH, # <<< 364 | Intervals.MAJOR_SEVENTH, # <<< 365 | ) 366 | 367 | class HarmonicMinorScale(MinorScale): 368 | notations = ( 369 | 'harmonic_minor', 370 | ) 371 | intervals = ( 372 | Intervals.UNISON, 373 | Intervals.MAJOR_SECOND, 374 | Intervals.MINOR_THIRD, 375 | Intervals.PERFECT_FOURTH, 376 | Intervals.PERFECT_FIFTH, 377 | Intervals.MINOR_SIXTH, 378 | Intervals.MAJOR_SEVENTH, # <<< 379 | ) 380 | 381 | class AeolianMode(MinorScale): 382 | notations = ( 383 | 'aeolian', 384 | ) 385 | 386 | class DorianMode(MinorScale): 387 | notations = ( 388 | 'dorian', 389 | ) 390 | intervals = ( 391 | Intervals.UNISON, 392 | Intervals.MAJOR_SECOND, 393 | Intervals.MINOR_THIRD, 394 | Intervals.PERFECT_FOURTH, 395 | Intervals.PERFECT_FIFTH, 396 | Intervals.MAJOR_SIXTH, # <<< 397 | Intervals.MINOR_SEVENTH, 398 | ) 399 | 400 | class PhrygianMode(MinorScale): 401 | notations = ( 402 | 'phrygian', 403 | ) 404 | intervals = ( 405 | Intervals.UNISON, 406 | Intervals.MINOR_SECOND, # <<< 407 | Intervals.MINOR_THIRD, 408 | Intervals.PERFECT_FOURTH, 409 | Intervals.PERFECT_FIFTH, 410 | Intervals.MINOR_SIXTH, 411 | Intervals.MINOR_SEVENTH, 412 | ) 413 | 414 | class LocrianMode(MinorScale): 415 | notations = ( 416 | 'locrian', 417 | ) 418 | intervals = ( 419 | Intervals.UNISON, 420 | Intervals.MINOR_SECOND, # <<< 421 | Intervals.MINOR_THIRD, 422 | Intervals.PERFECT_FOURTH, 423 | Intervals.DIMINISHED_FIFTH, # <<< 424 | Intervals.MINOR_SIXTH, 425 | Intervals.MINOR_SEVENTH, 426 | ) 427 | 428 | 429 | ##################### 430 | ### CHROMATIC KEY ### 431 | ##################### 432 | 433 | class ChromaticScale(TonalKey): 434 | notations = ( 435 | 'chromatic', 436 | ) 437 | intervals = ( 438 | Intervals.UNISON, 439 | Intervals.MINOR_SECOND, 440 | Intervals.MAJOR_SECOND, 441 | Intervals.MINOR_THIRD, 442 | Intervals.MAJOR_THIRD, 443 | Intervals.PERFECT_FOURTH, 444 | Intervals.AUGMENTED_FOURTH, 445 | Intervals.PERFECT_FIFTH, 446 | Intervals.MINOR_SIXTH, 447 | Intervals.MAJOR_SIXTH, 448 | Intervals.MINOR_SEVENTH, 449 | Intervals.MAJOR_SEVENTH, 450 | ) 451 | 452 | def __getitem__(self, d): 453 | 454 | if d < 1: 455 | return False 456 | 457 | if d == 1: 458 | return self.root 459 | 460 | # GET DEGREE's ROOT OFFSETS = PERFECT_OCTAVE + SPARE_STS 461 | octs_from_root, spare_sts = divmod( 462 | self.degree_root_interval(d), Intervals.PERFECT_OCTAVE 463 | ) 464 | note_oct = octs_from_root 465 | 466 | # GET DEGREE PROPERTIES FROM ENHARMONIC MATRIX 467 | # DO I REALLY NEED THESE 3 CHECKS ? 468 | # MATCH ROOT_TONE 469 | next_notes = [ 470 | n for n in NotePitch.EnharmonicMatrix()[ 471 | self.root.enharmonic_row + spare_sts 472 | ] if n ** self.root 473 | ] 474 | 475 | if not next_notes: 476 | # MATCH ROOT_ALT 477 | next_notes = [ 478 | n for n in NotePitch.EnharmonicMatrix()[ 479 | self.root.enharmonic_row + spare_sts 480 | ] if n.alt == self.root.alt[:-1] 481 | ] 482 | 483 | if not next_notes: 484 | # CHOOSE "#" or "" 485 | chosen_alt = '#' if self.root.alt == '' else self.root.alt 486 | next_notes = [ 487 | n for n in NotePitch.EnharmonicMatrix()[ 488 | self.root.enharmonic_row + spare_sts 489 | ] if n.alt == chosen_alt 490 | ] 491 | 492 | if len(next_notes) == 1: 493 | # AT THIS POINT DEG_OCT CAN EITHER STAY | +1 494 | if self.root.oversteps_oct(next_notes[0]): 495 | note_oct += 1 496 | 497 | # RETURN NEW OBJECT, DO NOT CHANGE ENHARMONIC MATRIX ITEM! 498 | return NotePitch(next_notes[0].chr, next_notes[0].alt, note_oct) 499 | 500 | raise InvalidNote 501 | 502 | 503 | class DiminishedScale(ChromaticScale): 504 | notations = ( 505 | 'diminished', 506 | ) 507 | degrees = (1, 3, 4, 6, 7, 9, 10, 12) 508 | 509 | # TODO: decide which AugmentedScale definition works best 510 | 511 | class AugmentedScale(ChromaticScale): 512 | ''' chromatic scale as parent enables augmented scales for __any__ root 513 | ''' 514 | notations = ( 515 | 'augmented', 516 | ) 517 | degrees = (1, 4, 5, 8, 9, 12) 518 | 519 | # class AugmentedScale(TonalKey): 520 | # ''' tonal scale as parent allows for combination of flats and sharps 521 | # ''' 522 | # notations = ( 523 | # 'augmented', 524 | # ) 525 | # intervals = ( 526 | # Intervals.UNISON, # C 527 | # Intervals.AUGMENTED_SECOND, # D# 528 | # Intervals.MAJOR_THIRD, # E 529 | # Intervals.PERFECT_FOURTH, # F exclude this interval using degrees... 530 | # Intervals.PERFECT_FIFTH, # G 531 | # Intervals.MINOR_SIXTH, # Ab 532 | # Intervals.MAJOR_SEVENTH, # B 533 | # ) 534 | # degrees = (1, 2, 3, 5, 6, 7) 535 | -------------------------------------------------------------------------------- /kord/keys/test_chords.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from .chords import ( 4 | PowerChord, 5 | Suspended4Chord, Suspended2Chord, 6 | MajorTriad, MinorTriad, AugmentedTriad, DiminishedTriad, 7 | MajorSixthChord, MinorSixthChord, 8 | MajorSeventhChord, MinorSeventhChord, DominantSeventhChord, 9 | DiminishedSeventhChord, HalfDiminishedSeventhChord, 10 | MajorAdd9Chord, MinorAdd9Chord, AugmentedAdd9Chord, DiminishedAdd9Chord, 11 | MajorNinthChord, MinorNinthChord, 12 | DominantNinthChord, DominantMinorNinthChord, 13 | ) 14 | 15 | from ..notes.constants import ( 16 | A_0, A_1, A_2, B_0, B_1, C_0, C_1, C_2, C_3, D_0, D_1, E_0, E_1, F_0, F_1, G_0, G_1, 17 | E_FLAT_0, E_FLAT_1, B_FLAT_0, B_FLAT_1, G_FLAT_0, G_FLAT_1, D_FLAT_0, D_FLAT_1, 18 | G_SHARP_0, G_SHARP_1, 19 | B_DOUBLE_FLAT_0, B_DOUBLE_FLAT_1, 20 | ) 21 | 22 | from ..errors import InvalidNote, InvalidAlteration, InvalidOctave, InvalidChord 23 | 24 | __all__ = [ 25 | 'ChordTest', 26 | ] 27 | 28 | 29 | class ChordTest(unittest.TestCase): 30 | 31 | def testPowerChord(self): 32 | chord = PowerChord(*C_3) 33 | assert not chord[0] , chord[0] 34 | assert chord[1] >> C_0, chord[1] 35 | assert not chord[2] , chord[2] 36 | assert not chord[3] , chord[3] 37 | assert not chord[4] , chord[4] 38 | assert chord[5] >> G_0, chord[5] 39 | assert not chord[6] , chord[6] 40 | assert not chord[7] , chord[7] 41 | assert chord[8] >> C_1, chord[8] 42 | assert not chord[9] , chord[9] 43 | assert not chord[10] , chord[10] 44 | assert not chord[11] , chord[11] 45 | assert chord[12] >> G_1, chord[12] 46 | assert not chord[13] , chord[13] 47 | assert not chord[14] , chord[14] 48 | assert chord[15] >> C_2, chord[15] 49 | 50 | 51 | def testSuspended4Chord(self): 52 | chord = Suspended4Chord(*C_3) 53 | assert not chord[0] , chord[0] 54 | assert chord[1] >> C_0, chord[1] 55 | assert not chord[2] , chord[2] 56 | assert not chord[3] , chord[3] 57 | assert chord[4] >> F_0, chord[4] 58 | assert chord[5] >> G_0, chord[5] 59 | assert not chord[6] , chord[6] 60 | assert not chord[7] , chord[7] 61 | assert chord[8] >> C_1, chord[8] 62 | assert not chord[9] , chord[9] 63 | assert not chord[10] , chord[10] 64 | assert chord[11] >> F_1, chord[11] 65 | assert chord[12] >> G_1, chord[12] 66 | assert not chord[13] , chord[13] 67 | assert not chord[14] , chord[14] 68 | assert chord[15] >> C_2, chord[15] 69 | 70 | 71 | def testSuspended2Chord(self): 72 | chord = Suspended2Chord(*C_3) 73 | assert not chord[0] , chord[0] 74 | assert chord[1] >> C_0, chord[1] 75 | assert chord[2] >> D_0, chord[2] 76 | assert not chord[3] , chord[3] 77 | assert not chord[4] , chord[4] 78 | assert chord[5] >> G_0, chord[5] 79 | assert not chord[6] , chord[6] 80 | assert not chord[7] , chord[7] 81 | assert chord[8] >> C_1, chord[8] 82 | assert chord[9] >> D_1, chord[9] 83 | assert not chord[10] , chord[10] 84 | assert not chord[11] , chord[11] 85 | assert chord[12] >> G_1, chord[12] 86 | assert not chord[13] , chord[13] 87 | assert not chord[14] , chord[14] 88 | assert chord[15] >> C_2, chord[15] 89 | 90 | 91 | def testMajorTriad(self): 92 | chord = MajorTriad(*C_3) 93 | assert not chord[0] , chord[0] 94 | assert chord[1] >> C_0, chord[1] 95 | assert not chord[2] , chord[2] 96 | assert chord[3] >> E_0, chord[3] 97 | assert not chord[4] , chord[4] 98 | assert chord[5] >> G_0, chord[5] 99 | assert not chord[6] , chord[6] 100 | assert not chord[7] , chord[7] 101 | assert chord[8] >> C_1, chord[8] 102 | assert not chord[9] , chord[9] 103 | assert chord[10] >> E_1, chord[10] 104 | assert not chord[11] , chord[11] 105 | assert chord[12] >> G_1, chord[12] 106 | assert not chord[13] , chord[13] 107 | assert not chord[14] , chord[14] 108 | assert chord[15] >> C_2, chord[15] 109 | 110 | 111 | def testMinorTriad(self): 112 | chord = MinorTriad(*C_3) 113 | assert not chord[0] , chord[0] 114 | assert chord[1] >> C_0, chord[1] 115 | assert not chord[2] , chord[2] 116 | assert chord[3] >> E_FLAT_0, chord[3] 117 | assert not chord[4] , chord[4] 118 | assert chord[5] >> G_0, chord[5] 119 | assert not chord[6] , chord[6] 120 | assert not chord[7] , chord[7] 121 | assert chord[8] >> C_1, chord[8] 122 | assert not chord[9] , chord[9] 123 | assert chord[10] >> E_FLAT_1, chord[10] 124 | assert not chord[11] , chord[11] 125 | assert chord[12] >> G_1, chord[12] 126 | assert not chord[13] , chord[13] 127 | assert not chord[14] , chord[14] 128 | assert chord[15] >> C_2, chord[15] 129 | 130 | 131 | def testAugmentedTriad(self): 132 | chord = AugmentedTriad(*C_3) 133 | assert not chord[0] , chord[0] 134 | assert chord[1] >> C_0, chord[1] 135 | assert not chord[2] , chord[2] 136 | assert chord[3] >> E_0, chord[3] 137 | assert not chord[4] , chord[4] 138 | assert chord[5] >> G_SHARP_0, chord[5] 139 | assert not chord[6] , chord[6] 140 | assert not chord[7] , chord[7] 141 | assert chord[8] >> C_1, chord[8] 142 | assert not chord[9] , chord[9] 143 | assert chord[10] >> E_1, chord[10] 144 | assert not chord[11] , chord[11] 145 | assert chord[12] >> G_SHARP_1, chord[12] 146 | assert not chord[13] , chord[13] 147 | assert not chord[14] , chord[14] 148 | assert chord[15] >> C_2, chord[15] 149 | 150 | 151 | def testDiminishedTriad(self): 152 | chord = DiminishedTriad(*C_3) 153 | assert not chord[0] , chord[0] 154 | assert chord[1] >> C_0, chord[1] 155 | assert not chord[2] , chord[2] 156 | assert chord[3] >> E_FLAT_0, chord[3] 157 | assert not chord[4] , chord[4] 158 | assert chord[5] >> G_FLAT_0, chord[5] 159 | assert not chord[6] , chord[6] 160 | assert not chord[7] , chord[7] 161 | assert chord[8] >> C_1, chord[8] 162 | assert not chord[9] , chord[9] 163 | assert chord[10] >> E_FLAT_1, chord[10] 164 | assert not chord[11] , chord[11] 165 | assert chord[12] >> G_FLAT_1, chord[12] 166 | assert not chord[13] , chord[13] 167 | assert not chord[14] , chord[14] 168 | assert chord[15] >> C_2, chord[15] 169 | 170 | 171 | def testMajorSixthChordChord(self): 172 | chord = MajorSixthChord(*C_3) 173 | assert not chord[0] , chord[0] 174 | assert chord[1] >> C_0, chord[1] 175 | assert not chord[2] , chord[2] 176 | assert chord[3] >> E_0, chord[3] 177 | assert not chord[4] , chord[4] 178 | assert chord[5] >> G_0, chord[5] 179 | assert chord[6] >> A_0, chord[6] 180 | assert not chord[7] , chord[7] 181 | assert chord[8] >> C_1, chord[8] 182 | assert not chord[9] , chord[9] 183 | assert chord[10] >> E_1, chord[10] 184 | assert not chord[11] , chord[11] 185 | assert chord[12] >> G_1, chord[12] 186 | assert chord[13] >> A_1, chord[13] 187 | assert not chord[14] , chord[14] 188 | assert chord[15] >> C_2, chord[15] 189 | 190 | 191 | def testMinorSixthChord(self): 192 | chord = MinorSixthChord(*C_3) 193 | assert not chord[0] , chord[0] 194 | assert chord[1] >> C_0, chord[1] 195 | assert not chord[2] , chord[2] 196 | assert chord[3] >> E_FLAT_0, chord[3] 197 | assert not chord[4] , chord[4] 198 | assert chord[5] >> G_0, chord[5] 199 | assert chord[6] >> A_0, chord[6] 200 | assert not chord[7] , chord[7] 201 | assert chord[8] >> C_1, chord[8] 202 | assert not chord[9] , chord[9] 203 | assert chord[10] >> E_FLAT_1, chord[10] 204 | assert not chord[11] , chord[11] 205 | assert chord[12] >> G_1, chord[12] 206 | assert chord[13] >> A_1, chord[13] 207 | assert not chord[14] , chord[14] 208 | assert chord[15] >> C_2, chord[15] 209 | 210 | 211 | def testMajorSeventhChord(self): 212 | chord = MajorSeventhChord(*C_3) 213 | assert not chord[0] , chord[0] 214 | assert chord[1] >> C_0, chord[1] 215 | assert not chord[2] , chord[2] 216 | assert chord[3] >> E_0, chord[3] 217 | assert not chord[4] , chord[4] 218 | assert chord[5] >> G_0, chord[5] 219 | assert not chord[6] , chord[6] 220 | assert chord[7] >> B_0, chord[7] 221 | assert chord[8] >> C_1, chord[8] 222 | assert not chord[9] , chord[9] 223 | assert chord[10] >> E_1, chord[10] 224 | assert not chord[11] , chord[11] 225 | assert chord[12] >> G_1, chord[12] 226 | assert not chord[13] , chord[13] 227 | assert chord[14] >> B_1, chord[14] 228 | assert chord[15] >> C_2, chord[15] 229 | 230 | 231 | def testMinorSeventhChord(self): 232 | chord = MinorSeventhChord(*C_3) 233 | assert not chord[0] , chord[0] 234 | assert chord[1] >> C_0, chord[1] 235 | assert not chord[2] , chord[2] 236 | assert chord[3] >> E_FLAT_0, chord[3] 237 | assert not chord[4] , chord[4] 238 | assert chord[5] >> G_0, chord[5] 239 | assert not chord[6] , chord[6] 240 | assert chord[7] >> B_FLAT_0, chord[7] 241 | assert chord[8] >> C_1, chord[8] 242 | assert not chord[9] , chord[9] 243 | assert chord[10] >> E_FLAT_1, chord[10] 244 | assert not chord[11] , chord[11] 245 | assert chord[12] >> G_1, chord[12] 246 | assert not chord[13] , chord[13] 247 | assert chord[14] >> B_FLAT_1, chord[14] 248 | assert chord[15] >> C_2, chord[15] 249 | 250 | 251 | def testDominantSeventhChord(self): 252 | chord = DominantSeventhChord(*C_3) 253 | assert not chord[0] , chord[0] 254 | assert chord[1] >> C_0, chord[1] 255 | assert not chord[2] , chord[2] 256 | assert chord[3] >> E_0, chord[3] 257 | assert not chord[4] , chord[4] 258 | assert chord[5] >> G_0, chord[5] 259 | assert not chord[6] , chord[6] 260 | assert chord[7] >> B_FLAT_0, chord[7] 261 | assert chord[8] >> C_1, chord[8] 262 | assert not chord[9] , chord[9] 263 | assert chord[10] >> E_1, chord[10] 264 | assert not chord[11] , chord[11] 265 | assert chord[12] >> G_1, chord[12] 266 | assert not chord[13] , chord[13] 267 | assert chord[14] >> B_FLAT_1, chord[14] 268 | assert chord[15] >> C_2, chord[15] 269 | 270 | 271 | def testHalfDiminishedSeventhChord(self): 272 | chord = HalfDiminishedSeventhChord(*C_3) 273 | assert not chord[0] , chord[0] 274 | assert chord[1] >> C_0, chord[1] 275 | assert not chord[2] , chord[2] 276 | assert chord[3] >> E_FLAT_0, chord[3] 277 | assert not chord[4] , chord[4] 278 | assert chord[5] >> G_FLAT_0, chord[5] 279 | assert not chord[6] , chord[6] 280 | assert chord[7] >> B_FLAT_0, chord[7] 281 | assert chord[8] >> C_1, chord[8] 282 | assert not chord[9] , chord[9] 283 | assert chord[10] >> E_FLAT_1, chord[10] 284 | assert not chord[11] , chord[11] 285 | assert chord[12] >> G_FLAT_1, chord[12] 286 | assert not chord[13] , chord[13] 287 | assert chord[14] >> B_FLAT_1, chord[14] 288 | assert chord[15] >> C_2, chord[15] 289 | 290 | 291 | def testDiminishedSeventhChord(self): 292 | chord = DiminishedSeventhChord(*C_3) 293 | assert not chord[0] , chord[0] 294 | assert chord[1] >> C_0, chord[1] 295 | assert not chord[2] , chord[2] 296 | assert chord[3] >> E_FLAT_0, chord[3] 297 | assert not chord[4] , chord[4] 298 | assert chord[5] >> G_FLAT_0, chord[5] 299 | assert not chord[6] , chord[6] 300 | assert chord[7] >> B_DOUBLE_FLAT_0, chord[7] 301 | assert chord[8] >> C_1, chord[8] 302 | assert not chord[9] , chord[9] 303 | assert chord[10] >> E_FLAT_1, chord[10] 304 | assert not chord[11] , chord[11] 305 | assert chord[12] >> G_FLAT_1, chord[12] 306 | assert not chord[13] , chord[13] 307 | assert chord[14] >> B_DOUBLE_FLAT_1, chord[14] 308 | assert chord[15] >> C_2, chord[15] 309 | 310 | 311 | def testMajorAdd9Chord(self): 312 | chord = MajorAdd9Chord(*C_3) 313 | assert not chord[0] , chord[0] 314 | assert chord[1] >> C_0, chord[1] 315 | assert chord[2] >> D_0, chord[2] 316 | assert chord[3] >> E_0, chord[3] 317 | assert not chord[4] , chord[4] 318 | assert chord[5] >> G_0, chord[5] 319 | assert not chord[6] , chord[6] 320 | assert not chord[7] , chord[7] 321 | assert chord[8] >> C_1, chord[8] 322 | assert chord[9] >> D_1, chord[9] 323 | assert chord[10] >> E_1, chord[10] 324 | assert not chord[11] , chord[11] 325 | assert chord[12] >> G_1, chord[12] 326 | assert not chord[13] , chord[13] 327 | assert not chord[14] , chord[14] 328 | assert chord[15] >> C_2, chord[15] 329 | 330 | 331 | def testMinorAdd9Chord(self): 332 | chord = MinorAdd9Chord(*C_3) 333 | assert not chord[0] , chord[0] 334 | assert chord[1] >> C_0, chord[1] 335 | assert chord[2] >> D_0, chord[2] 336 | assert chord[3] >> E_FLAT_0, chord[3] 337 | assert not chord[4] , chord[4] 338 | assert chord[5] >> G_0, chord[5] 339 | assert not chord[6] , chord[6] 340 | assert not chord[7] , chord[7] 341 | assert chord[8] >> C_1, chord[8] 342 | assert chord[9] >> D_1, chord[9] 343 | assert chord[10] >> E_FLAT_1, chord[10] 344 | assert not chord[11] , chord[11] 345 | assert chord[12] >> G_1, chord[12] 346 | assert not chord[13] , chord[13] 347 | assert not chord[14] , chord[14] 348 | assert chord[15] >> C_2, chord[15] 349 | 350 | 351 | def testAugmentedAdd9Chord(self): 352 | chord = AugmentedAdd9Chord(*C_3) 353 | assert not chord[0] , chord[0] 354 | assert chord[1] >> C_0, chord[1] 355 | assert chord[2] >> D_0, chord[2] 356 | assert chord[3] >> E_0, chord[3] 357 | assert not chord[4] , chord[4] 358 | assert chord[5] >> G_SHARP_0, chord[5] 359 | assert not chord[6] , chord[6] 360 | assert not chord[7] , chord[7] 361 | assert chord[8] >> C_1, chord[8] 362 | assert chord[9] >> D_1, chord[9] 363 | assert chord[10] >> E_1, chord[10] 364 | assert not chord[11] , chord[11] 365 | assert chord[12] >> G_SHARP_1, chord[12] 366 | assert not chord[13] , chord[13] 367 | assert not chord[14] , chord[14] 368 | assert chord[15] >> C_2, chord[15] 369 | 370 | 371 | def testDiminishedAdd9Chord(self): 372 | chord = DiminishedAdd9Chord(*C_3) 373 | assert not chord[0] , chord[0] 374 | assert chord[1] >> C_0, chord[1] 375 | assert chord[2] >> D_0, chord[2] 376 | assert chord[3] >> E_FLAT_0, chord[3] 377 | assert not chord[4] , chord[4] 378 | assert chord[5] >> G_FLAT_0, chord[5] 379 | assert not chord[6] , chord[6] 380 | assert not chord[7] , chord[7] 381 | assert chord[8] >> C_1, chord[8] 382 | assert chord[9] >> D_1, chord[9] 383 | assert chord[10] >> E_FLAT_1, chord[10] 384 | assert not chord[11] , chord[11] 385 | assert chord[12] >> G_FLAT_1, chord[12] 386 | assert not chord[13] , chord[13] 387 | assert not chord[14] , chord[14] 388 | assert chord[15] >> C_2, chord[15] 389 | 390 | 391 | def testMajorNinthChord(self): 392 | chord = MajorNinthChord(*C_3) 393 | assert not chord[0] , chord[0] 394 | assert chord[1] >> C_0, chord[1] 395 | assert chord[2] >> D_0, chord[2] 396 | assert chord[3] >> E_0, chord[3] 397 | assert not chord[4] , chord[4] 398 | assert chord[5] >> G_0, chord[5] 399 | assert not chord[6] , chord[6] 400 | assert chord[7] >> B_0, chord[7] 401 | assert chord[8] >> C_1, chord[8] 402 | assert chord[9] >> D_1, chord[9] 403 | assert chord[10] >> E_1, chord[10] 404 | assert not chord[11] , chord[11] 405 | assert chord[12] >> G_1, chord[12] 406 | assert not chord[13] , chord[13] 407 | assert chord[14] >> B_1, chord[14] 408 | assert chord[15] >> C_2, chord[15] 409 | 410 | 411 | def testMinorNinthChord(self): 412 | chord = MinorNinthChord(*C_3) 413 | assert not chord[0] , chord[0] 414 | assert chord[1] >> C_0, chord[1] 415 | assert chord[2] >> D_0, chord[2] 416 | assert chord[3] >> E_FLAT_0, chord[3] 417 | assert not chord[4] , chord[4] 418 | assert chord[5] >> G_0, chord[5] 419 | assert not chord[6] , chord[6] 420 | assert chord[7] >> B_FLAT_0, chord[7] 421 | assert chord[8] >> C_1, chord[8] 422 | assert chord[9] >> D_1, chord[9] 423 | assert chord[10] >> E_FLAT_1, chord[10] 424 | assert not chord[11] , chord[11] 425 | assert chord[12] >> G_1, chord[12] 426 | assert not chord[13] , chord[13] 427 | assert chord[14] >> B_FLAT_1, chord[14] 428 | assert chord[15] >> C_2, chord[15] 429 | 430 | 431 | def testDominantNinthChord(self): 432 | chord = DominantNinthChord(*C_3) 433 | assert not chord[0] , chord[0] 434 | assert chord[1] >> C_0, chord[1] 435 | assert chord[2] >> D_0, chord[2] 436 | assert chord[3] >> E_0, chord[3] 437 | assert not chord[4] , chord[4] 438 | assert chord[5] >> G_0, chord[5] 439 | assert not chord[6] , chord[6] 440 | assert chord[7] >> B_FLAT_0, chord[7] 441 | assert chord[8] >> C_1, chord[8] 442 | assert chord[9] >> D_1, chord[9] 443 | assert chord[10] >> E_1, chord[10] 444 | assert not chord[11] , chord[11] 445 | assert chord[12] >> G_1, chord[12] 446 | assert not chord[13] , chord[13] 447 | assert chord[14] >> B_FLAT_1, chord[14] 448 | assert chord[15] >> C_2, chord[15] 449 | 450 | 451 | def testDominantMinorNinthChord(self): 452 | chord = DominantMinorNinthChord(*C_3) 453 | assert not chord[0] , chord[0] 454 | assert chord[1] >> C_0, chord[1] 455 | assert chord[2] >> D_FLAT_0, chord[2] 456 | assert chord[3] >> E_0, chord[3] 457 | assert not chord[4] , chord[4] 458 | assert chord[5] >> G_0, chord[5] 459 | assert not chord[6] , chord[6] 460 | assert chord[7] >> B_FLAT_0, chord[7] 461 | assert chord[8] >> C_1, chord[8] 462 | assert chord[9] >> D_FLAT_1, chord[9] 463 | assert chord[10] >> E_1, chord[10] 464 | assert not chord[11] , chord[11] 465 | assert chord[12] >> G_1, chord[12] 466 | assert not chord[13] , chord[13] 467 | assert chord[14] >> B_FLAT_1, chord[14] 468 | assert chord[15] >> C_2, chord[15] 469 | -------------------------------------------------------------------------------- /kord/keys/test_scales.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import random 3 | 4 | from bestia.output import echo, Row, FString 5 | 6 | from .scales import ( 7 | ChromaticScale, MajorPentatonicScale, MajorScale, 8 | MinorScale, MelodicMinorScale, HarmonicMinorScale 9 | ) 10 | 11 | from ..notes import NotePitch 12 | from ..notes.constants import * 13 | 14 | from ..errors import InvalidOctave 15 | 16 | __all__ = [ 17 | 'ScaleValidityTest', 18 | 'ChromaticScalesTest', 19 | 'MajorScalesExpectedNotesTest', 20 | 'TonalScaleSpellMethodTest', 21 | ] 22 | 23 | 24 | class ScaleValidityTest(unittest.TestCase): 25 | 26 | ''' for a given key, allows to verify: 27 | * invalid roots 28 | * octave changes 29 | ''' 30 | 31 | def setUp(self): 32 | print() 33 | 34 | self.chromatic_key = ChromaticScale 35 | # test no invalid roots 36 | 37 | self.major_key = MajorScale 38 | # F𝄫¹ invalid MajorScale 39 | # B𝄪¹ invalid MajorScale 40 | # D𝄪¹ invalid MajorScale 41 | # E𝄪¹ invalid MajorScale 42 | # G𝄪¹ invalid MajorScale 43 | # A𝄪¹ invalid MajorScale 44 | 45 | self.minor_key = MinorScale 46 | # D𝄫² invalid MinorScale 47 | # F𝄫¹ invalid MinorScale 48 | # G𝄫¹ invalid MinorScale 49 | # C𝄫² invalid MinorScale 50 | # B𝄪¹ invalid MinorScale 51 | # E𝄪¹ invalid MinorScale 52 | 53 | self.mel_minor_key = MelodicMinorScale 54 | # F𝄫¹ invalid MelodicMinorScale 55 | # G𝄫¹ invalid MelodicMinorScale 56 | # C𝄫² invalid MelodicMinorScale 57 | # B𝄪¹ invalid MelodicMinorScale 58 | # D𝄪¹ invalid MelodicMinorScale 59 | # E𝄪¹ invalid MelodicMinorScale 60 | # G𝄪¹ invalid MelodicMinorScale 61 | # A𝄪¹ invalid MelodicMinorScale 62 | 63 | self.har_minor_key = HarmonicMinorScale 64 | # D𝄫² invalid HarmonicMinorScale 65 | # F𝄫¹ invalid HarmonicMinorScale 66 | # G𝄫¹ invalid HarmonicMinorScale 67 | # C𝄫² invalid HarmonicMinorScale 68 | # B𝄪¹ invalid HarmonicMinorScale 69 | # D𝄪¹ invalid HarmonicMinorScale 70 | # E𝄪¹ invalid HarmonicMinorScale 71 | # G𝄪¹ invalid HarmonicMinorScale 72 | # A𝄪¹ invalid HarmonicMinorScale 73 | 74 | 75 | def testValidMethod(self): 76 | for Scale in [self.major_key]: 77 | assert Scale('c').validate() 78 | 79 | 80 | def testValidRoots(self): 81 | 82 | for Scale in [self.major_key,self.minor_key ]: 83 | 84 | echo('\nValid {}s'.format(Scale.__name__), 'underline') 85 | 86 | for note in Scale.valid_root_notes(): 87 | 88 | line = Row() 89 | key = Scale(*note) 90 | for d in key.spell( 91 | note_count=len(key.intervals) +16, yield_all=False 92 | ): 93 | line.append( 94 | FString( 95 | d, 96 | size=5, 97 | fg='blue' if not (d.oct % 2) else 'white', 98 | ) 99 | ) 100 | line.echo() 101 | 102 | 103 | def testInvalidRoots(self): 104 | echo('\nInvalid {}s'.format(self.major_key.__name__), 'underline') 105 | for note in self.major_key.invalid_root_notes(): 106 | echo( 107 | '{}{} invalid {}'.format(note.chr, note.repr_alt, self.major_key.__name__), 108 | 'red', 'faint' 109 | ) 110 | 111 | 112 | class ChromaticScalesTest(unittest.TestCase): 113 | 114 | def setUp(self): 115 | print() 116 | self.c_chromatic = ChromaticScale('C') 117 | self.f_sharp_chromatic = ChromaticScale('F', '#') 118 | self.b_flat_chromatic = ChromaticScale('B', 'b') 119 | 120 | 121 | def testIntervalsCount(self): 122 | assert len(self.c_chromatic.intervals) == 12, self.c_chromatic.intervals 123 | assert len(self.f_sharp_chromatic.intervals) == 12, self.f_sharp_chromatic.intervals 124 | assert len(self.b_flat_chromatic.intervals) == 12, self.b_flat_chromatic.intervals 125 | 126 | 127 | def testCChromaticScaleGenerator(self): 128 | 129 | octaves_to_test = 18 130 | intervals = 12 131 | notes_to_test = octaves_to_test * intervals + 1 # 18 * 12 + 1 = 217 132 | 133 | for i, note in enumerate( 134 | self.c_chromatic.spell( 135 | note_count=notes_to_test, yield_all=False 136 | ) 137 | ): 138 | i += 1 139 | if i == 1: 140 | assert note >> C_0, note 141 | elif i == 2: 142 | assert note >> C_SHARP_0, note 143 | elif i == 3: 144 | assert note >> D_0, note 145 | elif i == 4: 146 | assert note >> D_SHARP_0, note 147 | elif i == 5: 148 | assert note >> E_0, note 149 | elif i == 6: 150 | assert note >> F_0, note 151 | elif i == 7: 152 | assert note >> F_SHARP_0, note 153 | elif i == 8: 154 | assert note >> G_0, note 155 | elif i == 9: 156 | assert note >> G_SHARP_0, note 157 | elif i == 10: 158 | assert note >> A_0, note 159 | elif i == 11: 160 | assert note >> A_SHARP_0, note 161 | elif i == 12: 162 | assert note >> B_0, note 163 | elif i == 13: 164 | assert note >> C_1, note 165 | elif i == 14: 166 | assert note >> C_SHARP_1, note 167 | elif i == 15: 168 | assert note >> D_1, note 169 | elif i == 16: 170 | assert note >> D_SHARP_1, note 171 | elif i == 17: 172 | assert note >> E_1, note 173 | elif i == 18: 174 | assert note >> F_1, note 175 | elif i == 19: 176 | assert note >> F_SHARP_1, note 177 | elif i == 20: 178 | assert note >> G_1, note 179 | elif i == 21: 180 | assert note >> G_SHARP_1, note 181 | elif i == 22: 182 | assert note >> A_1, note 183 | elif i == 23: 184 | assert note >> A_SHARP_1, note 185 | elif i == 24: 186 | assert note >> B_1, note 187 | elif i == 25: 188 | assert note >> C_2, note 189 | # .............................. 190 | elif i == 97: 191 | assert note >> C_8, note 192 | elif i == 98: 193 | assert note >> C_SHARP_8, note 194 | elif i == 99: 195 | assert note >> D_8, note 196 | elif i == 100: 197 | assert note >> D_SHARP_8, note 198 | elif i == 101: 199 | assert note >> E_8, note 200 | elif i == 102: 201 | assert note >> F_8, note 202 | elif i == 103: 203 | assert note >> F_SHARP_8, note 204 | elif i == 104: 205 | assert note >> G_8, note 206 | elif i == 105: 207 | assert note >> G_SHARP_8, note 208 | elif i == 106: 209 | assert note >> A_8, note 210 | elif i == 107: 211 | assert note >> A_SHARP_8, note 212 | elif i == 108: 213 | assert note >> B_8, note 214 | elif i == 109: 215 | assert note >> C_9, note 216 | # .............................. 217 | elif i == 205: 218 | assert note >> NotePitch('C', '', 17), note 219 | 220 | 221 | def testFSharpChromaticScaleGenerator(self): 222 | 223 | octaves_to_test = 18 224 | intervals = 12 225 | notes_to_test = octaves_to_test * intervals + 1 # 18 * 12 + 1 = 217 226 | 227 | for i, note in enumerate( 228 | self.f_sharp_chromatic.spell( 229 | note_count=notes_to_test, yield_all=False 230 | ) 231 | ): 232 | i += 1 233 | if i == 1: 234 | assert note >> F_SHARP_0, note 235 | elif i == 2: 236 | assert note >> G_0, note 237 | elif i == 3: 238 | assert note >> G_SHARP_0, note 239 | elif i == 4: 240 | assert note >> A_0, note 241 | elif i == 5: 242 | assert note >> A_SHARP_0, note 243 | elif i == 6: 244 | assert note >> B_0, note 245 | elif i == 7: 246 | assert note >> C_1, note 247 | elif i == 8: 248 | assert note >> C_SHARP_1, note 249 | elif i == 9: 250 | assert note >> D_1, note 251 | elif i == 10: 252 | assert note >> D_SHARP_1, note 253 | elif i == 11: 254 | assert note >> E_1, note 255 | elif i == 12: 256 | assert note >> F_1, note 257 | elif i == 13: 258 | assert note >> F_SHARP_1, note 259 | elif i == 14: 260 | assert note >> G_1, note 261 | elif i == 15: 262 | assert note >> G_SHARP_1, note 263 | elif i == 16: 264 | assert note >> A_1, note 265 | elif i == 17: 266 | assert note >> A_SHARP_1, note 267 | elif i == 18: 268 | assert note >> B_1, note 269 | elif i == 19: 270 | assert note >> C_2, note 271 | # .............................. 272 | elif i == 211: 273 | assert note >> NotePitch('C', '', 18), note 274 | 275 | 276 | def testBFlatChromaticScaleGenerator(self): 277 | 278 | octaves_to_test = 18 279 | intervals = 12 280 | notes_to_test = octaves_to_test * intervals + 1 # 18 * 12 + 1 = 217 281 | 282 | for i, note in enumerate( 283 | self.b_flat_chromatic.spell( 284 | note_count=notes_to_test, yield_all=False 285 | ) 286 | ): 287 | i += 1 288 | if i == 1: 289 | assert note >> B_FLAT_0, note 290 | elif i == 2: 291 | assert note >> B_0, note 292 | elif i == 3: 293 | assert note >> C_1, note 294 | elif i == 4: 295 | assert note >> D_FLAT_1, note 296 | elif i == 5: 297 | assert note >> D_1, note 298 | elif i == 6: 299 | assert note >> E_FLAT_1, note 300 | elif i == 7: 301 | assert note >> E_1, note 302 | elif i == 8: 303 | assert note >> F_1, note 304 | elif i == 9: 305 | assert note >> G_FLAT_1, note 306 | elif i == 10: 307 | assert note >> G_1, note 308 | elif i == 11: 309 | assert note >> A_FLAT_1, note 310 | elif i == 12: 311 | assert note >> A_1, note 312 | elif i == 13: 313 | assert note >> B_FLAT_1, note 314 | elif i == 14: 315 | assert note >> B_1, note 316 | elif i == 15: 317 | assert note >> C_2, note 318 | # .............................. 319 | elif i == 200: 320 | assert note >> NotePitch('F', '', 17), note 321 | 322 | 323 | class MajorScalesExpectedNotesTest(unittest.TestCase): 324 | 325 | def setUp(self): 326 | print() 327 | self.c_major = MajorScale('C') 328 | self.b_major = MajorScale('B') # 5 sharps 329 | self.d_flat_major = MajorScale('D', 'b') # 5 flats 330 | 331 | def testIntervalsCount(self): 332 | assert len(self.c_major.intervals) == 7, self.c_major.intervals 333 | assert len(self.b_major.intervals) == 7, self.b_major.intervals 334 | assert len(self.d_flat_major.intervals) == 7, self.d_flat_major.intervals 335 | 336 | 337 | def testCMajorScaleGenerator(self): 338 | 339 | octaves_to_test = 18 340 | intervals = 7 341 | notes_to_test = octaves_to_test * intervals + 1 # 18 * 7 + 1 = 127 342 | 343 | for i, note in enumerate( 344 | self.c_major.spell( 345 | note_count=notes_to_test, yield_all=False 346 | ) 347 | ): 348 | i += 1 349 | if i == 1: 350 | assert note >> C_0, note 351 | elif i == 2: 352 | assert note >> D_0, note 353 | elif i == 3: 354 | assert note >> E_0, note 355 | elif i == 4: 356 | assert note >> F_0, note 357 | elif i == 5: 358 | assert note >> G_0, note 359 | elif i == 6: 360 | assert note >> A_0, note 361 | elif i == 7: 362 | assert note >> B_0, note 363 | elif i == 8: 364 | assert note >> C_1, note 365 | elif i == 9: 366 | assert note >> D_1, note 367 | elif i == 10: 368 | assert note >> E_1, note 369 | elif i == 11: 370 | assert note >> F_1, note 371 | elif i == 12: 372 | assert note >> G_1, note 373 | elif i == 13: 374 | assert note >> A_1, note 375 | elif i == 14: 376 | assert note >> B_1, note 377 | elif i == 15: 378 | assert note >> C_2, note 379 | # .............................. 380 | elif i == 64: 381 | assert note >> C_9, note 382 | elif i == 65: 383 | assert note >> D_9, note 384 | elif i == 66: 385 | assert note >> E_9, note 386 | elif i == 67: 387 | assert note >> F_9, note 388 | elif i == 68: 389 | assert note >> G_9, note 390 | elif i == 69: 391 | assert note >> A_9, note 392 | elif i == 70: 393 | assert note >> B_9, note 394 | elif i == 71: 395 | assert note >> NotePitch('C', '', 10), note 396 | 397 | 398 | def testBMajorScaleGenerator(self): 399 | 400 | octaves_to_test = 18 401 | intervals = 7 402 | notes_to_test = octaves_to_test * intervals + 1 # 18 * 7 + 1 = 127 403 | 404 | for i, note in enumerate( 405 | self.b_major.spell( 406 | note_count=notes_to_test, yield_all=False 407 | ) 408 | ): 409 | i += 1 410 | if i == 1: 411 | assert note >> B_0, note 412 | elif i == 2: 413 | assert note >> C_SHARP_1, note 414 | elif i == 3: 415 | assert note >> D_SHARP_1, note 416 | elif i == 4: 417 | assert note >> E_1, note 418 | elif i == 5: 419 | assert note >> F_SHARP_1, note 420 | elif i == 6: 421 | assert note >> G_SHARP_1, note 422 | elif i == 7: 423 | assert note >> A_SHARP_1, note 424 | elif i == 8: 425 | assert note >> B_1, note 426 | elif i == 9: 427 | assert note >> C_SHARP_2, note 428 | elif i == 10: 429 | assert note >> D_SHARP_2, note 430 | elif i == 11: 431 | assert note >> E_2, note 432 | elif i == 12: 433 | assert note >> F_SHARP_2, note 434 | elif i == 13: 435 | assert note >> G_SHARP_2, note 436 | elif i == 14: 437 | assert note >> A_SHARP_2, note 438 | elif i == 15: 439 | assert note >> B_2, note 440 | elif i == 16: 441 | assert note >> C_SHARP_3, note 442 | # .............................. 443 | elif i == 64: 444 | assert note >> B_9, note 445 | elif i == 65: 446 | assert note >> NotePitch('C', '#', 10), note 447 | 448 | 449 | def testDFlatMajorScaleGenerator(self): 450 | 451 | octaves_to_test = 18 452 | intervals = 7 453 | notes_to_test = octaves_to_test * intervals + 1 # 18 * 7 + 1 = 127 454 | 455 | for i, note in enumerate( 456 | self.d_flat_major.spell( 457 | note_count=notes_to_test, yield_all=False 458 | ) 459 | ): 460 | i += 1 461 | if i == 1: 462 | assert note >> D_FLAT_0, note 463 | elif i == 2: 464 | assert note >> E_FLAT_0, note 465 | elif i == 3: 466 | assert note >> F_0, note 467 | elif i == 4: 468 | assert note >> G_FLAT_0, note 469 | elif i == 5: 470 | assert note >> A_FLAT_0, note 471 | elif i == 6: 472 | assert note >> B_FLAT_0, note 473 | elif i == 7: 474 | assert note >> C_1, note 475 | elif i == 8: 476 | assert note >> D_FLAT_1, note 477 | elif i == 9: 478 | assert note >> E_FLAT_1, note 479 | elif i == 10: 480 | assert note >> F_1, note 481 | elif i == 11: 482 | assert note >> G_FLAT_1, note 483 | elif i == 12: 484 | assert note >> A_FLAT_1, note 485 | elif i == 13: 486 | assert note >> B_FLAT_1, note 487 | elif i == 14: 488 | assert note >> C_2, note 489 | elif i == 15: 490 | assert note >> D_FLAT_2, note 491 | elif i == 16: 492 | assert note >> E_FLAT_2, note 493 | # .............................. 494 | elif i == 64: 495 | assert note >> D_FLAT_9, note 496 | elif i == 65: 497 | assert note >> E_FLAT_9, note 498 | elif i == 66: 499 | assert note >> F_9, note 500 | elif i == 67: 501 | assert note >> G_FLAT_9, note 502 | elif i == 68: 503 | assert note >> A_FLAT_9, note 504 | elif i == 69: 505 | assert note >> B_FLAT_9, note 506 | elif i == 70: 507 | assert note >> NotePitch('C', '', 10), note 508 | 509 | 510 | class TonalScaleSpellMethodTest(unittest.TestCase): 511 | 512 | def setUp(self): 513 | print() 514 | self.scales = { 515 | 'Ab_chromatic': ChromaticScale('A', 'b'), 516 | 'B_major': MajorScale('B'), 517 | 'Bb_minor': MinorScale('B', 'b'), 518 | 'C_mel_minor': MelodicMinorScale('C'), 519 | 'F#_har_minor': HarmonicMinorScale('F', '#'), 520 | } 521 | 522 | def testNoteCount(self): 523 | ''' tests yielded note count is what has been required ''' 524 | max_notes = 63 # why does it fail >= 64 525 | for key in self.scales.values(): 526 | random_max_notes = random.randint(2, max_notes) 527 | print( 528 | 'Testing {}{} {}._count_notes( note_count=1..{} ) ...'.format( 529 | key.root.chr, key.root.repr_alt, key.name(), random_max_notes 530 | ) 531 | ) 532 | for count in range(random_max_notes): 533 | count += 1 534 | yielded_notes = len( 535 | [ n for n in key._count_notes( 536 | note_count=count, start_note=None, yield_all=False 537 | ) ] 538 | ) 539 | assert yielded_notes == count, (yielded_notes, count) 540 | 541 | 542 | def testDiatonicStartNote(self): 543 | ''' tests that first yielded note == diatonic start_note ''' 544 | max_notes = 64 # max value is different for each scale 545 | for key in self.scales.values(): 546 | d = random.randint(2, max_notes) 547 | print( 548 | 'Testing {}{} {}._count_notes( start_note = note({}) ) ...'.format( 549 | key.root.chr, key.root.repr_alt, key.name(), d 550 | ), flush=1 551 | ) 552 | for note in key._count_notes( 553 | note_count=1, start_note=key[d], yield_all=True 554 | ): 555 | assert note >> NotePitch(*key[d]), note 556 | 557 | 558 | def testNonDiatonicStartNoteYieldNotes(self): 559 | ''' tests 1st yielded item == expected diatonic note when: 560 | * start_note is non-diatonic to the scale 561 | * Nones ARE NOT being yielded 562 | ''' 563 | test_parameters = [ 564 | 565 | { 566 | 'scale': self.scales['Ab_chromatic'], 567 | 'non_diatonic_note': A_SHARP_1, # enharmonic 568 | 'exp_diatonic_note': B_FLAT_1, # equals 569 | }, 570 | 571 | { 572 | 'scale': self.scales['B_major'], 573 | 'non_diatonic_note': D_1, # missing note 574 | 'exp_diatonic_note': D_SHARP_1, # next note 575 | }, 576 | 577 | { 578 | 'scale': self.scales['Bb_minor'], 579 | 'non_diatonic_note': D_SHARP_1, # enharmonic 580 | 'exp_diatonic_note': E_FLAT_1, # equals 581 | }, 582 | { 583 | 'scale': self.scales['C_mel_minor'], 584 | 'non_diatonic_note': F_SHARP_0, # missing note 585 | 'exp_diatonic_note': G_0, # next note 586 | }, 587 | { 588 | 'scale': self.scales['F#_har_minor'], 589 | 'non_diatonic_note': C_FLAT_4, # enharmonic 590 | 'exp_diatonic_note': B_3, # equals 591 | }, 592 | 593 | ] 594 | 595 | for param in test_parameters: 596 | key = param['scale'] 597 | non = param['non_diatonic_note'] 598 | exp = param['exp_diatonic_note'] 599 | print( 600 | 'Testing {}{} {}._count_notes( start_note = non_diatonic_note, yield_all = 0 ) ...'.format( 601 | key.root.chr, key.root.repr_alt, key.name() 602 | ) 603 | ) 604 | for note in key._count_notes( 605 | note_count=1, start_note=non, yield_all=False 606 | ): 607 | assert note != None, type(note) # yield all=False ensures no Nones 608 | assert note >> NotePitch(*exp), (note, exp) 609 | 610 | 611 | def testNonDiatonicStartNoteYieldAll(self): 612 | ''' tests 1st yielded item == expected value when: 613 | * start_note is non-diatonic to the scale 614 | * Nones ARE being yielded 615 | ''' 616 | test_parameters = [ 617 | { 618 | 'scale': self.scales['Ab_chromatic'], 619 | 'non_diatonic_note': A_SHARP_1, # enharmonic 620 | 'exp_diatonic_note': B_FLAT_1, # equals 621 | }, 622 | { 623 | 'scale': self.scales['B_major'], 624 | 'non_diatonic_note': D_1, # missing note 625 | 'exp_diatonic_note': None, # yields a None, not D# 626 | }, 627 | { 628 | 'scale': self.scales['Bb_minor'], 629 | 'non_diatonic_note': D_SHARP_1, # enharmonic 630 | 'exp_diatonic_note': E_FLAT_1, # equals 631 | }, 632 | { 633 | 'scale': self.scales['C_mel_minor'], 634 | 'non_diatonic_note': F_SHARP_0, # missing note 635 | 'exp_diatonic_note': None, # yields a None, not G 636 | }, 637 | { 638 | 'scale': self.scales['F#_har_minor'], 639 | 'non_diatonic_note': C_FLAT_4, # enharmonic 640 | 'exp_diatonic_note': B_3, # equals 641 | }, 642 | ] 643 | 644 | for param in test_parameters: 645 | key = param['scale'] 646 | non = param['non_diatonic_note'] 647 | exp = param['exp_diatonic_note'] 648 | 649 | print( 650 | 'Testing {}{} {}._count_notes( start_note = non_diatonic_note, yield_all = 1 ) ...'.format( 651 | key.root.chr, key.root.repr_alt, key.name() 652 | ) 653 | ) 654 | 655 | for note in key._count_notes( 656 | note_count=1, start_note=non, yield_all=True 657 | ): 658 | if note == None: 659 | assert note == exp, (note, exp) 660 | else: 661 | assert note >> exp, (note, exp) 662 | 663 | break # test only first yielded value even if not a note 664 | 665 | 666 | def testMajorNoneYields(self): 667 | for i, note in enumerate(MajorScale('C')._count_notes( 668 | note_count=64, start_note=None, yield_all=True 669 | )): 670 | if i == 0: 671 | assert note >> C_0, note 672 | elif i == 1: 673 | assert note == None 674 | elif i == 2: 675 | assert note >> D_0, note 676 | elif i == 3: 677 | assert note == None 678 | elif i == 4: 679 | assert note >> E_0, note 680 | elif i == 5: 681 | assert note >> F_0, note 682 | elif i == 6: 683 | assert note == None 684 | elif i == 7: 685 | assert note >> G_0, note 686 | elif i == 8: 687 | assert note == None 688 | elif i == 9: 689 | assert note >> A_0, note 690 | elif i == 10: 691 | assert note == None 692 | elif i == 11: 693 | assert note >> B_0, note 694 | elif i == 12: 695 | assert note >> C_1, note 696 | 697 | 698 | def testChromaticNoneYields(self): 699 | ''' tests Chromatic scale does NEVER yield None even with yield_all ''' 700 | for i, note in enumerate(self.scales['Ab_chromatic']._count_notes( 701 | note_count=128, start_note=None, yield_all=True 702 | )): 703 | assert note != None 704 | 705 | 706 | def testDegreeOrderOverOct(self): 707 | for i, note in enumerate( 708 | MajorScale('A').spell( 709 | note_count=6, 710 | start_note=None, 711 | yield_all=True, 712 | )): 713 | i += 1 714 | if i == 1: 715 | assert note >> A_0, note 716 | elif i == 2: 717 | assert not note, note 718 | elif i == 3: 719 | assert note >> B_0, note 720 | elif i == 4: 721 | assert not note, note 722 | elif i == 5: 723 | assert note >> C_SHARP_1, note 724 | elif i == 6: 725 | assert note >> D_1, note 726 | elif i == 7: 727 | assert not note, note 728 | elif i == 8: 729 | assert note >> E_1, note 730 | 731 | 732 | 733 | def testCustomDegrees(self): 734 | ''' 735 | B is not part of C major pentatonic => 736 | * 1st note should be None 737 | * 2nd note should be C 738 | ''' 739 | for i, note in enumerate( 740 | MajorPentatonicScale('C').spell( 741 | yield_all=1, 742 | start_note=B_3, 743 | note_count=3 744 | ) 745 | ): 746 | i += 1 747 | if i == 1: 748 | assert not note, note 749 | if i == 2: 750 | assert note, note 751 | assert note >> C_4, note 752 | 753 | 754 | def testMaxOctave(self): 755 | try: 756 | note = NotePitch('C', NotePitch.MAXIMUM_OCTAVE + 1) 757 | except InvalidOctave: 758 | note = None 759 | finally: 760 | assert not note 761 | --------------------------------------------------------------------------------