chord-progression-network 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chord_progression_network/__init__.py +4 -0
- chord_progression_network/chord_progression_network.py +228 -0
- chord_progression_network-0.1.0.dist-info/METADATA +754 -0
- chord_progression_network-0.1.0.dist-info/RECORD +7 -0
- chord_progression_network-0.1.0.dist-info/WHEEL +5 -0
- chord_progression_network-0.1.0.dist-info/licenses/LICENSE +674 -0
- chord_progression_network-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,228 @@
|
|
1
|
+
import random
|
2
|
+
import re
|
3
|
+
import networkx as nx
|
4
|
+
import musical_scales
|
5
|
+
from pychord import Chord
|
6
|
+
|
7
|
+
class Generator:
|
8
|
+
def __init__(
|
9
|
+
self,
|
10
|
+
max=8,
|
11
|
+
net=None,
|
12
|
+
chord_map=None,
|
13
|
+
scale_name='major',
|
14
|
+
scale_note='C',
|
15
|
+
octave=4,
|
16
|
+
tonic=1,
|
17
|
+
resolve=1,
|
18
|
+
substitute=False,
|
19
|
+
sub_cond=None,
|
20
|
+
flat=False,
|
21
|
+
chord_phrase=False,
|
22
|
+
verbose=False,
|
23
|
+
):
|
24
|
+
self.max = max
|
25
|
+
self.net = net if net is not None else {
|
26
|
+
1: [1, 2, 3, 4, 5, 6],
|
27
|
+
2: [3, 4, 5],
|
28
|
+
3: [1, 2, 4, 6],
|
29
|
+
4: [1, 3, 5, 6],
|
30
|
+
5: [1, 4, 6],
|
31
|
+
6: [1, 2, 4, 5],
|
32
|
+
7: [],
|
33
|
+
}
|
34
|
+
self.scale_name = scale_name
|
35
|
+
self.scale_note = scale_note
|
36
|
+
self.octave = octave
|
37
|
+
self.tonic = tonic
|
38
|
+
self.resolve = resolve
|
39
|
+
self.substitute = substitute
|
40
|
+
self.sub_cond = sub_cond if sub_cond is not None else lambda: random.randint(0, 3) == 0
|
41
|
+
self.flat = flat
|
42
|
+
self.chord_phrase = chord_phrase
|
43
|
+
self.verbose = verbose
|
44
|
+
self.chord_map = chord_map if chord_map is not None else self._build_chord_map()
|
45
|
+
self.scale = self._build_scale()
|
46
|
+
self.graph = self._build_graph()
|
47
|
+
self.phrase = None
|
48
|
+
self.chords = None
|
49
|
+
|
50
|
+
def _build_chord_map(self):
|
51
|
+
scale_maps = {
|
52
|
+
'chromatic': ['m'] * 12,
|
53
|
+
'major': ['', 'm', 'm', '', '', 'm', 'dim'],
|
54
|
+
'ionian': ['', 'm', 'm', '', '', 'm', 'dim'],
|
55
|
+
'dorian': ['m', 'm', '', '', 'm', 'dim', ''],
|
56
|
+
'phrygian': ['m', '', '', 'm', 'dim', '', 'm'],
|
57
|
+
'lydian': ['', '', 'm', 'dim', '', 'm', 'm'],
|
58
|
+
'mixolydian': ['', 'm', 'dim', '', 'm', 'm', ''],
|
59
|
+
'minor': ['m', 'dim', '', 'm', 'm', '', ''],
|
60
|
+
'aeolian': ['m', 'dim', '', 'm', 'm', '', ''],
|
61
|
+
'locrian': ['dim', '', 'm', 'm', '', '', 'm'],
|
62
|
+
}
|
63
|
+
return scale_maps.get(self.scale_name)
|
64
|
+
|
65
|
+
def _build_scale(self):
|
66
|
+
s = musical_scales.scale(self.scale_note)
|
67
|
+
# remove the octave number from the strinified Note
|
68
|
+
s2 = []
|
69
|
+
for n in s:
|
70
|
+
s2.append(re.sub(r"\d+", "", f"{n}"))
|
71
|
+
if self.flat:
|
72
|
+
flattened = [ self._equiv(note) for note in s2 ]
|
73
|
+
s2 = flattened
|
74
|
+
if self.verbose:
|
75
|
+
print('Scale:', s2)
|
76
|
+
return s2
|
77
|
+
|
78
|
+
def _equiv(self, note, is_chord=False):
|
79
|
+
equiv = {
|
80
|
+
'C#': 'Db',
|
81
|
+
'D#': 'Eb',
|
82
|
+
'E#': 'F',
|
83
|
+
'F#': 'Gb',
|
84
|
+
'G#': 'Ab',
|
85
|
+
'A#': 'Bb',
|
86
|
+
'B#': 'C',
|
87
|
+
'Cb': 'B',
|
88
|
+
'Dbb': 'C',
|
89
|
+
'Ebb': 'D',
|
90
|
+
'Fb': 'E',
|
91
|
+
'Gbb': 'F',
|
92
|
+
'Abb': 'G',
|
93
|
+
'Bbb': 'A',
|
94
|
+
}
|
95
|
+
if is_chord:
|
96
|
+
match = re.search(r"^([A-G][#b]+?)(.*)$", note)
|
97
|
+
if match:
|
98
|
+
note = match.group(1)
|
99
|
+
flavor = match.group(2)
|
100
|
+
return equiv.get(note) + flavor if note in equiv else note + flavor
|
101
|
+
else:
|
102
|
+
return note
|
103
|
+
else:
|
104
|
+
match = re.search(r"^([A-G][#b]+?)(\d)$", note)
|
105
|
+
if match:
|
106
|
+
note = match.group(1)
|
107
|
+
octave = match.group(2)
|
108
|
+
return equiv.get(note) + octave if note in equiv else note + octave
|
109
|
+
else:
|
110
|
+
return note
|
111
|
+
|
112
|
+
def _build_graph(self):
|
113
|
+
g = nx.DiGraph()
|
114
|
+
for posn, neighbors in self.net.items():
|
115
|
+
for neighbor in neighbors:
|
116
|
+
g.add_edge(posn, neighbor)
|
117
|
+
return g
|
118
|
+
|
119
|
+
def generate(self):
|
120
|
+
if len(self.chord_map) != len(self.net):
|
121
|
+
raise ValueError('chord_map length must equal number of net keys')
|
122
|
+
|
123
|
+
# build progression of successors of v
|
124
|
+
progression = []
|
125
|
+
v = None
|
126
|
+
for n in range(1, self.max + 1):
|
127
|
+
v = self._next_successor(n, v)
|
128
|
+
progression.append(v)
|
129
|
+
if self.verbose:
|
130
|
+
print('Progression:', progression)
|
131
|
+
|
132
|
+
chord_map = self.chord_map
|
133
|
+
if self.substitute:
|
134
|
+
for i, chord in enumerate(chord_map):
|
135
|
+
substitute = self.substitution(chord) if self.sub_cond() else chord
|
136
|
+
if substitute == chord and i < len(progression) and self.sub_cond():
|
137
|
+
progression[i] = str(progression[i]) + 't'
|
138
|
+
chord_map[i] = substitute
|
139
|
+
if self.verbose:
|
140
|
+
print('Chord map:', chord_map)
|
141
|
+
|
142
|
+
phrase = [self._tt_sub(chord_map, n) for n in progression]
|
143
|
+
self.phrase = phrase
|
144
|
+
if self.verbose:
|
145
|
+
print('Phrase:', self.phrase)
|
146
|
+
|
147
|
+
if self.chord_phrase:
|
148
|
+
if self.flat:
|
149
|
+
phrase = [self._equiv(chord, is_chord=True) for chord in phrase]
|
150
|
+
return phrase
|
151
|
+
else:
|
152
|
+
chords = [self._chord_with_octave(chord) for chord in phrase]
|
153
|
+
if self.flat:
|
154
|
+
chords = [[self._equiv(note) for note in chord] for chord in chords]
|
155
|
+
self.chords = chords
|
156
|
+
if self.verbose:
|
157
|
+
print('Chords:', self.chords)
|
158
|
+
return chords
|
159
|
+
|
160
|
+
def _next_successor(self, n, v):
|
161
|
+
v = v if v is not None else 1
|
162
|
+
s = None
|
163
|
+
if n == 1:
|
164
|
+
if self.tonic == 0:
|
165
|
+
s = self._random_successor(1)
|
166
|
+
elif self.tonic == 1:
|
167
|
+
s = 1
|
168
|
+
else:
|
169
|
+
s = self._full_keys()
|
170
|
+
elif n == self.max:
|
171
|
+
if self.resolve == 0:
|
172
|
+
s = self._random_successor(v) or self._full_keys()
|
173
|
+
elif self.resolve == 1:
|
174
|
+
s = 1
|
175
|
+
else:
|
176
|
+
s = self._full_keys()
|
177
|
+
else:
|
178
|
+
s = self._random_successor(v)
|
179
|
+
return s
|
180
|
+
|
181
|
+
def _random_successor(self, v):
|
182
|
+
successors = list(self.graph.successors(v))
|
183
|
+
return random.choice(successors) if successors else None
|
184
|
+
|
185
|
+
def _full_keys(self):
|
186
|
+
keys = [k for k, v in self.net.items() if len(v) > 0]
|
187
|
+
return random.choice(keys)
|
188
|
+
|
189
|
+
def _tt_sub(self, chord_map, n):
|
190
|
+
note = None
|
191
|
+
if isinstance(n, str) and 't' in n:
|
192
|
+
n = int(n.replace('t', ''))
|
193
|
+
# Tritone substitution logic for chromatic scale
|
194
|
+
chromatic = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']
|
195
|
+
idx = chromatic.index(self.scale[n - 1]) if self.scale[n - 1] in chromatic else 0
|
196
|
+
note = chromatic[(idx + 6) % len(chromatic)]
|
197
|
+
if self.verbose:
|
198
|
+
print(f'Tritone: {self.scale[n - 1]} => {note}')
|
199
|
+
else:
|
200
|
+
note = self.scale[int(n) - 1]
|
201
|
+
note = f"{note}" + chord_map[int(n) - 1]
|
202
|
+
return note
|
203
|
+
|
204
|
+
def _chord_with_octave(self, chord):
|
205
|
+
c = Chord(chord)
|
206
|
+
return c.components_with_pitch(root_pitch=self.octave)
|
207
|
+
|
208
|
+
def substitution(self, chord):
|
209
|
+
substitute = chord
|
210
|
+
if chord in ['', 'm']:
|
211
|
+
roll = random.randint(0, 1)
|
212
|
+
substitute = chord + 'M7' if roll == 0 else chord + '7'
|
213
|
+
elif chord in ['dim', 'aug']:
|
214
|
+
substitute = chord + '7'
|
215
|
+
elif chord in ['-5', '-9']:
|
216
|
+
substitute = f"7({chord})"
|
217
|
+
elif chord == 'M7':
|
218
|
+
roll = random.randint(0, 2)
|
219
|
+
substitute = ['M9', 'M11', 'M13'][roll]
|
220
|
+
elif chord == '7':
|
221
|
+
roll = random.randint(0, 2)
|
222
|
+
substitute = ['9', '11', '13'][roll]
|
223
|
+
elif chord == 'm7':
|
224
|
+
roll = random.randint(0, 2)
|
225
|
+
substitute = ['m9', 'm11', 'm13'][roll]
|
226
|
+
if self.verbose and substitute != chord:
|
227
|
+
print(f'Substitute: "{chord}" => "{substitute}"')
|
228
|
+
return substitute
|