From 068072658221329c680730dab197c73d75539d89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Sat, 18 Jan 2025 15:07:45 +0100 Subject: [PATCH] fix issue parsing incorrectly formatted fingering in MusicXML --- partitura/io/importmusicxml.py | 27 ++++++++++++++++++++++++--- partitura/score.py | 33 ++++++++++++++++++++++++++++++++- partitura/utils/misc.py | 23 ++++++++++++++++++++++- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index 69b7a04a..2632432c 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -21,7 +21,7 @@ import partitura.score as score from partitura.score import assign_note_ids from partitura.utils import ensure_notearray -from partitura.utils.misc import deprecated_alias, deprecated_parameter, PathLike +from partitura.utils.misc import deprecated_alias, deprecated_parameter, PathLike, parse_ints __all__ = ["load_musicxml", "musicxml_to_notearray"] @@ -1677,8 +1677,29 @@ def get_technical_notations(e: etree._Element) -> List[score.NoteTechnicalNotati def parse_fingering(e: etree._Element) -> score.Fingering: - fingering = score.Fingering(fingering=int(e.text)) - + try: + # There seems to be a few cases with fingerings encoded like 4_1. + # This is not standard in MusicXML according to the documentation, + # but since it appears in files from the web, and can be displayed + # with MuseScore, the solution for now is just to take the fist value. + finger_info = parse_ints(e.text) + except Exception as e: + # Do not raise an error if fingering info cannot be parsed, insted + # just set it as None. + warnings.warn(f"Cannot parse fingering info for {e.text}!") + finger_info = [None] + + is_alternate = e.attrib.get("alternate", False) + is_substitution = e.attrib.get("substitution", False) + placement = e.attrib.get("placement", None) + + # If there is more than one finger, only take the first one + fingering = score.Fingering( + fingering=finger_info[0], + is_substitution=is_alternate, + is_alternate=is_alternate, + placement=placement, + ) return fingering diff --git a/partitura/score.py b/partitura/score.py index 49477288..454c59cd 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -3396,12 +3396,43 @@ def __init__(self, type: str, info: Optional[Any] = None) -> None: class Fingering(NoteTechnicalNotation): - def __init__(self, fingering: int) -> None: + """ + This object represents fingering. For now, it supports attributes + present in MusicXML: + + https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/fingering/ + + Parameters + ---------- + fingering : Optional[int] + Fingering information. Can be None (usually the result of incorrect parsing of fingering). + + is_substitution: bool + Whether this fingering is a substitution in the middle of a note. Default is False + + is_alternate: bool + Whether this fingering is an alternative fingering. Default is False + + placement: str + Placement of the fingering (above or below a note) + """ + + def __init__( + self, + fingering: Optional[int], + is_substitution: bool = False, + placement: Optional[str] = None, + is_alternate: bool = False, + ) -> None: super().__init__( type="fingering", info=fingering, ) self.fingering = fingering + self.is_alternate = is_alternate + self.alternative_fingering = [] + self.is_substitution = is_substitution + self.placement = placement class PartGroup(object): diff --git a/partitura/utils/misc.py b/partitura/utils/misc.py index 3695e189..c38c13e8 100644 --- a/partitura/utils/misc.py +++ b/partitura/utils/misc.py @@ -8,8 +8,9 @@ import warnings from urllib.request import urlopen from shutil import copyfileobj +import re -from typing import Union, Callable, Dict, Any, Iterable, Optional +from typing import Union, Callable, Dict, Any, Iterable, Optional, List import numpy as np @@ -280,3 +281,23 @@ def download_file( """ with urlopen(url) as in_stream, open(out, "wb") as out_file: copyfileobj(in_stream, out_file) + + +def parse_ints(input_string: str) -> List[int]: + """ + Parse all numbers from a given string where numbers are separated by spaces or tabs. + + Parameters + ---------- + input_string : str + The input string containing numbers separated by spaces or tabs. + + Returns + ------- + List[int] + A list of integers extracted from the input string. + """ + # Regular expression to match numbers + pattern = r'\d+' + # Find all matches and convert them to integers + return list(map(int, re.findall(pattern, input_string))) \ No newline at end of file