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.
- partitura/directions.py +3 -0
- partitura/display.py +0 -1
- partitura/io/__init__.py +41 -35
- partitura/io/exportmatch.py +52 -10
- partitura/io/exportmidi.py +37 -19
- partitura/io/exportmusicxml.py +6 -92
- partitura/io/exportparangonada.py +18 -19
- partitura/io/importkern.py +2 -4
- partitura/io/importmatch.py +121 -39
- partitura/io/importmei.py +161 -34
- partitura/io/importmidi.py +23 -14
- partitura/io/importmusic21.py +0 -1
- partitura/io/importmusicxml.py +48 -63
- partitura/io/importparangonada.py +0 -1
- partitura/io/matchfile_base.py +0 -21
- partitura/io/matchfile_utils.py +29 -17
- partitura/io/matchlines_v0.py +0 -22
- partitura/io/matchlines_v1.py +8 -42
- partitura/io/musescore.py +68 -41
- partitura/musicanalysis/__init__.py +1 -1
- partitura/musicanalysis/note_array_to_score.py +147 -92
- partitura/musicanalysis/note_features.py +66 -51
- partitura/musicanalysis/performance_codec.py +140 -96
- partitura/musicanalysis/performance_features.py +190 -129
- partitura/musicanalysis/pitch_spelling.py +0 -2
- partitura/musicanalysis/tonal_tension.py +0 -6
- partitura/musicanalysis/voice_separation.py +1 -22
- partitura/performance.py +178 -5
- partitura/score.py +154 -74
- partitura/utils/__init__.py +1 -1
- partitura/utils/generic.py +3 -7
- partitura/utils/misc.py +0 -1
- partitura/utils/music.py +108 -66
- partitura/utils/normalize.py +75 -35
- partitura/utils/synth.py +1 -7
- {partitura-1.3.0.dist-info → partitura-1.4.0.dist-info}/METADATA +2 -2
- partitura-1.4.0.dist-info/RECORD +51 -0
- {partitura-1.3.0.dist-info → partitura-1.4.0.dist-info}/WHEEL +1 -1
- partitura-1.3.0.dist-info/RECORD +0 -51
- {partitura-1.3.0.dist-info → partitura-1.4.0.dist-info}/LICENSE +0 -0
- {partitura-1.3.0.dist-info → partitura-1.4.0.dist-info}/top_level.txt +0 -0
partitura/io/importmatch.py
CHANGED
|
@@ -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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
355
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|