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.
@@ -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