Last active
June 4, 2019 15:53
-
-
Save rcalsaverini/5bcf569fadd58e5fd48e4b1ed4004245 to your computer and use it in GitHub Desktop.
Chords from scale
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import pandas as pd | |
from itertools import product | |
from tqdm import tqdm | |
from pychord.analyzer import get_all_rotated_notes, notes_to_positions | |
from pychord.analyzer import find_quality | |
from pychord import Chord | |
COMPLEXITY_CLASSES = { | |
1: ["", "m", "5"], | |
2: ["7", "m7"], | |
3: ["M7", "mM7", "sus2", "sus4"], | |
4: ["6", "9", "7+9", "7-9", "7sus4", "9sus4", "m9", "m7-5", "add9", "M9"], | |
5: ["7+5", "7-5", "M7+5"], | |
6: ["11", "add11", "7+11", "7-13"], | |
10: ["aug", "dim", "dim6"], | |
} | |
COMPLEXITY = { | |
quality: complexity | |
for (complexity, qualities) in COMPLEXITY_CLASSES.items() | |
for quality in qualities | |
} | |
def note_to_chord(notes, raise_on_error=False): | |
root = notes[0] | |
root_and_positions = [ | |
[rotated_notes[0], notes_to_positions(rotated_notes, rotated_notes[0])] | |
for rotated_notes in get_all_rotated_notes(notes) | |
] | |
for temp_root, positions in root_and_positions: | |
quality = find_quality(positions) | |
if quality is None: | |
continue | |
chord_name = ( | |
"{}{}".format(root, quality) | |
if temp_root == root | |
else "{}{}/{}".format(temp_root, quality, root) | |
) | |
try: | |
chord = Chord(chord_name) | |
yield chord | |
except Exception as e: | |
if raise_on_error: | |
raise e | |
def relative_complexity(scale): | |
def complexity(chord): | |
degree = 1 + scale.index(chord.root) | |
quality_complexity = COMPLEXITY[chord.quality.quality] | |
is_inversion = int((chord.on is not None) and (chord.on != chord.root)) | |
return 3 * is_inversion + quality_complexity | |
return complexity | |
def iterate_scale(scale, times, verbose=0): | |
iterator = product(*[scale for _ in range(times)]) | |
if verbose == 0: | |
yield from iterator | |
else: | |
size = len(scale) ** times | |
desc = f"{scale}^{times}" | |
yield from tqdm(iterator, desc=desc, total=size) | |
def chords_from_notes(notes, inversions=False): | |
for chord in note_to_chord(notes): | |
is_inversion = (chord.on is not None) and (chord.on != chord.root) | |
if inversions or (not is_inversion): | |
yield chord | |
def chords_from_scale(scale, inversions=False, diversity=(2, 5), verbose=0): | |
min_diversity, max_diversity = diversity | |
complexity = relative_complexity(scale) | |
return pd.DataFrame( | |
[ | |
(k, chord, chord.root, chord.on, complexity(chord)) | |
for k in range(min_diversity, max_diversity + 1) | |
for notes in iterate_scale(scale, k, verbose=verbose) | |
for chord in chords_from_notes(notes, inversions=inversions) | |
], | |
columns=["diversity", "chord", "root", "bass", "complexity"], | |
) | |
def chord_table(scale, inversions=False, diversity=(2, 5), verbose=0, index=None, columns=None): | |
complexity = relative_complexity(scale) | |
def complexity_sort(xs): | |
sorted_chords = sorted(list(xs), key=complexity) | |
return ", ".join([chord.chord for chord in sorted_chords]) | |
chords = chords_from_scale(scale, inversions=inversions, diversity=diversity, verbose=verbose) | |
return chords.pivot_table( | |
columns=columns or "complexity", | |
index=index or "root", | |
values="chord", | |
aggfunc=complexity_sort, | |
).fillna("") | |
if __name__ == "__main__": | |
scale = ["E", "F", "G#", "A#", "B", "C", "D"] | |
table = chord_table(scale, inversions=False, columns=["complexity", "diversity"]).loc[scale] | |
print(table) |
Example output:
> scale = ["E", "F", "G#", "A#", "B", "C", "D"]
> chord_table(scale, inversions=False, columns=["complexity", "diversity"]).loc[scale]
Will output:
complexity | 1 | 2 | 3 | 4 | 6 | 10 | ||||
---|---|---|---|---|---|---|---|---|---|---|
diversity | 2 | 3 | 4 | 3 | 4 | 4 | 5 | 4 | 5 | 3 |
root | ||||||||||
C | C5 | C | Csus2, Csus4 | CM7 | Cadd9, C6 | CM9 | Cadd11 | |||
D | D5 | Dm | Dm7 | Dsus2, Dsus4 | D7sus4 | Dm9, D9sus4 | D11 | |||
E | E5 | Em | Em7 | Esus4 | E7sus4 | |||||
F | F5 | F | Fsus2 | FM7 | F6, Fadd9 | FM9 | ||||
G | G5 | G | G7 | Gsus4, Gsus2 | G7sus4, G6, Gadd9 | G9sus4, G9 | Gadd11 | G11 | ||
A | A5 | Am | Am7 | Asus4, Asus2 | A7sus4 | Am9, A9sus4 | A11 | |||
B | Bm7-5 | Bdim |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A script to generate all chords related to a given scale. It looks into all groupings of two, three, four, ... notes that have a name on the
pychord
library listing and create a pandasDataFrame
(for organization and visualization purposes) pivoting the notes by root, bass note, number of notes ("diversity") and a notion of how harmonically complex the chord.That last part is totally ad hoc and based on my own personal feeling. One possible improvement would be to use Euler's gradus function but... well. I'm too lazy to implement that and it doesn't make perfect sense in modern temperament systems. Instead, I grouped the chords and assigned a "complexity" number like this:
And I add 3 to this number if there's an inversion.