From ba0266d9b8b29ef4c0a6af9797659a9c7937cda4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Fri, 5 Apr 2024 16:24:31 +0200 Subject: [PATCH 1/4] parse fingering info from MusicXML --- partitura/io/exportmatch.py | 6 ++++ partitura/io/exportmusicxml.py | 13 +++++++ partitura/io/importmatch.py | 14 ++++++++ partitura/io/importmusicxml.py | 36 +++++++++++++++++++- partitura/io/matchfile_utils.py | 1 + partitura/musicanalysis/performance_codec.py | 2 +- partitura/score.py | 27 ++++++++++++++- 7 files changed, 96 insertions(+), 3 deletions(-) diff --git a/partitura/io/exportmatch.py b/partitura/io/exportmatch.py index 3311c75b..53735312 100644 --- a/partitura/io/exportmatch.py +++ b/partitura/io/exportmatch.py @@ -313,6 +313,7 @@ def matchfile_from_alignment( staff = getattr(snote, "staff", None) ornaments = getattr(snote, "ornaments", None) fermata = getattr(snote, "fermata", None) + technical = getattr(snote, "technical", None) if voice is not None: score_attributes_list.append(f"v{voice}") @@ -329,6 +330,11 @@ def matchfile_from_alignment( if fermata is not None: score_attributes_list.append("fermata") + if technical is not None: + for tech_el in technical: + if isinstance(tech_el, score.Fingering): + score_attributes_list.append(f"fingering{tech_el.fingering}") + if isinstance(snote, score.GraceNote): score_attributes_list.append("grace") diff --git a/partitura/io/exportmusicxml.py b/partitura/io/exportmusicxml.py index 0abe508b..97381d32 100644 --- a/partitura/io/exportmusicxml.py +++ b/partitura/io/exportmusicxml.py @@ -150,6 +150,19 @@ def make_note_el(note, dur, voice, counter, n_of_staves): articulations_e.extend(articulations) notations.append(articulations_e) + if note.technical: + technical = [] + for technical_notation in note.technical: + if isinstance(technical_notation, score.Fingering): + tech_el = etree.Element("fingering") + tech_el.text = str(technical_notation.fingering) + technical.append(tech_el) + + if technical: + technical_e = etree.Element("technical") + technical_e.extend(technical) + notations.append(technical_e) + sym_dur = note.symbolic_duration or {} if sym_dur.get("type") is not None: diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index dcb95723..014e91c9 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -68,6 +68,7 @@ Version, number_pattern, vnumber_pattern, + fingering_pattern, MatchTimeSignature, MatchKeySignature, format_pnote_id, @@ -622,6 +623,7 @@ def part_from_matchfile( alter=note.Modifier, id=note.Anchor, articulations=articulations, + technical=[], ) staff_nr = next( @@ -650,6 +652,18 @@ def part_from_matchfile( None, ) + if any(a.startswith("fingering") for a in note.ScoreAttributesList): + note_attributes["technical"].append( + next( + ( + score.Fingering(int(a[9:])) + for a in note.ScoreAttributesList + if fingering_pattern.match(a) + ), + None, + ) + ) + # get rid of this if as soon as we have a way to iterate over the # duration components. For now we have to treat the cases simple # and compound durations separately. diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index 1b3a8b95..b7669fa6 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -8,7 +8,7 @@ import warnings import zipfile -from typing import Union, Optional +from typing import Union, Optional, List import numpy as np from lxml import etree @@ -1238,6 +1238,12 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non else: ornaments = {} + technical_e = e.find("notations/technical") + if technical_e is not None: + technical_notations = get_technical_notations(technical_e) + else: + technical_notations = {} + pitch = e.find("pitch") unpitch = e.find("unpitched") if pitch is not None: @@ -1265,6 +1271,7 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non symbolic_duration=symbolic_duration, articulations=articulations, ornaments=ornaments, + technical=technical_notations, steal_proportion=steal_proportion, doc_order=doc_order, ) @@ -1302,6 +1309,7 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non symbolic_duration=symbolic_duration, articulations=articulations, ornaments=ornaments, + technical=technical_notations, doc_order=doc_order, ) @@ -1330,6 +1338,7 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non notehead=notehead, noteheadstyle=noteheadstylebool, articulations=articulations, + technical=technical_notations, symbolic_duration=symbolic_duration, doc_order=doc_order, ) @@ -1342,6 +1351,7 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non staff=staff, symbolic_duration=symbolic_duration, articulations=articulations, + technical=technical_notations, doc_order=doc_order, ) @@ -1634,6 +1644,30 @@ def get_ornaments(e): return [a for a in ornaments if e.find(a) is not None] +def get_technical_notations(e: etree._Element) -> List[score.NoteTechnicalNotation]: + # For a full list of technical notations + # https://usermanuals.musicxml.com/MusicXML/Content/EL-MusicXML-technical.htm + # for now we only support fingering + technical_notation_parsers = { + "fingering": parse_fingering, + } + + technical_notations = [ + parser(e.find(a)) + for a, parser in technical_notation_parsers.items() + if e.find(a) is not None + ] + + return technical_notations + + +def parse_fingering(e: etree._Element) -> score.Fingering: + + fingering = score.Fingering(fingering=int(e.text)) + + return fingering + + @deprecated_alias(fn="filename") def musicxml_to_notearray( filename, diff --git a/partitura/io/matchfile_utils.py b/partitura/io/matchfile_utils.py index 9fe76745..2f7340f7 100644 --- a/partitura/io/matchfile_utils.py +++ b/partitura/io/matchfile_utils.py @@ -47,6 +47,7 @@ number_pattern = re.compile(r"\d+") vnumber_pattern = re.compile(r"v\d+") +fingering_pattern = re.compile(r"fingering\d+") # For matchfiles before 1.0.0. old_version_pattern = re.compile(r"^(?P[0-9]+)\.(?P[0-9]+)") diff --git a/partitura/musicanalysis/performance_codec.py b/partitura/musicanalysis/performance_codec.py index 9da86785..bf6e57c7 100644 --- a/partitura/musicanalysis/performance_codec.py +++ b/partitura/musicanalysis/performance_codec.py @@ -767,7 +767,7 @@ def get_time_maps_from_alignment( # representing the "performeance time" of the position of the score # onsets eq_perf_onsets = np.array( - [np.mean(perf_onsets[u]) for u in score_unique_onset_idxs] + [np.mean(perf_onsets[u.astype(int)]) for u in score_unique_onset_idxs] ) # Get maps diff --git a/partitura/score.py b/partitura/score.py index 582925b2..9c0a8dca 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -20,7 +20,7 @@ import warnings, sys import numpy as np from scipy.interpolate import PPoly -from typing import Union, List, Optional, Iterator, Iterable as Itertype +from typing import Union, List, Optional, Iterator, Iterable as Itertype, Any from partitura.utils import ( ComparableMixin, @@ -1583,6 +1583,8 @@ class GenericNote(TimedObject): appearance of this note (with respect to other notes) in the document in case the Note belongs to a part that was imported from MusicXML. Defaults to None. + technical: list, optional + Technical notation elements. """ @@ -1595,6 +1597,7 @@ def __init__( articulations=None, ornaments=None, doc_order=None, + technical=None, **kwargs, ): self._sym_dur = None @@ -1605,6 +1608,7 @@ def __init__( self.symbolic_duration = symbolic_duration self.articulations = articulations self.ornaments = ornaments + self.technical = technical self.doc_order = doc_order # these attributes are set after the instance is constructed @@ -2939,6 +2943,27 @@ def reference_tempo(self): return direction +class NoteTechnicalNotation(object): + """ + This object represents technical notations that + are part of a GenericNote object (e.g., fingering,) + These elements depend on a note, but can have their own properties + """ + + def __init__(self, type: str, info: Optional[Any] = None) -> None: + self.type = type + self.info = info + + +class Fingering(NoteTechnicalNotation): + def __init__(self, fingering: int) -> None: + super().__init__( + type="fingering", + info=fingering, + ) + self.fingering = fingering + + class PartGroup(object): """Represents a grouping of several instruments, usually named, and expressed in the score with a group symbol such as a brace or From e0ed514e4e0b17c5439130ed2a47f411c8db01f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Tue, 9 Apr 2024 18:22:58 +0200 Subject: [PATCH 2/4] allow to_matched_score to use note arrays as inputs --- partitura/musicanalysis/performance_codec.py | 22 ++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/partitura/musicanalysis/performance_codec.py b/partitura/musicanalysis/performance_codec.py index bf6e57c7..4756a2b1 100644 --- a/partitura/musicanalysis/performance_codec.py +++ b/partitura/musicanalysis/performance_codec.py @@ -608,8 +608,8 @@ def tempo_by_derivative( @deprecated_alias(part="score", ppart="performance") def to_matched_score( - score: ScoreLike, - performance: PerformanceLike, + score: Union[ScoreLike, np.ndarray], + performance: Union[PerformanceLike, np.ndarray], alignment: list, include_score_markings=False, ): @@ -635,7 +635,7 @@ def to_matched_score( a["score_id"] = str(a["score_id"]) feature_functions = None - if include_score_markings: + if include_score_markings and not isinstance(score, np.ndarray): feature_functions = [ "loudness_direction_feature", "articulation_feature", @@ -643,8 +643,18 @@ def to_matched_score( "slur_feature", ] - na = note_features.compute_note_array(score, feature_functions=feature_functions) - p_na = performance.note_array() + if isinstance(score, np.ndarray): + na = score + else: + na = note_features.compute_note_array( + score, + feature_functions=feature_functions, + ) + + if isinstance(performance, np.ndarray): + p_na = performance + else: + p_na = performance.note_array() part_by_id = dict((n["id"], na[na["id"] == n["id"]]) for n in na) ppart_by_id = dict((n["id"], p_na[p_na["id"] == n["id"]]) for n in p_na) @@ -682,7 +692,7 @@ def to_matched_score( ("p_duration", "f4"), ("velocity", "i4"), ] - if include_score_markings: + if include_score_markings and not isinstance(score, np.ndarray): fields += [("voice", "i4")] fields += [ (field, sn.dtype.fields[field][0]) From 10ad1a572f8582f12e4bcb296b0ab80f2d1276a2 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 25 Apr 2024 11:18:29 +0200 Subject: [PATCH 3/4] added fingering support in MEI export. --- partitura/io/exportmei.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index a16261e5..c8ec7c6b 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -224,6 +224,7 @@ def _handle_measure(self, measure, measure_el): self._handle_harmony(measure_el, start=measure.start.t, end=measure.end.t) self._handle_fermata(measure_el, start=measure.start.t, end=measure.end.t) self._handle_barline(measure_el, start=measure.start.t, end=measure.end.t) + self._handle_fingering(measure_el, start=measure.start.t, end=measure.end.t) return measure_el def _handle_chord(self, chord, xml_voice_el): @@ -291,7 +292,7 @@ def _handle_note(self, note, xml_voice_el): note_el.set("grace", "acc") return duration - def _handle_tuplets(self, measure_el, start, end): + def _handle_tuplets(self, measure_el: lxml.etree._Element, start: int, end: int): for tuplet in self.part.iter_all(spt.Tuplet, start=start, end=end): start_note = tuplet.start_note end_note = tuplet.end_note @@ -329,7 +330,7 @@ def _handle_tuplets(self, measure_el, start, end): for el in xml_el_within_tuplet: tuplet_el.append(el) - def _handle_beams(self, measure_el, start, end): + def _handle_beams(self, measure_el: lxml.etree._Element, start: int, end: int): for beam in self.part.iter_all(spt.Beam, start=start, end=end): # If the beam has only one note, skip it if len(beam.notes) < 2: @@ -369,7 +370,7 @@ def _handle_beams(self, measure_el, start, end): if note_el.getparent() != beam_el: beam_el.append(note_el) - def _handle_clef_changes(self, measure_el, start, end): + def _handle_clef_changes(self, measure_el: lxml.etree._Element, start: int, end: int): for clef in self.part.iter_all(spt.Clef, start=start, end=end): # Clef element is parent of the note element if clef.start.t == 0: @@ -390,7 +391,7 @@ def _handle_clef_changes(self, measure_el, start, end): clef_el.set("shape", str(clef.sign)) clef_el.set("line", str(clef.line)) - def _handle_ks_changes(self, measure_el, start, end): + def _handle_ks_changes(self, measure_el: lxml.etree._Element, start: int, end: int): # For key signature changes, we add a new scoreDef element at the beginning of the measure # and add the key signature element as attributes of the scoreDef element for key_sig in self.part.iter_all(spt.KeySignature, start=start, end=end): @@ -420,7 +421,7 @@ def _handle_ks_changes(self, measure_el, start, end): parent = measure_el.getparent() parent.insert(parent.index(measure_el), score_def_el) - def _handle_ts_changes(self, measure_el, start, end): + def _handle_ts_changes(self, measure_el: lxml.etree._Element, start: int, end: int): # For key signature changes, we add a new scoreDef element at the beginning of the measure # and add the key signature element as attributes of the scoreDef element for time_sig in self.part.iter_all(spt.TimeSignature, start=start, end=end): @@ -436,7 +437,7 @@ def _handle_ts_changes(self, measure_el, start, end): score_def_el.set("count", str(time_sig.beats)) score_def_el.set("unit", str(time_sig.beat_type)) - def _handle_harmony(self, measure_el, start, end): + def _handle_harmony(self, measure_el: lxml.etree._Element, start: int, end: int): """ For harmonies we add a new harm element at the beginning of the measure. The position doesn't really matter since the tstamp attribute will place it correctly @@ -477,7 +478,7 @@ def _handle_harmony(self, measure_el, start, end): # text is a child element of harmony but not a xml element harm_el.text = "|" + harmony.text - def _handle_fermata(self, measure_el, start, end): + def _handle_fermata(self, measure_el: lxml.etree._Element, start: int, end: int): for fermata in self.part.iter_all(spt.Fermata, start=start, end=end): if fermata.ref is not None: note = fermata.ref @@ -495,7 +496,7 @@ def _handle_fermata(self, measure_el, start, end): # Set the fermata to be above the staff (the highest staff) fermata_el.set("staff", "1") - def _handle_barline(self, measure_el, start, end): + def _handle_barline(self, measure_el: lxml.etree._Element, start: int, end: int): for end_barline in self.part.iter_all( spt.Ending, start=end, end=end + 1, mode="ending" ): @@ -514,6 +515,22 @@ def _handle_barline(self, measure_el, start, end): ): measure_el.set("left", "rptstart") + def _handle_fingering(self, measure_el: lxml.etree._Element, start: int, end: int): + """ + For fingering we add a new fing element at the end of the measure. + The position doesn't really matter since the startid attribute will place it correctly + """ + for note in self.part.iter_all(spt.Note, start=start, end=end): + for technical_notation in note.technical: + if isinstance(technical_notation, score.Fingering) and note.id is not None: + fing_el = etree.SubElement(measure_el, "fing") + fing_el.set(XMLNS_ID, "fing-" + self.elc_id()) + fing_el.set("startid", note.id) + # Naive way to place the fingering notation + fing_el.set("place", ("above" if note.staff == 1 else "below")) + # text is a child element of fingering but not a xml element + fing_el.text = technical_notation.fingering + @deprecated_alias(parts="score_data") def save_mei( From 86117e8ffee5693fd3a84cc3e1ab44a481e9dc6e Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 3 Dec 2024 12:34:16 +0100 Subject: [PATCH 4/4] minor corrections. --- partitura/io/exportmei.py | 20 +++++++++++--------- partitura/score.py | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index 5a43c71a..c4ae4b15 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -6,6 +6,7 @@ import math from collections import defaultdict from lxml import etree +import lxml import partitura.score as spt from operator import itemgetter from itertools import groupby @@ -553,15 +554,16 @@ def _handle_fingering(self, measure_el: lxml.etree._Element, start: int, end: in The position doesn't really matter since the startid attribute will place it correctly """ for note in self.part.iter_all(spt.Note, start=start, end=end): - for technical_notation in note.technical: - if isinstance(technical_notation, score.Fingering) and note.id is not None: - fing_el = etree.SubElement(measure_el, "fing") - fing_el.set(XMLNS_ID, "fing-" + self.elc_id()) - fing_el.set("startid", note.id) - # Naive way to place the fingering notation - fing_el.set("place", ("above" if note.staff == 1 else "below")) - # text is a child element of fingering but not a xml element - fing_el.text = technical_notation.fingering + if note.technical is not None: + for technical_notation in note.technical: + if isinstance(technical_notation, score.Fingering) and note.id is not None: + fing_el = etree.SubElement(measure_el, "fing") + fing_el.set(XMLNS_ID, "fing-" + self.elc_id()) + fing_el.set("startid", note.id) + # Naive way to place the fingering notation + fing_el.set("place", ("above" if note.staff == 1 else "below")) + # text is a child element of fingering but not a xml element + fing_el.text = technical_notation.fingering @deprecated_alias(parts="score_data") diff --git a/partitura/score.py b/partitura/score.py index f5381d2c..acbd1507 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -21,7 +21,7 @@ import numpy as np import re from scipy.interpolate import PPoly -from typing import Union, List, Optional, Iterator, Iterable as Itertype +from typing import Union, List, Optional, Iterator, Any, Iterable as Itertype import difflib from partitura.utils import ( ComparableMixin,