diff --git a/.github/workflows/partitura_unittests.yml b/.github/workflows/partitura_unittests.yml index 8f3efe44..a0c2dd08 100644 --- a/.github/workflows/partitura_unittests.yml +++ b/.github/workflows/partitura_unittests.yml @@ -26,7 +26,7 @@ jobs: pip install -r requirements.txt pip install . - name: Install Optional dependencies - run: | + run: | pip install music21==8.3.0 Pillow==9.5.0 musescore==0.0.1 pip install miditok==2.0.6 tokenizers==0.13.3 - name: Run Tests diff --git a/CHANGES.md b/CHANGES.md index 0f73f721..7ce264e5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,33 @@ Release Notes ============= +Version 1.4.0 (Released on 2023-09-22) +-------------------------------------- + +New Features +------------ +* new class for performed notes +* minimal unfolding for part +* updated Musescore parser for version 4 +* `load_score` auto-selects parser based on file type +* new attributes for `Score` object for capturing meta information +* new score note attributes in matchfile export (`grace`, `voice_overlap`) +* new `tempo_indication` score property line in matchfile export + +Bug Fixes +------------ +* Fixed bug: #297 +* Fixed bug: #304 +* Fixed bug: #306 +* Fixed bug: #308 +* Fixed bug: #310 +* Fixed bug: #315 + +Other Changes +------------ +* new unit test for cross-staff beaming for musicxml + + Version 1.3.1 (Released on 2023-07-06) -------------------------------------- diff --git a/docs/source/conf.py b/docs/source/conf.py index f19709c9..f71055dd 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -29,9 +29,9 @@ # built documents. # # The short X.Y version. -version = "1.3.1" # pkg_resources.get_distribution("partitura").version +version = "1.4.0" # pkg_resources.get_distribution("partitura").version # The full version, including alpha/beta/rc tags. -release = "1.3.1" +release = "1.4.0" # # The full version, including alpha/beta/rc tags # release = pkg_resources.get_distribution("partitura").version diff --git a/partitura/directions.py b/partitura/directions.py index 80043d13..b52024f6 100644 --- a/partitura/directions.py +++ b/partitura/directions.py @@ -151,6 +151,8 @@ def unabbreviate(s): "adagio", "agitato", "andante", + "andante cantabile", + "andante amoroso", "andantino", "animato", "appassionato", @@ -193,6 +195,7 @@ def unabbreviate(s): "tranquilamente", "tranquilo", "recitativo", + "allegro moderato", r"/(vivo|vivacissimamente|vivace)/", r"/(allegro|allegretto)/", r"/(espressivo|espress\.?)/", diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index c142a681..4ad2cd47 100644 --- a/partitura/io/__init__.py +++ b/partitura/io/__init__.py @@ -4,6 +4,7 @@ This module contains methods for importing and exporting symbolic music formats. """ from typing import Union +import os from .importmusicxml import load_musicxml from .importmidi import load_score_midi, load_performance_midi @@ -35,7 +36,7 @@ def load_score(filename: PathLike, force_note_ids="keep") -> Score: """ Load a score format supported by partitura. Currently the accepted formats are MusicXML, MIDI, Kern and MEI, plus all formats for which - MuseScore has support import-support (requires MuseScore 3). + MuseScore has support import-support (requires MuseScore 4 or 3). Parameters ---------- @@ -54,20 +55,16 @@ def load_score(filename: PathLike, force_note_ids="keep") -> Score: scr: :class:`partitura.score.Score` A score instance. """ - part = None - # Catch exceptions - exception_dictionary = dict() - # Load MusicXML - try: + extension = os.path.splitext(filename)[-1].lower() + if extension in (".mxl", ".xml", ".musicxml"): + # Load MusicXML return load_musicxml( filename=filename, force_note_ids=force_note_ids, ) - except Exception as e: - exception_dictionary["MusicXML"] = e - # Load MIDI - try: + elif extension in [".midi", ".mid"]: + # Load MIDI if (force_note_ids is None) or (not force_note_ids): assign_note_ids = False else: @@ -76,44 +73,53 @@ def load_score(filename: PathLike, force_note_ids="keep") -> Score: filename=filename, assign_note_ids=assign_note_ids, ) - except Exception as e: - exception_dictionary["MIDI"] = e - # Load MEI - try: + elif extension in [".mei"]: + # Load MEI return load_mei(filename=filename) - except Exception as e: - exception_dictionary["MEI"] = e - # Load Kern - try: + elif extension in [".kern", ".krn"]: return load_kern( filename=filename, force_note_ids=force_note_ids, ) - except Exception as e: - exception_dictionary["Kern"] = e - # Load MuseScore - try: + elif extension in [ + ".mscz", + ".mscx", + ".musescore", + ".mscore", + ".ms", + ".kar", + ".md", + ".cap", + ".capx", + ".bww", + ".mgu", + ".sgu", + ".ove", + ".scw", + ".ptb", + ".gtp", + ".gp3", + ".gp4", + ".gp5", + ".gpx", + ".gp", + ]: + # Load MuseScore return load_via_musescore( filename=filename, force_note_ids=force_note_ids, ) - except Exception as e: - exception_dictionary["MuseScore"] = e - try: + elif extension in [".match"]: # Load the score information from a Matchfile - _, _, part = load_match( + _, _, score = load_match( filename=filename, create_score=True, ) - - except Exception as e: - exception_dictionary["matchfile"] = e - if part is None: - for score_format, exception in exception_dictionary.items(): - print(f"Error loading score as {score_format}:") - print(exception) - - raise NotSupportedFormatError + return score + else: + raise NotSupportedFormatError( + f"{extension} file extension is not supported. If this should be supported, consider editing partitura/io/__init__.py file" + ) def load_score_as_part(filename: PathLike) -> Part: diff --git a/partitura/io/exportmatch.py b/partitura/io/exportmatch.py index a23eee23..03e8ed3d 100644 --- a/partitura/io/exportmatch.py +++ b/partitura/io/exportmatch.py @@ -37,6 +37,7 @@ FractionalSymbolicDuration, MatchKeySignature, MatchTimeSignature, + MatchTempoIndication, Version, ) @@ -71,6 +72,8 @@ def matchfile_from_alignment( score_filename: Optional[PathLike] = None, performance_filename: Optional[PathLike] = None, assume_part_unfolded: bool = False, + tempo_indication: Optional[str] = None, + diff_score_version_notes: Optional[list] = None, version: Version = LATEST_VERSION, debug: bool = False, ) -> MatchFile: @@ -106,6 +109,10 @@ def matchfile_from_alignment( repetitions in the alignment. If False, the part will be automatically unfolded to have maximal coverage of the notes in the alignment. See `partitura.score.unfold_part_alignment`. + tempo_indication : str or None + The tempo direction indicated in the beginning of the score + diff_score_version_notes : list or None + A list of score notes that reflect a special score version (e.g., original edition/Erstdruck, Editors note etc.) version: Version Version of the match file. For now only 1.0.0 is supported. Returns @@ -199,7 +206,6 @@ def matchfile_from_alignment( # Score prop header lines scoreprop_lines = defaultdict(list) - # For score notes score_info = dict() # Info for sorting lines @@ -276,7 +282,6 @@ def matchfile_from_alignment( # Get all notes in the measure snotes = spart.iter_all(score.Note, m.start, m.end, include_subclasses=True) # Beginning of each measure - for snote in snotes: onset_divs, offset_divs = snote.start.t, snote.start.t + snote.duration_tied duration_divs = offset_divs - onset_divs @@ -324,6 +329,15 @@ def matchfile_from_alignment( if fermata is not None: score_attributes_list.append("fermata") + if isinstance(snote, score.GraceNote): + score_attributes_list.append("grace") + + if ( + diff_score_version_notes is not None + and snote.id in diff_score_version_notes + ): + score_attributes_list.append("diff_score_version") + score_info[snote.id] = MatchSnote( version=version, anchor=str(snote.id), @@ -346,6 +360,22 @@ def matchfile_from_alignment( ) snote_sort_info[snote.id] = (onset_beats, snote.doc_order) + # # NOTE time position is hardcoded, not pretty... Assumes there is only one tempo indication at the beginning of the score + if tempo_indication is not None: + score_tempo_direction_header = make_scoreprop( + version=version, + attribute="tempoIndication", + value=MatchTempoIndication( + tempo_indication, + is_list=False, + ), + measure=measure_starts[0][0], + beat=1, + offset=0, + time_in_beats=measure_starts[0][2], + ) + scoreprop_lines["tempo_indication"].append(score_tempo_direction_header) + perf_info = dict() pnote_sort_info = dict() for pnote in ppart.notes: @@ -372,6 +402,21 @@ def matchfile_from_alignment( sort_stime = [] note_lines = [] + + # Get ids of notes which voice overlap + sna = spart.note_array() + onset_pitch_slice = sna[["onset_div", "pitch"]] + uniques, counts = np.unique(onset_pitch_slice, return_counts=True) + duplicate_values = uniques[counts > 1] + duplicates = dict() + for v in duplicate_values: + idx = np.where(onset_pitch_slice == v)[0] + duplicates[tuple(v)] = idx + voice_overlap_note_ids = [] + if len(duplicates) > 0: + duplicate_idx = np.concatenate(np.array(list(duplicates.values()))).flatten() + voice_overlap_note_ids = list(sna[duplicate_idx]["id"]) + for al_note in alignment: label = al_note["label"] @@ -384,6 +429,8 @@ def matchfile_from_alignment( elif label == "deletion": snote = score_info[al_note["score_id"]] + if al_note["score_id"] in voice_overlap_note_ids: + snote.ScoreAttributesList.append("voice_overlap") deletion_line = MatchSnoteDeletion(version=version, snote=snote) note_lines.append(deletion_line) sort_stime.append(snote_sort_info[al_note["score_id"]]) @@ -441,6 +488,7 @@ def matchfile_from_alignment( "clock_rate", "key_signatures", "time_signatures", + "tempo_indication", ] all_match_lines = [] for h in header_order: @@ -537,7 +585,7 @@ def save_match( else: raise ValueError( "`performance_data` should be a `Performance`, a `PerformedPart`, or a " - f"list of `PerformedPart` objects, but is {type(score_data)}" + f"list of `PerformedPart` objects, but is {type(performance_data)}" ) # Get matchfile diff --git a/partitura/io/exportmidi.py b/partitura/io/exportmidi.py index 61960d99..c1639bb1 100644 --- a/partitura/io/exportmidi.py +++ b/partitura/io/exportmidi.py @@ -8,7 +8,7 @@ from collections import defaultdict, OrderedDict from typing import Optional, Iterable -from mido import MidiFile, MidiTrack, Message, MetaMessage +from mido import MidiFile, MidiTrack, Message, MetaMessage, merge_tracks import partitura.score as score from partitura.score import Score, Part, PartGroup, ScoreLike @@ -87,6 +87,7 @@ def save_performance_midi( mpq: int = 500000, ppq: int = 480, default_velocity: int = 64, + merge_tracks_save: Optional[bool] = False, ) -> Optional[MidiFile]: """Save a :class:`~partitura.performance.PerformedPart` or a :class:`~partitura.performance.Performance` as a MIDI file @@ -107,6 +108,8 @@ def save_performance_midi( default_velocity : int, optional A default velocity value (between 0 and 127) to be used for notes without a specified velocity. Defaults to 64. + merge_tracks_save : bool, optional + Determines whether midi tracks are merged when exporting to a midi file. Defaults to False. Returns ------- @@ -134,7 +137,6 @@ def save_performance_midi( ) track_events = defaultdict(lambda: defaultdict(list)) - for performed_part in performed_parts: for c in performed_part.controls: track = c.get("track", 0) @@ -217,6 +219,10 @@ def save_performance_midi( track.append(msg.copy(time=t_delta)) t_delta = 0 t = t_msg + + if merge_tracks_save and len(mf.tracks) > 1: + mf.tracks = [merge_tracks(mf.tracks)] + if out is not None: if hasattr(out, "write"): mf.save(file=out) diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index 04e96f8f..696b9331 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -6,7 +6,7 @@ import os from typing import Union, Tuple, Optional, Callable, List import warnings - +from functools import partial import numpy as np from partitura import score @@ -197,21 +197,23 @@ def load_matchfile( parsed_lines = list() # Functionality to remove duplicate lines - for i, line in enumerate(raw_lines): - if line in raw_lines[i + 1 :] and line != "": - warnings.warn(f"Duplicate line found in matchfile: {line}") - continue - parsed_line = parse_matchline(line, from_matchline_methods, version) - if parsed_line is None: - warnings.warn(f"Could not empty parse line: {line} ") - continue - parsed_lines.append(parsed_line) - + len_raw_lines = len(raw_lines) + np_lines = np.array(raw_lines, dtype=str) + # Remove empty lines + np_lines = np_lines[np_lines != ""] + # Remove duplicate lines + _, idx = np.unique(np_lines, return_index=True) + np_lines = np_lines[np.sort(idx)] + # Parse lines + f = partial( + parse_matchline, version=version, from_matchline_methods=from_matchline_methods + ) + f_vec = np.vectorize(f) + parsed_lines = f_vec(np_lines).tolist() + # Create MatchFile instance mf = MatchFile(lines=parsed_lines) - # Validate match for duplicate snote_ids or pnote_ids validate_match_ids(mf) - return mf @@ -371,29 +373,38 @@ def performed_part_from_match( # PerformedNote instances for all MatchNotes notes = [] - first_note = next(mf.iter_notes(), None) - if first_note and first_note_at_zero: - offset = midi_ticks_to_seconds(first_note.Onset, mpq=mpq, ppq=ppq) - offset_tick = first_note.Onset - else: - offset = 0 - offset_tick = 0 - - notes = [ - dict( - id=format_pnote_id(note.Id), - midi_pitch=note.MidiPitch, - note_on=midi_ticks_to_seconds(note.Onset, mpq, ppq) - offset, - note_off=midi_ticks_to_seconds(note.Offset, mpq, ppq) - offset, - note_on_tick=note.Onset - offset_tick, - note_off_tick=note.Offset - offset_tick, - sound_off=midi_ticks_to_seconds(note.Offset, mpq, ppq) - offset, - velocity=note.Velocity, - track=getattr(note, "Track", 0), - channel=getattr(note, "Channel", 1), + notes = list() + note_onsets_in_secs = np.array(np.zeros(len(mf.notes)), dtype=float) + note_onsets_in_tick = np.array(np.zeros(len(mf.notes)), dtype=int) + for i, note in enumerate(mf.notes): + n_onset_sec = midi_ticks_to_seconds(note.Onset, mpq, ppq) + note_onsets_in_secs[i] = n_onset_sec + note_onsets_in_tick[i] = note.Onset + notes.append( + dict( + id=format_pnote_id(note.Id), + midi_pitch=note.MidiPitch, + note_on=n_onset_sec, + note_off=midi_ticks_to_seconds(note.Offset, mpq, ppq), + note_on_tick=note.Onset, + note_off_tick=note.Offset, + sound_off=midi_ticks_to_seconds(note.Offset, mpq, ppq), + velocity=note.Velocity, + track=getattr(note, "Track", 0), + channel=getattr(note, "Channel", 1), + ) ) - for note in mf.notes - ] + # Set first note_on to zero in ticks and seconds if first_note_at_zero + if first_note_at_zero and len(note_onsets_in_secs) > 0: + offset = note_onsets_in_secs.min() + offset_tick = note_onsets_in_tick.min() + if offset > 0 and offset_tick > 0: + for note in notes: + note["note_on"] -= offset + note["note_off"] -= offset + note["sound_off"] -= offset + note["note_on_tick"] -= offset_tick + note["note_off_tick"] -= offset_tick # SustainPedal instances for sustain pedal lines sustain_pedal = [ diff --git a/partitura/io/importmei.py b/partitura/io/importmei.py index 4b012ddd..2f4ba704 100644 --- a/partitura/io/importmei.py +++ b/partitura/io/importmei.py @@ -3,7 +3,9 @@ """ This module contains methods for importing MEI files. """ +from collections import OrderedDict from lxml import etree +from fractions import Fraction from xmlschema.names import XML_NAMESPACE import partitura.score as score from partitura.utils.music import ( @@ -68,6 +70,11 @@ def __init__(self, mei_path: PathLike) -> None: self.parts = ( None # parts get initialized in create_parts() and filled in fill_parts() ) + # find the music tag inside the document + music_el = self.document.findall(self._ns_name("music", all=True)) + if len(music_el) != 1: + raise Exception("Only MEI with a single element are supported") + self.music_el = music_el[0] self.repetitions = ( [] ) # to be filled when we encounter repetitions and process in the end @@ -78,20 +85,21 @@ def __init__(self, mei_path: PathLike) -> None: def create_parts(self): # handle main scoreDef info: create the part list - main_partgroup_el = self.document.find(self._ns_name("staffGrp", all=True)) + main_partgroup_el = self.music_el.find(self._ns_name("staffGrp", all=True)) self.parts = self._handle_main_staff_group(main_partgroup_el) def fill_parts(self): # fill parts with the content of the score - scores_el = self.document.findall(self._ns_name("score", all=True)) + scores_el = self.music_el.findall(self._ns_name("score", all=True)) if len(scores_el) != 1: raise Exception("Only MEI with a single score element are supported") sections_el = scores_el[0].findall(self._ns_name("section")) position = 0 + measure_number = 1 for section_el in sections_el: # insert in parts all elements except ties - position = self._handle_section( - section_el, list(score.iter_parts(self.parts)), position + position, measure_number = self._handle_section( + section_el, list(score.iter_parts(self.parts)), position, measure_number ) # handles ties @@ -351,27 +359,39 @@ def _handle_staffdef(self, staffdef_el, position, part): self._handle_clef(staffdef_el, position, part) def _intsymdur_from_symbolic(self, symbolic_dur): - """Produce a int symbolic dur (e.g. 12 is a eight note triplet) and a dot number by looking at the symbolic dur dictionary: - i.e., symbol, eventual tuplet ancestors.""" + """Produce a int symbolic dur (e.g. 8 is a eight note), a dot number, and a tuplet modifier, + e.g., (2,3) means there are 3 notes in the space of 2 notes.""" intsymdur = SYMBOLIC_TO_INT_DURS[symbolic_dur["type"]] # deals with tuplets if symbolic_dur.get("actual_notes") is not None: assert symbolic_dur.get("normal_notes") is not None - intsymdur = ( - intsymdur * symbolic_dur["actual_notes"] / symbolic_dur["normal_notes"] + # intsymdur = ( + # intsymdur * symbolic_dur["actual_notes"] / symbolic_dur["normal_notes"] + # ) + tuplet_modifier = ( + symbolic_dur["normal_notes"], + symbolic_dur["actual_notes"], ) + else: + tuplet_modifier = None # deals with dots dots = symbolic_dur.get("dots") if symbolic_dur.get("dots") is not None else 0 - return intsymdur, dots + return intsymdur, dots, tuplet_modifier def _find_ppq(self): """Finds the ppq for MEI filed that do not explicitely encode this information""" - els_with_dur = self.document.xpath(".//*[@dur]") + els_with_dur = self.music_el.xpath(".//*[@dur]") durs = [] durs_ppq = [] for el in els_with_dur: symbolic_duration = self._get_symbolic_duration(el) - intsymdur, dots = self._intsymdur_from_symbolic(symbolic_duration) + intsymdur, dots, tuplet_mod = self._intsymdur_from_symbolic( + symbolic_duration + ) + if tuplet_mod is not None: + # consider time modifications keeping the numerator of the minimized fraction + minimized_fraction = Fraction(intsymdur * tuplet_mod[1], tuplet_mod[0]) + intsymdur = minimized_fraction.numerator # double the value if we have dots, to be sure be able to encode that with integers in partitura durs.append(intsymdur * (2**dots)) durs_ppq.append( @@ -574,9 +594,16 @@ def _duration_info(self, el, part): duration = 0 if el.get("grace") is not None else int(el.get("dur.ppq")) else: # compute the duration from the symbolic duration - intsymdur, dots = self._intsymdur_from_symbolic(symbolic_duration) + intsymdur, dots, tuplet_mod = self._intsymdur_from_symbolic( + symbolic_duration + ) divs = part._quarter_durations[0] # divs is the same as ppq - duration = divs * 4 / intsymdur + if tuplet_mod is None: + tuplet_mod = ( + 1, + 1, + ) # if no tuplet modifier, set one that does not change the duration + duration = (divs * 4 * tuplet_mod[0]) / (intsymdur * tuplet_mod[1]) for d in range(dots): duration = duration + 0.5 * duration # sanity check to verify the divs are correctly set @@ -728,6 +755,55 @@ def _handle_mrest(self, mrest_el, position, voice, staff, part): ) # add mrest to the part part.add(rest, position, position + parts_per_measure) + # return duration to update the position in the layer + return position + parts_per_measure + + def _handle_multirest(self, multirest_el, position, voice, staff, part): + """ + Handles a rest that spawn multiple measures + + Parameters + ---------- + multirest_el : lxml tree + A mrest element in the lxml tree. + position : int + The current position on the timeline. + voice : int + The voice of the section. + staff : int + The current staff also refers to a Part. + part : Partitura.Part + The created part to add elements to. + + Returns + ------- + position + duration : int + Next position on the timeline. + """ + # find id + multirest_id = multirest_el.attrib[self._ns_name("id", XML_NAMESPACE)] + # find how many measures + n_measures = int(multirest_el.attrib["num"]) + if n_measures > 1: + raise Exception( + f"Multi-rests with more than 1 measure are not supported yet. Found one with {n_measures}." + ) + # find closest time signature + last_ts = list(part.iter_all(cls=score.TimeSignature))[-1] + # find divs per measure + ppq = part.quarter_duration_map(position) + parts_per_measure = int(ppq * 4 * last_ts.beats / last_ts.beat_type) + + # create dummy rest to insert in the timeline + rest = score.Rest( + id=multirest_id, + voice=voice, + staff=1, + symbolic_duration=estimate_symbolic_duration(parts_per_measure, ppq), + articulations=None, + ) + # add mrest to the part + part.add(rest, position, position + parts_per_measure) # now iterate # return duration to update the position in the layer return position + parts_per_measure @@ -780,7 +856,20 @@ def _handle_chord(self, chord_el, position, voice, staff, part): def _handle_space(self, e, position, part): """Moves current position.""" - space_id, duration, symbolic_duration = self._duration_info(e, part) + try: + space_id, duration, symbolic_duration = self._duration_info(e, part) + except ( + KeyError + ): # if the space don't have a duration, move to the end of the measure + # find closest time signature + last_ts = list(part.iter_all(cls=score.TimeSignature))[-1] + # find divs per measure + ppq = part.quarter_duration_map(position) + parts_per_measure = int(ppq * 4 * last_ts.beats / last_ts.beat_type) + # find divs elapsed since last barline + last_barline = list(part.iter_all(cls=pt.score.Measure))[-1] + duration = position - last_barline.start.t + return position + duration def _handle_barline_symbols(self, measure_el, position: int, left_or_right: str): @@ -823,6 +912,12 @@ def _handle_layer_in_staff_in_measure( new_position = self._handle_mrest( e, position, ind_layer, ind_staff, part ) + elif e.tag == self._ns_name( + "multiRest" + ): # rest that spawn more than one measure + new_position = self._handle_multirest( + e, position, ind_layer, ind_staff, part + ) elif e.tag == self._ns_name("beam"): # TODO : add Beam element # recursive call to the elements inside beam @@ -846,7 +941,14 @@ def _handle_layer_in_staff_in_measure( position = new_position return position - def _handle_staff_in_measure(self, staff_el, staff_ind, position: int, part): + def _handle_staff_in_measure( + self, + staff_el, + staff_ind, + position: int, + part: pt.score.Part, + measure_number: int, + ): """ Handles staffs inside a measure element. @@ -860,6 +962,9 @@ def _handle_staff_in_measure(self, staff_el, staff_ind, position: int, part): The current position on the timeline. part : Partitura.Part The created partitura part object. + measure_number : int + The number of the measure. This number is independent of the measure name specified in the score. + It starts from 1 and always increases by 1 at each measure Returns ------- @@ -867,7 +972,9 @@ def _handle_staff_in_measure(self, staff_el, staff_ind, position: int, part): The final position on the timeline. """ # add measure - measure = score.Measure(number=staff_el.getparent().get("n")) + measure = score.Measure( + number=measure_number, name=staff_el.getparent().get("n") + ) part.add(measure, position) layers_el = staff_el.findall(self._ns_name("layer")) @@ -921,7 +1028,7 @@ def _handle_directives(self, measure_el, position): for dir_el in dir_els: self._handle_dir_element(dir_el, position) - def _handle_section(self, section_el, parts, position: int): + def _handle_section(self, section_el, parts, position: int, measure_number: int): """ Returns position and fills parts with elements. @@ -933,11 +1040,15 @@ def _handle_section(self, section_el, parts, position: int): A list of partitura Parts. position : int The current position on the timeline. + measure_number : int + The current measure_number Returns ------- position : int The end position of the section. + measure_number : int + The number of the last measure. """ for i_el, element in enumerate(section_el): # handle measures @@ -951,7 +1062,9 @@ def _handle_section(self, section_el, parts, position: int): end_positions = [] for i_s, (part, staff_el) in enumerate(zip(parts, staves_el)): end_positions.append( - self._handle_staff_in_measure(staff_el, i_s + 1, position, part) + self._handle_staff_in_measure( + staff_el, i_s + 1, position, part, measure_number + ) ) # handle directives (dir elements) self._handle_directives(element, position) @@ -961,19 +1074,19 @@ def _handle_section(self, section_el, parts, position: int): warnings.warn( f"Warning : parts have measures of different duration in measure {element.attrib[self._ns_name('id',XML_NAMESPACE)]}" ) - # enlarge measures to the max - for part in parts: - last_measure = list(part.iter_all(pt.score.Measure))[-1] - if last_measure.end.t != max_position: - part.add( - pt.score.Measure(number=last_measure.number), - position, - max_position, - ) - part.remove(last_measure) + # # enlarge measures to the max + # for part in parts: + # last_measure = list(part.iter_all(pt.score.Measure))[-1] + # if last_measure.end.t != max_position: + # part.add( + # pt.score.Measure(number=last_measure.number), + # max_position + # ) + # part.remove(last_measure) position = max_position # handle right barline symbol self._handle_barline_symbols(element, position, "right") + measure_number += 1 # handle staffDef elements elif element.tag == self._ns_name("scoreDef"): # meter modifications @@ -990,10 +1103,14 @@ def _handle_section(self, section_el, parts, position: int): self._handle_keysig(element, position, part) # handle nested section elif element.tag == self._ns_name("section"): - position = self._handle_section(element, parts, position) + position, measure_number = self._handle_section( + element, parts, position, measure_number + ) elif element.tag == self._ns_name("ending"): ending_start = position - position = self._handle_section(element, parts, position) + position, measure_number = self._handle_section( + element, parts, position, measure_number + ) # insert the ending element ending_number = int(re.sub("[^0-9]", "", element.attrib["n"])) self._add_ending(ending_start, position, ending_number, parts) @@ -1009,7 +1126,7 @@ def _handle_section(self, section_el, parts, position: int): else: raise Exception(f"element {element.tag} is not yet supported") - return position + return position, measure_number def _add_ending(self, start_ending, end_ending, ending_string, parts): for part in score.iter_parts(parts): @@ -1024,7 +1141,7 @@ def _tie_notes(self, section_el, part_list): all_notes = [ note for part in score.iter_parts(part_list) - for note in part.iter_all(cls=score.Note) + for note in part.iter_all(cls=score.Note, include_subclasses=True) ] all_notes_dict = {note.id: note for note in all_notes} for tie_el in ties_el: @@ -1052,6 +1169,7 @@ def _insert_repetitions(self): "WARNING : unmatched repetitions. adding a repetition start at position 0" ) self.repetitions.insert(0, {"type": "start", "pos": 0}) + status = "stop" sanitized_repetition_list = [] # check if start-stop are alternate @@ -1082,11 +1200,19 @@ def _insert_repetitions(self): # check if ending with a start if sanitized_repetition_list[-1] == "start": print("WARNING : unmatched repetitions. Ignoring last start") + ## sanitize the found repetitions to remove duplicates + sanitized_repetition_list = list( + OrderedDict( + (tuple(d.items()), d) for d in sanitized_repetition_list + ).values() + ) self.repetitions = sanitized_repetition_list ## insert the repetitions to all parts for rep_start, rep_stop in zip(self.repetitions[:-1:2], self.repetitions[1::2]): - assert rep_start["type"] == "start" and rep_stop["type"] == "stop" + assert ( + rep_start["type"] == "start" and rep_stop["type"] == "stop" + ), "Something wrong with repetitions" for part in score.iter_parts(self.parts): part.add(score.Repeat(), rep_start["pos"], rep_stop["pos"]) diff --git a/partitura/io/importmidi.py b/partitura/io/importmidi.py index ecfe2be3..5843b781 100644 --- a/partitura/io/importmidi.py +++ b/partitura/io/importmidi.py @@ -183,11 +183,10 @@ def load_performance_midi( # end note if it's a 'note off' event or 'note on' with velocity 0 elif note_off or (note_on and msg.velocity == 0): if note not in sounding_notes: - warnings.warn("ignoring MIDI message %s" % msg) + warnings.warn(f"ignoring MIDI message {msg}") continue # append the note to the list associated with the channel - notes.append( dict( # id=f"n{len(notes)}", @@ -473,6 +472,28 @@ def load_score_midi( else: note_ids = [None for i in range(len(note_array))] + ## sanitize time signature, when they are only present in one track, and no global is set + # find the number of ts per each track + number_of_time_sig_per_track = [ + len(time_sigs_by_track[t]) for t in key_sigs_by_track.keys() + ] + # if one track has 0 ts, and another has !=0 ts, and no global_time_sigs is present, sanitize + # all key signatures are copied to global, and the track ts are removed + if ( + len(global_time_sigs) == 0 + and min(number_of_time_sig_per_track) == 0 + and max(number_of_time_sig_per_track) != 0 + ): + warnings.warn( + "Sanitizing time signatures. They will be shared across all tracks." + ) + for ts in [ + ts for ts_track in time_sigs_by_track.values() for ts in ts_track + ]: # flattening all track time signatures to a list of ts + global_time_sigs.append(ts) + # now clear all track_ts + time_sigs_by_track.clear() + time_sigs_by_part = defaultdict(set) for tr, ts_list in time_sigs_by_track.items(): for ts in ts_list: diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index c7fc2bb8..b586b9f6 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -60,6 +60,22 @@ "sustain_pedal": score.SustainPedalDirection, } +TEMPO_DIRECTIONS = { + "Adagio": score.ConstantTempoDirection, + "Andante": score.ConstantTempoDirection, + "Andante amoroso": score.ConstantTempoDirection, + "Andante cantabile": score.ConstantTempoDirection, + "Andante grazioso": score.ConstantTempoDirection, + "Menuetto": score.ConstantTempoDirection, + "Allegretto grazioso": score.ConstantTempoDirection, + "Allegro moderato": score.ConstantTempoDirection, + "Allegro assai": score.ConstantTempoDirection, + "Allegro": score.ConstantTempoDirection, + "Allegretto": score.ConstantTempoDirection, + "Molto allegro": score.ConstantTempoDirection, + "Presto": score.ConstantTempoDirection, +} + OCTAVE_SHIFTS = {8: 1, 15: 2, 22: 3} @@ -114,7 +130,6 @@ def _parse_partlist(partlist): structure = [] current_group = None part_dict = {} - for e in partlist: if e.tag == "part-group": if e.get("type") == "start": @@ -146,7 +161,6 @@ def _parse_partlist(partlist): part.part_abbreviation = next( iter(e.xpath("part-abbreviation/text()")), None ) - part_dict[part_id] = part if current_group is None: @@ -239,6 +253,10 @@ def load_musicxml( composer = None scid = None + work_title = None + work_number = None + movement_title = None + movement_number = None title = None subtitle = None lyricist = None @@ -254,8 +272,20 @@ def load_musicxml( tag="work-title", as_type=str, ) + scidn = get_value_from_tag( + e=work_info_el, + tag="work-number", + as_type=str, + ) + work_title = scid + work_number = scidn - title = scid + movement_title_el = document.find(".//movement-title") + movement_number_el = document.find(".//movement-number") + if movement_title_el is not None: + movement_title = movement_title_el.text + if movement_number_el is not None: + movement_number = movement_number_el.text score_identification_el = document.find("identification") @@ -289,6 +319,10 @@ def load_musicxml( scr = score.Score( id=scid, partlist=partlist, + work_number=work_number, + work_title=work_title, + movement_number=movement_number, + movement_title=movement_title, title=title, subtitle=subtitle, composer=composer, @@ -841,8 +875,9 @@ def _handle_direction(e, position, part, ongoing): # first child of direction-type is dynamics, there may be subsequent # dynamics items, so we loop: for child in direction_type: + # check if child has no children, in which case continue # interpret as score.Direction, fall back to score.Words - dyn_el = next(iter(child)) + dyn_el = next(iter(child), None) if dyn_el is not None: direction = DYN_DIRECTIONS.get(dyn_el.tag, score.Words)( dyn_el.tag, staff=staff @@ -1137,7 +1172,7 @@ def _handle_sound(e, position, part): if "tempo" in e.attrib: tempo = score.Tempo(int(e.attrib["tempo"]), "q") # part.add_starting_object(position, tempo) - _add_tempo_if_unique(position, part, tempo) + (position, part, tempo) def _handle_note(e, position, part, ongoing, prev_note, doc_order): diff --git a/partitura/io/matchfile_utils.py b/partitura/io/matchfile_utils.py index 4ad49b27..e6b48dc4 100644 --- a/partitura/io/matchfile_utils.py +++ b/partitura/io/matchfile_utils.py @@ -871,6 +871,35 @@ def format_time_signature_list(value: MatchTimeSignature) -> str: return str(value) +class MatchTempoIndication(MatchParameter): + def __init__( + self, + value: str, + is_list: bool = False, + ): + super().__init__() + self.value = self.from_string(value)[0] + self.is_list = is_list + + def __str__(self): + return self.value + + @classmethod + def from_string(cls, string: str) -> MatchTempoIndication: + content = interpret_as_list(string) + return content + + +def interpret_as_tempo_indication(value: str) -> MatchTempoIndication: + tempo_indication = MatchTempoIndication.from_string(value) + return tempo_indication + + +def format_tempo_indication(value: MatchTempoIndication) -> str: + value.is_list = False + return str(value) + + ## Miscellaneous utils diff --git a/partitura/io/matchlines_v1.py b/partitura/io/matchlines_v1.py index 0900e969..1fdcfe69 100644 --- a/partitura/io/matchlines_v1.py +++ b/partitura/io/matchlines_v1.py @@ -58,6 +58,9 @@ format_key_signature_v1_0_0, to_snake_case, get_kwargs_from_matchline, + MatchTempoIndication, + interpret_as_tempo_indication, + format_tempo_indication, ) # Define current version of the match file format @@ -237,6 +240,11 @@ def from_instance( format_key_signature_v1_0_0, MatchKeySignature, ), + "tempoIndication": ( + interpret_as_tempo_indication, + format_tempo_indication, + MatchTempoIndication, + ), "beatSubDivision": (interpret_as_list_int, format_list, list), "directions": (interpret_as_list, format_list, list), } diff --git a/partitura/io/musescore.py b/partitura/io/musescore.py index 2977b79e..1ab934db 100644 --- a/partitura/io/musescore.py +++ b/partitura/io/musescore.py @@ -36,38 +36,70 @@ class FileImportException(Exception): pass -def find_musescore3(): - # # possible way to detect MuseScore... executable - # for p in os.environ['PATH'].split(':'): - # c = glob.glob(os.path.join(p, 'MuseScore*')) - # if c: - # print(c) - # break - - result = shutil.which("musescore") - - if result is None: - result = shutil.which("musescore3") - - if result is None: - result = shutil.which("mscore") - +def find_musescore_version(version=4): + """Find the path to the MuseScore executable for a specific version. + If version is a empty string it tries to find an unspecified version of + MuseScore which is used in some systems. + """ + result = shutil.which(f"musescore{version}") if result is None: - result = shutil.which("mscore3") - + result = shutil.which(f"mscore{version}") if result is None: if platform.system() == "Linux": pass - elif platform.system() == "Darwin": - result = shutil.which("/Applications/MuseScore 3.app/Contents/MacOS/mscore") - + result = shutil.which( + f"/Applications/MuseScore {version}.app/Contents/MacOS/mscore" + ) elif platform.system() == "Windows": - result = shutil.which(r"C:\Program Files\MuseScore 3\bin\MuseScore3.exe") + result = shutil.which( + rf"C:\Program Files\MuseScore {version}\bin\MuseScore{version}.exe" + ) return result +def find_musescore(): + """Find the path to the MuseScore executable. + + This function first tries to find the executable for MuseScore 4, + then for MuseScore 3, and finally for any version of MuseScore. + + Returns + ------- + str + Path to the MuseScore executable + + Raises + ------ + MuseScoreNotFoundException + When no MuseScore executable was found + """ + + mscore_exec = find_musescore_version(version=4) + if not mscore_exec: + mscore_exec = find_musescore_version(version=3) + if mscore_exec: + warnings.warn( + "Only Musescore 3 is installed. Consider upgrading to musescore 4." + ) + else: + mscore_exec = find_musescore_version(version="") + if mscore_exec: + warnings.warn( + "A unspecified version of MuseScore was found. Consider upgrading to musescore 4." + ) + else: + raise MuseScoreNotFoundException() + # check if a screen is available (only on Linux) + if "DISPLAY" not in os.environ and platform.system() == "Linux": + raise MuseScoreNotFoundException( + "Musescore Executable was found, but a screen is missing. Musescore needs a screen to load scores" + ) + + return mscore_exec + + @deprecated_alias(fn="filename") @deprecated_parameter("ensure_list") def load_via_musescore( @@ -103,15 +135,22 @@ def load_via_musescore( One or more part or partgroup objects """ + if filename.endswith(".mscz"): + pass + else: + # open the file as text and check if the first symbol is "<" to avoid + # further processing in case of non-XML files + with open(filename, "r") as f: + if f.read(1) != "<": + raise FileImportException( + "File {} is not a valid XML file.".format(filename) + ) - mscore_exec = find_musescore3() - - if not mscore_exec: - raise MuseScoreNotFoundException() + mscore_exec = find_musescore() xml_fh = os.path.splitext(os.path.basename(filename))[0] + ".musicxml" - cmd = [mscore_exec, "-o", xml_fh, filename] + cmd = [mscore_exec, "-o", xml_fh, filename, "-f"] try: ps = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) @@ -167,10 +206,7 @@ def render_musescore( out : Optional[PathLike] Path to the output generated image (or None if no image was generated) """ - mscore_exec = find_musescore3() - - if not mscore_exec: - return None + mscore_exec = find_musescore() if fmt not in ("png", "pdf"): warnings.warn("warning: unsupported output format") @@ -193,6 +229,7 @@ def render_musescore( "-o", os.fspath(img_fh), os.fspath(xml_fh), + "-f", ] try: ps = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) diff --git a/partitura/performance.py b/partitura/performance.py index f7ac86dc..d0818d7f 100644 --- a/partitura/performance.py +++ b/partitura/performance.py @@ -85,7 +85,11 @@ def __init__( super().__init__() self.id = id self.part_name = part_name - self.notes = notes + self.notes = list( + map( + lambda n: n if isinstance(n, PerformedNote) else PerformedNote(n), notes + ) + ) self.controls = controls or [] self.programs = programs or [] self.ppq = ppq @@ -203,7 +207,11 @@ def from_note_array( if "id" not in note_array.dtype.names: n_ids = ["n{0}".format(i) for i in range(len(note_array))] else: - n_ids = note_array["id"] + # Check if all ids are the same + if np.all(note_array["id"] == note_array["id"][0]): + n_ids = ["n{0}".format(i) for i in range(len(note_array))] + else: + n_ids = note_array["id"] if "track" not in note_array.dtype.names: tracks = np.zeros(len(note_array), dtype=int) @@ -280,6 +288,174 @@ def adjust_offsets_w_sustain( note["sound_off"] = offset +class PerformedNote: + """ + A dictionary-like object representing a performed note. + + Parameters + ---------- + pnote_dict : dict + A dictionary containing performed note information. + This information can contain the following fields: + "id", "pitch", "note_on", "note_off", "velocity", "track", "channel", "sound_off". + If not provided, the default values will be used. + Pitch, note_on, and note_off are required. + """ + + def __init__(self, pnote_dict): + self.pnote_dict = pnote_dict + self.pnote_dict["id"] = self.pnote_dict.get("id", None) + self.pnote_dict["pitch"] = self.pnote_dict.get("pitch", self["midi_pitch"]) + self.pnote_dict["note_on"] = self.pnote_dict.get("note_on", -1) + self.pnote_dict["note_off"] = self.pnote_dict.get("note_off", -1) + self.pnote_dict["sound_off"] = self.pnote_dict.get( + "sound_off", self["note_off"] + ) + self.pnote_dict["track"] = self.pnote_dict.get("track", 0) + self.pnote_dict["channel"] = self.pnote_dict.get("channel", 1) + self.pnote_dict["velocity"] = self.pnote_dict.get("velocity", 60) + self._validate_values(pnote_dict) + self._accepted_keys = [ + "id", + "pitch", + "note_on", + "note_off", + "velocity", + "track", + "channel", + "sound_off", + "note_on_tick", + "note_off_tick", + ] + + def __str__(self): + return f"PerformedNote: {self['id']}" + + def __eq__(self, other): + if not isinstance(PerformedNote): + return False + if not self.keys() == other.keys(): + return False + return np.all( + np.array([self[k] == other[k] for k in self.keys() if k in other.keys()]) + ) + + def keys(self): + return self.pnote_dict.keys() + + def get(self, key, default=None): + return self.pnote_dict.get(key, default) + + def __hash__(self): + return hash(self["id"]) + + def __lt__(self, other): + return self["note_on"] < other["note_on"] + + def __le__(self, other): + return self["note_on"] <= other["note_on"] + + def __gt__(self, other): + return self["note_on"] > other["note_on"] + + def __ge__(self, other): + return self["note_on"] >= other["note_on"] + + def __getitem__(self, key): + return self.pnote_dict.get(key, None) + + def __setitem__(self, key, value): + if key not in self._accepted_keys: + raise KeyError(f"Key {key} not accepted for PerformedNote") + self._validate_values((key, value)) + self.pnote_dict[key] = value + + def __delitem__(self, key): + raise KeyError("Cannot delete items from PerformedNote") + + def __iter__(self): + return iter(self.keys()) + + def __len__(self): + return len(self.keys()) + + def __contains__(self, key): + return key in self.keys() + + def copy(self): + return PerformedNote(self.pnote_dict.copy()) + + def _validate_values(self, d): + if isinstance(d, dict): + dd = d + elif isinstance(d, tuple): + dd = {d[0]: d[1]} + else: + raise ValueError(f"Invalid value {d} provided for PerformedNote") + + for key, value in dd.items(): + if key == "pitch": + self._validate_pitch(value) + elif key == "note_on": + self._validate_note_on(value) + elif key == "note_off": + self._validate_note_off(value) + elif key == "velocity": + self._validate_velocity(value) + elif key == "sound_off": + self._validate_sound_off(value) + elif key == "note_on_tick": + self._validate_note_on_tick(value) + elif key == "note_off_tick": + self._validate_note_off_tick(value) + + def _validate_sound_off(self, value): + if self.get("note_off", -1) < 0: + return + if value < 0: + raise ValueError(f"sound_off must be greater than or equal to 0") + if value < self.pnote_dict["note_off"]: + raise ValueError(f"sound_off must be greater or equal to note_off") + + def _validate_note_on(self, value): + if value < 0: + raise ValueError( + f"Note on value provided is invalid, must be greater than or equal to 0" + ) + + def _validate_note_off(self, value): + if self.pnote_dict.get("note_on", -1) < 0: + return + if value < 0 or value < self.pnote_dict["note_on"]: + raise ValueError( + f"Note off value provided is invalid, " + f"must be a positive value greater than or equal to 0 and greater or equal to note_on" + ) + + def _validate_note_on_tick(self, value): + if value < 0: + raise ValueError( + f"Note on tick value provided is invalid, must be greater than or equal to 0" + ) + + def _validate_note_off_tick(self, value): + if self.pnote_dict.get("note_on_tick", -1) < 0: + return + if value < 0 or value < self.pnote_dict["note_on_tick"]: + raise ValueError( + f"Note off tick value provided is invalid, " + f"must be a positive value greater than or equal to 0 and greater or equal to note_on_tick" + ) + + def _validate_pitch(self, value): + if value > 127 or value < 0: + raise ValueError(f"pitch must be between 0 and 127") + + def _validate_velocity(self, value): + if value > 127 or value < 0: + raise ValueError(f"velocity must be between 0 and 127") + + class Performance(object): """Main object for representing a performance. diff --git a/partitura/score.py b/partitura/score.py index 42554381..cbc0f445 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -9,14 +9,14 @@ are registered in terms of their start and end times. """ -from copy import copy +from copy import copy, deepcopy from collections import defaultdict from collections.abc import Iterable from numbers import Number # import copy from partitura.utils.music import MUSICAL_BEATS, INTERVALCLASSES -import warnings +import warnings, sys import numpy as np from scipy.interpolate import PPoly from typing import Union, List, Optional, Iterator, Iterable as Itertype @@ -615,6 +615,18 @@ def dynamics(self): """ return [e for e in self.iter_all(LoudnessDirection, include_subclasses=True)] + @property + def tempo_directions(self): + """Return a list of all tempo direction in the part + + Returns + ------- + list + List of TempoDirection objects + + """ + return [e for e in self.iter_all(TempoDirection, include_subclasses=True)] + @property def articulations(self): """Return a list of all Articulation markings in the part @@ -2990,8 +3002,16 @@ class Score(object): the identifier should not start with a number. partlist : `Part`, `PartGroup` or list of `Part` or `PartGroup` instances. List of `Part` or `PartGroup` objects. - title: str, optional - Title of the score. + work_title: str, optional + Work title of the score, if applicable. + work_number: str, optional + Work number of the score, if applicable. + movement_title: str, optional + Movement title of the score, if applicable. + movement_number: str, optional + Movement number of the score, if applicable. + title : str, optional + Title of the score, from tag subtitle: str, optional Subtitle of the score. composer: str, optional @@ -3010,7 +3030,13 @@ class Score(object): part_structure: list of `Part` or `PartGrop` List of all `Part` or `PartGroup` objects that specify the structure of the score. - title: str + work_title: str + See parameters. + work_number: str + See parameters. + movement_title: str + See parameters. + movement_number: str See parameters. subtitle: str See parameters. @@ -3024,6 +3050,10 @@ class Score(object): """ id: Optional[str] + work_title: Optional[str] + work_number: Optional[str] + movement_title: Optional[str] + movement_number: Optional[str] title: Optional[str] subtitle: Optional[str] composer: Optional[str] @@ -3036,6 +3066,10 @@ def __init__( self, partlist: Union[Part, PartGroup, Itertype[Union[Part, PartGroup]]], id: Optional[str] = None, + work_title: Optional[str] = None, + work_number: Optional[str] = None, + movement_title: Optional[str] = None, + movement_number: Optional[str] = None, title: Optional[str] = None, subtitle: Optional[str] = None, composer: Optional[str] = None, @@ -3045,6 +3079,10 @@ def __init__( self.id = id # Score Information (default from MuseScore/MusicXML) + self.work_title = work_title + self.work_number = work_number + self.movement_title = movement_title + self.movement_number = movement_number self.title = title self.subtitle = subtitle self.composer = composer @@ -4588,15 +4626,15 @@ def iter_unfolded_parts(part, update_ids=True): # UPDATED VERSION -def unfold_part_maximal(part, update_ids=True, ignore_leaps=True): - """Return the "maximally" unfolded part, that is, a copy of the +def unfold_part_maximal(score: ScoreLike, update_ids=True, ignore_leaps=True): + """Return the "maximally" unfolded part/score, that is, a copy of the part where all segments marked with repeat signs are included twice. Parameters ---------- - part : :class:`Part` - The Part to unfold. + score : ScoreLike + The Part/Score to unfold. update_ids : bool (optional) Update note ids to reflect the repetitions. Note IDs will have a '-', e.g., 'n132-1' and 'n132-2' @@ -4609,19 +4647,75 @@ def unfold_part_maximal(part, update_ids=True, ignore_leaps=True): Returns ------- - unfolded_part : :class:`Part` - The unfolded Part + unfolded_part : ScoreLike + The unfolded Part/Score """ + if isinstance(score, Score): + # Copy needs to be deep, otherwise the recursion limit will be exceeded + old_recursion_depth = sys.getrecursionlimit() + sys.setrecursionlimit(10000) + # Deep copy of score + new_score = deepcopy(score) + # Reset recursion limit to previous value to avoid side effects + sys.setrecursionlimit(old_recursion_depth) + new_partlist = list() + for score in new_score.parts: + unfolded_part = unfold_part_maximal( + score, update_ids=update_ids, ignore_leaps=ignore_leaps + ) + new_partlist.append(unfolded_part) + new_score.parts = new_partlist + return new_score paths = get_paths( - part, no_repeats=False, all_repeats=True, ignore_leap_info=ignore_leaps + score, no_repeats=False, all_repeats=True, ignore_leap_info=ignore_leaps ) - unfolded_part = new_part_from_path(paths[0], part, update_ids=update_ids) + unfolded_part = new_part_from_path(paths[0], score, update_ids=update_ids) return unfolded_part +def unfold_part_minimal(score: ScoreLike): + """Return the "minimally" unfolded score/part, that is, a copy of the + part where all segments marked with repeat are included only once. + For voltas only the last volta segment is included. + Note this might not be musically valid, e.g. a passing a "fine" even a first time will stop this unfolding. + Warning: The unfolding of repeats is computed part-wise, inconsistent repeat markings of parts of a single result + in inconsistent unfoldings. + + Parameters + ---------- + score: ScoreLike + The score/part to unfold. + + Returns + ------- + unfolded_score : ScoreLike + The unfolded Part + + """ + if isinstance(score, Score): + # Copy needs to be deep, otherwise the recursion limit will be exceeded + old_recursion_depth = sys.getrecursionlimit() + sys.setrecursionlimit(10000) + # Deep copy of score + unfolded_score = deepcopy(score) + # Reset recursion limit to previous value to avoid side effects + sys.setrecursionlimit(old_recursion_depth) + new_partlist = list() + for part in unfolded_score.parts: + unfolded_part = unfold_part_minimal(part) + new_partlist.append(unfolded_part) + unfolded_score.parts = new_partlist + return unfolded_score + + paths = get_paths(score, no_repeats=True, all_repeats=False, ignore_leap_info=True) + + unfolded_score = new_part_from_path(paths[0], score, update_ids=False) + return unfolded_score + + # UPDATED / UNCHANGED VERSION def unfold_part_alignment(part, alignment): """Return the unfolded part given an alignment, that is, a copy diff --git a/partitura/utils/music.py b/partitura/utils/music.py index a1469e50..c6fd9b02 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -488,8 +488,15 @@ def transpose(score: ScoreLike, interval: Interval) -> ScoreLike: Transposed score. """ import partitura.score as s + import sys + # Copy needs to be deep, otherwise the recursion limit will be exceeded + old_recursion_depth = sys.getrecursionlimit() + sys.setrecursionlimit(10000) + # Deep copy of score new_score = copy.deepcopy(score) + # Reset recursion limit to previous value to avoid side effects + sys.setrecursionlimit(old_recursion_depth) if isinstance(score, s.Score): for part in new_score.parts: transpose(part, interval) @@ -686,6 +693,8 @@ def midi_ticks_to_seconds( SIGN_TO_ALTER = { "n": 0, + "ns": 1, + "nf": -1, "#": 1, "s": 1, "ss": 2, @@ -3252,18 +3261,17 @@ def slice_ppart_by_time( # create a new (empty) instance of a PerformedPart # single dummy note added to be able to set sustain_pedal_threshold in __init__ # -> check `adjust_offsets_w_sustain` in partitura.performance - ppart_slice = PerformedPart([{"note_on": 0, "note_off": 0}]) + # ppart_slice = PerformedPart([{"note_on": 0, "note_off": 0, "pitch": 0}]) # get ppq if PerformedPart contains it, # else skip time_tick info when e.g. created with 'load_performance_midi' try: ppq = ppart.ppq - ppart_slice.ppq = ppq except AttributeError: ppq = None + controls_slice = [] if ppart.controls: - controls_slice = [] for cc in ppart.controls: if cc["time"] >= start_time and cc["time"] <= end_time: new_cc = cc.copy() @@ -3271,10 +3279,9 @@ def slice_ppart_by_time( if ppq: new_cc["time_tick"] = int(2 * ppq * cc["time"]) controls_slice.append(new_cc) - ppart_slice.controls = controls_slice + programs_slice = [] if ppart.programs: - programs_slice = [] for pr in ppart.programs: if pr["time"] >= start_time and pr["time"] <= end_time: new_pr = pr.copy() @@ -3282,7 +3289,6 @@ def slice_ppart_by_time( if ppq: new_pr["time_tick"] = int(2 * ppq * pr["time"]) programs_slice.append(new_pr) - ppart_slice.programs = programs_slice notes_slice = [] note_id = 0 @@ -3326,7 +3332,10 @@ def slice_ppart_by_time( else: break - ppart_slice.notes = notes_slice + # Create slice PerformedPart + ppart_slice = PerformedPart( + notes=notes_slice, programs=programs_slice, controls=controls_slice, ppq=ppq + ) # set threshold property after creating notes list to update 'sound_offset' values ppart_slice.sustain_pedal_threshold = ppart.sustain_pedal_threshold diff --git a/setup.py b/setup.py index b6b93d3f..10c3bbf2 100644 --- a/setup.py +++ b/setup.py @@ -14,9 +14,9 @@ KEYWORDS = "music notation musicxml midi" URL = "https://github.com/CPJKU/partitura" EMAIL = "partitura-users@googlegroups.com" -AUTHOR = "Maarten Grachten, Carlos Cancino-Chacón, Silvan Peter, Emmanouil Karystinaios, Francesco Foscarin, Thassilo Gadermaier" +AUTHOR = "Maarten Grachten, Carlos Cancino-Chacón, Silvan Peter, Emmanouil Karystinaios, Francesco Foscarin, Thassilo Gadermaier, Patricia Hu" REQUIRES_PYTHON = ">=3.7" -VERSION = "1.3.1" +VERSION = "1.4.0" # What packages are required for this module to be executed? REQUIRED = ["numpy", "scipy", "lxml", "lark-parser", "xmlschema", "mido"] diff --git a/tests/__init__.py b/tests/__init__.py index c24bdcd0..712e6a11 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -26,6 +26,10 @@ os.path.join(MUSICXML_PATH, fn) for fn in ["test_note_ties.xml", "test_note_ties_divs.xml"] ] +MUSICXML_SCORE_OBJECT_TESTFILES = [ + os.path.join(MUSICXML_PATH, fn) + for fn in ["test_score_object.musicxml"] +] MUSICXML_UNFOLD_TESTPAIRS = [ ( os.path.join(MUSICXML_PATH, fn1), @@ -150,10 +154,6 @@ MEI_TESTFILES = [ os.path.join(MEI_PATH, fn) for fn in [ - "example_noMeasures_noBeams.mei", - "example_noMeasures_withBeams.mei", - "example_withMeasures_noBeams.mei", - "example_withMeasures_withBeams.mei", "Bach_Prelude.mei", "Schubert_An_die_Sonne_D.439.mei", "test_tuplets.mei", @@ -171,6 +171,7 @@ "test_articulation.mei", "test_merge_voices2.mei", "CRIM_Mass_0030_4.mei", + "test_divs_tuplet.mei" ] ] @@ -188,6 +189,11 @@ ] ] +MUSESCORE_TESTFILES = [ + os.path.join(DATA_PATH, "musescore", fn) + for fn in ["160.03_Pastorale.mscx"] +] + KERN_TIES = [os.path.join(KERN_PATH, fn) for fn in ["tie_mismatch.krn"]] M21_TESTFILES = [ os.path.join(DATA_PATH, "musicxml", fn) @@ -234,4 +240,8 @@ MIDIEXPORT_TESTFILES = [ os.path.join(DATA_PATH, "musicxml", "test_anacrusis.xml") +] + +MIDIINPORT_TESTFILES = [ + os.path.join(DATA_PATH, "midi", "bach_midi_score.mid") ] \ No newline at end of file diff --git a/tests/data/match/mozart_k265_var1.match b/tests/data/match/mozart_k265_var1.match index fbf69446..9c29507e 100644 --- a/tests/data/match/mozart_k265_var1.match +++ b/tests/data/match/mozart_k265_var1.match @@ -299,4 +299,4 @@ sustain(20077,43). sustain(20107,127). sustain(20682,61). sustain(20711,50). -sustain(20740,0). +sustain(20740,0). \ No newline at end of file diff --git a/tests/data/mei/example_noMeasures_noBeams.mei b/tests/data/mei/example_noMeasures_noBeams.mei deleted file mode 100644 index 3b89444d..00000000 --- a/tests/data/mei/example_noMeasures_noBeams.mei +++ /dev/null @@ -1,53 +0,0 @@ - - - - - TEST - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- -
-
diff --git a/tests/data/mei/example_noMeasures_withBeams.mei b/tests/data/mei/example_noMeasures_withBeams.mei deleted file mode 100644 index 7619a276..00000000 --- a/tests/data/mei/example_noMeasures_withBeams.mei +++ /dev/null @@ -1,55 +0,0 @@ - - - - - TEST - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- -
-
diff --git a/tests/data/mei/example_withMeasures_noBeams.mei b/tests/data/mei/example_withMeasures_noBeams.mei deleted file mode 100644 index cd9a1e98..00000000 --- a/tests/data/mei/example_withMeasures_noBeams.mei +++ /dev/null @@ -1,51 +0,0 @@ - - - - - TEST - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- -
-
diff --git a/tests/data/mei/example_withMeasures_withBeams.mei b/tests/data/mei/example_withMeasures_withBeams.mei deleted file mode 100644 index 3b6b9f03..00000000 --- a/tests/data/mei/example_withMeasures_withBeams.mei +++ /dev/null @@ -1,53 +0,0 @@ - - - - - TEST - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- -
-
diff --git a/tests/data/mei/test_divs_tuplet.mei b/tests/data/mei/test_divs_tuplet.mei new file mode 100644 index 00000000..68fc1785 --- /dev/null +++ b/tests/data/mei/test_divs_tuplet.mei @@ -0,0 +1,67 @@ + + + + + + + + Untitled score + + Composer / arranger + + + + 2023-08-02 + + + + + + Verovio +

Transcoded from MusicXML

+
+
+
+
+ + + + + + + + + Picc. + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
diff --git a/tests/data/midi/bach_midi_score.mid b/tests/data/midi/bach_midi_score.mid new file mode 100644 index 00000000..d2f945a4 Binary files /dev/null and b/tests/data/midi/bach_midi_score.mid differ diff --git a/tests/data/musescore/160.03_Pastorale.mscx b/tests/data/musescore/160.03_Pastorale.mscx new file mode 100644 index 00000000..d9af4478 --- /dev/null +++ b/tests/data/musescore/160.03_Pastorale.mscx @@ -0,0 +1,12823 @@ + + + 3.6.2 + 3224f34 + + + 0 + 480 + + 1 + 1 + 1 + 0 + https://imslp.org/wiki/Special:ReverseLookup/176824 + Adrian Nagel (2.1.1), Amelia Brey (2.3.0) + + 1855 + OxfordMusicOnline + 1848 + Franz Liszt + + 2019-01-26 + 2.3.0 + https://imslp.org/wiki/Ann%C3%A9es_de_p%C3%A8lerinage_I%2C_S.160_(Liszt%2C_Franz) + + 3 + Pastorale + 3.02 + https://musicbrainz.org/work/dd78778a-3d31-4ac0-9809-398bf0a2ddeb + mxl + Microsoft Windows + + JH, AB, AN + Tom Schreyer + http://musescore.com/user/2749876/scores/4094931 + + https://musescore.com/user/2749876 + https://viaf.org/viaf/179020308/ + https://www.wikidata.org/wiki/Q567462 + S.160 + Années de Pèlerinage, Première année: Suisse + + + + + + 1 + + + + + F + + Piano + + Piano + Pno. + Piano + keyboard.piano + + 100 + 100 + + + 100 + 33 + + + 100 + 50 + + + 100 + 67 + + + 100 + 100 + + + 120 + 67 + + + 120 + 100 + + + + + Fluid + + + + + Fluid + + + + + + 16.474 + 0 + + + 3. Pastorale + + + + Franz Liszt + + + + Années de Pèlerinage, Première année: Suisse, S.160 + + + + + + + + 1 + + + 4 + + + 12 + 8 + + + pp + 60 + + + + + Vivace + + + eighth + + + quarter + + + eighth + + + + + + + measure + 3/2 + + + + + + line + + + + pp + 60 + + + + down + + + begin + eighth + + + up + + + + 1/4 + + + + + articStaccatoAbove + + + 83 + 19 + + + 87 + 23 + + + + mid + eighth + + articStaccatoAbove + + + 81 + 17 + + + 85 + 21 + + + + eighth + + + + -1/4 + + + + + articStaccatoAbove + + + 80 + 22 + + + 83 + 19 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 81 + 17 + + + + mid + eighth + + + + -1/8 + + + + + 76 + 18 + + + + eighth + + 76 + 18 + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoAbove + + + 80 + 22 + + + + mid + eighth + + articStaccatoAbove + + + 76 + 18 + + + + eighth + + + + -1/4 + + + + + articStaccatoAbove + + + 71 + 19 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 76 + 18 + + + + mid + eighth + + + + -1/8 + + + + + 71 + 19 + + + + eighth + + 71 + 19 + + + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 73 + 21 + + + + eighth + down + + 73 + 21 + + + + down + + + begin + eighth + + articStaccatoBelow + + + 71 + 19 + + + + mid + eighth + + articStaccatoBelow + + + 68 + 22 + + + + eighth + + articStaccatoBelow + + + 63 + 23 + + + 69 + 17 + + + + quarter + down + + 64 + 18 + + + 68 + 22 + + + + eighth + down + + 68 + 22 + + + + + + + + down + + + begin + eighth + + + up + + + + 1/4 + + + + + articStaccatoAbove + + + 83 + 19 + + + 87 + 23 + + + + mid + eighth + + articStaccatoAbove + + + 81 + 17 + + + 85 + 21 + + + + eighth + + + + -1/4 + + + + + articStaccatoAbove + + + 80 + 22 + + + 83 + 19 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 81 + 17 + + + + mid + eighth + + + + -1/8 + + + + + 76 + 18 + + + + eighth + + 76 + 18 + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoAbove + + + 80 + 22 + + + + mid + eighth + + articStaccatoAbove + + + 76 + 18 + + + + eighth + + + + -1/4 + + + + + articStaccatoAbove + + + 71 + 19 + + + + quarter + up + + 76 + 18 + + + + eighth + + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 73 + 21 + + + + eighth + down + + 73 + 21 + + + + down + + + begin + eighth + + articStaccatoBelow + + + 71 + 19 + + + + mid + eighth + + articStaccatoBelow + + + 68 + 22 + + + + eighth + + articStaccatoBelow + + + 63 + 23 + + + 69 + 17 + + + + quarter + down + + 64 + 18 + + + 68 + 22 + + + + 0 + eighth + + + + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + mid + eighth + + articStaccatoBelow + + + 69 + 17 + + + 78 + 20 + + + + eighth + + + + -1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 81 + 17 + + + + mid + eighth + + + + -1/8 + + + + + 76 + 18 + + + + eighth + + 76 + 18 + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + mid + eighth + + articStaccatoBelow + + + 69 + 17 + + + 78 + 20 + + + + eighth + + + + -1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 76 + 18 + + + + mid + eighth + + + + -1/8 + + + + + 71 + 19 + + + + eighth + + 71 + 19 + + + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 73 + 21 + + + + eighth + down + + 69 + 17 + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 68 + 22 + + + + eighth + down + + 68 + 22 + + + + + + + line + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + mid + eighth + + articStaccatoBelow + + + 69 + 17 + + + 78 + 20 + + + + eighth + + + + -1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 81 + 17 + + + + mid + eighth + + + + -1/8 + + + + + 76 + 18 + + + + eighth + + 76 + 18 + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + mid + eighth + + articStaccatoBelow + + + 69 + 17 + + + 78 + 20 + + + + eighth + + + + -1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + quarter + up + + 68 + 22 + + + 76 + 18 + + + + + eighth + + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 73 + 21 + + + + eighth + down + + 69 + 17 + + + + + 0 + 1 + quarter + + 0 + + + + + 0 + 1 + quarter + + 0 + + + + + + + + measure + 3/2 + + + + + + + down + + + begin + eighth + + + up + + + + 1/4 + + + + + articStaccatoAbove + + + 83 + 19 + + + 87 + 23 + + + + mid + eighth + + articStaccatoAbove + + + 81 + 17 + + + 85 + 21 + + + + eighth + + + + -1/4 + + + + + articStaccatoAbove + + + 80 + 22 + + + 83 + 19 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 81 + 17 + + + + mid + eighth + + + + -1/8 + + + + + 76 + 18 + + + + eighth + + 76 + 18 + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoAbove + + + 80 + 22 + + + + mid + eighth + + articStaccatoAbove + + + 76 + 18 + + + + eighth + + + + -1/4 + + + + + articStaccatoAbove + + + 71 + 19 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 76 + 18 + + + + mid + eighth + + + + -1/8 + + + + + 71 + 19 + + + + eighth + + 71 + 19 + + + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 73 + 21 + + + + eighth + down + + 73 + 21 + + + + down + + + begin + eighth + + articStaccatoBelow + + + 71 + 19 + + + + mid + eighth + + articStaccatoBelow + + + 68 + 22 + + + + eighth + + articStaccatoBelow + + + 63 + 23 + + + 69 + 17 + + + + quarter + down + + 64 + 18 + + + 68 + 22 + + + + eighth + down + + 68 + 22 + + + + + + + line + + + + down + + + begin + eighth + + + up + + + + 1/4 + + + + + articStaccatoAbove + + + 83 + 19 + + + 87 + 23 + + + + mid + eighth + + articStaccatoAbove + + + 81 + 17 + + + 85 + 21 + + + + eighth + + + + -1/4 + + + + + articStaccatoAbove + + + 80 + 22 + + + 83 + 19 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 81 + 17 + + + + mid + eighth + + + + -1/8 + + + + + 76 + 18 + + + + eighth + + 76 + 18 + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoAbove + + + 80 + 22 + + + + mid + eighth + + articStaccatoAbove + + + 76 + 18 + + + + eighth + + + + -1/4 + + + + + articStaccatoAbove + + + 71 + 19 + + + + quarter + up + + 76 + 18 + + + + eighth + + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 73 + 21 + + + + eighth + down + + 73 + 21 + + + + down + + + begin + eighth + + articStaccatoBelow + + + 71 + 19 + + + + mid + eighth + + articStaccatoBelow + + + 68 + 22 + + + + eighth + + articStaccatoBelow + + + 63 + 23 + + + 69 + 17 + + + + quarter + down + + 64 + 18 + + + 68 + 22 + + + + 0 + eighth + + + + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + mid + eighth + + articStaccatoBelow + + + 69 + 17 + + + 78 + 20 + + + + eighth + + + + -1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 81 + 17 + + + + mid + eighth + + + + -1/8 + + + + + 76 + 18 + + + + eighth + + 76 + 18 + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + mid + eighth + + articStaccatoBelow + + + 69 + 17 + + + 78 + 20 + + + + eighth + + + + -1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 76 + 18 + + + + mid + eighth + + + + -1/8 + + + + + 71 + 19 + + + + eighth + + 71 + 19 + + + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 73 + 21 + + + + eighth + down + + 69 + 17 + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 68 + 22 + + + + eighth + down + + 68 + 22 + + + + + + + page + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + mid + eighth + + articStaccatoBelow + + + 69 + 17 + + + 78 + 20 + + + + eighth + + + + -1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 81 + 17 + + + + mid + eighth + + + + -1/8 + + + + + 76 + 18 + + + + eighth + + 76 + 18 + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + mid + eighth + + articStaccatoBelow + + + 69 + 17 + + + 78 + 20 + + + + eighth + + + + -1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + quarter + up + + 68 + 22 + + + 76 + 18 + + + + + eighth + + + double + + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 73 + 21 + + + + eighth + down + + 69 + 17 + + + + + 0 + 1 + quarter + + 0 + + + + + 0 + 1 + quarter + + 0 + + + + + + + + 6 + 8 + + + below + + un poco marcato + + + 1 + quarter + + articAccentAbove + + up + + 71 + 19 + + + + 3 + 4 + eighth + + + 4 + + 2 + + + up + + + begin + eighth + + + + + + 9/32 + + + + + 66 + 20 + + + + mid + eighth + + 71 + 19 + + + + mid + eighth + + + accidentalSharp + + 70 + 24 + + + + eighth + + + + -9/32 + + + + + 66 + 20 + + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 61 + 21 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + 71 + 19 + + + + 3 + 4 + eighth + + + 4 + + 2 + + + up + + + begin + eighth + + + + + + 9/32 + + + + + 66 + 20 + + + + mid + eighth + + 71 + 19 + + + + mid + eighth + + + accidentalSharp + + 70 + 24 + + + + eighth + + + + -9/32 + + + + + 66 + 20 + + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 61 + 21 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + 71 + 19 + + + + 3 + 4 + eighth + + + 4 + + 2 + + + up + + + begin + eighth + + + + + + 9/32 + + + + + 66 + 20 + + + + mid + eighth + + 71 + 19 + + + + mid + eighth + + + accidentalSharp + + 70 + 24 + + + + eighth + + + + -9/32 + + + + + 66 + 20 + + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 61 + 21 + + + + + + + line + + + + 1 + quarter + + articAccentAbove + + up + + + 1 + accidentalNatural + + 69 + 17 + + + + 3 + 4 + eighth + + + 4 + + 2 + + + up + + + begin + eighth + + + + + + 9/32 + + + + + 66 + 20 + + + + mid + eighth + + 69 + 17 + + + + mid + eighth + + 68 + 22 + + + + eighth + + + + -9/32 + + + + + 64 + 18 + + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 59 + 19 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + 69 + 17 + + + + 3 + 4 + eighth + + + 4 + + 2 + + + up + + + begin + eighth + + + + + + 9/32 + + + + + 66 + 20 + + + + mid + eighth + + 69 + 17 + + + + mid + eighth + + 68 + 22 + + + + eighth + + + + -9/32 + + + + + 64 + 18 + + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 59 + 19 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + 69 + 17 + + + + 3 + 4 + eighth + + + 4 + + 2 + + + up + + + begin + eighth + + + + + + 9/32 + + + + + 66 + 20 + + + + mid + eighth + + 69 + 17 + + + + mid + eighth + + 68 + 22 + + + + eighth + + + + -9/32 + + + + + 64 + 18 + + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 59 + 19 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + 71 + 19 + + + + 3 + 4 + eighth + + + 4 + + 2 + + + up + + + begin + eighth + + + + + + 9/32 + + + + + 66 + 20 + + + + mid + eighth + + 71 + 19 + + + + mid + eighth + + + accidentalSharp + + 70 + 24 + + + + eighth + + + + -9/32 + + + + + 66 + 20 + + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 61 + 21 + + + + + + + line + + + + 1 + quarter + + articAccentAbove + + up + + 71 + 19 + + + + 3 + 4 + eighth + + + 4 + + 2 + + + up + + + begin + eighth + + + + + + 9/32 + + + + + 66 + 20 + + + + mid + eighth + + 71 + 19 + + + + mid + eighth + + + accidentalSharp + + 70 + 24 + + + + eighth + + + + -9/32 + + + + + 66 + 20 + + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 61 + 21 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + 71 + 19 + + + + 3 + 4 + eighth + + + 4 + + 2 + + + up + + + begin + eighth + + + + + + 9/32 + + + + + 66 + 20 + + + + mid + eighth + + 71 + 19 + + + + mid + eighth + + + accidentalSharp + + 70 + 24 + + + + eighth + + + + -9/32 + + + + + 66 + 20 + + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 61 + 21 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + + 1 + accidentalNatural + + 69 + 17 + + + + 3 + 4 + eighth + + + 4 + + 2 + + + up + + + begin + eighth + + + + + + 9/32 + + + + + 66 + 20 + + + + mid + eighth + + 69 + 17 + + + + mid + eighth + + 68 + 22 + + + + eighth + + + + -9/32 + + + + + 64 + 18 + + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 59 + 19 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + 69 + 17 + + + + 3 + 4 + eighth + + + 4 + + 2 + + + up + + + begin + eighth + + + + + + 9/32 + + + + + 66 + 20 + + + + mid + eighth + + 69 + 17 + + + + mid + eighth + + 68 + 22 + + + + eighth + + + + -9/32 + + + + + 64 + 18 + + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 59 + 19 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + 69 + 17 + + + + 3 + 4 + eighth + + + 4 + + 2 + + + up + + + begin + eighth + + + + + + 9/32 + + + + + 66 + 20 + + + + mid + eighth + + 69 + 17 + + + + mid + eighth + + 68 + 22 + + + + eighth + + + + -9/32 + + + + + 64 + 18 + + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 59 + 19 + + + + + + + line + + + + quarter + + + half + + + double + + + + + + + 12 + 8 + + + measure + 3/2 + + + + + + + pp + 60 + + + + down + + + begin + eighth + + + up + + + + 1/4 + + + + + articStaccatoAbove + + + 83 + 19 + + + 87 + 23 + + + + mid + eighth + + articStaccatoAbove + + + 81 + 17 + + + 85 + 21 + + + + eighth + + + + -1/4 + + + + + articStaccatoAbove + + + 80 + 22 + + + 83 + 19 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 81 + 17 + + + + mid + eighth + + + + -1/8 + + + + + 76 + 18 + + + + eighth + + 76 + 18 + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoAbove + + + 80 + 22 + + + + mid + eighth + + articStaccatoAbove + + + 76 + 18 + + + + eighth + + + + -1/4 + + + + + articStaccatoAbove + + + 71 + 19 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 76 + 18 + + + + mid + eighth + + + + -1/8 + + + + + 71 + 19 + + + + eighth + + 71 + 19 + + + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 73 + 21 + + + + eighth + down + + 73 + 21 + + + + down + + + begin + eighth + + articStaccatoBelow + + + 71 + 19 + + + + mid + eighth + + articStaccatoBelow + + + 68 + 22 + + + + eighth + + articStaccatoBelow + + + 63 + 23 + + + 69 + 17 + + + + quarter + down + + 64 + 18 + + + 68 + 22 + + + + eighth + down + + 68 + 22 + + + + + + + line + + + + down + + + begin + eighth + + + up + + + + 1/4 + + + + + articStaccatoAbove + + + 83 + 19 + + + 87 + 23 + + + + mid + eighth + + articStaccatoAbove + + + 81 + 17 + + + 85 + 21 + + + + eighth + + + + -1/4 + + + + + articStaccatoAbove + + + 80 + 22 + + + 83 + 19 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 81 + 17 + + + + mid + eighth + + + + -1/8 + + + + + 76 + 18 + + + + eighth + + 76 + 18 + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoAbove + + + 80 + 22 + + + + mid + eighth + + articStaccatoAbove + + + 76 + 18 + + + + eighth + + + + -1/4 + + + + + articStaccatoAbove + + + 71 + 19 + + + + quarter + up + + 76 + 18 + + + + eighth + + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 73 + 21 + + + + eighth + down + + 73 + 21 + + + + down + + + begin + eighth + + articStaccatoBelow + + + 71 + 19 + + + + mid + eighth + + articStaccatoBelow + + + 68 + 22 + + + + eighth + + articStaccatoBelow + + + 63 + 23 + + + 69 + 17 + + + + quarter + down + + 64 + 18 + + + 68 + 22 + + + + 0 + eighth + + + + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + mid + eighth + + articStaccatoBelow + + + 69 + 17 + + + 78 + 20 + + + + eighth + + + + -1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 81 + 17 + + + + mid + eighth + + + + -1/8 + + + + + 76 + 18 + + + + eighth + + 76 + 18 + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + mid + eighth + + articStaccatoBelow + + + 69 + 17 + + + 78 + 20 + + + + eighth + + + + -1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 76 + 18 + + + + mid + eighth + + + + -1/8 + + + + + 71 + 19 + + + + eighth + + 71 + 19 + + + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 73 + 21 + + + + eighth + down + + 69 + 17 + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 68 + 22 + + + + eighth + down + + 68 + 22 + + + + + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + mid + eighth + + articStaccatoBelow + + + 69 + 17 + + + 78 + 20 + + + + eighth + + + + -1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 81 + 17 + + + + mid + eighth + + + + -1/8 + + + + + 76 + 18 + + + + eighth + + 76 + 18 + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + mid + eighth + + articStaccatoBelow + + + 69 + 17 + + + 78 + 20 + + + + eighth + + + + -1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + quarter + up + + 68 + 22 + + + 76 + 18 + + + + + eighth + + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 73 + 21 + + + + eighth + down + + 69 + 17 + + + + + 0 + 1 + quarter + + 0 + + + + + 0 + 1 + quarter + + 0 + + + + + + + page + + + + measure + 3/2 + + + + + + + ppp + 50 + + + + down + + + begin + eighth + + + up + + + + 1/4 + + + + + articStaccatoAbove + + + 83 + 19 + + + 87 + 23 + + + + mid + eighth + + articStaccatoAbove + + + 81 + 17 + + + 85 + 21 + + + + eighth + + + + -1/4 + + + + + articStaccatoAbove + + + 80 + 22 + + + 83 + 19 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 81 + 17 + + + + mid + eighth + + + + -1/8 + + + + + 76 + 18 + + + + eighth + + 76 + 18 + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoAbove + + + 80 + 22 + + + + mid + eighth + + articStaccatoAbove + + + 76 + 18 + + + + eighth + + + + -1/4 + + + + + articStaccatoAbove + + + 71 + 19 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 76 + 18 + + + + mid + eighth + + + + -1/8 + + + + + 71 + 19 + + + + eighth + + 71 + 19 + + + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 73 + 21 + + + + eighth + down + + 73 + 21 + + + + down + + + begin + eighth + + articStaccatoBelow + + + 71 + 19 + + + + mid + eighth + + articStaccatoBelow + + + 68 + 22 + + + + eighth + + articStaccatoBelow + + + 63 + 23 + + + 69 + 17 + + + + quarter + down + + 64 + 18 + + + 68 + 22 + + + + eighth + down + + 68 + 22 + + + + + + + + down + + + begin + eighth + + + up + + + + 1/4 + + + + + articStaccatoAbove + + + 83 + 19 + + + 87 + 23 + + + + mid + eighth + + articStaccatoAbove + + + 81 + 17 + + + 85 + 21 + + + + eighth + + + + -1/4 + + + + + articStaccatoAbove + + + 80 + 22 + + + 83 + 19 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 81 + 17 + + + + mid + eighth + + + + -1/8 + + + + + 76 + 18 + + + + eighth + + 76 + 18 + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoAbove + + + 80 + 22 + + + + mid + eighth + + articStaccatoAbove + + + 76 + 18 + + + + eighth + + + + -1/4 + + + + + articStaccatoAbove + + + 71 + 19 + + + + quarter + up + + 76 + 18 + + + + eighth + + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 73 + 21 + + + + eighth + down + + 73 + 21 + + + + down + + + begin + eighth + + articStaccatoBelow + + + 71 + 19 + + + + mid + eighth + + articStaccatoBelow + + + 68 + 22 + + + + eighth + + articStaccatoBelow + + + 63 + 23 + + + 69 + 17 + + + + quarter + down + + 64 + 18 + + + 68 + 22 + + + + 0 + eighth + + + + + + line + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + mid + eighth + + articStaccatoBelow + + + 69 + 17 + + + 78 + 20 + + + + eighth + + + + -1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 81 + 17 + + + + mid + eighth + + + + -1/8 + + + + + 76 + 18 + + + + eighth + + 76 + 18 + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + mid + eighth + + articStaccatoBelow + + + 69 + 17 + + + 78 + 20 + + + + eighth + + + + -1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 76 + 18 + + + + mid + eighth + + + + -1/8 + + + + + 71 + 19 + + + + eighth + + 71 + 19 + + + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 73 + 21 + + + + eighth + down + + 69 + 17 + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 68 + 22 + + + + eighth + down + + 68 + 22 + + + + + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + mid + eighth + + articStaccatoBelow + + + 69 + 17 + + + 78 + 20 + + + + eighth + + + + -1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + up + + + begin + eighth + + + + + + 1/8 + + + + + 81 + 17 + + + + mid + eighth + + + + -1/8 + + + + + 76 + 18 + + + + eighth + + 76 + 18 + + + + up + + + begin + eighth + + + + + + 1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + mid + eighth + + articStaccatoBelow + + + 69 + 17 + + + 78 + 20 + + + + eighth + + + + -1/4 + + + + + articStaccatoBelow + + + 71 + 19 + + + 80 + 22 + + + + quarter + up + + 68 + 22 + + + 76 + 18 + + + + + eighth + + + double + + + + + + 0 + 1 + quarter + + 0 + + + + quarter + down + + 73 + 21 + + + + eighth + down + + 69 + 17 + + + + + 0 + 1 + quarter + + 0 + + + + + 0 + 1 + quarter + + 0 + + + + + + + + 6 + 8 + + + below + + un poco marcato + + + 1 + quarter + + articAccentAbove + + up + + 71 + 19 + + + + up + + + begin + 16th + + + + + + 5/16 + + + + + 66 + 20 + + + + mid + 16th + + 68 + 22 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 71 + 19 + + + + mid + 16th + + + accidentalSharp + + 70 + 24 + + + + 16th + + + + -5/16 + + + + + 66 + 20 + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 61 + 21 + + + + + + + line + + + + 1 + quarter + + articAccentAbove + + up + + 71 + 19 + + + + -1/16 + + + + 1 + + + + 1 + 1/16 + + + + + 1/16 + + + up + + + begin + 16th + + + + + + 5/16 + + + + + 66 + 20 + + + + mid + 16th + + 68 + 22 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 71 + 19 + + + + mid + 16th + + + accidentalSharp + + 70 + 24 + + + + 16th + + + + -5/16 + + + + + 66 + 20 + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 61 + 21 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + 71 + 19 + + + + + + -1 + -1/16 + + + + + up + + + begin + 16th + + + + + + 5/16 + + + + + 66 + 20 + + + + mid + 16th + + 68 + 22 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 71 + 19 + + + + mid + 16th + + + accidentalSharp + + 70 + 24 + + + + 16th + + + + -5/16 + + + + + 66 + 20 + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 61 + 21 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + + 1 + accidentalNatural + + 69 + 17 + + + + + 1 + + + + 2 + + + + + up + + + begin + 16th + + + + + + 5/16 + + + + + 66 + 20 + + + + mid + 16th + + 68 + 22 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 69 + 17 + + + + mid + 16th + + 68 + 22 + + + + 16th + + + + -5/16 + + + + + 64 + 18 + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 59 + 19 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + 69 + 17 + + + + up + + + begin + 16th + + + + + + 5/16 + + + + + 66 + 20 + + + + mid + 16th + + 68 + 22 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 69 + 17 + + + + mid + 16th + + 68 + 22 + + + + 16th + + + + -5/16 + + + + + 64 + 18 + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 59 + 19 + + + + + + + line + + + + 1 + quarter + + articAccentAbove + + up + + 69 + 17 + + + + + + -2 + + + + + up + + + begin + 16th + + + + + + 5/16 + + + + + 66 + 20 + + + + mid + 16th + + 68 + 22 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 69 + 17 + + + + mid + 16th + + 68 + 22 + + + + 16th + + + + -5/16 + + + + + 64 + 18 + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 59 + 19 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + 71 + 19 + + + + + 1 + + + + 2 + + + + + up + + + begin + 16th + + + + + + 5/16 + + + + + 66 + 20 + + + + mid + 16th + + 68 + 22 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 71 + 19 + + + + mid + 16th + + + accidentalSharp + + 70 + 24 + + + + 16th + + + + -5/16 + + + + + 66 + 20 + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 61 + 21 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + 71 + 19 + + + + up + + + begin + 16th + + + + + + 5/16 + + + + + 66 + 20 + + + + mid + 16th + + 68 + 22 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 71 + 19 + + + + mid + 16th + + + accidentalSharp + + 70 + 24 + + + + 16th + + + + -5/16 + + + + + 66 + 20 + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 61 + 21 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + 71 + 19 + + + + + + -2 + + + + + up + + + begin + 16th + + + + + + 5/16 + + + + + 66 + 20 + + + + mid + 16th + + 68 + 22 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 71 + 19 + + + + mid + 16th + + + accidentalSharp + + 70 + 24 + + + + 16th + + + + -5/16 + + + + + 66 + 20 + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 61 + 21 + + + + + + + line + + + + 1 + quarter + + articAccentAbove + + up + + + 1 + accidentalNatural + + 69 + 17 + + + + up + + + begin + 16th + + + + + + 5/16 + + + + + 66 + 20 + + + + mid + 16th + + 68 + 22 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 69 + 17 + + + + mid + 16th + + 68 + 22 + + + + 16th + + + + -5/16 + + + + + 64 + 18 + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 59 + 19 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + 69 + 17 + + + + + 3 + + + + + 3 + -3/8 + + + + + up + + + begin + 16th + + + + + + 5/16 + + + + + 66 + 20 + + + + mid + 16th + + 68 + 22 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 69 + 17 + + + + mid + 16th + + 68 + 22 + + + + 16th + + + + -5/16 + + + + + 64 + 18 + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 59 + 19 + + + + + + + + 1 + quarter + + articAccentAbove + + up + + 69 + 17 + + + + up + + + begin + 16th + + + + + + 5/16 + + + + + 66 + 20 + + + + mid + 16th + + 68 + 22 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 69 + 17 + + + + mid + 16th + + 68 + 22 + + + + 16th + + + + -5/16 + + + + + 64 + 18 + + + + + + down + + + begin + eighth + + + + + + 5/16 + + + + + 63 + 23 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 63 + 23 + + + + mid + 16th + + 59 + 19 + + + + 16th + + + + -5/16 + + + + + 63 + 23 + + + + 1 + quarter + down + + 59 + 19 + + + + + + + + up + + + begin + 16th + + + + + + 5/16 + + + + + 66 + 20 + + + + mid + 16th + + 68 + 22 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 69 + 17 + + + + mid + 16th + + 68 + 22 + + + + 16th + + + + -5/16 + + + + + 64 + 18 + + + + up + + + begin + 16th + + + + + + 5/16 + + + + + 66 + 20 + + + + mid + 16th + + 68 + 22 + + + + mid + 16th + + 66 + 20 + + + + mid + 16th + + 69 + 17 + + + + mid + 16th + + 68 + 22 + + + + 16th + + + + -5/16 + + + + + 64 + 18 + + + + + + 1 + quarter + down + + 59 + 19 + + + + 1 + quarter + down + + 59 + 19 + + + + + + + + below + smorz. + + + + + -3 + 3/8 + + + + + up + + + begin + eighth + + + + + + 5/8 + + + + + 66 + 20 + + + + mid + eighth + + 68 + 22 + + + + mid + eighth + + 66 + 20 + + + + mid + eighth + + 69 + 17 + + + + mid + eighth + + 68 + 22 + + + + eighth + + + + -5/8 + + + + + 64 + 18 + + + + + + 1 + half + down + + 59 + 19 + + + + + + + + ritenuto + + + up + + + begin + eighth + + + + + + 5/8 + + + + + 66 + 20 + + + + mid + eighth + + 68 + 22 + + + + mid + eighth + + 66 + 20 + + + + mid + eighth + + 69 + 17 + + + + mid + eighth + + 68 + 22 + + + + fermataAbove + + + eighth + + + + -5/8 + + + + + 64 + 18 + + + + end + + + + + 1 + half + down + + 59 + 19 + + + + + + + + + + 4 + + + 12 + 8 + + + below + + con + + + 1 + E.V + + + eighth + + + + + + 3/8 + + + + up + + + keyboardPedalPed + + + 59 + 19 + + + + quarter + up + + 47 + 19 + + + + eighth + + + + -3/8 + + + + up + + 59 + 19 + + + + + + + + 1 + I + + + quarter + up + + 40 + 18 + + + + eighth + + + + + + 3/8 + + + + up + + 59 + 19 + + + + 1 + V + + + quarter + up + + 47 + 19 + + + + eighth + + + + -3/8 + + + + up + + 59 + 19 + + + + 1 + I + + + quarter + up + + 40 + 18 + + + + eighth + + + + + + 3/8 + + + + up + + 59 + 19 + + + + 1 + V + + + quarter + up + + 47 + 19 + + + + eighth + + + + -3/8 + + + + up + + 59 + 19 + + + + + + + + 1 + I[V{ + + + quarter + + + -1/8 + + + 1 + IV + + + 1/8 + + + 1 + I + + + eighth + + + + + + 9/8 + + + + up + + 59 + 19 + + + + 1 + IV + + + quarter + up + + 52 + 18 + + + + eighth + up + + 59 + 19 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + 1 + V7 + + + eighth + up + + 59 + 19 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + eighth + + + + -9/8 + + + + up + + 59 + 19 + + + + + + 1 + whole + + 40 + 18 + + + + + + + + 1 + V + + + quarter + + + -1/8 + + + 1 + IV + + + 1/8 + + + 1 + I + + + eighth + + + + + + 9/8 + + + + up + + 59 + 19 + + + + 1 + IV + + + quarter + up + + 52 + 18 + + + + eighth + up + + 59 + 19 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + 1 + V7 + + + eighth + up + + 59 + 19 + + + + 1 + I|PAC} + + + quarter + up + + 52 + 18 + + + + eighth + + + + -9/8 + + + + up + + 59 + 19 + + + + + + 1 + whole + + 40 + 18 + + + + + + + + 1 + { + + + quarter + + + 1 + V7/IV + + + eighth + + + + + + 9/8 + + + + + + accidentalNatural + + 62 + 16 + + + + 1 + IV + + + quarter + + 52 + 18 + + + + eighth + + 61 + 21 + + + + 1 + I + + + quarter + + 52 + 18 + + + + 1 + V(6) + + + eighth + + + accidentalSharp + + 63 + 23 + + + + 1 + I + + + quarter + + 52 + 18 + + + + eighth + + + + -9/8 + + + + + 59 + 19 + + + 64 + 18 + + + + + + 1 + whole + + 40 + 18 + + + + + + + + quarter + + + 1 + V7/IV + + + eighth + + + + + + 7/8 + + + + up + + + accidentalNatural + + 62 + 16 + + + + 1 + IV + + + quarter + up + + 52 + 18 + + + + eighth + up + + 61 + 21 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + 1 + V(6) + + + eighth + up + + + accidentalSharp + + 63 + 23 + + + + 1 + I|PAC} + + + quarter + + + + -7/8 + + + + up + + 52 + 18 + + + 59 + 19 + + + + eighth + down + + 47 + 19 + + + + + + 1 + whole + + 40 + 18 + + + + + + + + 1 + I] + + + quarter + up + + 40 + 18 + + + + eighth + + + up + + + + 3/8 + + + + up + + 59 + 19 + + + + 1 + V + + + quarter + up + + 47 + 19 + + + + eighth + + + + -3/8 + + + + up + + 59 + 19 + + + + 1 + I + + + quarter + up + + 40 + 18 + + + + eighth + + + up + + + + 3/8 + + + + up + + 59 + 19 + + + + 1 + V + + + quarter + up + + 47 + 19 + + + + eighth + + + + -3/8 + + + + up + + 59 + 19 + + + + + + + + 1 + I[V{ + + + quarter + + + -1/8 + + + 1 + IV + + + 1/8 + + + 1 + I + + + eighth + + + + + + 9/8 + + + + up + + 59 + 19 + + + + 1 + IV + + + quarter + up + + 52 + 18 + + + + eighth + up + + 59 + 19 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + 1 + V7 + + + eighth + up + + 59 + 19 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + eighth + + + + -9/8 + + + + up + + 59 + 19 + + + + + + 1 + whole + + 40 + 18 + + + + + + + + 1 + V + + + quarter + + + -1/8 + + + 1 + IV + + + 1/8 + + + 1 + I + + + eighth + + + + + + 9/8 + + + + up + + 59 + 19 + + + + 1 + IV + + + quarter + up + + 52 + 18 + + + + eighth + up + + 59 + 19 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + 1 + V7 + + + eighth + up + + 59 + 19 + + + + 1 + I|PAC} + + + quarter + up + + 52 + 18 + + + + eighth + + + + -9/8 + + + + up + + 59 + 19 + + + + + + 1 + whole + + 40 + 18 + + + + + + + + quarter + + + 1 + V7/IV + + + eighth + + + + + + 9/8 + + + + up + + + accidentalNatural + + 62 + 16 + + + + 1 + IV + + + quarter + up + + 52 + 18 + + + + eighth + up + + 61 + 21 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + 1 + V(6) + + + eighth + up + + + accidentalSharp + + 63 + 23 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + eighth + + + + -9/8 + + + + up + + 59 + 19 + + + 64 + 18 + + + + + + 1 + { + + + 1 + whole + + 40 + 18 + + + + + + + + quarter + + + 1 + V7/IV + + + eighth + + + + + + 7/8 + + + + up + + + accidentalNatural + + 62 + 16 + + + + 1 + IV + + + quarter + up + + 52 + 18 + + + + eighth + up + + 61 + 21 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + 1 + V(6) + + + eighth + up + + + accidentalSharp + + 63 + 23 + + + + 1 + I]|PAC} + + + quarter + + + + -7/8 + + + + up + + 52 + 18 + + + 59 + 19 + + + + + eighth + + + double + + + + + 1 + whole + + 40 + 18 + + + + + + + + 6 + 8 + + + 1 + V.I6{ + + + eighth + + + 1 + I[I + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + V + + + 1 + quarter + + articAccentAbove + + down + + 47 + 19 + + + 54 + 20 + + + + + + + + 1 + I6 + + + eighth + + + 1 + I + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + V + + + 1 + quarter + + articAccentAbove + + down + + 47 + 19 + + + 54 + 20 + + + + + + + + 1 + I6 + + + eighth + + + 1 + I + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + V + + + 1 + quarter + + articAccentAbove + + down + + 47 + 19 + + + 54 + 20 + + + + + + + + 1 + V7/IV] + + + eighth + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + IV(94) + + + 1 + quarter + + articAccentBelow + + up + + 40 + 18 + + + 47 + 19 + + + + -3/16 + + + 1 + IV + + + + + + + 1 + V7/IV + + + eighth + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + IV(94) + + + 1 + quarter + + articAccentBelow + + up + + 40 + 18 + + + 47 + 19 + + + + -3/16 + + + 1 + IV + + + + + + + 1 + V7/IV + + + eighth + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + IV(94) + + + 1 + quarter + + articAccentBelow + + up + + 40 + 18 + + + 47 + 19 + + + + -3/16 + + + 1 + IV + + + + + + + 1 + I6 + + + eighth + + + 1 + I[I + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + V + + + 1 + quarter + + articAccentAbove + + down + + 47 + 19 + + + 54 + 20 + + + + + + + + 1 + I6 + + + eighth + + + 1 + I + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + V + + + 1 + quarter + + articAccentAbove + + down + + 47 + 19 + + + 54 + 20 + + + + + + + + 1 + I6 + + + eighth + + + 1 + I + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + V + + + 1 + quarter + + articAccentAbove + + down + + 47 + 19 + + + 54 + 20 + + + + + + + + 1 + V7/IV] + + + eighth + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + IV(94) + + + 1 + quarter + + articAccentBelow + + up + + 40 + 18 + + + 47 + 19 + + + + -3/16 + + + 1 + IV + + + + + + + 1 + V7/IV + + + eighth + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + IV(94) + + + 1 + quarter + + articAccentBelow + + up + + 40 + 18 + + + 47 + 19 + + + + -3/16 + + + 1 + IV + + + + + + + 1 + V7/IV + + + eighth + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + IV(94) + + + 1 + quarter + + articAccentBelow + + up + + 40 + 18 + + + 47 + 19 + + + + -3/16 + + + 1 + IV + + + + + + + quarter + + + pp + 60 + above + + + + 1 + I.V + + + eighth + + + + + + 3/8 + + + + up + + 59 + 19 + + + + quarter + up + + 47 + 19 + + + + eighth + + + + -3/8 + + + + up + + 59 + 19 + + + + double + + + + + + + 12 + 8 + + + 1 + I|PAC} + + + quarter + up + + 40 + 18 + + + + eighth + + + + + + 3/8 + + + + up + + 59 + 19 + + + + 1 + V + + + quarter + up + + 47 + 19 + + + + eighth + + + + -3/8 + + + + up + + 59 + 19 + + + + 1 + I + + + quarter + up + + 40 + 18 + + + + eighth + + + + + + 3/8 + + + + up + + 59 + 19 + + + + 1 + V + + + quarter + up + + 47 + 19 + + + + eighth + + + + -3/8 + + + + up + + 59 + 19 + + + + + + + + 1 + I[V{ + + + quarter + + + -1/8 + + + 1 + IV + + + 1/8 + + + 1 + I + + + eighth + + + + + + 9/8 + + + + up + + 59 + 19 + + + + 1 + IV + + + quarter + up + + 52 + 18 + + + + eighth + up + + 59 + 19 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + 1 + V7 + + + eighth + up + + 59 + 19 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + eighth + + + + -9/8 + + + + up + + 59 + 19 + + + + + + 1 + whole + + 40 + 18 + + + + + + + + 1 + V + + + quarter + + + -1/8 + + + 1 + IV + + + 1/8 + + + 1 + I + + + eighth + + + + + + 9/8 + + + + up + + 59 + 19 + + + + 1 + IV + + + quarter + up + + 52 + 18 + + + + eighth + up + + 59 + 19 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + 1 + V7 + + + eighth + up + + 59 + 19 + + + + 1 + I|PAC} + + + quarter + up + + 52 + 18 + + + + eighth + + + + -9/8 + + + + up + + 59 + 19 + + + + + + 1 + whole + + 40 + 18 + + + + + + + + quarter + + + 1 + V7/IV + + + eighth + + + + + + 9/8 + + + + up + + + accidentalNatural + + 62 + 16 + + + + 1 + IV + + + quarter + up + + 52 + 18 + + + + eighth + up + + 61 + 21 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + 1 + V(6) + + + eighth + up + + + accidentalSharp + + 63 + 23 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + eighth + + + + -9/8 + + + + up + + 59 + 19 + + + 64 + 18 + + + + + + 1 + { + + + 1 + whole + + 40 + 18 + + + + + + + + quarter + + + 1 + V7/IV + + + eighth + + + + + + 7/8 + + + + up + + + accidentalNatural + + 62 + 16 + + + + 1 + IV + + + quarter + up + + 52 + 18 + + + + eighth + up + + 61 + 21 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + 1 + V(6) + + + eighth + up + + + accidentalSharp + + 63 + 23 + + + + 1 + I|PAC} + + + quarter + + + + -7/8 + + + + up + + 52 + 18 + + + 59 + 19 + + + + eighth + down + + 47 + 19 + + + + + + 1 + whole + + 40 + 18 + + + + + + + + 1 + I] + + + quarter + up + + 40 + 18 + + + + eighth + + + + + + 3/8 + + + + up + + 59 + 19 + + + + 1 + V + + + quarter + up + + 47 + 19 + + + + eighth + + + + -3/8 + + + + up + + 59 + 19 + + + + 1 + I + + + quarter + up + + 40 + 18 + + + + eighth + + + + + + 3/8 + + + + up + + 59 + 19 + + + + 1 + V + + + quarter + up + + 47 + 19 + + + + eighth + + + + -3/8 + + + + up + + 59 + 19 + + + + + + + + 1 + I[V{ + + + quarter + + + -1/8 + + + 1 + IV + + + 1/8 + + + 1 + I + + + eighth + + + + + + 9/8 + + + + up + + 59 + 19 + + + + 1 + IV + + + quarter + up + + 52 + 18 + + + + eighth + up + + 59 + 19 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + 1 + V7 + + + eighth + up + + 59 + 19 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + eighth + + + + -9/8 + + + + up + + 59 + 19 + + + + + + 1 + whole + + 40 + 18 + + + + + + + + 1 + V + + + quarter + + + -1/8 + + + 1 + IV + + + 1/8 + + + 1 + I + + + eighth + + + + + + 9/8 + + + + up + + 59 + 19 + + + + 1 + IV + + + quarter + up + + 52 + 18 + + + + eighth + up + + 59 + 19 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + 1 + V7 + + + eighth + up + + 59 + 19 + + + + 1 + I|PAC} + + + quarter + up + + 52 + 18 + + + + eighth + + + + -9/8 + + + + up + + 59 + 19 + + + + + + 1 + whole + + 40 + 18 + + + + + + + + quarter + + + 1 + V7/IV + + + eighth + + + + + + 9/8 + + + + up + + + accidentalNatural + + 62 + 16 + + + + 1 + IV + + + quarter + up + + 52 + 18 + + + + eighth + up + + 61 + 21 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + 1 + V(6) + + + eighth + up + + + accidentalSharp + + 63 + 23 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + eighth + + + + -9/8 + + + + up + + 59 + 19 + + + 64 + 18 + + + + + + 1 + { + + + 1 + whole + + 40 + 18 + + + + + + + + quarter + + + 1 + V7/IV + + + eighth + + + + + + 7/8 + + + + up + + + accidentalNatural + + 62 + 16 + + + + 1 + IV + + + quarter + up + + 52 + 18 + + + + eighth + up + + 61 + 21 + + + + 1 + I + + + quarter + up + + 52 + 18 + + + + 1 + V(6) + + + eighth + up + + + accidentalSharp + + 63 + 23 + + + + 1 + I]|PAC} + + + quarter + + + + -7/8 + + + + up + + 52 + 18 + + + 59 + 19 + + + + eighth + + + double + + + + + 1 + whole + + 40 + 18 + + + + + + + + 6 + 8 + + + 1 + V.I6{ + + + eighth + + + 1 + I[I + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + V + + + 1 + quarter + + articAccentAbove + + down + + 47 + 19 + + + 54 + 20 + + + + + + + + 1 + I6 + + + eighth + + + 1 + I + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + V + + + 1 + quarter + + articAccentAbove + + down + + 47 + 19 + + + 54 + 20 + + + + + + + + 1 + I6 + + + eighth + + + 1 + I + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + V + + + 1 + quarter + + articAccentAbove + + down + + 47 + 19 + + + 54 + 20 + + + + + + + + 1 + V7/IV] + + + eighth + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + IV(9) + + + 1 + quarter + + articAccentBelow + + up + + 40 + 18 + + + 47 + 19 + + + + -1/8 + + + 1 + IV + + + + + + + 1 + V7/IV + + + eighth + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + IV(9) + + + 1 + quarter + + articAccentBelow + + up + + 40 + 18 + + + 47 + 19 + + + + -1/8 + + + 1 + IV + + + + + + + 1 + V7/IV + + + eighth + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + IV(9) + + + 1 + quarter + + articAccentBelow + + up + + 40 + 18 + + + 47 + 19 + + + + -1/8 + + + 1 + IV + + + + + + + 1 + I6 + + + eighth + + + 1 + I[I + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + V + + + 1 + quarter + + articAccentAbove + + down + + 47 + 19 + + + 54 + 20 + + + + + + + + 1 + I6 + + + eighth + + + 1 + I + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + V + + + 1 + quarter + + articAccentAbove + + down + + 47 + 19 + + + 54 + 20 + + + + + + + + 1 + I6 + + + eighth + + + 1 + I + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + V + + + 1 + quarter + + articAccentAbove + + down + + 47 + 19 + + + 54 + 20 + + + + + + + + 1 + V7/IV] + + + eighth + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + IV(9) + + + 1 + quarter + + articAccentBelow + + up + + 40 + 18 + + + 47 + 19 + + + + -1/8 + + + 1 + IV + + + + + + + 1 + V7/IV + + + eighth + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + IV(9) + + + 1 + quarter + + articAccentBelow + + up + + 40 + 18 + + + 47 + 19 + + + + -1/8 + + + 1 + IV + + + + + + + 1 + I.V7 + + + eighth + + + down + + + begin + eighth + + 47 + 19 + + + 54 + 20 + + + + eighth + + 47 + 19 + + + 54 + 20 + + + + 1 + I(9)|IAC} + + + 1 + quarter + + articAccentBelow + + up + + 40 + 18 + + + 47 + 19 + + + + -1/8 + + + 1 + I + + + + + + + 1 + I(9) + + + 1 + quarter + up + + 40 + 18 + + + 47 + 19 + + + + -1/8 + + + 1 + I + + + 1/8 + + + 1 + I(9) + + + 1 + quarter + up + + 40 + 18 + + + 47 + 19 + + + + -1/8 + + + 1 + I + + + + + + + ppp + 45 + above + + + + 1 + I(9) + + + 1 + half + up + + 40 + 18 + + + 47 + 19 + + + + -3/8 + + + 1 + I(4) + + + 1/8 + + + 1 + I + + + + + + + 1 + I(9) + + + fermataAbove + + + 1 + half + up + + 40 + 18 + + + 47 + 19 + + + + -3/8 + + + 1 + I(4) + + + 1/8 + + + 1 + I + + + 1/4 + + + end + + + + + + diff --git a/tests/data/musicxml/test_cross_staff_beaming.musicxml b/tests/data/musicxml/test_cross_staff_beaming.musicxml new file mode 100644 index 00000000..f30dcddc --- /dev/null +++ b/tests/data/musicxml/test_cross_staff_beaming.musicxml @@ -0,0 +1,390 @@ + + + + + Untitled score + + + Composer / arranger + + MuseScore 4.0.0 + 2023-09-20 + + + + + + + + + + Piano + Pno. + + Piano + + + + 1 + 1 + 78.7402 + 0 + + + + + + + 2 + + 0 + + + 2 + + G + 2 + + + F + 4 + + + + + G + 4 + + 1 + 1 + eighth + down + 1 + begin + + + + F + 4 + + 1 + 1 + eighth + down + 1 + continue + + + + E + 4 + + 1 + 1 + eighth + down + 1 + continue + + + + D + 4 + + 1 + 1 + eighth + down + 1 + end + + + + C + 4 + + 1 + 1 + eighth + down + 1 + begin + + + + B + 3 + + 1 + 1 + eighth + up + 2 + continue + + + + A + 3 + + 1 + 1 + eighth + up + 2 + continue + + + + G + 3 + + 1 + 1 + eighth + up + 2 + end + + + 8 + + + + C + 5 + + 4 + 2 + half + up + 1 + + + + D + 5 + + 4 + 2 + half + up + 1 + + + 8 + + + + C + 3 + + 4 + 5 + half + down + 2 + + + + + E + 3 + + 4 + 5 + half + down + 2 + + + + B + 2 + + 4 + 5 + half + down + 2 + + + + + D + 3 + + 4 + 5 + half + down + 2 + + + + + + G + 4 + + 1 + 1 + eighth + down + 1 + begin + + + + F + 3 + + 1 + 1 + eighth + up + 2 + continue + + + + F + 4 + + 1 + 1 + eighth + down + 1 + continue + + + + G + 3 + + 1 + 1 + eighth + up + 2 + end + + + + A + 3 + + 1 + 1 + eighth + up + 2 + begin + + + + B + 3 + + 1 + 1 + eighth + down + 1 + end + + + + C + 4 + + 2 + 1 + quarter + up + 1 + + + 8 + + + + E + 5 + + 4 + 2 + half + up + 1 + + + + D + 5 + + 4 + 2 + half + up + 1 + + + 8 + + + + B + 2 + + 4 + 5 + half + down + 2 + + + + + D + 3 + + 4 + 5 + half + down + 2 + + + + C + 3 + + 4 + 5 + half + down + 2 + + + + + E + 3 + + 4 + 5 + half + down + 2 + + + light-heavy + + + + diff --git a/tests/data/musicxml/test_note_features.xml b/tests/data/musicxml/test_note_features.xml index 343d951f..371ed54d 100644 --- a/tests/data/musicxml/test_note_features.xml +++ b/tests/data/musicxml/test_note_features.xml @@ -84,6 +84,14 @@ 2 + + + + + + 1 + + G diff --git a/tests/data/musicxml/test_score_object.musicxml b/tests/data/musicxml/test_score_object.musicxml index ab3b38f6..84025f84 100644 --- a/tests/data/musicxml/test_score_object.musicxml +++ b/tests/data/musicxml/test_score_object.musicxml @@ -3,6 +3,7 @@ Test Title + Test Opus 1 T. H. E. Composer diff --git a/tests/temp_test.py b/tests/temp_test.py new file mode 100644 index 00000000..219c22c3 --- /dev/null +++ b/tests/temp_test.py @@ -0,0 +1,4 @@ +import partitura as pt + +score = pt.load_mei(r"C:\Users\fosca\Desktop\JKU\partitura\tests\data\mei\example_noMeasures_noBeams.mei") +print(score.parts) diff --git a/tests/test_cross_staff_beaming.py b/tests/test_cross_staff_beaming.py new file mode 100644 index 00000000..1b8ecd8f --- /dev/null +++ b/tests/test_cross_staff_beaming.py @@ -0,0 +1,20 @@ +import unittest +import os +from tests import MUSICXML_PATH +from partitura import load_musicxml +import numpy as np + +EXAMPLE_FILE = os.path.join(MUSICXML_PATH, "test_cross_staff_beaming.musicxml") + +class CrossStaffBeaming(unittest.TestCase): + def test_cross_staff_single_part_musicxml(self): + score = load_musicxml(EXAMPLE_FILE) + note_array = score.note_array(include_staff=True) + expected_staff = np.array([1, 1, 1, 1, 1, 2, 2, 2, 1, 2, 1, 2, 2, 1, 1]) + cross_staff_mask = (note_array["pitch"] > 52) & (note_array["pitch"] < 72) + note_array_staff = note_array[cross_staff_mask]["staff"] + expected_voice = np.ones(len(note_array_staff), dtype=int) + note_array_voice = note_array[cross_staff_mask]["voice"] + self.assertTrue(np.all(note_array_staff == expected_staff)) + self.assertTrue(np.all(note_array_voice == expected_voice)) + diff --git a/tests/test_load_score.py b/tests/test_load_score.py index e7b3c8f5..474e927f 100644 --- a/tests/test_load_score.py +++ b/tests/test_load_score.py @@ -37,16 +37,12 @@ def test_load_score(self): + MATCH_IMPORT_EXPORT_TESTFILES + EXAMPLE_FILES ): - self.load_score(fn) - - def load_score(self, fn): - try: - score = load_score(fn) - self.assertTrue(isinstance(score, Score)) - - for pp in score.part_structure: - self.assertTrue(type(pp) in (Part, PartGroup)) - for pp in score.parts: - self.assertTrue(isinstance(pp, Part)) - except NotSupportedFormatError: - self.assertTrue(False) + self.check_return_type(fn) + + def check_return_type(self, fn): + score = load_score(fn) + self.assertTrue(isinstance(score, Score), f"results of load_score type are not Score for score {fn}.") + for pp in score.part_structure: + self.assertTrue(type(pp) in (Part, PartGroup), f"results of score.part_structure type are neither Part or PartGroup for score {fn}.") + for pp in score.parts: + self.assertTrue(isinstance(pp, Part), f"results of score.parts type are not Part for score {fn}.") diff --git a/tests/test_match_import.py b/tests/test_match_import.py index 0b703e9f..64356a8a 100644 --- a/tests/test_match_import.py +++ b/tests/test_match_import.py @@ -269,7 +269,8 @@ def test_info_lines(self): audioLastNote_line = "info(audioLastNote,9.8372)." composer_line = "info(composer,Frèdéryk Chopin)." performer_line = "info(performer,A. Human Pianist)." - midiClockUnits_line = "info(midiClockUnits,4000)." + midiClockUnits_line = "info(midiClockUnits,480)." + # midiClockUnits_line = "info(midiClockUnits,4000)." midiClockRate_line = "info(midiClockRate,500000)." approximateTempo_line = "info(approximateTempo,98.2902)." subtitle_line = "info(subtitle,Subtitle)." diff --git a/tests/test_mei.py b/tests/test_mei.py index 3a854792..782c7976 100644 --- a/tests/test_mei.py +++ b/tests/test_mei.py @@ -33,7 +33,7 @@ class TestImportMEI(unittest.TestCase): def test_main_part_group1(self): - parser = MeiParser(MEI_TESTFILES[5]) + parser = MeiParser(MEI_TESTFILES[1]) main_partgroup_el = parser.document.find(parser._ns_name("staffGrp", all=True)) part_list = parser._handle_main_staff_group(main_partgroup_el) self.assertTrue(len(part_list) == 2) @@ -65,14 +65,14 @@ def test_main_part_group1(self): self.assertTrue(part_list[1].id == "P5") def test_main_part_group2(self): - parser = MeiParser(MEI_TESTFILES[4]) + parser = MeiParser(MEI_TESTFILES[0]) main_partgroup_el = parser.document.find(parser._ns_name("staffGrp", all=True)) part_list = parser._handle_main_staff_group(main_partgroup_el) self.assertTrue(len(part_list) == 1) self.assertTrue(isinstance(part_list[0], score.PartGroup)) def test_handle_layer1(self): - parser = MeiParser(MEI_TESTFILES[5]) + parser = MeiParser(MEI_TESTFILES[1]) layer_el = [ e for e in parser.document.findall(parser._ns_name("layer", all=True)) @@ -83,7 +83,7 @@ def test_handle_layer1(self): self.assertTrue(len(part.note_array()) == 3) def test_handle_layer2(self): - parser = MeiParser(MEI_TESTFILES[5]) + parser = MeiParser(MEI_TESTFILES[1]) layer_el = [ e for e in parser.document.findall(parser._ns_name("layer", all=True)) @@ -94,7 +94,7 @@ def test_handle_layer2(self): self.assertTrue(len(part.note_array()) == 3) def test_handle_layer_tuplets(self): - parser = MeiParser(MEI_TESTFILES[6]) + parser = MeiParser(MEI_TESTFILES[2]) layer_el = [ e for e in parser.document.findall(parser._ns_name("layer", all=True)) @@ -105,13 +105,13 @@ def test_handle_layer_tuplets(self): self.assertTrue(len(part.note_array()) == 10) def test_ties1(self): - scr = load_mei(MEI_TESTFILES[7]) + scr = load_mei(MEI_TESTFILES[3]) part_list = scr.parts note_array = list(score.iter_parts(part_list))[0].note_array() self.assertTrue(len(note_array) == 4) def test_time_signatures(self): - scr = load_mei(MEI_TESTFILES[8]) + scr = load_mei(MEI_TESTFILES[4]) part_list = scr.parts part0 = list(score.iter_parts(part_list))[0] time_signatures = list(part0.iter_all(score.TimeSignature)) @@ -121,7 +121,7 @@ def test_time_signatures(self): self.assertTrue(time_signatures[2].start.t == 12.5 * 16) def test_clef(self): - part_list = load_mei(MEI_TESTFILES[9]).parts + part_list = load_mei(MEI_TESTFILES[5]).parts # test on part 2 part2 = list(score.iter_parts(part_list))[2] clefs2 = list(part2.iter_all(score.Clef)) @@ -148,7 +148,7 @@ def test_clef(self): self.assertTrue(clefs3[1].octave_change == -1) def test_key_signature1(self): - part_list = load_mei(MEI_TESTFILES[9]).parts + part_list = load_mei(MEI_TESTFILES[5]).parts for part in score.iter_parts(part_list): kss = list(part.iter_all(score.KeySignature)) self.assertTrue(len(kss) == 2) @@ -156,14 +156,14 @@ def test_key_signature1(self): self.assertTrue(kss[1].fifths == 4) def test_key_signature2(self): - part_list = load_mei(MEI_TESTFILES[10]).parts + part_list = load_mei(MEI_TESTFILES[6]).parts for part in score.iter_parts(part_list): kss = list(part.iter_all(score.KeySignature)) self.assertTrue(len(kss) == 1) self.assertTrue(kss[0].fifths == -1) def test_grace_note(self): - part_list = load_mei(MEI_TESTFILES[10]).parts + part_list = load_mei(MEI_TESTFILES[6]).parts part = list(score.iter_parts(part_list))[0] grace_notes = list(part.iter_all(score.GraceNote)) self.assertTrue(len(part.note_array()) == 7) @@ -172,42 +172,42 @@ def test_grace_note(self): self.assertTrue(grace_notes[1].grace_type == "appoggiatura") def test_meter_in_scoredef(self): - part_list = load_mei(MEI_TESTFILES[11]).parts + part_list = load_mei(MEI_TESTFILES[7]).parts self.assertTrue(True) def test_infer_ppq(self): - parser = MeiParser(MEI_TESTFILES[12]) + parser = MeiParser(MEI_TESTFILES[8]) inferred_ppq = parser._find_ppq() self.assertTrue(inferred_ppq == 15) def test_no_ppq(self): # compare the same piece with and without ppq annotations - parts_ppq = load_mei(MEI_TESTFILES[6]).parts + parts_ppq = load_mei(MEI_TESTFILES[2]).parts part_ppq = list(score.iter_parts(parts_ppq))[0] note_array_ppq = part_ppq.note_array() - parts_no_ppq = load_mei(MEI_TESTFILES[12]).parts + parts_no_ppq = load_mei(MEI_TESTFILES[8]).parts part_no_ppq = list(score.iter_parts(parts_no_ppq))[0] note_array_no_ppq = part_no_ppq.note_array() self.assertTrue(np.array_equal(note_array_ppq, note_array_no_ppq)) def test_part_duration(self): - parts_no_ppq = load_mei(MEI_TESTFILES[14]).parts + parts_no_ppq = load_mei(MEI_TESTFILES[10]).parts part_no_ppq = list(score.iter_parts(parts_no_ppq))[0] note_array_no_ppq = part_no_ppq.note_array() self.assertTrue(part_no_ppq._quarter_durations[0] == 4) self.assertTrue(sorted(part_no_ppq._points)[-1].t == 12) def test_part_duration2(self): - parts_no_ppq = load_mei(MEI_TESTFILES[15]).parts + parts_no_ppq = load_mei(MEI_TESTFILES[11]).parts part_no_ppq = list(score.iter_parts(parts_no_ppq))[0] note_array_no_ppq = part_no_ppq.note_array() self.assertTrue(part_no_ppq._quarter_durations[0] == 8) self.assertTrue(sorted(part_no_ppq._points)[-1].t == 22) def test_barline(self): - parts = load_mei(MEI_TESTFILES[16]).parts + parts = load_mei(MEI_TESTFILES[12]).parts part = list(score.iter_parts(parts))[0] barlines = list(part.iter_all(score.Barline)) expected_barlines_times = [0, 8, 8, 16, 20, 24, 28] @@ -224,7 +224,7 @@ def test_barline(self): self.assertTrue([bl.style for bl in barlines] == expected_barlines_style) def test_repetition1(self): - parts = load_mei(MEI_TESTFILES[16]).parts + parts = load_mei(MEI_TESTFILES[12]).parts part = list(score.iter_parts(parts))[0] repetitions = list(part.iter_all(score.Repeat)) expected_repeat_starts = [0, 8] @@ -233,7 +233,7 @@ def test_repetition1(self): self.assertTrue([rp.end.t for rp in repetitions] == expected_repeat_ends) def test_repetition2(self): - parts = load_mei(MEI_TESTFILES[17]).parts + parts = load_mei(MEI_TESTFILES[13]).parts part = list(score.iter_parts(parts))[0] fine_els = list(part.iter_all(score.Fine)) self.assertTrue(len(fine_els) == 1) @@ -253,33 +253,52 @@ def test_parse_mei_example(self): def test_parse_mei(self): # check if all test files load correctly - for mei in MEI_TESTFILES[4:]: + for mei in MEI_TESTFILES: + print("loading {}".format(mei)) part_list = load_mei(mei).parts self.assertTrue(True) def test_voice(self): - parts = load_mei(MEI_TESTFILES[19]) + parts = load_mei(MEI_TESTFILES[15]) merged_part = score.merge_parts(parts, reassign="voice") voices = merged_part.note_array()["voice"] expected_voices = [5, 4, 3, 2, 1, 1] self.assertTrue(np.array_equal(voices, expected_voices)) def test_staff(self): - parts = load_mei(MEI_TESTFILES[19]) + parts = load_mei(MEI_TESTFILES[15]) merged_part = score.merge_parts(parts, reassign="staff") staves = merged_part.note_array(include_staff=True)["staff"] expected_staves = [4, 3, 2, 1, 1, 1] self.assertTrue(np.array_equal(staves, expected_staves)) def test_nopart(self): - parts = load_mei(MEI_TESTFILES[20]) + my_score = load_mei(MEI_TESTFILES[16]) last_measure_duration = [ - list(p.iter_all(score.Measure))[-1].end.t + list(p.iter_all(score.Barline))[-1].start.t - list(p.iter_all(score.Measure))[-1].start.t - for p in parts + for p in my_score.parts ] self.assertTrue(all([d == 4096 for d in last_measure_duration])) + def test_tuplet_div(self): + score = load_mei(MEI_TESTFILES[17]) + self.assertTrue(np.array_equal(score.note_array()["duration_div"],[3,3,3,3,3,3,3,3,24])) + + def test_measure_number(self): + score = load_mei(MEI_TESTFILES[0]) + measure_number_map = score.parts[0].measure_number_map + onsets = score.note_array()["onset_div"] + measure_number_per_each_onset = measure_number_map(onsets) + self.assertTrue(measure_number_per_each_onset[0].dtype == int) + self.assertTrue(min(measure_number_per_each_onset) == 1) + self.assertTrue(max(measure_number_per_each_onset) == 34) + + def test_measure_number2(self): + score = load_mei(MEI_TESTFILES[13]) + measure_number_map = score.parts[0].measure_number_map + measure_number_per_each_onset = measure_number_map(score.note_array()["onset_div"]) + self.assertTrue(measure_number_per_each_onset.tolist()==[1,2,2,3,4,5,6,8]) if __name__ == "__main__": unittest.main() diff --git a/tests/test_midi_import.py b/tests/test_midi_import.py index cf31f169..0255d38e 100644 --- a/tests/test_midi_import.py +++ b/tests/test_midi_import.py @@ -13,6 +13,7 @@ from partitura import load_score_midi from partitura.utils import partition import partitura.score as score +from tests import MIDIINPORT_TESTFILES LOGGER = logging.getLogger(__name__) @@ -310,3 +311,11 @@ def test_midi_import_mode_5(self): def tearDown(self): # remove tmp file self.tmpfile = None + +class TestScoreMidi(unittest.TestCase): + def test_time_signature(self): + score = load_score_midi(MIDIINPORT_TESTFILES[0]) + self.assertEqual(score.note_array()["onset_beat"][2], 0.5) + na = score.note_array(include_time_signature=True) + self.assertTrue(all([n==3 for n in na["ts_beats"]])) + self.assertTrue(all([d==8 for d in na["ts_beat_type"]])) \ No newline at end of file diff --git a/tests/test_musescore.py b/tests/test_musescore.py new file mode 100644 index 00000000..feec7f83 --- /dev/null +++ b/tests/test_musescore.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This file contains test functions for MEI import +""" + +import unittest + +from tests import MUSESCORE_TESTFILES +from partitura import load_musicxml, load_mei, EXAMPLE_MEI +import partitura.score as score +from partitura.io.importmei import MeiParser +from partitura.utils import compute_pianoroll +from lxml import etree +from xmlschema.names import XML_NAMESPACE +from partitura.io import load_score, load_via_musescore +from partitura.io.musescore import find_musescore, MuseScoreNotFoundException +import platform + +import numpy as np +from pathlib import Path + +try: + if find_musescore(): + class TestImportMusescore(unittest.TestCase): + def test_epfl_scores(self): + score = load_via_musescore(MUSESCORE_TESTFILES[0]) + self.assertTrue(len(score.parts) == 1) + # try the generic loading function + score = load_score(MUSESCORE_TESTFILES[0]) + self.assertTrue(len(score.parts) == 1) +except MuseScoreNotFoundException: + pass diff --git a/tests/test_pianoroll.py b/tests/test_pianoroll.py index 18eed529..7e04e366 100644 --- a/tests/test_pianoroll.py +++ b/tests/test_pianoroll.py @@ -214,14 +214,12 @@ def test_pianoroll_to_notearray(self): self.assertTrue(test) def test_reconstruction_score(self): - for fn in MUSICXML_IMPORT_EXPORT_TESTFILES: score = load_musicxml(fn) note_array = score[0].note_array() pr = compute_pianoroll( score[0], time_unit="div", time_div=1, remove_silence=False ) - rec_note_array = pianoroll_to_notearray(pr, time_div=1, time_unit="div") original_pitch_idx = np.argsort(note_array["pitch"]) @@ -233,7 +231,7 @@ def test_reconstruction_score(self): rec_note_array = rec_note_array[rec_pitch_idx] rec_onset_idx = np.argsort(rec_note_array["onset_div"], kind="mergesort") rec_note_array = rec_note_array[rec_onset_idx] - + test_pitch = np.all(note_array["pitch"] == rec_note_array["pitch"]) self.assertTrue(test_pitch) test_onset = np.all(note_array["onset_div"] == rec_note_array["onset_div"]) @@ -442,3 +440,6 @@ def test_indices(self): # Onsets and offsets should be identical self.assertTrue(np.all(pr_idxs[:, 2:4] == pcr_idxs[:, 2:4])) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_xml.py b/tests/test_xml.py index 2cca87bc..05959d3f 100755 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -10,6 +10,7 @@ from tests import ( MUSICXML_IMPORT_EXPORT_TESTFILES, + MUSICXML_SCORE_OBJECT_TESTFILES, MUSICXML_UNFOLD_TESTPAIRS, MUSICXML_UNFOLD_COMPLEX, MUSICXML_UNFOLD_VOLTA, @@ -241,6 +242,14 @@ def _pretty_export_import_pretty_test(self, part1): show_diff(pstring1, pstring2) msg = "Exported and imported score does not yield identical pretty printed representations" self.assertTrue(equal, msg) + + def test_score_attribute(self): + score = load_musicxml(MUSICXML_SCORE_OBJECT_TESTFILES[0]) + test_work_title = "Test Title" + test_work_number = "Test Opus 1" + + self.assertTrue(score.work_title == test_work_title) + self.assertTrue(score.work_number == test_work_number) def make_part_slur():