timetoalign 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tta/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"))