chord-progressions 0.34.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,19 @@
1
+ import logging
2
+ import os
3
+
4
+ __version__ = "0.34.0" # updated by bumpversion, do not change
5
+
6
+ THIS_DIR = os.path.dirname(os.path.realpath(__file__))
7
+
8
+ OUTPUT_DIR = os.path.join(THIS_DIR, "../assets/output")
9
+
10
+ # a copy of /usr/share/dict/web2 from a macbook air (early 2014)
11
+ WORDS_FILEPATH = os.path.join(THIS_DIR, "..", "words")
12
+
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger("chord-progressions")
15
+
16
+ # Config
17
+ # TODO: move to config
18
+ DEFAULT_BPM = 120
19
+ DEFAULT_MIDI_TICKS_PER_BEAT = 480
@@ -0,0 +1,88 @@
1
+ from uuid import uuid4
2
+
3
+ from chord_progressions import logger
4
+ from chord_progressions.evaluate import evaluate_notes
5
+ from chord_progressions.io.audio import mk_chord_buffer, save_audio_buffer
6
+ from chord_progressions.io.midi import get_midi_from_chord
7
+ from chord_progressions.pitch import get_midi_num_from_note, get_note_from_midi_num
8
+ from chord_progressions.type_templates import (
9
+ get_template_from_notes,
10
+ get_type_from_midi_nums,
11
+ get_type_num_from_type,
12
+ )
13
+
14
+ NoteList = list[str]
15
+ MidiNumList = list[int]
16
+
17
+
18
+ class Chord:
19
+ """A set of unique notes.
20
+
21
+ Parameters
22
+ ----------
23
+ notes: list[Chord], default []
24
+ The set of notes in the chord. Can be specified as a list of midi numbers or as a list of note names.
25
+ e.g. Chord([60, 64, 67])
26
+ e.g. Chord(["C4", "E4", "G4"])
27
+ """
28
+
29
+ def __init__(self, notes: list = [], id: str = None):
30
+ if isinstance(notes, list) and len(notes) > 0 and isinstance(notes[0], str):
31
+ notes = [get_midi_num_from_note(n) for n in notes]
32
+
33
+ self.init_from_midi_nums(midi_nums=notes, id=id)
34
+
35
+ def init_from_midi_nums(self, midi_nums: MidiNumList = [], id: str = None):
36
+ if any([i < 0 or i > 128 for i in midi_nums]):
37
+ raise ValueError("The valid range of midi numbers is 0 to 128")
38
+
39
+ midi_nums = sorted(list(set(midi_nums)))
40
+ self.midi_nums = [int(i) for i in midi_nums]
41
+
42
+ chord_type = get_type_from_midi_nums(midi_nums)
43
+ self.type = chord_type
44
+ self.typeId = get_type_num_from_type(chord_type)
45
+
46
+ notes = [get_note_from_midi_num(n) for n in midi_nums]
47
+ self.notes = notes
48
+ self.metrics = evaluate_notes(notes)
49
+ self.template = get_template_from_notes(notes)
50
+
51
+ if not id:
52
+ id = str(uuid4())
53
+ self.id = id
54
+
55
+ def __repr__(self):
56
+ return "Chord " + self.to_string()
57
+
58
+ def to_string(self):
59
+ return str(self.to_json())
60
+
61
+ def to_json(self):
62
+ return {
63
+ "id": self.id,
64
+ "midi_nums": self.midi_nums,
65
+ "type": self.type,
66
+ "typeId": self.typeId,
67
+ "notes": self.notes,
68
+ "metrics": self.metrics,
69
+ }
70
+
71
+ def to_audio(self, outpath=None, n_overtones=4):
72
+ audio = mk_chord_buffer(self.notes, 1, n_overtones)
73
+
74
+ if outpath:
75
+ save_audio_buffer(audio, outpath)
76
+ logger.info(f"Audio saved to {outpath}")
77
+ else:
78
+ return audio
79
+
80
+ def to_midi(self, outpath=None):
81
+ mid = get_midi_from_chord(self.chords, 1, 120, "-".join(self.notes))
82
+
83
+ if outpath:
84
+ mid.filename = outpath
85
+ mid.save(outpath)
86
+ logger.info(f"Midi saved to {outpath}")
87
+ else:
88
+ return mid
@@ -0,0 +1,177 @@
1
+ """
2
+ Defines the metrics computed for every progression.
3
+
4
+ TODO:
5
+ - A common heuristic is that an interval is consonant when the ratio between the frequencies is simple.
6
+ - Resonance
7
+ - Brilliance: "for maximum brilliance let the lower tones [be somehow equally spaced out as] their overtones"
8
+ - Vincent Persechetti, Twentieth-Century Harmony
9
+ - something like https://en.xen.wiki/w/Harmonic_Entropy
10
+ """
11
+
12
+ import itertools
13
+
14
+ import numpy as np
15
+ from chord_progressions.pitch import (
16
+ get_freq_from_note,
17
+ get_midi_num_from_note,
18
+ get_n_overtones_harmonic,
19
+ get_pitch_class_from_note,
20
+ )
21
+ from chord_progressions.type_templates import get_template_from_notes
22
+
23
+
24
+ def get_interval_class_vector(notes):
25
+ """
26
+ Returns a vector of interval classes.
27
+ 0: minor second / major seventh (1 or 11 semitones)
28
+ 1: major second / minor seventh (2 or 10 semitones)
29
+ 2: minor third / major sixth (3 or 9 semitones)
30
+ 3: major third / minor sixth (4 or 8 semitones)
31
+ 4: perfect fourth / perfect fifth (5 or 7 semitones)
32
+ 5: tritone (6 semitones)
33
+ """
34
+ vec = [0] * 6
35
+
36
+ template = get_template_from_notes(notes)
37
+ one_indices = [ix for ix, i in enumerate(template) if i == 1]
38
+ pairs = list(itertools.combinations(one_indices, 2))
39
+
40
+ intervals = [p[1] - p[0] for p in pairs]
41
+
42
+ modded = [12 - i if i > 6 else i for i in intervals]
43
+
44
+ for i in range(6):
45
+ vec[i] = modded.count(i + 1)
46
+
47
+ return vec
48
+
49
+
50
+ def get_evenness(interval_class_vector):
51
+ """
52
+ A rough measure of acoustic consonance. Highly consonant chords divide the octave nearly evenly.
53
+ Note that acoustic consonance implies near-evenness, but not the reverse.
54
+ For background on the equation see https://www.researchgate.net/profile/Jack_Douthett/publication/249881698_Vector_Products_and_Intervallic_Weighting/links/575061d708ae1c34b39aaa1b.pdf
55
+ """
56
+ weight_vector = [0] * 6
57
+
58
+ for i in range(6):
59
+
60
+ k = i + 1
61
+
62
+ weight_vector[i] += 2 * np.sin(k * np.pi / 12)
63
+
64
+ weight = np.dot(weight_vector, interval_class_vector)
65
+
66
+ return weight
67
+
68
+
69
+ def get_relative_evenness(evenness, cardinality):
70
+ """
71
+ Calculate the evenness of a pitch class set relative to its possible range.
72
+ A relative value is useful because highly-even dyads will have a lower evenness than highly uneven hexachords.
73
+ The result is a value between 0 and 1.
74
+ """
75
+ # TODO: replace this table with a formula
76
+ MIN_MAX_EVENNESS_BY_CARDINALITY = [
77
+ [0.0, 0.0],
78
+ [0.5176380902050415, 2.0],
79
+ [2.035276180410083, 5.196152422706632],
80
+ [6.1815405503520555, 9.65685424949238],
81
+ [9.631030293135233, 15.287884542627612],
82
+ [16.226784405860382, 22.392304845413264],
83
+ [24.822538518585535, 30.479392768077915],
84
+ [35.350144283888824, 40.03987070039298],
85
+ [47.609800856760984, 50.77067709905754],
86
+ [61.28367099200624, 62.7660329018012],
87
+ [75.9575411272515, 75.9575411272515],
88
+ [91.14904935270181, 91.14904935270181],
89
+ ]
90
+
91
+ emin, emax = MIN_MAX_EVENNESS_BY_CARDINALITY[cardinality - 1]
92
+ return (evenness - emin) / (emax - emin)
93
+
94
+
95
+ def get_overtone_agreement(notes):
96
+
97
+ freqs = [get_freq_from_note(n) for n in notes]
98
+
99
+ all_partials = []
100
+
101
+ n_overtones = 10
102
+ for f in freqs:
103
+ all_partials.extend(get_n_overtones_harmonic(f, n_overtones))
104
+
105
+ sorted_partials = sorted(all_partials)
106
+
107
+ sum_of_differences = 0
108
+
109
+ for i in range(1, len(sorted_partials)):
110
+ diff = sorted_partials[i] - sorted_partials[i - 1]
111
+ sum_of_differences += diff
112
+
113
+ return sum_of_differences
114
+
115
+
116
+ def evaluate_notes(notes):
117
+ """list of notes, e.g. ["C4", "E4", "G4"]"""
118
+
119
+ metrics = {}
120
+
121
+ interval_class_vector = get_interval_class_vector(notes)
122
+
123
+ # evenness = get_evenness(interval_class_vector)
124
+
125
+ pc_cardinality = len(set([get_pitch_class_from_note(n) for n in notes]))
126
+ assert pc_cardinality <= 12, "Pitch class cardinality > 12"
127
+
128
+ metrics["num_notes"] = len(notes)
129
+ metrics["num_pitches"] = len(set(notes))
130
+ # metrics["pc_cardinality"] = pc_cardinality
131
+ metrics["interval_class_vector"] = interval_class_vector
132
+ # metrics["ambitus"] = get_ambitus(notes)
133
+ # metrics["evenness"] = evenness
134
+ # metrics["relative_evenness"] = get_relative_evenness(evenness, pc_cardinality)
135
+ # metrics["overtone_agreement"] = get_overtone_agreement(notes)
136
+
137
+ return metrics
138
+
139
+
140
+ def get_macroharmony(progression):
141
+ """The total collection of notes"""
142
+
143
+ collection = set()
144
+
145
+ for notes in progression:
146
+ for note in notes:
147
+ collection.add(note)
148
+
149
+ return collection
150
+
151
+
152
+ def get_ambitus(macroharmony):
153
+ """Ambitus, int, # semitones between the lowest and highest note"""
154
+ midi_notes = [get_midi_num_from_note(n) for n in macroharmony]
155
+
156
+ return max(midi_notes) - min(midi_notes)
157
+
158
+
159
+ def evaluate_progression(progression):
160
+ metrics = {}
161
+
162
+ # macroharmony = get_macroharmony(notes_list)
163
+
164
+ # metrics["ambitus"] = get_ambitus(macroharmony)
165
+
166
+ # metrics["density"] = len(macroharmony) / len(notes_list)
167
+
168
+ return metrics
169
+
170
+
171
+ def get_n_overtones_for_notes(notes, n):
172
+ """Calculates `n` overtone frequencies for each note in `notes`"""
173
+
174
+ freqs = [get_freq_from_note(n) for n in notes]
175
+ ofreqs = [get_n_overtones_harmonic(f, n) for f in freqs]
176
+
177
+ return ofreqs
@@ -0,0 +1,31 @@
1
+ import mido
2
+ from chord_progressions import DEFAULT_MIDI_TICKS_PER_BEAT
3
+
4
+
5
+ def make_midi_chord(chord, end_tick: int):
6
+ note_onsets = []
7
+ note_offsets = []
8
+
9
+ first_note = True
10
+ for midi_num in chord.midi_nums:
11
+ note_onsets.append(mido.Message("note_on", note=midi_num, time=0))
12
+
13
+ if not first_note:
14
+ end_tick = 0
15
+
16
+ note_offsets.append(mido.Message("note_off", note=midi_num, time=end_tick))
17
+ first_note = False
18
+
19
+ return note_onsets + note_offsets
20
+
21
+
22
+ def get_midi_ticks_from_seconds(
23
+ seconds, bpm, ticks_per_beat=DEFAULT_MIDI_TICKS_PER_BEAT
24
+ ):
25
+ midi_tempo = mido.bpm2tempo(bpm)
26
+ return int(mido.second2tick(seconds, ticks_per_beat, midi_tempo))
27
+
28
+
29
+ def get_seconds_from_midi_ticks(ticks, bpm, ticks_per_beat=DEFAULT_MIDI_TICKS_PER_BEAT):
30
+ midi_tempo = mido.bpm2tempo(bpm)
31
+ return mido.tick2second(ticks, ticks_per_beat, midi_tempo)
@@ -0,0 +1,393 @@
1
+ A4 = 440
2
+
3
+
4
+ NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
5
+
6
+ MIDI_NOTES = [
7
+ "C-1",
8
+ "C#-1",
9
+ "D-1",
10
+ "D#-1",
11
+ "E-1",
12
+ "F-1",
13
+ "F#-1",
14
+ "G-1",
15
+ "G#-1",
16
+ "A-1",
17
+ "A#-1",
18
+ "B-1",
19
+ "C0",
20
+ "C#0",
21
+ "D0",
22
+ "D#0",
23
+ "E0",
24
+ "F0",
25
+ "F#0",
26
+ "G0",
27
+ "G#0",
28
+ # start piano range
29
+ "A0",
30
+ "A#0",
31
+ "B0",
32
+ "C1",
33
+ "C#1",
34
+ "D1",
35
+ "D#1",
36
+ "E1",
37
+ "F1",
38
+ "F#1",
39
+ "G1",
40
+ "G#1",
41
+ "A1",
42
+ "A#1",
43
+ "B1",
44
+ "C2",
45
+ "C#2",
46
+ "D2",
47
+ "D#2",
48
+ "E2",
49
+ "F2",
50
+ "F#2",
51
+ "G2",
52
+ "G#2",
53
+ "A2",
54
+ "A#2",
55
+ "B2",
56
+ "C3",
57
+ "C#3",
58
+ "D3",
59
+ "D#3",
60
+ "E3",
61
+ "F3",
62
+ "F#3",
63
+ "G3",
64
+ "G#3",
65
+ "A3",
66
+ "A#3",
67
+ "B3",
68
+ "C4",
69
+ "C#4",
70
+ "D4",
71
+ "D#4",
72
+ "E4",
73
+ "F4",
74
+ "F#4",
75
+ "G4",
76
+ "G#4",
77
+ "A4",
78
+ "A#4",
79
+ "B4",
80
+ "C5",
81
+ "C#5",
82
+ "D5",
83
+ "D#5",
84
+ "E5",
85
+ "F5",
86
+ "F#5",
87
+ "G5",
88
+ "G#5",
89
+ "A5",
90
+ "A#5",
91
+ "B5",
92
+ "C6",
93
+ "C#6",
94
+ "D6",
95
+ "D#6",
96
+ "E6",
97
+ "F6",
98
+ "F#6",
99
+ "G6",
100
+ "G#6",
101
+ "A6",
102
+ "A#6",
103
+ "B6",
104
+ "C7",
105
+ "C#7",
106
+ "D7",
107
+ "D#7",
108
+ "E7",
109
+ "F7",
110
+ "F#7",
111
+ "G7",
112
+ "G#7",
113
+ "A7",
114
+ "A#7",
115
+ "B7",
116
+ "C8",
117
+ # end piano range
118
+ "C#8",
119
+ "D8",
120
+ "D#8",
121
+ "E8",
122
+ "F8",
123
+ "F#8",
124
+ "G8",
125
+ "G#8",
126
+ "A8",
127
+ "A#8",
128
+ "B8",
129
+ "C9",
130
+ "C#9",
131
+ "D9",
132
+ "D#9",
133
+ "E9",
134
+ "F9",
135
+ "F#9",
136
+ "G9",
137
+ ]
138
+
139
+ MIDI_NOTE_FREQUENCIES = [
140
+ 8.18,
141
+ 8.66,
142
+ 9.18,
143
+ 9.72,
144
+ 10.3,
145
+ 10.91,
146
+ 11.56,
147
+ 12.25,
148
+ 12.98,
149
+ 13.75,
150
+ 14.57,
151
+ 15.43,
152
+ 16.35,
153
+ 17.32,
154
+ 18.35,
155
+ 19.45,
156
+ 20.6,
157
+ 21.83,
158
+ 23.12,
159
+ 24.5,
160
+ 25.96,
161
+ # start piano range
162
+ 27.5,
163
+ 29.14,
164
+ 30.87,
165
+ 32.7,
166
+ 34.65,
167
+ 36.71,
168
+ 38.89,
169
+ 41.2,
170
+ 43.65,
171
+ 46.25,
172
+ 49.0,
173
+ 51.91,
174
+ 55.0,
175
+ 58.27,
176
+ 61.74,
177
+ 65.41,
178
+ 69.3,
179
+ 73.42,
180
+ 77.78,
181
+ 82.41,
182
+ 87.31,
183
+ 92.5,
184
+ 98.0,
185
+ 103.83,
186
+ 110.0,
187
+ 116.54,
188
+ 123.47,
189
+ 130.81,
190
+ 138.59,
191
+ 146.83,
192
+ 155.56,
193
+ 164.81,
194
+ 174.61,
195
+ 185.0,
196
+ 196.0,
197
+ 207.65,
198
+ 220.0,
199
+ 233.08,
200
+ 246.94,
201
+ 261.63,
202
+ 277.18,
203
+ 293.66,
204
+ 311.13,
205
+ 329.63,
206
+ 349.23,
207
+ 369.99,
208
+ 392.0,
209
+ 415.3,
210
+ 440.0,
211
+ 466.16,
212
+ 493.88,
213
+ 523.25,
214
+ 554.37,
215
+ 587.33,
216
+ 622.25,
217
+ 659.26,
218
+ 698.46,
219
+ 739.99,
220
+ 783.99,
221
+ 830.61,
222
+ 880.0,
223
+ 932.33,
224
+ 987.77,
225
+ 1046.5,
226
+ 1108.73,
227
+ 1174.66,
228
+ 1244.51,
229
+ 1318.51,
230
+ 1396.91,
231
+ 1479.98,
232
+ 1567.98,
233
+ 1661.22,
234
+ 1760.0,
235
+ 1864.66,
236
+ 1975.53,
237
+ 2093.0,
238
+ 2217.46,
239
+ 2349.32,
240
+ 2489.02,
241
+ 2637.02,
242
+ 2793.83,
243
+ 2959.96,
244
+ 3135.96,
245
+ 3322.44,
246
+ 3520.0,
247
+ 3729.31,
248
+ 3951.07,
249
+ 4186.01,
250
+ # end piano range
251
+ 4434.92,
252
+ 4698.64,
253
+ 4978.03,
254
+ 5274.04,
255
+ 5587.65,
256
+ 5919.91,
257
+ 6271.93,
258
+ 6644.88,
259
+ 7040.0,
260
+ 7458.62,
261
+ 7902.13,
262
+ 8372.02,
263
+ 8869.84,
264
+ 9397.27,
265
+ 9956.06,
266
+ 10548.08,
267
+ 11175.3,
268
+ 11839.82,
269
+ 12543.85,
270
+ ]
271
+
272
+
273
+ def get_note_from_midi_num(note_num):
274
+ try:
275
+ return MIDI_NOTES[int(note_num)]
276
+ except IndexError:
277
+ raise IndexError(f"Invalid midi note number: {note_num}")
278
+
279
+
280
+ def get_midi_num_from_note(note):
281
+ try:
282
+ return MIDI_NOTES.index(note)
283
+ except ValueError:
284
+ raise ValueError(f"Invalid note name: {note}")
285
+
286
+
287
+ def get_pitch_class_from_note(note):
288
+ """Returns the pitch class of a note, e.g. get_pitch_class_from_note(C4) -> 0"""
289
+ midi_num = get_midi_num_from_note(note)
290
+ return get_pitch_class_from_midi_num(midi_num)
291
+
292
+
293
+ def get_pitch_class_from_midi_num(note_num):
294
+ """Returns the pitch class of a midi num"""
295
+ return note_num % 12
296
+
297
+
298
+ def get_note_name_from_note(note):
299
+ """Drops the octave part of the note, e.g. A#4 -> A#"""
300
+
301
+ midi_num = get_midi_num_from_note(note) % 12
302
+ pitch_class = get_pitch_class_from_midi_num(midi_num)
303
+
304
+ return NOTE_NAMES[pitch_class]
305
+
306
+
307
+ def get_notes_from_midi_nums_str(midi_nums_str):
308
+ """e.g. "60-48" -> ["C4", "C3"]"""
309
+ return [get_note_from_midi_num(s) for s in midi_nums_str.split("-")]
310
+
311
+
312
+ def get_notes_list_from_midi_nums_str(midi_nums_str):
313
+ """e.g. "60-48_62-50" -> [["C4", "C3"], ["D4", "E3"]]"""
314
+ return [get_notes_from_midi_nums_str(s) for s in midi_nums_str.split("_")]
315
+
316
+
317
+ def get_midi_nums_list_from_midi_nums_str(midi_nums_str):
318
+ """e.g. "60-48_62-50" -> [[60, 48], [62, 50]]
319
+ If any chord in `midi_nums_str` is invalid, returns an empty string for that chord"""
320
+ midi_nums_list = []
321
+ for m in midi_nums_str.split("_"):
322
+ try:
323
+ midi_nums_list.append([int(i) for i in m.split("-")])
324
+ except:
325
+ pass
326
+
327
+ return midi_nums_list
328
+
329
+
330
+ def get_octave_from_note(note):
331
+
332
+ note_name = get_note_name_from_note(note)
333
+
334
+ octave = note.split(note_name)[1]
335
+
336
+ return int(octave)
337
+
338
+
339
+ def get_freq_from_midi_num(n):
340
+ return round(A4 * 2 ** ((n - 69) / 12), 2)
341
+
342
+
343
+ def increment_note(note):
344
+ """Returns the note a half step above `note`"""
345
+
346
+ note_name = get_note_name_from_note(note)
347
+
348
+ new_name_ix = NOTE_NAMES.index(note_name) + 1 % 12
349
+ new_note_name = NOTE_NAMES[new_name_ix % 12]
350
+
351
+ octave = get_octave_from_note(note)
352
+
353
+ if new_name_ix == 12:
354
+ octave += 1
355
+
356
+ return new_note_name + str(octave)
357
+
358
+
359
+ def create_notes_freqs_table():
360
+ """This is a one-off function that was used to create the MIDI_NOTE_FREQUENCIES constant"""
361
+
362
+ from collections import OrderedDict # # noqa
363
+
364
+ notes = OrderedDict()
365
+
366
+ current_note = "C-1"
367
+
368
+ for i in range(128):
369
+ notes[current_note] = get_freq_from_midi_num(i)
370
+ current_note = increment_note(current_note)
371
+
372
+ return notes
373
+
374
+
375
+ def get_note_list(note_range_low=0, note_range_high=127):
376
+
377
+ start_ix = note_range_low
378
+ end_ix = note_range_high
379
+
380
+ return MIDI_NOTES[start_ix : end_ix + 1]
381
+
382
+
383
+ def get_freq_from_note(note):
384
+ """Returns the frequency of a note, e.g. `A4`"""
385
+
386
+ ix = get_midi_num_from_note(note)
387
+
388
+ return MIDI_NOTE_FREQUENCIES[ix]
389
+
390
+
391
+ def get_n_overtones_harmonic(freq, n):
392
+ """Compute a list of n overtones above a given freq"""
393
+ return [i * freq for i in range(1, n + 2)]