timetoalign 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tta/parsing.py ADDED
@@ -0,0 +1,2236 @@
1
+ import json
2
+ import warnings
3
+ from collections import defaultdict
4
+ from itertools import zip_longest
5
+ from typing import Collection, Dict, Iterable, Optional, Tuple, Type, overload
6
+
7
+ import mido
8
+ import numpy as np
9
+ import pandas as pd
10
+ import partitura.performance as ptp
11
+ import partitura.score as pts
12
+ from partitura.utils import get_time_units_from_note_array, iter_subclasses
13
+ from partitura.utils.globals import TIME_UNITS
14
+ from partitura.utils.music import rec_collapse_rests, seconds_to_midi_ticks
15
+
16
+ from processing.metaframes import DF, Meta, pd_concat
17
+ from processing.tta.utils import make_argument_iterable
18
+
19
+ # region MIDI
20
+
21
+ CC_PURPOSE = {
22
+ 0: "Bank Select (MSB)",
23
+ 1: "Modulation Wheel (MSB)",
24
+ 2: "Breath Controller (MSB)",
25
+ 3: "Undefined (MSB)",
26
+ 4: "Foot Controller (MSB)",
27
+ 5: "Portamento Time (MSB)",
28
+ 6: "Data Entry (MSB)",
29
+ 7: "Channel Volume (MSB)",
30
+ 8: "Balance (MSB)",
31
+ 9: "Undefined (MSB)",
32
+ 10: "Pan (MSB)",
33
+ 11: "Expression Controller (MSB)",
34
+ 12: "Effect Control 1 (MSB)",
35
+ 13: "Effect Control 2 (MSB)",
36
+ 14: "Undefined (MSB)",
37
+ 15: "Undefined (MSB)",
38
+ 16: "General Purpose Controller 1 (MSB)",
39
+ 17: "General Purpose Controller 2 (MSB)",
40
+ 18: "General Purpose Controller 3 (MSB)",
41
+ 19: "General Purpose Controller 4 (MSB)",
42
+ 20: "Undefined (MSB)",
43
+ 21: "Undefined (MSB)",
44
+ 22: "Undefined (MSB)",
45
+ 23: "Undefined (MSB)",
46
+ 24: "Undefined (MSB)",
47
+ 25: "Undefined (MSB)",
48
+ 26: "Undefined (MSB)",
49
+ 27: "Undefined (MSB)",
50
+ 28: "Undefined (MSB)",
51
+ 29: "Undefined (MSB)",
52
+ 30: "Undefined (MSB)",
53
+ 31: "Undefined (MSB)",
54
+ 32: "Bank Select (LSB)",
55
+ 33: "Modulation Wheel (LSB)",
56
+ 34: "Breath Controller (LSB)",
57
+ 35: "Undefined (LSB)",
58
+ 36: "Foot Controller (LSB)",
59
+ 37: "Portamento Time (LSB)",
60
+ 38: "Data Entry (LSB)",
61
+ 39: "Channel Volume (LSB)",
62
+ 40: "Balance (LSB)",
63
+ 41: "Undefined (LSB)",
64
+ 42: "Pan (LSB)",
65
+ 43: "Expression Controller (LSB)",
66
+ 44: "Effect Control 1 (LSB)",
67
+ 45: "Effect Control 2 (LSB)",
68
+ 46: "Undefined (LSB)",
69
+ 47: "Undefined (LSB)",
70
+ 48: "General Purpose Controller 1 (LSB)",
71
+ 49: "General Purpose Controller 2 (LSB)",
72
+ 50: "General Purpose Controller 3 (LSB)",
73
+ 51: "General Purpose Controller 4 (LSB)",
74
+ 52: "Undefined (LSB)",
75
+ 53: "Undefined (LSB)",
76
+ 54: "Undefined (LSB)",
77
+ 55: "Undefined (LSB)",
78
+ 56: "Undefined (LSB)",
79
+ 57: "Undefined (LSB)",
80
+ 58: "Undefined (LSB)",
81
+ 59: "Undefined (LSB)",
82
+ 60: "Undefined (LSB)",
83
+ 61: "Undefined (LSB)",
84
+ 62: "Undefined (LSB)",
85
+ 63: "Undefined (LSB)",
86
+ 64: "Sustain Pedal",
87
+ 65: "Portamento",
88
+ 66: "Sostenuto Pedal",
89
+ 67: "Soft Pedal",
90
+ 68: "Legato Footswitch",
91
+ 69: "Hold 2 Pedal",
92
+ 70: "Sound Variation",
93
+ 71: "Resonance (Timbre)",
94
+ 72: "Release Time",
95
+ 73: "Attack Time",
96
+ 74: "Brightness (Filter Cutoff)",
97
+ 75: "Sound Controller 6", # Default: Decay Time (some implementations)
98
+ 76: "Sound Controller 7", # Default: Vibrato Rate (some implementations)
99
+ 77: "Sound Controller 8", # Default: Vibrato Depth (some implementations)
100
+ 78: "Sound Controller 9", # Default: Vibrato Delay (some implementations)
101
+ 79: "Sound Controller 10", # Default: Undefined
102
+ 80: "General Purpose Controller 5",
103
+ 81: "General Purpose Controller 6",
104
+ 82: "General Purpose Controller 7",
105
+ 83: "General Purpose Controller 8",
106
+ 84: "Portamento Control",
107
+ 85: "Undefined",
108
+ 86: "Undefined",
109
+ 87: "Undefined",
110
+ 88: "High Resolution Velocity Prefix",
111
+ 89: "Undefined",
112
+ 90: "Undefined",
113
+ 91: "Effects 1 Depth (Reverb Send Level)",
114
+ 92: "Effects 2 Depth (Tremolo Depth)",
115
+ 93: "Effects 3 Depth (Chorus Depth)",
116
+ 94: "Effects 4 Depth (Detune Depth)",
117
+ 95: "Effects 5 Depth (Phaser Depth)",
118
+ 96: "Data Increment",
119
+ 97: "Data Decrement",
120
+ 98: "Non-Registered Parameter Number (NRPN) LSB",
121
+ 99: "Non-Registered Parameter Number (NRPN) MSB",
122
+ 100: "Registered Parameter Number (RPN) LSB",
123
+ 101: "Registered Parameter Number (RPN) MSB",
124
+ 102: "Undefined",
125
+ 103: "Undefined",
126
+ 104: "Undefined",
127
+ 105: "Undefined",
128
+ 106: "Undefined",
129
+ 107: "Undefined",
130
+ 108: "Undefined",
131
+ 109: "Undefined",
132
+ 110: "Undefined",
133
+ 111: "Undefined",
134
+ 112: "Undefined",
135
+ 113: "Undefined",
136
+ 114: "Undefined",
137
+ 115: "Undefined",
138
+ 116: "Undefined",
139
+ 117: "Undefined",
140
+ 118: "Undefined",
141
+ 119: "Undefined",
142
+ 120: "All Sound Off",
143
+ 121: "Reset All Controllers",
144
+ 122: "Local Control",
145
+ 123: "All Notes Off",
146
+ 124: "Omni Mode Off",
147
+ 125: "Omni Mode On",
148
+ 126: "Mono Mode (Poly Off)",
149
+ 127: "Poly Mode (Mono Off)",
150
+ }
151
+
152
+
153
+ def midi_to_df(midi_filepath, parse_durations=True, on0_means_off=True) -> DF:
154
+ """
155
+ Converts a MIDI file into a DataFrame carrying metadata and
156
+ one event per row. For example, you may want to result.get_meta("ticks_per_beat").
157
+
158
+ Args:
159
+ midi_filepath (str): The path to the input MIDI file.
160
+ parse_durations (bool):
161
+ If True, parse the duration of each event, effectively eliminating note_off events.
162
+ on0_means_off (bool):
163
+ If true, note_on events with velocity 0 will be interpreted as note_off events.
164
+ """
165
+ midi_data = []
166
+ mid = mido.MidiFile(midi_filepath)
167
+ for i, track in enumerate(mid.tracks):
168
+ absolute_time = 0
169
+ active_note_events = {}
170
+ for msg in track:
171
+ absolute_time += msg.time
172
+ event_info = dict(msg.dict(), absolute_time=absolute_time)
173
+ if parse_durations:
174
+ if msg.type == "note_on":
175
+ if msg.velocity > 0:
176
+ active_note_events[(msg.channel, msg.note)] = event_info
177
+ elif on0_means_off:
178
+ if (msg.channel, msg.note) in active_note_events:
179
+ event_info = active_note_events.pop((msg.channel, msg.note))
180
+ event_info["duration"] = (
181
+ absolute_time - event_info["absolute_time"]
182
+ )
183
+ else:
184
+ warnings.warn(
185
+ f"note_on event with velocity 0 was to be interpreted as note_off event "
186
+ f"but there was no {msg.note}-note active on {msg.channel}."
187
+ )
188
+ elif msg.type == "note_off":
189
+ if (msg.channel, msg.note) in active_note_events:
190
+ event_info = active_note_events.pop((msg.channel, msg.note))
191
+ event_info["duration"] = (
192
+ absolute_time - event_info["absolute_time"]
193
+ )
194
+ else:
195
+ warnings.warn(
196
+ f"note_on event with velocity 0 was to be interpreted as note_off event "
197
+ f"but there was no {msg.note}-note active on {msg.channel}."
198
+ )
199
+ midi_data.append(event_info)
200
+ # end of message-wise loop
201
+ if parse_durations and active_note_events:
202
+ warning_msg = (
203
+ f"{len(active_note_events)} events on track {i} have not been ended by note_off. "
204
+ f"Their duration is set None."
205
+ )
206
+ if not on0_means_off:
207
+ warning_msg += (
208
+ " Setting on0_means_off to True might solve this problem."
209
+ )
210
+ warnings.warn(warning_msg)
211
+ # end of track-wise loop
212
+
213
+ metadata = {}
214
+ for attr_name in dir(mid):
215
+ if attr_name.startswith("_") or attr_name in ("merged_track", "tracks"):
216
+ continue
217
+ try:
218
+ attr_value = getattr(mid, attr_name)
219
+ if callable(attr_value):
220
+ continue
221
+ metadata[attr_name] = attr_value
222
+ except Exception:
223
+ pass
224
+
225
+ dtypes = dict(
226
+ tempo="Int64",
227
+ control="Int64",
228
+ value="Int64",
229
+ channel="Int64",
230
+ program="Int64",
231
+ note="Int64",
232
+ velocity="Int64",
233
+ duration="Int64",
234
+ )
235
+
236
+ # Create a pandas DataFrame with ._meta property
237
+ df = DF(
238
+ midi_data,
239
+ meta=metadata,
240
+ ).sort_values(["absolute_time", "type"])
241
+ df = df.astype({col: typ for col, typ in dtypes.items() if col in df})
242
+ if "control" in df:
243
+ purpose_col_position = df.columns.get_loc("control") + 1
244
+ df.insert(purpose_col_position, "control_purpose", df.control.map(CC_PURPOSE))
245
+ return df
246
+
247
+
248
+ # endregion MIDI
249
+
250
+ # region parsing with partitura
251
+
252
+
253
+ def note_array_from_part_list(
254
+ part_list,
255
+ unique_id_per_part=True,
256
+ include_pitch_spelling=False,
257
+ include_key_signature=False,
258
+ include_time_signature=False,
259
+ include_metrical_position=False,
260
+ include_grace_notes=False,
261
+ include_staff=False,
262
+ include_divs_per_quarter=False,
263
+ merge_tied_notes=True,
264
+ ):
265
+ """
266
+ Construct a structured Note array from a list of Part objects
267
+
268
+ Parameters
269
+ ----------
270
+ part_list : list
271
+ A list of `Part` or `PerformedPart` objects. All elements in
272
+ the list must be of the same type (i.e., no mixing `Part`
273
+ and `PerformedPart` objects in the same list.
274
+ unique_id_per_part : bool (optional)
275
+ Indicate from which part do each note come from in the note ids. Default is True.
276
+ **kwargs : dict
277
+ Additional keyword arguments to pass to `utils.music.note_array_from_part()`
278
+
279
+ Returns
280
+ -------
281
+ note_array: structured array
282
+ A structured array containing pitch, onset, duration, voice
283
+ and id for each note in each part of the `part_list`. The note
284
+ ids in this array include the number of the part to which they
285
+ belong.
286
+ """
287
+ from partitura.performance import PerformedPart
288
+ from partitura.score import Part, PartGroup
289
+
290
+ kwargs = dict(
291
+ include_pitch_spelling=include_pitch_spelling,
292
+ include_key_signature=include_key_signature,
293
+ include_time_signature=include_time_signature,
294
+ include_metrical_position=include_metrical_position,
295
+ include_grace_notes=include_grace_notes,
296
+ include_staff=include_staff,
297
+ include_divs_per_quarter=include_divs_per_quarter,
298
+ merge_tied_notes=merge_tied_notes,
299
+ )
300
+ is_score = False
301
+ note_array = []
302
+ for i, part in enumerate(part_list):
303
+ if isinstance(part, (Part, PartGroup)):
304
+ # set include_divs_per_quarter, to correctly merge different divs
305
+ kwargs["include_divs_per_quarter"] = True
306
+ is_score = True
307
+ if isinstance(part, Part):
308
+ na = note_array_from_part(part, **kwargs)
309
+ elif isinstance(part, PartGroup):
310
+ na = note_array_from_part_list(
311
+ part.children, unique_id_per_part=unique_id_per_part, **kwargs
312
+ )
313
+ elif isinstance(part, PerformedPart):
314
+ na = part.note_array()
315
+ if unique_id_per_part and len(part_list) > 1:
316
+ # Update id with part number
317
+ na["id"] = np.array(
318
+ ["P{0:02d}_".format(i) + nid for nid in na["id"]], dtype=na["id"].dtype
319
+ )
320
+ note_array.append(na)
321
+
322
+ if is_score:
323
+ # rescale if parts have different divs
324
+ divs_per_parts = [
325
+ part_na[0]["divs_pq"] for part_na in note_array if len(part_na)
326
+ ]
327
+ lcm = np.lcm.reduce(divs_per_parts)
328
+ time_multiplier_per_part = [int(lcm / d) for d in divs_per_parts]
329
+ for na, time_mult in zip(note_array, time_multiplier_per_part):
330
+ na["onset_div"] = na["onset_div"] * time_mult
331
+ na["duration_div"] = na["duration_div"] * time_mult
332
+ na["divs_pq"] = na["divs_pq"] * time_mult
333
+
334
+ # concatenate note_arrays
335
+ note_array = np.hstack(note_array)
336
+
337
+ onset_unit, _ = get_time_units_from_note_array(note_array)
338
+
339
+ # sort by onset and pitch
340
+ pitch_sort_idx = np.argsort(note_array["pitch"])
341
+ note_array = note_array[pitch_sort_idx]
342
+ onset_sort_idx = np.argsort(note_array[onset_unit], kind="mergesort")
343
+ note_array = note_array[onset_sort_idx]
344
+
345
+ return note_array
346
+
347
+
348
+ def rest_array_from_part_list(
349
+ part_list,
350
+ unique_id_per_part=True,
351
+ include_pitch_spelling=False,
352
+ include_key_signature=False,
353
+ include_time_signature=False,
354
+ include_grace_notes=False,
355
+ include_staff=False,
356
+ include_divs_per_quarter=False,
357
+ collapse=False,
358
+ ):
359
+ """
360
+ Construct a structured Rest array from a list of Part objects
361
+
362
+ Parameters
363
+ ----------
364
+ part_list : list
365
+ A list of `Part` or `PerformedPart` objects. All elements in
366
+ the list must be of the same type.
367
+ unique_id_per_part : bool (optional)
368
+ Indicate from which part do each rest come from in the rest ids.
369
+ include_pitch_spelling: bool (optional)
370
+ Include pitch spelling information in rest array.
371
+ This is a dummy attribute and returns zeros everywhere.
372
+ Default is False.
373
+ include_key_signature: bool (optional)
374
+ Include key signature information in output rest array.
375
+ Only valid if parts in `part_list` are `Part` objects.
376
+ See `rest_array_from_part` for more info.
377
+ Default is False.
378
+ include_time_signature : bool (optional)
379
+ Include time signature information in output rest array.
380
+ Only valid if parts in `part_list` are `Part` objects.
381
+ See `rest_array_from_part` for more info.
382
+ Default is False.
383
+ include_grace_notes : bool (optional)
384
+ If `True`, includes grace note information, i.e. "" for every rest).
385
+ Default is False
386
+ include_staff : bool (optional)
387
+ If `True`, includes note staff number.
388
+ Default is False
389
+ include_divs_per_quarter : bool (optional)
390
+ If `True`, include the number of divs (e.g. MIDI ticks,
391
+ MusicXML ppq) per quarter note of the current part.
392
+ Default is False
393
+
394
+ Returns
395
+ -------
396
+ rest_array: structured array
397
+ A structured array containing pitch (always zero), onset, duration, voice
398
+ and id for each rest in each part of the `part_list`. The rest
399
+ ids in this array include the number of the part to which they
400
+ belong.
401
+ """
402
+ from partitura.score import Part, PartGroup
403
+
404
+ rest_array = []
405
+ for i, part in enumerate(part_list):
406
+ if isinstance(part, (Part, PartGroup)):
407
+ if isinstance(part, Part):
408
+ na = rest_array_from_part(
409
+ part=part,
410
+ include_pitch_spelling=include_pitch_spelling,
411
+ include_key_signature=include_key_signature,
412
+ include_time_signature=include_time_signature,
413
+ include_grace_notes=include_grace_notes,
414
+ include_staff=include_staff,
415
+ include_divs_per_quarter=include_divs_per_quarter,
416
+ collapse=collapse,
417
+ )
418
+ elif isinstance(part, PartGroup):
419
+ na = rest_array_from_part_list(
420
+ part_list=part.children,
421
+ unique_id_per_part=unique_id_per_part,
422
+ include_pitch_spelling=include_pitch_spelling,
423
+ include_key_signature=include_key_signature,
424
+ include_time_signature=include_time_signature,
425
+ include_grace_notes=include_grace_notes,
426
+ include_staff=include_staff,
427
+ include_divs_per_quarter=include_divs_per_quarter,
428
+ collapse=collapse,
429
+ )
430
+ if unique_id_per_part:
431
+ # Update id with part number
432
+ na["id"] = np.array(
433
+ ["P{0:02d}_".format(i) + nid for nid in na["id"]], dtype=na["id"].dtype
434
+ )
435
+ rest_array.append(na)
436
+
437
+ # concatenate note_arrays
438
+ rest_array = np.hstack(rest_array)
439
+
440
+ onset_unit, _ = get_time_units_from_note_array(rest_array)
441
+
442
+ # sort by onset and pitch
443
+ pitch_sort_idx = np.argsort(rest_array["pitch"])
444
+ rest_array = rest_array[pitch_sort_idx]
445
+ onset_sort_idx = np.argsort(rest_array[onset_unit], kind="mergesort")
446
+ rest_array = rest_array[onset_sort_idx]
447
+
448
+ return rest_array
449
+
450
+
451
+ def event_array_from_part_list(
452
+ part_list,
453
+ include_key_signature=False,
454
+ include_time_signature=False,
455
+ include_metrical_position=False,
456
+ include_staff=False,
457
+ include_divs_per_quarter=False,
458
+ include_type=True,
459
+ ):
460
+ """
461
+ Construct a structured Event array from a list of Part objects
462
+
463
+ Parameters
464
+ ----------
465
+ part_list : list
466
+ A list of `Part` or `PerformedPart` objects. All elements in
467
+ the list must be of the same type (i.e., no mixing `Part`
468
+ and `PerformedPart` objects in the same list.
469
+ unique_id_per_part : bool (optional)
470
+ Indicate from which part do each note come from in the note ids. Default is True.
471
+ **kwargs : dict
472
+ Additional keyword arguments to pass to `utils.music.note_array_from_part()`
473
+
474
+ Returns
475
+ -------
476
+ note_array: structured array
477
+ A structured array containing pitch, onset, duration, voice
478
+ and id for each note in each part of the `part_list`. The note
479
+ ids in this array include the number of the part to which they
480
+ belong.
481
+ """
482
+ from partitura.performance import PerformedPart
483
+ from partitura.score import Part, PartGroup
484
+
485
+ kwargs = dict(
486
+ include_key_signature=include_key_signature,
487
+ include_time_signature=include_time_signature,
488
+ include_metrical_position=include_metrical_position,
489
+ include_staff=include_staff,
490
+ include_divs_per_quarter=include_divs_per_quarter,
491
+ include_type=include_type,
492
+ )
493
+ is_score = False
494
+ event_array = []
495
+ for i, part in enumerate(part_list):
496
+ if isinstance(part, (Part, PartGroup)):
497
+ # set include_divs_per_quarter, to correctly merge different divs
498
+ kwargs["include_divs_per_quarter"] = True
499
+ is_score = True
500
+ if isinstance(part, Part):
501
+ e_arr = event_array_from_part(part, **kwargs)
502
+ elif isinstance(part, PartGroup):
503
+ e_arr = event_array_from_part_list(part.children, **kwargs)
504
+ elif isinstance(part, PerformedPart):
505
+ e_arr = part.note_array()
506
+ event_array.append(e_arr)
507
+
508
+ if is_score:
509
+ # rescale if parts have different divs
510
+ divs_per_parts = [
511
+ part_na[0]["divs_pq"] for part_na in event_array if len(part_na)
512
+ ]
513
+ lcm = np.lcm.reduce(divs_per_parts)
514
+ time_multiplier_per_part = [int(lcm / d) for d in divs_per_parts]
515
+ for e_arr, time_mult in zip(event_array, time_multiplier_per_part):
516
+ e_arr["onset_div"] = e_arr["onset_div"] * time_mult
517
+ e_arr["duration_div"] = e_arr["duration_div"] * time_mult
518
+ e_arr["divs_pq"] = e_arr["divs_pq"] * time_mult
519
+
520
+ # concatenate note_arrays
521
+ event_array = np.hstack(event_array)
522
+
523
+ onset_unit, _ = get_time_units_from_note_array(event_array)
524
+
525
+ # sort by onset and pitch
526
+ onset_sort_idx = np.argsort(event_array[onset_unit], kind="mergesort")
527
+ event_array = event_array[onset_sort_idx]
528
+
529
+ return event_array
530
+
531
+
532
+ def slice_notearray_by_time(
533
+ note_array, start_time, end_time, time_unit="auto", clip_onset_duration=True
534
+ ):
535
+ """
536
+ Get a slice of a structured note array by time
537
+
538
+ Parameters
539
+ ----------
540
+ note_array : structured array
541
+ Structured array with score information.
542
+ start_time : float
543
+ Starting time
544
+ end_time : float
545
+ End time
546
+ time_unit : {'auto', 'beat', 'quarter', 'second', 'div'} optional
547
+ Time unit. If 'auto', the default time unit will be inferred
548
+ from the note_array.
549
+ clip_onset_duration : bool optional
550
+ Clip duration of the notes in the array to fit within the
551
+ specified window
552
+
553
+ Returns
554
+ -------
555
+ note_array_slice : stuctured array
556
+ Structured array with only the score information between
557
+ `start_time` and `end_time`.
558
+
559
+ TODO
560
+ ----
561
+ * adjust onsets and duration in other units
562
+ """
563
+
564
+ if time_unit not in TIME_UNITS + ["auto"]:
565
+ raise ValueError(
566
+ "`time_unit` must be 'beat', 'quarter', "
567
+ "'sec', 'div' or 'auto', but is "
568
+ "{0}".format(time_unit)
569
+ )
570
+ if time_unit == "auto":
571
+ onset_unit, duration_unit = get_time_units_from_note_array(note_array)
572
+ else:
573
+ onset_unit, duration_unit = [
574
+ "{0}_{1}".format(d, time_unit) for d in ("onset", "duration")
575
+ ]
576
+
577
+ onsets = note_array[onset_unit]
578
+ offsets = note_array[onset_unit] + note_array[duration_unit]
579
+
580
+ starting_idxs = set(np.where(onsets >= start_time)[0])
581
+ ending_idxs = set(np.where(onsets < end_time)[0])
582
+
583
+ prev_starting_idxs = set(np.where(onsets < start_time)[0])
584
+ sounding_after_start_idxs = set(np.where(offsets > start_time)[0])
585
+
586
+ active_idx = np.array(
587
+ list(
588
+ starting_idxs.intersection(ending_idxs).union(
589
+ prev_starting_idxs.intersection(sounding_after_start_idxs)
590
+ )
591
+ )
592
+ )
593
+ active_idx.sort()
594
+
595
+ if len(active_idx) == 0:
596
+ # If there are no elements, return an empty array
597
+ note_array_slice = np.empty(0, dtype=note_array.dtype)
598
+ else:
599
+ note_array_slice = note_array[active_idx]
600
+
601
+ if clip_onset_duration and len(active_idx) > 0:
602
+ psi = np.where(note_array_slice[onset_unit] < start_time)[0]
603
+ note_array_slice[psi] = start_time
604
+ adj_offsets = np.clip(
605
+ note_array_slice[onset_unit] + note_array_slice[duration_unit],
606
+ a_min=None,
607
+ a_max=end_time,
608
+ )
609
+ note_array_slice[duration_unit] = adj_offsets - note_array_slice[onset_unit]
610
+
611
+ return note_array_slice
612
+
613
+
614
+ def note_array_from_part(
615
+ part,
616
+ include_pitch_spelling=False,
617
+ include_key_signature=False,
618
+ include_time_signature=False,
619
+ include_metrical_position=False,
620
+ include_grace_notes=False,
621
+ include_staff=False,
622
+ include_divs_per_quarter=False,
623
+ include_type=True,
624
+ merge_tied_notes=True,
625
+ ):
626
+ """
627
+ Create a structured array with note information
628
+ from a `Part` object.
629
+
630
+ Parameters
631
+ ----------
632
+ part : partitura.score.Part
633
+ An object representing a score part.
634
+ include_pitch_spelling : bool (optional)
635
+ It's a dummy attribute for consistancy between note_array_from_part and note_array_from_part_list.
636
+ Default is False
637
+ include_pitch_spelling : bool (optional)
638
+ If `True`, includes pitch spelling information for each
639
+ note. Default is False
640
+ include_key_signature : bool (optional)
641
+ If `True`, includes key signature information, i.e.,
642
+ the key signature at the onset time of each note (all
643
+ notes starting at the same time have the same key signature).
644
+ Default is False
645
+ include_time_signature : bool (optional)
646
+ If `True`, includes time signature information, i.e.,
647
+ the time signature at the onset time of each note (all
648
+ notes starting at the same time have the same time signature).
649
+ Default is False
650
+ include_metrical_position : bool (optional)
651
+ If `True`, includes metrical position information, i.e.,
652
+ the position of the onset time of each note with respect to its
653
+ measure (all notes starting at the same time have the same metrical
654
+ position).
655
+ Default is False
656
+ include_grace_notes : bool (optional)
657
+ If `True`, includes grace note information, i.e. if a note is a
658
+ grace note and the grace type "" for non grace notes).
659
+ Default is False
660
+ include_staff : bool (optional)
661
+ If `True`, includes staff information
662
+ Default is False
663
+ include_divs_per_quarter : bool (optional)
664
+ If `True`, include the number of divs (e.g. MIDI ticks,
665
+ MusicXML ppq) per quarter note of the current part.
666
+ Default is False
667
+
668
+ Returns
669
+ -------
670
+ note_array : structured array
671
+ A structured array containing note information. The fields are
672
+ * 'onset_beat': onset time of the note in beats
673
+ * 'duration_beat': duration of the note in beats
674
+ * 'onset_quarter': onset time of the note in quarters
675
+ * 'duration_quarter': duration of the note in quarters
676
+ * 'onset_div': onset of the note in divs (e.g., MIDI ticks,
677
+ divisions in MusicXML)
678
+ * 'duration_div': duration of the note in divs
679
+ * 'pitch': MIDI pitch of a note.
680
+ * 'voice': Voice number of a note (if given in the score)
681
+ * 'id': Id of the note
682
+
683
+ If `include_pitch_spelling` is True:
684
+ * 'step': name of the note ("C", "D", "E", "F", "G", "A", "B")
685
+ * 'alter': alteration (0=natural, -1=flat, 1=sharp,
686
+ 2=double sharp, etc.)
687
+ * 'octave': octave of the note.
688
+
689
+ If `include_key_signature` is True:
690
+ * 'ks_fifths': Fifths starting from C in the circle of fifths
691
+ * 'mode': major or minor
692
+
693
+ If `include_time_signature` is True:
694
+ * 'ts_beats': number of beats in a measure
695
+ * 'ts_beat_type': type of beats (denominator of the time signature)
696
+ * 'ts_mus_beat' : number of musical beats is it's set, otherwise ts_beats
697
+
698
+ If `include_metrical_position` is True:
699
+ * 'is_downbeat': 1 if the note onset is on a downbeat, 0 otherwise
700
+ * 'rel_onset_div': number of divs elapsed from the beginning of the note measure
701
+ * 'tot_measure_divs' : total number of divs in the note measure
702
+
703
+ If 'include_grace_notes' is True:
704
+ * 'is_grace': 1 if the note is a grace 0 otherwise
705
+ * 'grace_type' : the type of the grace notes "" for non grace notes
706
+
707
+ If 'include_staff' is True:
708
+ * 'staff' : the staff number for each note
709
+
710
+ If 'include_divs_per_quarter' is True:
711
+ * 'divs_pq': the number of divs per quarter note
712
+ Examples
713
+ --------
714
+ >>> from partitura import load_musicxml, EXAMPLE_MUSICXML
715
+ >>> from partitura.utils import note_array_from_part
716
+ >>> part = load_musicxml(EXAMPLE_MUSICXML)
717
+ >>> note_array_from_part(part, True, True, True) # doctest: +NORMALIZE_WHITESPACE
718
+ array([(0., 4., 0., 4., 0, 48, 69, 1, 'n01', 'A', 0, 4, 0, 1, 4, 4),
719
+ (2., 2., 2., 2., 24, 24, 72, 2, 'n02', 'C', 0, 5, 0, 1, 4, 4),
720
+ (2., 2., 2., 2., 24, 24, 76, 2, 'n03', 'E', 0, 5, 0, 1, 4, 4)],
721
+ dtype=[('onset_beat', '<f4'),
722
+ ('duration_beat', '<f4'),
723
+ ('onset_quarter', '<f4'),
724
+ ('duration_quarter', '<f4'),
725
+ ('onset_div', '<i4'),
726
+ ('duration_div', '<i4'),
727
+ ('pitch', '<i4'),
728
+ ('voice', '<i4'),
729
+ ('id', '<U256'),
730
+ ('step', '<U256'),
731
+ ('alter', '<i4'),
732
+ ('octave', '<i4'),
733
+ ('ks_fifths', '<i4'),
734
+ ('ks_mode', '<i4'),
735
+ ('ts_beats', '<i4'),
736
+ ('ts_beat_type', '<i4')])
737
+ """
738
+ if include_time_signature:
739
+ time_signature_map = part.time_signature_map
740
+ else:
741
+ time_signature_map = None
742
+
743
+ if include_key_signature:
744
+ key_signature_map = part.key_signature_map
745
+ else:
746
+ key_signature_map = None
747
+
748
+ if include_metrical_position:
749
+ metrical_position_map = part.metrical_position_map
750
+ else:
751
+ metrical_position_map = None
752
+
753
+ if include_divs_per_quarter:
754
+ divs_per_quarter = get_divs_per_quarter(part)
755
+ else:
756
+ divs_per_quarter = None
757
+
758
+ note_array = note_array_from_note_list(
759
+ note_list=part.notes_tied,
760
+ beat_map=part.beat_map,
761
+ quarter_map=part.quarter_map,
762
+ time_signature_map=time_signature_map,
763
+ key_signature_map=key_signature_map,
764
+ metrical_position_map=metrical_position_map,
765
+ include_pitch_spelling=include_pitch_spelling,
766
+ include_grace_notes=include_grace_notes,
767
+ include_staff=include_staff,
768
+ divs_per_quarter=divs_per_quarter,
769
+ include_type=include_type,
770
+ merge_tied_notes=merge_tied_notes,
771
+ )
772
+
773
+ return note_array
774
+
775
+
776
+ def note_array_performed_part(part: ptp.PerformedPart) -> np.ndarray:
777
+ """Structured array containing performance information.
778
+ The fields are 'id', 'pitch', 'onset_tick', 'duration_tick',
779
+ 'onset_sec', 'duration_sec', 'track', 'channel', and 'velocity'.
780
+ """
781
+ return note_array_from_performed_note_list(
782
+ notes=part.notes,
783
+ ppq=part.ppq,
784
+ mpq=part.mpq,
785
+ )
786
+
787
+
788
+ def note_array_from_performed_note_list(
789
+ notes: Iterable[ptp.PerformedNote],
790
+ ppq: int = 480,
791
+ mpq: int = 500000,
792
+ ) -> np.ndarray:
793
+
794
+ fields = [
795
+ ("onset_sec", "f4"),
796
+ ("duration_sec", "f4"),
797
+ ("onset_tick", "i4"),
798
+ ("duration_tick", "i4"),
799
+ ("pitch", "i4"),
800
+ ("velocity", "i4"),
801
+ ("track", "i4"),
802
+ ("channel", "i4"),
803
+ ("id", "U256"),
804
+ ]
805
+ note_array = []
806
+ for n in notes:
807
+ note_on_tick, duration_tick, note_on_sec, duration_sec = get_pnote_durations(
808
+ n, ppq, mpq
809
+ )
810
+ note_array.append(
811
+ (
812
+ note_on_sec,
813
+ duration_sec,
814
+ note_on_tick,
815
+ duration_tick,
816
+ n["midi_pitch"],
817
+ n["velocity"],
818
+ n.get("track", 0),
819
+ n.get("channel", 1),
820
+ n["id"],
821
+ )
822
+ )
823
+
824
+ note_array = np.array(note_array, dtype=fields)
825
+ tick_sort_idx = np.argsort(note_array["onset_tick"])
826
+ note_array = note_array[tick_sort_idx]
827
+ return note_array
828
+
829
+
830
+ def get_pnote_durations(pnote, ppq, mpq):
831
+ note_on_sec = pnote["note_on"]
832
+ note_on_tick = pnote.get(
833
+ "note_on_tick",
834
+ seconds_to_midi_ticks(pnote["note_on"], mpq=mpq, ppq=ppq),
835
+ )
836
+ offset = pnote.get("sound_off", pnote["note_off"])
837
+ duration_sec = offset - note_on_sec
838
+ duration_tick = (
839
+ pnote.get(
840
+ seconds_to_midi_ticks(pnote["sound_off"], mpq=mpq, ppq=ppq),
841
+ seconds_to_midi_ticks(pnote["note_off"], mpq=mpq, ppq=ppq),
842
+ )
843
+ - note_on_tick
844
+ )
845
+ return note_on_tick, duration_tick, note_on_sec, duration_sec
846
+
847
+
848
+ def rest_array_from_part(
849
+ part,
850
+ include_pitch_spelling=False,
851
+ include_key_signature=False,
852
+ include_time_signature=False,
853
+ include_metrical_position=False,
854
+ include_grace_notes=False,
855
+ include_staff=False,
856
+ include_divs_per_quarter=False,
857
+ collapse=False,
858
+ include_type=True,
859
+ ):
860
+ """
861
+ Create a structured array with rest information
862
+ from a `Part` object Similar to note_array.
863
+
864
+ Parameters
865
+ ----------
866
+ part : partitura.score.Part
867
+ An object representing a score part.
868
+ include_pitch_spelling : bool (optional)
869
+ It's a dummy attribute for consistancy between rest_array_from_part and rest_array_from_part_list.
870
+ Default is False
871
+ include_key_signature : bool (optional)
872
+ If `True`, includes key signature information, i.e.,
873
+ the key signature at the onset time of each rest (all
874
+ notes starting at the same time have the same key signature).
875
+ Default is False
876
+ include_time_signature : bool (optional)
877
+ If `True`, includes time signature information, i.e.,
878
+ the time signature at the onset time of each rest (all
879
+ rests starting at the same time have the same time signature).
880
+ Default is False
881
+ include_metrical_position : bool (optional)
882
+ If `True`, includes metrical position information, i.e.,
883
+ the position of the onset time of each note with respect to its
884
+ measure.
885
+ Default is False
886
+ include_grace_notes : bool (optional)
887
+ If `True`, includes grace note information, i.e. the grace type is "" for all rests).
888
+ Default is False
889
+ include_divs_per_quarter : bool (optional)
890
+ If `True`, include the number of divs (e.g. MIDI ticks,
891
+ MusicXML ppq) per quarter note of the current part.
892
+ Default is False
893
+ collapse : bool (optional)
894
+ If 'True', collapses consecutive rest onsets on the same voice, to a single rest of their combined duration.
895
+ Default is False
896
+
897
+ Returns
898
+ -------
899
+ rest_array : structured array
900
+ A structured array containing rest information (pitch is always 0).
901
+ """
902
+ if include_time_signature:
903
+ time_signature_map = part.time_signature_map
904
+ else:
905
+ time_signature_map = None
906
+
907
+ if include_key_signature:
908
+ key_signature_map = part.key_signature_map
909
+ else:
910
+ key_signature_map = None
911
+
912
+ if include_metrical_position:
913
+ metrical_position_map = part.metrical_position_map
914
+ else:
915
+ metrical_position_map = None
916
+
917
+ if include_divs_per_quarter:
918
+ divs_per_quarter = get_divs_per_quarter(part)
919
+ else:
920
+ divs_per_quarter = None
921
+
922
+ rest_array = rest_array_from_rest_list(
923
+ rest_list=part.rests,
924
+ beat_map=part.beat_map,
925
+ quarter_map=part.quarter_map,
926
+ time_signature_map=time_signature_map,
927
+ key_signature_map=key_signature_map,
928
+ metrical_position_map=metrical_position_map,
929
+ include_pitch_spelling=include_pitch_spelling,
930
+ include_grace_notes=include_grace_notes,
931
+ include_staff=include_staff,
932
+ divs_per_quarter=divs_per_quarter,
933
+ collapse=collapse,
934
+ include_type=include_type,
935
+ )
936
+
937
+ return rest_array
938
+
939
+
940
+ def note_array_from_note_list(
941
+ note_list: Iterable[pts.Note] | Iterable[tuple[pts.Note, str]],
942
+ beat_map=None,
943
+ quarter_map=None,
944
+ time_signature_map=None,
945
+ key_signature_map=None,
946
+ metrical_position_map=None,
947
+ include_pitch_spelling=False,
948
+ include_grace_notes=False,
949
+ include_staff=False,
950
+ divs_per_quarter=None,
951
+ include_type=True,
952
+ merge_tied_notes=True,
953
+ ):
954
+ """
955
+ Create a structured array with note information
956
+ from a a list of `Note` objects.
957
+
958
+ Parameters
959
+ ----------
960
+ note_list : list of `Note` objects
961
+ A list of `Note` objects containing score information.
962
+ beat_map : callable or None
963
+ A function that maps score time in divs to score time in beats.
964
+ If `None` is given, the output structured array will not
965
+ include this information.
966
+ quarter_map: callable or None
967
+ A function that maps score time in divs to score time in quarters.
968
+ If `None` is given, the output structured array will not
969
+ include this information.
970
+ time_signature_map: callable or None (optional)
971
+ A function that maps score time in divs to the time signature at
972
+ that time (in terms of number of beats and beat type).
973
+ If `None` is given, the output structured array will not
974
+ include this information.
975
+ key_signature_map: callable or None (optional)
976
+ A function that maps score time in divs to the key signature at
977
+ that time (in terms of fifths and mode).
978
+ If `None` is given, the output structured array will not
979
+ include this information.
980
+ metrical_position_map: callable or None (optional)
981
+ A function that maps score time in divs to the position in
982
+ the measure at that time.
983
+ If `None` is given, the output structured array will not
984
+ include the metrical position information.
985
+ include_pitch_spelling : bool (optional)
986
+ If `True`, includes pitch spelling information for each
987
+ note. Default is False
988
+ include_grace_notes : bool (optional)
989
+ If `True`, includes grace note information, i.e. if a note is a
990
+ grace note has one of the types "appoggiatura, acciaccatura, grace" and
991
+ the grace type "" for non grace notes).
992
+ Default is False
993
+ include_staff : bool (optional)
994
+ If `True`, includes the staff number for every note.
995
+ Default is False
996
+ divs_per_quarter : int or None (optional)
997
+ The number of divs (e.g. MIDI ticks, MusicXML ppq) per quarter
998
+ note of the current part.
999
+ Default is None
1000
+ include_type: bool
1001
+
1002
+
1003
+ Returns
1004
+ -------
1005
+ note_array : structured array
1006
+ A structured array containing note information. The fields are
1007
+ * 'onset_beat': onset time of the note in beats.
1008
+ Included if `beat_map` is not `None`.
1009
+ * 'duration_beat': duration of the note in beats.
1010
+ Included if `beat_map` is not `None`.
1011
+ * 'onset_quarter': onset time of the note in quarters.
1012
+ Included if `quarter_map` is not `None`.
1013
+ * 'duration_quarter': duration of the note in quarters.
1014
+ Included if `quarter_map` is not `None`.
1015
+ * 'onset_div': onset of the note in divs (e.g., MIDI ticks,
1016
+ divisions in MusicXML)
1017
+ * 'duration_div': duration of the note in divs
1018
+ * 'pitch': MIDI pitch of a note.
1019
+ * 'voice': Voice number of a note (if given in the score)
1020
+ * 'id': Id of the note
1021
+ * 'step': name of the note ("C", "D", "E", "F", "G", "A", "B").
1022
+ Included if `include_pitch_spelling` is `True`.
1023
+ * 'alter': alteration (0=natural, -1=flat, 1=sharp,
1024
+ 2=double sharp, etc.). Included if `include_pitch_spelling`
1025
+ is `True`.
1026
+ * 'octave': octave of the note. Included if `include_pitch_spelling`
1027
+ is `True`.
1028
+ * 'is_grace' : Is the note a grace note. Yes if true.
1029
+ * 'grace_type' : The type of grace note. "" for non grace notes.
1030
+ * 'ks_fifths': Fifths starting from C in the circle of fifths.
1031
+ Included if `key_signature_map` is not `None`.
1032
+ * 'mode': major or minor. Included If `key_signature_map` is
1033
+ not `None`.
1034
+ * 'ts_beats': number of beats in a measure. If `time_signature_map`
1035
+ is True.
1036
+ * 'ts_beat_type': type of beats (denominator of the time signature).
1037
+ If `include_time_signature` is True.
1038
+ * 'is_downbeat': 1 if the note onset is on a downbeat, 0 otherwise.
1039
+ If `measure_map` is not None.
1040
+ * 'rel_onset_div': number of divs elapsed from the beginning of the
1041
+ note measure. If `measure_map` is not None.
1042
+ * 'tot_measure_div' : total number of divs in the note measure
1043
+ If `measure_map` is not None.
1044
+ * 'staff' : number of note staff.
1045
+ * 'divs_pq' : number of parts per quarter note.
1046
+ """
1047
+ has_type = False
1048
+ for note in note_list:
1049
+ has_type = isinstance(note, tuple)
1050
+ if include_type:
1051
+ fields = [("event_type", "U256")]
1052
+ else:
1053
+ fields = []
1054
+ if beat_map is not None:
1055
+ # Preserve the order of the fields
1056
+ fields += [("onset_beat", "f4"), ("duration_beat", "f4")]
1057
+
1058
+ if quarter_map is not None:
1059
+ fields += [("onset_quarter", "f4"), ("duration_quarter", "f4")]
1060
+ fields += [
1061
+ ("onset_div", "i4"),
1062
+ ("duration_div", "i4"),
1063
+ ("pitch", "i4"),
1064
+ ("voice", "i4"),
1065
+ ("id", "U256"),
1066
+ ]
1067
+
1068
+ # fields for pitch spelling
1069
+ if include_pitch_spelling:
1070
+ fields += [("step", "U256"), ("alter", "i4"), ("octave", "i4")]
1071
+
1072
+ # fields for pitch spelling
1073
+ if include_grace_notes:
1074
+ fields += [("is_grace", "b"), ("grace_type", "U256")]
1075
+
1076
+ # fields for key signature
1077
+ if key_signature_map is not None:
1078
+ fields += [("ks_fifths", "i4"), ("ks_mode", "i4")]
1079
+
1080
+ # fields for time signature
1081
+ if time_signature_map is not None:
1082
+ fields += [("ts_beats", "i4"), ("ts_beat_type", "i4"), ("ts_mus_beats", "i4")]
1083
+
1084
+ # fields for metrical position
1085
+ if metrical_position_map is not None:
1086
+ fields += [
1087
+ ("is_downbeat", "i4"),
1088
+ ("rel_onset_div", "i4"),
1089
+ ("tot_measure_div", "i4"),
1090
+ ]
1091
+ # field for staff
1092
+ if include_staff:
1093
+ fields += [("staff", "i4")]
1094
+
1095
+ # field for divs_pq
1096
+ if divs_per_quarter:
1097
+ fields += [("divs_pq", "i4")]
1098
+
1099
+ note_array = []
1100
+ for note in note_list:
1101
+ note_info = tuple()
1102
+ if has_type:
1103
+ note, note_type = note
1104
+ if merge_tied_notes and note.tie_prev is not None:
1105
+ # skip the note event if it is tied to the previous note
1106
+ # this emulates Part.notes_tied behaviour even with a full note list
1107
+ continue
1108
+ if include_type:
1109
+ if not has_type:
1110
+ note_type = note.__class__.__name__
1111
+ note_info += (note_type,)
1112
+
1113
+ note_on_div = note.start.t
1114
+ if merge_tied_notes:
1115
+ note_off_div = note.start.t + note.duration_tied
1116
+ else:
1117
+ note_off_div = note.start.t + note.duration
1118
+ note_dur_div = note_off_div - note_on_div
1119
+
1120
+ if beat_map is not None:
1121
+ note_on_beat, note_off_beat = beat_map([note_on_div, note_off_div])
1122
+ note_dur_beat = note_off_beat - note_on_beat
1123
+
1124
+ note_info += (note_on_beat, note_dur_beat)
1125
+
1126
+ if quarter_map is not None:
1127
+ note_on_quarter, note_off_quarter = quarter_map([note_on_div, note_off_div])
1128
+ note_dur_quarter = note_off_quarter - note_on_quarter
1129
+
1130
+ note_info += (note_on_quarter, note_dur_quarter)
1131
+
1132
+ note_info += (
1133
+ note_on_div,
1134
+ note_dur_div,
1135
+ note.midi_pitch,
1136
+ note.voice if note.voice is not None else -1,
1137
+ note.id,
1138
+ )
1139
+
1140
+ if include_pitch_spelling:
1141
+ step = note.step
1142
+ alter = note.alter if note.alter is not None else 0
1143
+ octave = note.octave
1144
+
1145
+ note_info += (step, alter, octave)
1146
+
1147
+ if include_grace_notes:
1148
+ is_grace = hasattr(note, "grace_type")
1149
+ if is_grace:
1150
+ grace_type = note.grace_type
1151
+ else:
1152
+ grace_type = ""
1153
+ note_info += (is_grace, grace_type)
1154
+
1155
+ if key_signature_map is not None:
1156
+ fifths, mode = key_signature_map(note.start.t)
1157
+
1158
+ note_info += (fifths, mode)
1159
+
1160
+ if time_signature_map is not None:
1161
+ beats, beat_type, mus_beats = time_signature_map(note.start.t)
1162
+
1163
+ note_info += (beats, beat_type, mus_beats)
1164
+
1165
+ if metrical_position_map is not None:
1166
+ rel_onset_div, tot_measure_div = metrical_position_map(note.start.t)
1167
+
1168
+ is_downbeat = 1 if rel_onset_div == 0 else 0
1169
+
1170
+ note_info += (is_downbeat, rel_onset_div, tot_measure_div)
1171
+
1172
+ if include_staff:
1173
+ note_info += ((note.staff if note.staff else 0),)
1174
+
1175
+ if divs_per_quarter:
1176
+ note_info += (divs_per_quarter,)
1177
+
1178
+ note_array.append(note_info)
1179
+
1180
+ note_array = np.array(note_array, dtype=fields)
1181
+
1182
+ # Sanitize voice information
1183
+ no_voice_idx = np.where(note_array["voice"] == -1)[0]
1184
+ try:
1185
+ max_voice = note_array["voice"].max()
1186
+ except ValueError: # raised if `note_array["voice"]` is empty.
1187
+ note_array["voice"] = 0
1188
+ max_voice = 0
1189
+ note_array["voice"][no_voice_idx] = max_voice + 1
1190
+
1191
+ # sort by onset and pitch
1192
+ onset_unit, _ = get_time_units_from_note_array(note_array)
1193
+ pitch_sort_idx = np.argsort(note_array["pitch"])
1194
+ note_array = note_array[pitch_sort_idx]
1195
+ onset_sort_idx = np.argsort(note_array[onset_unit], kind="mergesort")
1196
+ note_array = note_array[onset_sort_idx]
1197
+
1198
+ return note_array
1199
+
1200
+
1201
+ def rest_array_from_rest_list(
1202
+ rest_list: Iterable[pts.Rest] | Iterable[tuple[pts.Rest, str]],
1203
+ beat_map=None,
1204
+ quarter_map=None,
1205
+ time_signature_map=None,
1206
+ key_signature_map=None,
1207
+ metrical_position_map=None,
1208
+ include_pitch_spelling=False,
1209
+ include_grace_notes=False,
1210
+ include_staff=False,
1211
+ divs_per_quarter=None,
1212
+ collapse=False,
1213
+ include_type=True,
1214
+ ):
1215
+ """
1216
+ Create a structured array with rest information
1217
+ from a list of `Rest` objects.
1218
+
1219
+ Parameters
1220
+ ----------
1221
+ rest_list : list of `Rest` objects
1222
+ A list of `Rest` objects containing score information.
1223
+ beat_map : callable or None
1224
+ A function that maps score time in divs to score time in beats.
1225
+ If `None` is given, the output structured array will not
1226
+ include this information.
1227
+ quarter_map: callable or None
1228
+ A function that maps score time in divs to score time in quarters.
1229
+ If `None` is given, the output structured array will not
1230
+ include this information.
1231
+ time_signature_map: callable or None (optional)
1232
+ A function that maps score time in divs to the time signature at
1233
+ that time (in terms of number of beats and beat type).
1234
+ If `None` is given, the output structured array will not
1235
+ include this information.
1236
+ key_signature_map: callable or None (optional)
1237
+ A function that maps score time in divs to the key signature at
1238
+ that time (in terms of fifths and mode).
1239
+ If `None` is given, the output structured array will not
1240
+ include this information.
1241
+ metrical_position_map: callable or None (optional)
1242
+ A function that maps score time in divs to the position in
1243
+ the measure at that time.
1244
+ If `None` is given, the output structured array will not
1245
+ include the metrical position information.
1246
+ include_pitch_spelling : bool (optional)
1247
+ If `True`, includes pitch spelling information for each
1248
+ rest. This is a dummy attribute and returns zeros everywhere.
1249
+ Default is False
1250
+ include_grace_notes : bool (optional)
1251
+ If `True`, includes grace note information, i.e. "" for all rests).
1252
+ Default is False
1253
+ include_staff : bool (optional)
1254
+ If `True`, includes the staff number for every note.
1255
+ Default is False
1256
+ divs_per_quarter : int or None (optional)
1257
+ The number of divs (e.g. MIDI ticks, MusicXML ppq) per quarter
1258
+ note of the current part.
1259
+ Default is None
1260
+ collapse : bool (optional)
1261
+ If `True`, joins rests on consecutive onsets on the same voice and combines their durations.
1262
+ Keeps the id of the first one.
1263
+ Default is False
1264
+ include_type: bool
1265
+
1266
+ Returns
1267
+ -------
1268
+ rest_array : structured array
1269
+ A structured array containing rest information. Pitch is set to 0.
1270
+ """
1271
+ has_type = False
1272
+ for rest in rest_list:
1273
+ has_type = isinstance(rest, tuple)
1274
+ if include_type:
1275
+ fields = [("event_type", "U256")]
1276
+ else:
1277
+ fields = []
1278
+ if beat_map is not None:
1279
+ # Preserve the order of the fields
1280
+ fields += [("onset_beat", "f4"), ("duration_beat", "f4")]
1281
+
1282
+ if quarter_map is not None:
1283
+ fields += [("onset_quarter", "f4"), ("duration_quarter", "f4")]
1284
+ fields += [
1285
+ ("onset_div", "i4"),
1286
+ ("duration_div", "i4"),
1287
+ ("pitch", "i4"),
1288
+ ("voice", "i4"),
1289
+ ("id", "U256"),
1290
+ ]
1291
+
1292
+ # fields for pitch spelling
1293
+ if include_pitch_spelling:
1294
+ fields += [("step", "U256"), ("alter", "i4"), ("octave", "i4")]
1295
+
1296
+ # fields for pitch spelling
1297
+ if include_grace_notes:
1298
+ fields += [("is_grace", "b"), ("grace_type", "U256")]
1299
+
1300
+ # fields for key signature
1301
+ if key_signature_map is not None:
1302
+ fields += [("ks_fifths", "i4"), ("ks_mode", "i4")]
1303
+
1304
+ # fields for time signature
1305
+ if time_signature_map is not None:
1306
+ fields += [("ts_beats", "i4"), ("ts_beat_type", "i4"), ("ts_mus_beats", "i4")]
1307
+
1308
+ # fields for metrical position
1309
+ if metrical_position_map is not None:
1310
+ fields += [
1311
+ ("is_downbeat", "i4"),
1312
+ ("rel_onset_div", "i4"),
1313
+ ("tot_measure_div", "i4"),
1314
+ ]
1315
+ # fields for staff
1316
+ if include_staff:
1317
+ fields += [("staff", "i4")]
1318
+
1319
+ # field for divs_pq
1320
+ if divs_per_quarter:
1321
+ fields += [("divs_pq", "i4")]
1322
+
1323
+ rest_array = []
1324
+ for rest in rest_list:
1325
+ rest_info = tuple()
1326
+ if has_type:
1327
+ rest, rest_type = rest
1328
+ if include_type:
1329
+ if not has_type:
1330
+ rest_type = rest.__class__.__name__
1331
+ rest_info += (rest_type,)
1332
+ rest_on_div = rest.start.t
1333
+ rest_off_div = rest.start.t + rest.duration_tied
1334
+ rest_dur_div = rest_off_div - rest_on_div
1335
+
1336
+ if beat_map is not None:
1337
+ note_on_beat, note_off_beat = beat_map([rest_on_div, rest_off_div])
1338
+ note_dur_beat = note_off_beat - note_on_beat
1339
+
1340
+ rest_info += (note_on_beat, note_dur_beat)
1341
+
1342
+ if quarter_map is not None:
1343
+ note_on_quarter, note_off_quarter = quarter_map([rest_on_div, rest_off_div])
1344
+ note_dur_quarter = note_off_quarter - note_on_quarter
1345
+
1346
+ rest_info += (note_on_quarter, note_dur_quarter)
1347
+
1348
+ rest_info += (
1349
+ rest_on_div,
1350
+ rest_dur_div,
1351
+ 0,
1352
+ rest.voice if rest.voice is not None else -1,
1353
+ rest.id,
1354
+ )
1355
+
1356
+ if include_pitch_spelling:
1357
+ step = 0
1358
+ alter = 0
1359
+ octave = 0
1360
+
1361
+ rest_info += (step, alter, octave)
1362
+
1363
+ if include_grace_notes:
1364
+ is_grace = hasattr(rest, "grace_type")
1365
+ if is_grace:
1366
+ grace_type = rest.grace_type
1367
+ else:
1368
+ grace_type = ""
1369
+ rest_info += (is_grace, grace_type)
1370
+
1371
+ if key_signature_map is not None:
1372
+ fifths, mode = key_signature_map(rest.start.t)
1373
+
1374
+ rest_info += (fifths, mode)
1375
+
1376
+ if time_signature_map is not None:
1377
+ beats, beat_type, mus_beats = time_signature_map(rest.start.t)
1378
+
1379
+ rest_info += (beats, beat_type, mus_beats)
1380
+
1381
+ if metrical_position_map is not None:
1382
+ rel_onset_div, tot_measure_div = metrical_position_map(rest.start.t)
1383
+
1384
+ is_downbeat = 1 if rel_onset_div == 0 else 0
1385
+
1386
+ rest_info += (is_downbeat, rel_onset_div, tot_measure_div)
1387
+
1388
+ if include_staff:
1389
+ rest_info += ((rest.staff if rest.staff else 0),)
1390
+
1391
+ if divs_per_quarter:
1392
+ rest_info += (divs_per_quarter,)
1393
+
1394
+ rest_array.append(rest_info)
1395
+
1396
+ rest_array = np.array(rest_array, dtype=fields)
1397
+
1398
+ # Sanitize voice information
1399
+ if rest_list:
1400
+ no_voice_idx = np.where(rest_array["voice"] == -1)[0]
1401
+ max_voice = rest_array["voice"].max()
1402
+ rest_array["voice"][no_voice_idx] = max_voice + 1
1403
+
1404
+ # sort by onset and pitch
1405
+ onset_unit, _ = get_time_units_from_note_array(rest_array)
1406
+ pitch_sort_idx = np.argsort(rest_array["pitch"])
1407
+ rest_array = rest_array[pitch_sort_idx]
1408
+ onset_sort_idx = np.argsort(rest_array[onset_unit], kind="mergesort")
1409
+ rest_array = rest_array[onset_sort_idx]
1410
+
1411
+ if collapse:
1412
+ rest_array = rec_collapse_rests(rest_array)
1413
+ return rest_array
1414
+
1415
+
1416
+ def get_starting_objects_from_point(pt: pts.TimePoint):
1417
+ """Helper that can be mapped on the part._points array"""
1418
+ return pt.starting_objects
1419
+
1420
+
1421
+ def get_event_dict(
1422
+ parts: (
1423
+ pts.Part | ptp.PerformedPart | Iterable[pts.Part] | Iterable[ptp.PerformedPart]
1424
+ ),
1425
+ ) -> dict[Type[pts.TimedObject], set[pts.TimedObject]]:
1426
+ """Iterates through all TimePoints in a part merging the respective {cls -> {instances}} dicts."""
1427
+ if isinstance(parts, (pts.Part, ptp.PerformedPart)):
1428
+ parts = [parts]
1429
+ sparts, pparts = [], []
1430
+ for p in parts:
1431
+ if isinstance(p, pts.Part):
1432
+ sparts.append(p)
1433
+ elif isinstance(p, ptp.PerformedPart):
1434
+ pparts.append(p)
1435
+ else:
1436
+ raise TypeError(f"Expected Part or PerformedPart, got {type(p)}")
1437
+ events = defaultdict(set)
1438
+ for pp in pparts:
1439
+ events[ptp.PerformedNote].update(pp.notes)
1440
+ if not sparts:
1441
+ return dict(events)
1442
+ points = np.concatenate([p._points for p in sparts])
1443
+ iter_all_event_dicts = map(get_starting_objects_from_point, points)
1444
+ for cls2instances in iter_all_event_dicts:
1445
+ for cls, instances in cls2instances.items():
1446
+ if instances:
1447
+ events[cls].update(instances)
1448
+ return dict(events)
1449
+
1450
+
1451
+ def get_performed_notes(
1452
+ parts: ptp.PerformedPart | Iterable[ptp.PerformedPart],
1453
+ ppq: int = 480,
1454
+ mpq: int = 500000,
1455
+ ) -> set[ptp.PerformedNote]:
1456
+ """Iterates through all PerformedNotes and adds timing information."""
1457
+ parts = make_argument_iterable(parts)
1458
+ pnotes = set()
1459
+ for p in parts:
1460
+ if not isinstance(p, ptp.PerformedPart):
1461
+ raise TypeError(f"Expected Part or PerformedPart, got {type(p)}")
1462
+ for pnote in p.notes:
1463
+ note_on_tick, duration_tick, note_on_sec, duration_sec = (
1464
+ get_pnote_durations(pnote, ppq, mpq)
1465
+ )
1466
+ pnote.pnote_dict.update(
1467
+ dict(
1468
+ onset_tick=note_on_tick,
1469
+ duration_tick=duration_tick,
1470
+ onset_sec=note_on_sec,
1471
+ duration_sec=duration_sec,
1472
+ )
1473
+ )
1474
+ pnotes.add(pnote)
1475
+ return pnotes
1476
+
1477
+
1478
+ def get_events_from_event_dict(
1479
+ event_dict: dict[Type[pts.TimedObject], set[pts.TimedObject]],
1480
+ included_classes: Optional[Iterable[Type[pts.TimedObject]]] = None,
1481
+ excluded_classes: Optional[Iterable[Type[pts.TimedObject]]] = None,
1482
+ include_type: bool = True,
1483
+ ) -> set[pts.TimedObject] | set[tuple[pts.TimedObject, str]]:
1484
+ event_set = set()
1485
+ if included_classes is None:
1486
+ selected_classes = list(event_dict.keys())
1487
+ else:
1488
+ if not isinstance(included_classes, Iterable):
1489
+ included_classes = [included_classes]
1490
+ selected_classes = [c for c in included_classes if c in event_dict]
1491
+ if excluded_classes is not None:
1492
+ if not isinstance(excluded_classes, Iterable):
1493
+ excluded_classes = [excluded_classes]
1494
+ for cls in excluded_classes:
1495
+ if cls in selected_classes:
1496
+ selected_classes.remove(cls)
1497
+ for cls in selected_classes:
1498
+ if include_type:
1499
+ c_str = cls.__name__
1500
+ event_set.update((e, c_str) for e in event_dict[cls])
1501
+ else:
1502
+ event_set.update(event_dict[cls])
1503
+ return event_set
1504
+
1505
+
1506
+ def get_filtered_event_set(
1507
+ part: pts.Part, include_type: bool = True
1508
+ ) -> set[pts.TimedObject]:
1509
+ """
1510
+ Equivalent to `[e for e in part.iter_all() if not isinstance(e, (pts.Rest, pts.Note))]`
1511
+ but roughly twice as fast"""
1512
+ event_dict = get_event_dict(part)
1513
+ excluded_classes = [pts.Rest, pts.Note] + list(iter_subclasses(pts.Note))
1514
+ return get_events_from_event_dict(
1515
+ event_dict, excluded_classes=excluded_classes, include_type=include_type
1516
+ )
1517
+
1518
+
1519
+ def event_array_from_part(
1520
+ part,
1521
+ event_list: Optional[list[pts.TimedObject]] = None,
1522
+ include_key_signature=False,
1523
+ include_time_signature=False,
1524
+ include_metrical_position=False,
1525
+ include_staff=False,
1526
+ include_divs_per_quarter=False,
1527
+ include_type=True,
1528
+ ):
1529
+ if include_time_signature:
1530
+ time_signature_map = part.time_signature_map
1531
+ else:
1532
+ time_signature_map = None
1533
+
1534
+ if include_key_signature:
1535
+ key_signature_map = part.key_signature_map
1536
+ else:
1537
+ key_signature_map = None
1538
+
1539
+ if include_metrical_position:
1540
+ metrical_position_map = part.metrical_position_map
1541
+ else:
1542
+ metrical_position_map = None
1543
+
1544
+ if include_divs_per_quarter:
1545
+ divs_per_quarter = get_divs_per_quarter(part)
1546
+ else:
1547
+ divs_per_quarter = None
1548
+
1549
+ if event_list is None:
1550
+ event_list = list(get_filtered_event_set(part))
1551
+ events_array = event_array_from_event_list(
1552
+ event_list=event_list,
1553
+ beat_map=part.beat_map,
1554
+ quarter_map=part.quarter_map,
1555
+ time_signature_map=time_signature_map,
1556
+ key_signature_map=key_signature_map,
1557
+ metrical_position_map=metrical_position_map,
1558
+ include_staff=include_staff,
1559
+ divs_per_quarter=divs_per_quarter,
1560
+ include_type=include_type,
1561
+ )
1562
+
1563
+ return events_array
1564
+
1565
+
1566
+ def event_array_from_event_list(
1567
+ event_list: Iterable[pts.TimedObject] | Iterable[tuple[pts.TimedObject, str]],
1568
+ beat_map=None,
1569
+ quarter_map=None,
1570
+ time_signature_map=None,
1571
+ key_signature_map=None,
1572
+ metrical_position_map=None,
1573
+ include_staff=False,
1574
+ divs_per_quarter=None,
1575
+ include_type=True,
1576
+ ):
1577
+ has_type = False
1578
+ for event in event_list:
1579
+ has_type = isinstance(event, tuple)
1580
+ if include_type:
1581
+ fields = [("event_type", "U256")]
1582
+ else:
1583
+ fields = []
1584
+ if beat_map is not None:
1585
+ # Preserve the order of the fields
1586
+ fields += [("onset_beat", "f4"), ("duration_beat", "f4")]
1587
+
1588
+ if quarter_map is not None:
1589
+ fields += [("onset_quarter", "f4"), ("duration_quarter", "f4")]
1590
+ fields += [
1591
+ ("onset_div", "i4"),
1592
+ ("duration_div", "i4"),
1593
+ ]
1594
+
1595
+ # fields for key signature
1596
+ if key_signature_map is not None:
1597
+ fields += [("ks_fifths", "i4"), ("ks_mode", "i4")]
1598
+
1599
+ # fields for time signature
1600
+ if time_signature_map is not None:
1601
+ fields += [("ts_beats", "i4"), ("ts_beat_type", "i4"), ("ts_mus_beats", "i4")]
1602
+
1603
+ # fields for metrical position
1604
+ if metrical_position_map is not None:
1605
+ fields += [
1606
+ ("is_downbeat", "i4"),
1607
+ ("rel_onset_div", "i4"),
1608
+ ("tot_measure_div", "i4"),
1609
+ ]
1610
+ # field for staff
1611
+ if include_staff:
1612
+ fields += [("staff", "i4")]
1613
+
1614
+ # field for divs_pq
1615
+ if divs_per_quarter:
1616
+ fields += [("divs_pq", "i4")]
1617
+
1618
+ event_array = []
1619
+ for timed_object in event_list:
1620
+ event_info = tuple()
1621
+ if has_type:
1622
+ timed_object, event_type = timed_object
1623
+ if include_type:
1624
+ if not has_type:
1625
+ event_type = timed_object.__class__.__name__
1626
+ event_info += (event_type,)
1627
+ note_on_div = timed_object.start.t
1628
+ note_off_div = -1 if timed_object.end is None else timed_object.end.t
1629
+ note_dur_div = -1 if timed_object.duration is None else timed_object.duration
1630
+
1631
+ if beat_map is not None:
1632
+ note_on_beat, note_off_beat = beat_map([note_on_div, note_off_div])
1633
+ note_dur_beat = note_off_beat - note_on_beat
1634
+
1635
+ event_info += (note_on_beat, note_dur_beat)
1636
+
1637
+ if quarter_map is not None:
1638
+ note_on_quarter, note_off_quarter = quarter_map([note_on_div, note_off_div])
1639
+ note_dur_quarter = note_off_quarter - note_on_quarter
1640
+
1641
+ event_info += (note_on_quarter, note_dur_quarter)
1642
+
1643
+ event_info += (
1644
+ note_on_div,
1645
+ note_dur_div,
1646
+ )
1647
+
1648
+ if key_signature_map is not None:
1649
+ fifths, mode = key_signature_map(note_on_div)
1650
+
1651
+ event_info += (fifths, mode)
1652
+
1653
+ if time_signature_map is not None:
1654
+ beats, beat_type, mus_beats = time_signature_map(note_on_div)
1655
+
1656
+ event_info += (beats, beat_type, mus_beats)
1657
+
1658
+ if metrical_position_map is not None:
1659
+ rel_onset_div, tot_measure_div = metrical_position_map(note_on_div)
1660
+
1661
+ is_downbeat = 1 if rel_onset_div == 0 else 0
1662
+
1663
+ event_info += (is_downbeat, rel_onset_div, tot_measure_div)
1664
+
1665
+ if include_staff:
1666
+ if hasattr(timed_object, "staff") and timed_object.staff:
1667
+ event_info += (timed_object.staff,)
1668
+ else:
1669
+ event_info += (0,)
1670
+
1671
+ if divs_per_quarter:
1672
+ event_info += (divs_per_quarter,)
1673
+
1674
+ event_array.append(event_info)
1675
+
1676
+ event_array = np.array(event_array, dtype=fields)
1677
+
1678
+ # sort by onset
1679
+ onset_unit, _ = get_time_units_from_note_array(event_array)
1680
+ onset_sort_idx = np.argsort(event_array[onset_unit], kind="mergesort")
1681
+ event_array = event_array[onset_sort_idx]
1682
+
1683
+ return event_array
1684
+
1685
+
1686
+ def event_df_from_part(
1687
+ part: pts.Part,
1688
+ event_dict: Optional[dict[Type[pts.TimedObject], list[pts.TimedObject]]] = None,
1689
+ note_array=True,
1690
+ rest_array=True,
1691
+ event_array=True,
1692
+ include_pitch_spelling=False,
1693
+ include_key_signature=False,
1694
+ include_time_signature=False,
1695
+ include_metrical_position=False,
1696
+ include_grace_notes=False,
1697
+ include_staff=False,
1698
+ include_divs_per_quarter=False,
1699
+ merge_tied_notes=True,
1700
+ collapse_rests=True,
1701
+ include_type=True,
1702
+ meta: Optional[Meta | dict] = None,
1703
+ ) -> DF:
1704
+ n_arrays = sum([note_array, rest_array, event_array])
1705
+ if not n_arrays:
1706
+ raise ValueError(
1707
+ "At least one of note_array, rest_array or event_array must be True"
1708
+ )
1709
+ if meta is None:
1710
+ meta = {}
1711
+ if include_time_signature:
1712
+ time_signature_map = part.time_signature_map
1713
+ else:
1714
+ time_signature_map = None
1715
+
1716
+ if include_key_signature:
1717
+ key_signature_map = part.key_signature_map
1718
+ else:
1719
+ key_signature_map = None
1720
+
1721
+ if include_metrical_position:
1722
+ metrical_position_map = part.metrical_position_map
1723
+ else:
1724
+ metrical_position_map = None
1725
+
1726
+ div_pq = get_divs_per_quarter(part)
1727
+ meta["ticks_per_quarter"] = div_pq
1728
+ divs_per_quarter = div_pq if include_divs_per_quarter else None
1729
+
1730
+ if event_dict is None:
1731
+ event_dict = get_event_dict(part)
1732
+
1733
+ if isinstance(part, ptp.PerformedPart):
1734
+ note_array = note_array_from_performed_note_list(
1735
+ event_dict.get(ptp.PerformedNote, [])
1736
+ )
1737
+ return DF(note_array, meta=meta)
1738
+
1739
+ note_classes = [pts.Note] + list(iter_subclasses(pts.Note))
1740
+ rest_classes = [pts.Rest]
1741
+
1742
+ dfs = {}
1743
+ if note_array:
1744
+ note_set = get_events_from_event_dict(
1745
+ event_dict, note_classes, include_type=include_type
1746
+ )
1747
+ arr = note_array_from_note_list(
1748
+ note_list=note_set,
1749
+ beat_map=part.beat_map,
1750
+ quarter_map=part.quarter_map,
1751
+ time_signature_map=time_signature_map,
1752
+ key_signature_map=key_signature_map,
1753
+ metrical_position_map=metrical_position_map,
1754
+ include_pitch_spelling=include_pitch_spelling,
1755
+ include_grace_notes=include_grace_notes,
1756
+ include_staff=include_staff,
1757
+ divs_per_quarter=divs_per_quarter,
1758
+ include_type=include_type,
1759
+ merge_tied_notes=merge_tied_notes,
1760
+ )
1761
+ dfs["note"] = pd.DataFrame(arr)
1762
+ if rest_array:
1763
+ rest_set = get_events_from_event_dict(
1764
+ event_dict, rest_classes, include_type=include_type
1765
+ )
1766
+ arr = rest_array_from_rest_list(
1767
+ rest_list=rest_set,
1768
+ beat_map=part.beat_map,
1769
+ quarter_map=part.quarter_map,
1770
+ time_signature_map=time_signature_map,
1771
+ key_signature_map=key_signature_map,
1772
+ metrical_position_map=metrical_position_map,
1773
+ include_pitch_spelling=include_pitch_spelling,
1774
+ include_grace_notes=include_grace_notes,
1775
+ include_staff=include_staff,
1776
+ divs_per_quarter=divs_per_quarter,
1777
+ collapse=collapse_rests,
1778
+ include_type=include_type,
1779
+ )
1780
+ dfs["rest"] = pd.DataFrame(arr)
1781
+ if event_array:
1782
+ event_set = get_events_from_event_dict(
1783
+ event_dict,
1784
+ excluded_classes=note_classes + rest_classes,
1785
+ include_type=include_type,
1786
+ )
1787
+ arr = event_array_from_event_list(
1788
+ event_list=event_set,
1789
+ beat_map=part.beat_map,
1790
+ quarter_map=part.quarter_map,
1791
+ time_signature_map=time_signature_map,
1792
+ key_signature_map=key_signature_map,
1793
+ metrical_position_map=metrical_position_map,
1794
+ include_staff=include_staff,
1795
+ divs_per_quarter=divs_per_quarter,
1796
+ include_type=include_type,
1797
+ )
1798
+ dfs["other"] = pd.DataFrame(arr)
1799
+ if n_arrays == 1:
1800
+ return list(dfs.values())[0]
1801
+ dfs = {k: v for k, v in dfs.items() if not v.empty}
1802
+ if len(dfs) < 2:
1803
+ return list(dfs.values())[0] # return the first of the empty frames
1804
+ df = pd_concat(dfs, names=["event_category", "ix"], meta=meta).sort_values(
1805
+ "onset_div"
1806
+ )
1807
+ df = df.reset_index(level=0, drop=False)
1808
+ df = df.reset_index(level=0, drop=True)
1809
+ return df
1810
+
1811
+
1812
+ def event_df_from_part_list(
1813
+ parts: list[pts.Part | ptp.PerformedPart],
1814
+ note_array=True,
1815
+ rest_array=True,
1816
+ event_array=True,
1817
+ include_pitch_spelling=False,
1818
+ include_key_signature=False,
1819
+ include_time_signature=False,
1820
+ include_metrical_position=False,
1821
+ include_grace_notes=False,
1822
+ include_staff=False,
1823
+ include_divs_per_quarter=False,
1824
+ merge_tied_notes=True,
1825
+ collapse_rests=True,
1826
+ include_type=True,
1827
+ meta: Optional[Meta | dict] = None,
1828
+ ) -> DF:
1829
+ parts = make_argument_iterable(parts)
1830
+ if not parts:
1831
+ raise ValueError("No parts were given.")
1832
+ div_pq_values = {get_divs_per_quarter(p) for p in parts}
1833
+ if len(div_pq_values) != 1:
1834
+ raise ValueError(
1835
+ f"All parts must have the same divs_per_quarter value, got {div_pq_values}"
1836
+ )
1837
+ # div_pq = div_pq_values.pop()
1838
+ # meta["ticks_per_quarter"] = div_pq
1839
+ dfs = []
1840
+ for part in parts:
1841
+ dfs.append(
1842
+ event_df_from_part(
1843
+ part=part,
1844
+ note_array=note_array,
1845
+ rest_array=rest_array,
1846
+ event_array=event_array,
1847
+ include_pitch_spelling=include_pitch_spelling,
1848
+ include_key_signature=include_key_signature,
1849
+ include_time_signature=include_time_signature,
1850
+ include_metrical_position=include_metrical_position,
1851
+ include_grace_notes=include_grace_notes,
1852
+ include_staff=include_staff,
1853
+ include_divs_per_quarter=include_divs_per_quarter,
1854
+ merge_tied_notes=merge_tied_notes,
1855
+ collapse_rests=collapse_rests,
1856
+ include_type=include_type,
1857
+ meta=meta,
1858
+ )
1859
+ )
1860
+ if len(dfs) == 1:
1861
+ return dfs[0]
1862
+ df = pd_concat(dfs, meta=meta).sort_values("onset_div")
1863
+ return df
1864
+
1865
+
1866
+ def get_divs_per_quarter(part: pts.Part | ptp.PerformedPart):
1867
+ """Get the number of divisions per quarter note for a part.
1868
+ Raises NotImplementedError if the part has multiple quarter_durations.
1869
+ This will not happen for a PerformedPart because it is always a unique value.
1870
+ """
1871
+ try:
1872
+ parts_quarter_times = part._quarter_times
1873
+ except AttributeError:
1874
+ return part.ppq
1875
+ parts_quarter_durations = part._quarter_durations
1876
+ if not len(parts_quarter_durations) == 1:
1877
+ raise NotImplementedError(
1878
+ "Parts changing their quarter_durations midway are currently not supported. Found quarter_durations",
1879
+ parts_quarter_durations,
1880
+ "at times",
1881
+ parts_quarter_times,
1882
+ )
1883
+ divs_per_quarter = parts_quarter_durations[0]
1884
+ return divs_per_quarter
1885
+
1886
+
1887
+ # endregion parsing with partitura
1888
+
1889
+ # region JSON formats
1890
+
1891
+
1892
+ @overload
1893
+ def explode_nested_json(
1894
+ first_level_df: DF,
1895
+ column2type: dict[str, str] | str | list,
1896
+ type_column_name: Optional[str] = None,
1897
+ prepend_column_names: bool = True,
1898
+ **kwargs,
1899
+ ) -> DF: ...
1900
+
1901
+
1902
+ @overload
1903
+ def explode_nested_json(
1904
+ first_level_df: pd.DataFrame,
1905
+ column2type: dict[str, str] | str | list,
1906
+ type_column_name: Optional[str] = None,
1907
+ prepend_column_names: bool = True,
1908
+ **kwargs,
1909
+ ) -> pd.DataFrame: ...
1910
+
1911
+
1912
+ def explode_nested_json(
1913
+ first_level_df: DF | pd.DataFrame,
1914
+ column2type: dict[str, str] | str | list,
1915
+ type_column_name: Optional[str] = None,
1916
+ prepend_column_names: bool = True,
1917
+ **kwargs,
1918
+ ) -> DF | pd.DataFrame:
1919
+ """Explodes columns containing JSON arrays, joins them onto the other existing columns,
1920
+ and concatenates the resulting dataframes.
1921
+
1922
+ Args:
1923
+ first_level_df:
1924
+ A DataFrame resulting from pd.json_normalize which has at least one column containing JSON arrays.
1925
+ The index needs to be unique.
1926
+ column2type:
1927
+ A mapping of an arbitrary type name to the column to be exploded using pd.json_normalize.
1928
+ The type names are only relevant when type_column_name is specified, which is usually a good idea
1929
+ when exploding multiple columns.
1930
+ Otherwise, this argument can simply be a column name or a list of them.
1931
+ type_column_name:
1932
+ Name of the indicator column that specifies from which exploded column a record comes.
1933
+ prepend_column_names:
1934
+ By default, the column name is prepended to the new columns resulting from the explosion.
1935
+ Pass False to prevent that.
1936
+ kwargs:
1937
+ Keyword arguments passed to pd.json_normalize when exploding any array.
1938
+
1939
+ Returns:
1940
+ A Dataframe with one row per item in the JSON arrays in the specified columns.
1941
+ If type_column_name is specified, it comes with an indicator column of that name.
1942
+ """
1943
+ drop_type_column = not bool(type_column_name)
1944
+ if isinstance(column2type, str):
1945
+ column2type = {column2type: column2type}
1946
+ elif isinstance(column2type, list):
1947
+ column2type = {name: name for name in column2type}
1948
+ assert all(
1949
+ col in first_level_df.columns for col in column2type.keys()
1950
+ ), f"Not all columns specified in type2column are present: {first_level_df.columns}"
1951
+ assert (
1952
+ first_level_df.index.is_unique
1953
+ ), "Dataframe index needs to be unique. Please de-duplicate/reset."
1954
+
1955
+ def explode_arrays(df: pd.DataFrame) -> Optional[pd.DataFrame]:
1956
+ """Takes a single-row DataFrame and json_normalizes the specified columns,
1957
+ returning a DataFrame that has one row per item per JSON array.
1958
+ """
1959
+ dfs = {}
1960
+ row = df.iloc[0]
1961
+ for column, event_type in column2type.items():
1962
+ try:
1963
+ exploded_array = pd.json_normalize(row[column], **kwargs)
1964
+ if prepend_column_names:
1965
+ exploded_array.columns = [
1966
+ f"{column}.{col}" for col in exploded_array
1967
+ ]
1968
+ dfs[event_type] = exploded_array
1969
+ except Exception:
1970
+ continue
1971
+ if not dfs:
1972
+ return
1973
+ names = [type_column_name] if type_column_name else None
1974
+ return pd.concat(dfs, names=names).droplevel(-1)
1975
+
1976
+ second_level_df = (
1977
+ first_level_df.groupby(level=0)
1978
+ .apply(explode_arrays)
1979
+ .reset_index(level=-1, drop=drop_type_column)
1980
+ )
1981
+ result = (
1982
+ first_level_df.drop(columns=column2type.keys())
1983
+ .join(second_level_df, rsuffix="_exploded")
1984
+ .reset_index(drop=True)
1985
+ )
1986
+ if isinstance(first_level_df, DF):
1987
+ return DF(result, meta=first_level_df.get_meta())
1988
+ return result
1989
+
1990
+
1991
+ def explode_multiply_nested_arrays(
1992
+ df: DF | pd.DataFrame,
1993
+ explosion_path: Collection[str | Collection[str]],
1994
+ type_column_name: Optional[str] = None,
1995
+ prepend_column_names: bool = True,
1996
+ **kwargs,
1997
+ ) -> DF | pd.DataFrame:
1998
+ """Like explode_nested_json(), but allows for "explosion_paths", meaning that this function
1999
+ takes a list where for each string element it behaves exactly like explode_nested_json(), but
2000
+ for each list of strings, explode_nested_json() is called in a loop. In other words:
2001
+
2002
+ explode_multiply_nested_arrays(df, [["a", "b", "c"], "d", ["e", "f"]]) is equivalent to:
2003
+ explode_nested_json(
2004
+ explode_nested_json(
2005
+ explode_nested_json(df, ["a", "d", "e"]),
2006
+ ["b", "f"]
2007
+ ), "c"
2008
+ )
2009
+
2010
+ When prepend_column_names is True, paths look like this: ["measure", "measure.voice"] because
2011
+ the nested arrays are children of previously exploded arrays.
2012
+ """
2013
+ paths_to_explode = [
2014
+ [path] if isinstance(path, str) else path for path in explosion_path
2015
+ ]
2016
+ assert all(
2017
+ path[0] in df.columns for path in paths_to_explode
2018
+ ), f"Not all columns specified in explosion_path {explosion_path} are present: {df.columns}"
2019
+ exploded_df = df
2020
+ for columns_at_level in zip_longest(*paths_to_explode):
2021
+ cols = [col for col in columns_at_level if col]
2022
+ n_items = len(cols)
2023
+ n_paths = n_items - sum(isinstance(col, str) for col in cols)
2024
+ if n_paths > 1 and n_items > 1:
2025
+ # there are more paths to resolve at this level
2026
+ exploded_df = explode_multiply_nested_arrays(
2027
+ exploded_df,
2028
+ explosion_path=cols,
2029
+ type_column_name=type_column_name,
2030
+ prepend_column_names=prepend_column_names,
2031
+ **kwargs,
2032
+ )
2033
+ else:
2034
+ if n_paths == 1 and n_items == 1:
2035
+ cols = cols[0]
2036
+ if type_column_name is None:
2037
+ type_column_name = "item_type" if len(cols) > 1 else None
2038
+ exploded_df = explode_nested_json(
2039
+ exploded_df,
2040
+ column2type=cols,
2041
+ type_column_name=type_column_name,
2042
+ **kwargs,
2043
+ )
2044
+ return exploded_df
2045
+
2046
+
2047
+ def recursively_explode_all_arrays(df):
2048
+ """Recursively calls explode_multiply_nested_arrays() on the dataframe as long as it still contains columns where
2049
+ the first element is a list.
2050
+ """
2051
+ column_has_arrays = df.bfill().iloc[0].map(type) == list
2052
+ if column_has_arrays.any():
2053
+ array_cols = df.columns[column_has_arrays].tolist()
2054
+ result = explode_multiply_nested_arrays(df, [array_cols]).dropna(
2055
+ axis=1, how="all"
2056
+ )
2057
+ return recursively_explode_all_arrays(result)
2058
+ return df
2059
+
2060
+
2061
+ def turn_json_into_dfs(
2062
+ json_like: dict | list,
2063
+ metadata: Optional[dict] = None,
2064
+ prefix: str = "",
2065
+ remove_keys: Optional[Collection[str]] = None,
2066
+ ) -> Dict[str, DF | dict]:
2067
+ """Recurses through the JSON structure, collecting literal values as metadata,
2068
+ and turning each array into a DF, that is a DataFrame with a ._meta property.
2069
+ Nodes that do not contain any array are returned as simple dict.
2070
+ """
2071
+
2072
+ def parse_array(
2073
+ json_array: list,
2074
+ metadata: Meta | dict,
2075
+ xml_node: str,
2076
+ ) -> DF:
2077
+ df = pd.json_normalize(json_array)
2078
+ return DF(df, meta=Meta(metadata, xml_node=xml_node))
2079
+
2080
+ result = {}
2081
+ if metadata:
2082
+ metadata = Meta(metadata)
2083
+ else:
2084
+ metadata = Meta()
2085
+ if not remove_keys:
2086
+ remove_keys = []
2087
+ elif isinstance(remove_keys, str):
2088
+ remove_keys = [remove_keys]
2089
+ if isinstance(json_like, list):
2090
+ return {prefix: parse_array(json_like, metadata=metadata, xml_node=prefix)}
2091
+ children: Dict[str, Tuple[list | dict, str]] = {}
2092
+ for key, value in json_like.items():
2093
+ # collect metadata first in order to pass it to the lower levels
2094
+ if not value or key in remove_keys:
2095
+ continue
2096
+ if isinstance(value, dict):
2097
+ children[key] = (value, "dict")
2098
+ elif isinstance(value, list):
2099
+ children[key] = (value, "list")
2100
+ else:
2101
+ record_prefix = f"{prefix}.{key}" if prefix else key
2102
+ metadata[record_prefix] = value
2103
+ for key, (value, value_type) in children.items():
2104
+ record_prefix = f"{prefix}.{key}" if prefix else key
2105
+ if value_type == "list":
2106
+ result[record_prefix] = parse_array(
2107
+ value, metadata=metadata, xml_node=record_prefix
2108
+ )
2109
+ else:
2110
+ result.update(
2111
+ turn_json_into_dfs(
2112
+ value,
2113
+ metadata=metadata,
2114
+ prefix=record_prefix,
2115
+ remove_keys=remove_keys,
2116
+ )
2117
+ )
2118
+ if not result and metadata:
2119
+ return {prefix: metadata}
2120
+ return result
2121
+
2122
+
2123
+ def explode_all_array_columns(
2124
+ df: DF,
2125
+ type_column_name: Optional[str] = None,
2126
+ prepend_column_names: bool = True,
2127
+ exclude_columns: Optional[Collection[str]] = None,
2128
+ ) -> DF:
2129
+ """type_column_name = None (default) means automatic behaviour:
2130
+ add "item_type" column only when more than one array columns are exploded and have to
2131
+ to be distinguished.
2132
+ """
2133
+ if not exclude_columns:
2134
+ exclude_columns = []
2135
+ array_cols = [
2136
+ column
2137
+ for column, value in df.iloc[0].items()
2138
+ if isinstance(value, list) and column not in exclude_columns
2139
+ ]
2140
+ if not array_cols:
2141
+ return df
2142
+ if type_column_name is None:
2143
+ type_column_name = "item_type" if len(array_cols) > 1 else None
2144
+ return explode_nested_json(
2145
+ df,
2146
+ column2type=array_cols,
2147
+ type_column_name=type_column_name,
2148
+ prepend_column_names=prepend_column_names,
2149
+ )
2150
+
2151
+
2152
+ def fully_explode_json(
2153
+ json_like: dict | list,
2154
+ remove_keys: Optional[Collection[str]] = None,
2155
+ exclude_columns: Optional[Collection[str]] = None,
2156
+ ) -> Dict[str, DF | dict]:
2157
+ """Like turn_json_into_dfs() but in addition, this function explodes columns
2158
+ containing JSON arrays.
2159
+ """
2160
+ dfs = turn_json_into_dfs(json_like, remove_keys=remove_keys)
2161
+ result = {}
2162
+ for key, df in dfs.items():
2163
+ if isinstance(df, dict):
2164
+ result[key] = df
2165
+ else:
2166
+ result[key] = explode_all_array_columns(df, exclude_columns=exclude_columns)
2167
+ return result
2168
+
2169
+
2170
+ def partially_explode_json(
2171
+ json_like: dict | list,
2172
+ explosion_paths: Collection[str | Collection[str]],
2173
+ remove_keys: Optional[Collection[str]] = None,
2174
+ ) -> Dict[str, DF | dict]:
2175
+ """Like turn_json_into_dfs() but in addition, this function explodes selected columns
2176
+ containing JSON arrays.
2177
+ """
2178
+ columns_to_explode = [
2179
+ [path] if isinstance(path, str) else path for path in explosion_paths
2180
+ ]
2181
+ dfs = turn_json_into_dfs(json_like, remove_keys=remove_keys)
2182
+ result = {}
2183
+ for key, df in dfs.items():
2184
+ if isinstance(df, dict):
2185
+ result[key] = df
2186
+ continue
2187
+ explosion_path = [path for path in columns_to_explode if path[0] in df.columns]
2188
+ if not explosion_path:
2189
+ result[key] = df
2190
+ continue
2191
+ exploded_df = explode_multiply_nested_arrays(df, explosion_path)
2192
+ result[key] = exploded_df
2193
+ return result
2194
+
2195
+
2196
+ def load_json_file(filepath: str) -> dict | list:
2197
+ with open(filepath, encoding="utf-8") as f:
2198
+ d = json.load(f)
2199
+ return d
2200
+
2201
+
2202
+ TILIA_COLUMN_DTYPES = dict(
2203
+ time=float,
2204
+ level="Int64",
2205
+ measure="Int64",
2206
+ beat="Int64",
2207
+ start_beat="Int64",
2208
+ end_beat="Int64",
2209
+ start_measure="Int64",
2210
+ end_measure="Int64",
2211
+ page_number="Int64",
2212
+ label="string",
2213
+ comments="string",
2214
+ )
2215
+
2216
+
2217
+ def update_dtypes(df: pd.DataFrame) -> pd.DataFrame:
2218
+ dtypes = {
2219
+ col: dtype for col, dtype in TILIA_COLUMN_DTYPES.items() if col in df.columns
2220
+ }
2221
+ df = df.astype(dtypes)
2222
+ return df
2223
+
2224
+
2225
+ def parse_tilia_json(filepath) -> DF:
2226
+ assert not filepath.endswith(
2227
+ ".tla"
2228
+ ), "This function parses JSON exported from TiLiA, not .tla files!"
2229
+ tla_json = load_json_file(filepath)
2230
+ df = fully_explode_json(tla_json)["timelines"]
2231
+ df = df.rename(columns={"kind": "timeline", "components.kind": "component"})
2232
+ df = df.rename(columns=lambda c: c.replace("components.", ""))
2233
+ return update_dtypes(df)
2234
+
2235
+
2236
+ # endregion JSON formats