import logging
import re
import sys
-from typing import Any, Callable, Dict, Iterator, List, Optional
+from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Tuple
import antlr4 # type: ignore
import acl
import bootstrap
import decorator_utils
+import list_utils
from music.chordsLexer import chordsLexer # type: ignore
from music.chordsListener import chordsListener # type: ignore
from music.chordsParser import chordsParser # type: ignore
logger = logging.getLogger(__name__)
-notes = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#']
-
-
-def generate_scale(starting_note: str) -> Iterator[str]:
- starting_note = starting_note.upper()
- start = 0
- while notes[start] != starting_note:
- start += 1
- while True:
- if start >= len(notes):
- start = 0
- yield notes[start]
- start += 1
-
-
-def degree_of_note(root_note: str, target_note: str) -> Optional[int]:
- root_note = root_note.upper()
- target_note = target_note.upper()
- for degree, note in enumerate(itertools.islice(generate_scale(root_note), 24)):
- if note == target_note:
- return degree - 1
- return None
def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
class Chord:
- def __init__(self, root_note: str, other_notes: Dict[str, int]):
+ NOTES = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#']
+
+ def __init__(self, root_note: str, other_notes: Set[int]) -> None:
self.root_note = root_note.upper()
- self.other_notes = other_notes
+ self.other_notes: Dict[int, Optional[str]] = {}
+ for semitone_count in other_notes:
+ name = Chord.describe_interval(semitone_count)
+ if name:
+ self.other_notes[semitone_count] = name
+ else:
+ self.other_notes[semitone_count] = None
+ self.scale = list(itertools.islice(self.generate_scale(), 24))
+
+ def generate_scale(self) -> Iterator[str]:
+ starting_note = self.root_note
+ start = 0
+ while Chord.NOTES[start] != starting_note:
+ start += 1
+ while True:
+ if start >= len(Chord.NOTES):
+ start = 0
+ yield Chord.NOTES[start]
+ start += 1
+
+ @staticmethod
+ def describe_interval(semitone_count: int) -> Optional[str]:
+ names: Dict[int, str] = {
+ 0: 'perfect unison',
+ 1: 'minor 2nd',
+ 2: 'major 2nd',
+ 3: 'minor 3rd',
+ 4: 'major 3rd',
+ 5: 'perfect 4th',
+ 6: 'diminished 5th',
+ 7: 'perfect 5th',
+ 8: 'minor 6th',
+ 9: 'major 6th',
+ 10: 'minor 7th',
+ 11: 'major 7th',
+ 12: 'perfect octave',
+ 13: 'minor 9th',
+ 14: 'major 9th',
+ 15: 'minor 10th',
+ 16: 'major 10th',
+ 17: 'perfect 11th',
+ 18: 'diminished 12th',
+ 19: 'perfect 12th',
+ 20: 'minor 13th',
+ 21: 'major 13th',
+ 22: 'minor 14th',
+ 23: 'major 14th',
+ 24: 'double octave',
+ 25: 'augmented 15th',
+ }
+ return names.get(semitone_count, None)
+
+ def degree_of_note(self, target_note: str) -> Optional[Tuple[Optional[str], int]]:
+ target_note = target_note.upper()
+ for degree, note in enumerate(itertools.islice(self.generate_scale(), 24)):
+ if note == target_note:
+ return (Chord.describe_interval(degree), degree)
+ return None
+
+ def note_at_degree(self, semitone_count: int) -> Optional[str]:
+ if semitone_count < len(self.scale):
+ return self.scale[semitone_count]
+ return None
+
+ def add_bass(self, bass_note: str) -> None:
+ name, semitone_count = self.degree_of_note(bass_note)
+ if name:
+ self.other_notes[semitone_count] = f'{name} (bass)'
+ else:
+ self.other_notes[semitone_count] = 'bass note'
+
+ def describe_chord(self) -> str:
+ names: Dict[Tuple[int], str] = {
+ (2, 7): 'sus2',
+ (4, 7): 'major',
+ (5, 7): 'sus4',
+ (3, 7): 'minor',
+ (4, 7, 10): 'dom7',
+ (4, 7, 11): 'maj7',
+ (4, 8): 'aug',
+ (3, 5): 'dim',
+ (4, 7, 10, 17): 'add11',
+ (4, 7, 10, 14): 'add9',
+ (5): 'power chord',
+ }
+ intervals = list(self.other_notes.keys())
+ intervals.sort()
+ intervals_set = set(intervals)
+ for interval in reversed(list(list_utils.powerset(intervals))):
+ name = names.get(tuple(interval), None)
+ if name:
+ for x in interval:
+ intervals_set.remove(x)
+ break
+
+ if not name:
+ name = 'An unknown chord'
+
+ for semitone_count, note_name in self.other_notes.items():
+ if 'bass' in note_name:
+ note = self.note_at_degree(semitone_count)
+ name += f' with a bass {note}'
+ if semitone_count in intervals_set:
+ intervals_set.remove(semitone_count)
+
+ for note in intervals_set:
+ note = self.note_at_degree(note)
+ name += f' add {note}'
+
+ return f'{self.root_note} ({name})'
def __repr__(self):
- return f'root={self.root_note}, others={self.other_notes}'
+ name = self.describe_chord()
+ ret = f'{name}\n'
+ ret += f'root={self.root_note}\n'
+ for semitone_count, interval_name in self.other_notes.items():
+ note = self.note_at_degree(semitone_count)
+ ret += f'+ {interval_name} ({semitone_count}) => {note}\n'
+ return ret
@decorator_utils.decorate_matching_methods_with(
self.addedNotes.append('maj3')
self.addedNotes.append('aug5')
- other_notes: Dict[str, int] = {}
+ other_notes: Set[int] = set()
for expression in self.addedNotes:
semitone_count = ChordParser.interval_name_to_semitone_count(expression, self.rootNote)
+ other_notes.add(semitone_count)
if semitone_count in (14, 17, 19):
- other_notes['min7'] = 10
- other_notes[expression] = semitone_count
-
- if self.bassNote:
- degree = degree_of_note(self.rootNote, self.bassNote)
- other_notes[self.bassNote] = degree
+ other_notes.add(10)
self.chord = Chord(self.rootNote, other_notes)
+ if self.bassNote:
+ self.chord.add_bass(self.bassNote)
def exitRootNote(self, ctx: chordsParser.RootNoteContext):
self.rootNote = ctx.NOTE().__str__().upper()
self.addedNotes.append('min7')
if ctx.MAJ_SEVEN():
self.addedNotes.append('maj7')
- if ctx.ADD_NINE():
+ if ctx.NINE():
self.addedNotes.append('maj9')
+ if ctx.ELEVEN():
+ self.addedNotes.append('min7')
+ self.addedNotes.append('maj11')
def exitExtensionExpr(self, ctx: chordsParser.ExtensionExprContext):
self.addedNotes.append(ctx.getText())
class TestChordParder(unittest.TestCase):
def test_with_known_correct_answers(self):
expected_answers = {
- 'D': "root=D, others={'maj3': 4, 'perfect5th': 7}",
- 'Dmaj': "root=D, others={'maj3': 4, 'perfect5th': 7}",
- 'D major': "root=D, others={'maj3': 4, 'perfect5th': 7}",
- 'DM': "root=D, others={'maj3': 4, 'perfect5th': 7}",
- 'Dm': "root=D, others={'min3': 3, 'perfect5th': 7}",
- 'Dmin': "root=D, others={'min3': 3, 'perfect5th': 7}",
- 'D minor': "root=D, others={'min3': 3, 'perfect5th': 7}",
- 'Asus2': "root=A, others={'maj2': 2, 'perfect5th': 7}",
- 'Bsus4': "root=B, others={'perfect4': 5, 'perfect5th': 7}",
- 'F5': "root=F, others={'perfect5th': 7}",
- 'G/B': "root=G, others={'maj3': 4, 'perfect5th': 7, 'B': 3}",
+ 'D': "D (major)\nroot=D\n+ major 3rd (4) => F#\n+ perfect 5th (7) => A\n",
+ 'DM': "D (major)\nroot=D\n+ major 3rd (4) => F#\n+ perfect 5th (7) => A\n",
+ 'Dmaj': "D (major)\nroot=D\n+ major 3rd (4) => F#\n+ perfect 5th (7) => A\n",
+ 'D major': "D (major)\nroot=D\n+ major 3rd (4) => F#\n+ perfect 5th (7) => A\n",
}
cp = ChordParser()