X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=music%2Fchords.py;h=dc2220545ad5b0f0836e91010b2b697694950760;hb=57acb381eb927bcafd5a0c496e4fec8917f361a7;hp=fc2bd5fa7aeb3af3b62cc6c8a6bf81740418805d;hpb=068d71327bf6ec8618cd70a6d3fce5d075503cae;p=python_utils.git diff --git a/music/chords.py b/music/chords.py index fc2bd5f..dc22205 100755 --- a/music/chords.py +++ b/music/chords.py @@ -12,41 +12,19 @@ import itertools 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)): - print(f'"{target_note}", "{note}", {degree}') - if note == target_note: - return degree - 1 - return None def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]): @@ -94,9 +72,129 @@ class RaisingErrorListener(antlr4.DiagnosticErrorListener): 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): + 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( @@ -213,8 +311,6 @@ class ChordParser(chordsListener): def exitParse(self, ctx: chordsParser.ParseContext) -> None: """Populate self.chord""" - print(f'Root note is a {self.rootNote}') - scale = list(itertools.islice(generate_scale(self.rootNote), 24)) chord_types_with_perfect_5th = set( [ @@ -250,21 +346,15 @@ class ChordParser(chordsListener): 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 - - for expression, semitone_count in other_notes.items(): - note_name = scale[semitone_count] - print(f'Contains: {expression} ({semitone_count} semitones) => {note_name}') - if self.bassNote: - degree = degree_of_note(self.rootNote, self.bassNote) - print(f'Add a {self.bassNote} ({degree}) in the bass') - 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() @@ -309,8 +399,14 @@ class ChordParser(chordsListener): self.addedNotes.append('min7') if ctx.MAJ_SEVEN(): self.addedNotes.append('maj7') - if ctx.ADD_NINE(): + if ctx.MIN_SEVEN(): + self.addedNotes.append('min7') + self.chordType = ChordParser.MINOR + 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())