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/events.py
ADDED
|
@@ -0,0 +1,1527 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from functools import cache, partial
|
|
6
|
+
from numbers import Number
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from pprint import pformat
|
|
9
|
+
from typing import (
|
|
10
|
+
TYPE_CHECKING,
|
|
11
|
+
Any,
|
|
12
|
+
Callable,
|
|
13
|
+
Generic,
|
|
14
|
+
Iterable,
|
|
15
|
+
Iterator,
|
|
16
|
+
Literal,
|
|
17
|
+
Optional,
|
|
18
|
+
Protocol,
|
|
19
|
+
Self,
|
|
20
|
+
Type,
|
|
21
|
+
TypeAlias,
|
|
22
|
+
TypeVar,
|
|
23
|
+
Union,
|
|
24
|
+
overload,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
import librosa
|
|
28
|
+
import numpy as np
|
|
29
|
+
import pandas as pd
|
|
30
|
+
from partitura import performance as ptp
|
|
31
|
+
from partitura import score as pts
|
|
32
|
+
from partitura.utils.generic import interp1d as pt_interpolator
|
|
33
|
+
from scipy.interpolate import interp1d as sp_interpolator
|
|
34
|
+
|
|
35
|
+
from processing.metaframes import DF, Meta
|
|
36
|
+
from processing.tta import utils
|
|
37
|
+
from processing.tta.common import RegisteredObject
|
|
38
|
+
from processing.tta.conversions import (
|
|
39
|
+
ConversionMap,
|
|
40
|
+
Coordinate,
|
|
41
|
+
FixedCoordinateTypeObject,
|
|
42
|
+
RegionMap,
|
|
43
|
+
SamplesToSeconds,
|
|
44
|
+
make_coordinate,
|
|
45
|
+
ticks2seconds_map_from_midi_df,
|
|
46
|
+
)
|
|
47
|
+
from processing.tta.parsing import (
|
|
48
|
+
event_df_from_part_list,
|
|
49
|
+
get_divs_per_quarter,
|
|
50
|
+
get_event_dict,
|
|
51
|
+
get_performed_notes,
|
|
52
|
+
load_json_file,
|
|
53
|
+
midi_to_df,
|
|
54
|
+
parse_tilia_json,
|
|
55
|
+
)
|
|
56
|
+
from processing.tta.registry import (
|
|
57
|
+
flyweight,
|
|
58
|
+
get_class,
|
|
59
|
+
get_registry_by_prefix,
|
|
60
|
+
iter_objects_by_ids,
|
|
61
|
+
)
|
|
62
|
+
from processing.tta.utils import Missing, NumberType, PartMap, TimeUnit
|
|
63
|
+
|
|
64
|
+
if TYPE_CHECKING:
|
|
65
|
+
from processing.tta.timelines import Timeline
|
|
66
|
+
|
|
67
|
+
ST = TypeVar("ST") # type of silo data
|
|
68
|
+
ET = TypeVar("ET") # type of event data
|
|
69
|
+
Interpolator: TypeAlias = Union[pt_interpolator, sp_interpolator]
|
|
70
|
+
|
|
71
|
+
# region Event types
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_event_properties(
|
|
75
|
+
ids: str | Iterable[str],
|
|
76
|
+
include_shared=False,
|
|
77
|
+
include_individual=False,
|
|
78
|
+
exclude_properties: Optional[tuple[str]] = ("data",),
|
|
79
|
+
skip_missing=True,
|
|
80
|
+
**column2field,
|
|
81
|
+
):
|
|
82
|
+
idx = pd.Index(utils.make_argument_iterable(ids))
|
|
83
|
+
id_is_duplicate = idx.duplicated()
|
|
84
|
+
if id_is_duplicate.any():
|
|
85
|
+
event_ids = idx.unique()
|
|
86
|
+
else:
|
|
87
|
+
event_ids = idx
|
|
88
|
+
event_records = []
|
|
89
|
+
for evt in iter_objects_by_ids(*event_ids):
|
|
90
|
+
try:
|
|
91
|
+
event_info = evt.get_property_values(
|
|
92
|
+
include_shared=include_shared,
|
|
93
|
+
include_individual=include_individual,
|
|
94
|
+
exclude_properties=exclude_properties,
|
|
95
|
+
**column2field,
|
|
96
|
+
)
|
|
97
|
+
if not include_individual:
|
|
98
|
+
event_info["id"] = evt.id
|
|
99
|
+
except AttributeError:
|
|
100
|
+
if skip_missing:
|
|
101
|
+
continue
|
|
102
|
+
else:
|
|
103
|
+
raise
|
|
104
|
+
event_records.append(event_info)
|
|
105
|
+
events = pd.DataFrame.from_records(event_records).set_index("id")
|
|
106
|
+
return events.reindex(idx).reset_index()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@flyweight()
|
|
110
|
+
class Event(FixedCoordinateTypeObject[ET]):
|
|
111
|
+
"""
|
|
112
|
+
The base types are initialized with a dict-like or any other object that can be indexed:
|
|
113
|
+
Event.get(x) is equivalent to Event._data[x]. Subclasses may be adapted for other data types
|
|
114
|
+
by overriding :meth:`get_accessor` and/or :meth:`get`.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
_property_defaults = None
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
unit: TimeUnit | str,
|
|
122
|
+
number_type: NumberType,
|
|
123
|
+
data: Optional[ET] = None,
|
|
124
|
+
id_prefix: str = "ev",
|
|
125
|
+
uid: Optional[str] = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
super().__init__(
|
|
128
|
+
unit=unit, number_type=number_type, id_prefix=id_prefix, uid=uid
|
|
129
|
+
)
|
|
130
|
+
self._data = None
|
|
131
|
+
self.data = data
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def data(self):
|
|
135
|
+
return self._data
|
|
136
|
+
|
|
137
|
+
@data.setter
|
|
138
|
+
def data(self, data: Any):
|
|
139
|
+
self._data = data
|
|
140
|
+
|
|
141
|
+
def assert_property(self, name):
|
|
142
|
+
assert getattr(self, name, None) is not None, (
|
|
143
|
+
f"No {name!r} has been defined for {self.class_name} " f"{self.id!r}."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def validate(self) -> None:
|
|
147
|
+
assert self.data is not None, f"{self.class_name} does not point to any data."
|
|
148
|
+
|
|
149
|
+
def get_accessor(self) -> Callable:
|
|
150
|
+
return self._data.__getitem__
|
|
151
|
+
|
|
152
|
+
def get(self, field: str) -> Literal:
|
|
153
|
+
"""Get property value from the event_data."""
|
|
154
|
+
if field is None:
|
|
155
|
+
return Missing.FIELD_NAME_UNDEFINED
|
|
156
|
+
if not self._data:
|
|
157
|
+
return Missing.DATA_UNDEFINED
|
|
158
|
+
accessor = self.get_accessor()
|
|
159
|
+
try:
|
|
160
|
+
return accessor(field)
|
|
161
|
+
except (KeyError, AttributeError):
|
|
162
|
+
return Missing.FIELD_MISSING_FROM_EVENT_DATA
|
|
163
|
+
|
|
164
|
+
def get_shared_property_value(self, field_name):
|
|
165
|
+
return self._shared[field_name]
|
|
166
|
+
|
|
167
|
+
def __getattr__(self, name: str):
|
|
168
|
+
"""Returning plain values for flyweights for which no @property has been defined."""
|
|
169
|
+
if name in self._shared:
|
|
170
|
+
return self.get_shared_property_value(name)
|
|
171
|
+
raise AttributeError(f"{self.class_name} has no {name!r} property.")
|
|
172
|
+
|
|
173
|
+
@cache
|
|
174
|
+
def get_property(self, name: str):
|
|
175
|
+
field_name = self.get_shared_property_value(name)
|
|
176
|
+
if field_name is None:
|
|
177
|
+
return Missing.FIELD_NAME_UNDEFINED
|
|
178
|
+
return self.get(field_name)
|
|
179
|
+
|
|
180
|
+
def __repr__(self):
|
|
181
|
+
properties = self.get_property_values(
|
|
182
|
+
include_shared=True
|
|
183
|
+
) # method injected by @flyweight
|
|
184
|
+
prop_str = ", ".join([f"{k}={v!r}" for k, v in properties.items()])
|
|
185
|
+
return f"{self.class_name}(id={self.id!r}, {prop_str})"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@flyweight()
|
|
189
|
+
class InstantEvent(Event[ET]):
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def from_event_data(cls, data: ET, **kwargs) -> Self:
|
|
193
|
+
return cls(instant=data.get("instant"), data=data, **kwargs)
|
|
194
|
+
|
|
195
|
+
def __init__(
|
|
196
|
+
self,
|
|
197
|
+
instant: Coordinate | Number,
|
|
198
|
+
unit: Optional[TimeUnit | str] = None,
|
|
199
|
+
number_type: Optional[NumberType] = None,
|
|
200
|
+
data: Optional[ET] = None,
|
|
201
|
+
id_prefix: str = "ev",
|
|
202
|
+
uid: Optional[str] = None,
|
|
203
|
+
):
|
|
204
|
+
instant = make_coordinate(
|
|
205
|
+
value=instant,
|
|
206
|
+
unit=unit,
|
|
207
|
+
number_type=number_type,
|
|
208
|
+
default_unit=self._default_unit,
|
|
209
|
+
default_number_type=self._default_number_type,
|
|
210
|
+
)
|
|
211
|
+
super().__init__(
|
|
212
|
+
unit=instant.unit,
|
|
213
|
+
number_type=instant.number_type,
|
|
214
|
+
data=data,
|
|
215
|
+
id_prefix=id_prefix,
|
|
216
|
+
uid=uid,
|
|
217
|
+
)
|
|
218
|
+
self._instant = instant
|
|
219
|
+
|
|
220
|
+
def validate(self) -> None:
|
|
221
|
+
super().validate()
|
|
222
|
+
self.assert_property("instant")
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def instant(self):
|
|
226
|
+
return self._instant
|
|
227
|
+
|
|
228
|
+
@instant.setter
|
|
229
|
+
def instant(self, instant: Coordinate):
|
|
230
|
+
if not isinstance(instant, Coordinate):
|
|
231
|
+
raise ValueError(f"Expected a Coordinate, got a {type(instant).__name__}.")
|
|
232
|
+
self._instant = instant
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def start(self):
|
|
236
|
+
return self.instant
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def length(self):
|
|
240
|
+
return self.make_coordinate(0)
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def end(self):
|
|
244
|
+
return self.instant
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def interval(self) -> tuple[Number, Number]:
|
|
248
|
+
"""Pair of coordinate values understood as [start, end), i.e., as a left-inclusive interval."""
|
|
249
|
+
return (self.instant.value, self.instant.value)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@flyweight()
|
|
253
|
+
class IntervalEvent(Event[ET]):
|
|
254
|
+
|
|
255
|
+
@classmethod
|
|
256
|
+
def from_event_data(cls, data: ET, **kwargs) -> Self:
|
|
257
|
+
return cls(
|
|
258
|
+
start=data.get("start"),
|
|
259
|
+
end=data.get("end"),
|
|
260
|
+
length=data.get("length"),
|
|
261
|
+
data=data,
|
|
262
|
+
**kwargs,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def __init__(
|
|
266
|
+
self,
|
|
267
|
+
start: Coordinate | Number,
|
|
268
|
+
end: Optional[Coordinate | Number] = None,
|
|
269
|
+
length: Optional[Coordinate | Number] = None,
|
|
270
|
+
unit: Optional[TimeUnit | str] = None,
|
|
271
|
+
number_type: Optional[NumberType] = None,
|
|
272
|
+
data: Optional[ET] = None,
|
|
273
|
+
id_prefix: str = "ev",
|
|
274
|
+
uid: Optional[str] = None,
|
|
275
|
+
):
|
|
276
|
+
start = make_coordinate(
|
|
277
|
+
value=start,
|
|
278
|
+
unit=unit,
|
|
279
|
+
number_type=number_type,
|
|
280
|
+
default_unit=self._default_unit,
|
|
281
|
+
default_number_type=self._default_number_type,
|
|
282
|
+
)
|
|
283
|
+
super().__init__(
|
|
284
|
+
unit=start.unit,
|
|
285
|
+
number_type=start.number_type,
|
|
286
|
+
data=data,
|
|
287
|
+
id_prefix=id_prefix,
|
|
288
|
+
uid=uid,
|
|
289
|
+
)
|
|
290
|
+
utils.validate_interval_args(start=start, end=end, length=length)
|
|
291
|
+
self._start = None
|
|
292
|
+
self._end = None
|
|
293
|
+
self._length = None
|
|
294
|
+
self.start = start
|
|
295
|
+
if end is not None:
|
|
296
|
+
self.end = end
|
|
297
|
+
if length is not None:
|
|
298
|
+
self.length = length
|
|
299
|
+
|
|
300
|
+
def validate(self) -> None:
|
|
301
|
+
super().validate()
|
|
302
|
+
self.assert_property("start")
|
|
303
|
+
self.assert_property("end")
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def start(self) -> Coordinate:
|
|
307
|
+
return self._start
|
|
308
|
+
|
|
309
|
+
@start.setter
|
|
310
|
+
def start(self, value: Coordinate):
|
|
311
|
+
if self._end is not None:
|
|
312
|
+
assert value <= self.end, f"Start {value} is after end {self.end}"
|
|
313
|
+
self._start = self.make_coordinate(value)
|
|
314
|
+
self._update_length()
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def length(self):
|
|
318
|
+
if self._length is not None:
|
|
319
|
+
return self._length
|
|
320
|
+
return self.end - self.start
|
|
321
|
+
|
|
322
|
+
@length.setter
|
|
323
|
+
def length(self, length: Coordinate | Number):
|
|
324
|
+
assert length >= 0, f"Length cannot be negative, got {length}"
|
|
325
|
+
self._length = self.make_coordinate(length)
|
|
326
|
+
self._end = self.start + self._length
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def end(self):
|
|
330
|
+
if self._end is not None:
|
|
331
|
+
return self._end
|
|
332
|
+
return self.start + self.length
|
|
333
|
+
|
|
334
|
+
@end.setter
|
|
335
|
+
def end(self, value: Coordinate | Number):
|
|
336
|
+
assert value >= self.start, f"End {value} is before start {self.start}"
|
|
337
|
+
self._end = self.make_coordinate(value)
|
|
338
|
+
self._update_length()
|
|
339
|
+
|
|
340
|
+
# this has decidedly no 'instants' property so that the presence of it can be used to
|
|
341
|
+
# differentiate between events that implement the PInstantEvent Protocol from those that
|
|
342
|
+
# implement the PIntervalEvent Protocol in an efficient way
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def interval(self) -> tuple[Number, Number]:
|
|
346
|
+
"""Pair of coordinate values understood as [start, end), i.e., as a left-inclusive interval."""
|
|
347
|
+
return (self.start.value, self.end.value)
|
|
348
|
+
|
|
349
|
+
def _update_length(self):
|
|
350
|
+
if self._end is None and self._length is None:
|
|
351
|
+
return
|
|
352
|
+
new_length = self.end - self.start
|
|
353
|
+
assert new_length >= 0, f"Start {self.start} is after end {self.end}"
|
|
354
|
+
self._length = new_length
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class GraphicalEvent:
|
|
358
|
+
_default_unit = TimeUnit.pixels
|
|
359
|
+
_default_number_type = NumberType.int
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@flyweight()
|
|
363
|
+
class GraphicalInstantEvent(GraphicalEvent, InstantEvent[ET], Generic[ET]):
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@flyweight()
|
|
368
|
+
class GraphicalIntervalEvent(GraphicalEvent, IntervalEvent[ET], Generic[ET]):
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class LogicalEvent(Event):
|
|
373
|
+
_default_unit = TimeUnit.quarters
|
|
374
|
+
_default_number_type = NumberType.fraction
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@flyweight()
|
|
378
|
+
class LogicalInstantEvent(LogicalEvent, InstantEvent[ET], Generic[ET]):
|
|
379
|
+
pass
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@flyweight()
|
|
383
|
+
class LogicalIntervalEvent(LogicalEvent, IntervalEvent[ET], Generic[ET]):
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class PhysicalEvent:
|
|
388
|
+
_default_unit = TimeUnit.seconds
|
|
389
|
+
_default_number_type = NumberType.float
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@flyweight()
|
|
393
|
+
class PhysicalInstantEvent(PhysicalEvent, InstantEvent[ET], Generic[ET]):
|
|
394
|
+
pass
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@flyweight()
|
|
398
|
+
class PhysicalIntervalEvent(PhysicalEvent, IntervalEvent[ET], Generic[ET]):
|
|
399
|
+
pass
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class PEvent(Protocol):
|
|
403
|
+
id: str
|
|
404
|
+
data: Any
|
|
405
|
+
|
|
406
|
+
def get(self, field: str) -> Literal: ...
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class PInstantEvent(PEvent):
|
|
410
|
+
""""""
|
|
411
|
+
|
|
412
|
+
instant: Number
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class PIntervalEvent(PEvent):
|
|
416
|
+
""""""
|
|
417
|
+
|
|
418
|
+
start: Number
|
|
419
|
+
end: Number
|
|
420
|
+
length: Number
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
Inst = TypeVar("Inst", bound=InstantEvent | PInstantEvent)
|
|
424
|
+
Intv = TypeVar("Intv", bound=IntervalEvent | PIntervalEvent)
|
|
425
|
+
|
|
426
|
+
# endregion Event types
|
|
427
|
+
# region Silo types
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class Silo(RegisteredObject[ST], Generic[ST]):
|
|
431
|
+
_default_timeline_type: Optional[str] = None
|
|
432
|
+
|
|
433
|
+
def __init__(
|
|
434
|
+
self,
|
|
435
|
+
data: ST,
|
|
436
|
+
meta: Optional[Meta | dict] = None,
|
|
437
|
+
id_prefix: str = "silo",
|
|
438
|
+
uid: Optional[str] = None,
|
|
439
|
+
**kwargs,
|
|
440
|
+
):
|
|
441
|
+
self.validate_silo_data(data)
|
|
442
|
+
super().__init__(
|
|
443
|
+
id_prefix=id_prefix,
|
|
444
|
+
uid=uid,
|
|
445
|
+
)
|
|
446
|
+
self._data = None
|
|
447
|
+
self.data = data
|
|
448
|
+
self._meta = meta
|
|
449
|
+
self.meta = meta
|
|
450
|
+
|
|
451
|
+
@classmethod
|
|
452
|
+
def from_filepath(
|
|
453
|
+
cls,
|
|
454
|
+
filepath: str | Path,
|
|
455
|
+
instant_event_type: Optional[Type[Inst]] = None,
|
|
456
|
+
interval_event_type: Optional[Type[Intv]] = None,
|
|
457
|
+
**kwargs,
|
|
458
|
+
):
|
|
459
|
+
try:
|
|
460
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
461
|
+
data = f.read()
|
|
462
|
+
except UnicodeDecodeError:
|
|
463
|
+
with open(filepath, "rb") as f:
|
|
464
|
+
data = f.read()
|
|
465
|
+
return cls(
|
|
466
|
+
data=data,
|
|
467
|
+
instant_event_type=instant_event_type,
|
|
468
|
+
interval_event_type=interval_event_type,
|
|
469
|
+
**kwargs,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
@property
|
|
473
|
+
def data(self) -> ET:
|
|
474
|
+
return self._data
|
|
475
|
+
|
|
476
|
+
@data.setter
|
|
477
|
+
def data(self, data: ET):
|
|
478
|
+
self._data = data
|
|
479
|
+
|
|
480
|
+
@property
|
|
481
|
+
def meta(self) -> Meta:
|
|
482
|
+
"""Metadata associated with the EventSilo."""
|
|
483
|
+
return Meta(
|
|
484
|
+
id=self.id,
|
|
485
|
+
**self._meta,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
@meta.setter
|
|
489
|
+
def meta(self, meta: Meta | dict):
|
|
490
|
+
self._meta = Meta() if meta is None else Meta(meta)
|
|
491
|
+
|
|
492
|
+
def validate_silo_data(self, data: ST) -> None:
|
|
493
|
+
assert data is not None, "data cannot be None"
|
|
494
|
+
|
|
495
|
+
def initialize_empty_timeline(
|
|
496
|
+
self, timeline_class: Optional[Type[Timeline] | str] = None, **kwargs
|
|
497
|
+
):
|
|
498
|
+
"""Create a new timeline using the default timeline class if not otherwise specified."""
|
|
499
|
+
if timeline_class is None:
|
|
500
|
+
if self._default_timeline_type is None:
|
|
501
|
+
raise ValueError(
|
|
502
|
+
f"No default timeline type has been defined for {self.class_name}"
|
|
503
|
+
)
|
|
504
|
+
timeline_class = self._default_timeline_type
|
|
505
|
+
timeline_class = get_class(timeline_class)
|
|
506
|
+
id_prefix = kwargs.pop("id_prefix", f"{self.id}/tl")
|
|
507
|
+
meta = self.meta.update(kwargs.pop("meta", {}))
|
|
508
|
+
timeline: Timeline = timeline_class(id_prefix=id_prefix, meta=meta, **kwargs)
|
|
509
|
+
return timeline
|
|
510
|
+
|
|
511
|
+
def get_default_conversion_maps(self) -> list[ConversionMap]:
|
|
512
|
+
return []
|
|
513
|
+
|
|
514
|
+
def get_default_cmaps(self):
|
|
515
|
+
"""Alias for get_default_conversion_maps."""
|
|
516
|
+
return self.get_default_conversion_maps()
|
|
517
|
+
|
|
518
|
+
def make_timeline(
|
|
519
|
+
self, timeline_class: Optional[Type[Timeline] | str] = None, **kwargs
|
|
520
|
+
):
|
|
521
|
+
timeline = self.initialize_empty_timeline(timeline_class, **kwargs)
|
|
522
|
+
self.populate_timeline(timeline)
|
|
523
|
+
cmaps = self.get_default_conversion_maps()
|
|
524
|
+
timeline.add_conversion_maps(*cmaps)
|
|
525
|
+
return timeline
|
|
526
|
+
|
|
527
|
+
def populate_timeline(self, timeline):
|
|
528
|
+
"""Most subclasses will override this method to add events to the timeline."""
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
class AudioSilo(Silo[np.ndarray]):
|
|
533
|
+
|
|
534
|
+
_default_timeline_type = "DiscretePhysicalTimeline"
|
|
535
|
+
|
|
536
|
+
@classmethod
|
|
537
|
+
def from_filepath(
|
|
538
|
+
cls,
|
|
539
|
+
filepath: str | Path,
|
|
540
|
+
instant_event_type: Optional[Type[Inst]] = None,
|
|
541
|
+
interval_event_type: Optional[Type[Intv]] = None,
|
|
542
|
+
**kwargs,
|
|
543
|
+
):
|
|
544
|
+
with warnings.catch_warnings():
|
|
545
|
+
warnings.simplefilter("ignore", FutureWarning)
|
|
546
|
+
y, sr = librosa.load(filepath, sr=None, mono=True)
|
|
547
|
+
return cls(
|
|
548
|
+
data=y,
|
|
549
|
+
sample_rate=sr,
|
|
550
|
+
instant_event_type=instant_event_type,
|
|
551
|
+
interval_event_type=interval_event_type,
|
|
552
|
+
**kwargs,
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
def __init__(
|
|
556
|
+
self,
|
|
557
|
+
data: np.ndarray,
|
|
558
|
+
sample_rate: int | float,
|
|
559
|
+
instant_event_type: Optional[Type[Inst]] = None,
|
|
560
|
+
interval_event_type: Optional[Type[Intv]] = None,
|
|
561
|
+
id_prefix: str = "silo",
|
|
562
|
+
uid: Optional[str] = None,
|
|
563
|
+
**kwargs,
|
|
564
|
+
):
|
|
565
|
+
super().__init__(
|
|
566
|
+
data=data,
|
|
567
|
+
instant_event_type=instant_event_type,
|
|
568
|
+
interval_event_type=interval_event_type,
|
|
569
|
+
id_prefix=id_prefix,
|
|
570
|
+
uid=uid,
|
|
571
|
+
**kwargs,
|
|
572
|
+
)
|
|
573
|
+
self._sample_rate = sample_rate
|
|
574
|
+
|
|
575
|
+
@property
|
|
576
|
+
def sample_rate(self) -> int | float:
|
|
577
|
+
return self._sample_rate
|
|
578
|
+
|
|
579
|
+
def make_timeline(
|
|
580
|
+
self, timeline_class: Optional[Type[Timeline] | str] = None, **kwargs
|
|
581
|
+
):
|
|
582
|
+
length = Coordinate(self.data.shape[0], "samples_int")
|
|
583
|
+
tl_args = dict(kwargs, length=length)
|
|
584
|
+
return super().make_timeline(timeline_class=timeline_class, **tl_args)
|
|
585
|
+
|
|
586
|
+
def get_default_conversion_maps(self) -> list[ConversionMap]:
|
|
587
|
+
cmaps = super().get_default_conversion_maps()
|
|
588
|
+
cmaps.append(SamplesToSeconds(sample_rate=self.sample_rate))
|
|
589
|
+
return cmaps
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
# endregion Silo types
|
|
593
|
+
# region EventSilo types
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
class PEventSilo(Protocol):
|
|
597
|
+
|
|
598
|
+
def iter_events(self, **kwargs) -> Iterator[Inst | Intv]: ...
|
|
599
|
+
|
|
600
|
+
@overload
|
|
601
|
+
def iter_instant_events(
|
|
602
|
+
self, event_type: Literal[None]
|
|
603
|
+
) -> Iterator[PInstantEvent]: ...
|
|
604
|
+
|
|
605
|
+
@overload
|
|
606
|
+
def iter_instant_events(self, event_type: Type[Inst]) -> Iterator[Inst]: ...
|
|
607
|
+
|
|
608
|
+
def iter_instant_events(
|
|
609
|
+
self, event_type: Optional[Type[Inst]] = None
|
|
610
|
+
) -> Iterator[PInstantEvent] | Iterator[Inst]: ...
|
|
611
|
+
|
|
612
|
+
@overload
|
|
613
|
+
def iter_interval_events(
|
|
614
|
+
self, event_type: Literal[None]
|
|
615
|
+
) -> Iterator[PIntervalEvent]: ...
|
|
616
|
+
|
|
617
|
+
@overload
|
|
618
|
+
def iter_interval_events(self, event_type: Type[Inst]) -> Iterator[Inst]: ...
|
|
619
|
+
|
|
620
|
+
def iter_interval_events(
|
|
621
|
+
self, event_type: Optional[Type[Inst]] = None
|
|
622
|
+
) -> Iterator[PIntervalEvent] | Iterator[Inst]: ...
|
|
623
|
+
|
|
624
|
+
def iter_raw_event_data(self) -> Iterator[Any]: ...
|
|
625
|
+
|
|
626
|
+
def iter_raw_instant_event_data(self) -> Iterator[Any]: ...
|
|
627
|
+
|
|
628
|
+
def iter_raw_interval_event_data(self) -> Iterator[Any]: ...
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
class _EventTypesMixin:
|
|
632
|
+
"""Injects instant_event_type and interval_event_type as init arguments and properties.
|
|
633
|
+
The latter make use of the class attributes _default_instant_event_type and
|
|
634
|
+
_default_interval_event_type.
|
|
635
|
+
"""
|
|
636
|
+
|
|
637
|
+
_default_instant_event_type: Optional[Type[PInstantEvent]] = None
|
|
638
|
+
_default_interval_event_type: Optional[Type[PIntervalEvent]] = None
|
|
639
|
+
|
|
640
|
+
def __init__(
|
|
641
|
+
self,
|
|
642
|
+
instant_event_type: Optional[Type[Inst]] = None,
|
|
643
|
+
interval_event_type: Optional[Type[Intv]] = None,
|
|
644
|
+
**kwargs,
|
|
645
|
+
):
|
|
646
|
+
self._instant_event_type: Optional[Type[Inst]] = instant_event_type
|
|
647
|
+
self._interval_event_type: Optional[Type[Intv]] = interval_event_type
|
|
648
|
+
super().__init__(**kwargs)
|
|
649
|
+
|
|
650
|
+
@property
|
|
651
|
+
def instant_event_type(self) -> Type[Inst]:
|
|
652
|
+
if self._instant_event_type is not None:
|
|
653
|
+
return self._instant_event_type
|
|
654
|
+
if self._default_instant_event_type is not None:
|
|
655
|
+
return self._default_instant_event_type
|
|
656
|
+
raise ValueError(
|
|
657
|
+
f"No instant_event_type has been defined for this {self.class_name}."
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
@property
|
|
661
|
+
def interval_event_type(self) -> Type[Intv]:
|
|
662
|
+
if self._interval_event_type is not None:
|
|
663
|
+
return self._interval_event_type
|
|
664
|
+
if self._default_interval_event_type is not None:
|
|
665
|
+
return self._default_interval_event_type
|
|
666
|
+
raise ValueError(
|
|
667
|
+
f"No interval_event_type has been defined for this {self.class_name}."
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
class EventSilo(_EventTypesMixin, Silo[ST], PEventSilo, Generic[ST, Inst, Intv]):
|
|
672
|
+
"""Silo"""
|
|
673
|
+
|
|
674
|
+
_default_instant_event_type = InstantEvent
|
|
675
|
+
_default_interval_event_type = IntervalEvent
|
|
676
|
+
|
|
677
|
+
def __init__(
|
|
678
|
+
self,
|
|
679
|
+
data: ST,
|
|
680
|
+
instant_event_type: Optional[Type[Inst]] = None,
|
|
681
|
+
interval_event_type: Optional[Type[Intv]] = None,
|
|
682
|
+
meta: Optional[Meta | dict] = None,
|
|
683
|
+
id_prefix: str = "silo",
|
|
684
|
+
uid: Optional[str] = None,
|
|
685
|
+
**kwargs,
|
|
686
|
+
):
|
|
687
|
+
super().__init__(
|
|
688
|
+
instant_event_type=instant_event_type,
|
|
689
|
+
interval_event_type=interval_event_type,
|
|
690
|
+
data=data,
|
|
691
|
+
meta=meta,
|
|
692
|
+
id_prefix=id_prefix,
|
|
693
|
+
uid=uid,
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
def iter_raw_instant_event_data(self) -> Iterator[Any]:
|
|
697
|
+
raise NotImplementedError
|
|
698
|
+
|
|
699
|
+
def iter_raw_interval_event_data(self) -> Iterator[Any]:
|
|
700
|
+
raise NotImplementedError
|
|
701
|
+
|
|
702
|
+
def iter_events(self, **kwargs) -> Iterator[PInstantEvent | PIntervalEvent]:
|
|
703
|
+
yield from self.iter_instant_events(**kwargs)
|
|
704
|
+
yield from self.iter_interval_events(**kwargs)
|
|
705
|
+
|
|
706
|
+
def iter_instant_events(
|
|
707
|
+
self, event_type: Optional[Type[PInstantEvent]] = None, **kwargs
|
|
708
|
+
) -> Iterator[Inst] | Iterator[PInstantEvent]:
|
|
709
|
+
id_prefix = kwargs.pop("id_prefix", f"{self.id}/ev")
|
|
710
|
+
if event_type is None:
|
|
711
|
+
event_type = self.instant_event_type
|
|
712
|
+
for raw_instant_event in self.iter_raw_instant_event_data():
|
|
713
|
+
yield event_type.from_event_data(
|
|
714
|
+
raw_instant_event, id_prefix=id_prefix, **kwargs
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
def iter_interval_events(
|
|
718
|
+
self, event_type: Optional[Type[Inst]] = None, **kwargs
|
|
719
|
+
) -> Iterator[Intv] | Iterator[PIntervalEvent]:
|
|
720
|
+
id_prefix = kwargs.pop("id_prefix", f"{self.id}/ev")
|
|
721
|
+
if event_type is None:
|
|
722
|
+
event_type = self.interval_event_type
|
|
723
|
+
for raw_interval_event in self.iter_raw_interval_event_data():
|
|
724
|
+
yield event_type.from_event_data(
|
|
725
|
+
raw_interval_event, id_prefix=id_prefix, **kwargs
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
def populate_timeline(self, timeline):
|
|
729
|
+
timeline.add_events(
|
|
730
|
+
self.iter_events(id_prefix=f"{timeline.id}/ev"), allow_expansion=True
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
class DataFrameSilo(EventSilo[pd.DataFrame, Inst, Intv]):
|
|
735
|
+
"""A Silo that creates events from a pandas DataFrame."""
|
|
736
|
+
|
|
737
|
+
@classmethod
|
|
738
|
+
def from_csv_file(
|
|
739
|
+
cls,
|
|
740
|
+
filepath: str | Path,
|
|
741
|
+
instant_event_type: Optional[Type[Inst]] = None,
|
|
742
|
+
interval_event_type: Optional[Type[Intv]] = None,
|
|
743
|
+
parse_options: Optional[dict] = None,
|
|
744
|
+
**kwargs,
|
|
745
|
+
) -> Self:
|
|
746
|
+
if parse_options is None:
|
|
747
|
+
parse_options = {}
|
|
748
|
+
data = pd.read_csv(filepath, **parse_options)
|
|
749
|
+
return cls(
|
|
750
|
+
data=data,
|
|
751
|
+
instant_event_type=instant_event_type,
|
|
752
|
+
interval_event_type=interval_event_type,
|
|
753
|
+
**kwargs,
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
@classmethod
|
|
757
|
+
def from_json_file(
|
|
758
|
+
cls,
|
|
759
|
+
filepath: str | Path,
|
|
760
|
+
instant_event_type: Optional[Type[Inst]] = None,
|
|
761
|
+
interval_event_type: Optional[Type[Intv]] = None,
|
|
762
|
+
parse_options: Optional[dict] = None,
|
|
763
|
+
**kwargs,
|
|
764
|
+
) -> Self:
|
|
765
|
+
data = load_json_file(filepath)
|
|
766
|
+
|
|
767
|
+
return cls(
|
|
768
|
+
data=data,
|
|
769
|
+
instant_event_type=instant_event_type,
|
|
770
|
+
interval_event_type=interval_event_type,
|
|
771
|
+
**kwargs,
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
@classmethod
|
|
775
|
+
def from_filepath(
|
|
776
|
+
cls,
|
|
777
|
+
filepath: str | Path,
|
|
778
|
+
instant_event_type: Optional[Type[Inst]] = None,
|
|
779
|
+
interval_event_type: Optional[Type[Intv]] = None,
|
|
780
|
+
parse_options: Optional[dict] = None,
|
|
781
|
+
**kwargs,
|
|
782
|
+
) -> Self:
|
|
783
|
+
if isinstance(filepath, str):
|
|
784
|
+
filepath = Path(filepath)
|
|
785
|
+
fext = filepath.suffix.lower()
|
|
786
|
+
if fext in (".csv", ".tsv"):
|
|
787
|
+
if fext == ".tsv" and "sep" not in parse_options:
|
|
788
|
+
parse_options["sep"] = "\t"
|
|
789
|
+
return cls.from_csv_file(
|
|
790
|
+
filepath=filepath,
|
|
791
|
+
instant_event_type=instant_event_type,
|
|
792
|
+
interval_event_type=interval_event_type,
|
|
793
|
+
parse_options=parse_options,
|
|
794
|
+
**kwargs,
|
|
795
|
+
)
|
|
796
|
+
elif fext in (".json", ".jsonl"):
|
|
797
|
+
return cls.from_json_file(
|
|
798
|
+
filepath=filepath,
|
|
799
|
+
instant_event_type=instant_event_type,
|
|
800
|
+
interval_event_type=interval_event_type,
|
|
801
|
+
parse_options=parse_options,
|
|
802
|
+
**kwargs,
|
|
803
|
+
)
|
|
804
|
+
else:
|
|
805
|
+
raise ValueError(
|
|
806
|
+
f"Unsupported file extension {fext!r} for {cls.class_name}. "
|
|
807
|
+
"Supported extensions are: .csv, .tsv, .json, .jsonl."
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
def __init__(
|
|
811
|
+
self,
|
|
812
|
+
data: pd.DataFrame,
|
|
813
|
+
instant_event_selector: Optional[
|
|
814
|
+
pd.Series | pd.Index | np.ndarray | list | tuple
|
|
815
|
+
] = None,
|
|
816
|
+
interval_event_selector: Optional[
|
|
817
|
+
pd.Series | pd.Index | np.ndarray | list | tuple
|
|
818
|
+
] = None,
|
|
819
|
+
instant_event_type: Optional[Type[Inst]] = None,
|
|
820
|
+
interval_event_type: Optional[Type[Intv]] = None,
|
|
821
|
+
instant_col: str = "instant",
|
|
822
|
+
start_col: str = "start",
|
|
823
|
+
end_col: str = "end",
|
|
824
|
+
length_col: str = "length",
|
|
825
|
+
**kwargs,
|
|
826
|
+
):
|
|
827
|
+
super().__init__(
|
|
828
|
+
data=data,
|
|
829
|
+
instant_event_type=instant_event_type,
|
|
830
|
+
interval_event_type=interval_event_type,
|
|
831
|
+
**kwargs,
|
|
832
|
+
)
|
|
833
|
+
if instant_event_selector is None and interval_event_selector is None:
|
|
834
|
+
warnings.warn(
|
|
835
|
+
"No event selectors have been defined, assuming all events are intervals."
|
|
836
|
+
)
|
|
837
|
+
self.instant_event_selector = None
|
|
838
|
+
self.interval_event_selector = pd.Series(True, index=data.index)
|
|
839
|
+
else:
|
|
840
|
+
self.instant_event_selector = instant_event_selector
|
|
841
|
+
self.interval_event_selector = interval_event_selector
|
|
842
|
+
self.instant_col: str = instant_col
|
|
843
|
+
self.start_col: str = start_col
|
|
844
|
+
self.end_col: str = end_col
|
|
845
|
+
self.length_col: str = length_col
|
|
846
|
+
|
|
847
|
+
def validate_silo_data(self, data: ST) -> None:
|
|
848
|
+
assert (
|
|
849
|
+
data.index.is_unique
|
|
850
|
+
), "DataFrame index has duplicates. Use .reset_index()"
|
|
851
|
+
duplicate_cols = data.columns[data.columns.duplicated()]
|
|
852
|
+
assert (
|
|
853
|
+
duplicate_cols.size == 0
|
|
854
|
+
), f"DataFrame has duplicate columns: {duplicate_cols.to_list()}"
|
|
855
|
+
|
|
856
|
+
def subselect_instant_events(self) -> pd.DataFrame:
|
|
857
|
+
if self.instant_event_selector is None:
|
|
858
|
+
return
|
|
859
|
+
selection = self.data[self.instant_event_selector]
|
|
860
|
+
if self.instant_col is not None and self.instant_col != "instant":
|
|
861
|
+
renaming = {self.instant_col: "instant"}
|
|
862
|
+
return selection.rename(columns=renaming)
|
|
863
|
+
return selection
|
|
864
|
+
|
|
865
|
+
def subselect_interval_events(self) -> pd.DataFrame:
|
|
866
|
+
if self.interval_event_selector is None:
|
|
867
|
+
return
|
|
868
|
+
selection = self.data[self.interval_event_selector]
|
|
869
|
+
renaming = {}
|
|
870
|
+
if self.start_col is not None and self.start_col != "start":
|
|
871
|
+
renaming[self.start_col] = "start"
|
|
872
|
+
if self.end_col is not None and self.end_col != "end":
|
|
873
|
+
renaming[self.end_col] = "end"
|
|
874
|
+
if self.length_col is not None and self.length_col != "length":
|
|
875
|
+
renaming[self.length_col] = "length"
|
|
876
|
+
if renaming:
|
|
877
|
+
return selection.rename(columns=renaming)
|
|
878
|
+
return selection
|
|
879
|
+
|
|
880
|
+
def iter_raw_instant_event_data(self) -> Iterator[dict]:
|
|
881
|
+
selection = self.subselect_instant_events()
|
|
882
|
+
if selection is None:
|
|
883
|
+
return
|
|
884
|
+
yield from selection.to_dict(orient="records")
|
|
885
|
+
|
|
886
|
+
def iter_raw_interval_event_data(self) -> Iterator[dict]:
|
|
887
|
+
selection = self.subselect_interval_events()
|
|
888
|
+
if selection is None:
|
|
889
|
+
return
|
|
890
|
+
yield from selection.to_dict(orient="records")
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
# endregion EventSilo types
|
|
894
|
+
# region MidiDataFrameSilo
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
class MidiEvent:
|
|
898
|
+
"""This is a mix-in class used for class composition."""
|
|
899
|
+
|
|
900
|
+
_property_defaults = dict(type="type", note="note", velocity="velocity")
|
|
901
|
+
_default_unit = TimeUnit.ticks
|
|
902
|
+
_default_number_type = NumberType.int
|
|
903
|
+
|
|
904
|
+
@property
|
|
905
|
+
def type(self):
|
|
906
|
+
return self.get_property("type")
|
|
907
|
+
|
|
908
|
+
@property
|
|
909
|
+
def note(self):
|
|
910
|
+
return self.get_property("note")
|
|
911
|
+
|
|
912
|
+
@property
|
|
913
|
+
def velocity(self):
|
|
914
|
+
return self.get_property("velocity")
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
@flyweight()
|
|
918
|
+
class MidiIntervalEvent(MidiEvent, IntervalEvent):
|
|
919
|
+
pass
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
@flyweight()
|
|
923
|
+
class MidiInstantEvent(MidiEvent, InstantEvent):
|
|
924
|
+
pass
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
class MidiDataFrameSilo(DataFrameSilo):
|
|
928
|
+
_default_timeline_type = "DiscreteLogicalTimeline"
|
|
929
|
+
_default_instant_event_type = MidiInstantEvent
|
|
930
|
+
_default_interval_event_type = MidiIntervalEvent
|
|
931
|
+
|
|
932
|
+
@classmethod
|
|
933
|
+
def from_filepath(
|
|
934
|
+
cls,
|
|
935
|
+
filepath: str | Path,
|
|
936
|
+
instant_event_type: Optional[Type[Inst]] = None,
|
|
937
|
+
interval_event_type: Optional[Type[Intv]] = None,
|
|
938
|
+
**kwargs,
|
|
939
|
+
):
|
|
940
|
+
fp = Path(filepath)
|
|
941
|
+
if fp.suffix.lower() in (".midi", ".mid"):
|
|
942
|
+
midi_df = midi_to_df(filepath)
|
|
943
|
+
has_duration = midi_df.duration.notna()
|
|
944
|
+
return cls(
|
|
945
|
+
data=midi_df,
|
|
946
|
+
instant_event_selector=~has_duration,
|
|
947
|
+
interval_event_selector=has_duration,
|
|
948
|
+
instant_event_type=instant_event_type,
|
|
949
|
+
interval_event_type=interval_event_type,
|
|
950
|
+
**kwargs,
|
|
951
|
+
)
|
|
952
|
+
return super().from_filepath(
|
|
953
|
+
filepath=filepath,
|
|
954
|
+
instant_event_type=instant_event_type,
|
|
955
|
+
interval_event_type=interval_event_type,
|
|
956
|
+
**kwargs,
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
def __init__(
|
|
960
|
+
self,
|
|
961
|
+
data: pd.DataFrame,
|
|
962
|
+
instant_event_selector: pd.Series | pd.Index | np.ndarray | list | tuple,
|
|
963
|
+
interval_event_selector: pd.Series | pd.Index | np.ndarray | list | tuple,
|
|
964
|
+
instant_event_type: Optional[Type[Inst]] = None,
|
|
965
|
+
interval_event_type: Optional[Type[Intv]] = None,
|
|
966
|
+
instant_col: str = "absolute_time",
|
|
967
|
+
start_col: str = "absolute_time",
|
|
968
|
+
end_col: str = "end",
|
|
969
|
+
length_col: str = "duration",
|
|
970
|
+
**kwargs,
|
|
971
|
+
):
|
|
972
|
+
super().__init__(
|
|
973
|
+
data=data,
|
|
974
|
+
instant_event_selector=instant_event_selector,
|
|
975
|
+
interval_event_selector=interval_event_selector,
|
|
976
|
+
instant_event_type=instant_event_type,
|
|
977
|
+
interval_event_type=interval_event_type,
|
|
978
|
+
instant_col=instant_col,
|
|
979
|
+
start_col=start_col,
|
|
980
|
+
end_col=end_col,
|
|
981
|
+
length_col=length_col,
|
|
982
|
+
**kwargs,
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
def get_default_conversion_maps(self) -> list[ConversionMap]:
|
|
986
|
+
return [ticks2seconds_map_from_midi_df(self.data)]
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
# endregion MidiDataFrameSilo
|
|
990
|
+
# region TiliaDataFrameSilo
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
class TiliaEvent:
|
|
994
|
+
_default_unit = TimeUnit.seconds
|
|
995
|
+
_default_number_type = NumberType.float
|
|
996
|
+
_property_defaults = dict(
|
|
997
|
+
timeline="timeline",
|
|
998
|
+
name="name",
|
|
999
|
+
component="component",
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
@property
|
|
1003
|
+
def timeline(self):
|
|
1004
|
+
return self.get_property("timeline")
|
|
1005
|
+
|
|
1006
|
+
@property
|
|
1007
|
+
def name(self):
|
|
1008
|
+
return self.get_property("name")
|
|
1009
|
+
|
|
1010
|
+
@property
|
|
1011
|
+
def component(self):
|
|
1012
|
+
return self.get_property("component")
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
@flyweight()
|
|
1016
|
+
class TiliaInstantEvent(TiliaEvent, PhysicalInstantEvent):
|
|
1017
|
+
pass
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
@flyweight()
|
|
1021
|
+
class TiliaIntervalEvent(TiliaEvent, PhysicalIntervalEvent):
|
|
1022
|
+
pass
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
class TiliaDataFrameSilo(DataFrameSilo):
|
|
1026
|
+
_default_timeline_type = "ContinuousPhysicalTimeline"
|
|
1027
|
+
_default_instant_event_type = TiliaInstantEvent
|
|
1028
|
+
_default_interval_event_type = TiliaIntervalEvent
|
|
1029
|
+
|
|
1030
|
+
@classmethod
|
|
1031
|
+
def from_csv_file(
|
|
1032
|
+
cls,
|
|
1033
|
+
filepath: str | Path,
|
|
1034
|
+
instant_event_type: Optional[Type[Inst]] = None,
|
|
1035
|
+
interval_event_type: Optional[Type[Intv]] = None,
|
|
1036
|
+
parse_options: Optional[dict] = None,
|
|
1037
|
+
**kwargs,
|
|
1038
|
+
) -> Self:
|
|
1039
|
+
raise NotImplementedError(f"CSV files are not supported for {cls.class_name}.")
|
|
1040
|
+
|
|
1041
|
+
@classmethod
|
|
1042
|
+
def from_json_file(
|
|
1043
|
+
cls,
|
|
1044
|
+
filepath: str | Path,
|
|
1045
|
+
instant_event_type: Optional[Type[Inst]] = None,
|
|
1046
|
+
interval_event_type: Optional[Type[Intv]] = None,
|
|
1047
|
+
parse_options: Optional[dict] = None,
|
|
1048
|
+
**kwargs,
|
|
1049
|
+
) -> Self:
|
|
1050
|
+
data = parse_tilia_json(filepath)
|
|
1051
|
+
is_instant = data.time.notna()
|
|
1052
|
+
is_interval = data.start.notna()
|
|
1053
|
+
return cls(
|
|
1054
|
+
data=data,
|
|
1055
|
+
instant_event_type=instant_event_type,
|
|
1056
|
+
interval_event_type=interval_event_type,
|
|
1057
|
+
instant_event_selector=is_instant,
|
|
1058
|
+
interval_event_selector=is_interval,
|
|
1059
|
+
**kwargs,
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
def __init__(
|
|
1063
|
+
self,
|
|
1064
|
+
data: pd.DataFrame,
|
|
1065
|
+
instant_event_selector: pd.Series | pd.Index | np.ndarray | list | tuple,
|
|
1066
|
+
interval_event_selector: pd.Series | pd.Index | np.ndarray | list | tuple,
|
|
1067
|
+
instant_event_type: Optional[Type[Inst]] = None,
|
|
1068
|
+
interval_event_type: Optional[Type[Intv]] = None,
|
|
1069
|
+
instant_col: str = "time",
|
|
1070
|
+
start_col: str = "start",
|
|
1071
|
+
end_col: str = "end",
|
|
1072
|
+
length_col: str = None,
|
|
1073
|
+
**kwargs,
|
|
1074
|
+
):
|
|
1075
|
+
super().__init__(
|
|
1076
|
+
data=data,
|
|
1077
|
+
instant_event_selector=instant_event_selector,
|
|
1078
|
+
interval_event_selector=interval_event_selector,
|
|
1079
|
+
instant_event_type=instant_event_type,
|
|
1080
|
+
interval_event_type=interval_event_type,
|
|
1081
|
+
instant_col=instant_col,
|
|
1082
|
+
start_col=start_col,
|
|
1083
|
+
end_col=end_col,
|
|
1084
|
+
length_col=length_col,
|
|
1085
|
+
**kwargs,
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
def get_default_conversion_maps(self) -> list[ConversionMap]:
|
|
1089
|
+
measure_map = make_region_map_from_tilia_df(self.data, column_type="measure")
|
|
1090
|
+
beat_map = make_region_map_from_tilia_df(self.data, column_type="beat")
|
|
1091
|
+
return [measure_map, beat_map]
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def make_region_map_from_tilia_df(
|
|
1095
|
+
tla: pd.DataFrame, column_type: Literal["measure", "beat"]
|
|
1096
|
+
):
|
|
1097
|
+
seconds2values = make_unique_constants_map_from_tilia_df(tla, column_type)
|
|
1098
|
+
breaks = seconds2values.index.to_list() + [np.inf]
|
|
1099
|
+
return RegionMap.from_breaks(
|
|
1100
|
+
breaks,
|
|
1101
|
+
seconds2values.values,
|
|
1102
|
+
source_unit="seconds",
|
|
1103
|
+
target_unit=column_type + "s",
|
|
1104
|
+
column_name=column_type,
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def make_unique_constants_map_from_tilia_df(
|
|
1109
|
+
tla: pd.DataFrame, column_type: Literal["measure", "beat"]
|
|
1110
|
+
):
|
|
1111
|
+
intv_start_col = f"start_{column_type}"
|
|
1112
|
+
intv_end_col = f"end_{column_type}"
|
|
1113
|
+
instant2measure_complete = pd.concat(
|
|
1114
|
+
[
|
|
1115
|
+
tla[["start", f"start_{column_type}"]].rename(
|
|
1116
|
+
columns={"start": "instant", intv_start_col: column_type}
|
|
1117
|
+
),
|
|
1118
|
+
tla[["end", "end_measure"]].rename(
|
|
1119
|
+
columns={"end": "instant", intv_end_col: column_type}
|
|
1120
|
+
),
|
|
1121
|
+
tla[["time", column_type]].rename(columns=dict(time="instant")),
|
|
1122
|
+
]
|
|
1123
|
+
)
|
|
1124
|
+
instant2measure = instant2measure_complete.groupby("instant").agg(set).iloc[:, 0]
|
|
1125
|
+
(instant2measure.map(len) > 1).any()
|
|
1126
|
+
values = instant2measure.map(lambda x: x.pop())
|
|
1127
|
+
values = values.loc[values != values.shift()]
|
|
1128
|
+
return values
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
# endregion TiliaDataFrameSilo
|
|
1132
|
+
# region partitura parsing
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
class PartituraEvent(LogicalEvent):
|
|
1136
|
+
_default_unit = TimeUnit.ticks
|
|
1137
|
+
_default_number_type = NumberType.int
|
|
1138
|
+
|
|
1139
|
+
_property_defaults = dict(type="type", note_id="id")
|
|
1140
|
+
|
|
1141
|
+
def get_accessor(self) -> Callable:
|
|
1142
|
+
return partial(getattr, self._data)
|
|
1143
|
+
|
|
1144
|
+
@property
|
|
1145
|
+
def type(self):
|
|
1146
|
+
return self.data.__class__.__name__
|
|
1147
|
+
|
|
1148
|
+
@property
|
|
1149
|
+
def note_id(self):
|
|
1150
|
+
return self.get_property("note_id")
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
@flyweight()
|
|
1154
|
+
class PartituraInstantEvent(PartituraEvent, LogicalInstantEvent[pts.TimedObject]):
|
|
1155
|
+
|
|
1156
|
+
@classmethod
|
|
1157
|
+
def from_event_data(cls, data: pts.TimedObject, **kwargs) -> Self:
|
|
1158
|
+
instant = data.start.t
|
|
1159
|
+
return cls(instant=instant, data=data, uid=getattr(data, "id", None), **kwargs)
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
@flyweight()
|
|
1163
|
+
class PartituraIntervalEvent(PartituraEvent, LogicalIntervalEvent[pts.TimedObject]):
|
|
1164
|
+
|
|
1165
|
+
@classmethod
|
|
1166
|
+
def from_event_data(cls, data: pts.TimedObject, **kwargs) -> Self:
|
|
1167
|
+
start_val = data.start.t
|
|
1168
|
+
end_val = data.end.t
|
|
1169
|
+
length = data.duration
|
|
1170
|
+
return cls(
|
|
1171
|
+
start=start_val,
|
|
1172
|
+
end=end_val,
|
|
1173
|
+
length=length,
|
|
1174
|
+
data=data,
|
|
1175
|
+
uid=getattr(data, "id", None),
|
|
1176
|
+
**kwargs,
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
@flyweight()
|
|
1181
|
+
class PartituraPerformedNote(PartituraEvent, LogicalIntervalEvent[ptp.PerformedNote]):
|
|
1182
|
+
|
|
1183
|
+
@classmethod
|
|
1184
|
+
def from_event_data(cls, data: ptp.PerformedNote, **kwargs) -> Self:
|
|
1185
|
+
"""Does not work directly with PerformedNote objects because they have insufficient
|
|
1186
|
+
timing information. The PartituraPerformanceSilo enriches them using the same
|
|
1187
|
+
function used for partitura's note arrays.
|
|
1188
|
+
"""
|
|
1189
|
+
start_val = data.get("onset_tick")
|
|
1190
|
+
length = data.get("duration_tick")
|
|
1191
|
+
end_val = start_val + length
|
|
1192
|
+
return cls(
|
|
1193
|
+
start=start_val,
|
|
1194
|
+
end=end_val,
|
|
1195
|
+
length=length,
|
|
1196
|
+
data=data,
|
|
1197
|
+
uid=getattr(data, "id", None),
|
|
1198
|
+
**kwargs,
|
|
1199
|
+
)
|
|
1200
|
+
|
|
1201
|
+
def get_accessor(self) -> Callable:
|
|
1202
|
+
return self._data.get
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
class _PartituraMixin:
|
|
1206
|
+
"""Mixin for Partitura-specific silos."""
|
|
1207
|
+
|
|
1208
|
+
_default_timeline_type = "DiscreteLogicalTimeline"
|
|
1209
|
+
|
|
1210
|
+
@property
|
|
1211
|
+
def timed_object_counts(self) -> dict[Type[pts.TimedObject], int]:
|
|
1212
|
+
"""An overview dict counting this silo's raw event items by type."""
|
|
1213
|
+
return {k: len(v) for k, v in self._events_by_type.items()}
|
|
1214
|
+
|
|
1215
|
+
@property
|
|
1216
|
+
def n_instant_events(self) -> int:
|
|
1217
|
+
return sum(1 for _ in self.iter_raw_instant_event_data())
|
|
1218
|
+
|
|
1219
|
+
@property
|
|
1220
|
+
def n_events(self) -> int:
|
|
1221
|
+
return sum(map(len, self._events_by_type.values()))
|
|
1222
|
+
|
|
1223
|
+
@property
|
|
1224
|
+
def n_parts(self) -> int:
|
|
1225
|
+
return len(self.data)
|
|
1226
|
+
|
|
1227
|
+
@property
|
|
1228
|
+
def n_interval_events(self) -> int:
|
|
1229
|
+
"""Number of all interval events without merging tied notes (i.e., two tied notes count as 2)."""
|
|
1230
|
+
return sum(1 for _ in self.iter_raw_interval_event_data(merge_tied_notes=False))
|
|
1231
|
+
|
|
1232
|
+
def __repr__(self):
|
|
1233
|
+
type_counts = {cls.__name__: n for cls, n in self.timed_object_counts.items()}
|
|
1234
|
+
type_counts = pformat(type_counts)
|
|
1235
|
+
return (
|
|
1236
|
+
f"{self.class_name}("
|
|
1237
|
+
f"id={self.id!r}, "
|
|
1238
|
+
f"n_parts={self.n_parts}, "
|
|
1239
|
+
f"n_interval_events={self.n_interval_events}, "
|
|
1240
|
+
f"n_instant_events={self.n_instant_events}"
|
|
1241
|
+
f")\n{type_counts}"
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
class PartituraPerformanceSilo(
|
|
1246
|
+
_PartituraMixin,
|
|
1247
|
+
EventSilo[list[pts.Part], PartituraInstantEvent, PartituraIntervalEvent],
|
|
1248
|
+
):
|
|
1249
|
+
_default_instant_event_type = None
|
|
1250
|
+
_default_interval_event_type = PartituraPerformedNote
|
|
1251
|
+
|
|
1252
|
+
def __init__(
|
|
1253
|
+
self,
|
|
1254
|
+
data: ptp.Performance | ptp.PerformedPart | Iterable[ptp.PerformedPart],
|
|
1255
|
+
instant_event_type: Optional[Type[PInstantEvent]] = None,
|
|
1256
|
+
interval_event_type: Optional[Type[PIntervalEvent]] = None,
|
|
1257
|
+
**kwargs,
|
|
1258
|
+
):
|
|
1259
|
+
self._performance: Optional[ptp.Performance] = None
|
|
1260
|
+
super().__init__(
|
|
1261
|
+
data=data,
|
|
1262
|
+
instant_event_type=instant_event_type,
|
|
1263
|
+
interval_event_type=interval_event_type,
|
|
1264
|
+
**kwargs,
|
|
1265
|
+
)
|
|
1266
|
+
self.ppq_values = []
|
|
1267
|
+
pnotes = set()
|
|
1268
|
+
for ppart in self.data:
|
|
1269
|
+
self.ppq_values.append(get_divs_per_quarter(ppart))
|
|
1270
|
+
pnotes.update(get_performed_notes(ppart, ppart.ppq, ppart.mpq))
|
|
1271
|
+
self._events_by_type: dict[Type[ptp.PerformedNote], set[ptp.PerformedNote]] = {
|
|
1272
|
+
ptp.PerformedNote: pnotes,
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
@property
|
|
1276
|
+
def data(self):
|
|
1277
|
+
return self._data
|
|
1278
|
+
|
|
1279
|
+
@data.setter
|
|
1280
|
+
def data(
|
|
1281
|
+
self, data: ptp.Performance | ptp.PerformedPart | Iterable[ptp.PerformedPart]
|
|
1282
|
+
):
|
|
1283
|
+
if isinstance(data, ptp.Performance):
|
|
1284
|
+
self._data = data.performedparts
|
|
1285
|
+
self._performance = data
|
|
1286
|
+
else:
|
|
1287
|
+
parts = list(utils.make_argument_iterable(data))
|
|
1288
|
+
if not parts:
|
|
1289
|
+
raise ValueError(
|
|
1290
|
+
f"No parts provided to the {self.class_name} {self.id!r}."
|
|
1291
|
+
)
|
|
1292
|
+
self._data = []
|
|
1293
|
+
for p in parts:
|
|
1294
|
+
if isinstance(p, ptp.PerformedPart):
|
|
1295
|
+
self._data.append(p)
|
|
1296
|
+
else:
|
|
1297
|
+
warnings.warn(
|
|
1298
|
+
f"Ignoring unsupported type {type(p).__name__} in data."
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
def iter_events(self, **kwargs) -> Iterator[PInstantEvent | PIntervalEvent]:
|
|
1302
|
+
yield from self.iter_interval_events(**kwargs)
|
|
1303
|
+
|
|
1304
|
+
def iter_instant_events(self):
|
|
1305
|
+
yield from []
|
|
1306
|
+
|
|
1307
|
+
def iter_raw_event_data(self, merge_tied_notes=True) -> Iterator[Any]:
|
|
1308
|
+
if len(set(self.ppq_values)) > 1:
|
|
1309
|
+
raise NotImplementedError(
|
|
1310
|
+
f"Performed parts need to have the same ppq value, got {self.ppq_values}"
|
|
1311
|
+
)
|
|
1312
|
+
for instances in self._events_by_type.values():
|
|
1313
|
+
yield from instances
|
|
1314
|
+
|
|
1315
|
+
def iter_raw_instant_event_data(self) -> Iterator[Any]:
|
|
1316
|
+
yield from []
|
|
1317
|
+
|
|
1318
|
+
def iter_raw_interval_event_data(self, merge_tied_notes=True) -> Iterator[Any]:
|
|
1319
|
+
yield from self.iter_raw_event_data()
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
class PartituraScoreSilo(
|
|
1323
|
+
_PartituraMixin,
|
|
1324
|
+
EventSilo[list[pts.Part], PartituraInstantEvent, PartituraIntervalEvent],
|
|
1325
|
+
):
|
|
1326
|
+
_default_instant_event_type = PartituraInstantEvent
|
|
1327
|
+
_default_interval_event_type = PartituraIntervalEvent
|
|
1328
|
+
|
|
1329
|
+
def __init__(
|
|
1330
|
+
self,
|
|
1331
|
+
data: pts.Score | pts.Part | Iterable[pts.Part],
|
|
1332
|
+
instant_event_type: Optional[Type[PInstantEvent]] = None,
|
|
1333
|
+
interval_event_type: Optional[Type[PIntervalEvent]] = None,
|
|
1334
|
+
**kwargs,
|
|
1335
|
+
):
|
|
1336
|
+
self._score: Optional[pts.Score] = None
|
|
1337
|
+
super().__init__(
|
|
1338
|
+
data=data,
|
|
1339
|
+
instant_event_type=instant_event_type,
|
|
1340
|
+
interval_event_type=interval_event_type,
|
|
1341
|
+
**kwargs,
|
|
1342
|
+
)
|
|
1343
|
+
self.ppq_values = [get_divs_per_quarter(part) for part in self.data]
|
|
1344
|
+
self._events_by_type: dict[Type[pts.TimedObject], set[pts.TimedObject]] = (
|
|
1345
|
+
get_event_dict(self._data)
|
|
1346
|
+
)
|
|
1347
|
+
|
|
1348
|
+
@property
|
|
1349
|
+
def data(self):
|
|
1350
|
+
return self._data
|
|
1351
|
+
|
|
1352
|
+
@data.setter
|
|
1353
|
+
def data(self, data: pts.Score | pts.Part | Iterable[pts.Part]):
|
|
1354
|
+
if isinstance(data, pts.Score):
|
|
1355
|
+
self._data = data.parts
|
|
1356
|
+
self._score = data
|
|
1357
|
+
else:
|
|
1358
|
+
parts = list(utils.make_argument_iterable(data))
|
|
1359
|
+
if not parts:
|
|
1360
|
+
raise ValueError(
|
|
1361
|
+
f"No parts provided to the {self.class_name} {self.id!r}."
|
|
1362
|
+
)
|
|
1363
|
+
self._data = []
|
|
1364
|
+
for p in parts:
|
|
1365
|
+
if isinstance(p, pts.Part):
|
|
1366
|
+
self._data.append(p)
|
|
1367
|
+
else:
|
|
1368
|
+
warnings.warn(
|
|
1369
|
+
f"Ignoring unsupported type {type(p).__name__} in data."
|
|
1370
|
+
)
|
|
1371
|
+
for part in self._data:
|
|
1372
|
+
pts.add_segments(part)
|
|
1373
|
+
|
|
1374
|
+
def make_df(
|
|
1375
|
+
self,
|
|
1376
|
+
note_array=True,
|
|
1377
|
+
rest_array=True,
|
|
1378
|
+
event_array=True,
|
|
1379
|
+
include_pitch_spelling=False,
|
|
1380
|
+
include_key_signature=False,
|
|
1381
|
+
include_time_signature=False,
|
|
1382
|
+
include_metrical_position=False,
|
|
1383
|
+
include_grace_notes=False,
|
|
1384
|
+
include_staff=False,
|
|
1385
|
+
include_divs_per_quarter=False,
|
|
1386
|
+
merge_tied_notes=True,
|
|
1387
|
+
collapse_rests=True,
|
|
1388
|
+
include_type=True,
|
|
1389
|
+
meta: Optional[Meta | dict] = None,
|
|
1390
|
+
) -> DF:
|
|
1391
|
+
return event_df_from_part_list(
|
|
1392
|
+
parts=self.data,
|
|
1393
|
+
note_array=note_array,
|
|
1394
|
+
rest_array=rest_array,
|
|
1395
|
+
event_array=event_array,
|
|
1396
|
+
include_pitch_spelling=include_pitch_spelling,
|
|
1397
|
+
include_key_signature=include_key_signature,
|
|
1398
|
+
include_time_signature=include_time_signature,
|
|
1399
|
+
include_metrical_position=include_metrical_position,
|
|
1400
|
+
include_grace_notes=include_grace_notes,
|
|
1401
|
+
include_staff=include_staff,
|
|
1402
|
+
include_divs_per_quarter=include_divs_per_quarter,
|
|
1403
|
+
merge_tied_notes=merge_tied_notes,
|
|
1404
|
+
collapse_rests=collapse_rests,
|
|
1405
|
+
include_type=include_type,
|
|
1406
|
+
meta=meta,
|
|
1407
|
+
)
|
|
1408
|
+
|
|
1409
|
+
@overload
|
|
1410
|
+
def get_part_maps(
|
|
1411
|
+
self, map_types: PartMap, part_index: Optional[Iterable[int | slice]] = None
|
|
1412
|
+
) -> dict[str, Interpolator]: ...
|
|
1413
|
+
|
|
1414
|
+
@overload
|
|
1415
|
+
def get_part_maps(
|
|
1416
|
+
self,
|
|
1417
|
+
map_types: Iterable[PartMap] | Literal[None],
|
|
1418
|
+
part_index: Optional[Iterable[int | slice]] = None,
|
|
1419
|
+
) -> dict[str, dict[PartMap, Interpolator]]: ...
|
|
1420
|
+
|
|
1421
|
+
def get_part_maps(
|
|
1422
|
+
self,
|
|
1423
|
+
map_types: Optional[PartMap | Iterable[PartMap]] = None,
|
|
1424
|
+
part_index: Optional[Iterable[int | slice]] = None,
|
|
1425
|
+
) -> dict[str, Interpolator | dict[PartMap, Interpolator]]:
|
|
1426
|
+
single_element = False
|
|
1427
|
+
if map_types is None:
|
|
1428
|
+
map_types = list(PartMap)
|
|
1429
|
+
else:
|
|
1430
|
+
single_element = isinstance(map_types, str)
|
|
1431
|
+
map_types = [PartMap(mt) for mt in utils.make_argument_iterable(map_types)]
|
|
1432
|
+
if part_index is None:
|
|
1433
|
+
parts = self.data
|
|
1434
|
+
else:
|
|
1435
|
+
idx = utils.make_argument_iterable(part_index)
|
|
1436
|
+
parts = [self.data[i] for i in idx]
|
|
1437
|
+
result = {} if single_element else defaultdict(dict)
|
|
1438
|
+
for part in parts:
|
|
1439
|
+
if single_element:
|
|
1440
|
+
result[part.id] = getattr(part, map_types[0].value)
|
|
1441
|
+
else:
|
|
1442
|
+
result[part.id] = {m: getattr(part, m.value) for m in map_types}
|
|
1443
|
+
return result
|
|
1444
|
+
|
|
1445
|
+
def iter_events(
|
|
1446
|
+
self, merge_tied_notes=True, **kwargs
|
|
1447
|
+
) -> Iterator[PInstantEvent | PIntervalEvent]:
|
|
1448
|
+
yield from self.iter_instant_events(**kwargs)
|
|
1449
|
+
yield from self.iter_interval_events(
|
|
1450
|
+
merge_tied_notes=merge_tied_notes, **kwargs
|
|
1451
|
+
)
|
|
1452
|
+
|
|
1453
|
+
def iter_interval_events(
|
|
1454
|
+
self, event_type: Optional[Type[Inst]] = None, merge_tied_notes=True, **kwargs
|
|
1455
|
+
) -> Iterator[Intv] | Iterator[PIntervalEvent]:
|
|
1456
|
+
id_prefix = kwargs.pop("id_prefix", f"{self.id}/ev")
|
|
1457
|
+
if event_type is None:
|
|
1458
|
+
event_type = self.interval_event_type
|
|
1459
|
+
for raw_interval_event in self.iter_raw_interval_event_data(
|
|
1460
|
+
merge_tied_notes=merge_tied_notes
|
|
1461
|
+
):
|
|
1462
|
+
yield event_type.from_event_data(
|
|
1463
|
+
raw_interval_event, id_prefix=id_prefix, **kwargs
|
|
1464
|
+
)
|
|
1465
|
+
|
|
1466
|
+
def iter_raw_event_data(self, merge_tied_notes=True) -> Iterator[Any]:
|
|
1467
|
+
div_pq_values = [get_divs_per_quarter(part) for part in self.data]
|
|
1468
|
+
if len(set(div_pq_values)) > 1:
|
|
1469
|
+
merged_parts = pts.merge_parts(self.data, reassign="staff")
|
|
1470
|
+
self.logger.warning(
|
|
1471
|
+
f"The parts had different quarter_durations ({div_pq_values}) and "
|
|
1472
|
+
f"had to be merged and converted to a quarter_duration of {get_divs_per_quarter(merged_parts)}"
|
|
1473
|
+
)
|
|
1474
|
+
merged_events = get_event_dict(merged_parts)
|
|
1475
|
+
event_iterator = merged_events.values()
|
|
1476
|
+
else:
|
|
1477
|
+
event_iterator = self._events_by_type.values()
|
|
1478
|
+
for instances in event_iterator:
|
|
1479
|
+
for instance in instances:
|
|
1480
|
+
instance_id = getattr(instance, "id", None)
|
|
1481
|
+
if instance.duration is not None and instance.duration < 0:
|
|
1482
|
+
self.logger.warning(
|
|
1483
|
+
f"Skipped {instance.__class__.__name__} @ {instance.start} because of "
|
|
1484
|
+
f"its negative duration {instance.duration}"
|
|
1485
|
+
)
|
|
1486
|
+
continue
|
|
1487
|
+
if merge_tied_notes and hasattr(instance, "tie_prev"):
|
|
1488
|
+
if instance.tie_prev is not None:
|
|
1489
|
+
obj_info = instance.__class__.__name__
|
|
1490
|
+
if instance_id is not None:
|
|
1491
|
+
obj_info += f" (ID {instance_id!r})"
|
|
1492
|
+
self.logger.debug(
|
|
1493
|
+
f"Skipped {obj_info} @ {instance.start} because "
|
|
1494
|
+
f"it is tied to the previous note and merge_tied_notes=True"
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
continue
|
|
1498
|
+
instance.length = instance.duration_tied
|
|
1499
|
+
else:
|
|
1500
|
+
instance.length = instance.duration
|
|
1501
|
+
yield instance
|
|
1502
|
+
|
|
1503
|
+
def iter_raw_instant_event_data(self) -> Iterator[Any]:
|
|
1504
|
+
yield from filter(lambda evt: evt.duration is None, self.iter_raw_event_data())
|
|
1505
|
+
|
|
1506
|
+
def iter_raw_interval_event_data(self, merge_tied_notes=True) -> Iterator[Any]:
|
|
1507
|
+
yield from filter(
|
|
1508
|
+
lambda evt: evt.duration is not None,
|
|
1509
|
+
self.iter_raw_event_data(merge_tied_notes=merge_tied_notes),
|
|
1510
|
+
)
|
|
1511
|
+
|
|
1512
|
+
|
|
1513
|
+
# endregion partitura parsing
|
|
1514
|
+
|
|
1515
|
+
if __name__ == "__main__":
|
|
1516
|
+
from processing.notebooks.midi_parsing import load_supra_midi
|
|
1517
|
+
|
|
1518
|
+
midi_df = load_supra_midi()
|
|
1519
|
+
ev_data = midi_df.loc[151]
|
|
1520
|
+
inst_ev = InstantEvent(ev_data, instant="absolute_time")
|
|
1521
|
+
print(inst_ev.instant)
|
|
1522
|
+
print(inst_ev)
|
|
1523
|
+
intv_ev = IntervalEvent(ev_data, start="absolute_time", length="duration")
|
|
1524
|
+
print(intv_ev)
|
|
1525
|
+
print(intv_ev.length)
|
|
1526
|
+
reg = get_registry_by_prefix("ev")
|
|
1527
|
+
print(reg.get("ev2"))
|