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.
- timetoalign-0.0.1.dist-info/METADATA +11 -0
- timetoalign-0.0.1.dist-info/RECORD +13 -0
- timetoalign-0.0.1.dist-info/WHEEL +5 -0
- timetoalign-0.0.1.dist-info/top_level.txt +1 -0
- tta/__init__.py +1 -0
- tta/common.py +58 -0
- tta/conversions.py +3777 -0
- tta/events.py +1527 -0
- tta/parsing.py +2236 -0
- tta/registry.py +546 -0
- tta/representations.py +826 -0
- tta/timelines.py +2192 -0
- tta/utils.py +963 -0
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
|