klotho-cac 2.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.
- klotho/__init__.py +35 -0
- klotho/aikous/__init__.py +42 -0
- klotho/aikous/expression/__init__.py +29 -0
- klotho/aikous/expression/dynamics.py +174 -0
- klotho/aikous/expression/enevelopes.py +89 -0
- klotho/aikous/instruments/__init__.py +1 -0
- klotho/aikous/instruments/instrument.py +76 -0
- klotho/aikous/parameters/__init__.py +1 -0
- klotho/aikous/parameters/instruments.py +142 -0
- klotho/aikous/parameters/parameter_tree.py +69 -0
- klotho/chronos/__init__.py +53 -0
- klotho/chronos/rhythm_pairs/__init__.py +3 -0
- klotho/chronos/rhythm_pairs/rhythm_pair.py +102 -0
- klotho/chronos/rhythm_trees/__init__.py +7 -0
- klotho/chronos/rhythm_trees/algorithms.py +219 -0
- klotho/chronos/rhythm_trees/meas.py +232 -0
- klotho/chronos/rhythm_trees/rhythm_tree.py +212 -0
- klotho/chronos/temporal_units/__init__.py +4 -0
- klotho/chronos/temporal_units/algorithms.py +142 -0
- klotho/chronos/temporal_units/temporal.py +865 -0
- klotho/chronos/utils/__init__.py +3 -0
- klotho/chronos/utils/beat.py +50 -0
- klotho/chronos/utils/tempo.py +85 -0
- klotho/chronos/utils/time_conversion.py +129 -0
- klotho/skora/__init__.py +16 -0
- klotho/skora/animation/__init__.py +1 -0
- klotho/skora/animation/animate.py +193 -0
- klotho/skora/notation/__init__.py +1 -0
- klotho/skora/notation/notation.py +546 -0
- klotho/skora/notation/notation_OLD.py +261 -0
- klotho/skora/skora.py +329 -0
- klotho/skora/visualization/__init__.py +3 -0
- klotho/skora/visualization/field_plots.py +78 -0
- klotho/skora/visualization/plots.py +999 -0
- klotho/skora/visualization/ut_plots.py +54 -0
- klotho/tonos/__init__.py +125 -0
- klotho/tonos/chords/__init__.py +1 -0
- klotho/tonos/chords/chord.py +144 -0
- klotho/tonos/pitch/__init__.py +21 -0
- klotho/tonos/pitch/pitch.py +173 -0
- klotho/tonos/pitch/pitch_collections.py +610 -0
- klotho/tonos/scales/__init__.py +1 -0
- klotho/tonos/scales/scale.py +127 -0
- klotho/tonos/systems/__init__.py +17 -0
- klotho/tonos/systems/combination_product_sets/__init__.py +13 -0
- klotho/tonos/systems/combination_product_sets/combination_product_networks.py +36 -0
- klotho/tonos/systems/combination_product_sets/cps.py +435 -0
- klotho/tonos/systems/harmonic_trees/__init__.py +7 -0
- klotho/tonos/systems/harmonic_trees/algorithms.py +16 -0
- klotho/tonos/systems/harmonic_trees/harmonic_tree.py +83 -0
- klotho/tonos/systems/harmonic_trees/spectrum.py +207 -0
- klotho/tonos/utils/__init__.py +17 -0
- klotho/tonos/utils/frequency_conversion.py +175 -0
- klotho/tonos/utils/harmonics.py +48 -0
- klotho/tonos/utils/interval_normalization.py +179 -0
- klotho/tonos/utils/intervals.py +251 -0
- klotho/topos/__init__.py +55 -0
- klotho/topos/collections/__init__.py +7 -0
- klotho/topos/collections/patterns.py +226 -0
- klotho/topos/collections/sequences.py +114 -0
- klotho/topos/collections/sets.py +397 -0
- klotho/topos/formal_grammars/__init__.py +5 -0
- klotho/topos/formal_grammars/alphabets.py +582 -0
- klotho/topos/formal_grammars/grammars.py +92 -0
- klotho/topos/graphs/__init__.py +8 -0
- klotho/topos/graphs/fields/__init__.py +1 -0
- klotho/topos/graphs/fields/algorithms/__init__.py +1 -0
- klotho/topos/graphs/fields/algorithms/field_algs.py +63 -0
- klotho/topos/graphs/fields/fields.py +140 -0
- klotho/topos/graphs/fields/functions/__init__.py +1 -0
- klotho/topos/graphs/fields/functions/field_funcs.py +89 -0
- klotho/topos/graphs/graphs.py +244 -0
- klotho/topos/graphs/networks/__init__.py +1 -0
- klotho/topos/graphs/networks/algorithms/__init__.py +1 -0
- klotho/topos/graphs/networks/algorithms/network_algs.py +49 -0
- klotho/topos/graphs/networks/networks.py +75 -0
- klotho/topos/graphs/trees/__init__.py +2 -0
- klotho/topos/graphs/trees/algorithms.py +171 -0
- klotho/topos/graphs/trees/trees.py +379 -0
- klotho/topos/random/__init__.py +4 -0
- klotho/topos/random/rando.py +48 -0
- klotho/types.py +83 -0
- klotho/utils/__init__.py +0 -0
- klotho/utils/algorithms/__init__.py +6 -0
- klotho/utils/algorithms/costs.py +78 -0
- klotho/utils/algorithms/cps_algorithms.py +37 -0
- klotho/utils/algorithms/factors.py +42 -0
- klotho/utils/data_structures/__init__.py +7 -0
- klotho/utils/data_structures/dictionaries.py +5 -0
- klotho/utils/data_structures/enums.py +32 -0
- klotho/utils/data_structures/graphs.py +136 -0
- klotho/utils/data_structures/group.py +29 -0
- klotho/utils/playback/__init__.py +1 -0
- klotho/utils/playback/player.py +378 -0
- klotho/utils/playback/scheduler copy.py +175 -0
- klotho/utils/playback/scheduler.py +281 -0
- klotho_cac-2.1.0.dist-info/METADATA +123 -0
- klotho_cac-2.1.0.dist-info/RECORD +101 -0
- klotho_cac-2.1.0.dist-info/WHEEL +5 -0
- klotho_cac-2.1.0.dist-info/licenses/LICENSE +15 -0
- klotho_cac-2.1.0.dist-info/top_level.txt +1 -0
klotho/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Klotho: A graph-oriented Python package for computational composition.
|
|
3
|
+
|
|
4
|
+
This package provides tools for working with musical structures across multiple domains:
|
|
5
|
+
- Topos: Abstract structures and relationships
|
|
6
|
+
- Chronos: Time and rhythm
|
|
7
|
+
- Tonos: Pitch and harmony
|
|
8
|
+
- Aikous: Expression and parameters
|
|
9
|
+
- Skora: Visualization and notation
|
|
10
|
+
"""
|
|
11
|
+
from . import topos
|
|
12
|
+
from . import chronos
|
|
13
|
+
from . import tonos
|
|
14
|
+
from . import aikous
|
|
15
|
+
from . import skora
|
|
16
|
+
from . import utils
|
|
17
|
+
|
|
18
|
+
from .topos.collections import patterns, sequences, sets, Pattern, CombinationSet, PartitionSet
|
|
19
|
+
from .topos.graphs import trees, networks, fields, Tree, Network, Field, Graph
|
|
20
|
+
|
|
21
|
+
from .chronos import RhythmPair, RhythmTree, TemporalUnit, TemporalUnitSequence, TemporalBlock
|
|
22
|
+
|
|
23
|
+
from .tonos import Pitch, Scale, Chord, AddressedScale, AddressedChord
|
|
24
|
+
|
|
25
|
+
from .types import frequency, cent, midicent, midi, amplitude, decibel, onset, duration
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
'topos', 'chronos', 'tonos', 'aikous', 'skora',
|
|
29
|
+
'Pitch', 'Scale', 'Chord',
|
|
30
|
+
'AddressedScale', 'AddressedChord',
|
|
31
|
+
'frequency', 'cent', 'midicent', 'midi',
|
|
32
|
+
'amplitude', 'decibel', 'onset', 'duration'
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
__version__ = '2.1.0'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aikous: A specialized module for working with expression and parameters in music.
|
|
3
|
+
|
|
4
|
+
From the Greek "ακούω" (akoúō) meaning "to hear" or "to listen," this module
|
|
5
|
+
deals with the expressive aspects of music that affect how we perceive sound.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from . import expression
|
|
9
|
+
from . import parameters
|
|
10
|
+
from . import instruments
|
|
11
|
+
|
|
12
|
+
# Import classes
|
|
13
|
+
from .expression import Dynamic, DynamicRange
|
|
14
|
+
from .parameters import ParameterTree
|
|
15
|
+
from .instruments import Instrument
|
|
16
|
+
|
|
17
|
+
# Import expression utility functions
|
|
18
|
+
from .expression import dbamp, ampdb, freq_amp_scale
|
|
19
|
+
from .expression import line, arch, map_curve
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
# Modules
|
|
23
|
+
'expression',
|
|
24
|
+
'parameters',
|
|
25
|
+
'instruments',
|
|
26
|
+
|
|
27
|
+
# Classes
|
|
28
|
+
'DynamicRange',
|
|
29
|
+
'Dynamic',
|
|
30
|
+
'ParameterTree',
|
|
31
|
+
'Instrument',
|
|
32
|
+
|
|
33
|
+
# Expression functions
|
|
34
|
+
'dbamp',
|
|
35
|
+
'ampdb',
|
|
36
|
+
'freq_amp_scale',
|
|
37
|
+
'line',
|
|
38
|
+
'arch',
|
|
39
|
+
'map_curve',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
__version__ = '2.0.0'
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'''
|
|
2
|
+
--------------------------------------------------------------------------------------
|
|
3
|
+
General psychoacoustic tools for working with music synthesis.
|
|
4
|
+
|
|
5
|
+
`Aikous` is a specialized module for working with psychoacoustics in the context of music.
|
|
6
|
+
|
|
7
|
+
see: https://en.wikipedia.org/wiki/Psychoacoustics
|
|
8
|
+
|
|
9
|
+
The word "aikous" is a portmanteau derived from Ancient Greek, blending the elements of
|
|
10
|
+
"αἰσθάνομαι" (aisthanomai), meaning "to perceive" or "to feel," and "ἀκούω" (akouo),
|
|
11
|
+
meaning "to hear."
|
|
12
|
+
|
|
13
|
+
The `aikous` module contains tools for translating physics phenomena, as perceived by
|
|
14
|
+
humans, into algebraic musical representations.
|
|
15
|
+
--------------------------------------------------------------------------------------
|
|
16
|
+
'''
|
|
17
|
+
from .dynamics import Dynamic, DynamicRange, dbamp, ampdb, freq_amp_scale
|
|
18
|
+
from .enevelopes import line, arch, map_curve
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
'Dynamic',
|
|
22
|
+
'DynamicRange',
|
|
23
|
+
'dbamp',
|
|
24
|
+
'ampdb',
|
|
25
|
+
'freq_amp_scale',
|
|
26
|
+
'line',
|
|
27
|
+
'arch',
|
|
28
|
+
'map_curve'
|
|
29
|
+
]
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# ------------------------------------------------------------------------------------
|
|
2
|
+
# Klotho/klotho/aikous/dynamics.py
|
|
3
|
+
# ------------------------------------------------------------------------------------
|
|
4
|
+
'''
|
|
5
|
+
--------------------------------------------------------------------------------------
|
|
6
|
+
Classes and functions for working with musical dynamics.
|
|
7
|
+
--------------------------------------------------------------------------------------
|
|
8
|
+
'''
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from numpy.polynomial import Polynomial
|
|
12
|
+
from scipy import interpolate
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'DynamicRange',
|
|
16
|
+
'ampdb',
|
|
17
|
+
'dbamp',
|
|
18
|
+
# 'amp_freq_scale',
|
|
19
|
+
'freq_amp_scale',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
DYNAMIC_MARKINGS = ('ppp', 'pp', 'p', 'mp', 'mf', 'f', 'ff', 'fff')
|
|
23
|
+
|
|
24
|
+
class Dynamic:
|
|
25
|
+
def __init__(self, db_value):
|
|
26
|
+
self._db_value = db_value
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def db(self):
|
|
30
|
+
return self._db_value
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def amp(self):
|
|
34
|
+
return dbamp(self._db_value)
|
|
35
|
+
|
|
36
|
+
def __float__(self):
|
|
37
|
+
return float(self._db_value)
|
|
38
|
+
|
|
39
|
+
def __repr__(self):
|
|
40
|
+
return f"Dynamic(db={self._db_value:.2f}, amp={self.amp:.4f})"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DynamicRange:
|
|
44
|
+
'''
|
|
45
|
+
Musical dynamics mapped to decibels.
|
|
46
|
+
|
|
47
|
+
Note: the decibel level for the loudest
|
|
48
|
+
dynamic (ffff) is 0 dB as this translates
|
|
49
|
+
to an amplitude of 1.0.
|
|
50
|
+
|
|
51
|
+
----------------|---------|----------------
|
|
52
|
+
Name | Letters | Level
|
|
53
|
+
----------------|---------|----------------
|
|
54
|
+
fortississimo | fff | very very loud
|
|
55
|
+
fortissimo | ff | very loud
|
|
56
|
+
forte | f | loud
|
|
57
|
+
mezzo-forte | mf | moderately loud
|
|
58
|
+
mezzo-piano | mp | moderately quiet
|
|
59
|
+
piano | p | quiet
|
|
60
|
+
pianissimo | pp | very quiet
|
|
61
|
+
pianississimo | ppp | very very quiet
|
|
62
|
+
----------------|---------|----------------
|
|
63
|
+
|
|
64
|
+
see https://en.wikipedia.org/wiki/Dynamics_(music)#
|
|
65
|
+
'''
|
|
66
|
+
def __init__(self, min_dynamic=-60, max_dynamic=0, curve=0, dynamics=DYNAMIC_MARKINGS):
|
|
67
|
+
self._min_dynamic = min_dynamic if isinstance(min_dynamic, Dynamic) else Dynamic(min_dynamic)
|
|
68
|
+
self._max_dynamic = max_dynamic if isinstance(max_dynamic, Dynamic) else Dynamic(max_dynamic)
|
|
69
|
+
self._curve = curve
|
|
70
|
+
self._dynamics = dynamics
|
|
71
|
+
self._range = self._calculate_range()
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def min_dynamic(self):
|
|
75
|
+
return self._min_dynamic
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def max_dynamic(self):
|
|
79
|
+
return self._max_dynamic
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def curve(self):
|
|
83
|
+
return self._curve
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def ranges(self):
|
|
87
|
+
return self._range
|
|
88
|
+
|
|
89
|
+
def _calculate_range(self):
|
|
90
|
+
min_db = float(self._min_dynamic.db)
|
|
91
|
+
max_db = float(self._max_dynamic.db)
|
|
92
|
+
num_dynamics = len(self._dynamics)
|
|
93
|
+
|
|
94
|
+
result = {}
|
|
95
|
+
for i, dyn in enumerate(self._dynamics):
|
|
96
|
+
normalized_pos = i / (num_dynamics - 1)
|
|
97
|
+
|
|
98
|
+
if self._curve == 0:
|
|
99
|
+
curved_pos = normalized_pos
|
|
100
|
+
elif self._curve > 0:
|
|
101
|
+
curved_pos = normalized_pos ** (1 + self._curve)
|
|
102
|
+
else:
|
|
103
|
+
curved_pos = 1 - ((1 - normalized_pos) ** (1 - self._curve))
|
|
104
|
+
|
|
105
|
+
db_value = min_db + curved_pos * (max_db - min_db)
|
|
106
|
+
result[dyn] = Dynamic(db_value)
|
|
107
|
+
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
def __getitem__(self, dynamic):
|
|
111
|
+
return self._range[dynamic]
|
|
112
|
+
|
|
113
|
+
def ampdb(amp: float) -> float:
|
|
114
|
+
'''
|
|
115
|
+
Convert amplitude to decibels (dB).
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
amp (float): The amplitude to convert.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
float: The amplitude in decibels.
|
|
122
|
+
'''
|
|
123
|
+
return 20 * np.log10(amp)
|
|
124
|
+
|
|
125
|
+
def dbamp(db: float) -> float:
|
|
126
|
+
'''
|
|
127
|
+
Convert decibels (dB) to amplitude.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
db (float): The decibels to convert.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
float: The amplitude.
|
|
134
|
+
'''
|
|
135
|
+
return 10 ** (db / 20)
|
|
136
|
+
|
|
137
|
+
# def amp_freq_scale(freq: float,
|
|
138
|
+
# freqs: list = [20, 100, 250, 500, 1000, 2000, 3000, 4000, 6000, 10000, 20000],
|
|
139
|
+
# amps: list = [0.3, 0.5, 0.7, 0.8, 0.5, 0.6, 0.5, 0.6, 0.7, 0.5, 0.3],
|
|
140
|
+
# deg: int = 4) -> float:
|
|
141
|
+
# frequencies_sample = np.array(freqs, dtype=float)
|
|
142
|
+
# loudness_sample = np.array(amps, dtype=float)
|
|
143
|
+
# p = Polynomial.fit(frequencies_sample, loudness_sample, deg=deg)
|
|
144
|
+
# return p(freq)
|
|
145
|
+
|
|
146
|
+
def freq_amp_scale(freq: float, db_level: float, min_db: float = -60) -> float:
|
|
147
|
+
"""
|
|
148
|
+
Scale amplitude based on frequency and loudness according to psychoacoustic principles.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
freq (float): The frequency in Hz
|
|
152
|
+
db_level (float): The input level in dB
|
|
153
|
+
min_db (float): The minimum dB level in the dynamic range (default -60)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
float: The perceptually scaled amplitude (linear scale)
|
|
157
|
+
"""
|
|
158
|
+
range_db = abs(min_db)
|
|
159
|
+
phon_level = 40 + ((db_level - min_db) / range_db) * 60
|
|
160
|
+
|
|
161
|
+
frequencies = np.array([20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000], dtype=float)
|
|
162
|
+
|
|
163
|
+
if phon_level <= 40:
|
|
164
|
+
scaling_curve = np.array([0.2, 0.3, 0.5, 0.7, 0.9, 1.0, 1.0, 0.9, 0.7, 0.4], dtype=float)
|
|
165
|
+
elif phon_level <= 70:
|
|
166
|
+
scaling_curve = np.array([0.3, 0.45, 0.6, 0.8, 0.95, 1.0, 1.0, 0.95, 0.8, 0.5], dtype=float)
|
|
167
|
+
else:
|
|
168
|
+
scaling_curve = np.array([0.5, 0.6, 0.7, 0.85, 0.95, 1.0, 1.0, 0.95, 0.85, 0.6], dtype=float)
|
|
169
|
+
|
|
170
|
+
spline = interpolate.CubicSpline(frequencies, scaling_curve, extrapolate=True)
|
|
171
|
+
scaling_factor = max(0.01, float(spline(freq)))
|
|
172
|
+
|
|
173
|
+
raw_amp = dbamp(db_level)
|
|
174
|
+
return raw_amp * scaling_factor
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# --------------------------------------------------
|
|
2
|
+
# Klotho/klotho/aikous/envelopes.py
|
|
3
|
+
# --------------------------------------------------
|
|
4
|
+
'''
|
|
5
|
+
--------------------------------------------------------------------------------------
|
|
6
|
+
Envelopes for shaping the dynamics of a sequence of discrete values.
|
|
7
|
+
--------------------------------------------------------------------------------------
|
|
8
|
+
'''
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'line',
|
|
14
|
+
'arch',
|
|
15
|
+
'map_curve',
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
def line(start=0.0, end=1.0, steps=100, curve=0.0):
|
|
19
|
+
'''
|
|
20
|
+
Generate a curved line from start to end value over n steps.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
start: Starting value
|
|
24
|
+
end: Ending value
|
|
25
|
+
steps: Number of steps
|
|
26
|
+
curve: Shape of the curve. Negative for exponential, positive for logarithmic, 0 for linear
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
numpy.ndarray: Array of values following the specified curve
|
|
30
|
+
'''
|
|
31
|
+
if curve == 0:
|
|
32
|
+
return np.linspace(start, end, steps)
|
|
33
|
+
|
|
34
|
+
t = np.linspace(0, 1, steps)
|
|
35
|
+
curved_t = np.exp(curve * t) - 1
|
|
36
|
+
curved_t = curved_t / (np.exp(curve) - 1)
|
|
37
|
+
|
|
38
|
+
return start + (end - start) * curved_t
|
|
39
|
+
|
|
40
|
+
def arch(base=0.0, peak=1.0, steps=100, curve=0.0, axis=0):
|
|
41
|
+
'''
|
|
42
|
+
Generate a swelling curve that rises and falls, starting and ending at base value, peaking at peak value.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
base: Starting and ending value
|
|
46
|
+
peak: Peak value
|
|
47
|
+
steps: Number of steps
|
|
48
|
+
curve: Shape of the curve. Can be:
|
|
49
|
+
- A single number: Same curve applied to both sides (negative for exponential, positive for logarithmic)
|
|
50
|
+
- A tuple/list of two values: First value for ascending curve, second for descending
|
|
51
|
+
axis: Position of the peak (-1 to 1). 0 centers the peak, negative shifts earlier, positive shifts later
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
numpy.ndarray: Array of values following a swell curve
|
|
55
|
+
'''
|
|
56
|
+
axis = np.clip(axis, -1, 1)
|
|
57
|
+
split_point = int((0.5 + axis * 0.4) * steps)
|
|
58
|
+
|
|
59
|
+
if isinstance(curve, (list, tuple)) and len(curve) == 2:
|
|
60
|
+
up_curve, down_curve = curve
|
|
61
|
+
else:
|
|
62
|
+
up_curve = down_curve = curve
|
|
63
|
+
|
|
64
|
+
up = line(base, peak, split_point + 1, up_curve)
|
|
65
|
+
down = line(peak, base, steps - split_point, down_curve)
|
|
66
|
+
|
|
67
|
+
return np.concatenate([up[:-1], down])
|
|
68
|
+
|
|
69
|
+
def map_curve(value, in_range, out_range, curve=0.0):
|
|
70
|
+
'''
|
|
71
|
+
Map a value from an input range to an output range with optional curve shaping.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
value: Input value to map
|
|
75
|
+
in_range: Tuple of (min, max) for input range
|
|
76
|
+
out_range: Tuple of (min, max) for output range
|
|
77
|
+
curve: Shape of the curve. Negative for exponential, positive for logarithmic, 0 for linear
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
float: Mapped value with curve applied
|
|
81
|
+
'''
|
|
82
|
+
normalized = np.interp(value, in_range, (0, 1))
|
|
83
|
+
|
|
84
|
+
if curve != 0:
|
|
85
|
+
normalized = np.exp(curve * normalized) - 1
|
|
86
|
+
normalized = normalized / (np.exp(curve) - 1)
|
|
87
|
+
|
|
88
|
+
return np.interp(normalized, (0, 1), out_range)
|
|
89
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .instrument import Instrument
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from klotho.aikous.expression.dynamics import Dynamic, DynamicRange
|
|
3
|
+
from klotho.tonos.pitch import Pitch
|
|
4
|
+
from klotho.utils.data_structures.dictionaries import SafeDict
|
|
5
|
+
from typing import List, Dict, TypeVar, Union
|
|
6
|
+
|
|
7
|
+
class Instrument(ABC):
|
|
8
|
+
|
|
9
|
+
def __init__(self, name, freq_range=None, dynamic_range=None, pfields=None):
|
|
10
|
+
"""
|
|
11
|
+
Initialize an Instrument.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
name (str): The name of the instrument
|
|
15
|
+
freq_range (tuple): A tuple of (min, max) frequency values or Pitch instances
|
|
16
|
+
dynamic_range: A DynamicRange instance or a tuple of (min, max) dB values
|
|
17
|
+
pfields (dict or SafeDict): Parameter fields with default values
|
|
18
|
+
"""
|
|
19
|
+
self._name = name
|
|
20
|
+
|
|
21
|
+
if freq_range is None:
|
|
22
|
+
freq_range = (20.0, 20000.0)
|
|
23
|
+
self._freq_range = self._process_freq_range(freq_range)
|
|
24
|
+
|
|
25
|
+
if dynamic_range is None:
|
|
26
|
+
dynamic_range = (-60, 0)
|
|
27
|
+
self._dynamic_range = self._process_dynamic_range(dynamic_range)
|
|
28
|
+
|
|
29
|
+
if pfields is None:
|
|
30
|
+
pfields = {}
|
|
31
|
+
self._pfields = pfields if isinstance(pfields, SafeDict) else SafeDict(pfields)
|
|
32
|
+
|
|
33
|
+
def _process_freq_range(self, freq_range):
|
|
34
|
+
min_freq, max_freq = freq_range
|
|
35
|
+
|
|
36
|
+
if not isinstance(min_freq, Pitch):
|
|
37
|
+
min_freq = Pitch.from_freq(float(min_freq))
|
|
38
|
+
|
|
39
|
+
if not isinstance(max_freq, Pitch):
|
|
40
|
+
max_freq = Pitch.from_freq(float(max_freq))
|
|
41
|
+
|
|
42
|
+
return (min_freq, max_freq)
|
|
43
|
+
|
|
44
|
+
def _process_dynamic_range(self, dynamic_range):
|
|
45
|
+
if isinstance(dynamic_range, DynamicRange):
|
|
46
|
+
return dynamic_range
|
|
47
|
+
|
|
48
|
+
min_dyn, max_dyn = dynamic_range
|
|
49
|
+
|
|
50
|
+
if isinstance(min_dyn, Dynamic):
|
|
51
|
+
min_dyn = min_dyn.db
|
|
52
|
+
|
|
53
|
+
if isinstance(max_dyn, Dynamic):
|
|
54
|
+
max_dyn = max_dyn.db
|
|
55
|
+
|
|
56
|
+
return DynamicRange(min_dynamic=min_dyn, max_dynamic=max_dyn)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def name(self):
|
|
60
|
+
return self._name
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def freq_range(self):
|
|
64
|
+
return self._freq_range
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def dynamic_range(self):
|
|
68
|
+
return self._dynamic_range
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def pfields(self):
|
|
72
|
+
return self._pfields
|
|
73
|
+
|
|
74
|
+
def __repr__(self):
|
|
75
|
+
# return f"Instrument(name='{self._name}', freq_range={self._freq_range}, dynamic_range={self._dynamic_range}, pfields={dict(self._pfields)})"
|
|
76
|
+
return f"Instrument(name='{self._name}', pfields={dict(self._pfields)})"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .parameter_tree import *
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from klotho.utils.data_structures.dictionaries import SafeDict
|
|
3
|
+
|
|
4
|
+
class PFIELDS(Enum):
|
|
5
|
+
def __call__(self):
|
|
6
|
+
# return SafeDict(self.value.copy())
|
|
7
|
+
return self.value.copy()
|
|
8
|
+
|
|
9
|
+
SineEnv = SafeDict({
|
|
10
|
+
'start' : 0,
|
|
11
|
+
'dur' : 1,
|
|
12
|
+
'synthName' : 'SineEnv',
|
|
13
|
+
'amplitude' : 0.45,
|
|
14
|
+
'frequency' : 440,
|
|
15
|
+
'attackTime' : 0.01,
|
|
16
|
+
'releaseTime': 0.1,
|
|
17
|
+
'pan' : 0.0,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
Vib = SafeDict({
|
|
21
|
+
'start' : 0,
|
|
22
|
+
'dur' : 1,
|
|
23
|
+
'synthName' : 'Vib',
|
|
24
|
+
'amplitude' : 0.45,
|
|
25
|
+
'frequency' : 440,
|
|
26
|
+
'attackTime' : 0.01,
|
|
27
|
+
'releaseTime': 0.1,
|
|
28
|
+
'sustain' : 0.5,
|
|
29
|
+
'curve' : 4.0,
|
|
30
|
+
'pan' : 0.0,
|
|
31
|
+
'table' : 0,
|
|
32
|
+
'vibRate1' : 3.5,
|
|
33
|
+
'vibRate2' : 5.8,
|
|
34
|
+
'vibRise' : 0.5,
|
|
35
|
+
'vibDepth' : 0.005,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
FMWT = SafeDict({
|
|
39
|
+
'start' : 0,
|
|
40
|
+
'dur' : 1,
|
|
41
|
+
'synthName' : 'FMWT',
|
|
42
|
+
'frequency' : 440,
|
|
43
|
+
'amplitude' : 0.45,
|
|
44
|
+
'attackTime' : 0.01,
|
|
45
|
+
'releaseTime' : 0.1,
|
|
46
|
+
'sustain' : 0.5,
|
|
47
|
+
'idx1' : 0.01,
|
|
48
|
+
'idx2' : 7,
|
|
49
|
+
'idx3' : 5,
|
|
50
|
+
'carMul' : 1,
|
|
51
|
+
'modMul' : 1.0007,
|
|
52
|
+
'vibRate1' : 0.01,
|
|
53
|
+
'vibRate2' : 0.5,
|
|
54
|
+
'vibRise' : 0,
|
|
55
|
+
'vibDepth' : 0,
|
|
56
|
+
'pan' : 0.0,
|
|
57
|
+
'table' : 0,
|
|
58
|
+
'reverberation': 0.0,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
OscTrm = SafeDict({
|
|
62
|
+
'start' : 0,
|
|
63
|
+
'dur' : 1,
|
|
64
|
+
'synthName' : 'OscTrm',
|
|
65
|
+
'amplitude' : 0.45,
|
|
66
|
+
'frequency' : 440,
|
|
67
|
+
'attackTime' : 0.01,
|
|
68
|
+
'releaseTime': 0.1,
|
|
69
|
+
'sustain' : 0.5,
|
|
70
|
+
'curve' : 4.0,
|
|
71
|
+
'pan' : 0.0,
|
|
72
|
+
'table' : 0,
|
|
73
|
+
'trm1' : 3.5,
|
|
74
|
+
'trm2' : 5.8,
|
|
75
|
+
'trmRise' : 0.5,
|
|
76
|
+
'trmDepth' : 0.1,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
OscAM = SafeDict({
|
|
80
|
+
'start' : 0,
|
|
81
|
+
'dur' : 1,
|
|
82
|
+
'synthName' : 'OscAM',
|
|
83
|
+
'amplitude' : 0.45,
|
|
84
|
+
'frequency' : 440,
|
|
85
|
+
'attackTime' : 0.01,
|
|
86
|
+
'releaseTime' : 0.1,
|
|
87
|
+
'sustain' : 0.5,
|
|
88
|
+
'pan' : 0.0,
|
|
89
|
+
'amFunc' : 0.0,
|
|
90
|
+
'am1' : 0.75,
|
|
91
|
+
'am2' : 0.75,
|
|
92
|
+
'amRise' : 0.75,
|
|
93
|
+
'amRatio' : 0.75,
|
|
94
|
+
'reverberation': 0.0,
|
|
95
|
+
'visualMode' : 0.0,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
AddSyn = SafeDict({
|
|
99
|
+
'start' : 0,
|
|
100
|
+
'dur' : 1,
|
|
101
|
+
'synthName' : 'AddSyn',
|
|
102
|
+
'amp' : 0.45,
|
|
103
|
+
'frequency' : 440,
|
|
104
|
+
'ampStri' : 0.5,
|
|
105
|
+
'attackStri' : 0.1,
|
|
106
|
+
'releaseStri' : 0.1,
|
|
107
|
+
'sustainStri' : 0.8,
|
|
108
|
+
'ampLow' : 0.5,
|
|
109
|
+
'attackLow' : 0.001,
|
|
110
|
+
'releaseLow' : 0.1,
|
|
111
|
+
'sustainLow' : 0.8,
|
|
112
|
+
'ampUp' : 0.6,
|
|
113
|
+
'attackUp' : 0.01,
|
|
114
|
+
'releaseUp' : 0.075,
|
|
115
|
+
'sustainUp' : 0.9,
|
|
116
|
+
'freqStri1' : 1.0,
|
|
117
|
+
'freqStri2' : 2.001,
|
|
118
|
+
'freqStri3' : 3.0,
|
|
119
|
+
'freqLow1' : 4.009,
|
|
120
|
+
'freqLow2' : 5.002,
|
|
121
|
+
'freqUp1' : 6.0,
|
|
122
|
+
'freqUp2' : 7.0,
|
|
123
|
+
'freqUp3' : 8.0,
|
|
124
|
+
'freqUp4' : 9.0,
|
|
125
|
+
'pan' : 0.0,
|
|
126
|
+
'reverberation': 0.0,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
PluckedString = SafeDict({
|
|
130
|
+
'start' : 0,
|
|
131
|
+
'dur' : 1,
|
|
132
|
+
'synthName' : 'PluckedString',
|
|
133
|
+
'amplitude' : 0.45,
|
|
134
|
+
'frequency' : 440,
|
|
135
|
+
'attackTime' : 0.01,
|
|
136
|
+
'releaseTime': 0.1,
|
|
137
|
+
'sustain' : 0.5,
|
|
138
|
+
'Pan1' : 0.0,
|
|
139
|
+
'Pan2' : 0.0,
|
|
140
|
+
'PanRise' : 0.0,
|
|
141
|
+
})
|
|
142
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from ...topos.graphs.trees import Tree
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
class ParameterTree(Tree):
|
|
5
|
+
def __init__(self, root, children:tuple):
|
|
6
|
+
super().__init__(root, children)
|
|
7
|
+
self._meta['pfields'] = pd.Series([set()], index=[''])
|
|
8
|
+
|
|
9
|
+
def __getitem__(self, node):
|
|
10
|
+
return ParameterNode(self, node)
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def pfields(self):
|
|
14
|
+
return sorted(self._meta.loc['', 'pfields'])
|
|
15
|
+
|
|
16
|
+
def set(self, node, **kwargs):
|
|
17
|
+
self._meta.loc['', 'pfields'].update(kwargs.keys())
|
|
18
|
+
|
|
19
|
+
for key, value in kwargs.items():
|
|
20
|
+
self.graph.nodes[node][key] = value
|
|
21
|
+
|
|
22
|
+
for descendant in self.descendants(node):
|
|
23
|
+
for key, value in kwargs.items():
|
|
24
|
+
self.graph.nodes[descendant][key] = value
|
|
25
|
+
|
|
26
|
+
def get(self, node, key):
|
|
27
|
+
if key in self.graph.nodes[node]:
|
|
28
|
+
return self.graph.nodes[node][key]
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
def clear(self, node=None):
|
|
32
|
+
if node is None:
|
|
33
|
+
for n in self.graph.nodes:
|
|
34
|
+
self.graph.nodes[n].clear()
|
|
35
|
+
else:
|
|
36
|
+
self.graph.nodes[node].clear()
|
|
37
|
+
for descendant in self.descendants(node):
|
|
38
|
+
self.graph.nodes[descendant].clear()
|
|
39
|
+
|
|
40
|
+
def items(self, node):
|
|
41
|
+
return dict(self.graph.nodes[node])
|
|
42
|
+
|
|
43
|
+
class ParameterNode:
|
|
44
|
+
def __init__(self, tree, node):
|
|
45
|
+
self._tree = tree
|
|
46
|
+
self._node = node
|
|
47
|
+
|
|
48
|
+
def __getitem__(self, key):
|
|
49
|
+
if isinstance(key, str):
|
|
50
|
+
return self._tree.get(self._node, key)
|
|
51
|
+
raise TypeError("Key must be a string")
|
|
52
|
+
|
|
53
|
+
def __setitem__(self, key, value):
|
|
54
|
+
self._tree.set(self._node, **{key: value})
|
|
55
|
+
|
|
56
|
+
def clear(self):
|
|
57
|
+
self._tree.clear(self._node)
|
|
58
|
+
|
|
59
|
+
def items(self):
|
|
60
|
+
return self._tree.items(self._node)
|
|
61
|
+
|
|
62
|
+
def __dict__(self):
|
|
63
|
+
return self._tree.items(self._node)
|
|
64
|
+
|
|
65
|
+
def __str__(self):
|
|
66
|
+
return str(self._tree.items(self._node))
|
|
67
|
+
|
|
68
|
+
def __repr__(self):
|
|
69
|
+
return repr(self._tree.items(self._node))
|