partitura 1.3.1__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.
partitura/io/musescore.py CHANGED
@@ -36,38 +36,70 @@ class FileImportException(Exception):
36
36
  pass
37
37
 
38
38
 
39
- def find_musescore3():
40
- # # possible way to detect MuseScore... executable
41
- # for p in os.environ['PATH'].split(':'):
42
- # c = glob.glob(os.path.join(p, 'MuseScore*'))
43
- # if c:
44
- # print(c)
45
- # break
46
-
47
- result = shutil.which("musescore")
48
-
49
- if result is None:
50
- result = shutil.which("musescore3")
51
-
52
- if result is None:
53
- result = shutil.which("mscore")
54
-
39
+ def find_musescore_version(version=4):
40
+ """Find the path to the MuseScore executable for a specific version.
41
+ If version is a empty string it tries to find an unspecified version of
42
+ MuseScore which is used in some systems.
43
+ """
44
+ result = shutil.which(f"musescore{version}")
55
45
  if result is None:
56
- result = shutil.which("mscore3")
57
-
46
+ result = shutil.which(f"mscore{version}")
58
47
  if result is None:
59
48
  if platform.system() == "Linux":
60
49
  pass
61
-
62
50
  elif platform.system() == "Darwin":
63
- result = shutil.which("/Applications/MuseScore 3.app/Contents/MacOS/mscore")
64
-
51
+ result = shutil.which(
52
+ f"/Applications/MuseScore {version}.app/Contents/MacOS/mscore"
53
+ )
65
54
  elif platform.system() == "Windows":
66
- result = shutil.which(r"C:\Program Files\MuseScore 3\bin\MuseScore3.exe")
55
+ result = shutil.which(
56
+ rf"C:\Program Files\MuseScore {version}\bin\MuseScore{version}.exe"
57
+ )
67
58
 
68
59
  return result
69
60
 
70
61
 
62
+ def find_musescore():
63
+ """Find the path to the MuseScore executable.
64
+
65
+ This function first tries to find the executable for MuseScore 4,
66
+ then for MuseScore 3, and finally for any version of MuseScore.
67
+
68
+ Returns
69
+ -------
70
+ str
71
+ Path to the MuseScore executable
72
+
73
+ Raises
74
+ ------
75
+ MuseScoreNotFoundException
76
+ When no MuseScore executable was found
77
+ """
78
+
79
+ mscore_exec = find_musescore_version(version=4)
80
+ if not mscore_exec:
81
+ mscore_exec = find_musescore_version(version=3)
82
+ if mscore_exec:
83
+ warnings.warn(
84
+ "Only Musescore 3 is installed. Consider upgrading to musescore 4."
85
+ )
86
+ else:
87
+ mscore_exec = find_musescore_version(version="")
88
+ if mscore_exec:
89
+ warnings.warn(
90
+ "A unspecified version of MuseScore was found. Consider upgrading to musescore 4."
91
+ )
92
+ else:
93
+ raise MuseScoreNotFoundException()
94
+ # check if a screen is available (only on Linux)
95
+ if "DISPLAY" not in os.environ and platform.system() == "Linux":
96
+ raise MuseScoreNotFoundException(
97
+ "Musescore Executable was found, but a screen is missing. Musescore needs a screen to load scores"
98
+ )
99
+
100
+ return mscore_exec
101
+
102
+
71
103
  @deprecated_alias(fn="filename")
72
104
  @deprecated_parameter("ensure_list")
73
105
  def load_via_musescore(
@@ -103,15 +135,22 @@ or a list of these
103
135
  One or more part or partgroup objects
104
136
 
105
137
  """
138
+ if filename.endswith(".mscz"):
139
+ pass
140
+ else:
141
+ # open the file as text and check if the first symbol is "<" to avoid
142
+ # further processing in case of non-XML files
143
+ with open(filename, "r") as f:
144
+ if f.read(1) != "<":
145
+ raise FileImportException(
146
+ "File {} is not a valid XML file.".format(filename)
147
+ )
106
148
 
107
- mscore_exec = find_musescore3()
108
-
109
- if not mscore_exec:
110
- raise MuseScoreNotFoundException()
149
+ mscore_exec = find_musescore()
111
150
 
112
151
  xml_fh = os.path.splitext(os.path.basename(filename))[0] + ".musicxml"
113
152
 
114
- cmd = [mscore_exec, "-o", xml_fh, filename]
153
+ cmd = [mscore_exec, "-o", xml_fh, filename, "-f"]
115
154
 
116
155
  try:
117
156
  ps = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
@@ -167,10 +206,7 @@ def render_musescore(
167
206
  out : Optional[PathLike]
168
207
  Path to the output generated image (or None if no image was generated)
169
208
  """
170
- mscore_exec = find_musescore3()
171
-
172
- if not mscore_exec:
173
- return None
209
+ mscore_exec = find_musescore()
174
210
 
175
211
  if fmt not in ("png", "pdf"):
176
212
  warnings.warn("warning: unsupported output format")
@@ -193,6 +229,7 @@ def render_musescore(
193
229
  "-o",
194
230
  os.fspath(img_fh),
195
231
  os.fspath(xml_fh),
232
+ "-f",
196
233
  ]
197
234
  try:
198
235
  ps = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
partitura/performance.py CHANGED
@@ -85,7 +85,11 @@ class PerformedPart(object):
85
85
  super().__init__()
86
86
  self.id = id
87
87
  self.part_name = part_name
88
- self.notes = notes
88
+ self.notes = list(
89
+ map(
90
+ lambda n: n if isinstance(n, PerformedNote) else PerformedNote(n), notes
91
+ )
92
+ )
89
93
  self.controls = controls or []
90
94
  self.programs = programs or []
91
95
  self.ppq = ppq
@@ -203,7 +207,11 @@ class PerformedPart(object):
203
207
  if "id" not in note_array.dtype.names:
204
208
  n_ids = ["n{0}".format(i) for i in range(len(note_array))]
205
209
  else:
206
- n_ids = note_array["id"]
210
+ # Check if all ids are the same
211
+ if np.all(note_array["id"] == note_array["id"][0]):
212
+ n_ids = ["n{0}".format(i) for i in range(len(note_array))]
213
+ else:
214
+ n_ids = note_array["id"]
207
215
 
208
216
  if "track" not in note_array.dtype.names:
209
217
  tracks = np.zeros(len(note_array), dtype=int)
@@ -280,6 +288,174 @@ def adjust_offsets_w_sustain(
280
288
  note["sound_off"] = offset
281
289
 
282
290
 
291
+ class PerformedNote:
292
+ """
293
+ A dictionary-like object representing a performed note.
294
+
295
+ Parameters
296
+ ----------
297
+ pnote_dict : dict
298
+ A dictionary containing performed note information.
299
+ This information can contain the following fields:
300
+ "id", "pitch", "note_on", "note_off", "velocity", "track", "channel", "sound_off".
301
+ If not provided, the default values will be used.
302
+ Pitch, note_on, and note_off are required.
303
+ """
304
+
305
+ def __init__(self, pnote_dict):
306
+ self.pnote_dict = pnote_dict
307
+ self.pnote_dict["id"] = self.pnote_dict.get("id", None)
308
+ self.pnote_dict["pitch"] = self.pnote_dict.get("pitch", self["midi_pitch"])
309
+ self.pnote_dict["note_on"] = self.pnote_dict.get("note_on", -1)
310
+ self.pnote_dict["note_off"] = self.pnote_dict.get("note_off", -1)
311
+ self.pnote_dict["sound_off"] = self.pnote_dict.get(
312
+ "sound_off", self["note_off"]
313
+ )
314
+ self.pnote_dict["track"] = self.pnote_dict.get("track", 0)
315
+ self.pnote_dict["channel"] = self.pnote_dict.get("channel", 1)
316
+ self.pnote_dict["velocity"] = self.pnote_dict.get("velocity", 60)
317
+ self._validate_values(pnote_dict)
318
+ self._accepted_keys = [
319
+ "id",
320
+ "pitch",
321
+ "note_on",
322
+ "note_off",
323
+ "velocity",
324
+ "track",
325
+ "channel",
326
+ "sound_off",
327
+ "note_on_tick",
328
+ "note_off_tick",
329
+ ]
330
+
331
+ def __str__(self):
332
+ return f"PerformedNote: {self['id']}"
333
+
334
+ def __eq__(self, other):
335
+ if not isinstance(PerformedNote):
336
+ return False
337
+ if not self.keys() == other.keys():
338
+ return False
339
+ return np.all(
340
+ np.array([self[k] == other[k] for k in self.keys() if k in other.keys()])
341
+ )
342
+
343
+ def keys(self):
344
+ return self.pnote_dict.keys()
345
+
346
+ def get(self, key, default=None):
347
+ return self.pnote_dict.get(key, default)
348
+
349
+ def __hash__(self):
350
+ return hash(self["id"])
351
+
352
+ def __lt__(self, other):
353
+ return self["note_on"] < other["note_on"]
354
+
355
+ def __le__(self, other):
356
+ return self["note_on"] <= other["note_on"]
357
+
358
+ def __gt__(self, other):
359
+ return self["note_on"] > other["note_on"]
360
+
361
+ def __ge__(self, other):
362
+ return self["note_on"] >= other["note_on"]
363
+
364
+ def __getitem__(self, key):
365
+ return self.pnote_dict.get(key, None)
366
+
367
+ def __setitem__(self, key, value):
368
+ if key not in self._accepted_keys:
369
+ raise KeyError(f"Key {key} not accepted for PerformedNote")
370
+ self._validate_values((key, value))
371
+ self.pnote_dict[key] = value
372
+
373
+ def __delitem__(self, key):
374
+ raise KeyError("Cannot delete items from PerformedNote")
375
+
376
+ def __iter__(self):
377
+ return iter(self.keys())
378
+
379
+ def __len__(self):
380
+ return len(self.keys())
381
+
382
+ def __contains__(self, key):
383
+ return key in self.keys()
384
+
385
+ def copy(self):
386
+ return PerformedNote(self.pnote_dict.copy())
387
+
388
+ def _validate_values(self, d):
389
+ if isinstance(d, dict):
390
+ dd = d
391
+ elif isinstance(d, tuple):
392
+ dd = {d[0]: d[1]}
393
+ else:
394
+ raise ValueError(f"Invalid value {d} provided for PerformedNote")
395
+
396
+ for key, value in dd.items():
397
+ if key == "pitch":
398
+ self._validate_pitch(value)
399
+ elif key == "note_on":
400
+ self._validate_note_on(value)
401
+ elif key == "note_off":
402
+ self._validate_note_off(value)
403
+ elif key == "velocity":
404
+ self._validate_velocity(value)
405
+ elif key == "sound_off":
406
+ self._validate_sound_off(value)
407
+ elif key == "note_on_tick":
408
+ self._validate_note_on_tick(value)
409
+ elif key == "note_off_tick":
410
+ self._validate_note_off_tick(value)
411
+
412
+ def _validate_sound_off(self, value):
413
+ if self.get("note_off", -1) < 0:
414
+ return
415
+ if value < 0:
416
+ raise ValueError(f"sound_off must be greater than or equal to 0")
417
+ if value < self.pnote_dict["note_off"]:
418
+ raise ValueError(f"sound_off must be greater or equal to note_off")
419
+
420
+ def _validate_note_on(self, value):
421
+ if value < 0:
422
+ raise ValueError(
423
+ f"Note on value provided is invalid, must be greater than or equal to 0"
424
+ )
425
+
426
+ def _validate_note_off(self, value):
427
+ if self.pnote_dict.get("note_on", -1) < 0:
428
+ return
429
+ if value < 0 or value < self.pnote_dict["note_on"]:
430
+ raise ValueError(
431
+ f"Note off value provided is invalid, "
432
+ f"must be a positive value greater than or equal to 0 and greater or equal to note_on"
433
+ )
434
+
435
+ def _validate_note_on_tick(self, value):
436
+ if value < 0:
437
+ raise ValueError(
438
+ f"Note on tick value provided is invalid, must be greater than or equal to 0"
439
+ )
440
+
441
+ def _validate_note_off_tick(self, value):
442
+ if self.pnote_dict.get("note_on_tick", -1) < 0:
443
+ return
444
+ if value < 0 or value < self.pnote_dict["note_on_tick"]:
445
+ raise ValueError(
446
+ f"Note off tick value provided is invalid, "
447
+ f"must be a positive value greater than or equal to 0 and greater or equal to note_on_tick"
448
+ )
449
+
450
+ def _validate_pitch(self, value):
451
+ if value > 127 or value < 0:
452
+ raise ValueError(f"pitch must be between 0 and 127")
453
+
454
+ def _validate_velocity(self, value):
455
+ if value > 127 or value < 0:
456
+ raise ValueError(f"velocity must be between 0 and 127")
457
+
458
+
283
459
  class Performance(object):
284
460
  """Main object for representing a performance.
285
461
 
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
@@ -615,6 +615,18 @@ class Part(object):
615
615
  """
616
616
  return [e for e in self.iter_all(LoudnessDirection, include_subclasses=True)]
617
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
+
618
630
  @property
619
631
  def articulations(self):
620
632
  """Return a list of all Articulation markings in the part
@@ -2990,8 +3002,16 @@ class Score(object):
2990
3002
  the identifier should not start with a number.
2991
3003
  partlist : `Part`, `PartGroup` or list of `Part` or `PartGroup` instances.
2992
3004
  List of `Part` or `PartGroup` objects.
2993
- title: str, optional
2994
- 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
2995
3015
  subtitle: str, optional
2996
3016
  Subtitle of the score.
2997
3017
  composer: str, optional
@@ -3010,7 +3030,13 @@ class Score(object):
3010
3030
  part_structure: list of `Part` or `PartGrop`
3011
3031
  List of all `Part` or `PartGroup` objects that specify the structure of
3012
3032
  the score.
3013
- 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
3014
3040
  See parameters.
3015
3041
  subtitle: str
3016
3042
  See parameters.
@@ -3024,6 +3050,10 @@ class Score(object):
3024
3050
  """
3025
3051
 
3026
3052
  id: Optional[str]
3053
+ work_title: Optional[str]
3054
+ work_number: Optional[str]
3055
+ movement_title: Optional[str]
3056
+ movement_number: Optional[str]
3027
3057
  title: Optional[str]
3028
3058
  subtitle: Optional[str]
3029
3059
  composer: Optional[str]
@@ -3036,6 +3066,10 @@ class Score(object):
3036
3066
  self,
3037
3067
  partlist: Union[Part, PartGroup, Itertype[Union[Part, PartGroup]]],
3038
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,
3039
3073
  title: Optional[str] = None,
3040
3074
  subtitle: Optional[str] = None,
3041
3075
  composer: Optional[str] = None,
@@ -3045,6 +3079,10 @@ class Score(object):
3045
3079
  self.id = id
3046
3080
 
3047
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
3048
3086
  self.title = title
3049
3087
  self.subtitle = subtitle
3050
3088
  self.composer = composer
@@ -4588,15 +4626,15 @@ def iter_unfolded_parts(part, update_ids=True):
4588
4626
 
4589
4627
 
4590
4628
  # UPDATED VERSION
4591
- def unfold_part_maximal(part, update_ids=True, ignore_leaps=True):
4592
- """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
4593
4631
  part where all segments marked with repeat signs are included
4594
4632
  twice.
4595
4633
 
4596
4634
  Parameters
4597
4635
  ----------
4598
- part : :class:`Part`
4599
- The Part to unfold.
4636
+ score : ScoreLike
4637
+ The Part/Score to unfold.
4600
4638
  update_ids : bool (optional)
4601
4639
  Update note ids to reflect the repetitions. Note IDs will have
4602
4640
  a '-<repetition number>', e.g., 'n132-1' and 'n132-2'
@@ -4609,19 +4647,75 @@ def unfold_part_maximal(part, update_ids=True, ignore_leaps=True):
4609
4647
 
4610
4648
  Returns
4611
4649
  -------
4612
- unfolded_part : :class:`Part`
4613
- The unfolded Part
4650
+ unfolded_part : ScoreLike
4651
+ The unfolded Part/Score
4614
4652
 
4615
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
4616
4670
 
4617
4671
  paths = get_paths(
4618
- 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
4619
4673
  )
4620
4674
 
4621
- 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)
4622
4676
  return unfolded_part
4623
4677
 
4624
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
+
4625
4719
  # UPDATED / UNCHANGED VERSION
4626
4720
  def unfold_part_alignment(part, alignment):
4627
4721
  """Return the unfolded part given an alignment, that is, a copy
partitura/utils/music.py CHANGED
@@ -488,8 +488,15 @@ def transpose(score: ScoreLike, interval: Interval) -> ScoreLike:
488
488
  Transposed score.
489
489
  """
490
490
  import partitura.score as s
491
+ import sys
491
492
 
493
+ # Copy needs to be deep, otherwise the recursion limit will be exceeded
494
+ old_recursion_depth = sys.getrecursionlimit()
495
+ sys.setrecursionlimit(10000)
496
+ # Deep copy of score
492
497
  new_score = copy.deepcopy(score)
498
+ # Reset recursion limit to previous value to avoid side effects
499
+ sys.setrecursionlimit(old_recursion_depth)
493
500
  if isinstance(score, s.Score):
494
501
  for part in new_score.parts:
495
502
  transpose(part, interval)
@@ -686,6 +693,8 @@ def midi_ticks_to_seconds(
686
693
 
687
694
  SIGN_TO_ALTER = {
688
695
  "n": 0,
696
+ "ns": 1,
697
+ "nf": -1,
689
698
  "#": 1,
690
699
  "s": 1,
691
700
  "ss": 2,
@@ -3252,18 +3261,17 @@ def slice_ppart_by_time(
3252
3261
  # create a new (empty) instance of a PerformedPart
3253
3262
  # single dummy note added to be able to set sustain_pedal_threshold in __init__
3254
3263
  # -> check `adjust_offsets_w_sustain` in partitura.performance
3255
- ppart_slice = PerformedPart([{"note_on": 0, "note_off": 0}])
3264
+ # ppart_slice = PerformedPart([{"note_on": 0, "note_off": 0, "pitch": 0}])
3256
3265
 
3257
3266
  # get ppq if PerformedPart contains it,
3258
3267
  # else skip time_tick info when e.g. created with 'load_performance_midi'
3259
3268
  try:
3260
3269
  ppq = ppart.ppq
3261
- ppart_slice.ppq = ppq
3262
3270
  except AttributeError:
3263
3271
  ppq = None
3264
3272
 
3273
+ controls_slice = []
3265
3274
  if ppart.controls:
3266
- controls_slice = []
3267
3275
  for cc in ppart.controls:
3268
3276
  if cc["time"] >= start_time and cc["time"] <= end_time:
3269
3277
  new_cc = cc.copy()
@@ -3271,10 +3279,9 @@ def slice_ppart_by_time(
3271
3279
  if ppq:
3272
3280
  new_cc["time_tick"] = int(2 * ppq * cc["time"])
3273
3281
  controls_slice.append(new_cc)
3274
- ppart_slice.controls = controls_slice
3275
3282
 
3283
+ programs_slice = []
3276
3284
  if ppart.programs:
3277
- programs_slice = []
3278
3285
  for pr in ppart.programs:
3279
3286
  if pr["time"] >= start_time and pr["time"] <= end_time:
3280
3287
  new_pr = pr.copy()
@@ -3282,7 +3289,6 @@ def slice_ppart_by_time(
3282
3289
  if ppq:
3283
3290
  new_pr["time_tick"] = int(2 * ppq * pr["time"])
3284
3291
  programs_slice.append(new_pr)
3285
- ppart_slice.programs = programs_slice
3286
3292
 
3287
3293
  notes_slice = []
3288
3294
  note_id = 0
@@ -3326,7 +3332,10 @@ def slice_ppart_by_time(
3326
3332
  else:
3327
3333
  break
3328
3334
 
3329
- ppart_slice.notes = notes_slice
3335
+ # Create slice PerformedPart
3336
+ ppart_slice = PerformedPart(
3337
+ notes=notes_slice, programs=programs_slice, controls=controls_slice, ppq=ppq
3338
+ )
3330
3339
 
3331
3340
  # set threshold property after creating notes list to update 'sound_offset' values
3332
3341
  ppart_slice.sustain_pedal_threshold = ppart.sustain_pedal_threshold
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: partitura
3
- Version: 1.3.1
3
+ Version: 1.4.0
4
4
  Summary: A package for handling symbolic musical information
5
5
  Home-page: https://github.com/CPJKU/partitura
6
- Author: Maarten Grachten, Carlos Cancino-Chacón, Silvan Peter, Emmanouil Karystinaios, Francesco Foscarin, Thassilo Gadermaier
6
+ Author: Maarten Grachten, Carlos Cancino-Chacón, Silvan Peter, Emmanouil Karystinaios, Francesco Foscarin, Thassilo Gadermaier, Patricia Hu
7
7
  Author-email: partitura-users@googlegroups.com
8
8
  License: Apache 2.0
9
9
  Keywords: music notation musicxml midi