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.
- chord_progressions-0.34.0/LICENSE +21 -0
- chord_progressions-0.34.0/PKG-INFO +104 -0
- chord_progressions-0.34.0/README.md +47 -0
- chord_progressions-0.34.0/chord_progressions/__init__.py +19 -0
- chord_progressions-0.34.0/chord_progressions/chord.py +88 -0
- chord_progressions-0.34.0/chord_progressions/evaluate.py +177 -0
- chord_progressions-0.34.0/chord_progressions/midi.py +31 -0
- chord_progressions-0.34.0/chord_progressions/pitch.py +393 -0
- chord_progressions-0.34.0/chord_progressions/progression.py +211 -0
- chord_progressions-0.34.0/chord_progressions/rhythm.py +85 -0
- chord_progressions-0.34.0/chord_progressions/solver.py +400 -0
- chord_progressions-0.34.0/chord_progressions/type_templates.py +324 -0
- chord_progressions-0.34.0/chord_progressions/utils.py +78 -0
- chord_progressions-0.34.0/chord_progressions.egg-info/PKG-INFO +104 -0
- chord_progressions-0.34.0/chord_progressions.egg-info/SOURCES.txt +27 -0
- chord_progressions-0.34.0/chord_progressions.egg-info/dependency_links.txt +1 -0
- chord_progressions-0.34.0/chord_progressions.egg-info/requires.txt +22 -0
- chord_progressions-0.34.0/chord_progressions.egg-info/top_level.txt +1 -0
- chord_progressions-0.34.0/pyproject.toml +62 -0
- chord_progressions-0.34.0/setup.cfg +26 -0
- chord_progressions-0.34.0/tests/test_chord.py +15 -0
- chord_progressions-0.34.0/tests/test_evaluate.py +43 -0
- chord_progressions-0.34.0/tests/test_pitch.py +120 -0
- chord_progressions-0.34.0/tests/test_progression.py +76 -0
- chord_progressions-0.34.0/tests/test_rhythm.py +28 -0
- chord_progressions-0.34.0/tests/test_solver.py +172 -0
- chord_progressions-0.34.0/tests/test_type_templates.py +79 -0
- 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)
|