diff options
| author | Scott Gasch <[email protected]> | 2022-04-22 14:28:44 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2022-04-22 14:28:44 -0700 |
| commit | 290e40e0bf150ab889ada06e500a5835d3935da6 (patch) | |
| tree | 6eff266f7ad9a6c62c682317a2f75746c19c6193 /music | |
| parent | 711a9fd699f7c724f0aab7c1c43511cfbf7b2281 (diff) | |
Add powerset to list_utils; improve chord parser.
Diffstat (limited to 'music')
| -rw-r--r-- | music/chords.g4 | 19 | ||||
| -rwxr-xr-x | music/chords.py | 165 |
2 files changed, 143 insertions, 41 deletions
diff --git a/music/chords.g4 b/music/chords.g4 index bd180a9..9083e75 100644 --- a/music/chords.g4 +++ b/music/chords.g4 @@ -50,10 +50,11 @@ augmentedExpr: AUGMENTED; powerChordExpr: '5'; addNotesExpr - : SIX - | SEVEN + : ADD* SIX + | ADD* SEVEN | MAJ_SEVEN - | ADD_NINE + | ADD* NINE + | ADD* ELEVEN ; extensionExpr @@ -108,19 +109,23 @@ MINOR: ('m'|'min'|'minor'); SUS: ('sus'|'suspended'); -DIMINISHED: ('dim'|'diminished'); +DIMINISHED: ('dim'|'diminished'|'-'); -AUGMENTED: ('aug'|'augmented'); +AUGMENTED: ('aug'|'augmented'|'+'); SLASH: ('/'|'\\'); +ADD: ('add'|'Add'|'dom'); + SIX: '6'; SEVEN: '7'; -MAJ_SEVEN: MAJOR '7'; +NINE: '9'; -ADD_NINE: ('add'|'Add')* '9'; +ELEVEN: '11'; + +MAJ_SEVEN: MAJOR '7'; INTERVAL: (MAJOR|MINOR)* ('b'|'#')* DIGITS ; diff --git a/music/chords.py b/music/chords.py index ff4b540..838ad17 100755 --- a/music/chords.py +++ b/music/chords.py @@ -12,40 +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)): - if note == target_note: - return degree - 1 - return None def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]): @@ -93,12 +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): - 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( @@ -250,17 +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 - - 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() @@ -305,8 +399,11 @@ class ChordParser(chordsListener): 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()) |
