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
@@ -6,7 +6,7 @@ This module contains methods for parsing matchfiles
6
6
  import os
7
7
  from typing import Union, Tuple, Optional, Callable, List
8
8
  import warnings
9
-
9
+ from functools import partial
10
10
  import numpy as np
11
11
 
12
12
  from partitura import score
@@ -121,7 +121,6 @@ def get_version(line: str) -> Version:
121
121
  return version
122
122
 
123
123
  except MatchError:
124
-
125
124
  pass
126
125
 
127
126
  return version
@@ -196,14 +195,25 @@ def load_matchfile(
196
195
  if version < Version(1, 0, 0):
197
196
  from_matchline_methods = FROM_MATCHLINE_METHODSV0
198
197
 
199
- parsed_lines = [
200
- parse_matchline(line, from_matchline_methods, version) for line in raw_lines
201
- ]
202
-
203
- parsed_lines = [pl for pl in parsed_lines if pl is not None]
204
-
198
+ parsed_lines = list()
199
+ # Functionality to remove duplicate lines
200
+ len_raw_lines = len(raw_lines)
201
+ np_lines = np.array(raw_lines, dtype=str)
202
+ # Remove empty lines
203
+ np_lines = np_lines[np_lines != ""]
204
+ # Remove duplicate lines
205
+ _, idx = np.unique(np_lines, return_index=True)
206
+ np_lines = np_lines[np.sort(idx)]
207
+ # Parse lines
208
+ f = partial(
209
+ parse_matchline, version=version, from_matchline_methods=from_matchline_methods
210
+ )
211
+ f_vec = np.vectorize(f)
212
+ parsed_lines = f_vec(np_lines).tolist()
213
+ # Create MatchFile instance
205
214
  mf = MatchFile(lines=parsed_lines)
206
-
215
+ # Validate match for duplicate snote_ids or pnote_ids
216
+ validate_match_ids(mf)
207
217
  return mf
208
218
 
209
219
 
@@ -363,29 +373,38 @@ def performed_part_from_match(
363
373
  # PerformedNote instances for all MatchNotes
364
374
  notes = []
365
375
 
366
- first_note = next(mf.iter_notes(), None)
367
- if first_note and first_note_at_zero:
368
- offset = midi_ticks_to_seconds(first_note.Onset, mpq=mpq, ppq=ppq)
369
- offset_tick = first_note.Onset
370
- else:
371
- offset = 0
372
- offset_tick = 0
373
-
374
- notes = [
375
- dict(
376
- id=format_pnote_id(note.Id),
377
- midi_pitch=note.MidiPitch,
378
- note_on=midi_ticks_to_seconds(note.Onset, mpq, ppq) - offset,
379
- note_off=midi_ticks_to_seconds(note.Offset, mpq, ppq) - offset,
380
- note_on_tick=note.Onset - offset_tick,
381
- note_off_tick=note.Offset - offset_tick,
382
- sound_off=midi_ticks_to_seconds(note.Offset, mpq, ppq) - offset,
383
- velocity=note.Velocity,
384
- track=getattr(note, "Track", 0),
385
- channel=getattr(note, "Channel", 1),
376
+ notes = list()
377
+ note_onsets_in_secs = np.array(np.zeros(len(mf.notes)), dtype=float)
378
+ note_onsets_in_tick = np.array(np.zeros(len(mf.notes)), dtype=int)
379
+ for i, note in enumerate(mf.notes):
380
+ n_onset_sec = midi_ticks_to_seconds(note.Onset, mpq, ppq)
381
+ note_onsets_in_secs[i] = n_onset_sec
382
+ note_onsets_in_tick[i] = note.Onset
383
+ notes.append(
384
+ dict(
385
+ id=format_pnote_id(note.Id),
386
+ midi_pitch=note.MidiPitch,
387
+ note_on=n_onset_sec,
388
+ note_off=midi_ticks_to_seconds(note.Offset, mpq, ppq),
389
+ note_on_tick=note.Onset,
390
+ note_off_tick=note.Offset,
391
+ sound_off=midi_ticks_to_seconds(note.Offset, mpq, ppq),
392
+ velocity=note.Velocity,
393
+ track=getattr(note, "Track", 0),
394
+ channel=getattr(note, "Channel", 1),
395
+ )
386
396
  )
387
- for note in mf.notes
388
- ]
397
+ # Set first note_on to zero in ticks and seconds if first_note_at_zero
398
+ if first_note_at_zero and len(note_onsets_in_secs) > 0:
399
+ offset = note_onsets_in_secs.min()
400
+ offset_tick = note_onsets_in_tick.min()
401
+ if offset > 0 and offset_tick > 0:
402
+ for note in notes:
403
+ note["note_on"] -= offset
404
+ note["note_off"] -= offset
405
+ note["sound_off"] -= offset
406
+ note["note_on_tick"] -= offset_tick
407
+ note["note_off_tick"] -= offset_tick
389
408
 
390
409
  # SustainPedal instances for sustain pedal lines
391
410
  sustain_pedal = [
@@ -530,7 +549,6 @@ def part_from_matchfile(
530
549
  t = t - t % beats_map(min_time)
531
550
 
532
551
  for b0, b1 in iter_current_next(bars, end=bars[-1] + 1):
533
-
534
552
  bar_times.setdefault(b0, t)
535
553
  if t < 0:
536
554
  t = 0
@@ -683,7 +701,11 @@ def part_from_matchfile(
683
701
  # iterate over all notes in the Timeline that end at the starting point.
684
702
  for el in part_note.start.iter_ending(score.Note):
685
703
  if isinstance(el, score.Note):
686
- condition = el.step == note_attributes["step"] and el.octave == note_attributes["octave"] and el.alter == note_attributes["alter"]
704
+ condition = (
705
+ el.step == note_attributes["step"]
706
+ and el.octave == note_attributes["octave"]
707
+ and el.alter == note_attributes["alter"]
708
+ )
687
709
  if condition:
688
710
  el.tie_next = part_note
689
711
  part_note.tie_prev = el
@@ -691,11 +713,13 @@ def part_from_matchfile(
691
713
  break
692
714
  if not found:
693
715
  warnings.warn(
694
- "Tie information found, but no previous note found to tie to for note {}.".format(part_note.id)
716
+ "Tie information found, but no previous note found to tie to for note {}.".format(
717
+ part_note.id
718
+ )
695
719
  )
696
720
 
697
721
  # add time signatures
698
- for (ts_beat_time, ts_bar, tsg) in ts:
722
+ for ts_beat_time, ts_bar, tsg in ts:
699
723
  ts_beats = tsg.numerator
700
724
  ts_beat_type = tsg.denominator
701
725
  # check if time signature is in a known measure (from notes)
@@ -707,8 +731,7 @@ def part_from_matchfile(
707
731
  part.add(score.TimeSignature(ts_beats, ts_beat_type), bar_start_divs)
708
732
 
709
733
  # add key signatures
710
- for (ks_beat_time, ks_bar, keys) in mf.key_signatures:
711
-
734
+ for ks_beat_time, ks_bar, keys in mf.key_signatures:
712
735
  if ks_bar in bar_times.keys():
713
736
  bar_start_divs = int(divs * (bar_times[ks_bar] - offset)) # in quarters
714
737
  bar_start_divs = max(0, bar_start_divs)
@@ -827,7 +850,6 @@ def add_staffs(part: Part, split: int = 55, only_missing: bool = True) -> None:
827
850
  # assign staffs using a hard limit
828
851
  notes = part.notes_tied
829
852
  for n in notes:
830
-
831
853
  if only_missing and n.staff:
832
854
  continue
833
855
 
@@ -847,6 +869,66 @@ def add_staffs(part: Part, split: int = 55, only_missing: bool = True) -> None:
847
869
  part.add(score.Clef(staff=2, sign="F", line=4, octave_change=0), 0)
848
870
 
849
871
 
850
- if __name__ == "__main__":
872
+ def validate_match_ids(mf):
873
+ """
874
+ Check a matchfile for duplicate snote and note IDs.
875
+
876
+ This function will:
877
+ - remove all deletions with a score ID that occurs in multiple lines.
878
+ - remove all insertions with a performance ID that occurs in multiple lines.
879
+
880
+ Handles cases with conflicting match/insertion(s) and match/deletion(s) tuples
881
+ with any number of insertions or deletions and a single match by keeping
882
+ only the match.
883
+
884
+ Unhandled cases:
885
+ - multiple conflicting matches: all are kept.
886
+ - multiple insertions with the same ID: all are deleted.
887
+ - multiple deletions with the same ID: all are deleted.
851
888
 
889
+ Parameters
890
+ ----------
891
+ mf: MatchFile
892
+ MatchFile to validate
893
+
894
+ Returns
895
+ -------
896
+ Updates the representation of the matchfile by removing match lines.
897
+ """
898
+
899
+ # Check if the matchfile is valid (i.e. check for snote duplicates)
900
+ sids = np.array([n.Anchor for n in mf.snotes])
901
+ # First check if score ids are unique
902
+ sids_unique, counts = np.unique(sids, return_counts=True)
903
+ sids_to_check = sids_unique[np.where(counts > 1)[0]]
904
+ if len(sids_to_check) > 0:
905
+ indices_to_del = []
906
+ for i, line in enumerate(mf.lines):
907
+ if isinstance(line, BaseDeletionLine):
908
+ if line.Anchor in sids_to_check:
909
+ indices_to_del.append(i)
910
+ warnings.warn(
911
+ "Matchfile contains duplicate score notes. "
912
+ "Removing {} deletions.".format(len(indices_to_del))
913
+ )
914
+ mf.lines = np.delete(mf.lines, indices_to_del)
915
+
916
+ # Check if the matchfile is valid (i.e. check for performance note duplicates)
917
+ pids = np.array([n.Id for n in mf.notes])
918
+ pids_unique, counts = np.unique(pids, return_counts=True)
919
+ pids_to_check = pids_unique[np.where(counts > 1)[0]]
920
+ if len(pids_to_check) > 0:
921
+ indices_to_del = []
922
+ for i, line in enumerate(mf.lines):
923
+ if isinstance(line, BaseInsertionLine):
924
+ if line.Id in pids_to_check:
925
+ indices_to_del.append(i)
926
+ warnings.warn(
927
+ "Matchfile contains duplicate performance notes. "
928
+ "Removing {} insertions.".format(len(indices_to_del))
929
+ )
930
+ mf.lines = np.delete(mf.lines, indices_to_del)
931
+
932
+
933
+ if __name__ == "__main__":
852
934
  pass
partitura/io/importmei.py CHANGED
@@ -3,7 +3,9 @@
3
3
  """
4
4
  This module contains methods for importing MEI files.
5
5
  """
6
+ from collections import OrderedDict
6
7
  from lxml import etree
8
+ from fractions import Fraction
7
9
  from xmlschema.names import XML_NAMESPACE
8
10
  import partitura.score as score
9
11
  from partitura.utils.music import (
@@ -68,6 +70,11 @@ class MeiParser(object):
68
70
  self.parts = (
69
71
  None # parts get initialized in create_parts() and filled in fill_parts()
70
72
  )
73
+ # find the music tag inside the document
74
+ music_el = self.document.findall(self._ns_name("music", all=True))
75
+ if len(music_el) != 1:
76
+ raise Exception("Only MEI with a single <music> element are supported")
77
+ self.music_el = music_el[0]
71
78
  self.repetitions = (
72
79
  []
73
80
  ) # to be filled when we encounter repetitions and process in the end
@@ -78,20 +85,21 @@ class MeiParser(object):
78
85
 
79
86
  def create_parts(self):
80
87
  # handle main scoreDef info: create the part list
81
- main_partgroup_el = self.document.find(self._ns_name("staffGrp", all=True))
88
+ main_partgroup_el = self.music_el.find(self._ns_name("staffGrp", all=True))
82
89
  self.parts = self._handle_main_staff_group(main_partgroup_el)
83
90
 
84
91
  def fill_parts(self):
85
92
  # fill parts with the content of the score
86
- scores_el = self.document.findall(self._ns_name("score", all=True))
93
+ scores_el = self.music_el.findall(self._ns_name("score", all=True))
87
94
  if len(scores_el) != 1:
88
95
  raise Exception("Only MEI with a single score element are supported")
89
96
  sections_el = scores_el[0].findall(self._ns_name("section"))
90
97
  position = 0
98
+ measure_number = 1
91
99
  for section_el in sections_el:
92
100
  # insert in parts all elements except ties
93
- position = self._handle_section(
94
- section_el, list(score.iter_parts(self.parts)), position
101
+ position, measure_number = self._handle_section(
102
+ section_el, list(score.iter_parts(self.parts)), position, measure_number
95
103
  )
96
104
 
97
105
  # handles ties
@@ -351,27 +359,39 @@ class MeiParser(object):
351
359
  self._handle_clef(staffdef_el, position, part)
352
360
 
353
361
  def _intsymdur_from_symbolic(self, symbolic_dur):
354
- """Produce a int symbolic dur (e.g. 12 is a eight note triplet) and a dot number by looking at the symbolic dur dictionary:
355
- i.e., symbol, eventual tuplet ancestors."""
362
+ """Produce a int symbolic dur (e.g. 8 is a eight note), a dot number, and a tuplet modifier,
363
+ e.g., (2,3) means there are 3 notes in the space of 2 notes."""
356
364
  intsymdur = SYMBOLIC_TO_INT_DURS[symbolic_dur["type"]]
357
365
  # deals with tuplets
358
366
  if symbolic_dur.get("actual_notes") is not None:
359
367
  assert symbolic_dur.get("normal_notes") is not None
360
- intsymdur = (
361
- intsymdur * symbolic_dur["actual_notes"] / symbolic_dur["normal_notes"]
368
+ # intsymdur = (
369
+ # intsymdur * symbolic_dur["actual_notes"] / symbolic_dur["normal_notes"]
370
+ # )
371
+ tuplet_modifier = (
372
+ symbolic_dur["normal_notes"],
373
+ symbolic_dur["actual_notes"],
362
374
  )
375
+ else:
376
+ tuplet_modifier = None
363
377
  # deals with dots
364
378
  dots = symbolic_dur.get("dots") if symbolic_dur.get("dots") is not None else 0
365
- return intsymdur, dots
379
+ return intsymdur, dots, tuplet_modifier
366
380
 
367
381
  def _find_ppq(self):
368
382
  """Finds the ppq for MEI filed that do not explicitely encode this information"""
369
- els_with_dur = self.document.xpath(".//*[@dur]")
383
+ els_with_dur = self.music_el.xpath(".//*[@dur]")
370
384
  durs = []
371
385
  durs_ppq = []
372
386
  for el in els_with_dur:
373
387
  symbolic_duration = self._get_symbolic_duration(el)
374
- intsymdur, dots = self._intsymdur_from_symbolic(symbolic_duration)
388
+ intsymdur, dots, tuplet_mod = self._intsymdur_from_symbolic(
389
+ symbolic_duration
390
+ )
391
+ if tuplet_mod is not None:
392
+ # consider time modifications keeping the numerator of the minimized fraction
393
+ minimized_fraction = Fraction(intsymdur * tuplet_mod[1], tuplet_mod[0])
394
+ intsymdur = minimized_fraction.numerator
375
395
  # double the value if we have dots, to be sure be able to encode that with integers in partitura
376
396
  durs.append(intsymdur * (2**dots))
377
397
  durs_ppq.append(
@@ -489,7 +509,8 @@ class MeiParser(object):
489
509
 
490
510
  def _note_el_to_accid_int(self, note_el) -> int:
491
511
  """Accidental strings to integer pitch.
492
- It consider the two values of accid and accid.ges (when the accidental is implicit in the bar)"""
512
+ It consider the two values of accid and accid.ges (when the accidental is implicit in the bar)
513
+ """
493
514
  if note_el.get("accid") is not None:
494
515
  return SIGN_TO_ALTER[note_el.get("accid")]
495
516
  elif note_el.get("accid.ges") is not None:
@@ -573,9 +594,16 @@ class MeiParser(object):
573
594
  duration = 0 if el.get("grace") is not None else int(el.get("dur.ppq"))
574
595
  else:
575
596
  # compute the duration from the symbolic duration
576
- intsymdur, dots = self._intsymdur_from_symbolic(symbolic_duration)
597
+ intsymdur, dots, tuplet_mod = self._intsymdur_from_symbolic(
598
+ symbolic_duration
599
+ )
577
600
  divs = part._quarter_durations[0] # divs is the same as ppq
578
- duration = divs * 4 / intsymdur
601
+ if tuplet_mod is None:
602
+ tuplet_mod = (
603
+ 1,
604
+ 1,
605
+ ) # if no tuplet modifier, set one that does not change the duration
606
+ duration = (divs * 4 * tuplet_mod[0]) / (intsymdur * tuplet_mod[1])
579
607
  for d in range(dots):
580
608
  duration = duration + 0.5 * duration
581
609
  # sanity check to verify the divs are correctly set
@@ -727,6 +755,55 @@ class MeiParser(object):
727
755
  )
728
756
  # add mrest to the part
729
757
  part.add(rest, position, position + parts_per_measure)
758
+ # return duration to update the position in the layer
759
+ return position + parts_per_measure
760
+
761
+ def _handle_multirest(self, multirest_el, position, voice, staff, part):
762
+ """
763
+ Handles a rest that spawn multiple measures
764
+
765
+ Parameters
766
+ ----------
767
+ multirest_el : lxml tree
768
+ A mrest element in the lxml tree.
769
+ position : int
770
+ The current position on the timeline.
771
+ voice : int
772
+ The voice of the section.
773
+ staff : int
774
+ The current staff also refers to a Part.
775
+ part : Partitura.Part
776
+ The created part to add elements to.
777
+
778
+ Returns
779
+ -------
780
+ position + duration : int
781
+ Next position on the timeline.
782
+ """
783
+ # find id
784
+ multirest_id = multirest_el.attrib[self._ns_name("id", XML_NAMESPACE)]
785
+ # find how many measures
786
+ n_measures = int(multirest_el.attrib["num"])
787
+ if n_measures > 1:
788
+ raise Exception(
789
+ f"Multi-rests with more than 1 measure are not supported yet. Found one with {n_measures}."
790
+ )
791
+ # find closest time signature
792
+ last_ts = list(part.iter_all(cls=score.TimeSignature))[-1]
793
+ # find divs per measure
794
+ ppq = part.quarter_duration_map(position)
795
+ parts_per_measure = int(ppq * 4 * last_ts.beats / last_ts.beat_type)
796
+
797
+ # create dummy rest to insert in the timeline
798
+ rest = score.Rest(
799
+ id=multirest_id,
800
+ voice=voice,
801
+ staff=1,
802
+ symbolic_duration=estimate_symbolic_duration(parts_per_measure, ppq),
803
+ articulations=None,
804
+ )
805
+ # add mrest to the part
806
+ part.add(rest, position, position + parts_per_measure)
730
807
  # now iterate
731
808
  # return duration to update the position in the layer
732
809
  return position + parts_per_measure
@@ -779,7 +856,20 @@ class MeiParser(object):
779
856
 
780
857
  def _handle_space(self, e, position, part):
781
858
  """Moves current position."""
782
- space_id, duration, symbolic_duration = self._duration_info(e, part)
859
+ try:
860
+ space_id, duration, symbolic_duration = self._duration_info(e, part)
861
+ except (
862
+ KeyError
863
+ ): # if the space don't have a duration, move to the end of the measure
864
+ # find closest time signature
865
+ last_ts = list(part.iter_all(cls=score.TimeSignature))[-1]
866
+ # find divs per measure
867
+ ppq = part.quarter_duration_map(position)
868
+ parts_per_measure = int(ppq * 4 * last_ts.beats / last_ts.beat_type)
869
+ # find divs elapsed since last barline
870
+ last_barline = list(part.iter_all(cls=pt.score.Measure))[-1]
871
+ duration = position - last_barline.start.t
872
+
783
873
  return position + duration
784
874
 
785
875
  def _handle_barline_symbols(self, measure_el, position: int, left_or_right: str):
@@ -822,6 +912,12 @@ class MeiParser(object):
822
912
  new_position = self._handle_mrest(
823
913
  e, position, ind_layer, ind_staff, part
824
914
  )
915
+ elif e.tag == self._ns_name(
916
+ "multiRest"
917
+ ): # rest that spawn more than one measure
918
+ new_position = self._handle_multirest(
919
+ e, position, ind_layer, ind_staff, part
920
+ )
825
921
  elif e.tag == self._ns_name("beam"):
826
922
  # TODO : add Beam element
827
923
  # recursive call to the elements inside beam
@@ -845,7 +941,14 @@ class MeiParser(object):
845
941
  position = new_position
846
942
  return position
847
943
 
848
- def _handle_staff_in_measure(self, staff_el, staff_ind, position: int, part):
944
+ def _handle_staff_in_measure(
945
+ self,
946
+ staff_el,
947
+ staff_ind,
948
+ position: int,
949
+ part: pt.score.Part,
950
+ measure_number: int,
951
+ ):
849
952
  """
850
953
  Handles staffs inside a measure element.
851
954
 
@@ -859,6 +962,9 @@ class MeiParser(object):
859
962
  The current position on the timeline.
860
963
  part : Partitura.Part
861
964
  The created partitura part object.
965
+ measure_number : int
966
+ The number of the measure. This number is independent of the measure name specified in the score.
967
+ It starts from 1 and always increases by 1 at each measure
862
968
 
863
969
  Returns
864
970
  -------
@@ -866,7 +972,9 @@ class MeiParser(object):
866
972
  The final position on the timeline.
867
973
  """
868
974
  # add measure
869
- measure = score.Measure(number=staff_el.getparent().get("n"))
975
+ measure = score.Measure(
976
+ number=measure_number, name=staff_el.getparent().get("n")
977
+ )
870
978
  part.add(measure, position)
871
979
 
872
980
  layers_el = staff_el.findall(self._ns_name("layer"))
@@ -920,7 +1028,7 @@ class MeiParser(object):
920
1028
  for dir_el in dir_els:
921
1029
  self._handle_dir_element(dir_el, position)
922
1030
 
923
- def _handle_section(self, section_el, parts, position: int):
1031
+ def _handle_section(self, section_el, parts, position: int, measure_number: int):
924
1032
  """
925
1033
  Returns position and fills parts with elements.
926
1034
 
@@ -932,11 +1040,15 @@ class MeiParser(object):
932
1040
  A list of partitura Parts.
933
1041
  position : int
934
1042
  The current position on the timeline.
1043
+ measure_number : int
1044
+ The current measure_number
935
1045
 
936
1046
  Returns
937
1047
  -------
938
1048
  position : int
939
1049
  The end position of the section.
1050
+ measure_number : int
1051
+ The number of the last measure.
940
1052
  """
941
1053
  for i_el, element in enumerate(section_el):
942
1054
  # handle measures
@@ -950,7 +1062,9 @@ class MeiParser(object):
950
1062
  end_positions = []
951
1063
  for i_s, (part, staff_el) in enumerate(zip(parts, staves_el)):
952
1064
  end_positions.append(
953
- self._handle_staff_in_measure(staff_el, i_s + 1, position, part)
1065
+ self._handle_staff_in_measure(
1066
+ staff_el, i_s + 1, position, part, measure_number
1067
+ )
954
1068
  )
955
1069
  # handle directives (dir elements)
956
1070
  self._handle_directives(element, position)
@@ -960,19 +1074,19 @@ class MeiParser(object):
960
1074
  warnings.warn(
961
1075
  f"Warning : parts have measures of different duration in measure {element.attrib[self._ns_name('id',XML_NAMESPACE)]}"
962
1076
  )
963
- # enlarge measures to the max
964
- for part in parts:
965
- last_measure = list(part.iter_all(pt.score.Measure))[-1]
966
- if last_measure.end.t != max_position:
967
- part.add(
968
- pt.score.Measure(number=last_measure.number),
969
- position,
970
- max_position,
971
- )
972
- part.remove(last_measure)
1077
+ # # enlarge measures to the max
1078
+ # for part in parts:
1079
+ # last_measure = list(part.iter_all(pt.score.Measure))[-1]
1080
+ # if last_measure.end.t != max_position:
1081
+ # part.add(
1082
+ # pt.score.Measure(number=last_measure.number),
1083
+ # max_position
1084
+ # )
1085
+ # part.remove(last_measure)
973
1086
  position = max_position
974
1087
  # handle right barline symbol
975
1088
  self._handle_barline_symbols(element, position, "right")
1089
+ measure_number += 1
976
1090
  # handle staffDef elements
977
1091
  elif element.tag == self._ns_name("scoreDef"):
978
1092
  # meter modifications
@@ -989,10 +1103,14 @@ class MeiParser(object):
989
1103
  self._handle_keysig(element, position, part)
990
1104
  # handle nested section
991
1105
  elif element.tag == self._ns_name("section"):
992
- position = self._handle_section(element, parts, position)
1106
+ position, measure_number = self._handle_section(
1107
+ element, parts, position, measure_number
1108
+ )
993
1109
  elif element.tag == self._ns_name("ending"):
994
1110
  ending_start = position
995
- position = self._handle_section(element, parts, position)
1111
+ position, measure_number = self._handle_section(
1112
+ element, parts, position, measure_number
1113
+ )
996
1114
  # insert the ending element
997
1115
  ending_number = int(re.sub("[^0-9]", "", element.attrib["n"]))
998
1116
  self._add_ending(ending_start, position, ending_number, parts)
@@ -1008,7 +1126,7 @@ class MeiParser(object):
1008
1126
  else:
1009
1127
  raise Exception(f"element {element.tag} is not yet supported")
1010
1128
 
1011
- return position
1129
+ return position, measure_number
1012
1130
 
1013
1131
  def _add_ending(self, start_ending, end_ending, ending_string, parts):
1014
1132
  for part in score.iter_parts(parts):
@@ -1023,7 +1141,7 @@ class MeiParser(object):
1023
1141
  all_notes = [
1024
1142
  note
1025
1143
  for part in score.iter_parts(part_list)
1026
- for note in part.iter_all(cls=score.Note)
1144
+ for note in part.iter_all(cls=score.Note, include_subclasses=True)
1027
1145
  ]
1028
1146
  all_notes_dict = {note.id: note for note in all_notes}
1029
1147
  for tie_el in ties_el:
@@ -1051,6 +1169,7 @@ class MeiParser(object):
1051
1169
  "WARNING : unmatched repetitions. adding a repetition start at position 0"
1052
1170
  )
1053
1171
  self.repetitions.insert(0, {"type": "start", "pos": 0})
1172
+
1054
1173
  status = "stop"
1055
1174
  sanitized_repetition_list = []
1056
1175
  # check if start-stop are alternate
@@ -1081,11 +1200,19 @@ class MeiParser(object):
1081
1200
  # check if ending with a start
1082
1201
  if sanitized_repetition_list[-1] == "start":
1083
1202
  print("WARNING : unmatched repetitions. Ignoring last start")
1203
+ ## sanitize the found repetitions to remove duplicates
1204
+ sanitized_repetition_list = list(
1205
+ OrderedDict(
1206
+ (tuple(d.items()), d) for d in sanitized_repetition_list
1207
+ ).values()
1208
+ )
1084
1209
  self.repetitions = sanitized_repetition_list
1085
1210
 
1086
1211
  ## insert the repetitions to all parts
1087
1212
  for rep_start, rep_stop in zip(self.repetitions[:-1:2], self.repetitions[1::2]):
1088
- assert rep_start["type"] == "start" and rep_stop["type"] == "stop"
1213
+ assert (
1214
+ rep_start["type"] == "start" and rep_stop["type"] == "stop"
1215
+ ), "Something wrong with repetitions"
1089
1216
  for part in score.iter_parts(self.parts):
1090
1217
  part.add(score.Repeat(), rep_start["pos"], rep_stop["pos"])
1091
1218