maialib 1.10.2__cp311-cp311-win_amd64.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.
- maialib/__init__.py +4 -0
- maialib/maiacore/Release/__init__.pyi +3 -0
- maialib/maiacore/Release/maiacore.cp311-win_amd64.pyd +0 -0
- maialib/maiacore/Release/maiacore.pyi +1414 -0
- maialib/maiacore/__init__.py +4 -0
- maialib/maiacore/__init__.pyi +24 -0
- maialib/maiapy/__init__.py +3 -0
- maialib/maiapy/__init__.pyi +3 -0
- maialib/maiapy/other.py +148 -0
- maialib/maiapy/other.pyi +79 -0
- maialib/maiapy/plots.py +806 -0
- maialib/maiapy/plots.pyi +103 -0
- maialib/maiapy/sethares_dissonance.py +481 -0
- maialib/maiapy/sethares_dissonance.pyi +128 -0
- maialib/setup.py +61 -0
- maialib/xml-scores-examples/Bach_Cello_Suite_1.mxl +0 -0
- maialib/xml-scores-examples/Beethoven_Symphony_5_mov_1.xml +170525 -0
- maialib/xml-scores-examples/Chopin_Fantasie_Impromptu.mxl +0 -0
- maialib/xml-scores-examples/Dvorak_Symphony_9_mov_4.mxl +0 -0
- maialib/xml-scores-examples/Mahler_Symphony_8_Finale.mxl +0 -0
- maialib/xml-scores-examples/Mozart_Requiem_Introitus.mxl +0 -0
- maialib/xml-scores-examples/Strauss_Also_Sprach_Zarathustra.mxl +0 -0
- maialib-1.10.2.dist-info/METADATA +822 -0
- maialib-1.10.2.dist-info/RECORD +27 -0
- maialib-1.10.2.dist-info/WHEEL +5 -0
- maialib-1.10.2.dist-info/licenses/LICENSE.txt +674 -0
- maialib-1.10.2.dist-info/top_level.txt +2 -0
maialib/maiapy/plots.py
ADDED
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
import plotly.graph_objects as go
|
|
2
|
+
import maialib.maiacore as mc
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import plotly.express as px
|
|
5
|
+
import plotly
|
|
6
|
+
from typing import List, Tuple
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"plotPartsActivity",
|
|
10
|
+
"plotPianoRoll",
|
|
11
|
+
"plotScorePitchEnvelope",
|
|
12
|
+
"plotChordsNumberOfNotes",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _score2DataFrame(score: mc.Score, kwargs) -> Tuple[pd.DataFrame, str, str]:
|
|
17
|
+
"""Auxiliar function to convert a maialib Score object to a Pandas DataFrame
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
score (maialib.Score): A maialib Score object loaded with a valid MusicXML file
|
|
21
|
+
|
|
22
|
+
Kwargs:
|
|
23
|
+
measureStart (int): Start measure to plot
|
|
24
|
+
measureEnd (int): End measure to plot
|
|
25
|
+
partNames (list): A str list that contains the filtered desired score parts to plot
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Tuple: DataFrame, author, work_title
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
RuntimeError, KeyError
|
|
32
|
+
"""
|
|
33
|
+
# ===== INPUT VALIDATION ===== #
|
|
34
|
+
|
|
35
|
+
# 1) Validate keywords arguments keys
|
|
36
|
+
params = ["measureStart", "measureEnd", "partNames"]
|
|
37
|
+
for k in kwargs.keys():
|
|
38
|
+
if k not in params:
|
|
39
|
+
raise RuntimeError(
|
|
40
|
+
f"plotPartsActivity() got an unexpected keyword argument '{k}'.\nThe valid keywords are: {params}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# 2) Validate 'partNames'
|
|
44
|
+
# 2.1) Type cheking
|
|
45
|
+
if "partNames" in kwargs and not isinstance(kwargs["partNames"], list):
|
|
46
|
+
print(
|
|
47
|
+
"ERROR: 'partNames' is a optional kwargs argument and MUST BE a strings array")
|
|
48
|
+
print(score.getPartsNames())
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
# 2.2) Check each individual part name
|
|
52
|
+
partNames = []
|
|
53
|
+
if not "partNames" in kwargs:
|
|
54
|
+
partNames = score.getPartsNames()
|
|
55
|
+
else:
|
|
56
|
+
for partNameValue in kwargs["partNames"]:
|
|
57
|
+
idx = 0
|
|
58
|
+
isValid = score.getPartIndex(partNameValue, idx)
|
|
59
|
+
|
|
60
|
+
if not isValid:
|
|
61
|
+
print(f"ERROR: Invalid part name: {partNameValue}")
|
|
62
|
+
print(score.getPartsNames())
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
partNames.append(partNameValue)
|
|
66
|
+
|
|
67
|
+
# 3) Validate 'author' and 'work title'
|
|
68
|
+
author = score.getComposerName()
|
|
69
|
+
work_title = score.getTitle()
|
|
70
|
+
|
|
71
|
+
if str(author) == "":
|
|
72
|
+
author = "No Author"
|
|
73
|
+
|
|
74
|
+
if str(work_title) == "":
|
|
75
|
+
work_title = "No Title"
|
|
76
|
+
|
|
77
|
+
# 4) Validade 'measureStart' and 'measureEnd'
|
|
78
|
+
measureStart = 1
|
|
79
|
+
measureEnd = score.getNumMeasures() + 1
|
|
80
|
+
|
|
81
|
+
if "measureStart" in kwargs:
|
|
82
|
+
measureStart = kwargs["measureStart"]
|
|
83
|
+
if measureStart < 1:
|
|
84
|
+
print("ERROR: 'measureStart' must be greater than 1")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
if "measureEnd" in kwargs:
|
|
88
|
+
measureEnd = kwargs["measureEnd"]
|
|
89
|
+
if measureEnd > score.getNumMeasures():
|
|
90
|
+
print(
|
|
91
|
+
f"ERROR: 'measureEnd' must be lesser than than {score.getNumMeasures() + 1}'")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
if measureEnd < measureStart:
|
|
95
|
+
print("ERROR: 'measureEnd' must be greater than 'measureStart'")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
# ===== CREATE BASIC DATA STRUCTURE ===== #
|
|
99
|
+
plotData = {}
|
|
100
|
+
plotData["notesData"] = []
|
|
101
|
+
|
|
102
|
+
# ===== ITERATE ON EACH SCORE NOTE ===== #
|
|
103
|
+
for partName in partNames: # For each part 'p'
|
|
104
|
+
currentPart = score.getPart(partName)
|
|
105
|
+
# partName = currentPart.getName()
|
|
106
|
+
|
|
107
|
+
# Control variable: note time position on score
|
|
108
|
+
currentTimePosition = 0
|
|
109
|
+
|
|
110
|
+
for m in range(measureStart - 1, measureEnd - 1): # for each measure 'm'
|
|
111
|
+
currentMeasure = currentPart.getMeasure(m)
|
|
112
|
+
|
|
113
|
+
measureQuarterTimeAmount = 0
|
|
114
|
+
for n in range(currentMeasure.getNumNotes(0)): # for each note 'n'
|
|
115
|
+
note = currentMeasure.getNote(n, 0)
|
|
116
|
+
|
|
117
|
+
# Skip other voices
|
|
118
|
+
if note.getVoice() != 1 or note.inChord():
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
measureQuarterTimeAmount = measureQuarterTimeAmount + note.getQuarterDuration()
|
|
122
|
+
|
|
123
|
+
if m == measureStart - 1:
|
|
124
|
+
currentTimePosition = measureQuarterTimeAmount * measureStart
|
|
125
|
+
|
|
126
|
+
for s in range(currentMeasure.getNumStaves()): # for each stave 's'
|
|
127
|
+
staveNumElememts = currentMeasure.getNumNotes(s)
|
|
128
|
+
|
|
129
|
+
# Temp control variables
|
|
130
|
+
internalStaveCurrentTime = 0
|
|
131
|
+
currentVoice = 1
|
|
132
|
+
for n in range(staveNumElememts): # for each note 'n'
|
|
133
|
+
currentNote = currentMeasure.getNote(n, s)
|
|
134
|
+
|
|
135
|
+
voice = currentNote.getVoice()
|
|
136
|
+
|
|
137
|
+
# Reset time on voice change
|
|
138
|
+
if voice != currentVoice:
|
|
139
|
+
internalStaveCurrentTime = 0
|
|
140
|
+
currentVoice = voice
|
|
141
|
+
|
|
142
|
+
# Skip chord upper notes (unnecessary for this plot type)
|
|
143
|
+
if currentNote.inChord():
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
# Get note data
|
|
147
|
+
noteDuration = currentNote.getQuarterDuration()
|
|
148
|
+
midiValue = currentNote.getMidiNumber()
|
|
149
|
+
notePitch = currentNote.getPitch()
|
|
150
|
+
|
|
151
|
+
aux = currentTimePosition + internalStaveCurrentTime
|
|
152
|
+
noteStart = aux / measureQuarterTimeAmount
|
|
153
|
+
noteFinish = (aux + noteDuration) / \
|
|
154
|
+
measureQuarterTimeAmount
|
|
155
|
+
|
|
156
|
+
# This plotly timeline function requires the use of these 3 names below: 'Tasks', 'Start' and 'Finish'
|
|
157
|
+
noteData = {
|
|
158
|
+
"Task": partName,
|
|
159
|
+
"Start": noteStart,
|
|
160
|
+
"Finish": noteFinish,
|
|
161
|
+
"midiValue": midiValue,
|
|
162
|
+
"notePitch": notePitch,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Increment control time variable
|
|
166
|
+
internalStaveCurrentTime = internalStaveCurrentTime + noteDuration
|
|
167
|
+
|
|
168
|
+
# Skip rests
|
|
169
|
+
if currentNote.isNoteOff():
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
# Add 'noteData' object to the list
|
|
173
|
+
plotData["notesData"].append(noteData)
|
|
174
|
+
|
|
175
|
+
# Update the global current time position
|
|
176
|
+
currentTimePosition = currentTimePosition + measureQuarterTimeAmount
|
|
177
|
+
|
|
178
|
+
# ===== CREATE THE VISUAL PLOT ===== #
|
|
179
|
+
df = pd.DataFrame(plotData["notesData"])
|
|
180
|
+
df["delta"] = df["Finish"] - df["Start"]
|
|
181
|
+
|
|
182
|
+
return df, author, work_title
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# plotPartsActivity
|
|
186
|
+
#
|
|
187
|
+
# This function plots a timeline graph showing the musical activity of each score instrument
|
|
188
|
+
#
|
|
189
|
+
# Paper: An analysis of the organization and fragmentation of Farben by Arnold Schoenberg (2013)
|
|
190
|
+
# Author: Prof. Igor Leão Maia (UFMG)
|
|
191
|
+
# Contributor: Prof. Adolfo Maia Junior (UNICAMP)
|
|
192
|
+
# Code Implementation: PhD. Nycholas Maia (UNICAMP) - 01/02/2023
|
|
193
|
+
#
|
|
194
|
+
# To get more information about it:
|
|
195
|
+
# https://www.researchgate.net/publication/321335427_Uma_analise_da_organizacao_e_fragmentacao_de_Farben_de_Arnold_Schoenberg
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def plotPartsActivity(
|
|
199
|
+
score: mc.Score, **kwargs
|
|
200
|
+
) -> Tuple[plotly.graph_objs._figure.Figure, pd.DataFrame]:
|
|
201
|
+
"""Plots a timeline graph showing the musical activity of each score instrument
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
score (maialib.Score): A maialib Score object loaded with a valid MusicXML file
|
|
205
|
+
|
|
206
|
+
Kwargs:
|
|
207
|
+
measureStart (int): Start measure to plot
|
|
208
|
+
measureEnd (int): End measure to plot
|
|
209
|
+
partNames (list): A str list that contains the filtered desired score parts to plot
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
A list: [Plotly Figure, The plot data as a Pandas Dataframe]
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
RuntimeError, KeyError
|
|
216
|
+
|
|
217
|
+
Examples of use:
|
|
218
|
+
|
|
219
|
+
>>> plotPartsActivity(myScore)
|
|
220
|
+
>>> plotPartsActivity(myScore, measureStart=50)
|
|
221
|
+
>>> plotPartsActivity(myScore, measureStart=50, measureEnd=100)
|
|
222
|
+
>>> plotPartsActivity(myScore, measureStart=50, measureEnd=100, partNames=["Violin 1", "Cello"])
|
|
223
|
+
"""
|
|
224
|
+
# ===== CREATE A PLOTLY TIMELINE PLOT ===== #
|
|
225
|
+
df, author, work_title = _score2DataFrame(score, kwargs)
|
|
226
|
+
fig = px.timeline(
|
|
227
|
+
df,
|
|
228
|
+
template="plotly_white",
|
|
229
|
+
x_start="Start",
|
|
230
|
+
x_end="Finish",
|
|
231
|
+
y="Task",
|
|
232
|
+
color="midiValue",
|
|
233
|
+
hover_data=["notePitch"],
|
|
234
|
+
labels={
|
|
235
|
+
"Task": "Part",
|
|
236
|
+
"Start": "Start Measure",
|
|
237
|
+
"Finish": "Finish Measure",
|
|
238
|
+
"notePitch": "Pitch",
|
|
239
|
+
"midiValue": "MIDI Value",
|
|
240
|
+
},
|
|
241
|
+
color_continuous_scale=px.colors.sequential.Turbo_r,
|
|
242
|
+
title=f"<b>Parts Activity<br>{work_title} - {author}</b>",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
fig.data[0].x = df.delta.tolist()
|
|
246
|
+
|
|
247
|
+
# Update plot layout
|
|
248
|
+
if "measureStart" not in kwargs:
|
|
249
|
+
kwargs["measureStart"] = 1
|
|
250
|
+
if "measureEnd" not in kwargs:
|
|
251
|
+
kwargs["measureEnd"] = score.getNumMeasures() + 1
|
|
252
|
+
|
|
253
|
+
# Update plot layout
|
|
254
|
+
fig.update_xaxes(type="linear",
|
|
255
|
+
showgrid=True,
|
|
256
|
+
gridwidth=1,
|
|
257
|
+
title="Measures",
|
|
258
|
+
range=[kwargs["measureStart"], kwargs["measureEnd"]]
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
fig.update_yaxes(autorange="reversed", showgrid=True,
|
|
262
|
+
gridwidth=1, ticksuffix=" ")
|
|
263
|
+
fig.update_layout(
|
|
264
|
+
title_x=0.5,
|
|
265
|
+
yaxis_title=None,
|
|
266
|
+
font={
|
|
267
|
+
"size": 18,
|
|
268
|
+
},
|
|
269
|
+
coloraxis_colorbar=dict(
|
|
270
|
+
title="Pitch",
|
|
271
|
+
tickvals=[12, 24, 36, 48, 60, 72, 84, 96, 108, 120],
|
|
272
|
+
ticktext=["C0", "C1", "C2", "C3", "C4",
|
|
273
|
+
"C5", "C6", "C7", "C8", "C9"],
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
fig.add_shape(
|
|
278
|
+
# Rectangle with reference to the plot
|
|
279
|
+
type="rect",
|
|
280
|
+
xref="paper",
|
|
281
|
+
yref="paper",
|
|
282
|
+
x0=0,
|
|
283
|
+
y0=0,
|
|
284
|
+
x1=1.0,
|
|
285
|
+
y1=1.0,
|
|
286
|
+
line=dict(
|
|
287
|
+
color="black",
|
|
288
|
+
width=1,
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return fig, df
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def plotPianoRoll(
|
|
296
|
+
score: mc.Score, **kwargs
|
|
297
|
+
) -> Tuple[plotly.graph_objs._figure.Figure, pd.DataFrame]:
|
|
298
|
+
"""Plots a piano roll graph showing the musical activity of each score instrument
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
score (maialib.Score): A maialib Score object loaded with a valid MusicXML file
|
|
302
|
+
|
|
303
|
+
Kwargs:
|
|
304
|
+
measureStart (int): Start measure to plot
|
|
305
|
+
measureEnd (int): End measure to plot
|
|
306
|
+
partNames (list): A str list that contains the filtered desired score parts to plot
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
A list: [Plotly Figure, The plot data as a Pandas Dataframe]
|
|
310
|
+
|
|
311
|
+
Raises:
|
|
312
|
+
RuntimeError, KeyError
|
|
313
|
+
|
|
314
|
+
Examples of use:
|
|
315
|
+
|
|
316
|
+
>>> plotPianoRoll(myScore)
|
|
317
|
+
>>> plotPianoRoll(myScore, measureStart=50)
|
|
318
|
+
>>> plotPianoRoll(myScore, measureStart=50, measureEnd=100)
|
|
319
|
+
>>> plotPianoRoll(myScore, measureStart=50, measureEnd=100, partNames=["Violin 1", "Cello"])
|
|
320
|
+
"""
|
|
321
|
+
# ===== CREATE A PLOTLY TIMELINE PLOT ===== #
|
|
322
|
+
df, author, work_title = _score2DataFrame(score, kwargs)
|
|
323
|
+
|
|
324
|
+
fig = px.timeline(
|
|
325
|
+
df,
|
|
326
|
+
template="plotly_white",
|
|
327
|
+
x_start="Start",
|
|
328
|
+
x_end="Finish",
|
|
329
|
+
y="midiValue",
|
|
330
|
+
color="Task",
|
|
331
|
+
hover_data=["notePitch"],
|
|
332
|
+
labels={
|
|
333
|
+
"Task": "Part",
|
|
334
|
+
"Start": "Start Measure",
|
|
335
|
+
"Finish": "Finish Measure",
|
|
336
|
+
"notePitch": "Pitch",
|
|
337
|
+
"midiValue": "MIDI Value",
|
|
338
|
+
},
|
|
339
|
+
title=f"<b>Piano Roll<br>{work_title} - {author}</b>",
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
for d in fig.data:
|
|
343
|
+
d.x = df.delta.tolist()
|
|
344
|
+
|
|
345
|
+
if "measureStart" not in kwargs:
|
|
346
|
+
kwargs["measureStart"] = 1
|
|
347
|
+
if "measureEnd" not in kwargs:
|
|
348
|
+
kwargs["measureEnd"] = score.getNumMeasures() + 1
|
|
349
|
+
|
|
350
|
+
# Update plot layout
|
|
351
|
+
fig.update_xaxes(
|
|
352
|
+
type="linear",
|
|
353
|
+
range=[kwargs["measureStart"], kwargs["measureEnd"]],
|
|
354
|
+
showgrid=True,
|
|
355
|
+
gridwidth=1,
|
|
356
|
+
title="Measures",
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
fig.update_yaxes(
|
|
360
|
+
autorange=True,
|
|
361
|
+
showgrid=True,
|
|
362
|
+
gridwidth=1,
|
|
363
|
+
title="Pitch",
|
|
364
|
+
tickvals=[12, 24, 36, 48, 60, 72, 84, 96, 108, 120],
|
|
365
|
+
ticktext=["C0 ", "C1 ", "C2 ", "C3 ", "C4 ",
|
|
366
|
+
"C5 ", "C6 ", "C7 ", "C8 ", "C9 "],
|
|
367
|
+
)
|
|
368
|
+
fig.update_layout(title_x=0.5, font={"size": 16})
|
|
369
|
+
|
|
370
|
+
fig.add_shape(
|
|
371
|
+
# Rectangle with reference to the plot
|
|
372
|
+
type="rect",
|
|
373
|
+
xref="paper",
|
|
374
|
+
yref="paper",
|
|
375
|
+
x0=0,
|
|
376
|
+
y0=0,
|
|
377
|
+
x1=1.0,
|
|
378
|
+
y1=1.0,
|
|
379
|
+
line=dict(
|
|
380
|
+
color="black",
|
|
381
|
+
width=1,
|
|
382
|
+
),
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
fig.update_traces(width=0.8)
|
|
386
|
+
|
|
387
|
+
return fig, df
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _removeNoteOffLines(df: pd.DataFrame) -> pd.DataFrame:
|
|
391
|
+
df["low"] = df["low"].map(lambda x: None if x == 0 else x)
|
|
392
|
+
df["high"] = df["high"].map(lambda x: None if x == 0 else x)
|
|
393
|
+
df["mean"] = df["mean"].map(lambda x: None if x == 0 else x)
|
|
394
|
+
df["meanOfExtremes"] = df["meanOfExtremes"].map(
|
|
395
|
+
lambda x: None if x == 0 else x)
|
|
396
|
+
|
|
397
|
+
return df
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _scoreEnvelopeDataFrame(df: pd.DataFrame) -> pd.DataFrame:
|
|
401
|
+
data = []
|
|
402
|
+
for index, row in df.iterrows():
|
|
403
|
+
chord = row["chord"]
|
|
404
|
+
chordSize = chord.size()
|
|
405
|
+
|
|
406
|
+
if chordSize == 0:
|
|
407
|
+
obj = {
|
|
408
|
+
"floatMeasure": row["floatMeasure"],
|
|
409
|
+
"low": 0,
|
|
410
|
+
"meanOfExtremes": 0,
|
|
411
|
+
"mean": 0,
|
|
412
|
+
"high": 0,
|
|
413
|
+
"lowPitch": None,
|
|
414
|
+
"meanOfExtremesPitch": None,
|
|
415
|
+
"meanPitch": None,
|
|
416
|
+
"highPitch": None,
|
|
417
|
+
}
|
|
418
|
+
else:
|
|
419
|
+
obj = {
|
|
420
|
+
"floatMeasure": row["floatMeasure"],
|
|
421
|
+
"low": chord.getNote(0).getMidiNumber(),
|
|
422
|
+
"meanOfExtremes": chord.getMeanOfExtremesMidiValue(),
|
|
423
|
+
"mean": chord.getMeanMidiValue(),
|
|
424
|
+
"high": chord.getNote(chordSize - 1).getMidiNumber(),
|
|
425
|
+
"lowPitch": chord.getNote(0).getPitch(),
|
|
426
|
+
"meanOfExtremesPitch": chord.getMeanOfExtremesPitch(),
|
|
427
|
+
"meanPitch": chord.getMeanPitch(),
|
|
428
|
+
"highPitch": chord.getNote(chordSize - 1).getPitch(),
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
data.append(obj)
|
|
432
|
+
|
|
433
|
+
new_df = pd.DataFrame.from_records(data)
|
|
434
|
+
return new_df
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _envelopeDataFrameInterpolation(df: pd.DataFrame, interpolatePoints: int) -> pd.DataFrame:
|
|
438
|
+
def split(a, n):
|
|
439
|
+
k, m = divmod(len(a), n)
|
|
440
|
+
return (a[i * k + min(i, m): (i + 1) * k + min(i + 1, m)] for i in range(n))
|
|
441
|
+
|
|
442
|
+
totalMeasures = int(df.floatMeasure.max())
|
|
443
|
+
|
|
444
|
+
if interpolatePoints >= totalMeasures:
|
|
445
|
+
raise Exception(
|
|
446
|
+
"ERROR: The score number of measures must be greater then the interpolate points value"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
ranges = list(split(range(1, totalMeasures + 1), interpolatePoints))
|
|
450
|
+
|
|
451
|
+
data = []
|
|
452
|
+
for sub in ranges:
|
|
453
|
+
sub_df = df[(df.floatMeasure >= float(sub.start))
|
|
454
|
+
& (df.floatMeasure < float(sub.stop))]
|
|
455
|
+
floatMeasure = (sub.start + sub.stop) / 2
|
|
456
|
+
low = round(sub_df.low.mean())
|
|
457
|
+
meanOfExtremes = round(sub_df["meanOfExtremes"].mean())
|
|
458
|
+
mean = round(sub_df["mean"].mean())
|
|
459
|
+
high = round(sub_df.high.mean())
|
|
460
|
+
|
|
461
|
+
obj = {
|
|
462
|
+
"floatMeasure": floatMeasure,
|
|
463
|
+
"low": low,
|
|
464
|
+
"meanOfExtremes": meanOfExtremes,
|
|
465
|
+
"mean": mean,
|
|
466
|
+
"high": high,
|
|
467
|
+
"lowPitch": mc.Helper.midiNote2pitch(low),
|
|
468
|
+
"meanOfExtremesPitch": mc.Helper.midiNote2pitch(meanOfExtremes),
|
|
469
|
+
"meanPitch": mc.Helper.midiNote2pitch(mean),
|
|
470
|
+
"highPitch": mc.Helper.midiNote2pitch(high),
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
data.append(obj)
|
|
474
|
+
|
|
475
|
+
new_df = pd.DataFrame.from_records(data)
|
|
476
|
+
return new_df
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _chordNumNotesDataFrameInterpolation(df: pd.DataFrame, interpolatePoints: int) -> pd.DataFrame:
|
|
480
|
+
def split(a, n):
|
|
481
|
+
k, m = divmod(len(a), n)
|
|
482
|
+
return (a[i * k + min(i, m): (i + 1) * k + min(i + 1, m)] for i in range(n))
|
|
483
|
+
|
|
484
|
+
firstMeasureNumber = df.measure.min(skipna=True)
|
|
485
|
+
lastMeasureNumber = df.measure.max(skipna=True)
|
|
486
|
+
|
|
487
|
+
if interpolatePoints >= lastMeasureNumber:
|
|
488
|
+
raise Exception(
|
|
489
|
+
"ERROR: The score number of measures must be greater then the interpolate points value"
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
ranges = list(
|
|
493
|
+
split(range(firstMeasureNumber, lastMeasureNumber + 1), interpolatePoints))
|
|
494
|
+
data = []
|
|
495
|
+
for sub in ranges:
|
|
496
|
+
sub_df = df.query(f"(measure >= {sub.start}) & (measure < {sub.stop})")
|
|
497
|
+
floatMeasure = (sub.start + sub.stop) / 2
|
|
498
|
+
numNotes = round(sub_df["numNotes"].mean(skipna=True))
|
|
499
|
+
|
|
500
|
+
obj = {"floatMeasure": floatMeasure, "numNotes": numNotes}
|
|
501
|
+
|
|
502
|
+
data.append(obj)
|
|
503
|
+
|
|
504
|
+
new_df = pd.DataFrame.from_records(data)
|
|
505
|
+
return new_df
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def plotScorePitchEnvelope(
|
|
509
|
+
score: mc.Score, **kwargs
|
|
510
|
+
) -> Tuple[plotly.graph_objs._figure.Figure, pd.DataFrame]:
|
|
511
|
+
"""Plot a score pitch envelope
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
score (maialib.Score): A maialib Score object loaded with a valid MusicXML file
|
|
515
|
+
|
|
516
|
+
Kwargs:
|
|
517
|
+
numPoints: (int): Number of interpolated points
|
|
518
|
+
showHigher (bool): Plot the envelop upper limit
|
|
519
|
+
showLower (bool): Plot the envelop lower limit
|
|
520
|
+
showMean (bool): Plot the envelop mean curve
|
|
521
|
+
showMeanOfExtremes (bool): Plot the envelop mean of extremes curve
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
A list: [Plotly Figure, The plot data as a Pandas Dataframe]
|
|
525
|
+
|
|
526
|
+
Raises:
|
|
527
|
+
RuntimeError, KeyError
|
|
528
|
+
|
|
529
|
+
Examples of use:
|
|
530
|
+
|
|
531
|
+
>>> myScore = ml.Score("/path/to/score.xml")
|
|
532
|
+
>>> plotScorePitchEnvelope(myScore)
|
|
533
|
+
>>> plotScorePitchEnvelope(myScore, numPoints=10)
|
|
534
|
+
>>> plotScorePitchEnvelope(myScore, showLower=False)
|
|
535
|
+
>>> plotScorePitchEnvelope(myScore, showMean=False, showMean=True)
|
|
536
|
+
"""
|
|
537
|
+
# ===== GET BASIC INFO ===== #
|
|
538
|
+
df = score.getChordsDataFrame()
|
|
539
|
+
df = _scoreEnvelopeDataFrame(df)
|
|
540
|
+
df = _removeNoteOffLines(df)
|
|
541
|
+
|
|
542
|
+
if "numPoints" in kwargs:
|
|
543
|
+
df = _envelopeDataFrameInterpolation(df, kwargs["numPoints"])
|
|
544
|
+
|
|
545
|
+
workTitle = score.getTitle()
|
|
546
|
+
author = score.getComposerName()
|
|
547
|
+
|
|
548
|
+
if str(author) == "":
|
|
549
|
+
author = "No Author"
|
|
550
|
+
|
|
551
|
+
if str(workTitle) == "":
|
|
552
|
+
workTitle = "No Title"
|
|
553
|
+
|
|
554
|
+
# ===== PLOT DATA ===== #
|
|
555
|
+
fig = go.Figure()
|
|
556
|
+
|
|
557
|
+
# Get mouse houver plot data
|
|
558
|
+
customLow = list(df[["lowPitch"]].to_numpy())
|
|
559
|
+
customMeanOfExtremes = list(df[["meanOfExtremesPitch"]].to_numpy())
|
|
560
|
+
customMean = list(df[["meanPitch"]].to_numpy())
|
|
561
|
+
customHigh = list(df[["highPitch"]].to_numpy())
|
|
562
|
+
|
|
563
|
+
# ===== PLOT TRACES CONTROL ===== #
|
|
564
|
+
showHigher = True
|
|
565
|
+
if "showHigher" in kwargs:
|
|
566
|
+
showHigher = kwargs["showHigher"]
|
|
567
|
+
|
|
568
|
+
showLower = True
|
|
569
|
+
if "showLower" in kwargs:
|
|
570
|
+
showLower = kwargs["showLower"]
|
|
571
|
+
|
|
572
|
+
showMeanOfExtremes = True
|
|
573
|
+
if "showMeanOfExtremes" in kwargs:
|
|
574
|
+
showMeanOfExtremes = kwargs["showMeanOfExtremes"]
|
|
575
|
+
|
|
576
|
+
showMean = True
|
|
577
|
+
if "showMean" in kwargs:
|
|
578
|
+
showMean = kwargs["showMean"]
|
|
579
|
+
|
|
580
|
+
# Create plot traces
|
|
581
|
+
if showHigher:
|
|
582
|
+
fig.add_trace(
|
|
583
|
+
go.Scatter(
|
|
584
|
+
x=df.floatMeasure,
|
|
585
|
+
y=df.high,
|
|
586
|
+
name="higher pitch",
|
|
587
|
+
line_shape="spline",
|
|
588
|
+
customdata=customHigh,
|
|
589
|
+
hovertemplate="%{customdata[0]}",
|
|
590
|
+
line_color="blue",
|
|
591
|
+
)
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
if showMeanOfExtremes:
|
|
595
|
+
fig.add_trace(
|
|
596
|
+
go.Scatter(
|
|
597
|
+
x=df.floatMeasure,
|
|
598
|
+
y=df["meanOfExtremes"],
|
|
599
|
+
name="mean of extremes",
|
|
600
|
+
line_shape="spline",
|
|
601
|
+
customdata=customMeanOfExtremes,
|
|
602
|
+
hovertemplate="%{customdata[0]}",
|
|
603
|
+
line_color="black",
|
|
604
|
+
)
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
if showMean:
|
|
608
|
+
fig.add_trace(
|
|
609
|
+
go.Scatter(
|
|
610
|
+
x=df.floatMeasure,
|
|
611
|
+
y=df["mean"],
|
|
612
|
+
name="mean",
|
|
613
|
+
line_shape="spline",
|
|
614
|
+
customdata=customMean,
|
|
615
|
+
hovertemplate="%{customdata[0]}",
|
|
616
|
+
line_color="green",
|
|
617
|
+
)
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
if showLower:
|
|
621
|
+
fig.add_trace(
|
|
622
|
+
go.Scatter(
|
|
623
|
+
x=df.floatMeasure,
|
|
624
|
+
y=df.low,
|
|
625
|
+
name="lower pitch",
|
|
626
|
+
line_shape="spline",
|
|
627
|
+
customdata=customLow,
|
|
628
|
+
hovertemplate="%{customdata[0]}",
|
|
629
|
+
line_color="red",
|
|
630
|
+
)
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# ===== PLOT LAYOUT ===== #
|
|
634
|
+
fig.update_layout(
|
|
635
|
+
title=f"<b>Pitchs Envelope<br>{workTitle} - {author}</b>", title_x=0.5, font={"size": 18}
|
|
636
|
+
)
|
|
637
|
+
fig.update_xaxes(type="linear", autorange=True,
|
|
638
|
+
showgrid=True, gridwidth=1, title="Measures")
|
|
639
|
+
fig.update_yaxes(autorange=True, showgrid=True,
|
|
640
|
+
gridwidth=1, ticksuffix=" ")
|
|
641
|
+
fig.update_layout(
|
|
642
|
+
title_x=0.5,
|
|
643
|
+
yaxis_title=None,
|
|
644
|
+
font={
|
|
645
|
+
"size": 18,
|
|
646
|
+
},
|
|
647
|
+
yaxis=dict(
|
|
648
|
+
title="Pitch",
|
|
649
|
+
tickvals=[12, 24, 36, 48, 60, 72, 84, 96, 108, 120],
|
|
650
|
+
ticktext=["C0 ", "C1 ", "C2 ", "C3 ", "C4 ",
|
|
651
|
+
"C5 ", "C6 ", "C7 ", "C8 ", "C9 "],
|
|
652
|
+
),
|
|
653
|
+
)
|
|
654
|
+
fig.update_layout(hovermode="x unified",
|
|
655
|
+
template="plotly_white", yaxis_showticksuffix="all")
|
|
656
|
+
|
|
657
|
+
fig.update_layout(
|
|
658
|
+
legend=dict(
|
|
659
|
+
orientation="h", # Horizontal
|
|
660
|
+
yanchor="bottom", # Anchor the bottom of the legend
|
|
661
|
+
# Place the legend below the plot (adjust as needed)
|
|
662
|
+
y=-0.4,
|
|
663
|
+
xanchor="center", # Center horizontally
|
|
664
|
+
x=0.5, # In the middle of the plot
|
|
665
|
+
)
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
fig.add_shape(
|
|
669
|
+
# Rectangle with reference to the plot
|
|
670
|
+
type="rect",
|
|
671
|
+
xref="paper",
|
|
672
|
+
yref="paper",
|
|
673
|
+
x0=0,
|
|
674
|
+
y0=0,
|
|
675
|
+
x1=1.0,
|
|
676
|
+
y1=1.0,
|
|
677
|
+
line=dict(
|
|
678
|
+
color="black",
|
|
679
|
+
width=1,
|
|
680
|
+
),
|
|
681
|
+
)
|
|
682
|
+
return fig, df
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def plotChordsNumberOfNotes(
|
|
686
|
+
score: mc.Score, **kwargs
|
|
687
|
+
) -> Tuple[plotly.graph_objs._figure.Figure, pd.DataFrame]:
|
|
688
|
+
"""Plot chord number of notes varying in time
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
score (maialib.Score): A maialib Score object loaded with a valid MusicXML file
|
|
692
|
+
|
|
693
|
+
Kwargs:
|
|
694
|
+
measureStart (int): Start measure to plot
|
|
695
|
+
measureEnd (int): End measure to plot
|
|
696
|
+
numPoints (int): Number of interpolated points
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
A list: [Plotly Figure, The plot data as a Pandas Dataframe]
|
|
700
|
+
|
|
701
|
+
Raises:
|
|
702
|
+
RuntimeError, KeyError
|
|
703
|
+
|
|
704
|
+
Examples of use:
|
|
705
|
+
|
|
706
|
+
>>> myScore = ml.Score("/path/to/score.xml")
|
|
707
|
+
>>> plotChordsNumberOfNotes(myScore)
|
|
708
|
+
>>> plotChordsNumberOfNotes(myScore, numPoints=15)
|
|
709
|
+
>>> plotChordsNumberOfNotes(myScore, measureStart=10, measureEnd=20)
|
|
710
|
+
"""
|
|
711
|
+
# ===== INPUT VALIDATION ===== #
|
|
712
|
+
# Validade 'measureStart' and 'measureEnd'
|
|
713
|
+
measureStart = 1
|
|
714
|
+
measureEnd = score.getNumMeasures() + 1
|
|
715
|
+
|
|
716
|
+
if "measureStart" in kwargs:
|
|
717
|
+
measureStart = kwargs["measureStart"]
|
|
718
|
+
if measureStart < 1:
|
|
719
|
+
print("ERROR: 'measureStart' must be greater than 1")
|
|
720
|
+
return
|
|
721
|
+
|
|
722
|
+
if "measureEnd" in kwargs:
|
|
723
|
+
measureEnd = kwargs["measureEnd"]
|
|
724
|
+
if measureEnd > score.getNumMeasures():
|
|
725
|
+
print(
|
|
726
|
+
f"ERROR: 'measureEnd' must be lesser than than {score.getNumMeasures() + 1}'")
|
|
727
|
+
return
|
|
728
|
+
|
|
729
|
+
if measureEnd < measureStart:
|
|
730
|
+
print("ERROR: 'measureEnd' must be greater than 'measureStart'")
|
|
731
|
+
return
|
|
732
|
+
# ===== GET BASIC DATA ===== #
|
|
733
|
+
df = score.getChordsDataFrame()
|
|
734
|
+
df["numNotes"] = df.apply(lambda line: line.chord.size(), axis=1)
|
|
735
|
+
df = df.query(f"(measure >= {measureStart}) & (measure < {measureEnd})")
|
|
736
|
+
|
|
737
|
+
if "numPoints" in kwargs:
|
|
738
|
+
df = _chordNumNotesDataFrameInterpolation(df, kwargs["numPoints"])
|
|
739
|
+
|
|
740
|
+
df["numNotes"] = df["numNotes"].map(lambda x: None if x == 0 else x)
|
|
741
|
+
|
|
742
|
+
# ===== COMPUTE AUX DATA ===== #
|
|
743
|
+
minNumNotes = df["numNotes"].min()
|
|
744
|
+
maxNumNotes = df["numNotes"].max()
|
|
745
|
+
meanOfExtremesNumNotes = (minNumNotes + maxNumNotes) / 2
|
|
746
|
+
meanNumNotes = df["numNotes"].sum() / df.shape[0]
|
|
747
|
+
|
|
748
|
+
# ===== CREATE PLOT TRACES ===== #
|
|
749
|
+
fig = px.line(df, x="floatMeasure", y="numNotes",
|
|
750
|
+
title="Chords number of notes")
|
|
751
|
+
fig.add_hline(
|
|
752
|
+
y=meanOfExtremesNumNotes,
|
|
753
|
+
line_width=1,
|
|
754
|
+
line_dash="dash",
|
|
755
|
+
line_color="green",
|
|
756
|
+
annotation_text="Mean of Extremes",
|
|
757
|
+
annotation_position="bottom right",
|
|
758
|
+
annotation_font_size=14,
|
|
759
|
+
annotation_font_color="green",
|
|
760
|
+
)
|
|
761
|
+
fig.add_hline(
|
|
762
|
+
y=meanNumNotes,
|
|
763
|
+
line_width=2,
|
|
764
|
+
line_dash="solid",
|
|
765
|
+
line_color="black",
|
|
766
|
+
annotation_text="Mean",
|
|
767
|
+
annotation_position="bottom left",
|
|
768
|
+
annotation_font_size=14,
|
|
769
|
+
annotation_font_color="black",
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
# ===== PLOT LAYOUT ===== #
|
|
773
|
+
fig.update_xaxes(type="linear", autorange=True,
|
|
774
|
+
showgrid=True, gridwidth=1, title="Measures")
|
|
775
|
+
fig.update_yaxes(
|
|
776
|
+
autorange=True,
|
|
777
|
+
showgrid=True,
|
|
778
|
+
gridwidth=1,
|
|
779
|
+
ticksuffix=" ",
|
|
780
|
+
title="Number of Notes",
|
|
781
|
+
gridcolor="lightgray",
|
|
782
|
+
)
|
|
783
|
+
fig.update_layout(
|
|
784
|
+
title_x=0.5,
|
|
785
|
+
font={
|
|
786
|
+
"size": 14,
|
|
787
|
+
},
|
|
788
|
+
plot_bgcolor="white",
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
fig.add_shape(
|
|
792
|
+
# Rectangle with reference to the plot
|
|
793
|
+
type="rect",
|
|
794
|
+
xref="paper",
|
|
795
|
+
yref="paper",
|
|
796
|
+
x0=0,
|
|
797
|
+
y0=0,
|
|
798
|
+
x1=1.0,
|
|
799
|
+
y1=1.0,
|
|
800
|
+
line=dict(
|
|
801
|
+
color="black",
|
|
802
|
+
width=1,
|
|
803
|
+
),
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
return fig, df
|