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