partitura 1.3.0__py3-none-any.whl → 1.4.0__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.
Files changed (41) hide show
  1. partitura/directions.py +3 -0
  2. partitura/display.py +0 -1
  3. partitura/io/__init__.py +41 -35
  4. partitura/io/exportmatch.py +52 -10
  5. partitura/io/exportmidi.py +37 -19
  6. partitura/io/exportmusicxml.py +6 -92
  7. partitura/io/exportparangonada.py +18 -19
  8. partitura/io/importkern.py +2 -4
  9. partitura/io/importmatch.py +121 -39
  10. partitura/io/importmei.py +161 -34
  11. partitura/io/importmidi.py +23 -14
  12. partitura/io/importmusic21.py +0 -1
  13. partitura/io/importmusicxml.py +48 -63
  14. partitura/io/importparangonada.py +0 -1
  15. partitura/io/matchfile_base.py +0 -21
  16. partitura/io/matchfile_utils.py +29 -17
  17. partitura/io/matchlines_v0.py +0 -22
  18. partitura/io/matchlines_v1.py +8 -42
  19. partitura/io/musescore.py +68 -41
  20. partitura/musicanalysis/__init__.py +1 -1
  21. partitura/musicanalysis/note_array_to_score.py +147 -92
  22. partitura/musicanalysis/note_features.py +66 -51
  23. partitura/musicanalysis/performance_codec.py +140 -96
  24. partitura/musicanalysis/performance_features.py +190 -129
  25. partitura/musicanalysis/pitch_spelling.py +0 -2
  26. partitura/musicanalysis/tonal_tension.py +0 -6
  27. partitura/musicanalysis/voice_separation.py +1 -22
  28. partitura/performance.py +178 -5
  29. partitura/score.py +154 -74
  30. partitura/utils/__init__.py +1 -1
  31. partitura/utils/generic.py +3 -7
  32. partitura/utils/misc.py +0 -1
  33. partitura/utils/music.py +108 -66
  34. partitura/utils/normalize.py +75 -35
  35. partitura/utils/synth.py +1 -7
  36. {partitura-1.3.0.dist-info → partitura-1.4.0.dist-info}/METADATA +2 -2
  37. partitura-1.4.0.dist-info/RECORD +51 -0
  38. {partitura-1.3.0.dist-info → partitura-1.4.0.dist-info}/WHEEL +1 -1
  39. partitura-1.3.0.dist-info/RECORD +0 -51
  40. {partitura-1.3.0.dist-info → partitura-1.4.0.dist-info}/LICENSE +0 -0
  41. {partitura-1.3.0.dist-info → partitura-1.4.0.dist-info}/top_level.txt +0 -0
partitura/score.py CHANGED
@@ -9,14 +9,14 @@ object). This object serves as a timeline at which musical elements
9
9
  are registered in terms of their start and end times.
10
10
  """
11
11
 
12
- from copy import copy
12
+ from copy import copy, deepcopy
13
13
  from collections import defaultdict
14
14
  from collections.abc import Iterable
15
15
  from numbers import Number
16
16
 
17
17
  # import copy
18
18
  from partitura.utils.music import MUSICAL_BEATS, INTERVALCLASSES
19
- import warnings
19
+ import warnings, sys
20
20
  import numpy as np
21
21
  from scipy.interpolate import PPoly
22
22
  from typing import Union, List, Optional, Iterator, Iterable as Itertype
@@ -282,7 +282,7 @@ class Part(object):
282
282
  """
283
283
  # operations to avoid None values and filter them efficiently.
284
284
  m_it = self.measures
285
-
285
+
286
286
  measures = np.array(
287
287
  [
288
288
  [
@@ -387,7 +387,6 @@ class Part(object):
387
387
  return int_interp1d
388
388
 
389
389
  def _time_interpolator(self, quarter=False, inv=False, musical_beat=False):
390
-
391
390
  if len(self._points) < 2:
392
391
  return lambda x: np.zeros(len(x))
393
392
 
@@ -434,13 +433,11 @@ class Part(object):
434
433
  m1 = next(self.first_point.iter_starting(Measure), None)
435
434
 
436
435
  if m1 and m1.start is not None and m1.end is not None:
437
-
438
436
  f = interp1d(x, y)
439
437
  actual_dur = np.diff(f((m1.start.t, m1.end.t)))[0]
440
438
  ts = next(m1.start.iter_starting(TimeSignature), None)
441
439
 
442
440
  if ts:
443
-
444
441
  normal_dur = ts.beats
445
442
  if quarter:
446
443
  normal_dur *= 4 / ts.beat_type
@@ -618,6 +615,18 @@ class Part(object):
618
615
  """
619
616
  return [e for e in self.iter_all(LoudnessDirection, include_subclasses=True)]
620
617
 
618
+ @property
619
+ def tempo_directions(self):
620
+ """Return a list of all tempo direction in the part
621
+
622
+ Returns
623
+ -------
624
+ list
625
+ List of TempoDirection objects
626
+
627
+ """
628
+ return [e for e in self.iter_all(TempoDirection, include_subclasses=True)]
629
+
621
630
  @property
622
631
  def articulations(self):
623
632
  """Return a list of all Articulation markings in the part
@@ -643,9 +652,7 @@ class Part(object):
643
652
 
644
653
  """
645
654
  add_segments(self)
646
- return [
647
- e for e in self.iter_all(Segment, include_subclasses=False)
648
- ]
655
+ return [e for e in self.iter_all(Segment, include_subclasses=False)]
649
656
 
650
657
  def quarter_durations(self, start=None, end=None):
651
658
  """Return an Nx2 array with quarter duration (second column)
@@ -1459,7 +1466,6 @@ class TimePoint(ComparableMixin):
1459
1466
  ]
1460
1467
 
1461
1468
  if ending_items:
1462
-
1463
1469
  result.append("{}".format(tree).rstrip())
1464
1470
 
1465
1471
  if starting_items:
@@ -1472,7 +1478,6 @@ class TimePoint(ComparableMixin):
1472
1478
  result.append("{}".format(tree).rstrip())
1473
1479
 
1474
1480
  for i, item in enumerate(ending_items):
1475
-
1476
1481
  if i == (len(ending_items) - 1):
1477
1482
  tree.last_item()
1478
1483
  else:
@@ -1483,7 +1488,6 @@ class TimePoint(ComparableMixin):
1483
1488
  tree.pop()
1484
1489
 
1485
1490
  if starting_items:
1486
-
1487
1491
  result.append("{}".format(tree).rstrip())
1488
1492
  tree.last_item()
1489
1493
  result.append("{}starting objects".format(tree))
@@ -1491,7 +1495,6 @@ class TimePoint(ComparableMixin):
1491
1495
  result.append("{}".format(tree).rstrip())
1492
1496
 
1493
1497
  for i, item in enumerate(starting_items):
1494
-
1495
1498
  if i == (len(starting_items) - 1):
1496
1499
  tree.last_item()
1497
1500
  else:
@@ -2160,7 +2163,6 @@ class Clef(TimedObject):
2160
2163
  """
2161
2164
 
2162
2165
  def __init__(self, staff, sign, line, octave_change):
2163
-
2164
2166
  super().__init__()
2165
2167
  self.staff = staff
2166
2168
  self.sign = sign
@@ -2572,7 +2574,6 @@ class Staff(TimedObject):
2572
2574
  return f"{super().__str__()} number={self.number} lines={self.lines}"
2573
2575
 
2574
2576
 
2575
-
2576
2577
  class KeySignature(TimedObject):
2577
2578
  """Key signature.
2578
2579
 
@@ -2677,7 +2678,6 @@ class Words(TimedObject):
2677
2678
  return f'{super().__str__()} "{self.text}"'
2678
2679
 
2679
2680
 
2680
-
2681
2681
  class OctaveShiftDirection(TimedObject):
2682
2682
  """An octave shift direction.
2683
2683
 
@@ -2685,6 +2685,7 @@ class OctaveShiftDirection(TimedObject):
2685
2685
  ----------
2686
2686
 
2687
2687
  """
2688
+
2688
2689
  def __init__(self, shift_type, shift_size=8, staff=None):
2689
2690
  super().__init__()
2690
2691
  self.shift_type = shift_type
@@ -2699,16 +2700,16 @@ class OctaveShiftDirection(TimedObject):
2699
2700
  class Harmony(TimedObject):
2700
2701
  """A harmony element in the score not currently used.
2701
2702
 
2702
- Parameters
2703
- ----------
2704
- text : str
2705
- The harmony text
2703
+ Parameters
2704
+ ----------
2705
+ text : str
2706
+ The harmony text
2706
2707
 
2707
- Attributes
2708
- ----------
2709
- text : str
2710
- See parameters
2711
- """
2708
+ Attributes
2709
+ ----------
2710
+ text : str
2711
+ See parameters
2712
+ """
2712
2713
 
2713
2714
  def __init__(self, text):
2714
2715
  super().__init__()
@@ -2744,6 +2745,7 @@ class RomanNumeral(TimedObject):
2744
2745
 
2745
2746
  class ChordSymbol(TimedObject):
2746
2747
  """A harmony element in the score usually for Chord Symbols."""
2748
+
2747
2749
  def __init__(self, root, kind, bass=None):
2748
2750
  super().__init__()
2749
2751
  self.kind = kind
@@ -2767,6 +2769,7 @@ class Interval(object):
2767
2769
  direction : str
2768
2770
  The interval direction (e.g. up, down)
2769
2771
  """
2772
+
2770
2773
  def __init__(self, number, quality, direction="up"):
2771
2774
  self.number = number
2772
2775
  self.quality = quality
@@ -2776,8 +2779,13 @@ class Interval(object):
2776
2779
  def validate(self):
2777
2780
  number = self.number % 7
2778
2781
  number = 7 if number == 0 else number
2779
- assert self.quality+str(number) in INTERVALCLASSES, f"Interval {number}{self.quality} not found"
2780
- assert self.direction in ["up", "down"], f"Interval direction {self.direction} not found"
2782
+ assert (
2783
+ self.quality + str(number) in INTERVALCLASSES
2784
+ ), f"Interval {number}{self.quality} not found"
2785
+ assert self.direction in [
2786
+ "up",
2787
+ "down",
2788
+ ], f"Interval direction {self.direction} not found"
2781
2789
 
2782
2790
  def __str__(self):
2783
2791
  return f'{super().__str__()} "{self.number}{self.quality}"'
@@ -2994,8 +3002,16 @@ class Score(object):
2994
3002
  the identifier should not start with a number.
2995
3003
  partlist : `Part`, `PartGroup` or list of `Part` or `PartGroup` instances.
2996
3004
  List of `Part` or `PartGroup` objects.
2997
- title: str, optional
2998
- Title of the score.
3005
+ work_title: str, optional
3006
+ Work title of the score, if applicable.
3007
+ work_number: str, optional
3008
+ Work number of the score, if applicable.
3009
+ movement_title: str, optional
3010
+ Movement title of the score, if applicable.
3011
+ movement_number: str, optional
3012
+ Movement number of the score, if applicable.
3013
+ title : str, optional
3014
+ Title of the score, from <credit-words> tag
2999
3015
  subtitle: str, optional
3000
3016
  Subtitle of the score.
3001
3017
  composer: str, optional
@@ -3014,7 +3030,13 @@ class Score(object):
3014
3030
  part_structure: list of `Part` or `PartGrop`
3015
3031
  List of all `Part` or `PartGroup` objects that specify the structure of
3016
3032
  the score.
3017
- title: str
3033
+ work_title: str
3034
+ See parameters.
3035
+ work_number: str
3036
+ See parameters.
3037
+ movement_title: str
3038
+ See parameters.
3039
+ movement_number: str
3018
3040
  See parameters.
3019
3041
  subtitle: str
3020
3042
  See parameters.
@@ -3028,6 +3050,10 @@ class Score(object):
3028
3050
  """
3029
3051
 
3030
3052
  id: Optional[str]
3053
+ work_title: Optional[str]
3054
+ work_number: Optional[str]
3055
+ movement_title: Optional[str]
3056
+ movement_number: Optional[str]
3031
3057
  title: Optional[str]
3032
3058
  subtitle: Optional[str]
3033
3059
  composer: Optional[str]
@@ -3040,6 +3066,10 @@ class Score(object):
3040
3066
  self,
3041
3067
  partlist: Union[Part, PartGroup, Itertype[Union[Part, PartGroup]]],
3042
3068
  id: Optional[str] = None,
3069
+ work_title: Optional[str] = None,
3070
+ work_number: Optional[str] = None,
3071
+ movement_title: Optional[str] = None,
3072
+ movement_number: Optional[str] = None,
3043
3073
  title: Optional[str] = None,
3044
3074
  subtitle: Optional[str] = None,
3045
3075
  composer: Optional[str] = None,
@@ -3049,6 +3079,10 @@ class Score(object):
3049
3079
  self.id = id
3050
3080
 
3051
3081
  # Score Information (default from MuseScore/MusicXML)
3082
+ self.work_title = work_title
3083
+ self.work_number = work_number
3084
+ self.movement_title = movement_title
3085
+ self.movement_number = movement_number
3052
3086
  self.title = title
3053
3087
  self.subtitle = subtitle
3054
3088
  self.composer = composer
@@ -3181,7 +3215,6 @@ class ScoreVariant(object):
3181
3215
  tp_new = part.get_or_add_point(tp.t + delta)
3182
3216
  o_gen = (o for oo in tp.starting_objects.values() for o in oo)
3183
3217
  for o in o_gen:
3184
-
3185
3218
  # special cases:
3186
3219
 
3187
3220
  # don't include some TimedObjects in the unfolded part
@@ -3326,7 +3359,6 @@ def add_measures(part):
3326
3359
  pos = ts_start
3327
3360
 
3328
3361
  while pos < ts_end:
3329
-
3330
3362
  measure_start = pos
3331
3363
  measure_end_beats = min(beat_map(pos) + measure_dur, beat_map(end))
3332
3364
  measure_end = min(ts_end, inv_beat_map(measure_end_beats))
@@ -3348,7 +3380,11 @@ def add_measures(part):
3348
3380
  else:
3349
3381
  measure_end = existing_measure.start.t
3350
3382
 
3351
- part.add(Measure(number=mcounter, name=str(mcounter)), int(measure_start), int(measure_end))
3383
+ part.add(
3384
+ Measure(number=mcounter, name=str(mcounter)),
3385
+ int(measure_start),
3386
+ int(measure_end),
3387
+ )
3352
3388
 
3353
3389
  # if measure exists but was not at measure_start,
3354
3390
  # a filler measure is added with number mcounter
@@ -3586,7 +3622,6 @@ def tie_notes(part):
3586
3622
  succeeded = 0
3587
3623
  for i, note in enumerate(list(part.iter_all(Note))):
3588
3624
  if note.symbolic_duration is None:
3589
-
3590
3625
  splits = find_tie_split(
3591
3626
  note.start.t, note.end.t, int(divs_map(note.start.t)), max_splits
3592
3627
  )
@@ -3626,29 +3661,22 @@ def _set_end_times(part, cls):
3626
3661
  t = None
3627
3662
 
3628
3663
  for obj in part.iter_all(cls, include_subclasses=True):
3629
-
3630
3664
  if obj.start == t:
3631
-
3632
3665
  if obj.end is None:
3633
-
3634
3666
  acc.append(obj)
3635
3667
 
3636
3668
  else:
3637
-
3638
3669
  for o in acc:
3639
-
3640
3670
  part.add(o, end=obj.start.t)
3641
3671
 
3642
3672
  acc = []
3643
3673
 
3644
3674
  if obj.end is None:
3645
-
3646
3675
  acc.append(obj)
3647
3676
 
3648
3677
  t = obj.start
3649
3678
 
3650
3679
  for o in acc:
3651
-
3652
3680
  part.add(o, end=part.last_point.t)
3653
3681
 
3654
3682
 
@@ -3728,7 +3756,6 @@ def find_tuplets(part):
3728
3756
 
3729
3757
  # 1. group consecutive notes without symbolic_duration
3730
3758
  for note in part.iter_all(GenericNote, include_subclasses=True):
3731
-
3732
3759
  if note.symbolic_duration is None:
3733
3760
  if note.start.t == prev_end:
3734
3761
  candidates[-1].append(note)
@@ -3738,10 +3765,8 @@ def find_tuplets(part):
3738
3765
 
3739
3766
  # 2. within each group
3740
3767
  for group in candidates:
3741
-
3742
3768
  # 3. search for the predefined list of tuplets
3743
3769
  for actual_notes in search_for_tuplets:
3744
-
3745
3770
  if actual_notes > len(group):
3746
3771
  # tuplet requires more notes than we have
3747
3772
  continue
@@ -3759,7 +3784,6 @@ def find_tuplets(part):
3759
3784
  # continue
3760
3785
  tup_start += 1
3761
3786
  else:
3762
-
3763
3787
  start = note_tuplet[0].start.t
3764
3788
  end = note_tuplet[-1].end.t
3765
3789
  total_dur = end - start
@@ -3813,7 +3837,6 @@ def sanitize_part(part, tie_tolerance=0):
3813
3837
  for no in part.iter_all(
3814
3838
  Note, include_subclasses=False, start=gn.start.t, end=gn.start.t + 1
3815
3839
  ):
3816
-
3817
3840
  if no.voice == gn.voice:
3818
3841
  gn.last_grace_note_in_seq.grace_next = no
3819
3842
 
@@ -4004,7 +4027,6 @@ def add_segments(part):
4004
4027
 
4005
4028
  # loop through the boundaries at the end of current segment
4006
4029
  for boundary_type in boundaries[se].keys():
4007
-
4008
4030
  # REPEATS
4009
4031
  if boundary_type == "repeat_start":
4010
4032
  segment_info[ss]["to"].append(segment_info[se]["ID"])
@@ -4073,7 +4095,7 @@ def add_segments(part):
4073
4095
  )
4074
4096
 
4075
4097
  # NAVIGATION SYMBOLS
4076
-
4098
+
4077
4099
  # Navigation1_ = destinations that should only be used after all others
4078
4100
  # Navigation2_ = destinations that are used *after* a jump
4079
4101
  if boundary_type == "coda":
@@ -4103,10 +4125,10 @@ def add_segments(part):
4103
4125
  # find the segno and jump there
4104
4126
  segno_time = destinations["segno"][0]
4105
4127
  segment_info[ss]["to"].append(
4106
- "Navigation1_" + segment_info[segno_time]["ID"]
4128
+ "Navigation1_" + segment_info[segno_time]["ID"]
4107
4129
  )
4108
4130
  segment_info[ss]["to"].append(
4109
- "Navigation2_" + segment_info[se]["ID"]
4131
+ "Navigation2_" + segment_info[se]["ID"]
4110
4132
  )
4111
4133
  segment_info[ss]["type"] = "leap_start"
4112
4134
  segment_info[ss]["info"].append("dal segno")
@@ -4118,7 +4140,7 @@ def add_segments(part):
4118
4140
  "Navigation1_" + segment_info[part.first_point.t]["ID"]
4119
4141
  )
4120
4142
  segment_info[ss]["to"].append(
4121
- "Navigation2_" + segment_info[se]["ID"]
4143
+ "Navigation2_" + segment_info[se]["ID"]
4122
4144
  )
4123
4145
  segment_info[ss]["type"] = "leap_start"
4124
4146
  segment_info[ss]["info"].append("da capo")
@@ -4240,8 +4262,9 @@ def pretty_segments(part):
4240
4262
  + str(part.beat_map(segments[p].end.t))
4241
4263
  )
4242
4264
  + "\t duration: "
4243
- + "{:<6}".format(str(part.beat_map(segments[p].end.t) - \
4244
- part.beat_map(segments[p].start.t)))
4265
+ + "{:<6}".format(
4266
+ str(part.beat_map(segments[p].end.t) - part.beat_map(segments[p].start.t))
4267
+ )
4245
4268
  + "\t info: "
4246
4269
  + str(segments[p].info)
4247
4270
  for p in segments.keys()
@@ -4279,7 +4302,6 @@ class Path:
4279
4302
  all_repeats=False,
4280
4303
  jumped=False,
4281
4304
  ):
4282
-
4283
4305
  self.path = path_list
4284
4306
  self.segments = segments
4285
4307
  if used_segment_jumps is None:
@@ -4353,7 +4375,7 @@ class Path:
4353
4375
  for key in self.used_segment_jumps:
4354
4376
  for used_dest in self.used_segment_jumps[key]:
4355
4377
  new_path.used_segment_jumps[key].append(used_dest)
4356
-
4378
+
4357
4379
  return new_path
4358
4380
 
4359
4381
  def make_copy_with_jump_to(self, destination, ignore_leap_info=True):
@@ -4366,7 +4388,7 @@ class Path:
4366
4388
  new_path = self.copy()
4367
4389
  new_path.used_segment_jumps[new_path.path[-1]].append(destination)
4368
4390
  new_path.path.append(destination)
4369
-
4391
+
4370
4392
  if (
4371
4393
  new_path.segments[destination].type == "leap_end"
4372
4394
  and new_path.segments[new_path.path[-2]].type == "leap_start"
@@ -4386,25 +4408,25 @@ class Path:
4386
4408
  # delete used destinations
4387
4409
  new_path.used_segment_jumps[segid] = list()
4388
4410
  # add the jump destination to the used ones
4389
- new_path.used_segment_jumps[new_path.path[-2]].append(destination)
4390
-
4411
+ new_path.used_segment_jumps[new_path.path[-2]].append(destination)
4412
+
4391
4413
  if not ignore_leap_info:
4392
4414
  new_path.no_repeats = True
4393
4415
  return new_path
4394
4416
 
4395
4417
  @property
4396
- def list_of_destinations_from_last_segment(self):
4418
+ def list_of_destinations_from_last_segment(self):
4397
4419
  destinations = list(self.segments[self.path[-1]].to)
4398
4420
  previously_used_destinations = self.used_segment_jumps[self.path[-1]]
4399
4421
  # only continue in order of the sequence, after full consumption, start at zero
4400
4422
  # if the full or minimal sequence is forced,
4401
4423
  # return only the single possible jump destination, else return possibly many.
4402
-
4403
- if len(previously_used_destinations) != 0:
4424
+
4425
+ if len(previously_used_destinations) != 0:
4404
4426
  last_destination = previously_used_destinations[-1]
4405
4427
  last_destination_count = previously_used_destinations.count(
4406
4428
  last_destination
4407
- )
4429
+ )
4408
4430
  last_destination_index = [
4409
4431
  i for i, n in enumerate(destinations * 100) if n == last_destination
4410
4432
  ][last_destination_count - 1]
@@ -4604,15 +4626,15 @@ def iter_unfolded_parts(part, update_ids=True):
4604
4626
 
4605
4627
 
4606
4628
  # UPDATED VERSION
4607
- def unfold_part_maximal(part, update_ids=True, ignore_leaps=True):
4608
- """Return the "maximally" unfolded part, that is, a copy of the
4629
+ def unfold_part_maximal(score: ScoreLike, update_ids=True, ignore_leaps=True):
4630
+ """Return the "maximally" unfolded part/score, that is, a copy of the
4609
4631
  part where all segments marked with repeat signs are included
4610
4632
  twice.
4611
4633
 
4612
4634
  Parameters
4613
4635
  ----------
4614
- part : :class:`Part`
4615
- The Part to unfold.
4636
+ score : ScoreLike
4637
+ The Part/Score to unfold.
4616
4638
  update_ids : bool (optional)
4617
4639
  Update note ids to reflect the repetitions. Note IDs will have
4618
4640
  a '-<repetition number>', e.g., 'n132-1' and 'n132-2'
@@ -4625,19 +4647,75 @@ def unfold_part_maximal(part, update_ids=True, ignore_leaps=True):
4625
4647
 
4626
4648
  Returns
4627
4649
  -------
4628
- unfolded_part : :class:`Part`
4629
- The unfolded Part
4650
+ unfolded_part : ScoreLike
4651
+ The unfolded Part/Score
4630
4652
 
4631
4653
  """
4654
+ if isinstance(score, Score):
4655
+ # Copy needs to be deep, otherwise the recursion limit will be exceeded
4656
+ old_recursion_depth = sys.getrecursionlimit()
4657
+ sys.setrecursionlimit(10000)
4658
+ # Deep copy of score
4659
+ new_score = deepcopy(score)
4660
+ # Reset recursion limit to previous value to avoid side effects
4661
+ sys.setrecursionlimit(old_recursion_depth)
4662
+ new_partlist = list()
4663
+ for score in new_score.parts:
4664
+ unfolded_part = unfold_part_maximal(
4665
+ score, update_ids=update_ids, ignore_leaps=ignore_leaps
4666
+ )
4667
+ new_partlist.append(unfolded_part)
4668
+ new_score.parts = new_partlist
4669
+ return new_score
4632
4670
 
4633
4671
  paths = get_paths(
4634
- part, no_repeats=False, all_repeats=True, ignore_leap_info=ignore_leaps
4672
+ score, no_repeats=False, all_repeats=True, ignore_leap_info=ignore_leaps
4635
4673
  )
4636
4674
 
4637
- unfolded_part = new_part_from_path(paths[0], part, update_ids=update_ids)
4675
+ unfolded_part = new_part_from_path(paths[0], score, update_ids=update_ids)
4638
4676
  return unfolded_part
4639
4677
 
4640
4678
 
4679
+ def unfold_part_minimal(score: ScoreLike):
4680
+ """Return the "minimally" unfolded score/part, that is, a copy of the
4681
+ part where all segments marked with repeat are included only once.
4682
+ For voltas only the last volta segment is included.
4683
+ Note this might not be musically valid, e.g. a passing a "fine" even a first time will stop this unfolding.
4684
+ Warning: The unfolding of repeats is computed part-wise, inconsistent repeat markings of parts of a single result
4685
+ in inconsistent unfoldings.
4686
+
4687
+ Parameters
4688
+ ----------
4689
+ score: ScoreLike
4690
+ The score/part to unfold.
4691
+
4692
+ Returns
4693
+ -------
4694
+ unfolded_score : ScoreLike
4695
+ The unfolded Part
4696
+
4697
+ """
4698
+ if isinstance(score, Score):
4699
+ # Copy needs to be deep, otherwise the recursion limit will be exceeded
4700
+ old_recursion_depth = sys.getrecursionlimit()
4701
+ sys.setrecursionlimit(10000)
4702
+ # Deep copy of score
4703
+ unfolded_score = deepcopy(score)
4704
+ # Reset recursion limit to previous value to avoid side effects
4705
+ sys.setrecursionlimit(old_recursion_depth)
4706
+ new_partlist = list()
4707
+ for part in unfolded_score.parts:
4708
+ unfolded_part = unfold_part_minimal(part)
4709
+ new_partlist.append(unfolded_part)
4710
+ unfolded_score.parts = new_partlist
4711
+ return unfolded_score
4712
+
4713
+ paths = get_paths(score, no_repeats=True, all_repeats=False, ignore_leap_info=True)
4714
+
4715
+ unfolded_score = new_part_from_path(paths[0], score, update_ids=False)
4716
+ return unfolded_score
4717
+
4718
+
4641
4719
  # UPDATED / UNCHANGED VERSION
4642
4720
  def unfold_part_alignment(part, alignment):
4643
4721
  """Return the unfolded part given an alignment, that is, a copy
@@ -4858,13 +4936,15 @@ def merge_parts(parts, reassign="voice"):
4858
4936
  e.voice = e.voice + sum(maximum_voices[:p_ind])
4859
4937
  elif reassign == "staff":
4860
4938
  if isinstance(e, (GenericNote, Words, Direction)):
4861
-
4862
- e.staff = (e.staff if e.staff is not None else 1) + sum(maximum_staves[:p_ind])
4939
+ e.staff = (e.staff if e.staff is not None else 1) + sum(
4940
+ maximum_staves[:p_ind]
4941
+ )
4863
4942
  elif isinstance(
4864
4943
  e, Clef
4865
4944
  ): # TODO: to update if "number" get changed in "staff"
4866
-
4867
- e.staff = (e.staff if e.staff is not None else 1) + sum(maximum_staves[:p_ind])
4945
+ e.staff = (e.staff if e.staff is not None else 1) + sum(
4946
+ maximum_staves[:p_ind]
4947
+ )
4868
4948
  new_part.add(e, start=new_start, end=new_end)
4869
4949
 
4870
4950
  # new_part.add(copy.deepcopy(e), start=new_start, end=new_end)
@@ -79,5 +79,5 @@ __all__ = [
79
79
  "show_diff",
80
80
  "PrettyPrintTree",
81
81
  "synthesize",
82
- "normalize"
82
+ "normalize",
83
83
  ]
@@ -114,18 +114,15 @@ def iter_current_next(iterable, start=_sentinel, end=_sentinel):
114
114
 
115
115
  cur = start
116
116
  try:
117
-
118
117
  if cur is _sentinel:
119
118
  cur = next(iterable)
120
119
 
121
120
  while True:
122
-
123
121
  nxt = next(iterable)
124
122
  yield (cur, nxt)
125
123
  cur = nxt
126
124
 
127
125
  except StopIteration:
128
-
129
126
  if cur is not _sentinel and end is not _sentinel:
130
127
  yield (cur, end)
131
128
 
@@ -570,14 +567,12 @@ def interp1d(
570
567
  )
571
568
 
572
569
  else:
573
-
574
570
  # If there is only one value for x and y, assume that the method
575
571
  # will always return the same value for any input.
576
572
 
577
573
  def interp_fun(
578
574
  input_var: Union[float, int, np.ndarray]
579
575
  ) -> Callable[[Union[float, int, np.ndarray]], np.ndarray]:
580
-
581
576
  if y.ndim > 1:
582
577
  result = np.broadcast_to(y, (len(np.atleast_1d(input_var)), y.shape[1]))
583
578
  else:
@@ -635,7 +630,7 @@ def monotonize_times(
635
630
  s_mono: np.ndarray
636
631
  a monotonic sequence that has been linearly interpolated using a subset of s
637
632
  x_mono: np.ndarray
638
- The input of the monotonic sequence.
633
+ The input of the monotonic sequence.
639
634
  """
640
635
  eps = np.finfo(float).eps
641
636
 
@@ -649,9 +644,10 @@ def monotonize_times(
649
644
  mask = np.r_[False, True, (np.diff(s_mono) != 0), False]
650
645
  x_mono = _x[1:-1]
651
646
  s_mono = interp1d(_x[mask], _s[mask], fill_value="extrapolate")(x_mono)
652
-
647
+
653
648
  return s_mono, x_mono
654
649
 
650
+
655
651
  if __name__ == "__main__":
656
652
  import doctest
657
653
 
partitura/utils/misc.py CHANGED
@@ -239,7 +239,6 @@ def concatenate_images(
239
239
  anchor_x = 0
240
240
  anchor_y = 0
241
241
  for img, size in zip(images, image_sizes):
242
-
243
242
  new_image.paste(img, (anchor_x, anchor_y))
244
243
 
245
244
  # update coordinates according to the concatenation mode