chord-progressions 0.34.0__tar.gz

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.
Files changed (28) hide show
  1. chord_progressions-0.34.0/LICENSE +21 -0
  2. chord_progressions-0.34.0/PKG-INFO +104 -0
  3. chord_progressions-0.34.0/README.md +47 -0
  4. chord_progressions-0.34.0/chord_progressions/__init__.py +19 -0
  5. chord_progressions-0.34.0/chord_progressions/chord.py +88 -0
  6. chord_progressions-0.34.0/chord_progressions/evaluate.py +177 -0
  7. chord_progressions-0.34.0/chord_progressions/midi.py +31 -0
  8. chord_progressions-0.34.0/chord_progressions/pitch.py +393 -0
  9. chord_progressions-0.34.0/chord_progressions/progression.py +211 -0
  10. chord_progressions-0.34.0/chord_progressions/rhythm.py +85 -0
  11. chord_progressions-0.34.0/chord_progressions/solver.py +400 -0
  12. chord_progressions-0.34.0/chord_progressions/type_templates.py +324 -0
  13. chord_progressions-0.34.0/chord_progressions/utils.py +78 -0
  14. chord_progressions-0.34.0/chord_progressions.egg-info/PKG-INFO +104 -0
  15. chord_progressions-0.34.0/chord_progressions.egg-info/SOURCES.txt +27 -0
  16. chord_progressions-0.34.0/chord_progressions.egg-info/dependency_links.txt +1 -0
  17. chord_progressions-0.34.0/chord_progressions.egg-info/requires.txt +22 -0
  18. chord_progressions-0.34.0/chord_progressions.egg-info/top_level.txt +1 -0
  19. chord_progressions-0.34.0/pyproject.toml +62 -0
  20. chord_progressions-0.34.0/setup.cfg +26 -0
  21. chord_progressions-0.34.0/tests/test_chord.py +15 -0
  22. chord_progressions-0.34.0/tests/test_evaluate.py +43 -0
  23. chord_progressions-0.34.0/tests/test_pitch.py +120 -0
  24. chord_progressions-0.34.0/tests/test_progression.py +76 -0
  25. chord_progressions-0.34.0/tests/test_rhythm.py +28 -0
  26. chord_progressions-0.34.0/tests/test_solver.py +172 -0
  27. chord_progressions-0.34.0/tests/test_type_templates.py +79 -0
  28. chord_progressions-0.34.0/tests/test_utils.py +26 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Patricio Ovalle
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.1
2
+ Name: chord-progressions
3
+ Version: 0.34.0
4
+ Summary: A Python package for extracting, analyzing, and generating chord progressions
5
+ Author-email: p3zo <p3zo@users.noreply.github.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 Patricio Ovalle
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/p3zo/chord-progressions
29
+ Project-URL: Issues, https://github.com/p3zo/chord-progressions/issues
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Operating System :: OS Independent
33
+ Classifier: Development Status :: 3 - Alpha
34
+ Requires-Python: >=3.9
35
+ Description-Content-Type: text/markdown
36
+ License-File: LICENSE
37
+ Requires-Dist: matplotlib~=3.5.1
38
+ Requires-Dist: mido~=1.2.10
39
+ Requires-Dist: networkx~=2.8
40
+ Requires-Dist: numpy~=1.22.3
41
+ Requires-Dist: pandas~=1.4.2
42
+ Requires-Dist: pretty_midi~=0.2.10
43
+ Requires-Dist: scipy~=1.8.0
44
+ Provides-Extra: dev
45
+ Requires-Dist: pytest; extra == "dev"
46
+ Requires-Dist: pytest-cov; extra == "dev"
47
+ Requires-Dist: black; extra == "dev"
48
+ Requires-Dist: flake8; extra == "dev"
49
+ Requires-Dist: ipdb; extra == "dev"
50
+ Requires-Dist: ipython; extra == "dev"
51
+ Requires-Dist: isort; extra == "dev"
52
+ Requires-Dist: bump-my-version; extra == "dev"
53
+ Requires-Dist: mkdocs-material; extra == "dev"
54
+ Provides-Extra: test
55
+ Requires-Dist: pytest; extra == "test"
56
+ Requires-Dist: pytest-cov; extra == "test"
57
+
58
+ # Chord progressions
59
+
60
+ ## Usage
61
+
62
+ See the [Makefile](Makefile) for available actions.
63
+
64
+ ## Docs
65
+
66
+ The documentation uses [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) to build a static site from
67
+ Markdown files. Use `mkdocs serve` to start a live preview server of the site that automatically rebuilds upon saving.
68
+ Use `mkdocs build` to build the site.
69
+
70
+ ## TODO
71
+
72
+ Analysis
73
+
74
+ - [feature] get key for progression
75
+ - [feature] get functional names for chords given key
76
+ - [feature] extend interval class vectors to "polychord content" vectors
77
+
78
+ I/O
79
+
80
+ - [feature] add `from_audio` and `from_midi` constructors to Progression class
81
+ - [maintainability] use a singular method of counting in `extract_harman.py`
82
+ - `get_segment_label` uses counter
83
+ - `get_segment_pc_weights` uses defaultdict
84
+ - `MinimalSegment.get_pitch_class_weights` uses list.count()
85
+ - [performance] `extract.midi_harman.segment_and_label()` gets really slow for many consecutive segments. test that a
86
+ maximum of six segment evaluations occur per note
87
+
88
+ Generation
89
+
90
+ - [feature] VAE solver
91
+ - [feature] use variable chord durations
92
+ - [feature] consider lower and upper structures in select_voicing
93
+ - [feature] add the possibility of repeated notes in different octaves
94
+ - [feature] ability to constrain key
95
+
96
+ Misc
97
+
98
+ - [feature] Host docs site
99
+ - [feature] Generalize `noteNumberToFrequency` to use any periodic
100
+ tuning (see https://github.com/soul-lang/SOUL/pull/26/files)
101
+ - [feature] implement `is_partial_circular_match`, `chord_contained_in_type`, and `get_possible_types_of_chord`
102
+ - [maintainability] mk `pitch_class` refer to "C" and call 0 `pitch_class_ix`
103
+ - [maintainability] pass midi note numbers everywhere as "notes" and include note names as "noteNames"
104
+ - [maintainability] test the merging of chord types
@@ -0,0 +1,47 @@
1
+ # Chord progressions
2
+
3
+ ## Usage
4
+
5
+ See the [Makefile](Makefile) for available actions.
6
+
7
+ ## Docs
8
+
9
+ The documentation uses [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) to build a static site from
10
+ Markdown files. Use `mkdocs serve` to start a live preview server of the site that automatically rebuilds upon saving.
11
+ Use `mkdocs build` to build the site.
12
+
13
+ ## TODO
14
+
15
+ Analysis
16
+
17
+ - [feature] get key for progression
18
+ - [feature] get functional names for chords given key
19
+ - [feature] extend interval class vectors to "polychord content" vectors
20
+
21
+ I/O
22
+
23
+ - [feature] add `from_audio` and `from_midi` constructors to Progression class
24
+ - [maintainability] use a singular method of counting in `extract_harman.py`
25
+ - `get_segment_label` uses counter
26
+ - `get_segment_pc_weights` uses defaultdict
27
+ - `MinimalSegment.get_pitch_class_weights` uses list.count()
28
+ - [performance] `extract.midi_harman.segment_and_label()` gets really slow for many consecutive segments. test that a
29
+ maximum of six segment evaluations occur per note
30
+
31
+ Generation
32
+
33
+ - [feature] VAE solver
34
+ - [feature] use variable chord durations
35
+ - [feature] consider lower and upper structures in select_voicing
36
+ - [feature] add the possibility of repeated notes in different octaves
37
+ - [feature] ability to constrain key
38
+
39
+ Misc
40
+
41
+ - [feature] Host docs site
42
+ - [feature] Generalize `noteNumberToFrequency` to use any periodic
43
+ tuning (see https://github.com/soul-lang/SOUL/pull/26/files)
44
+ - [feature] implement `is_partial_circular_match`, `chord_contained_in_type`, and `get_possible_types_of_chord`
45
+ - [maintainability] mk `pitch_class` refer to "C" and call 0 `pitch_class_ix`
46
+ - [maintainability] pass midi note numbers everywhere as "notes" and include note names as "noteNames"
47
+ - [maintainability] test the merging of chord types
@@ -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)