maialib 1.5.0__cp311-cp311-musllinux_1_2_x86_64.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.

Potentially problematic release.


This version of maialib might be problematic. Click here for more details.

@@ -0,0 +1,331 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import plotly
4
+ import plotly.express as px
5
+ import plotly.graph_objects as go
6
+ from maialib import maiacore as mc
7
+ from typing import List, Tuple, Callable, Optional
8
+
9
+ __all__ = ["plotSetharesDissonanceCurve", "plotScoreSetharesDissonance", "plotChordDyadsSetharesDissonanceHeatmap"]
10
+
11
+ def _dissmeasure(fvec: List[float], amp: List[float], model: str = 'min') -> float:
12
+ """
13
+ Given a list of partials in fvec, with amplitudes in amp, this routine
14
+ calculates the dissonance by summing the roughness of every sine pair
15
+ based on a model of Plomp-Levelt's roughness curve.
16
+
17
+ The older model (model='product') was based on the product of the two
18
+ amplitudes, but the newer model (model='min') is based on the minimum
19
+ of the two amplitudes, since this matches the beat frequency amplitude.
20
+ """
21
+ # Sort by frequency
22
+ sort_idx = np.argsort(fvec)
23
+ am_sorted = np.asarray(amp)[sort_idx]
24
+ fr_sorted = np.asarray(fvec)[sort_idx]
25
+
26
+ # Sum amplitudes for unique frequencies
27
+ freq_amp_dict = {}
28
+ for f, a in zip(fr_sorted, am_sorted):
29
+ freq_amp_dict[f] = freq_amp_dict.get(f, 0) + a
30
+
31
+ # Extract updated frequencies and amplitudes from the dictionary
32
+ fr_sorted = np.array(list(freq_amp_dict.keys()))
33
+ am_sorted = np.array(list(freq_amp_dict.values()))
34
+
35
+ # Used to stretch dissonance curve for different freqs:
36
+ Dstar = 0.24 # Point of maximum dissonance
37
+ S1 = 0.0207
38
+ S2 = 18.96
39
+
40
+ C1 = 5
41
+ C2 = -5
42
+
43
+ # Plomp-Levelt roughness curve:
44
+ A1 = -3.51
45
+ A2 = -5.75
46
+
47
+ # Generate all combinations of frequency components
48
+ idx = np.transpose(np.triu_indices_from(np.eye(len(fr_sorted)), k=0))
49
+ fr_pairs = fr_sorted[idx]
50
+ am_pairs = am_sorted[idx]
51
+
52
+ Fmin = fr_pairs[:, 0]
53
+ S = Dstar / (S1 * Fmin + S2)
54
+ Fdif = fr_pairs[:, 1] - fr_pairs[:, 0]
55
+
56
+ if model == 'min':
57
+ a = np.amin(am_pairs, axis=1)
58
+ elif model == 'product':
59
+ a = np.prod(am_pairs, axis=1) # Older model
60
+ else:
61
+ raise ValueError('model should be "min" or "product"')
62
+ SFdif = S * Fdif
63
+ D = np.sum(a * (C1 * np.exp(A1 * SFdif) + C2 * np.exp(A2 * SFdif)))
64
+
65
+ return D, fr_pairs, am_pairs
66
+
67
+ def plotSetharesDissonanceCurve(fundamentalFreq: float = 440, numPartials: int = 6, ratioLowLimit: float = 1.0, ratioHighLimit: float = 2.3, ratioStepIncrement: float = 0.001, amplCallback: Optional[Callable[[List[float]], List[float]]] = None) -> Tuple[go.Figure, pd.DataFrame]:
68
+ """
69
+ Compute the Sethares Dissonance Curve of a given base frequency
70
+
71
+ Return
72
+ A tuple that contains a Plotly figure, and the pair 'ratios' and 'dissonance' lists
73
+ """
74
+ freqs = fundamentalFreq * np.array(list(range(1, numPartials+1)))
75
+ amps = 0.88**np.array(list(range(0, numPartials))
76
+ ) if amplCallback == None else amplCallback(freqs)
77
+
78
+ if len(freqs) != len(amps):
79
+ raise ValueError(
80
+ "The size of amplVec must be equal to the 'numPartials' (6 is the default)")
81
+
82
+ # Calculate the number of points based on ratioStepIncrement
83
+ numPoints = int((ratioHighLimit - ratioLowLimit) / ratioStepIncrement) + 1
84
+
85
+ ratios = np.linspace(ratioLowLimit, ratioHighLimit, numPoints)
86
+ dissonances = []
87
+ fr_pairsVec = []
88
+ amp_pairsVec = []
89
+ for r in ratios:
90
+ extended_freqs = np.concatenate([freqs, r * freqs])
91
+ extended_amps = np.concatenate([amps, amps])
92
+ d, fr_pairs, amp_pairs = _dissmeasure(extended_freqs, extended_amps)
93
+ dissonances.append(d)
94
+ fr_pairsVec.append(fr_pairs)
95
+ amp_pairsVec.append(amp_pairs)
96
+
97
+ # Plotting using Plotly
98
+ fig = go.Figure()
99
+ fig.add_trace(go.Scatter(x=ratios, y=dissonances,
100
+ mode='lines', name='Dissonance'))
101
+
102
+ # Adding lines for the notable intervals
103
+ intervals = [(1, 1), (6, 5), (5, 4), (4, 3), (3, 2), (5, 3),
104
+ (2, 1), (4, 1), (8, 1), (16, 1), (32, 1)]
105
+
106
+ # Filter intervals based on ratioHighLimit
107
+ filtered_intervals = [(n, d)
108
+ for n, d in intervals if n/d <= ratioHighLimit]
109
+
110
+ for n, d in filtered_intervals:
111
+ fig.add_shape(
112
+ type="line",
113
+ x0=n/d,
114
+ y0=min(dissonances),
115
+ x1=n/d,
116
+ y1=max(dissonances),
117
+ line=dict(color="Silver", width=1)
118
+ )
119
+
120
+ partialsTitle = "partial" if numPartials == 1 else "partials"
121
+ fig.update_layout(
122
+ title=f"<b>Sethares' Sensory Dissonance Curve</b><br><i>f<sub>0</sub>={fundamentalFreq}Hz | {numPartials} harmonic {partialsTitle}</i>",
123
+ title_x=0.5,
124
+ xaxis_title="Frequency Ratio",
125
+ yaxis_title="Sensory Dissonance",
126
+ xaxis_type="log",
127
+ xaxis=dict(
128
+ tickvals=[n/d for n, d in filtered_intervals],
129
+ ticktext=['{}/{}'.format(n, d) for n, d in filtered_intervals]
130
+ ),
131
+ yaxis=dict(showticklabels=True)
132
+ )
133
+
134
+ fig.add_shape(
135
+ # Rectangle with reference to the plot
136
+ type="rect",
137
+ xref="paper",
138
+ yref="paper",
139
+ x0=0,
140
+ y0=0,
141
+ x1=1.0,
142
+ y1=1.0,
143
+ line=dict(
144
+ color="black",
145
+ width=1,
146
+ )
147
+ )
148
+
149
+ df = pd.DataFrame(data=list(zip(ratios, dissonances, fr_pairsVec,
150
+ amp_pairsVec)), columns=['ratio', 'dissonance', 'freqs', 'amps'])
151
+
152
+ return fig, df
153
+
154
+
155
+ def _setharesDissonanceDataFrameInterpolation(df: pd.DataFrame, interpolatePoints: int) -> pd.DataFrame:
156
+ def split(a, n):
157
+ k, m = divmod(len(a), n)
158
+ return (a[i*k+min(i, m):(i+1)*k+min(i+1, m)] for i in range(n))
159
+
160
+ firstMeasureNumber = df.measure.min(skipna=True)
161
+ lastMeasureNumber = df.measure.max(skipna=True)
162
+
163
+ if interpolatePoints >= lastMeasureNumber:
164
+ raise Exception(
165
+ "ERROR: The score number of measures must be greater then the interpolate points value")
166
+
167
+ ranges = list(
168
+ split(range(firstMeasureNumber, lastMeasureNumber+1), interpolatePoints))
169
+ data = []
170
+ for sub in ranges:
171
+ sub_df = df.query(f'(measure >= {sub.start}) & (measure < {sub.stop})')
172
+ floatMeasure = (sub.start + sub.stop) / 2
173
+ dissonance = round(sub_df["dissonance"].mean(skipna=True))
174
+ chordSizeMean = round(sub_df["chordSize"].mean(skipna=True))
175
+
176
+ obj = {
177
+ "floatMeasure": floatMeasure,
178
+ "dissonance": dissonance,
179
+ "chordSizeMean": chordSizeMean,
180
+ }
181
+
182
+ data.append(obj)
183
+
184
+ new_df = pd.DataFrame.from_records(data)
185
+ return new_df
186
+
187
+
188
+ def plotScoreSetharesDissonance(score: mc.Score, plotType='line', lineShape='linear', numPartialsPerNote: int = 6, useMinModel: bool = True,
189
+ amplCallback: Optional[Callable[[
190
+ List[float]], List[float]]] = None,
191
+ dissCallback: Optional[Callable[[List[float]], float]] = None, **kwargs) -> Tuple[go.Figure, pd.DataFrame]:
192
+ """Plot 2D line graph of the Sethares Dissonance over time
193
+
194
+ Args:
195
+ score (maialib.Score): A maialib Score object loaded with a valid MusicXML file
196
+ plotType (str): Can be 'line' or 'scatter'
197
+ lineShape (str): Can be 'linear' or 'spline'
198
+ numPartialsPerNote (int): Amount of spectral partials for each note
199
+ useMinModel (bool): Sethares dissonance values can be computed using the 'minimal amplitude' model
200
+ or the 'product amplitudes' model. The 'min' model is a more recent approach
201
+ amplCallback: Custom user function callback to generate the amplitude of each spectrum partial
202
+ dissCallback: Custom user function callback to receive all paired partial dissonances and computes
203
+ a single total dissonance value output
204
+ Kwargs:
205
+ measureStart (int): Start measure to plot
206
+ measureEnd (int): End measure to plot
207
+ numPoints (int): Number of interpolated points
208
+
209
+ Returns:
210
+ A list: [Plotly Figure, The plot data as a Pandas Dataframe]
211
+
212
+ Raises:
213
+ RuntimeError, KeyError
214
+
215
+ Examples of use:
216
+
217
+ >>> myScore = ml.Score("/path/to/score.xml")
218
+ >>> ml.plotScoreSetharesDissonance(myScore)
219
+ >>> ml.plotScoreSetharesDissonance(myScore, numPoints=15)
220
+ >>> ml.plotScoreSetharesDissonance(myScore, measureStart=10, measureEnd=20)
221
+ """
222
+ # ===== GET THE PLOT TITLE ===== #
223
+ workTitle = score.getTitle()
224
+ if workTitle.strip() == "":
225
+ workTitle = "No Title"
226
+ plotTitle = f'<b>Sethares Sensory Dissonance</b><br><i>{workTitle}</i>'
227
+
228
+ # ===== COMPUTE THE SETHARES DISSONANCE ===== #
229
+ df = score.getChordsDataFrame(kwargs)
230
+
231
+ df["dissonance"] = df.apply(lambda row: row.chord.getSetharesDissonance(
232
+ numPartialsPerNote, useMinModel, amplCallback, dissCallback), axis=1)
233
+ df["chordNotes"] = df.apply(lambda row: ', '.join(
234
+ [str(x.getPitch()) for x in row.chord.getNotes()]), axis=1)
235
+ df["chordSize"] = df.apply(lambda row: row.chord.size(), axis=1)
236
+ dissonanceMean = df.dissonance.mean()
237
+
238
+ # ===== CHECK THE INTERPOLATION POINTS ===== #
239
+ plotHoverData = ["chordNotes", "chordSize"]
240
+ if "numPoints" in kwargs:
241
+ df = _setharesDissonanceDataFrameInterpolation(df, kwargs["numPoints"])
242
+ plotHoverData = ["chordSizeMean"]
243
+
244
+ # ===== PLOT ===== #
245
+ fig = None
246
+ if plotType == 'line':
247
+ fig = px.line(df, x="floatMeasure", y="dissonance",
248
+ hover_data=plotHoverData, title=plotTitle, line_shape=lineShape)
249
+ else:
250
+ fig = px.scatter(df, x="floatMeasure", y="dissonance",
251
+ hover_data=plotHoverData, title=plotTitle)
252
+
253
+ fig.add_hline(y=dissonanceMean, line_width=3, line_dash="dash",
254
+ line_color="green", annotation_text="Mean")
255
+
256
+ fig.update_layout(
257
+ title_x=0.5,
258
+ xaxis_title="Measures",
259
+ yaxis_title="Sensory Dissonance",
260
+ )
261
+
262
+ fig.add_shape(
263
+ # Rectangle with reference to the plot
264
+ type="rect",
265
+ xref="paper",
266
+ yref="paper",
267
+ x0=0,
268
+ y0=0,
269
+ x1=1.0,
270
+ y1=1.0,
271
+ line=dict(
272
+ color="black",
273
+ width=1,
274
+ )
275
+ )
276
+
277
+ return fig, df
278
+
279
+ def plotChordDyadsSetharesDissonanceHeatmap(chord: mc.Chord, numPartialsPerNote: int = 6, useMinModel: bool = True, amplCallback: Optional[Callable[[
280
+ List[float]], List[float]]] = None, dissonanceThreshold: float = 0.1, dissonanceDecimalPoint: int = 2) -> Tuple[plotly.graph_objs._figure.Figure, pd.DataFrame]:
281
+ """Plot chord dyads Sethares dissonance heatmap
282
+
283
+ Args:
284
+ chord (maialib.Chord): A maialib Chord
285
+
286
+ Kwargs:
287
+ numPartialsPerNote (int): Amount of spectral partials for each note
288
+ useMinModel (bool): Sethares dissonance values can be computed using the 'minimal amplitude' model
289
+ or the 'product amplitudes' model. The 'min' model is a more recent approach
290
+ amplCallback: Custom user function callback to generate the amplitude of each spectrum partial
291
+ dissonanceThreshold (float): Dissonance threshold to skip small dissonance values
292
+ dissonanceDecimalPoint (int): Round chord dissonance value in the plot title
293
+
294
+ Returns:
295
+ A list: [Plotly Figure, The plot data as a Pandas Dataframe]
296
+
297
+ Raises:
298
+ RuntimeError, KeyError
299
+
300
+ Examples of use:
301
+
302
+ >>> import maialib as ml
303
+ >>> myChord = ml.Chord(["C3", "E3", "G3"])
304
+ >>> fig, df = plotChordDyadsSetharesDissonanceHeatmap(myChord)
305
+ >>> fig.show()
306
+ """
307
+ df = chord.getSetharesDyadsDataFrame(numPartialsPerNote=numPartialsPerNote, useMinModel=useMinModel, amplCallback=amplCallback)
308
+ dfFiltered = df[df.dissonance > dissonanceThreshold]
309
+
310
+ # Pivot the dataframe to create a matrix
311
+ matrix_df = dfFiltered.pivot(index='baseFreq', columns='targetFreq', values='dissonance')
312
+
313
+ # Create a heatmap using Plotly
314
+ fig = px.imshow(matrix_df,
315
+ labels=dict(x="Target Frequency (Hz)", y="Base Frequency (Hz)", color="Dissonance"), color_continuous_scale='Inferno')
316
+
317
+ # Extract unique frequencies for x and y ticks
318
+ x_ticks = sorted(matrix_df.columns.unique())
319
+ y_ticks = sorted(matrix_df.index.unique())
320
+
321
+ roundedXTicksValues = [round(num, 0) for num in x_ticks]
322
+ roundedYTicksValues = [round(num, 0) for num in y_ticks]
323
+
324
+ # Update x and y ticks to only show unique frequencies and set log scale
325
+ fig.update_xaxes(type='log', tickvals=x_ticks, ticktext=roundedXTicksValues)
326
+ fig.update_yaxes(type='log', tickvals=y_ticks, ticktext=roundedYTicksValues)
327
+
328
+ roundedDissonanceValue = round(df.dissonance.sum(), dissonanceDecimalPoint)
329
+ fig.update_layout(title=f'<b>Chord Dyads Sethares Dissonance Heatmap</b><br><i>Chord Dissonance={str(roundedDissonanceValue)}</i>', title_x=0.5)
330
+
331
+ return fig, dfFiltered
@@ -0,0 +1,11 @@
1
+ import pandas as pd
2
+ import plotly
3
+ import plotly.graph_objects as go
4
+ from maialib import maiacore as mc
5
+ from typing import Callable
6
+
7
+ __all__ = ['plotSetharesDissonanceCurve', 'plotScoreSetharesDissonance', 'plotChordDyadsSetharesDissonanceHeatmap']
8
+
9
+ def plotSetharesDissonanceCurve(fundamentalFreq: float = 440, numPartials: int = 6, ratioLowLimit: float = 1.0, ratioHighLimit: float = 2.3, ratioStepIncrement: float = 0.001, amplCallback: Callable[[list[float]], list[float]] | None = None) -> tuple[go.Figure, pd.DataFrame]: ...
10
+ def plotScoreSetharesDissonance(score: mc.Score, plotType: str = 'line', lineShape: str = 'linear', numPartialsPerNote: int = 6, useMinModel: bool = True, amplCallback: Callable[[list[float]], list[float]] | None = None, dissCallback: Callable[[list[float]], float] | None = None, **kwargs) -> tuple[go.Figure, pd.DataFrame]: ...
11
+ def plotChordDyadsSetharesDissonanceHeatmap(chord: mc.Chord, numPartialsPerNote: int = 6, useMinModel: bool = True, amplCallback: Callable[[list[float]], list[float]] | None = None, dissonanceThreshold: float = 0.1, dissonanceDecimalPoint: int = 2) -> tuple[plotly.graph_objs._figure.Figure, pd.DataFrame]: ...
maialib/setup.py ADDED
@@ -0,0 +1,48 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ with open("README.md", "r", encoding="utf-8") as fh:
4
+ long_description = fh.read()
5
+
6
+ with open("LICENSE.txt", "r", encoding="utf-8") as fh:
7
+ license_txt = fh.read()
8
+
9
+ setup(
10
+ name="maialib",
11
+ version="1.5.0-dev",
12
+ author="Nycholas Maia",
13
+ author_email="nyckmaia@gmail.com",
14
+ description="A C++/Python library to manipulate music data",
15
+ long_description=long_description,
16
+ long_description_content_type="text/markdown",
17
+ license="GNU General Public License v3 or later (GPLv3+)",
18
+ url="https://github.com/nyckmaia/maialib",
19
+ project_urls={
20
+ "Bug Tracker": "https://github.com/nyckmaia/maialib/issues",
21
+ },
22
+ keywords=["music", "score", "sheet music", "analysis"],
23
+ packages=find_packages(),
24
+ package_data={"": ['*.so', '*.pyd', '__init__.pyi', 'maiacore/__init__.pyi',
25
+ "py.typed", "*.pyi", "**/*.pyi",
26
+ "xml-scores-examples/Bach_Cello_Suite_1.mxl",
27
+ "xml-scores-examples/Beethoven_Symphony_5_mov_1.xml",
28
+ "xml-scores-examples/Chopin_Fantasie_Impromptu.mxl",
29
+ "xml-scores-examples/Dvorak_Symphony_9_mov_4.mxl",
30
+ "xml-scores-examples/Mahler_Symphony_8_Finale.mxl",
31
+ "xml-scores-examples/Mozart_Requiem_Introitus.mxl",
32
+ "xml-scores-examples/Strauss_Also_Sprach_Zarathustra.mxl"]},
33
+
34
+ include_package_data=True,
35
+ py_modules=["maiacore", "maiapy"],
36
+ classifiers=[
37
+ "Development Status :: 3 - Alpha",
38
+ "Programming Language :: Python :: 3",
39
+ "Programming Language :: C++",
40
+ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
41
+ "Operating System :: OS Independent",
42
+ "Intended Audience :: Science/Research",
43
+ "Natural Language :: English",
44
+ "Topic :: Software Development :: Libraries"
45
+ ],
46
+ python_requires=">=3.8.0",
47
+ zip_safe=False,
48
+ )