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.
@@ -0,0 +1,4 @@
1
+ # The root of the entire project
2
+ from chord_progression_network.chord_progression_network import Generator
3
+
4
+ __version__ = "0.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