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 001/151] 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 002/151] 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 003/151] 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 ea5bc08b94ec835c68ca3afe4587ef901b5fdc46 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 1 May 2024 21:58:15 +0200 Subject: [PATCH 004/151] first draft for importing romantext annotations. --- partitura/io/importrntxt.py | 132 ++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 partitura/io/importrntxt.py diff --git a/partitura/io/importrntxt.py b/partitura/io/importrntxt.py new file mode 100644 index 00000000..08171654 --- /dev/null +++ b/partitura/io/importrntxt.py @@ -0,0 +1,132 @@ +import partitura.score as spt +import os.path as osp +import numpy as np +from urllib.parse import urlparse +import urllib.request + + +def load_rntxt(path: spt.Path, part=None, return_part=False): + if not osp.exists(path): + raise FileNotFoundError(f"File not found: {path}") + if is_url(path): + data = load_data_from_url(path) + lines = data.split("\n") + else: + with open(path, "r") as f: + lines = f.readlines() + assert validate_rntxt(lines) + + # remove empty lines + lines = [line for line in lines if line.strip()] + + parser = RntxtParser(part) + parser.parse(lines) + if return_part or part is None: + return parser.part + return + + +def validate_rntxt(lines): + # TODO: Implement + return True + + +def load_data_from_url(url: str): + with urllib.request.urlopen(url) as response: + data = response.read().decode() + return data + + +def is_url(input): + try: + result = urlparse(input) + return all([result.scheme, result.netloc]) + except ValueError: + return False + + +class RntxtParser: + def __init__(self, score=None): + if score is not None: + self.ref_part = score.parts[0] + quarter_duration = self.ref_part._quarter_durations[0] + ref_measures = self.ref_part.measures + else: + quarter_duration = 4 + ref_measures = [] + self.part = spt.Part(id="rn", part_name="Rn", part_abbreviation="rnp", quarter_duration=quarter_duration) + # include measures + for measure in ref_measures: + self.part.add(measure) + self.measures = {m.number: m for m in self.part.measures} + self.current_measure = None + self.measure_beat_position = 1 + self.current_voice = None + self.current_note = None + self.current_chord = None + self.current_tie = None + self.key = None + + def parse(self, lines): + np_lines = np.array(lines) + potential_measure_lines = np.lines[np.char.startswith(np_lines, "m")] + for line in potential_measure_lines: + self._handle_measure(line) + + def _handle_measure(self, line): + if not self._validate_measure_line(line): + return + elements = line.split(" ") + measure_number = elements[0].strip("m") + if not measure_number.isnumeric(): + # TODO: check if it is a valid measure number or variation + raise ValueError(f"Invalid measure number: {measure_number}") + if measure_number not in self.measures.keys(): + self.current_measure = spt.Measure(number=measure_number) + self.measures[measure_number] = self.current_measure + else: + self.current_measure = self.measures[measure_number] + + # starts counting beats from 1 + self.measure_beat_position = 1 + for element in elements[1:]: + self._handle_element(element) + + def _handle_element(self, element): + # if element starts with "b" followed by a number ("float" or "int") it is a beat + if element.startswith("b") and element[1:].replace(".", "").isnumeric(): + self.measure_beat_position = float(element[1:]) + + # if element starts with [A-G] and it includes : it is a key + elif element[0] in "ABCDEFG" and ":" in element: + self._handle_key(element) + # if element only contains "|" or ":" (and combinations) it is a barline + elif all(c in "|:" for c in element): + self._handle_barline(element) + # else it is a roman numeral + else: + self._handle_roman_numeral(element) + + def _handle_key(self, element): + # key is in the format "C:" or "c:" for C major or c minor + # for alterations use "C#:" or "c#:" for C# major or c# minor + name = element[0] + mode = "major" if name.isupper() else "minor" + step = name.upper() + # handle alterations + alter = element.count("#") - element.count("b") + # step and alter to fifths + + def _handle_barline(self, element): + pass + + def _handle_roman_numeral(self, element): + rn = spt.RomanNumeral(text=element, local_key=self.key) + + def _validate_measure_line(self, line): + # does it have elements + if not len(line.split(" ") > 1): + return False + + + From 417c88c10278ac9a5db4966e88ab41c7672f32c3 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 2 May 2024 11:05:27 +0200 Subject: [PATCH 005/151] some edits on Rntxt parser for import. --- partitura/io/importrntxt.py | 47 ++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/partitura/io/importrntxt.py b/partitura/io/importrntxt.py index 08171654..63ff345d 100644 --- a/partitura/io/importrntxt.py +++ b/partitura/io/importrntxt.py @@ -3,15 +3,17 @@ import numpy as np from urllib.parse import urlparse import urllib.request +from partitura.utils.music import key_name_to_fifths_mode def load_rntxt(path: spt.Path, part=None, return_part=False): - if not osp.exists(path): - raise FileNotFoundError(f"File not found: {path}") + if is_url(path): data = load_data_from_url(path) lines = data.split("\n") else: + if not osp.exists(path): + raise FileNotFoundError(f"File not found: {path}") with open(path, "r") as f: lines = f.readlines() assert validate_rntxt(lines) @@ -57,21 +59,30 @@ def __init__(self, score=None): self.part = spt.Part(id="rn", part_name="Rn", part_abbreviation="rnp", quarter_duration=quarter_duration) # include measures for measure in ref_measures: - self.part.add(measure) + self.part.add(measure, measure.start.t, measure.end.t) self.measures = {m.number: m for m in self.part.measures} self.current_measure = None + self.current_position = 0 self.measure_beat_position = 1 self.current_voice = None self.current_note = None self.current_chord = None self.current_tie = None - self.key = None + self.num_parsed_romans = 0 + self.key = "C" def parse(self, lines): - np_lines = np.array(lines) - potential_measure_lines = np.lines[np.char.startswith(np_lines, "m")] - for line in potential_measure_lines: - self._handle_measure(line) + # np_lines = np.array(lines) + # potential_measure_lines = np.lines[np.char.startswith(np_lines, "m")] + # for line in potential_measure_lines: + # self._handle_measure(line) + for line in lines: + if line.startswith("Time Signature:"): + self.time_signature = line.split(":")[1].strip() + elif line.startswith("Pedal:"): + self.pedal = line.split(":")[1].strip() + elif line.startswith("m"): + self._handle_measure(line) def _handle_measure(self, line): if not self._validate_measure_line(line): @@ -81,12 +92,15 @@ def _handle_measure(self, line): if not measure_number.isnumeric(): # TODO: check if it is a valid measure number or variation raise ValueError(f"Invalid measure number: {measure_number}") + measure_number = int(measure_number) if measure_number not in self.measures.keys(): self.current_measure = spt.Measure(number=measure_number) self.measures[measure_number] = self.current_measure + self.part.add(self.current_measure, self.current_position) else: self.current_measure = self.measures[measure_number] + self.current_position = self.current_measure.start.t # starts counting beats from 1 self.measure_beat_position = 1 for element in elements[1:]: @@ -96,6 +110,13 @@ def _handle_element(self, element): # if element starts with "b" followed by a number ("float" or "int") it is a beat if element.startswith("b") and element[1:].replace(".", "").isnumeric(): self.measure_beat_position = float(element[1:]) + if self.current_measure.number == 0: + if (self.current_position == 0 and self.num_parsed_romans == 0): + self.current_position = 0 + else: + self.current_position = self.part.inv_beat_map(self.part.beat_map(self.current_position) + self.measure_beat_position - 1) + else: + self.current_position = self.part.inv_beat_map(self.part.beat_map(self.current_measure.start.t) + self.measure_beat_position - 1) # if element starts with [A-G] and it includes : it is a key elif element[0] in "ABCDEFG" and ":" in element: @@ -116,17 +137,25 @@ def _handle_key(self, element): # handle alterations alter = element.count("#") - element.count("b") # step and alter to fifths + fifths, mode = key_name_to_fifths_mode(element.strip(":")) + ks = spt.KeySignature(fifths=fifths, mode=mode) + self.key = element.strip(":") + self.part.add(ks, self.current_position) def _handle_barline(self, element): pass def _handle_roman_numeral(self, element): + element = element.strip() rn = spt.RomanNumeral(text=element, local_key=self.key) + self.part.add(rn, self.current_position) + self.num_parsed_romans += 1 def _validate_measure_line(self, line): # does it have elements - if not len(line.split(" ") > 1): + if not len(line.split(" ")) > 1: return False + return True From 8a870477bef86de111dbddadbc9407bba84d1773 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 2 May 2024 15:08:36 +0200 Subject: [PATCH 006/151] Rntxt parser first draft complete. --- partitura/io/importrntxt.py | 52 ++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/partitura/io/importrntxt.py b/partitura/io/importrntxt.py index 63ff345d..1e51e4b0 100644 --- a/partitura/io/importrntxt.py +++ b/partitura/io/importrntxt.py @@ -53,13 +53,23 @@ def __init__(self, score=None): self.ref_part = score.parts[0] quarter_duration = self.ref_part._quarter_durations[0] ref_measures = self.ref_part.measures + ref_time_sigs = self.ref_part.time_sigs + ref_keys = self.ref_part.key_sigs else: quarter_duration = 4 ref_measures = [] + ref_time_sigs = [] + ref_keys = [] self.part = spt.Part(id="rn", part_name="Rn", part_abbreviation="rnp", quarter_duration=quarter_duration) # include measures for measure in ref_measures: self.part.add(measure, measure.start.t, measure.end.t) + # include time signatures + for time_sig in ref_time_sigs: + self.part.add(time_sig, time_sig.start.t) + # include key signatures + for key in ref_keys: + self.part.add(key, key.start.t) self.measures = {m.number: m for m in self.part.measures} self.current_measure = None self.current_position = 0 @@ -84,19 +94,34 @@ def parse(self, lines): elif line.startswith("m"): self._handle_measure(line) + self.currate_ending_times() + + def currate_ending_times(self): + romans = list(self.part.iter_all(spt.RomanNumeral)) + starting_times = [rn.start.t for rn in romans] + argsort_start = np.argsort(starting_times) + for i, rn_idx in enumerate(argsort_start[:-1]): + rn = romans[rn_idx] + if rn.end is None: + rn.end = romans[argsort_start[i+1]].start + def _handle_measure(self, line): if not self._validate_measure_line(line): return elements = line.split(" ") measure_number = elements[0].strip("m") if not measure_number.isnumeric(): - # TODO: check if it is a valid measure number or variation - raise ValueError(f"Invalid measure number: {measure_number}") + # TODO: complete check for variation measures + if "var" in measure_number: + return + else: + + raise ValueError(f"Invalid measure number: {measure_number}") measure_number = int(measure_number) if measure_number not in self.measures.keys(): self.current_measure = spt.Measure(number=measure_number) self.measures[measure_number] = self.current_measure - self.part.add(self.current_measure, self.current_position) + self.part.add(self.current_measure, int(self.current_position)) else: self.current_measure = self.measures[measure_number] @@ -114,9 +139,9 @@ def _handle_element(self, element): if (self.current_position == 0 and self.num_parsed_romans == 0): self.current_position = 0 else: - self.current_position = self.part.inv_beat_map(self.part.beat_map(self.current_position) + self.measure_beat_position - 1) + self.current_position = self.part.inv_beat_map(self.part.beat_map(self.current_position) + self.measure_beat_position - 1).item() else: - self.current_position = self.part.inv_beat_map(self.part.beat_map(self.current_measure.start.t) + self.measure_beat_position - 1) + self.current_position = self.part.inv_beat_map(self.part.beat_map(self.current_measure.start.t) + self.measure_beat_position - 1).item() # if element starts with [A-G] and it includes : it is a key elif element[0] in "ABCDEFG" and ":" in element: @@ -140,15 +165,28 @@ def _handle_key(self, element): fifths, mode = key_name_to_fifths_mode(element.strip(":")) ks = spt.KeySignature(fifths=fifths, mode=mode) self.key = element.strip(":") - self.part.add(ks, self.current_position) + self.part.add(ks, int(self.current_position)) def _handle_barline(self, element): pass def _handle_roman_numeral(self, element): element = element.strip() + # change RN6/5 to RN65 + if "/" in element: + if element.split("/")[1][0].isnumeric(): + # replace only the first occurrence of "/" with "" + element = element.replace("/", "", 1) rn = spt.RomanNumeral(text=element, local_key=self.key) - self.part.add(rn, self.current_position) + + try: + self.part.add(rn, int(self.current_position)) + except ValueError: + print(f"Could not add roman numeral {element} at position {self.current_position}") + return + # Set the end of the previous roman numeral + # if self.previous_rn is not None: + # self.previous_rn.end = spt.TimePoint(t=self.current_position) self.num_parsed_romans += 1 def _validate_measure_line(self, line): From 5b5d358d3fcabe571988ab6f65422adebf5019c4 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 7 May 2024 11:41:34 +0200 Subject: [PATCH 007/151] minor changes on RN parser. --- partitura/io/importrntxt.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/partitura/io/importrntxt.py b/partitura/io/importrntxt.py index 1e51e4b0..fa2b080f 100644 --- a/partitura/io/importrntxt.py +++ b/partitura/io/importrntxt.py @@ -71,6 +71,7 @@ def __init__(self, score=None): for key in ref_keys: self.part.add(key, key.start.t) self.measures = {m.number: m for m in self.part.measures} + self.part.add(spt.Staff(number=1, lines=1), 0) self.current_measure = None self.current_position = 0 self.measure_beat_position = 1 @@ -103,7 +104,7 @@ def currate_ending_times(self): for i, rn_idx in enumerate(argsort_start[:-1]): rn = romans[rn_idx] if rn.end is None: - rn.end = romans[argsort_start[i+1]].start + rn.end = romans[argsort_start[i+1]].start if rn.start.t < romans[argsort_start[i+1]].start.t else rn.start.t + 1 def _handle_measure(self, line): if not self._validate_measure_line(line): @@ -172,7 +173,7 @@ def _handle_barline(self, element): def _handle_roman_numeral(self, element): element = element.strip() - # change RN6/5 to RN65 + # change RN6/5 to RN65 but keep RN65/RN if "/" in element: if element.split("/")[1][0].isnumeric(): # replace only the first occurrence of "/" with "" From a71a6925e0505286edf4236ee64fd1747b476856 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 11 May 2024 11:05:00 +0200 Subject: [PATCH 008/151] some changes and first rntxt test draft. --- partitura/__init__.py | 1 + partitura/io/__init__.py | 1 + partitura/io/importrntxt.py | 19 ++++++++++++++++++- tests/test_rntxt.py | 16 ++++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 tests/test_rntxt.py diff --git a/partitura/__init__.py b/partitura/__init__.py index 975f8dba..68a1768f 100644 --- a/partitura/__init__.py +++ b/partitura/__init__.py @@ -14,6 +14,7 @@ from .io.exportmusicxml import save_musicxml from .io.importmei import load_mei from .io.importkern import load_kern +from .io.importrntxt import load_rntxt from .io.importmusic21 import load_music21 from .io.importdcml import load_dcml from .io.importmidi import load_score_midi, load_performance_midi, midi_to_notearray diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index 6d454df3..b3053733 100644 --- a/partitura/io/__init__.py +++ b/partitura/io/__init__.py @@ -12,6 +12,7 @@ from .importmatch import load_match from .importmei import load_mei from .importkern import load_kern +from .importrntxt import load_rntxt from .importparangonada import load_parangonada_csv from .exportparangonada import save_parangonada_csv from .importmusic21 import load_music21 diff --git a/partitura/io/importrntxt.py b/partitura/io/importrntxt.py index fa2b080f..e00be55d 100644 --- a/partitura/io/importrntxt.py +++ b/partitura/io/importrntxt.py @@ -48,6 +48,12 @@ def is_url(input): class RntxtParser: + """ + A parser for RNtxt format to a partitura Part. + + For full specification of the format visit: + https://github.com/MarkGotham/When-in-Rome/blob/master/syntax.md + """ def __init__(self, score=None): if score is not None: self.ref_part = score.parts[0] @@ -172,12 +178,23 @@ def _handle_barline(self, element): pass def _handle_roman_numeral(self, element): + """ + The handling or roman numeral aims to translate rntxt notation to internal partitura notation. + + Parameters + ---------- + element: txt + The element is a rntxt notation string + """ + # Remove line endings and spaces element = element.strip() - # change RN6/5 to RN65 but keep RN65/RN + # change strings such as RN6/5 to RN65 but keep RN65/RN for the secondary degree if "/" in element: if element.split("/")[1][0].isnumeric(): # replace only the first occurrence of "/" with "" element = element.replace("/", "", 1) + # Validity checks happen inside the Roman Numeral object + # The checks include 1 & 2 Degree, Root, Bass, Inversion, and Quality extraction. rn = spt.RomanNumeral(text=element, local_key=self.key) try: diff --git a/tests/test_rntxt.py b/tests/test_rntxt.py new file mode 100644 index 00000000..269f276f --- /dev/null +++ b/tests/test_rntxt.py @@ -0,0 +1,16 @@ +from partitura import load_rntxt +from partitura import load_musicxml +import urllib.request +import unittest + + +class TextRNtxtImport(unittest.TestCase): + def text_chorale_from_url(self): + score_url = "" + rntxt_url = "" + with urllib.request.urlopen(score_url) as response: + data = response.read().decode() + score = load_musicxml(data) + rn_part = load_rntxt(rntxt_url, score, return_part=True) + self.assertTrue() + From 700474e50e6a5bb51f59efb178ca7ab5b613ecbe Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 11 May 2024 19:53:36 +0200 Subject: [PATCH 009/151] Minor Changes for Roman Numeral Checking. --- partitura/score.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index cb19eb0d..3d368551 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -2821,7 +2821,7 @@ def __init__(self, text, inversion=None, local_key=None, primary_degree=None, se self.secondary_degree = secondary_degree if secondary_degree is not None else self._process_secondary_degree() self.quality = quality if quality is not None and quality in self.accepted_qualities else self._process_quality() # only process the root note if the roman numeral is valid - if self.local_key and self.primary_degree and self.secondary_degree and self.quality and self.inversion: + if self.local_key and self.primary_degree and self.secondary_degree and self.quality and self.inversion is not None: self.root = self.find_root_note() self.bass_note = self.find_bass_note() @@ -5632,8 +5632,8 @@ def process_local_key(loc_k, glob_k, return_step_alter=False): "iii": Interval(3, "m"), "iv": Interval(4, "P"), "v": Interval(5, "P"), - "vi": Interval(6, "m"), - "vii": Interval(7, "m"), + "vi": Interval(6, "M"), + "vii": Interval(7, "M"), "viio": Interval(7, "M"), "N": Interval(2, "m"), "iio": Interval(2, "M"), From 4e03cd10622680397ac157e9fa65e6c08cae5b4a Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 11 May 2024 19:53:56 +0200 Subject: [PATCH 010/151] Better loading for rntxt format. --- partitura/io/importrntxt.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/partitura/io/importrntxt.py b/partitura/io/importrntxt.py index e00be55d..a13c5c58 100644 --- a/partitura/io/importrntxt.py +++ b/partitura/io/importrntxt.py @@ -1,3 +1,5 @@ +import re + import partitura.score as spt import os.path as osp import numpy as np @@ -151,7 +153,7 @@ def _handle_element(self, element): self.current_position = self.part.inv_beat_map(self.part.beat_map(self.current_measure.start.t) + self.measure_beat_position - 1).item() # if element starts with [A-G] and it includes : it is a key - elif element[0] in "ABCDEFG" and ":" in element: + elif len(re.findall(r"[A-Ga-g#b:]", element)) == len(element) and element[-1] == ":": self._handle_key(element) # if element only contains "|" or ":" (and combinations) it is a barline elif all(c in "|:" for c in element): @@ -167,9 +169,10 @@ def _handle_key(self, element): mode = "major" if name.isupper() else "minor" step = name.upper() # handle alterations - alter = element.count("#") - element.count("b") + alter = element[1:].strip(":") + key_name = f"{step}{alter}{('m' if mode == 'minor' else '')}" # step and alter to fifths - fifths, mode = key_name_to_fifths_mode(element.strip(":")) + fifths, mode = key_name_to_fifths_mode(key_name) ks = spt.KeySignature(fifths=fifths, mode=mode) self.key = element.strip(":") self.part.add(ks, int(self.current_position)) @@ -190,9 +193,14 @@ def _handle_roman_numeral(self, element): element = element.strip() # change strings such as RN6/5 to RN65 but keep RN65/RN for the secondary degree if "/" in element: - if element.split("/")[1][0].isnumeric(): - # replace only the first occurrence of "/" with "" - element = element.replace("/", "", 1) + # if all elements between "/" are either digits or one of [o, +] then remove "/" else leave it in place + el_list = element.split("/") + element = el_list[0] + for el in el_list[1:]: + if len(re.findall(r"[1-9\+o]", el)) == len(el): + element += el + else: + element += "/" + el # Validity checks happen inside the Roman Numeral object # The checks include 1 & 2 Degree, Root, Bass, Inversion, and Quality extraction. rn = spt.RomanNumeral(text=element, local_key=self.key) From cb14c20f035a7666ba671cd827e12d15c2dfc7e7 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 11 May 2024 19:54:07 +0200 Subject: [PATCH 011/151] Test case for rntxt import. --- tests/test_rntxt.py | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/tests/test_rntxt.py b/tests/test_rntxt.py index 269f276f..fb8569c7 100644 --- a/tests/test_rntxt.py +++ b/tests/test_rntxt.py @@ -1,16 +1,39 @@ -from partitura import load_rntxt +from partitura import load_rntxt, load_kern +from partitura.score import RomanNumeral from partitura import load_musicxml import urllib.request import unittest +import os +from tests import KERN_PATH class TextRNtxtImport(unittest.TestCase): - def text_chorale_from_url(self): - score_url = "" - rntxt_url = "" - with urllib.request.urlopen(score_url) as response: - data = response.read().decode() - score = load_musicxml(data) + + def test_chorale_001_from_url(self): + score_path = os.path.join(KERN_PATH, "chor228.krn") + rntxt_url = "https://raw.githubusercontent.com/MarkGotham/When-in-Rome/master/Corpus/Early_Choral/Bach%2C_Johann_Sebastian/Chorales/228/analysis.txt" + score = load_kern(score_path) rn_part = load_rntxt(rntxt_url, score, return_part=True) - self.assertTrue() + romans = list(rn_part.iter_all(RomanNumeral)) + roots = [r.root for r in romans] + bass = [r.bass_note for r in romans] + primary_degree = [r.primary_degree for r in romans] + secondary_degree = [r.secondary_degree for r in romans] + local_key = [r.local_key for r in romans] + quality = [r.quality for r in romans] + inversion = [r.inversion for r in romans] + expected_roots = ['A', 'A', 'E', 'A', 'A', 'G', 'C', 'C', 'G', 'G', 'A', 'A', 'E', 'E', 'E', 'E', 'A', 'D', 'G#', 'A', 'E', 'A', 'D', 'A', 'B'] + expected_bass = ['A', 'A', 'G#', 'A', 'A', 'B', 'C', 'E', 'G', 'G', 'A', 'A', 'E', 'E', 'E', 'D', 'C', 'C', 'B', 'A', 'E', 'A', 'F', 'E', 'D'] + expected_pdegree = ['i', 'i', 'V', 'i', 'vi', 'V', 'I', 'I', 'V', 'V', 'vi', 'i', 'V', 'V', 'V', 'V', 'i', 'IV', 'viio', 'i', 'V', 'i', 'iv', 'i', 'iio'] + expected_sdegree = ['i', 'i', 'i', 'i', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i'] + expected_lkey = ['a', 'a', 'a', 'a', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'] + expected_quality = ['min', 'min', 'maj', 'min', 'min', 'maj', 'maj', 'maj', 'maj', '7', 'min', 'min', 'maj', 'maj', 'maj', '7', 'min', '7', 'dim', 'min', 'maj', 'min', 'min', 'min', 'dim7'] + expected_inversion = [0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 3, 1, 3, 1, 0, 0, 0, 1, 2, 1] + self.assertEqual(roots, expected_roots) + self.assertEqual(bass, expected_bass) + self.assertEqual(primary_degree, expected_pdegree) + self.assertEqual(secondary_degree, expected_sdegree) + self.assertEqual(local_key, expected_lkey) + self.assertEqual(quality, expected_quality) + self.assertEqual(inversion, expected_inversion) From eae45426db49fa1cf6c632d480cddae0f41894cf Mon Sep 17 00:00:00 2001 From: sildater Date: Fri, 19 Jul 2024 12:08:53 +0200 Subject: [PATCH 012/151] fix 358 --- partitura/musicanalysis/performance_codec.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/partitura/musicanalysis/performance_codec.py b/partitura/musicanalysis/performance_codec.py index 2eac40c2..3998797f 100644 --- a/partitura/musicanalysis/performance_codec.py +++ b/partitura/musicanalysis/performance_codec.py @@ -747,21 +747,15 @@ def get_time_maps_from_alignment( # Remove grace notes if remove_ornaments: - # TODO: check that all onsets have a duration? + # check that all onsets have a duration # ornaments (grace notes) do not have a duration - score_unique_onset_idxs = np.array( - [ + score_unique_onset_idxs = [ np.where(np.logical_and(score_onsets == u, score_durations > 0))[0] for u in score_unique_onsets - ], - dtype=object, - ) + ] else: - score_unique_onset_idxs = np.array( - [np.where(score_onsets == u)[0] for u in score_unique_onsets], - dtype=object, - ) + score_unique_onset_idxs = [np.where(score_onsets == u)[0] for u in score_unique_onsets] # For chords, we use the average performed onset as a proxy for # representing the "performeance time" of the position of the score From 50d334300be05e955135e93337f2c27a5a4e84b4 Mon Sep 17 00:00:00 2001 From: sildater Date: Fri, 19 Jul 2024 13:13:24 +0200 Subject: [PATCH 013/151] add check for empty note array --- partitura/utils/music.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index f9678228..38c60091 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -1182,6 +1182,9 @@ def _make_pianoroll( onset = note_info[:, 1] duration = note_info[:, 2] + if len(note_info) == 0: + raise ValueError("Note array is empty") + if np.any(duration < 0): raise ValueError("Note durations should be >= 0!") From cbc9f48ea45379c7a1c776520a32bb71629c3462 Mon Sep 17 00:00:00 2001 From: sildater Date: Fri, 19 Jul 2024 14:10:13 +0200 Subject: [PATCH 014/151] improve documentation performance note_array --- partitura/performance.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/partitura/performance.py b/partitura/performance.py index 8957491b..ef434120 100644 --- a/partitura/performance.py +++ b/partitura/performance.py @@ -152,8 +152,8 @@ def num_tracks(self) -> int: def note_array(self, *args, **kwargs) -> np.ndarray: """Structured array containing performance information. - The fields are 'id', 'pitch', 'onset_div', 'duration_div', - 'onset_sec', 'duration_sec' and 'velocity'. + The fields are 'id', 'pitch', 'onset_tick', 'duration_tick', + 'onset_sec', 'duration_sec', 'track', 'channel', and 'velocity'. """ fields = [ @@ -206,9 +206,19 @@ def from_note_array( id: str = None, part_name: str = None, ): - """Create an instance of PerformedPart from a note_array. + """ + Create an instance of PerformedPart from a note_array. Note that this property does not include non-note information (i.e. - controls such as sustain pedal). + controls such as sustain pedal, program changes, tempo changes, etc.). + + The following fields are mandatory: + 'pitch', 'onset_sec', 'duration_sec', and 'velocity'. + + The following fields are used if available: + 'id', 'track', 'channel'. + + The following fields are ignored: + 'onset_tick', 'duration_tick', all others """ if "id" not in note_array.dtype.names: n_ids = ["n{0}".format(i) for i in range(len(note_array))] From 693b2c428a71f61cb9e1eae5495f10c6dffc9cac Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 22 Jul 2024 13:28:04 +0000 Subject: [PATCH 015/151] Format code with black (bot) --- partitura/musicanalysis/performance_codec.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/partitura/musicanalysis/performance_codec.py b/partitura/musicanalysis/performance_codec.py index 3998797f..54c1b0ac 100644 --- a/partitura/musicanalysis/performance_codec.py +++ b/partitura/musicanalysis/performance_codec.py @@ -750,12 +750,14 @@ def get_time_maps_from_alignment( # check that all onsets have a duration # ornaments (grace notes) do not have a duration score_unique_onset_idxs = [ - np.where(np.logical_and(score_onsets == u, score_durations > 0))[0] - for u in score_unique_onsets - ] + np.where(np.logical_and(score_onsets == u, score_durations > 0))[0] + for u in score_unique_onsets + ] else: - score_unique_onset_idxs = [np.where(score_onsets == u)[0] for u in score_unique_onsets] + score_unique_onset_idxs = [ + np.where(score_onsets == u)[0] for u in score_unique_onsets + ] # For chords, we use the average performed onset as a proxy for # representing the "performeance time" of the position of the score From 1e801956fb56f9212d17025ebbbbc3d4d1d98464 Mon Sep 17 00:00:00 2001 From: sildater Date: Thu, 25 Jul 2024 13:02:19 +0200 Subject: [PATCH 016/151] fix bug, update documentation --- partitura/musicanalysis/note_features.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 57b9f9b5..ef02e20a 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -1049,6 +1049,12 @@ def metrical_strength_feature(na, part, **kwargs): This feature encodes the beat phase (relative position of a note within the measure), as well as metrical strength of common time signatures. + + 'beat_phase' encodes the position in the measure as value between 0.0 and 1.0 + 'metrical_strength_downbeat' is 1.0 on downbeats, 0.0 elsewhere + 'metrical_strength_secondary' is 1.0 on measure midpoint, 0.0 elsewhere, + not valid for triple meters + 'metrical_strength_weak' is 1.0 where both others are 0.0, 0.0 elsewhere """ names = [ "beat_phase", @@ -1063,7 +1069,7 @@ def metrical_strength_feature(na, part, **kwargs): W[:, 0] = np.divide(relod, totmd) # Onset Phase W[:, 1] = na["is_downbeat"].astype(float) W[:, 2][W[:, 0] == 0.5] = 1.00 - W[:, 3][np.nonzero(np.add(W[:, 1], W[:, 0]) == 1.00)] = 1.00 + W[:, 3][W[:, 1] == W[:, 2]] = 1.00 return W, names From 498995e02928ad581001f92b40141169109c8d29 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 1 Aug 2024 17:05:44 +0200 Subject: [PATCH 017/151] Corrected Offsets for tied notes. Ref issue: #365 --- partitura/io/importkern.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index b7012569..8419938d 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -435,12 +435,12 @@ def parse(self, spline: np.array): notes[self.tie_next], notes[np.roll(self.tie_next, -1)] ]: to_tie.tie_next = note - # note.tie_prev = to_tie - for note, to_tie in np.c_[ - notes[self.tie_prev], notes[np.roll(self.tie_prev, 1)] - ]: note.tie_prev = to_tie - # to_tie.tie_next = note + # for note, to_tie in np.c_[ + # notes[self.tie_prev], notes[np.roll(self.tie_prev, 1)] + # ]: + # note.tie_prev = to_tie + # # to_tie.tie_next = note elements[note_mask] = notes # Find Slur indices, i.e. where spline cells contain "(" or ")" From e06b9757557c2f2f0760d102d1c9c8558f1b9faa Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 3 Aug 2024 08:35:34 +0200 Subject: [PATCH 018/151] Correction for staff line 2 as int. Ref. Issue #367 --- partitura/io/importkern.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index 8419938d..316d5d2b 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -561,7 +561,7 @@ def process_clef_line(self, line: str): else: raise ValueError("Unrecognized clef line: {}".format(line)) else: - clef_line = has_line.group(0) + clef_line = int(has_line.group(0)) if octave_change and clef_line == 2 and clef == "G": octave = -1 elif octave_change: From 75b72135783445b97314c403f2bc176bc57a9f06 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 6 Aug 2024 02:42:02 +0200 Subject: [PATCH 019/151] Removed unused code and added comments. --- partitura/io/importkern.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index 316d5d2b..1935faf4 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -430,17 +430,15 @@ def parse(self, spline: np.array): self.tie_prev = np.zeros(note_num, dtype=bool) notes = np.vectorize(self.meta_note_line, otypes=[object])(spline[note_mask]) self.total_duration_values[note_mask] = self.note_duration_values - # shift tie_next by one to the right + # Notes should appear in order within stream so shift tie_next by one to the right + # and tie next and inversingly tie_prev also + # Case of note to chord tie or chord to note tie is not handled yet for note, to_tie in np.c_[ notes[self.tie_next], notes[np.roll(self.tie_next, -1)] ]: to_tie.tie_next = note note.tie_prev = to_tie - # for note, to_tie in np.c_[ - # notes[self.tie_prev], notes[np.roll(self.tie_prev, 1)] - # ]: - # note.tie_prev = to_tie - # # to_tie.tie_next = note + elements[note_mask] = notes # Find Slur indices, i.e. where spline cells contain "(" or ")" From 8dccc0a6b5c9f5411d7a10ba51f3725262ece620 Mon Sep 17 00:00:00 2001 From: huispaty Date: Thu, 8 Aug 2024 14:11:07 +0000 Subject: [PATCH 020/151] Format code with black (bot) --- partitura/musicanalysis/note_features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index ef02e20a..92e07c0b 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -1053,7 +1053,7 @@ def metrical_strength_feature(na, part, **kwargs): 'beat_phase' encodes the position in the measure as value between 0.0 and 1.0 'metrical_strength_downbeat' is 1.0 on downbeats, 0.0 elsewhere 'metrical_strength_secondary' is 1.0 on measure midpoint, 0.0 elsewhere, - not valid for triple meters + not valid for triple meters 'metrical_strength_weak' is 1.0 where both others are 0.0, 0.0 elsewhere """ names = [ From f532be461a0800a3049de933cb1beba4b6f748ac Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 13 Aug 2024 09:57:53 +0200 Subject: [PATCH 021/151] update tutorial with correct import --- docs/source/Tutorial/notebook.ipynb | 186 ++++++++++++++-------------- 1 file changed, 93 insertions(+), 93 deletions(-) diff --git a/docs/source/Tutorial/notebook.ipynb b/docs/source/Tutorial/notebook.ipynb index 31d98713..b90d44e4 100644 --- a/docs/source/Tutorial/notebook.ipynb +++ b/docs/source/Tutorial/notebook.ipynb @@ -47,11 +47,12 @@ }, "id": "PeabdL1k7YC4", "outputId": "fcb7d1be-27a1-4c79-c5d3-8cbfa54cae44", - "scrolled": true, "pycharm": { "is_executing": true - } + }, + "scrolled": true }, + "outputs": [], "source": [ "# Install partitura\n", "! pip install partitura\n", @@ -64,21 +65,20 @@ "import sys, os\n", "sys.path.insert(0, os.path.join(os.getcwd(), \"partitura_tutorial\", \"content\"))\n", "sys.path.insert(0,'/content/partitura_tutorial/content')\n" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 2, "id": "impressed-principle", "metadata": {}, + "outputs": [], "source": [ "import glob\n", "import partitura as pt\n", "import numpy as np\n", "import matplotlib.pyplot as plt" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -103,6 +103,7 @@ "execution_count": 3, "id": "photographic-profession", "metadata": {}, + "outputs": [], "source": [ "# setup the dataset\n", "from load_data import init_dataset\n", @@ -110,8 +111,7 @@ "MUSICXML_DIR = os.path.join(DATASET_DIR, 'musicxml')\n", "MIDI_DIR = os.path.join(DATASET_DIR, 'midi')\n", "MATCH_DIR = os.path.join(DATASET_DIR, 'match')" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -220,12 +220,12 @@ "execution_count": 4, "id": "c9179e78", "metadata": {}, + "outputs": [], "source": [ "path_to_musicxml = pt.EXAMPLE_MUSICXML\n", "part = pt.load_musicxml(path_to_musicxml)[0]\n", "print(part.pretty())" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -250,20 +250,20 @@ "execution_count": 5, "id": "423aac6a", "metadata": {}, + "outputs": [], "source": [ "part.notes" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 6, "id": "0a929369", "metadata": {}, + "outputs": [], "source": [ "dir(part.notes[0])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -278,23 +278,23 @@ "execution_count": 7, "id": "2a8293c9", "metadata": {}, + "outputs": [], "source": [ "a_new_note = pt.score.Note(id='n04', step='A', octave=4, voice=1)\n", "part.add(a_new_note, start=3, end=15)\n", "# print(part.pretty())" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 8, "id": "eba2fa93", "metadata": {}, + "outputs": [], "source": [ "part.remove(a_new_note)\n", "# print(part.pretty())" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -313,10 +313,10 @@ "execution_count": 9, "id": "e95eb0f7", "metadata": {}, + "outputs": [], "source": [ "part.beat_map(part.notes[0].end.t)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -331,10 +331,10 @@ "execution_count": 10, "id": "05346a03", "metadata": {}, + "outputs": [], "source": [ "part.time_signature_map(part.notes[0].end.t)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -356,22 +356,22 @@ "execution_count": 11, "id": "74943a93", "metadata": {}, + "outputs": [], "source": [ "for measure in part.iter_all(pt.score.Measure):\n", " print(measure)" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 12, "id": "6cbfd044", "metadata": {}, + "outputs": [], "source": [ "for note in part.iter_all(pt.score.GenericNote, include_subclasses=True, start=0, end=24):\n", " print(note)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -388,6 +388,7 @@ "execution_count": 13, "id": "fe430921", "metadata": {}, + "outputs": [], "source": [ "# figure out the last measure position, time signature and beat length in divs\n", "measures = [m for m in part.iter_all(pt.score.Measure)]\n", @@ -405,18 +406,17 @@ "# add a note\n", "a_new_note = pt.score.Note(id='n04', step='A', octave=4, voice=1)\n", "part.add(a_new_note, start=append_measure_start, end=append_measure_start+one_beat_in_divs_at_the_end)" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 14, "id": "f9d738a5", "metadata": {}, + "outputs": [], "source": [ "# print(part.pretty())" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -436,21 +436,21 @@ "execution_count": 15, "id": "5d82a340", "metadata": {}, + "outputs": [], "source": [ "path_to_midifile = pt.EXAMPLE_MIDI\n", "performedpart = pt.load_performance_midi(path_to_midifile)[0]" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 16, "id": "4e3090d9", "metadata": {}, + "outputs": [], "source": [ "performedpart.notes" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -465,6 +465,7 @@ "execution_count": 17, "id": "d6eb12f2", "metadata": {}, + "outputs": [], "source": [ "import numpy as np \n", "\n", @@ -491,14 +492,14 @@ " part.add(pt.score.Note(id='n{}'.format(idx), step=step, \n", " octave=int(octave), alter=alter, voice=voice, staff=str((voice-1)%2+1)), \n", " start=start, end=end)" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 18, "id": "572e856c", "metadata": {}, + "outputs": [], "source": [ "l = 200\n", "p = pt.score.Part('CoK', 'Cat on Keyboard', quarter_duration=8)\n", @@ -509,54 +510,53 @@ " np.random.randint(40,60, size=(1,l+1)),\n", " np.random.randint(40,60, size=(1,l+1))\n", " ))" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 19, "id": "f9f03a50", "metadata": {}, + "outputs": [], "source": [ "for k in range(l):\n", " for j in range(4):\n", " addnote(pitch[j,k], p, j+1, ons[j,k], ons[j,k]+dur[j,k+1], \"v\"+str(j)+\"n\"+str(k))" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 20, "id": "09fb6b45", "metadata": {}, + "outputs": [], "source": [ "p.add(pt.score.TimeSignature(4, 4), start=0)\n", "p.add(pt.score.Clef(1, \"G\", line = 3, octave_change=0),start=0)\n", "p.add(pt.score.Clef(2, \"G\", line = 3, octave_change=0),start=0)\n", "pt.score.add_measures(p)\n", "pt.score.tie_notes(p)" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 21, "id": "834582d5", "metadata": {}, + "outputs": [], "source": [ "# pt.save_score_midi(p, \"CatPerformance.mid\", part_voice_assign_mode=2)" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 22, "id": "006f02ed", "metadata": {}, + "outputs": [], "source": [ "# pt.save_musicxml(p, \"CatScore.xml\")" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -602,6 +602,7 @@ "execution_count": 23, "id": "first-basin", "metadata": {}, + "outputs": [], "source": [ "# Note array from a score\n", "\n", @@ -613,8 +614,7 @@ "\n", "# Get note array.\n", "score_note_array = score_part.note_array()" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -629,11 +629,11 @@ "execution_count": 24, "id": "alternate-coordinate", "metadata": {}, + "outputs": [], "source": [ "# Lets see the first notes in this note array\n", "print(score_note_array[:10])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -650,10 +650,10 @@ "execution_count": 25, "id": "subtle-millennium", "metadata": {}, + "outputs": [], "source": [ "print(score_note_array.dtype.names)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -686,6 +686,7 @@ "execution_count": 26, "id": "passing-lending", "metadata": {}, + "outputs": [], "source": [ "# Note array from a performance\n", "\n", @@ -697,8 +698,7 @@ "\n", "# Get note array!\n", "performance_note_array = performance_part.note_array()" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -713,10 +713,10 @@ "execution_count": 27, "id": "pointed-stupid", "metadata": {}, + "outputs": [], "source": [ "print(performance_note_array.dtype.names)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -737,10 +737,10 @@ "execution_count": 28, "id": "subject-reducing", "metadata": {}, + "outputs": [], "source": [ "print(performance_note_array[:5])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -755,6 +755,7 @@ "execution_count": 29, "id": "spread-performer", "metadata": {}, + "outputs": [], "source": [ "note_array = np.array(\n", " [(60, 0, 2, 40),\n", @@ -771,8 +772,7 @@ "\n", "# Note array to `PerformedPart`\n", "performed_part = pt.performance.PerformedPart.from_note_array(note_array)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -787,11 +787,11 @@ "execution_count": 30, "id": "changed-check", "metadata": {}, + "outputs": [], "source": [ "# export as MIDI file\n", "pt.save_performance_midi(performed_part, \"example.mid\")" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -808,6 +808,7 @@ "execution_count": 31, "id": "figured-coordinator", "metadata": {}, + "outputs": [], "source": [ "extended_score_note_array = pt.utils.music.ensure_notearray(\n", " score_part,\n", @@ -817,18 +818,17 @@ " # include_metrical_position=True, # adds 3 fields: is_downbeat, rel_onset_div, tot_measure_div\n", " include_grace_notes=True # adds 2 fields: is_grace, grace_type\n", ")" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 32, "id": "vietnamese-pathology", "metadata": {}, + "outputs": [], "source": [ "extended_score_note_array.dtype.names" - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -837,6 +837,7 @@ "metadata": { "scrolled": true }, + "outputs": [], "source": [ "print(extended_score_note_array[['id', \n", " 'step', \n", @@ -845,8 +846,7 @@ " 'ks_fifths', \n", " 'ks_mode', #'is_downbeat'\n", " ]][:10])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -879,6 +879,7 @@ "metadata": { "scrolled": true }, + "outputs": [], "source": [ "# Path to the MusicXML file\n", "score_fn = os.path.join(MUSICXML_DIR, 'Chopin_op10_no3.musicxml')\n", @@ -914,8 +915,7 @@ "\n", "accented_note_idxs = np.where(accent_note_array['accent'])\n", "print(accent_note_array[accented_note_idxs][:5])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -944,6 +944,7 @@ "execution_count": 35, "id": "essential-academy", "metadata": {}, + "outputs": [], "source": [ "# TODO: change the example\n", "# Path to the MusicXML file\n", @@ -953,8 +954,7 @@ "score_part = pt.load_musicxml(score_fn)\n", "# compute piano roll\n", "pianoroll = pt.utils.compute_pianoroll(score_part)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -969,6 +969,7 @@ "execution_count": 36, "id": "massive-monaco", "metadata": {}, + "outputs": [], "source": [ "piano_range = True\n", "time_unit = 'beat'\n", @@ -979,8 +980,7 @@ " time_div=time_div, # Number of cells per time unit\n", " piano_range=piano_range # Use range of the piano (88 keys)\n", ")" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1007,14 +1007,14 @@ "execution_count": 37, "id": "mature-dylan", "metadata": {}, + "outputs": [], "source": [ "fig, ax = plt.subplots(1, figsize=(20, 10))\n", "ax.imshow(pianoroll.toarray(), origin=\"lower\", cmap='gray', interpolation='nearest', aspect='auto')\n", "ax.set_xlabel(f'Time ({time_unit}s/{time_div})')\n", "ax.set_ylabel('Piano key' if piano_range else 'MIDI pitch')\n", "plt.show()" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1031,13 +1031,13 @@ "metadata": { "scrolled": true }, + "outputs": [], "source": [ "pianoroll, note_indices = pt.utils.compute_pianoroll(score_part, return_idxs=True)\n", "\n", "# MIDI pitch, start, end\n", "print(note_indices[:5])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1055,6 +1055,7 @@ "execution_count": 39, "id": "parental-links", "metadata": {}, + "outputs": [], "source": [ "pianoroll = pt.utils.compute_pianoroll(score_part)\n", "\n", @@ -1064,8 +1065,7 @@ "ppart = pt.performance.PerformedPart.from_note_array(new_note_array)\n", "\n", "pt.save_performance_midi(ppart, \"newmidi.mid\")" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1112,13 +1112,13 @@ "execution_count": 40, "id": "rolled-cloud", "metadata": {}, + "outputs": [], "source": [ "# path to the match\n", "match_fn = os.path.join(MATCH_DIR, 'Chopin_op10_no3_p01.match')\n", "# loading a match file\n", "performed_part, alignment, score_part = pt.load_match(match_fn, create_part=True)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1140,6 +1140,7 @@ "execution_count": 41, "id": "latest-smell", "metadata": {}, + "outputs": [], "source": [ "# path to the match\n", "match_fn = os.path.join(MATCH_DIR, 'Chopin_op10_no3_p01.match')\n", @@ -1150,8 +1151,7 @@ "\n", "# loading a match file\n", "performed_part, alignment = pt.load_match(match_fn)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1175,10 +1175,10 @@ "execution_count": 42, "id": "radio-interim", "metadata": {}, + "outputs": [], "source": [ "alignment[:10]" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1197,6 +1197,7 @@ "execution_count": 43, "id": "published-understanding", "metadata": {}, + "outputs": [], "source": [ "# note array of the score\n", "snote_array = score_part.note_array()\n", @@ -1209,8 +1210,7 @@ "matched_snote_array = snote_array[matched_note_idxs[:, 0]]\n", "# note array of the matched performed notes\n", "matched_pnote_array = pnote_array[matched_note_idxs[:, 1]]" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1227,6 +1227,7 @@ "execution_count": 44, "id": "offshore-bridal", "metadata": {}, + "outputs": [], "source": [ "# get all match files\n", "matchfiles = glob.glob(os.path.join(MATCH_DIR, 'Chopin_op10_no3_p*.match'))\n", @@ -1248,13 +1249,12 @@ " performance, alignment = pt.load_match(matchfile)\n", " ppart = performance[0]\n", " # Get score time to performance time map\n", - " _, stime_to_ptime_map = pt.utils.music.get_time_maps_from_alignment(\n", + " _, stime_to_ptime_map = pt.musicanalysis.performance_codec.get_time_maps_from_alignment(\n", " ppart, score_part, alignment)\n", " # Compute naïve tempo curve\n", " performance_time = stime_to_ptime_map(score_time_ending)\n", " tempo_curves[i,:] = 60 * np.diff(score_time_ending) / np.diff(performance_time)" - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -1263,6 +1263,7 @@ "metadata": { "scrolled": false }, + "outputs": [], "source": [ "fig, ax = plt.subplots(1, figsize=(15, 8))\n", "color = plt.cm.rainbow(np.linspace(0, 1, len(tempo_curves)))\n", @@ -1284,8 +1285,7 @@ "plt.legend(frameon=False, bbox_to_anchor = (1.15, .9))\n", "plt.grid(axis='x')\n", "plt.show()" - ], - "outputs": [] + ] }, { "cell_type": "markdown", From 26d30c5994c09fdb0d65a4c9fcba916e9f269faa Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 28 Aug 2024 15:09:37 +0200 Subject: [PATCH 022/151] Edits to support or remove editorials. --- partitura/io/importkern.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index 1935faf4..4c81f50f 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -69,6 +69,14 @@ "256": {"type": "256th"}, } +class KernElement(object): + def __init__(self, element): + self.editorial_start = True if "ossia" in element else False + self.editorial_end = True if "Xstrophe" in element else False + self.voice_end = True if "*v" in element else False + self.voice_start = True if "*^" in element else False + self.element = element.replace("*", "") + def add_durations(a, b): return a * b / (a + b) @@ -102,8 +110,10 @@ def parse_by_voice(file: list, dtype=np.object_): data[line, voice] = file[line][voice] data = data.T if num_voices > 1: - # Copy global lines from the first voice to all other voices + # Copy global lines from the first voice to all other voices unless they are the string "*S/ossia" cp_idx = np.char.startswith(data[0], "*") + un_idx = np.char.startswith(data[0], "*S/ossia") + cp_idx = np.logical_and(cp_idx, ~un_idx) for i in range(1, num_voices): data[i][cp_idx] = data[0][cp_idx] # Copy Measure Lines from the first voice to all other voices @@ -171,10 +181,16 @@ def element_parsing( ): divs_pq = part._quarter_durations[0] current_tl_pos = 0 + editorial = False measure_mapping = {m.number: m.start.t for m in part.iter_all(spt.Measure)} for i in range(elements.shape[0]): element = elements[i] - if element is None: + if isinstance(element, KernElement): + if element.editorial_start: + editorial = True + if element.editorial_end: + editorial = False + if element is None or editorial: continue if isinstance(element, spt.GenericNote): if total_duration_values[i] == 0: @@ -251,8 +267,8 @@ def load_kern( # Get Splines splines = file[1:].T[note_parts] # Inverse Order - splines = splines[::-1] - parsing_idxs = parsing_idxs[::-1] + # splines = splines[::-1] + # parsing_idxs = parsing_idxs[::-1] prev_staff = 1 has_instrument = np.char.startswith(splines, "*I") # if all parts have the same instrument, then they are the same part. @@ -326,6 +342,7 @@ def load_kern( for part in copy_partlist: part.set_quarter_duration(0, divs_pq) + for part, elements, total_duration_values, same_part in zip( copy_partlist, elements_list, total_durations_list, part_assignments ): @@ -351,7 +368,7 @@ def load_kern( ) doc_name = get_document_name(filename) - score = spt.Score(partlist=partlist, id=doc_name) + score = spt.Score(partlist=partlist[::-1], id=doc_name) return score @@ -503,10 +520,16 @@ def meta_tandem_line(self, line: str): return self.process_key_line(rest) elif line.startswith("*-"): return self.process_fine() + else: + return KernElement(element=line) + def process_tempo_line(self, line: str): return spt.Tempo(float(line)) + def process_ossia_line(self): + return + def process_fine(self): return spt.Fine() From 0a139f2bf59a8846a0ee3cc003c70631ddeb8026 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 28 Aug 2024 15:19:29 +0200 Subject: [PATCH 023/151] Removed unnecessary code. --- partitura/io/importkern.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index 4c81f50f..783f7939 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -527,9 +527,6 @@ def meta_tandem_line(self, line: str): def process_tempo_line(self, line: str): return spt.Tempo(float(line)) - def process_ossia_line(self): - return - def process_fine(self): return spt.Fine() From bedc78de4abe22213963404bd63d49f9f3321803 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 5 Sep 2024 12:15:48 +0200 Subject: [PATCH 024/151] Naive solution for estimate_symbolic_duration Adressed issue #371 --- partitura/utils/music.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 38c60091..9ed17ac0 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -762,8 +762,18 @@ def estimate_symbolic_duration( # 2. The duration is a composite duration # For composite duration. We can use the following approach: j = find_nearest(COMPOSITE_DURS, qdur) - if np.abs(qdur - COMPOSITE_DURS[j]) < eps and return_com_durations: - return copy.copy(SYM_COMPOSITE_DURS[j]) + if np.abs(qdur - COMPOSITE_DURS[j]) < eps: + if return_com_durations: + return copy.copy(SYM_COMPOSITE_DURS[j]) + else: + warnings.warn(f"Quarter duration {qdur} from {dur}/{div} is a composite" + f"duration but composite durations are not allowed. Returning empty symbolic duration.") + return {} + # Naive condition to only apply tuplet estimation if the quarter duration is less than a bar (4) + elif qdur > 4: + warnings.warn(f"Quarter duration {qdur} from {dur}/{div} is not a tuplet or composite duration." + f"Returning empty symbolic duration.") + return {} else: # NOTE: Guess tuplets (Naive) it doesn't cover composite durations from tied notes. type = SYM_DURS[i + 3]["type"] From ffeb58794b39fb3a21e371ee2385dccb0235958d Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 9 Sep 2024 15:51:13 +0200 Subject: [PATCH 025/151] Update for symbolic estimation function to cover most cases from 0-4 quarters. --- partitura/utils/globals.py | 90 +++++++++++++++++++++++++++++++++++++- partitura/utils/music.py | 7 ++- 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/partitura/utils/globals.py b/partitura/utils/globals.py index 3739e0d4..ca42dbb6 100644 --- a/partitura/utils/globals.py +++ b/partitura/utils/globals.py @@ -192,6 +192,24 @@ {"type": "long", "dots": 3}, ] +STRAIGHT_DURS = np.array( + [ 4 / 256, 4 / 128, 4 / 64, 4 / 32, 4 / 16, 4 / 8, 4 / 4, 4 / 2, 4 / 1, 4 / 0.5, 4 / 0.25] +) + +SYM_STRAIGHT_DURS = [ + {"type": "256th", "dots": 0}, + {"type": "128th", "dots": 0}, + {"type": "64th", "dots": 0}, + {"type": "32nd", "dots": 0}, + {"type": "16th", "dots": 0}, + {"type": "eighth", "dots": 0}, + {"type": "quarter", "dots": 0}, + {"type": "half", "dots": 0}, + {"type": "whole", "dots": 0}, + {"type": "breve", "dots": 0}, + {"type": "long", "dots": 0}, +] + MAJOR_KEYS = [ "Cb", "Gb", @@ -281,14 +299,84 @@ # Standard tuning frequency of A4 in Hz A4 = 440.0 -COMPOSITE_DURS = np.array([1 + 4 / 32, 1 + 4 / 16, 2 + 4 / 32, 2 + 4 / 16, 2 + 4 / 8]) +COMPOSITE_DURS = np.array( + [ + 1/4 + 1/6, + 1/2 + 1/12, + 1/2 + 1/3, + 1/2 + 1/4 + 1/6, + 1 + 1/12, + 1 + 1 / 8, + 1 + 1 / 6, + 1 + 1 / 4, + 1 + 1 / 4 + 1 / 6, + 1 + 1 / 2 + 1 / 12, + 1 + 1 / 2 + 1 / 6, + 1 + 1 / 2 + 1 / 3, + 1 + 1 / 2 + 1 / 4 + 1 / 6, + 2 + 1 / 12, + 2 + 1 / 8, + 2 + 1 / 6, + 2 + 1 / 4, + 2 + 1 / 3, + 2 + 1 / 4 + 1 / 6, + 2 + 1 / 2, + 2 + 1 / 2 + 1 / 12, + 2 + 2 / 3, + 2 + 1 / 2 + 1 / 4, + 2 + 1 / 2 + 1 / 3, + 2 + 1 / 2 + 1 / 4 + 1 / 6, + 3 + 1 / 12, + 3 + 1 / 8, + 3 + 1 / 6, + 3 + 1 / 4, + 3 + 1 / 3, + 3 + 1 / 4 + 1 / 6, + 3 + 1 / 2 + 1 / 12, + 3 + 2 / 3, + 3 + 1 / 2 + 1 / 3, + 3 + 1 / 2 + 1 / 4 + 1 / 6, + ] +) SYM_COMPOSITE_DURS = [ + ({"type": "16th", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "eighth", "dots": 0}, {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "eighth", "dots": 0}, {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "eighth", "dots": 1}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "quarter", "dots": 0}, {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}), ({"type": "quarter", "dots": 0}, {"type": "32nd", "dots": 0}), + ({"type": "quarter", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), ({"type": "quarter", "dots": 0}, {"type": "16th", "dots": 0}), + ({"type": "quarter", "dots": 0}, {"type": "16th", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "quarter", "dots": 1}, {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "quarter", "dots": 1}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "quarter", "dots": 1}, {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "quarter", "dots": 2}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "half", "dots": 0}, {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}), ({"type": "half", "dots": 0}, {"type": "32nd", "dots": 0}), + ({"type": "half", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), ({"type": "half", "dots": 0}, {"type": "16th", "dots": 0}), + ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "half", "dots": 0}, {"type": "16th", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "half", "dots": 0}, {"type": "16th", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0}), + ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0}, {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "half", "dots": 0}, {"type": "quarter", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 1}), + ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0}, {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 1}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "half", "dots": 1}, {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "half", "dots": 1}, {"type": "32nd", "dots": 0}), + ({"type": "half", "dots": 1}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "half", "dots": 1}, {"type": "16th", "dots": 0}), + ({"type": "half", "dots": 1}, {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "half", "dots": 1}, {"type": "16th", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "half", "dots": 1}, {"type": "16th", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "half", "dots": 2}, {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "half", "dots": 1}, {"type": "quarter", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "half", "dots": 2}, {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ({"type": "half", "dots": 3}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), ] diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 9ed17ac0..e6afe4ea 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -775,12 +775,15 @@ def estimate_symbolic_duration( f"Returning empty symbolic duration.") return {} else: + i = find_nearest(STRAIGHT_DURS, qdur) # NOTE: Guess tuplets (Naive) it doesn't cover composite durations from tied notes. - type = SYM_DURS[i + 3]["type"] + type = SYM_STRAIGHT_DURS[i+1]["type"] normal_notes = 2 + while (normal_notes * STRAIGHT_DURS[i + 1] / qdur) % 1 > eps: + normal_notes += 1 return { "type": type, - "actual_notes": math.ceil(normal_notes / qdur), + "actual_notes": math.ceil(normal_notes * STRAIGHT_DURS[i+1] / qdur), "normal_notes": normal_notes, } From 0e7794762822412430362fad88dc9461316ab74f Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 9 Sep 2024 15:51:34 +0200 Subject: [PATCH 026/151] Test for symbolic duration estimator. --- tests/test_utils.py | 65 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 87e95bdd..1d558f4a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -643,3 +643,68 @@ def test_tokenize2(self): pt_tokens = [tok for tok in pt_tokens if not tok.startswith("Velocity")] mtok_tokens = [tok for tok in mtok_tokens if not tok.startswith("Velocity")] self.assertTrue(pt_tokens == mtok_tokens) + + + class TestSymbolicDurationEstimator(unittest.TestCase): + def test_estimate_symbolic_duration(self): + """ + Test `estimate_symbolic_duration` + """ + divs = 12 + quarters = 4 + expected = [ + {}, + {'type': '32nd', 'actual_notes': 3, 'normal_notes': 2}, + {'type': '16th', 'actual_notes': 3, 'normal_notes': 2}, + {'type': '16th', 'dots': 0}, + {'type': 'eighth', 'actual_notes': 3, 'normal_notes': 2}, + ({'type': '16th', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + {'type': 'eighth', 'dots': 0}, + ({'type': 'eighth', 'dots': 0}, {'type': '32nd', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + {'type': 'quarter', 'actual_notes': 3, 'normal_notes': 2}, + {'type': 'eighth', 'dots': 1}, + ({'type': 'eighth', 'dots': 0}, {'type': 'eighth', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'eighth', 'dots': 1}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + {'type': 'quarter', 'dots': 0}, + ({'type': 'quarter', 'dots': 0}, {'type': '32nd', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'quarter', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'quarter', 'dots': 0}, {'type': '16th', 'dots': 0}), + {'type': 'half', 'actual_notes': 3, 'normal_notes': 2}, + ({'type': 'quarter', 'dots': 0}, {'type': '16th', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + {'type': 'quarter', 'dots': 1}, + ({'type': 'quarter', 'dots': 1}, {'type': '32nd', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'quarter', 'dots': 1}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + {'type': 'quarter', 'dots': 2}, + ({'type': 'quarter', 'dots': 1}, {'type': 'eighth', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'quarter', 'dots': 2}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + {'type': 'half', 'dots': 0}, + ({'type': 'half', 'dots': 0}, {'type': '32nd', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'half', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'half', 'dots': 0}, {'type': '16th', 'dots': 0}), + ({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'half', 'dots': 0}, {'type': '16th', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'half', 'dots': 0}, {'type': '16th', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 0}), + ({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 0}, {'type': '32nd', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'half', 'dots': 0}, {'type': 'quarter', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 1}), + ({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 0}, {'type': 'eighth', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + {'type': 'half', 'dots': 1}, + ({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 1}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'half', 'dots': 1}, {'type': '32nd', 'dots': 0}), + ({'type': 'half', 'dots': 1}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'half', 'dots': 1}, {'type': '16th', 'dots': 0}), + ({'type': 'half', 'dots': 1}, {'type': 'eighth', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + {'type': 'half', 'dots': 2}, + ({'type': 'half', 'dots': 1}, {'type': '16th', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'half', 'dots': 1}, {'type': '16th', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + {'type': 'half', 'dots': 3}, + ({'type': 'half', 'dots': 2}, {'type': '32nd', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'half', 'dots': 1}, {'type': 'quarter', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ] + predicted = [] + for k in range(divs * 0, divs * quarters): + a = partitura.utils.estimate_symbolic_duration( + k, divs, return_com_durations=True) + predicted.append(a) + self.assertTrue(all([expected[i] == predicted[i] for i in range(len(expected))])) \ No newline at end of file From d401dcb65295c151a2652ab45618826c05e864d4 Mon Sep 17 00:00:00 2001 From: fosfrancesco Date: Mon, 16 Sep 2024 12:37:04 +0200 Subject: [PATCH 027/151] estimate_voice_info now default to false --- partitura/io/importmidi.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/partitura/io/importmidi.py b/partitura/io/importmidi.py index 96ee2756..6bfcfe05 100644 --- a/partitura/io/importmidi.py +++ b/partitura/io/importmidi.py @@ -306,7 +306,7 @@ def load_score_midi( filename: Union[PathLike, mido.MidiFile], part_voice_assign_mode: Optional[int] = 0, quantization_unit: Optional[int] = None, - estimate_voice_info: bool = True, + estimate_voice_info: bool = False, estimate_key: bool = False, assign_note_ids: bool = True, ) -> score.Score: @@ -363,10 +363,8 @@ def load_score_midi( the MIDI file. estimate_voice_info : bool, optional When True use Chew and Wu's voice separation algorithm [2]_ to - estimate voice information. This option is ignored for - part/voice assignment modes that infer voice information from - the track/channel info (i.e. `part_voice_assign_mode` equals - 1, 3, 4, or 5). Defaults to True. + estimate voice information. If the voice information was imported + from the file, it will be overridden. Defaults to False. Returns ------- @@ -524,6 +522,8 @@ def load_score_midi( warnings.warn("pitch spelling") spelling_global = analysis.estimate_spelling(note_array) + + if estimate_voice_info: warnings.warn("voice estimation", stacklevel=2) # TODO: deal with zero duration notes in note_array. From ed0e8bb38b009be794170586ea1613e4179e5d65 Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 23 Sep 2024 12:28:47 +0200 Subject: [PATCH 028/151] int casting --- partitura/io/exportmusicxml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/exportmusicxml.py b/partitura/io/exportmusicxml.py index 168fd430..aafff721 100644 --- a/partitura/io/exportmusicxml.py +++ b/partitura/io/exportmusicxml.py @@ -658,7 +658,7 @@ def merge_measure_contents(notes, other, measure_start): elif gap > 0: e = etree.Element("forward") ee = etree.SubElement(e, "duration") - ee.text = "{:d}".format(gap) + ee.text = "{:d}".format(int(gap)) result.append(e) result.extend([e for _, _, e in elements]) From 35d56a77e2f1124443db2f1e709f90a1ee733e70 Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 23 Sep 2024 18:41:08 +0200 Subject: [PATCH 029/151] update docstring load_match --- partitura/io/importmatch.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index bb1f061a..253c1bc0 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -244,6 +244,12 @@ def load_match( first_note_at_zero : bool, optional When True the note_on and note_off times in the performance are shifted to make the first note_on time equal zero. + Defaults to False. + offset_duration_whole: Boolean, optional + A flag for the type of offset and duration given in the matchfile. + When true, the function expects the values to be given in whole + notes (e.g. 1/4 for a quarter note) independet of time signature. + Defaults to True. Returns ------- From 699738e62fc84e367c8431413d6addd3f06b3bc5 Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 23 Sep 2024 18:45:13 +0200 Subject: [PATCH 030/151] remove unused match line base class imports --- partitura/io/importmatch.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index 253c1bc0..b049dd42 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -16,37 +16,13 @@ from partitura.io.matchlines_v0 import ( FROM_MATCHLINE_METHODS as FROM_MATCHLINE_METHODSV0, - parse_matchline as parse_matchlinev0, MatchInfo as MatchInfoV0, - MatchMeta as MatchMetaV0, - MatchSnote as MatchSnoteV0, - MatchNote as MatchNoteV0, - MatchSnoteNote as MatchSnoteNoteV0, - MatchSnoteDeletion as MatchSnoteDeletionV0, - MatchSnoteTrailingScore as MatchSnoteTrailingScoreV0, - MatchInsertionNote as MatchInsertionNoteV0, - MatchHammerBounceNote as MatchHammerBounceNoteV0, - MatchTrailingPlayedNote as MatchTrailingPlayedNoteV0, - MatchSustainPedal as MatchSustainPedalV0, - MatchSoftPedal as MatchSoftPedalV0, MatchTrillNote as MatchTrillNoteV0, ) from partitura.io.matchlines_v1 import ( FROM_MATCHLINE_METHODS as FROM_MATCHLINE_METHODSV1, MatchInfo as MatchInfoV1, - MatchScoreProp as MatchScorePropV1, - MatchSection as MatchSectionV1, - MatchStime as MatchStimeV1, - MatchPtime as MatchPtimeV1, - MatchStimePtime as MatchStimePtimeV1, - MatchSnote as MatchSnoteV1, - MatchNote as MatchNoteV1, - MatchSnoteNote as MatchSnoteNoteV1, - MatchSnoteDeletion as MatchSnoteDeletionV1, - MatchInsertionNote as MatchInsertionNoteV1, - MatchSustainPedal as MatchSustainPedalV1, - MatchSoftPedal as MatchSoftPedalV1, MatchOrnamentNote as MatchOrnamentNoteV1, ) From ac9b64a6f39b64d65e304543ec33f7a2dc731f4c Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 23 Sep 2024 18:46:52 +0200 Subject: [PATCH 031/151] removed more unused imports --- partitura/io/importmatch.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index b049dd42..025a5052 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -12,7 +12,6 @@ from partitura import score from partitura.score import Part, Score from partitura.performance import PerformedPart, Performance -from partitura.musicanalysis import estimate_voices, estimate_key from partitura.io.matchlines_v0 import ( FROM_MATCHLINE_METHODS as FROM_MATCHLINE_METHODSV0, @@ -32,12 +31,9 @@ MatchLine, BaseSnoteLine, BaseSnoteNoteLine, - BaseStimePtimeLine, BaseDeletionLine, BaseInsertionLine, BaseOrnamentLine, - BaseSustainPedalLine, - BaseSoftPedalLine, ) from partitura.io.matchfile_utils import ( @@ -45,29 +41,24 @@ number_pattern, vnumber_pattern, MatchTimeSignature, - MatchKeySignature, format_pnote_id, ) from partitura.utils.music import ( - midi_ticks_to_seconds, - pitch_spelling_to_midi_pitch, - ensure_pitch_spelling_format, - key_name_to_fifths_mode, - estimate_clef_properties, - note_array_from_note_list, + midi_ticks_to_seconds ) from partitura.utils.misc import ( deprecated_alias, - deprecated_parameter, PathLike, - get_document_name, + get_document_name ) -from partitura.utils.generic import interp1d, partition, iter_current_next - +from partitura.utils.generic import ( + interp1d, + iter_current_next +) __all__ = ["load_match"] From 98e90866706161277a2f767e79f8771b2f8c5976 Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 23 Sep 2024 18:51:52 +0200 Subject: [PATCH 032/151] fix typing --- partitura/io/importmatch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index 025a5052..c9fadf15 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -753,7 +753,7 @@ def part_from_matchfile( def make_timesig_maps( ts_orig: List[Tuple[float, int, MatchTimeSignature]], max_time: float, -) -> (Callable, Callable, Callable, Callable, float, float): +) -> tuple[Callable, Callable, Callable, Callable, float, float]: """ Create time signature (interpolation) maps @@ -833,7 +833,7 @@ def add_staffs(part: Part, split: int = 55, only_missing: bool = True) -> None: MIDI pitch to split staff into upper and lower. Default is 55 only_missing: bool If True, only add staff to those notes that do not have staff info already. - x""" + """ # assign staffs using a hard limit notes = part.notes_tied for n in notes: From fb64f4f155b384bc53b0b7eebdc81e73e27d5b60 Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 23 Sep 2024 20:18:18 +0200 Subject: [PATCH 033/151] refactor bar lines --- partitura/io/importmatch.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index c9fadf15..bc7058b6 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -524,16 +524,16 @@ def part_from_matchfile( offset = 0 t = t - t % beats_map(min_time) - for b0, b1 in iter_current_next(bars, end=bars[-1] + 1): - bar_times.setdefault(b0, t) - if t < 0: - t = 0 - - else: - # multiply by diff between consecutive bar numbers - n_bars = b1 - b0 - if t <= max_time_q: - t += (n_bars * 4 * beats_map(t)) / beat_type_map(t) + # for b0, b1 in iter_current_next(bars, end=bars[-1] + 1): + # bar_times.setdefault(b0, t) + # if t < 0: + # t = 0 + + # else: + # # multiply by diff between consecutive bar numbers + # n_bars = b1 - b0 + # if t <= max_time_q: + # t += (n_bars * 4 * beats_map(t)) / beat_type_map(t) for ni, note in enumerate(snotes): # start of bar in quarter units From bb363d89933fa6678bc70115aab492ea1b8b33c6 Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 23 Sep 2024 20:26:40 +0200 Subject: [PATCH 034/151] compute bar times wip --- partitura/io/importmatch.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index bc7058b6..b2ccf240 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -463,7 +463,7 @@ def part_from_matchfile( ts = mf.time_signatures min_time = snotes[0].OnsetInBeats # sorted by OnsetInBeats max_time = max(n.OffsetInBeats for n in snotes) - _, beats_map, _, beat_type_map, min_time_q, max_time_q = make_timesig_maps( + beats_map_from_beats, beats_map, beat_type_map_from_beats, beat_type_map, min_time_q, max_time_q = make_timesig_maps( ts, max_time ) @@ -524,6 +524,16 @@ def part_from_matchfile( offset = 0 t = t - t % beats_map(min_time) + for b_name in bars: + a_note_in_this_bar = [n for n in snotes if n.Measure == b_name][0] + bar_offset = (a_note_in_this_bar.Beat - 1) * 4 / beat_type_map_from_beats(a_note_in_this_bar.OnsetInBeats) + beat_offset = ( + 4 + / on_off_scale + * a_note_in_this_bar.Offset.numerator + / (a_note_in_this_bar.Offset.denominator * (a_note_in_this_bar.Offset.tuple_div or 1)) + ) + # for b0, b1 in iter_current_next(bars, end=bars[-1] + 1): # bar_times.setdefault(b0, t) # if t < 0: From 125e1d244dc545b817776111729e2f080054ca3b Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 23 Sep 2024 20:31:47 +0200 Subject: [PATCH 035/151] barline in quarters wip --- partitura/io/importmatch.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index b2ccf240..5767ea81 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -525,7 +525,9 @@ def part_from_matchfile( t = t - t % beats_map(min_time) for b_name in bars: - a_note_in_this_bar = [n for n in snotes if n.Measure == b_name][0] + notes_in_this_bar = [(ni, n) for ni, n in enumerate(snotes) if n.Measure == b_name] + a_note_in_this_bar = notes_in_this_bar[0][1] + a_note_id_in_this_bar = notes_in_this_bar[0][0] bar_offset = (a_note_in_this_bar.Beat - 1) * 4 / beat_type_map_from_beats(a_note_in_this_bar.OnsetInBeats) beat_offset = ( 4 @@ -534,6 +536,9 @@ def part_from_matchfile( / (a_note_in_this_bar.Offset.denominator * (a_note_in_this_bar.Offset.tuple_div or 1)) ) + barline_in_quarters = onset_in_quarters[a_note_id_in_this_bar] - bar_offset - beat_offset + + # for b0, b1 in iter_current_next(bars, end=bars[-1] + 1): # bar_times.setdefault(b0, t) # if t < 0: From b68d1202284cde6645afe0236df569bccf2d7ca0 Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 23 Sep 2024 20:38:56 +0200 Subject: [PATCH 036/151] on_off_scale debug --- partitura/io/importmatch.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index 5767ea81..ecfb8965 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -511,6 +511,7 @@ def part_from_matchfile( t = t * 4 / beat_type_map(min_time) offset = t bar_times = {} + bar_times_original = {} if t > 0: # if we have an incomplete first measure that isn't an anacrusis @@ -529,6 +530,9 @@ def part_from_matchfile( a_note_in_this_bar = notes_in_this_bar[0][1] a_note_id_in_this_bar = notes_in_this_bar[0][0] bar_offset = (a_note_in_this_bar.Beat - 1) * 4 / beat_type_map_from_beats(a_note_in_this_bar.OnsetInBeats) + on_off_scale = 1 + if not match_offset_duration_in_whole: + on_off_scale = beat_type_map_from_beats(a_note_in_this_bar.OnsetInBeats) beat_offset = ( 4 / on_off_scale @@ -537,18 +541,21 @@ def part_from_matchfile( ) barline_in_quarters = onset_in_quarters[a_note_id_in_this_bar] - bar_offset - beat_offset + bar_times[b_name] = barline_in_quarters + for b0, b1 in iter_current_next(bars, end=bars[-1] + 1): + bar_times_original.setdefault(b0, t) + if t < 0: + t = 0 - # for b0, b1 in iter_current_next(bars, end=bars[-1] + 1): - # bar_times.setdefault(b0, t) - # if t < 0: - # t = 0 + else: + # multiply by diff between consecutive bar numbers + n_bars = b1 - b0 + if t <= max_time_q: + t += (n_bars * 4 * beats_map(t)) / beat_type_map(t) - # else: - # # multiply by diff between consecutive bar numbers - # n_bars = b1 - b0 - # if t <= max_time_q: - # t += (n_bars * 4 * beats_map(t)) / beat_type_map(t) + for key in bar_times_original.keys(): + print(bar_times_original[key], bar_times[key]) for ni, note in enumerate(snotes): # start of bar in quarter units From c24062f1b7f5b087492f7cb3007e9d6338ed7b95 Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 23 Sep 2024 21:02:06 +0200 Subject: [PATCH 037/151] add measures --- partitura/io/importmatch.py | 45 ++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index ecfb8965..70b753eb 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -511,7 +511,7 @@ def part_from_matchfile( t = t * 4 / beat_type_map(min_time) offset = t bar_times = {} - bar_times_original = {} + # bar_times_original = {} if t > 0: # if we have an incomplete first measure that isn't an anacrusis @@ -543,19 +543,19 @@ def part_from_matchfile( barline_in_quarters = onset_in_quarters[a_note_id_in_this_bar] - bar_offset - beat_offset bar_times[b_name] = barline_in_quarters - for b0, b1 in iter_current_next(bars, end=bars[-1] + 1): - bar_times_original.setdefault(b0, t) - if t < 0: - t = 0 + # for b0, b1 in iter_current_next(bars, end=bars[-1] + 1): + # bar_times_original.setdefault(b0, t) + # if t < 0: + # t = 0 - else: - # multiply by diff between consecutive bar numbers - n_bars = b1 - b0 - if t <= max_time_q: - t += (n_bars * 4 * beats_map(t)) / beat_type_map(t) + # else: + # # multiply by diff between consecutive bar numbers + # n_bars = b1 - b0 + # if t <= max_time_q: + # t += (n_bars * 4 * beats_map(t)) / beat_type_map(t) - for key in bar_times_original.keys(): - print(bar_times_original[key], bar_times[key]) + # for key in bar_times_original.keys(): + # print(key, bar_times_original[key], bar_times[key]) for ni, note in enumerate(snotes): # start of bar in quarter units @@ -749,12 +749,25 @@ def part_from_matchfile( add_staffs(part) # add_clefs(part) + prev_measure = None + for measure_counter, measure_name in enumerate(bar_times.keys()): + barline_in_quarters = bar_times[measure_name] + barline_in_divs = int(round(divs * (barline_in_quarters - offset))) + if barline_in_divs < 0: + barline_in_divs = 0 + if prev_measure is not None: + part.add(prev_measure, None, barline_in_divs) + prev_measure = score.Measure(number=measure_counter, name = str(measure_name)) + part.add(prev_measure, barline_in_divs) + last_closing_barline = barline_in_divs + int(round(divs * beats_map(barline_in_quarters) * 4 / beat_type_map(barline_in_quarters))) + part.add(prev_measure, None, last_closing_barline) + # add incomplete measure if necessary - if offset < 0: - part.add(score.Measure(number=0), 0, int(-offset * divs)) + # if offset < 0: + # part.add(score.Measure(number=0), 0, int(-offset * divs)) - # add the rest of the measures automatically - score.add_measures(part) + # # add the rest of the measures automatically + # score.add_measures(part) score.tie_notes(part) score.find_tuplets(part) From 4eafa93511825bfaf2691017db305496fe0d1f52 Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 23 Sep 2024 21:06:26 +0200 Subject: [PATCH 038/151] cleanup --- partitura/io/importmatch.py | 37 ++----------------------------------- 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index 70b753eb..6faa5b32 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -484,7 +484,6 @@ def part_from_matchfile( onset_in_beats = np.array([note.OnsetInBeats for note in snotes]) unique_onsets, inv_idxs = np.unique(onset_in_beats, return_inverse=True) - # unique_onset_idxs = [np.where(onset_in_beats == u) for u in unique_onsets] iois_in_beats = np.diff(unique_onsets) beat_to_quarter = 4 / beat_type_map(onset_in_beats) @@ -501,23 +500,16 @@ def part_from_matchfile( onset_in_divs = np.r_[0, np.cumsum(divs * iois_in_quarters)][inv_idxs] onset_in_quarters = onset_in_quarters[inv_idxs] - # duration_in_beats = np.array([note.DurationInBeats for note in snotes]) - # duration_in_quarters = duration_in_beats * beat_to_quarter - # duration_in_divs = duration_in_quarters * divs - part.set_quarter_duration(0, divs) bars = np.unique([n.Measure for n in snotes]) t = min_time t = t * 4 / beat_type_map(min_time) offset = t bar_times = {} - # bar_times_original = {} if t > 0: # if we have an incomplete first measure that isn't an anacrusis # measure, add a rest (dummy) - # t = t-t%beats_map(min_time) - # if starting beat is above zero, add padding rest = score.Rest() part.add(rest, start=0, end=t * divs) @@ -543,19 +535,6 @@ def part_from_matchfile( barline_in_quarters = onset_in_quarters[a_note_id_in_this_bar] - bar_offset - beat_offset bar_times[b_name] = barline_in_quarters - # for b0, b1 in iter_current_next(bars, end=bars[-1] + 1): - # bar_times_original.setdefault(b0, t) - # if t < 0: - # t = 0 - - # else: - # # multiply by diff between consecutive bar numbers - # n_bars = b1 - b0 - # if t <= max_time_q: - # t += (n_bars * 4 * beats_map(t)) / beat_type_map(t) - - # for key in bar_times_original.keys(): - # print(key, bar_times_original[key], bar_times[key]) for ni, note in enumerate(snotes): # start of bar in quarter units @@ -580,15 +559,6 @@ def part_from_matchfile( / (note.Offset.denominator * (note.Offset.tuple_div or 1)) ) - # check anacrusis measure beat counting type for the first note - if bar_start < 0 and (bar_offset != 0 or beat_offset != 0) and ni == 0: - # in case of fully counted anacrusis we set the bar_start - # to -bar_duration (in quarters) so that the below calculation is correct - # not active for shortened anacrusis measures - bar_start = -beats_map(bar_start) * 4 / beat_type_map(bar_start) - # reset the bar_start for other notes in the anacrusis measure - bar_times[note.Bar] = bar_start - # convert the onset time in quarters (0 at first barline) to onset # time in divs (0 at first note) onset_divs = int(round(divs * (bar_start + bar_offset + beat_offset - offset))) @@ -762,12 +732,9 @@ def part_from_matchfile( last_closing_barline = barline_in_divs + int(round(divs * beats_map(barline_in_quarters) * 4 / beat_type_map(barline_in_quarters))) part.add(prev_measure, None, last_closing_barline) - # add incomplete measure if necessary - # if offset < 0: - # part.add(score.Measure(number=0), 0, int(-offset * divs)) - # # add the rest of the measures automatically - # score.add_measures(part) + # add the rest of the measures automatically + score.add_measures(part) score.tie_notes(part) score.find_tuplets(part) From 4ed8241038450fb946242197bf4f35b15ccc9b7a Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 23 Sep 2024 21:12:49 +0200 Subject: [PATCH 039/151] measure number starts at 1 --- partitura/io/importmatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index 6faa5b32..73f96337 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -727,7 +727,7 @@ def part_from_matchfile( barline_in_divs = 0 if prev_measure is not None: part.add(prev_measure, None, barline_in_divs) - prev_measure = score.Measure(number=measure_counter, name = str(measure_name)) + prev_measure = score.Measure(number=measure_counter + 1, name = str(measure_name)) part.add(prev_measure, barline_in_divs) last_closing_barline = barline_in_divs + int(round(divs * beats_map(barline_in_quarters) * 4 / beat_type_map(barline_in_quarters))) part.add(prev_measure, None, last_closing_barline) From 7c4a6958b14912ae1685101dabc44842a3c8b3c7 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 24 Sep 2024 06:35:04 +0200 Subject: [PATCH 040/151] remove anacrusis measure conditional --- partitura/score.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index e882151a..a08066a1 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -3769,11 +3769,8 @@ def add_measures(part): if existing_measure.start.t == measure_start: assert existing_measure.end.t > pos pos = existing_measure.end.t - if existing_measure.number != 0: - # if existing_measure is a match anacrusis measure, - # keep number 0 - existing_measure.number = mcounter - mcounter += 1 + existing_measure.number = mcounter + mcounter += 1 continue else: From f8120dc1d8b45d553c0b116cfe966b26817da6b9 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 24 Sep 2024 06:40:25 +0200 Subject: [PATCH 041/151] note_array_to_score measure number and name --- partitura/musicanalysis/note_array_to_score.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/musicanalysis/note_array_to_score.py b/partitura/musicanalysis/note_array_to_score.py index a1dc19a3..370bfeec 100644 --- a/partitura/musicanalysis/note_array_to_score.py +++ b/partitura/musicanalysis/note_array_to_score.py @@ -190,7 +190,7 @@ def create_part( warnings.warn("add measures", stacklevel=2) if not barebones and anacrusis_divs > 0: - part.add(score.Measure(0), 0, anacrusis_divs) + part.add(score.Measure(number = 1, name = str(0)), 0, anacrusis_divs) if not barebones and sanitize: warnings.warn("Inferring measures", stacklevel=2) From 0eee6a8d1ee908e3928f39adb6f2b10c107e472e Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 24 Sep 2024 06:46:24 +0200 Subject: [PATCH 042/151] measure export musicxml --- partitura/io/exportmusicxml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/partitura/io/exportmusicxml.py b/partitura/io/exportmusicxml.py index aafff721..0d93fcc9 100644 --- a/partitura/io/exportmusicxml.py +++ b/partitura/io/exportmusicxml.py @@ -1053,8 +1053,8 @@ def handle_parents(part): part_e.append(etree.Comment(MEASURE_SEP_COMMENT)) attrib = {} - if measure.number is not None: - attrib["number"] = str(measure.number) + if measure.name is not None: + attrib["number"] = str(measure.name) measure_e = etree.SubElement(part_e, "measure", **attrib) contents = linearize_measure_contents( From f69b9831076b2f5674c4780330b7e8dcffa0258d Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 24 Sep 2024 06:58:45 +0200 Subject: [PATCH 043/151] reformat symbolic duration in match test file --- tests/data/match/test_fuer_elise.match | 90 +++++++++++++------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/tests/data/match/test_fuer_elise.match b/tests/data/match/test_fuer_elise.match index 4a1aea34..511b2338 100644 --- a/tests/data/match/test_fuer_elise.match +++ b/tests/data/match/test_fuer_elise.match @@ -844,70 +844,70 @@ snote(828,[B,n],4,99:1,0,1/8,291.0,292.0,[voice1,staff1,s])-note(829,[B,n],4,117 snote(829,[E,n],4,99:1,0,1/8,291.0,292.0,[voice2,staff1])-note(831,[E,n],4,117548,117658,118466,24). snote(830,[E,n],3,99:1,0,1/8,291.0,292.0,[voice5,staff2])-note(828,[E,n],3,117496,117777,118466,51). snote(831,[G,#],3,99:1,0,1/8,291.0,292.0,[voice5,staff2])-note(830,[G,#],3,117546,117668,118466,48). -snote(832,[A,n],3,100:1,0,11184811/268435456,294.0,294.3333,[voice1,staff1,s])-note(832,[A,n],3,118744,118860,119820,54). +snote(832,[A,n],3,100:1,0,1/24,294.0,294.3333,[voice1,staff1,s])-note(832,[A,n],3,118744,118860,119820,54). snote(833,[A,n],1,100:1,0,1/8,294.0,295.0,[voice5,staff2])-note(833,[A,n],1,118776,118820,119820,60). -snote(834,[C,n],4,100:1,11184811/268435456,11184811/268435456,294.3333,294.6667,[voice1,staff1,s])-note(834,[C,n],4,118876,118951,119820,59). -snote(835,[E,n],4,100:1,11184811/134217728,11184811/268435456,294.6667,295.0,[voice1,staff1,s])-note(835,[E,n],4,118974,119018,119820,54). -snote(836,[A,n],4,100:2,0,11184811/268435456,295.0,295.3333,[voice1,staff1,s])-note(836,[A,n],4,119152,119227,119820,37). -snote(837,[C,n],5,100:2,11184811/268435456,11184811/268435456,295.3333,295.6667,[voice1,staff1,s])-note(837,[C,n],5,119245,119367,119820,59). -snote(838,[E,n],5,100:2,11184811/134217728,11184811/268435456,295.6667,296.0,[voice1,staff1,s])-note(838,[E,n],5,119371,119536,119820,67). -snote(839,[D,n],5,100:3,0,11184811/268435456,296.0,296.3333,[voice1,staff1,s])-note(839,[D,n],5,119511,119643,119820,67). +snote(834,[C,n],4,100:1,1/24,1/24,294.3333,294.6667,[voice1,staff1,s])-note(834,[C,n],4,118876,118951,119820,59). +snote(835,[E,n],4,100:1,1/12,1/24,294.6667,295.0,[voice1,staff1,s])-note(835,[E,n],4,118974,119018,119820,54). +snote(836,[A,n],4,100:2,0,1/24,295.0,295.3333,[voice1,staff1,s])-note(836,[A,n],4,119152,119227,119820,37). +snote(837,[C,n],5,100:2,1/24,1/24,295.3333,295.6667,[voice1,staff1,s])-note(837,[C,n],5,119245,119367,119820,59). +snote(838,[E,n],5,100:2,1/12,1/24,295.6667,296.0,[voice1,staff1,s])-note(838,[E,n],5,119371,119536,119820,67). +snote(839,[D,n],5,100:3,0,1/24,296.0,296.3333,[voice1,staff1,s])-note(839,[D,n],5,119511,119643,119820,67). snote(840,[A,n],3,100:3,0,1/8,296.0,297.0,[voice5,staff2])-note(841,[A,n],3,119541,119596,119820,63). snote(841,[C,n],4,100:3,0,1/8,296.0,297.0,[voice5,staff2])-note(840,[C,n],4,119534,119609,119820,59). snote(842,[E,n],4,100:3,0,1/8,296.0,297.0,[voice5,staff2])-note(842,[E,n],4,119601,119642,119820,26). -snote(843,[C,n],5,100:3,11184811/268435456,11184811/268435456,296.3333,296.6667,[voice1,staff1,s])-note(843,[C,n],5,119606,119732,119820,64). -snote(844,[B,n],4,100:3,11184811/134217728,11184811/268435456,296.6667,297.0,[voice1,staff1,s])-note(844,[B,n],4,119713,119752,119820,64). -snote(845,[A,n],4,101:1,0,11184811/268435456,297.0,297.3333,[voice1,staff1,s])-note(845,[A,n],4,119862,119975,120886,63). +snote(843,[C,n],5,100:3,1/24,1/24,296.3333,296.6667,[voice1,staff1,s])-note(843,[C,n],5,119606,119732,119820,64). +snote(844,[B,n],4,100:3,1/12,1/24,296.6667,297.0,[voice1,staff1,s])-note(844,[B,n],4,119713,119752,119820,64). +snote(845,[A,n],4,101:1,0,1/24,297.0,297.3333,[voice1,staff1,s])-note(845,[A,n],4,119862,119975,120886,63). snote(846,[A,n],3,101:1,0,1/8,297.0,298.0,[voice5,staff2])-note(846,[A,n],3,119899,120012,120886,59). snote(847,[C,n],4,101:1,0,1/8,297.0,298.0,[voice5,staff2])-note(847,[C,n],4,119909,120022,120886,59). snote(848,[E,n],4,101:1,0,1/8,297.0,298.0,[voice5,staff2])-note(848,[E,n],4,119914,119999,120886,55). -snote(849,[C,n],5,101:1,11184811/268435456,11184811/268435456,297.3333,297.6667,[voice1,staff1,s])-note(849,[C,n],5,119971,120089,120886,69). -snote(850,[E,n],5,101:1,11184811/134217728,11184811/268435456,297.6667,298.0,[voice1,staff1,s])-note(850,[E,n],5,120103,120147,120886,54). -snote(851,[A,n],5,101:2,0,11184811/268435456,298.0,298.3333,[voice1,staff1,s])-note(851,[A,n],5,120259,120320,120886,52). -snote(852,[C,n],6,101:2,11184811/268435456,11184811/268435456,298.3333,298.6667,[voice1,staff1,s])-note(852,[C,n],6,120341,120450,120886,61). -snote(853,[E,n],6,101:2,11184811/134217728,11184811/268435456,298.6667,299.0,[voice1,staff1,s])-note(853,[E,n],6,120460,120629,120886,66). -snote(854,[D,n],6,101:3,0,11184811/268435456,299.0,299.3333,[voice1,staff1,s])-note(854,[D,n],6,120585,120721,120886,63). +snote(849,[C,n],5,101:1,1/24,1/24,297.3333,297.6667,[voice1,staff1,s])-note(849,[C,n],5,119971,120089,120886,69). +snote(850,[E,n],5,101:1,1/12,1/24,297.6667,298.0,[voice1,staff1,s])-note(850,[E,n],5,120103,120147,120886,54). +snote(851,[A,n],5,101:2,0,1/24,298.0,298.3333,[voice1,staff1,s])-note(851,[A,n],5,120259,120320,120886,52). +snote(852,[C,n],6,101:2,1/24,1/24,298.3333,298.6667,[voice1,staff1,s])-note(852,[C,n],6,120341,120450,120886,61). +snote(853,[E,n],6,101:2,1/12,1/24,298.6667,299.0,[voice1,staff1,s])-note(853,[E,n],6,120460,120629,120886,66). +snote(854,[D,n],6,101:3,0,1/24,299.0,299.3333,[voice1,staff1,s])-note(854,[D,n],6,120585,120721,120886,63). snote(855,[A,n],3,101:3,0,1/8,299.0,300.0,[voice5,staff2])-note(855,[A,n],3,120636,120695,120886,62). snote(856,[C,n],4,101:3,0,1/8,299.0,300.0,[voice5,staff2])-note(856,[C,n],4,120639,120692,120886,60). snote(857,[E,n],4,101:3,0,1/8,299.0,300.0,[voice5,staff2])-note(857,[E,n],4,120649,120696,120886,56). -snote(858,[C,n],6,101:3,11184811/268435456,11184811/268435456,299.3333,299.6667,[voice1,staff1,s])-note(858,[C,n],6,120665,120815,120886,59). -snote(859,[B,n],5,101:3,11184811/134217728,11184811/268435456,299.6667,300.0,[voice1,staff1,s])-note(859,[B,n],5,120763,120817,120886,67). -snote(860,[A,n],5,102:1,0,11184811/268435456,300.0,300.3333,[voice1,staff1,s])-note(860,[A,n],5,120919,121042,122009,73). +snote(858,[C,n],6,101:3,1/24,1/24,299.3333,299.6667,[voice1,staff1,s])-note(858,[C,n],6,120665,120815,120886,59). +snote(859,[B,n],5,101:3,1/12,1/24,299.6667,300.0,[voice1,staff1,s])-note(859,[B,n],5,120763,120817,120886,67). +snote(860,[A,n],5,102:1,0,1/24,300.0,300.3333,[voice1,staff1,s])-note(860,[A,n],5,120919,121042,122009,73). snote(861,[A,n],3,102:1,0,1/8,300.0,301.0,[voice5,staff2])-note(861,[A,n],3,120955,121092,122009,69). snote(862,[C,n],4,102:1,0,1/8,300.0,301.0,[voice5,staff2])-note(862,[C,n],4,120958,121087,122009,63). snote(863,[E,n],4,102:1,0,1/8,300.0,301.0,[voice5,staff2])-note(863,[E,n],4,120967,121042,122009,54). -snote(864,[C,n],6,102:1,11184811/268435456,11184811/268435456,300.3333,300.6667,[voice1,staff1,s])-note(864,[C,n],6,121030,121136,122009,69). -snote(865,[E,n],6,102:1,11184811/134217728,11184811/268435456,300.6667,301.0,[voice1,staff1,s])-note(865,[E,n],6,121128,121163,122009,62). -snote(866,[A,n],6,102:2,0,11184811/268435456,301.0,301.3333,[voice1,staff1,s])-note(866,[A,n],6,121285,121390,122009,68). -snote(867,[C,n],7,102:2,11184811/268435456,11184811/268435456,301.3333,301.6667,[voice1,staff1,s])-note(867,[C,n],7,121407,121515,122009,59). -snote(868,[E,n],7,102:2,11184811/134217728,11184811/268435456,301.6667,302.0,[voice1,staff1,s])-note(868,[E,n],7,121521,121731,122009,71). -snote(869,[D,n],7,102:3,0,11184811/268435456,302.0,302.3333,[voice1,staff1,s])-note(870,[D,n],7,121659,121790,122009,62). +snote(864,[C,n],6,102:1,1/24,1/24,300.3333,300.6667,[voice1,staff1,s])-note(864,[C,n],6,121030,121136,122009,69). +snote(865,[E,n],6,102:1,1/12,1/24,300.6667,301.0,[voice1,staff1,s])-note(865,[E,n],6,121128,121163,122009,62). +snote(866,[A,n],6,102:2,0,1/24,301.0,301.3333,[voice1,staff1,s])-note(866,[A,n],6,121285,121390,122009,68). +snote(867,[C,n],7,102:2,1/24,1/24,301.3333,301.6667,[voice1,staff1,s])-note(867,[C,n],7,121407,121515,122009,59). +snote(868,[E,n],7,102:2,1/12,1/24,301.6667,302.0,[voice1,staff1,s])-note(868,[E,n],7,121521,121731,122009,71). +snote(869,[D,n],7,102:3,0,1/24,302.0,302.3333,[voice1,staff1,s])-note(870,[D,n],7,121659,121790,122009,62). snote(870,[A,n],3,102:3,0,1/8,302.0,303.0,[voice5,staff2])-note(869,[A,n],3,121656,121762,122009,60). snote(871,[C,n],4,102:3,0,1/8,302.0,303.0,[voice5,staff2])-note(871,[C,n],4,121665,121771,122009,62). snote(872,[E,n],4,102:3,0,1/8,302.0,303.0,[voice5,staff2])-note(872,[E,n],4,121694,121740,122009,54). -snote(873,[C,n],7,102:3,11184811/268435456,11184811/268435456,302.3333,302.6667,[voice1,staff1,s])-note(873,[C,n],7,121750,121845,122009,62). -snote(874,[B,n],6,102:3,11184811/134217728,11184811/268435456,302.6667,303.0,[voice1,staff1,s])-note(874,[B,n],6,121839,121930,122009,71). -snote(875,[B,b],6,103:1,0,11184811/268435456,303.0,303.3333,[voice1,staff1,s])-note(875,[B,b],6,121990,122083,122083,67). +snote(873,[C,n],7,102:3,1/24,1/24,302.3333,302.6667,[voice1,staff1,s])-note(873,[C,n],7,121750,121845,122009,62). +snote(874,[B,n],6,102:3,1/12,1/24,302.6667,303.0,[voice1,staff1,s])-note(874,[B,n],6,121839,121930,122009,71). +snote(875,[B,b],6,103:1,0,1/24,303.0,303.3333,[voice1,staff1,s])-note(875,[B,b],6,121990,122083,122083,67). snote(876,[A,n],3,103:1,0,1/8,303.0,304.0,[voice5,staff2])-note(877,[A,n],3,122034,122201,124486,65). snote(877,[C,n],4,103:1,0,1/8,303.0,304.0,[voice5,staff2])-note(876,[C,n],4,122020,122200,124486,65). snote(878,[E,n],4,103:1,0,1/8,303.0,304.0,[voice5,staff2])-note(878,[E,n],4,122041,122224,124486,63). -snote(879,[A,n],6,103:1,11184811/268435456,11184811/268435456,303.3333,303.6667,[voice1,staff1,s])-note(879,[A,n],6,122105,122192,124486,68). -snote(880,[G,#],6,103:1,11184811/134217728,11184811/268435456,303.6667,304.0,[voice1,staff1,s])-note(880,[G,#],6,122238,122328,124486,61). -snote(881,[G,n],6,103:2,0,11184811/268435456,304.0,304.3333,[voice1,staff1,s])-note(881,[G,n],6,122334,122435,124486,71). -snote(882,[F,#],6,103:2,11184811/268435456,11184811/268435456,304.3333,304.6667,[voice1,staff1,s])-note(882,[F,#],6,122454,122559,124486,66). -snote(883,[F,n],6,103:2,11184811/134217728,11184811/268435456,304.6667,305.0,[voice1,staff1,s])-note(883,[F,n],6,122557,122599,124486,66). -snote(884,[E,n],6,103:3,0,11184811/268435456,305.0,305.3333,[voice1,staff1,s])-note(884,[E,n],6,122674,122806,124486,64). -snote(885,[D,#],6,103:3,11184811/268435456,11184811/268435456,305.3333,305.6667,[voice1,staff1,s])-note(885,[D,#],6,122799,122888,124486,68). -snote(886,[D,n],6,103:3,11184811/134217728,11184811/268435456,305.6667,306.0,[voice1,staff1,s])-note(886,[D,n],6,122891,122969,124486,63). -snote(887,[C,#],6,104:1,0,11184811/268435456,306.0,306.3333,[voice1,staff1,s])-note(887,[C,#],6,123010,123116,124486,64). -snote(888,[C,n],6,104:1,11184811/268435456,11184811/268435456,306.3333,306.6667,[voice1,staff1,s])-note(888,[C,n],6,123123,123168,124486,69). -snote(889,[B,n],5,104:1,11184811/134217728,11184811/268435456,306.6667,307.0,[voice1,staff1,s])-note(889,[B,n],5,123249,123354,124486,69). -snote(890,[B,b],5,104:2,0,11184811/268435456,307.0,307.3333,[voice1,staff1,s])-note(890,[B,b],5,123350,123444,124486,62). -snote(891,[A,n],5,104:2,11184811/268435456,11184811/268435456,307.3333,307.6667,[voice1,staff1,s])-note(891,[A,n],5,123452,123537,124486,65). -snote(892,[G,#],5,104:2,11184811/134217728,11184811/268435456,307.6667,308.0,[voice1,staff1,s])-note(892,[G,#],5,123577,123667,124486,66). -snote(893,[G,n],5,104:3,0,11184811/268435456,308.0,308.3333,[voice1,staff1,s])-note(893,[G,n],5,123701,123761,124486,62). -snote(894,[F,#],5,104:3,11184811/268435456,11184811/268435456,308.3333,308.6667,[voice1,staff1,s])-note(894,[F,#],5,123781,123906,124486,73). -snote(895,[F,n],5,104:3,11184811/134217728,11184811/268435456,308.6667,309.0,[voice1,staff1,s])-note(895,[F,n],5,123915,123969,124486,67). +snote(879,[A,n],6,103:1,1/24,1/24,303.3333,303.6667,[voice1,staff1,s])-note(879,[A,n],6,122105,122192,124486,68). +snote(880,[G,#],6,103:1,1/12,1/24,303.6667,304.0,[voice1,staff1,s])-note(880,[G,#],6,122238,122328,124486,61). +snote(881,[G,n],6,103:2,0,1/24,304.0,304.3333,[voice1,staff1,s])-note(881,[G,n],6,122334,122435,124486,71). +snote(882,[F,#],6,103:2,1/24,1/24,304.3333,304.6667,[voice1,staff1,s])-note(882,[F,#],6,122454,122559,124486,66). +snote(883,[F,n],6,103:2,1/12,1/24,304.6667,305.0,[voice1,staff1,s])-note(883,[F,n],6,122557,122599,124486,66). +snote(884,[E,n],6,103:3,0,1/24,305.0,305.3333,[voice1,staff1,s])-note(884,[E,n],6,122674,122806,124486,64). +snote(885,[D,#],6,103:3,1/24,1/24,305.3333,305.6667,[voice1,staff1,s])-note(885,[D,#],6,122799,122888,124486,68). +snote(886,[D,n],6,103:3,1/12,1/24,305.6667,306.0,[voice1,staff1,s])-note(886,[D,n],6,122891,122969,124486,63). +snote(887,[C,#],6,104:1,0,1/24,306.0,306.3333,[voice1,staff1,s])-note(887,[C,#],6,123010,123116,124486,64). +snote(888,[C,n],6,104:1,1/24,1/24,306.3333,306.6667,[voice1,staff1,s])-note(888,[C,n],6,123123,123168,124486,69). +snote(889,[B,n],5,104:1,1/12,1/24,306.6667,307.0,[voice1,staff1,s])-note(889,[B,n],5,123249,123354,124486,69). +snote(890,[B,b],5,104:2,0,1/24,307.0,307.3333,[voice1,staff1,s])-note(890,[B,b],5,123350,123444,124486,62). +snote(891,[A,n],5,104:2,1/24,1/24,307.3333,307.6667,[voice1,staff1,s])-note(891,[A,n],5,123452,123537,124486,65). +snote(892,[G,#],5,104:2,1/12,1/24,307.6667,308.0,[voice1,staff1,s])-note(892,[G,#],5,123577,123667,124486,66). +snote(893,[G,n],5,104:3,0,1/24,308.0,308.3333,[voice1,staff1,s])-note(893,[G,n],5,123701,123761,124486,62). +snote(894,[F,#],5,104:3,1/24,1/24,308.3333,308.6667,[voice1,staff1,s])-note(894,[F,#],5,123781,123906,124486,73). +snote(895,[F,n],5,104:3,1/12,1/24,308.6667,309.0,[voice1,staff1,s])-note(895,[F,n],5,123915,123969,124486,67). snote(896,[E,n],5,105:1,0,1/16,309.0,309.5,[voice1,staff1,s])-note(896,[E,n],5,124064,124272,124486,69). snote(897,[D,#],5,105:1,1/16,1/16,309.5,310.0,[voice1,staff1,s])-note(897,[D,#],5,124242,124436,124486,61). snote(898,[E,n],5,105:2,0,1/16,310.0,310.5,[voice1,staff1,s])-note(898,[E,n],5,124440,124614,124614,67). From c0668c74976f4b0f447de938b67d03f96939a82e Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 24 Sep 2024 07:25:22 +0200 Subject: [PATCH 044/151] clean imports --- partitura/io/exportmatch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/partitura/io/exportmatch.py b/partitura/io/exportmatch.py index 72435956..4656c724 100644 --- a/partitura/io/exportmatch.py +++ b/partitura/io/exportmatch.py @@ -21,7 +21,6 @@ from partitura.io.matchlines_v1 import ( make_info, make_scoreprop, - make_section, MatchSnote, MatchNote, MatchSnoteNote, From 5a1a6a8b82de92ccaaf31b6a137b61f279fd4913 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 24 Sep 2024 05:31:23 +0000 Subject: [PATCH 045/151] Format code with black (bot) --- partitura/io/importmidi.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/partitura/io/importmidi.py b/partitura/io/importmidi.py index 6bfcfe05..50f8ad0f 100644 --- a/partitura/io/importmidi.py +++ b/partitura/io/importmidi.py @@ -522,8 +522,6 @@ def load_score_midi( warnings.warn("pitch spelling") spelling_global = analysis.estimate_spelling(note_array) - - if estimate_voice_info: warnings.warn("voice estimation", stacklevel=2) # TODO: deal with zero duration notes in note_array. From 07eb9ed6b6f046c728a4e083f6b39075fd9f8573 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 24 Sep 2024 07:36:48 +0200 Subject: [PATCH 046/151] use typing tuple --- partitura/io/importmatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index 73f96337..b1b34722 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -755,7 +755,7 @@ def part_from_matchfile( def make_timesig_maps( ts_orig: List[Tuple[float, int, MatchTimeSignature]], max_time: float, -) -> tuple[Callable, Callable, Callable, Callable, float, float]: +) -> Tuple[Callable, Callable, Callable, Callable, float, float]: """ Create time signature (interpolation) maps From 5784c1c874b8deb497dd6fd8fdfc3325b6c6300e Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 24 Sep 2024 13:51:07 +0200 Subject: [PATCH 047/151] find closest straight duration below --- partitura/utils/music.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index e6afe4ea..dccc4192 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -775,7 +775,7 @@ def estimate_symbolic_duration( f"Returning empty symbolic duration.") return {} else: - i = find_nearest(STRAIGHT_DURS, qdur) + i = np.searchsorted(STRAIGHT_DURS, qdur, side="left") - 1 # NOTE: Guess tuplets (Naive) it doesn't cover composite durations from tied notes. type = SYM_STRAIGHT_DURS[i+1]["type"] normal_notes = 2 From c3e35cebd816db86963ed7afcd4b710f71695c64 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 24 Sep 2024 13:53:11 +0200 Subject: [PATCH 048/151] remove duplicate sym durs --- partitura/utils/globals.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/partitura/utils/globals.py b/partitura/utils/globals.py index ca42dbb6..8571e025 100644 --- a/partitura/utils/globals.py +++ b/partitura/utils/globals.py @@ -359,7 +359,6 @@ ({"type": "half", "dots": 0}, {"type": "16th", "dots": 0}), ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}), ({"type": "half", "dots": 0}, {"type": "16th", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "half", "dots": 0}, {"type": "16th", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0}), ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0}, {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}), ({"type": "half", "dots": 0}, {"type": "quarter", "dots": 0, "actual_notes": 3, "normal_notes": 2}), @@ -372,7 +371,6 @@ ({"type": "half", "dots": 1}, {"type": "16th", "dots": 0}), ({"type": "half", "dots": 1}, {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}), ({"type": "half", "dots": 1}, {"type": "16th", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "half", "dots": 1}, {"type": "16th", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), ({"type": "half", "dots": 2}, {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}), ({"type": "half", "dots": 1}, {"type": "quarter", "dots": 0, "actual_notes": 3, "normal_notes": 2}), ({"type": "half", "dots": 2}, {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}), From 7ab8b3f4852c2b779c55c799be04bbbea89b9a11 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 24 Sep 2024 14:28:19 +0200 Subject: [PATCH 049/151] set test to eighth note quintuplets --- tests/test_midi_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_midi_import.py b/tests/test_midi_import.py index 0255d38e..9389a34b 100644 --- a/tests/test_midi_import.py +++ b/tests/test_midi_import.py @@ -136,7 +136,7 @@ def make_triplets_example_2(): fill_track(track, notes, divs) # target: actual_notes = [5] * 5 + [3] * 3 - normal_notes = [2] * 5 + [2] * 3 + normal_notes = [4] * 5 + [2] * 3 return mid, actual_notes, normal_notes From e51b3e3fdff3564a555482fc0207f235974b85d1 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 24 Sep 2024 14:49:22 +0200 Subject: [PATCH 050/151] duration test estimation --- tests/test_utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 1d558f4a..8eff8147 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -683,24 +683,24 @@ def test_estimate_symbolic_duration(self): ({'type': 'half', 'dots': 0}, {'type': '16th', 'dots': 0}), ({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), ({'type': 'half', 'dots': 0}, {'type': '16th', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), - ({'type': 'half', 'dots': 0}, {'type': '16th', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), ({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 0}), ({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 0}, {'type': '32nd', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), ({'type': 'half', 'dots': 0}, {'type': 'quarter', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), ({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 1}), ({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 0}, {'type': 'eighth', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), - {'type': 'half', 'dots': 1}, ({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 1}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), - ({'type': 'half', 'dots': 1}, {'type': '32nd', 'dots': 0}), + {'type': 'half', 'dots': 1}, + ({'type': 'half', 'dots': 1}, {'type': '32nd', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), ({'type': 'half', 'dots': 1}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), ({'type': 'half', 'dots': 1}, {'type': '16th', 'dots': 0}), ({'type': 'half', 'dots': 1}, {'type': 'eighth', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), - {'type': 'half', 'dots': 2}, ({'type': 'half', 'dots': 1}, {'type': '16th', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), - ({'type': 'half', 'dots': 1}, {'type': '16th', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), - {'type': 'half', 'dots': 3}, + {'type': 'half', 'dots': 2}, ({'type': 'half', 'dots': 2}, {'type': '32nd', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), ({'type': 'half', 'dots': 1}, {'type': 'quarter', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + {'type': 'half', 'dots': 3}, + ({'type': 'half', 'dots': 2}, {'type': 'eighth', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), + ({'type': 'half', 'dots': 3}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}), ] predicted = [] for k in range(divs * 0, divs * quarters): From 3056e0a6a54b2a8ef9d684c8a129f88b622cab3c Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 24 Sep 2024 14:55:23 +0200 Subject: [PATCH 051/151] file formatting --- tests/test_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 8eff8147..a8d539c5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -707,4 +707,5 @@ def test_estimate_symbolic_duration(self): a = partitura.utils.estimate_symbolic_duration( k, divs, return_com_durations=True) predicted.append(a) - self.assertTrue(all([expected[i] == predicted[i] for i in range(len(expected))])) \ No newline at end of file + self.assertTrue(all([expected[i] == predicted[i] for i in range(len(expected))])) + \ No newline at end of file From 1e76b640f0ffa9af8e33fce6803f3fa1bad8eb54 Mon Sep 17 00:00:00 2001 From: sildater Date: Wed, 25 Sep 2024 14:44:02 +0200 Subject: [PATCH 052/151] measure feature wip --- partitura/musicanalysis/note_features.py | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 92e07c0b..8e1f39f8 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -1073,6 +1073,35 @@ def metrical_strength_feature(na, part, **kwargs): return W, names +def measure_feature(na, part, **kwargs): + """Measure feature + + This feature encodes the measure each note is in. + + """ + notes = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests + bm = part.beat_map + eps = 10**-6 + + names = [ + "measure_number", + "measure_name", + "measure_start_beats", + "measure_end_beats", + ] + + for i, n in enumerate(notes): + measure = next(n.start.iter_prev(score.Measure, eq=True), None) + + if measure: + measure_start = measure.start.t + else: + pass + + W = np.zeros((len(notes), 4)) + + return W, names + def time_signature_feature(na, part, **kwargs): """TIme Signature feature From e5d8938c7b7945423a32a7166aca697b781d3b1a Mon Sep 17 00:00:00 2001 From: sildater Date: Wed, 25 Sep 2024 14:47:33 +0200 Subject: [PATCH 053/151] single measure wip --- partitura/musicanalysis/note_features.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 8e1f39f8..05918ab5 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -1083,9 +1083,14 @@ def measure_feature(na, part, **kwargs): bm = part.beat_map eps = 10**-6 + measures = np.array([(m.start.t, m.end.t) for m in part.iter_all(score.Measure)]) + if len(measures) == 0: + start = bm(part.first_point.t) + end = bm(part.last_point.t) + number = 1 + names = [ "measure_number", - "measure_name", "measure_start_beats", "measure_end_beats", ] From de5cc0bd28e7f4624fbcab9bf15bcafecc7b0771 Mon Sep 17 00:00:00 2001 From: sildater Date: Wed, 25 Sep 2024 14:50:14 +0200 Subject: [PATCH 054/151] rework default wip --- partitura/musicanalysis/note_features.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 05918ab5..ff288abb 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -1083,11 +1083,10 @@ def measure_feature(na, part, **kwargs): bm = part.beat_map eps = 10**-6 - measures = np.array([(m.start.t, m.end.t) for m in part.iter_all(score.Measure)]) - if len(measures) == 0: - start = bm(part.first_point.t) - end = bm(part.last_point.t) - number = 1 + + global_start = bm(part.first_point.t) + global_end = bm(part.last_point.t) + global_number = 0 # default global measure number names = [ "measure_number", @@ -1099,9 +1098,14 @@ def measure_feature(na, part, **kwargs): measure = next(n.start.iter_prev(score.Measure, eq=True), None) if measure: - measure_start = measure.start.t + start = bm(measure.start.t) + end = bm(measure.end.t) + number = measure.number else: - pass + start = bm(measure.start.t) + end = bm(measure.end.t) + number = measure.number + W = np.zeros((len(notes), 4)) From 7db9f9b7776929d534fa88cbec0bf867b9dca6e8 Mon Sep 17 00:00:00 2001 From: sildater Date: Wed, 25 Sep 2024 14:52:23 +0200 Subject: [PATCH 055/151] fill measure info wip --- partitura/musicanalysis/note_features.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index ff288abb..b2d6c9c6 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -1081,8 +1081,6 @@ def measure_feature(na, part, **kwargs): """ notes = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests bm = part.beat_map - eps = 10**-6 - global_start = bm(part.first_point.t) global_end = bm(part.last_point.t) @@ -1093,6 +1091,7 @@ def measure_feature(na, part, **kwargs): "measure_start_beats", "measure_end_beats", ] + W = np.zeros((len(notes), 3)) for i, n in enumerate(notes): measure = next(n.start.iter_prev(score.Measure, eq=True), None) @@ -1105,9 +1104,10 @@ def measure_feature(na, part, **kwargs): start = bm(measure.start.t) end = bm(measure.end.t) number = measure.number - - W = np.zeros((len(notes), 4)) + W[i, 0] = number + W[i, 1] = start + W[i, 2] = end return W, names From 4306340bb7d329d17bb1c3fa2c8ccf264e9bf363 Mon Sep 17 00:00:00 2001 From: sildater Date: Wed, 25 Sep 2024 15:00:30 +0200 Subject: [PATCH 056/151] measure feature complete --- partitura/musicanalysis/note_features.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index b2d6c9c6..92935aca 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -1088,8 +1088,8 @@ def measure_feature(na, part, **kwargs): names = [ "measure_number", - "measure_start_beats", - "measure_end_beats", + "measure_start_beat", + "measure_end_beat", ] W = np.zeros((len(notes), 3)) @@ -1101,9 +1101,9 @@ def measure_feature(na, part, **kwargs): end = bm(measure.end.t) number = measure.number else: - start = bm(measure.start.t) - end = bm(measure.end.t) - number = measure.number + start = global_start + end = global_end + number = global_number W[i, 0] = number W[i, 1] = start From 3fdfd39c716975158073da070f283154bd972161 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 1 Oct 2024 08:16:29 +0200 Subject: [PATCH 057/151] fix time maps import --- docs/source/Tutorial/notebook.ipynb | 186 ++++++++++++++-------------- 1 file changed, 93 insertions(+), 93 deletions(-) diff --git a/docs/source/Tutorial/notebook.ipynb b/docs/source/Tutorial/notebook.ipynb index 31d98713..b90d44e4 100644 --- a/docs/source/Tutorial/notebook.ipynb +++ b/docs/source/Tutorial/notebook.ipynb @@ -47,11 +47,12 @@ }, "id": "PeabdL1k7YC4", "outputId": "fcb7d1be-27a1-4c79-c5d3-8cbfa54cae44", - "scrolled": true, "pycharm": { "is_executing": true - } + }, + "scrolled": true }, + "outputs": [], "source": [ "# Install partitura\n", "! pip install partitura\n", @@ -64,21 +65,20 @@ "import sys, os\n", "sys.path.insert(0, os.path.join(os.getcwd(), \"partitura_tutorial\", \"content\"))\n", "sys.path.insert(0,'/content/partitura_tutorial/content')\n" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 2, "id": "impressed-principle", "metadata": {}, + "outputs": [], "source": [ "import glob\n", "import partitura as pt\n", "import numpy as np\n", "import matplotlib.pyplot as plt" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -103,6 +103,7 @@ "execution_count": 3, "id": "photographic-profession", "metadata": {}, + "outputs": [], "source": [ "# setup the dataset\n", "from load_data import init_dataset\n", @@ -110,8 +111,7 @@ "MUSICXML_DIR = os.path.join(DATASET_DIR, 'musicxml')\n", "MIDI_DIR = os.path.join(DATASET_DIR, 'midi')\n", "MATCH_DIR = os.path.join(DATASET_DIR, 'match')" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -220,12 +220,12 @@ "execution_count": 4, "id": "c9179e78", "metadata": {}, + "outputs": [], "source": [ "path_to_musicxml = pt.EXAMPLE_MUSICXML\n", "part = pt.load_musicxml(path_to_musicxml)[0]\n", "print(part.pretty())" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -250,20 +250,20 @@ "execution_count": 5, "id": "423aac6a", "metadata": {}, + "outputs": [], "source": [ "part.notes" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 6, "id": "0a929369", "metadata": {}, + "outputs": [], "source": [ "dir(part.notes[0])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -278,23 +278,23 @@ "execution_count": 7, "id": "2a8293c9", "metadata": {}, + "outputs": [], "source": [ "a_new_note = pt.score.Note(id='n04', step='A', octave=4, voice=1)\n", "part.add(a_new_note, start=3, end=15)\n", "# print(part.pretty())" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 8, "id": "eba2fa93", "metadata": {}, + "outputs": [], "source": [ "part.remove(a_new_note)\n", "# print(part.pretty())" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -313,10 +313,10 @@ "execution_count": 9, "id": "e95eb0f7", "metadata": {}, + "outputs": [], "source": [ "part.beat_map(part.notes[0].end.t)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -331,10 +331,10 @@ "execution_count": 10, "id": "05346a03", "metadata": {}, + "outputs": [], "source": [ "part.time_signature_map(part.notes[0].end.t)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -356,22 +356,22 @@ "execution_count": 11, "id": "74943a93", "metadata": {}, + "outputs": [], "source": [ "for measure in part.iter_all(pt.score.Measure):\n", " print(measure)" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 12, "id": "6cbfd044", "metadata": {}, + "outputs": [], "source": [ "for note in part.iter_all(pt.score.GenericNote, include_subclasses=True, start=0, end=24):\n", " print(note)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -388,6 +388,7 @@ "execution_count": 13, "id": "fe430921", "metadata": {}, + "outputs": [], "source": [ "# figure out the last measure position, time signature and beat length in divs\n", "measures = [m for m in part.iter_all(pt.score.Measure)]\n", @@ -405,18 +406,17 @@ "# add a note\n", "a_new_note = pt.score.Note(id='n04', step='A', octave=4, voice=1)\n", "part.add(a_new_note, start=append_measure_start, end=append_measure_start+one_beat_in_divs_at_the_end)" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 14, "id": "f9d738a5", "metadata": {}, + "outputs": [], "source": [ "# print(part.pretty())" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -436,21 +436,21 @@ "execution_count": 15, "id": "5d82a340", "metadata": {}, + "outputs": [], "source": [ "path_to_midifile = pt.EXAMPLE_MIDI\n", "performedpart = pt.load_performance_midi(path_to_midifile)[0]" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 16, "id": "4e3090d9", "metadata": {}, + "outputs": [], "source": [ "performedpart.notes" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -465,6 +465,7 @@ "execution_count": 17, "id": "d6eb12f2", "metadata": {}, + "outputs": [], "source": [ "import numpy as np \n", "\n", @@ -491,14 +492,14 @@ " part.add(pt.score.Note(id='n{}'.format(idx), step=step, \n", " octave=int(octave), alter=alter, voice=voice, staff=str((voice-1)%2+1)), \n", " start=start, end=end)" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 18, "id": "572e856c", "metadata": {}, + "outputs": [], "source": [ "l = 200\n", "p = pt.score.Part('CoK', 'Cat on Keyboard', quarter_duration=8)\n", @@ -509,54 +510,53 @@ " np.random.randint(40,60, size=(1,l+1)),\n", " np.random.randint(40,60, size=(1,l+1))\n", " ))" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 19, "id": "f9f03a50", "metadata": {}, + "outputs": [], "source": [ "for k in range(l):\n", " for j in range(4):\n", " addnote(pitch[j,k], p, j+1, ons[j,k], ons[j,k]+dur[j,k+1], \"v\"+str(j)+\"n\"+str(k))" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 20, "id": "09fb6b45", "metadata": {}, + "outputs": [], "source": [ "p.add(pt.score.TimeSignature(4, 4), start=0)\n", "p.add(pt.score.Clef(1, \"G\", line = 3, octave_change=0),start=0)\n", "p.add(pt.score.Clef(2, \"G\", line = 3, octave_change=0),start=0)\n", "pt.score.add_measures(p)\n", "pt.score.tie_notes(p)" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 21, "id": "834582d5", "metadata": {}, + "outputs": [], "source": [ "# pt.save_score_midi(p, \"CatPerformance.mid\", part_voice_assign_mode=2)" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 22, "id": "006f02ed", "metadata": {}, + "outputs": [], "source": [ "# pt.save_musicxml(p, \"CatScore.xml\")" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -602,6 +602,7 @@ "execution_count": 23, "id": "first-basin", "metadata": {}, + "outputs": [], "source": [ "# Note array from a score\n", "\n", @@ -613,8 +614,7 @@ "\n", "# Get note array.\n", "score_note_array = score_part.note_array()" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -629,11 +629,11 @@ "execution_count": 24, "id": "alternate-coordinate", "metadata": {}, + "outputs": [], "source": [ "# Lets see the first notes in this note array\n", "print(score_note_array[:10])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -650,10 +650,10 @@ "execution_count": 25, "id": "subtle-millennium", "metadata": {}, + "outputs": [], "source": [ "print(score_note_array.dtype.names)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -686,6 +686,7 @@ "execution_count": 26, "id": "passing-lending", "metadata": {}, + "outputs": [], "source": [ "# Note array from a performance\n", "\n", @@ -697,8 +698,7 @@ "\n", "# Get note array!\n", "performance_note_array = performance_part.note_array()" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -713,10 +713,10 @@ "execution_count": 27, "id": "pointed-stupid", "metadata": {}, + "outputs": [], "source": [ "print(performance_note_array.dtype.names)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -737,10 +737,10 @@ "execution_count": 28, "id": "subject-reducing", "metadata": {}, + "outputs": [], "source": [ "print(performance_note_array[:5])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -755,6 +755,7 @@ "execution_count": 29, "id": "spread-performer", "metadata": {}, + "outputs": [], "source": [ "note_array = np.array(\n", " [(60, 0, 2, 40),\n", @@ -771,8 +772,7 @@ "\n", "# Note array to `PerformedPart`\n", "performed_part = pt.performance.PerformedPart.from_note_array(note_array)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -787,11 +787,11 @@ "execution_count": 30, "id": "changed-check", "metadata": {}, + "outputs": [], "source": [ "# export as MIDI file\n", "pt.save_performance_midi(performed_part, \"example.mid\")" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -808,6 +808,7 @@ "execution_count": 31, "id": "figured-coordinator", "metadata": {}, + "outputs": [], "source": [ "extended_score_note_array = pt.utils.music.ensure_notearray(\n", " score_part,\n", @@ -817,18 +818,17 @@ " # include_metrical_position=True, # adds 3 fields: is_downbeat, rel_onset_div, tot_measure_div\n", " include_grace_notes=True # adds 2 fields: is_grace, grace_type\n", ")" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 32, "id": "vietnamese-pathology", "metadata": {}, + "outputs": [], "source": [ "extended_score_note_array.dtype.names" - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -837,6 +837,7 @@ "metadata": { "scrolled": true }, + "outputs": [], "source": [ "print(extended_score_note_array[['id', \n", " 'step', \n", @@ -845,8 +846,7 @@ " 'ks_fifths', \n", " 'ks_mode', #'is_downbeat'\n", " ]][:10])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -879,6 +879,7 @@ "metadata": { "scrolled": true }, + "outputs": [], "source": [ "# Path to the MusicXML file\n", "score_fn = os.path.join(MUSICXML_DIR, 'Chopin_op10_no3.musicxml')\n", @@ -914,8 +915,7 @@ "\n", "accented_note_idxs = np.where(accent_note_array['accent'])\n", "print(accent_note_array[accented_note_idxs][:5])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -944,6 +944,7 @@ "execution_count": 35, "id": "essential-academy", "metadata": {}, + "outputs": [], "source": [ "# TODO: change the example\n", "# Path to the MusicXML file\n", @@ -953,8 +954,7 @@ "score_part = pt.load_musicxml(score_fn)\n", "# compute piano roll\n", "pianoroll = pt.utils.compute_pianoroll(score_part)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -969,6 +969,7 @@ "execution_count": 36, "id": "massive-monaco", "metadata": {}, + "outputs": [], "source": [ "piano_range = True\n", "time_unit = 'beat'\n", @@ -979,8 +980,7 @@ " time_div=time_div, # Number of cells per time unit\n", " piano_range=piano_range # Use range of the piano (88 keys)\n", ")" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1007,14 +1007,14 @@ "execution_count": 37, "id": "mature-dylan", "metadata": {}, + "outputs": [], "source": [ "fig, ax = plt.subplots(1, figsize=(20, 10))\n", "ax.imshow(pianoroll.toarray(), origin=\"lower\", cmap='gray', interpolation='nearest', aspect='auto')\n", "ax.set_xlabel(f'Time ({time_unit}s/{time_div})')\n", "ax.set_ylabel('Piano key' if piano_range else 'MIDI pitch')\n", "plt.show()" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1031,13 +1031,13 @@ "metadata": { "scrolled": true }, + "outputs": [], "source": [ "pianoroll, note_indices = pt.utils.compute_pianoroll(score_part, return_idxs=True)\n", "\n", "# MIDI pitch, start, end\n", "print(note_indices[:5])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1055,6 +1055,7 @@ "execution_count": 39, "id": "parental-links", "metadata": {}, + "outputs": [], "source": [ "pianoroll = pt.utils.compute_pianoroll(score_part)\n", "\n", @@ -1064,8 +1065,7 @@ "ppart = pt.performance.PerformedPart.from_note_array(new_note_array)\n", "\n", "pt.save_performance_midi(ppart, \"newmidi.mid\")" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1112,13 +1112,13 @@ "execution_count": 40, "id": "rolled-cloud", "metadata": {}, + "outputs": [], "source": [ "# path to the match\n", "match_fn = os.path.join(MATCH_DIR, 'Chopin_op10_no3_p01.match')\n", "# loading a match file\n", "performed_part, alignment, score_part = pt.load_match(match_fn, create_part=True)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1140,6 +1140,7 @@ "execution_count": 41, "id": "latest-smell", "metadata": {}, + "outputs": [], "source": [ "# path to the match\n", "match_fn = os.path.join(MATCH_DIR, 'Chopin_op10_no3_p01.match')\n", @@ -1150,8 +1151,7 @@ "\n", "# loading a match file\n", "performed_part, alignment = pt.load_match(match_fn)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1175,10 +1175,10 @@ "execution_count": 42, "id": "radio-interim", "metadata": {}, + "outputs": [], "source": [ "alignment[:10]" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1197,6 +1197,7 @@ "execution_count": 43, "id": "published-understanding", "metadata": {}, + "outputs": [], "source": [ "# note array of the score\n", "snote_array = score_part.note_array()\n", @@ -1209,8 +1210,7 @@ "matched_snote_array = snote_array[matched_note_idxs[:, 0]]\n", "# note array of the matched performed notes\n", "matched_pnote_array = pnote_array[matched_note_idxs[:, 1]]" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1227,6 +1227,7 @@ "execution_count": 44, "id": "offshore-bridal", "metadata": {}, + "outputs": [], "source": [ "# get all match files\n", "matchfiles = glob.glob(os.path.join(MATCH_DIR, 'Chopin_op10_no3_p*.match'))\n", @@ -1248,13 +1249,12 @@ " performance, alignment = pt.load_match(matchfile)\n", " ppart = performance[0]\n", " # Get score time to performance time map\n", - " _, stime_to_ptime_map = pt.utils.music.get_time_maps_from_alignment(\n", + " _, stime_to_ptime_map = pt.musicanalysis.performance_codec.get_time_maps_from_alignment(\n", " ppart, score_part, alignment)\n", " # Compute naïve tempo curve\n", " performance_time = stime_to_ptime_map(score_time_ending)\n", " tempo_curves[i,:] = 60 * np.diff(score_time_ending) / np.diff(performance_time)" - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -1263,6 +1263,7 @@ "metadata": { "scrolled": false }, + "outputs": [], "source": [ "fig, ax = plt.subplots(1, figsize=(15, 8))\n", "color = plt.cm.rainbow(np.linspace(0, 1, len(tempo_curves)))\n", @@ -1284,8 +1285,7 @@ "plt.legend(frameon=False, bbox_to_anchor = (1.15, .9))\n", "plt.grid(axis='x')\n", "plt.show()" - ], - "outputs": [] + ] }, { "cell_type": "markdown", From 69f89459e4c3fd50088261d9079efa9043bfca9b Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 1 Oct 2024 08:22:34 +0200 Subject: [PATCH 058/151] add warning --- partitura/musicanalysis/performance_codec.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/partitura/musicanalysis/performance_codec.py b/partitura/musicanalysis/performance_codec.py index 54c1b0ac..2064f1b8 100644 --- a/partitura/musicanalysis/performance_codec.py +++ b/partitura/musicanalysis/performance_codec.py @@ -735,6 +735,7 @@ def get_time_maps_from_alignment( # Get indices of the matched notes (notes in the score # for which there is a performance note match_idx = get_matched_notes(score_note_array, perf_note_array, alignment) + print(match_idx) # Get onsets and durations score_onsets = score_note_array[match_idx[:, 0]]["onset_beat"] @@ -824,6 +825,11 @@ def get_matched_notes(spart_note_array, ppart_note_array, alignment): p_idx = int(p_idx) matched_idxs.append((s_idx, p_idx)) + if len(matched_idxs) == 0: + warnings.warn( + "No matched note IDs found." + ) + return np.array(matched_idxs) From 6d24b6a34517b5f8f3fee01c11573e3de99a3560 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 1 Oct 2024 08:25:15 +0200 Subject: [PATCH 059/151] explainer in warning --- partitura/musicanalysis/performance_codec.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/partitura/musicanalysis/performance_codec.py b/partitura/musicanalysis/performance_codec.py index 2064f1b8..ea863c5e 100644 --- a/partitura/musicanalysis/performance_codec.py +++ b/partitura/musicanalysis/performance_codec.py @@ -735,7 +735,6 @@ def get_time_maps_from_alignment( # Get indices of the matched notes (notes in the score # for which there is a performance note match_idx = get_matched_notes(score_note_array, perf_note_array, alignment) - print(match_idx) # Get onsets and durations score_onsets = score_note_array[match_idx[:, 0]]["onset_beat"] @@ -828,6 +827,9 @@ def get_matched_notes(spart_note_array, ppart_note_array, alignment): if len(matched_idxs) == 0: warnings.warn( "No matched note IDs found." + "Either the alignment contains no matches" + "or the IDs in score of performance do not correspond to the alignment" + "(repeat unfolding, etc.)" ) return np.array(matched_idxs) From 9754a90da2ca9c6ba46c9081c32e308bbf21d1f8 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 1 Oct 2024 08:35:40 +0200 Subject: [PATCH 060/151] correct formatting --- partitura/musicanalysis/performance_codec.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/partitura/musicanalysis/performance_codec.py b/partitura/musicanalysis/performance_codec.py index ea863c5e..434767de 100644 --- a/partitura/musicanalysis/performance_codec.py +++ b/partitura/musicanalysis/performance_codec.py @@ -826,10 +826,10 @@ def get_matched_notes(spart_note_array, ppart_note_array, alignment): if len(matched_idxs) == 0: warnings.warn( - "No matched note IDs found." - "Either the alignment contains no matches" - "or the IDs in score of performance do not correspond to the alignment" - "(repeat unfolding, etc.)" + "No matched note IDs found. " + "Either the alignment contains no matches " + "or the IDs in score of performance do not correspond to the alignment " + "(maybe due to repeat unfolding)." ) return np.array(matched_idxs) From 2d96acf4e2ed9b26694a9dd668b78049a2ca94e2 Mon Sep 17 00:00:00 2001 From: sildater Date: Wed, 2 Oct 2024 09:04:35 +0200 Subject: [PATCH 061/151] basic feature --- partitura/musicanalysis/note_features.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 92e07c0b..1b338cea 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -525,6 +525,10 @@ def grace_feature(na, part, **kwargs): ) return W, feature_names +def measure_feature(na, part, **kwargs): + """Clef feature + This feature encodes the current clef of the staff of each note. + """ def loudness_direction_feature(na, part, **kwargs): """The loudness directions in part. From cec10ffcfdb79c93f9fb3a5bcc0674b200cd3902 Mon Sep 17 00:00:00 2001 From: sildater Date: Wed, 2 Oct 2024 09:09:04 +0200 Subject: [PATCH 062/151] clef feature wip --- partitura/musicanalysis/note_features.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 1b338cea..09453661 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -525,10 +525,24 @@ def grace_feature(na, part, **kwargs): ) return W, feature_names -def measure_feature(na, part, **kwargs): +def clef_feature(na, part, **kwargs): """Clef feature + This feature encodes the current clef of the staff of each note. """ + notes = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests + staff_dict = {n.id: n.staff for n in part.notes_tied} + + names = [ + "clef_sign", + "clef_line", + "clef_number" + ] + + W = np.zeros((len(notes), 3)) + + for i, n in enumerate(notes): + pass def loudness_direction_feature(na, part, **kwargs): """The loudness directions in part. From cc8c25c25809ef9e1005ae8a62e8ea49c8f91bc4 Mon Sep 17 00:00:00 2001 From: sildater Date: Wed, 2 Oct 2024 09:20:07 +0200 Subject: [PATCH 063/151] clef dictionary wip --- partitura/musicanalysis/note_features.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 09453661..75fab1ab 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -13,6 +13,7 @@ from typing import List, Union, Tuple from partitura.utils import ensure_notearray, ensure_rest_array from partitura.score import ScoreLike +from collections import defaultdict __all__ = [ "list_note_feats_functions", @@ -538,6 +539,14 @@ def clef_feature(na, part, **kwargs): "clef_line", "clef_number" ] + clef_start_dict = defaultdict(list) + + for clef in part.iter_all(score.Clef): + staff = clef.staff + time_key = "time_"+str(staff) + clef_key = "clef_"+str(staff) + clef_start_dict[time_key].append(clef.start.t) + clef_start_dict[clef_key].append(clef) W = np.zeros((len(notes), 3)) From 01c0ecf34e4e3d67a38e8c38ac449fa4b05a32b8 Mon Sep 17 00:00:00 2001 From: sildater Date: Wed, 2 Oct 2024 09:28:08 +0200 Subject: [PATCH 064/151] clef interpolator wip --- partitura/musicanalysis/note_features.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 75fab1ab..def87b7c 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -532,26 +532,40 @@ def clef_feature(na, part, **kwargs): This feature encodes the current clef of the staff of each note. """ notes = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests - staff_dict = {n.id: n.staff for n in part.notes_tied} names = [ "clef_sign", "clef_line", "clef_number" ] - clef_start_dict = defaultdict(list) + clef_dict = defaultdict(list) + staff_numbers = set() for clef in part.iter_all(score.Clef): staff = clef.staff + staff_numbers.add(staff) time_key = "time_"+str(staff) clef_key = "clef_"+str(staff) - clef_start_dict[time_key].append(clef.start.t) - clef_start_dict[clef_key].append(clef) + clef_dict[time_key].append(clef.start.t) + clef_dict[clef_key].append(clef) + + for staff in staff_numbers: + time_key = "time_"+str(staff) + interpolator_key = "interp_"+str(staff) + start_times = np.array(clef_dict[time_key]) + clef_indices = np.arange(len(start_times)) + interpolator = interp1d(start_times, clef_indices, + kind = "previous", + bounds_error=False, fill_value=0) + clef_dict[interpolator_key].append(interpolator) W = np.zeros((len(notes), 3)) for i, n in enumerate(notes): - pass + note_staff = n.staff + time_key = "time_"+str(staff) + clef_key = "clef_"+str(staff) + def loudness_direction_feature(na, part, **kwargs): """The loudness directions in part. From 726ff82149794e6302d16cc4e0053d61229f5550 Mon Sep 17 00:00:00 2001 From: sildater Date: Wed, 2 Oct 2024 09:32:06 +0200 Subject: [PATCH 065/151] compute note-wise clef features wip --- partitura/musicanalysis/note_features.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index def87b7c..bdc9a279 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -562,9 +562,16 @@ def clef_feature(na, part, **kwargs): W = np.zeros((len(notes), 3)) for i, n in enumerate(notes): - note_staff = n.staff - time_key = "time_"+str(staff) + staff = n.staff + time = n.start.t clef_key = "clef_"+str(staff) + interpolator_key = "interp_"+str(staff) + clef_idx = clef_dict[interpolator_key](time) + clef = clef_dict[clef_key][clef_idx] + W[i,0] + W[i,1] = clef.line + W[i,2] = clef.number + def loudness_direction_feature(na, part, **kwargs): From 4ae870da41e7b9c58a4c3624684e27db373728d9 Mon Sep 17 00:00:00 2001 From: sildater Date: Wed, 2 Oct 2024 09:37:07 +0200 Subject: [PATCH 066/151] numerical clef signs --- partitura/musicanalysis/note_features.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index bdc9a279..71d3ebf2 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -533,6 +533,8 @@ def clef_feature(na, part, **kwargs): """ notes = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests + numerical_clef_dict ={'G':0, 'F':1, 'C':2, 'percussion':3, 'TAB':4, 'jianpu':5, 'none':6} + names = [ "clef_sign", "clef_line", @@ -542,7 +544,7 @@ def clef_feature(na, part, **kwargs): staff_numbers = set() for clef in part.iter_all(score.Clef): - staff = clef.staff + staff = clef.staff or 1 staff_numbers.add(staff) time_key = "time_"+str(staff) clef_key = "clef_"+str(staff) @@ -562,7 +564,7 @@ def clef_feature(na, part, **kwargs): W = np.zeros((len(notes), 3)) for i, n in enumerate(notes): - staff = n.staff + staff = n.staff or 1 time = n.start.t clef_key = "clef_"+str(staff) interpolator_key = "interp_"+str(staff) From 788e743d94cdd247eac4be9dc3d8b91fc4bc3099 Mon Sep 17 00:00:00 2001 From: sildater Date: Wed, 2 Oct 2024 09:46:01 +0200 Subject: [PATCH 067/151] add some defaults and safety --- partitura/musicanalysis/note_features.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 71d3ebf2..c1525003 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -538,7 +538,7 @@ def clef_feature(na, part, **kwargs): names = [ "clef_sign", "clef_line", - "clef_number" + "clef_octave_change" ] clef_dict = defaultdict(list) @@ -568,11 +568,14 @@ def clef_feature(na, part, **kwargs): time = n.start.t clef_key = "clef_"+str(staff) interpolator_key = "interp_"+str(staff) - clef_idx = clef_dict[interpolator_key](time) - clef = clef_dict[clef_key][clef_idx] - W[i,0] - W[i,1] = clef.line - W[i,2] = clef.number + clef_idx = clef_dict[interpolator_key][0](time) + clef = clef_dict[clef_key][int(clef_idx)] + sign = clef.sign or "G" + W[i,0] = numerical_clef_dict[sign] + W[i,1] = clef.line or 2 + W[i,2] = clef.octave_change or 0 + + return W, names From ace407d8c6540483eaf070e3166625a0ca33d525 Mon Sep 17 00:00:00 2001 From: sildater Date: Wed, 2 Oct 2024 09:49:56 +0200 Subject: [PATCH 068/151] cleanup and documentation --- partitura/musicanalysis/note_features.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index c1525003..b4aae399 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -526,23 +526,27 @@ def grace_feature(na, part, **kwargs): ) return W, feature_names + def clef_feature(na, part, **kwargs): """Clef feature This feature encodes the current clef of the staff of each note. + Note that this feature does not return the staff number per note, + see staff_feature for this information. """ notes = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests - - numerical_clef_dict ={'G':0, 'F':1, 'C':2, 'percussion':3, 'TAB':4, 'jianpu':5, 'none':6} - + numerical_clef_dict ={ + 'G':0, 'F':1, 'C':2, + 'percussion':3, 'TAB':4, + 'jianpu':5, 'none':6} names = [ "clef_sign", "clef_line", "clef_octave_change" ] clef_dict = defaultdict(list) - staff_numbers = set() + for clef in part.iter_all(score.Clef): staff = clef.staff or 1 staff_numbers.add(staff) @@ -578,7 +582,6 @@ def clef_feature(na, part, **kwargs): return W, names - def loudness_direction_feature(na, part, **kwargs): """The loudness directions in part. From 435edc112eed8d6047ec2032c3f513d6c24d2e52 Mon Sep 17 00:00:00 2001 From: sildater Date: Wed, 2 Oct 2024 10:29:07 +0200 Subject: [PATCH 069/151] add safety for part without clef --- partitura/musicanalysis/note_features.py | 70 +++++++++++++----------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index b4aae399..379e0d52 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -546,38 +546,46 @@ def clef_feature(na, part, **kwargs): ] clef_dict = defaultdict(list) staff_numbers = set() + clef_list = [clef for clef in part.iter_all(score.Clef)] + if len(clef_list) > 0: + for clef in clef_list: + staff = clef.staff or 1 + staff_numbers.add(staff) + time_key = "time_"+str(staff) + clef_key = "clef_"+str(staff) + clef_dict[time_key].append(clef.start.t) + clef_dict[clef_key].append(clef) + + for staff in staff_numbers: + time_key = "time_"+str(staff) + interpolator_key = "interp_"+str(staff) + start_times = np.array(clef_dict[time_key]) + clef_indices = np.arange(len(start_times)) + interpolator = interp1d(start_times, clef_indices, + kind = "previous", + bounds_error=False, fill_value=0) + clef_dict[interpolator_key].append(interpolator) + + W = np.zeros((len(notes), 3)) + + for i, n in enumerate(notes): + staff = n.staff or 1 + time = n.start.t + clef_key = "clef_"+str(staff) + interpolator_key = "interp_"+str(staff) + clef_idx = clef_dict[interpolator_key][0](time) + clef = clef_dict[clef_key][int(clef_idx)] + sign = clef.sign or "none" + W[i,0] = numerical_clef_dict[sign] + W[i,1] = clef.line or 0 + W[i,2] = clef.octave_change or 0 - for clef in part.iter_all(score.Clef): - staff = clef.staff or 1 - staff_numbers.add(staff) - time_key = "time_"+str(staff) - clef_key = "clef_"+str(staff) - clef_dict[time_key].append(clef.start.t) - clef_dict[clef_key].append(clef) - - for staff in staff_numbers: - time_key = "time_"+str(staff) - interpolator_key = "interp_"+str(staff) - start_times = np.array(clef_dict[time_key]) - clef_indices = np.arange(len(start_times)) - interpolator = interp1d(start_times, clef_indices, - kind = "previous", - bounds_error=False, fill_value=0) - clef_dict[interpolator_key].append(interpolator) - - W = np.zeros((len(notes), 3)) - - for i, n in enumerate(notes): - staff = n.staff or 1 - time = n.start.t - clef_key = "clef_"+str(staff) - interpolator_key = "interp_"+str(staff) - clef_idx = clef_dict[interpolator_key][0](time) - clef = clef_dict[clef_key][int(clef_idx)] - sign = clef.sign or "G" - W[i,0] = numerical_clef_dict[sign] - W[i,1] = clef.line or 2 - W[i,2] = clef.octave_change or 0 + else: + # add dummy clef + W = np.zeros((len(notes), 3)) + W[:,0] = 6 # "none" + W[:,1] = 0 + W[:,2] = 0 return W, names From 9c23a9ebf20399a2aa40126a28ea7a166c22fe8d Mon Sep 17 00:00:00 2001 From: sildater Date: Wed, 2 Oct 2024 10:42:23 +0200 Subject: [PATCH 070/151] extrapolate --- partitura/musicanalysis/note_features.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 379e0d52..66894dd8 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -563,7 +563,8 @@ def clef_feature(na, part, **kwargs): clef_indices = np.arange(len(start_times)) interpolator = interp1d(start_times, clef_indices, kind = "previous", - bounds_error=False, fill_value=0) + bounds_error=False, + fill_value="extrapolate") clef_dict[interpolator_key].append(interpolator) W = np.zeros((len(notes), 3)) From c159bc99b4589c37eb4558c2f03fdaee8ed96027 Mon Sep 17 00:00:00 2001 From: Emmanouil Karystinaios Date: Wed, 2 Oct 2024 14:40:35 +0200 Subject: [PATCH 071/151] Update globals.py added comment for global straght_durs. --- partitura/utils/globals.py | 1 + 1 file changed, 1 insertion(+) diff --git a/partitura/utils/globals.py b/partitura/utils/globals.py index 8571e025..ad8e17dc 100644 --- a/partitura/utils/globals.py +++ b/partitura/utils/globals.py @@ -192,6 +192,7 @@ {"type": "long", "dots": 3}, ] +# Straight durs do not include copies for naming or dots, when searching they work better for base triplet types in `estimate_symbolic_duration`. STRAIGHT_DURS = np.array( [ 4 / 256, 4 / 128, 4 / 64, 4 / 32, 4 / 16, 4 / 8, 4 / 4, 4 / 2, 4 / 1, 4 / 0.5, 4 / 0.25] ) From 14d2d777b35443a3f5fe54f82dde892068e70961 Mon Sep 17 00:00:00 2001 From: huispaty Date: Wed, 2 Oct 2024 14:43:42 +0200 Subject: [PATCH 072/151] fix for notes with reonset during sustain pedal on --- partitura/performance.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/partitura/performance.py b/partitura/performance.py index 8957491b..6fc1d6eb 100644 --- a/partitura/performance.py +++ b/partitura/performance.py @@ -178,7 +178,8 @@ def note_array(self, *args, **kwargs) -> np.ndarray: duration_sec = offset - note_on_sec duration_tick = ( n.get( - "note_off_tick", + seconds_to_midi_ticks( + n["sound_off"], mpq=self.mpq, ppq=self.ppq), seconds_to_midi_ticks(n["note_off"], mpq=self.mpq, ppq=self.ppq), ) - note_on_tick @@ -271,7 +272,6 @@ def adjust_offsets_w_sustain( pedal = pedal[np.argsort(pedal[:, 0]), :] # reduce the pedal info to just the times where there is a change in pedal state - pedal = np.vstack( ( (min(pedal[0, 0] - 1, first_off - 1), 0), @@ -289,7 +289,23 @@ def adjust_offsets_w_sustain( next_pedal_time = pedal[last_pedal_change_before_off + 1, 0] offs[pedal_down_at_off] = next_pedal_time[pedal_down_at_off] + + # adjust offset times of notes that have a reonset while the sustain pedal is on + pitches = np.array([n["midi_pitch"] for n in notes]) + note_ons = np.array([n["note_on"] for n in notes]) + + for pitch in np.unique(pitches): + pitch_indices = np.where(pitches == pitch)[0] + + sorted_indices = pitch_indices[np.argsort(note_ons[pitch_indices])] + sorted_note_ons = note_ons[sorted_indices] + sorted_sound_offs = offs[sorted_indices] + + adjusted_sound_offs = np.minimum( + sorted_sound_offs[:-1], sorted_note_ons[1:]) + offs[sorted_indices[:-1]] = adjusted_sound_offs + for offset, note in zip(offs, notes): note["sound_off"] = offset From 00f632991076855f9f884d1856fdd4787e194a05 Mon Sep 17 00:00:00 2001 From: manoskary Date: Wed, 2 Oct 2024 12:54:08 +0000 Subject: [PATCH 073/151] Format code with black (bot) --- partitura/utils/globals.py | 165 +++++++++++++++++++++++++++++-------- partitura/utils/music.py | 16 ++-- 2 files changed, 142 insertions(+), 39 deletions(-) diff --git a/partitura/utils/globals.py b/partitura/utils/globals.py index ad8e17dc..116ed82a 100644 --- a/partitura/utils/globals.py +++ b/partitura/utils/globals.py @@ -194,7 +194,19 @@ # Straight durs do not include copies for naming or dots, when searching they work better for base triplet types in `estimate_symbolic_duration`. STRAIGHT_DURS = np.array( - [ 4 / 256, 4 / 128, 4 / 64, 4 / 32, 4 / 16, 4 / 8, 4 / 4, 4 / 2, 4 / 1, 4 / 0.5, 4 / 0.25] + [ + 4 / 256, + 4 / 128, + 4 / 64, + 4 / 32, + 4 / 16, + 4 / 8, + 4 / 4, + 4 / 2, + 4 / 1, + 4 / 0.5, + 4 / 0.25, + ] ) SYM_STRAIGHT_DURS = [ @@ -302,11 +314,11 @@ COMPOSITE_DURS = np.array( [ - 1/4 + 1/6, - 1/2 + 1/12, - 1/2 + 1/3, - 1/2 + 1/4 + 1/6, - 1 + 1/12, + 1 / 4 + 1 / 6, + 1 / 2 + 1 / 12, + 1 / 2 + 1 / 3, + 1 / 2 + 1 / 4 + 1 / 6, + 1 + 1 / 12, 1 + 1 / 8, 1 + 1 / 6, 1 + 1 / 4, @@ -341,41 +353,128 @@ ) SYM_COMPOSITE_DURS = [ - ({"type": "16th", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "eighth", "dots": 0}, {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "eighth", "dots": 0}, {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "eighth", "dots": 1}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "quarter", "dots": 0}, {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ( + {"type": "16th", "dots": 0}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "eighth", "dots": 0}, + {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "eighth", "dots": 0}, + {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "eighth", "dots": 1}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "quarter", "dots": 0}, + {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), ({"type": "quarter", "dots": 0}, {"type": "32nd", "dots": 0}), - ({"type": "quarter", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ( + {"type": "quarter", "dots": 0}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), ({"type": "quarter", "dots": 0}, {"type": "16th", "dots": 0}), - ({"type": "quarter", "dots": 0}, {"type": "16th", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "quarter", "dots": 1}, {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "quarter", "dots": 1}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "quarter", "dots": 1}, {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "quarter", "dots": 2}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "half", "dots": 0}, {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ( + {"type": "quarter", "dots": 0}, + {"type": "16th", "dots": 0}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "quarter", "dots": 1}, + {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "quarter", "dots": 1}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "quarter", "dots": 1}, + {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "quarter", "dots": 2}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 0}, + {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), ({"type": "half", "dots": 0}, {"type": "32nd", "dots": 0}), - ({"type": "half", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ( + {"type": "half", "dots": 0}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), ({"type": "half", "dots": 0}, {"type": "16th", "dots": 0}), - ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "half", "dots": 0}, {"type": "16th", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ( + {"type": "half", "dots": 0}, + {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 0}, + {"type": "16th", "dots": 0}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0}), - ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0}, {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "half", "dots": 0}, {"type": "quarter", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ( + {"type": "half", "dots": 0}, + {"type": "eighth", "dots": 0}, + {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 0}, + {"type": "quarter", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 1}), - ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0}, {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 1}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "half", "dots": 1}, {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ( + {"type": "half", "dots": 0}, + {"type": "eighth", "dots": 0}, + {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 0}, + {"type": "eighth", "dots": 1}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 1}, + {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), ({"type": "half", "dots": 1}, {"type": "32nd", "dots": 0}), - ({"type": "half", "dots": 1}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ( + {"type": "half", "dots": 1}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), ({"type": "half", "dots": 1}, {"type": "16th", "dots": 0}), - ({"type": "half", "dots": 1}, {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "half", "dots": 1}, {"type": "16th", "dots": 0}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "half", "dots": 2}, {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "half", "dots": 1}, {"type": "quarter", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "half", "dots": 2}, {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}), - ({"type": "half", "dots": 3}, {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}), + ( + {"type": "half", "dots": 1}, + {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 1}, + {"type": "16th", "dots": 0}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 2}, + {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 1}, + {"type": "quarter", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 2}, + {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 3}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), ] diff --git a/partitura/utils/music.py b/partitura/utils/music.py index dccc4192..d72b1e4c 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -766,24 +766,28 @@ def estimate_symbolic_duration( if return_com_durations: return copy.copy(SYM_COMPOSITE_DURS[j]) else: - warnings.warn(f"Quarter duration {qdur} from {dur}/{div} is a composite" - f"duration but composite durations are not allowed. Returning empty symbolic duration.") + warnings.warn( + f"Quarter duration {qdur} from {dur}/{div} is a composite" + f"duration but composite durations are not allowed. Returning empty symbolic duration." + ) return {} # Naive condition to only apply tuplet estimation if the quarter duration is less than a bar (4) elif qdur > 4: - warnings.warn(f"Quarter duration {qdur} from {dur}/{div} is not a tuplet or composite duration." - f"Returning empty symbolic duration.") + warnings.warn( + f"Quarter duration {qdur} from {dur}/{div} is not a tuplet or composite duration." + f"Returning empty symbolic duration." + ) return {} else: i = np.searchsorted(STRAIGHT_DURS, qdur, side="left") - 1 # NOTE: Guess tuplets (Naive) it doesn't cover composite durations from tied notes. - type = SYM_STRAIGHT_DURS[i+1]["type"] + type = SYM_STRAIGHT_DURS[i + 1]["type"] normal_notes = 2 while (normal_notes * STRAIGHT_DURS[i + 1] / qdur) % 1 > eps: normal_notes += 1 return { "type": type, - "actual_notes": math.ceil(normal_notes * STRAIGHT_DURS[i+1] / qdur), + "actual_notes": math.ceil(normal_notes * STRAIGHT_DURS[i + 1] / qdur), "normal_notes": normal_notes, } From b61d8bf90adf240fd666bf9b81fcb598109dd079 Mon Sep 17 00:00:00 2001 From: huispaty Date: Wed, 2 Oct 2024 15:04:04 +0200 Subject: [PATCH 074/151] updated performance features test --- tests/test_performance_features.py | 48 ++++++++++++++++-------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/tests/test_performance_features.py b/tests/test_performance_features.py index e0c4723a..566ba105 100644 --- a/tests/test_performance_features.py +++ b/tests/test_performance_features.py @@ -11,32 +11,36 @@ import os - class TestPerformanceFeatures(unittest.TestCase): def test_performance_features(self): - fields = ['id','pedal_feature.onset_value','pedal_feature.offset_value','pedal_feature.to_prev_release', - 'pedal_feature.to_next_release','onset','duration', 'pitch', 'p_onset', 'p_duration','velocity', 'beat_period'] - True_array = np.array([('n1', 0.23374297, 89.74999 , 62.000057, 0., 0.16015087, -0.5, 0.5 , 59, 4.9925 , 0.8775 , 44, 1.4700003), - ('n4', 0.03011051, 114.25004 , 61.000244, 0., 0.4027142 , 0. , 1. , 40, 5.7025 , 2.4375 , 22, 2.8474998), - ('n3', 2.527984 , 87.500046, 61.000244, 0., 0.4027142 , 0. , 0.25, 56, 5.77625, 2.36375, 26, 2.8474998)], - dtype=[('id', ' Date: Wed, 2 Oct 2024 15:15:24 +0200 Subject: [PATCH 075/151] Update importkern.py --- partitura/io/importkern.py | 1 + 1 file changed, 1 insertion(+) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index 783f7939..17b32407 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -368,6 +368,7 @@ def load_kern( ) doc_name = get_document_name(filename) + # inversing the partlist results to correct part order and visualization for exporting musicxml files score = spt.Score(partlist=partlist[::-1], id=doc_name) return score From ee8dfd35ba5e62f758a8e086f1df9ffdc8f8ec0f Mon Sep 17 00:00:00 2001 From: Emmanouil Karystinaios Date: Wed, 2 Oct 2024 15:16:33 +0200 Subject: [PATCH 076/151] Update importkern.py removing unused comments. --- partitura/io/importkern.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index 17b32407..2768d0c4 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -266,9 +266,6 @@ def load_kern( ) # Get Splines splines = file[1:].T[note_parts] - # Inverse Order - # splines = splines[::-1] - # parsing_idxs = parsing_idxs[::-1] prev_staff = 1 has_instrument = np.char.startswith(splines, "*I") # if all parts have the same instrument, then they are the same part. From f424010ba2afa66c7a3d149410d14caa500d2aa4 Mon Sep 17 00:00:00 2001 From: manoskary Date: Wed, 2 Oct 2024 13:25:38 +0000 Subject: [PATCH 077/151] Format code with black (bot) --- partitura/io/importkern.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index 2768d0c4..e853840f 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -69,6 +69,7 @@ "256": {"type": "256th"}, } + class KernElement(object): def __init__(self, element): self.editorial_start = True if "ossia" in element else False @@ -339,7 +340,6 @@ def load_kern( for part in copy_partlist: part.set_quarter_duration(0, divs_pq) - for part, elements, total_duration_values, same_part in zip( copy_partlist, elements_list, total_durations_list, part_assignments ): @@ -521,7 +521,6 @@ def meta_tandem_line(self, line: str): else: return KernElement(element=line) - def process_tempo_line(self, line: str): return spt.Tempo(float(line)) From 90ae765b06a65d7a6236cdfb59e1112c0dcd1dca Mon Sep 17 00:00:00 2001 From: leleogere Date: Thu, 3 Oct 2024 16:04:54 +0200 Subject: [PATCH 078/151] Add clef_map --- partitura/score.py | 72 ++++++++++++++++++++++++++++++++++++- partitura/utils/__init__.py | 4 +++ partitura/utils/globals.py | 12 +++++++ partitura/utils/music.py | 6 ++++ 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/partitura/score.py b/partitura/score.py index e882151a..35f85801 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -8,7 +8,7 @@ object). This object serves as a timeline at which musical elements are registered in terms of their start and end times. """ - +from collections.abc import Callable from copy import copy, deepcopy from collections import defaultdict from collections.abc import Iterable @@ -46,6 +46,7 @@ key_mode_to_int, _OrderedSet, update_note_ids_after_unfolding, + clef_sign_to_int, ) from partitura.utils.generic import interp1d from partitura.utils.music import transpose_note, step2pc @@ -229,6 +230,75 @@ def key_signature_map(self): fill_value="extrapolate", ) + @property + def clef_map(self) -> Callable[int, np.ndarray]: + """A function mapping timeline times to the clef in each + staff at that time. The function can take scalar + values or lists/arrays of values + + Returns + ------- + function + The mapping function + """ + clefs = np.array( + [ + ( + c.start.t, + c.staff, + clef_sign_to_int(c.sign), + c.line, + c.octave_change if c.octave_change is not None else 0 + ) + for c in self.iter_all(Clef) + ] + ) + + interpolators = [] + for s in range(1, self.number_of_staves + 1): + staff_clefs = clefs[clefs[:, 1] == s] + if len(staff_clefs) == 0: + # default treble clef + staff, clef, line, octave_change = s, clef_sign_to_int("G"), 2, 0 + + warnings.warn( + "No clefs found on staff {}, assuming {} clef.".format(s, clef) + ) + if self.first_point is None: + t0, tN = 0, 0 + else: + t0 = self.first_point.t + tN = self.last_point.t + staff_clefs = np.array( + [ + (t0, staff, clef, line, octave_change), + (tN, staff, clef, line, octave_change), + ] + ) + elif len(staff_clefs) == 1: + # If there is only a single clef + staff_clefs = np.array([staff_clefs[0, :], staff_clefs[0, :]]) + elif staff_clefs[0, 0] > self.first_point.t: + staff_clefs = np.vstack( + ((self.first_point.t, *staff_clefs[0, 1:]), staff_clefs) + ) + + interpolators.append( + interp1d( + staff_clefs[:, 0], + staff_clefs[:, 1:], + axis=0, + kind="previous", + bounds_error=False, + fill_value="extrapolate", + ) + ) + + def collator(time: int) -> np.ndarray: + return np.array([interpolator(time) for interpolator in interpolators], dtype=int) + + return collator + @property def measure_map(self): """A function mapping timeline times to the start and end of diff --git a/partitura/utils/__init__.py b/partitura/utils/__init__.py index 97f07142..b9605316 100644 --- a/partitura/utils/__init__.py +++ b/partitura/utils/__init__.py @@ -37,6 +37,8 @@ pianoroll_to_notearray, match_note_arrays, key_mode_to_int, + clef_sign_to_int, + clef_int_to_sign, remove_silence_from_performed_part, note_array_from_part_list, slice_notearray_by_time, @@ -74,6 +76,8 @@ "key_name_to_fifths_mode", "fifths_mode_to_key_name", "key_mode_to_int", + "clef_sign_to_int", + "clef_int_to_sign", "pitch_spelling_to_midi_pitch", "pitch_spelling_to_note_name", "show_diff", diff --git a/partitura/utils/globals.py b/partitura/utils/globals.py index 3739e0d4..15c95361 100644 --- a/partitura/utils/globals.py +++ b/partitura/utils/globals.py @@ -543,3 +543,15 @@ "vii": (7, "M"), }, } + +#["G", "F", "C", "percussion", "TAB", "jianpu", "none"] +CLEF_TO_INT = { + "G": 0, + "F": 1, + "C": 2, + "percussion": 3, + "TAB": 4, + "jianpu": 5, + "none": 6, +} +INT_TO_CLEF = {v: k for k, v in CLEF_TO_INT.items()} diff --git a/partitura/utils/music.py b/partitura/utils/music.py index f9678228..b6be09b8 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -700,6 +700,12 @@ def key_int_to_mode(mode): raise ValueError("Unknown mode {}".format(mode)) +def clef_sign_to_int(clef_sign: str) -> int: + return CLEF_TO_INT[clef_sign] + +def clef_int_to_sign(clef_int: int) -> str: + return INT_TO_CLEF[clef_int] + def estimate_symbolic_duration( dur, div, eps=10**-3, return_com_durations=False ) -> Union[Dict[str, Any], Tuple[Dict[str, Any]]]: From 7d2623eed537fcd3e64c32707539a5abc25cc238 Mon Sep 17 00:00:00 2001 From: leleogere Date: Thu, 3 Oct 2024 16:21:35 +0200 Subject: [PATCH 079/151] Fix a typing issue --- partitura/score.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/score.py b/partitura/score.py index 35f85801..6f3d2962 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -231,7 +231,7 @@ def key_signature_map(self): ) @property - def clef_map(self) -> Callable[int, np.ndarray]: + def clef_map(self) -> Callable[[int], np.ndarray]: """A function mapping timeline times to the clef in each staff at that time. The function can take scalar values or lists/arrays of values From b585213a5c3b3b3018bf02c60f4a0d0e2248514a Mon Sep 17 00:00:00 2001 From: leleogere Date: Thu, 3 Oct 2024 17:00:25 +0200 Subject: [PATCH 080/151] Add a test --- tests/__init__.py | 5 + tests/data/musicxml/test_clef_map.musicxml | 301 +++++++++++++++++++++ tests/test_metrical_position.py | 30 ++ 3 files changed, 336 insertions(+) create mode 100644 tests/data/musicxml/test_clef_map.musicxml diff --git a/tests/__init__.py b/tests/__init__.py index 32824bcc..9e574b41 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -112,6 +112,11 @@ for fn in ["test_ts_map_ts_starts_not_at_zero.xml"] ] +CLEF_MAP_TESTFILES = [ + os.path.join(MUSICXML_PATH, fn) + for fn in ["test_clef_map.musicxml"] +] + REST_ARRAY_TESTFILES = [ os.path.join(MUSICXML_PATH, fn) for fn in ["test_unfold_complex.xml", "test_rest.musicxml"] diff --git a/tests/data/musicxml/test_clef_map.musicxml b/tests/data/musicxml/test_clef_map.musicxml new file mode 100644 index 00000000..dd785afc --- /dev/null +++ b/tests/data/musicxml/test_clef_map.musicxml @@ -0,0 +1,301 @@ + + + + + Partition sans titre + + + Compositeur / Arrangeur + + MuseScore 4.4.2 + 2024-10-03 + + + + + + + + + + Piano + Pno. + + Piano + keyboard.piano + + + + 1 + 1 + 78.7402 + 0 + + + + + + + 2 + + 0 + + + 2 + + G + 2 + + + F + 4 + + + + + C + 4 + + 2 + 1 + quarter + up + 1 + + + + G + 2 + -2 + + + + + C + 2 + + 2 + 1 + quarter + up + 1 + + + + C + 2 + + 2 + 1 + quarter + up + 1 + + + + C + 2 + + 2 + 1 + quarter + up + 1 + + + 8 + + + + C + 3 + + 2 + 5 + quarter + up + 2 + + + + C + 3 + + 2 + 5 + quarter + up + 2 + + + + G + 2 + + + + + A + 4 + + 2 + 5 + quarter + up + 2 + + + + A + 4 + + 2 + 5 + quarter + up + 2 + + + + + + G + 2 + 1 + + + F + 4 + + + + + C + 5 + + 2 + 1 + quarter + up + 1 + + + + C + 3 + + + + + D + 3 + + 2 + 1 + quarter + up + 1 + + + + D + 3 + + 1 + 1 + eighth + down + 1 + begin + + + + C + 4 + + + + + B + 2 + + 1 + 1 + eighth + down + 1 + end + + + + B + 2 + + 2 + 1 + quarter + up + 1 + + + 8 + + + + C + 3 + + 2 + 5 + quarter + up + 2 + + + + C + 3 + + 2 + 5 + quarter + up + 2 + + + + F + 3 + + + + + E + 3 + + 2 + 5 + quarter + up + 2 + + + + E + 3 + + 2 + 5 + quarter + up + 2 + + + light-heavy + + + + diff --git a/tests/test_metrical_position.py b/tests/test_metrical_position.py index 9d2612fd..b2e1b29e 100644 --- a/tests/test_metrical_position.py +++ b/tests/test_metrical_position.py @@ -14,6 +14,7 @@ from tests import ( METRICAL_POSITION_TESTFILES, TIME_SIGNATURE_MAP_EDGECASES_TESTFILES, + CLEF_MAP_TESTFILES, ) @@ -133,5 +134,34 @@ def test_time_signature_map(self): ) +class TestClefMap(unittest.TestCase): + def test_clef_map(self): + score = load_musicxml(CLEF_MAP_TESTFILES[0]) + for part in score: + # clef = (staff_no, sign_shape, line, octave_shift) + map_fn = part.clef_map + self.assertTrue( + np.all(map_fn(part.first_point.t) == np.array([[1, 0, 2, 0], [2, 1, 4, 0]])) # treble / bass + ) + self.assertTrue( + np.all(map_fn(7) == np.array([[1, 0, 2, -2], [2, 0, 2, 0]])) # treble15vb / treble + ) + self.assertTrue( + np.all(map_fn(8) == np.array([[1, 0, 2, 1], [2, 1, 4, 0]])) # treble8va / bass + ) + self.assertTrue( + np.all(map_fn(11) == np.array([[1, 2, 3, 0], [2, 1, 4, 0]])) # ut3 / bass + ) + self.assertTrue( + np.all(map_fn(12) == np.array([[1, 2, 3, 0], [2, 1, 3, 0]])) # ut3 / bass3 + ) + self.assertTrue( + np.all(map_fn(13) == np.array([[1, 2, 4, 0], [2, 1, 3, 0]])) # ut4 / bass3 + ) + self.assertTrue( + np.all(map_fn(part.last_point.t) == np.array([[1, 2, 4, 0], [2, 1, 3, 0]])) # ut4 / bass3 + ) + + if __name__ == "__main__": unittest.main() From b2a8f5daa99cbcbf7db79028761518fae81335a3 Mon Sep 17 00:00:00 2001 From: sildater Date: Fri, 4 Oct 2024 18:33:36 +0200 Subject: [PATCH 081/151] add test file --- tests/data/musicxml/test_clef.musicxml | 343 +++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 tests/data/musicxml/test_clef.musicxml diff --git a/tests/data/musicxml/test_clef.musicxml b/tests/data/musicxml/test_clef.musicxml new file mode 100644 index 00000000..b17cf157 --- /dev/null +++ b/tests/data/musicxml/test_clef.musicxml @@ -0,0 +1,343 @@ + + + + + Clef Test + + + Composer / arranger + + MuseScore 4.4.2 + 2024-10-04 + + + + + + + + + + Piano 1 + Pno. 1 + + Piano + keyboard.piano + + + + 1 + 1 + 78.7402 + 0 + + + + Piano 2 + Pno. 2 + + Piano + keyboard.piano + + + + 2 + 1 + 78.7402 + 0 + + + + + + + 1 + + 0 + + + 2 + + G + 2 + + + F + 4 + + + + + G + 4 + + 4 + 1 + whole + 1 + + + 4 + + + + B + 2 + + 4 + 5 + whole + 2 + + + + + + G + 2 + 1 + + + C + 3 + + + + + G + 5 + + 2 + 1 + half + up + 1 + + + + G + 5 + + 2 + 1 + half + up + 1 + + + 4 + + + + A + 3 + + 4 + 5 + whole + 2 + + + + + + G + 2 + -1 + + + C + 4 + + + + + G + 3 + + 2 + 1 + half + up + 1 + + + + G + 2 + + + + + G + 4 + + 2 + 1 + half + up + 1 + + + 4 + + + + F + 3 + + 4 + 5 + whole + 2 + + + + + + F + 4 + + + + + G + 4 + + 4 + 1 + whole + 1 + + + 4 + + + + B + 2 + + 4 + 5 + whole + 2 + + + light-heavy + + + + + + + 1 + + 0 + + + 2 + + G + 2 + + + G + 2 + + + + + 4 + 1 + 1 + + + 4 + + + + 4 + 5 + 2 + + + + + + 4 + 1 + 1 + + + 4 + + + + 4 + 5 + 2 + + + + + + F + 4 + + + G + 2 + + + + + B + 2 + + 4 + 1 + whole + 1 + + + 4 + + + + G + 4 + + 4 + 5 + whole + 2 + + + + + + 4 + 1 + 1 + + + 4 + + + + 4 + 5 + 2 + + + light-heavy + + + + From 446340c4a27a2fb44ae5090e9b97669997814023 Mon Sep 17 00:00:00 2001 From: sildater Date: Fri, 4 Oct 2024 18:34:42 +0200 Subject: [PATCH 082/151] remove initial clefs --- tests/data/musicxml/test_clef.musicxml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/data/musicxml/test_clef.musicxml b/tests/data/musicxml/test_clef.musicxml index b17cf157..f17890c1 100644 --- a/tests/data/musicxml/test_clef.musicxml +++ b/tests/data/musicxml/test_clef.musicxml @@ -242,14 +242,6 @@ 4 2 - - G - 2 - - - G - 2 - From a59cf1dbd2c46e2d9c450279926bea74029de8f3 Mon Sep 17 00:00:00 2001 From: sildater Date: Fri, 4 Oct 2024 18:38:09 +0200 Subject: [PATCH 083/151] tidy test files wip --- .../midi}/test_basic_midi.mid | Bin .../Three-Part_Invention_No_13_(fragment).xml | 0 .../musicxml}/test_basic_midi.musicxml | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename tests/{data_examples => data/midi}/test_basic_midi.mid (100%) rename tests/{data_examples => data/musicxml}/Three-Part_Invention_No_13_(fragment).xml (100%) rename tests/{data_examples => data/musicxml}/test_basic_midi.musicxml (100%) diff --git a/tests/data_examples/test_basic_midi.mid b/tests/data/midi/test_basic_midi.mid similarity index 100% rename from tests/data_examples/test_basic_midi.mid rename to tests/data/midi/test_basic_midi.mid diff --git a/tests/data_examples/Three-Part_Invention_No_13_(fragment).xml b/tests/data/musicxml/Three-Part_Invention_No_13_(fragment).xml similarity index 100% rename from tests/data_examples/Three-Part_Invention_No_13_(fragment).xml rename to tests/data/musicxml/Three-Part_Invention_No_13_(fragment).xml diff --git a/tests/data_examples/test_basic_midi.musicxml b/tests/data/musicxml/test_basic_midi.musicxml similarity index 100% rename from tests/data_examples/test_basic_midi.musicxml rename to tests/data/musicxml/test_basic_midi.musicxml From 05e955b0f937d518d5a8871cc689cf6a054251d8 Mon Sep 17 00:00:00 2001 From: sildater Date: Fri, 4 Oct 2024 19:02:28 +0200 Subject: [PATCH 084/151] test setup wip --- tests/__init__.py | 9 +++++++-- tests/test_clef.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 tests/test_clef.py diff --git a/tests/__init__.py b/tests/__init__.py index 32824bcc..3ed54288 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -196,6 +196,7 @@ ] KERN_TIES = [os.path.join(KERN_PATH, fn) for fn in ["tie_mismatch.krn"]] + M21_TESTFILES = [ os.path.join(DATA_PATH, "musicxml", fn) for fn in [ @@ -205,6 +206,7 @@ "test_note_ties.xml", ] ] + HARMONY_TESTFILES = [os.path.join(MUSICXML_PATH, fn) for fn in ["test_harmony.musicxml"]] MOZART_VARIATION_FILES = dict( @@ -220,7 +222,6 @@ parangonada_zalign=os.path.join(PARANGONADA_PATH, "mozart_k265_var1", "zalign.csv"), ) - WAV_TESTFILES = [ os.path.join(WAV_PATH, fn) for fn in [ @@ -245,4 +246,8 @@ MIDIINPORT_TESTFILES = [ os.path.join(DATA_PATH, "midi", "bach_midi_score.mid") -] \ No newline at end of file +] + +CLEF_TESTFILES = [ + os.path.join(DATA_PATH, "musicxml", "test_clef.musicxml") +] diff --git a/tests/test_clef.py b/tests/test_clef.py new file mode 100644 index 00000000..3ccced5c --- /dev/null +++ b/tests/test_clef.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains tests for clef related methods. +""" +import unittest +from tests import ( + CLEF_TESTFILES, +) +from partitura import load_musicxml +from partitura.musicanalysis import compute_note_array From 340b6f99196ce4131b840202e98165423e2784f0 Mon Sep 17 00:00:00 2001 From: sildater Date: Fri, 4 Oct 2024 19:06:51 +0200 Subject: [PATCH 085/151] test case extraction wip --- tests/test_clef.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/test_clef.py b/tests/test_clef.py index 3ccced5c..35944744 100644 --- a/tests/test_clef.py +++ b/tests/test_clef.py @@ -5,7 +5,18 @@ """ import unittest from tests import ( - CLEF_TESTFILES, + CLEF_TESTFILES ) from partitura import load_musicxml from partitura.musicanalysis import compute_note_array +from partitura.musicanalysis.note_features import clef_feature + +class TestingClefFeatureExtraction(unittest.TestCase): + def test_feature_exctraction(self): + for fn in CLEF_TESTFILES: + score = load_musicxml(fn) + sna1 = compute_note_array(score.parts[0], + feature_functions="clef_feature") + sna2 = compute_note_array(score.parts[1], + feature_functions="clef_feature") + From e9d3845f451cb41dfaa1e16665ca904edcb28e07 Mon Sep 17 00:00:00 2001 From: sildater Date: Fri, 4 Oct 2024 19:10:20 +0200 Subject: [PATCH 086/151] another try --- tests/test_clef.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_clef.py b/tests/test_clef.py index 35944744..c61e83fc 100644 --- a/tests/test_clef.py +++ b/tests/test_clef.py @@ -16,7 +16,7 @@ def test_feature_exctraction(self): for fn in CLEF_TESTFILES: score = load_musicxml(fn) sna1 = compute_note_array(score.parts[0], - feature_functions="clef_feature") + feature_functions=["clef_feature"]) sna2 = compute_note_array(score.parts[1], - feature_functions="clef_feature") + feature_functions=["clef_feature"]) From 1ddc2595854d9a490dd74f0f70fba26eb7f6b93b Mon Sep 17 00:00:00 2001 From: fosfrancesco Date: Mon, 7 Oct 2024 12:19:08 +0000 Subject: [PATCH 087/151] Format code with black (bot) --- partitura/io/importmatch.py | 79 +++++++++++-------- .../musicanalysis/note_array_to_score.py | 2 +- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index b1b34722..a32cb812 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -44,21 +44,13 @@ format_pnote_id, ) -from partitura.utils.music import ( - midi_ticks_to_seconds -) +from partitura.utils.music import midi_ticks_to_seconds -from partitura.utils.misc import ( - deprecated_alias, - PathLike, - get_document_name -) +from partitura.utils.misc import deprecated_alias, PathLike, get_document_name + +from partitura.utils.generic import interp1d, iter_current_next -from partitura.utils.generic import ( - interp1d, - iter_current_next -) __all__ = ["load_match"] @@ -463,9 +455,14 @@ def part_from_matchfile( ts = mf.time_signatures min_time = snotes[0].OnsetInBeats # sorted by OnsetInBeats max_time = max(n.OffsetInBeats for n in snotes) - beats_map_from_beats, beats_map, beat_type_map_from_beats, beat_type_map, min_time_q, max_time_q = make_timesig_maps( - ts, max_time - ) + ( + beats_map_from_beats, + beats_map, + beat_type_map_from_beats, + beat_type_map, + min_time_q, + max_time_q, + ) = make_timesig_maps(ts, max_time) # compute necessary divs based on the types of notes in the # match snotes (only integers) @@ -518,10 +515,16 @@ def part_from_matchfile( t = t - t % beats_map(min_time) for b_name in bars: - notes_in_this_bar = [(ni, n) for ni, n in enumerate(snotes) if n.Measure == b_name] + notes_in_this_bar = [ + (ni, n) for ni, n in enumerate(snotes) if n.Measure == b_name + ] a_note_in_this_bar = notes_in_this_bar[0][1] a_note_id_in_this_bar = notes_in_this_bar[0][0] - bar_offset = (a_note_in_this_bar.Beat - 1) * 4 / beat_type_map_from_beats(a_note_in_this_bar.OnsetInBeats) + bar_offset = ( + (a_note_in_this_bar.Beat - 1) + * 4 + / beat_type_map_from_beats(a_note_in_this_bar.OnsetInBeats) + ) on_off_scale = 1 if not match_offset_duration_in_whole: on_off_scale = beat_type_map_from_beats(a_note_in_this_bar.OnsetInBeats) @@ -529,13 +532,17 @@ def part_from_matchfile( 4 / on_off_scale * a_note_in_this_bar.Offset.numerator - / (a_note_in_this_bar.Offset.denominator * (a_note_in_this_bar.Offset.tuple_div or 1)) + / ( + a_note_in_this_bar.Offset.denominator + * (a_note_in_this_bar.Offset.tuple_div or 1) + ) ) - barline_in_quarters = onset_in_quarters[a_note_id_in_this_bar] - bar_offset - beat_offset + barline_in_quarters = ( + onset_in_quarters[a_note_id_in_this_bar] - bar_offset - beat_offset + ) bar_times[b_name] = barline_in_quarters - for ni, note in enumerate(snotes): # start of bar in quarter units bar_start = bar_times[note.Measure] @@ -727,12 +734,18 @@ def part_from_matchfile( barline_in_divs = 0 if prev_measure is not None: part.add(prev_measure, None, barline_in_divs) - prev_measure = score.Measure(number=measure_counter + 1, name = str(measure_name)) + prev_measure = score.Measure(number=measure_counter + 1, name=str(measure_name)) part.add(prev_measure, barline_in_divs) - last_closing_barline = barline_in_divs + int(round(divs * beats_map(barline_in_quarters) * 4 / beat_type_map(barline_in_quarters))) + last_closing_barline = barline_in_divs + int( + round( + divs + * beats_map(barline_in_quarters) + * 4 + / beat_type_map(barline_in_quarters) + ) + ) part.add(prev_measure, None, last_closing_barline) - # add the rest of the measures automatically score.add_measures(part) score.tie_notes(part) @@ -825,16 +838,16 @@ def make_timesig_maps( def add_staffs(part: Part, split: int = 55, only_missing: bool = True) -> None: """ - Method to add staff information to a part - - Parameters - ---------- - part: Part - Part to add staff information to. - split: int - MIDI pitch to split staff into upper and lower. Default is 55 - only_missing: bool - If True, only add staff to those notes that do not have staff info already. + Method to add staff information to a part + + Parameters + ---------- + part: Part + Part to add staff information to. + split: int + MIDI pitch to split staff into upper and lower. Default is 55 + only_missing: bool + If True, only add staff to those notes that do not have staff info already. """ # assign staffs using a hard limit notes = part.notes_tied diff --git a/partitura/musicanalysis/note_array_to_score.py b/partitura/musicanalysis/note_array_to_score.py index 370bfeec..08d35218 100644 --- a/partitura/musicanalysis/note_array_to_score.py +++ b/partitura/musicanalysis/note_array_to_score.py @@ -190,7 +190,7 @@ def create_part( warnings.warn("add measures", stacklevel=2) if not barebones and anacrusis_divs > 0: - part.add(score.Measure(number = 1, name = str(0)), 0, anacrusis_divs) + part.add(score.Measure(number=1, name=str(0)), 0, anacrusis_divs) if not barebones and sanitize: warnings.warn("Inferring measures", stacklevel=2) From 682f668185bf694649091a08080832f49d916135 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 10 Oct 2024 16:56:55 +0200 Subject: [PATCH 088/151] Relax assertion for tuplets parsing in musicxml Adressed issue #386 --- partitura/io/importmusicxml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index a359fbe3..11df8d40 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -1453,7 +1453,7 @@ def handle_tuplets(notations, ongoing, note): # assert that starting tuplet times are before stopping tuplet times for start_tuplet, stop_tuplet in zip(starting_tuplets, stopping_tuplets): assert ( - start_tuplet.start_note.start.t < stop_tuplet.end_note.start.t + start_tuplet.start_note.start.t <= stop_tuplet.end_note.start.t ), "Tuplet start time is after tuplet stop time" return starting_tuplets, stopping_tuplets From 307f09dc3d748c95c6fb8f6c4b004fc378a560ce Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 10 Oct 2024 16:59:27 +0200 Subject: [PATCH 089/151] Relax conditions for ChordSymbols by accepting empty kind. Sometimes, some instructions in the asap-dataset are pushed like harmony labels, in the future test should ideally filter them out in partitura. --- partitura/score.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/score.py b/partitura/score.py index a08066a1..f8bbdefd 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -3102,7 +3102,7 @@ class ChordSymbol(Harmony): """A harmony element in the score usually for Chord Symbols.""" def __init__(self, root, kind, bass=None): - super().__init__(text=root + kind + (f"/{bass}" if bass else "")) + super().__init__(text=root + (f"/{kind}" if kind else "") + (f"/{bass}" if bass else "")) self.kind = kind self.root = root self.bass = bass From 4c9679d302420cf1d4cbed931f455059fef1f185 Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 14 Oct 2024 11:13:23 +0200 Subject: [PATCH 090/151] clef feature ordered by na --- partitura/musicanalysis/note_features.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 66894dd8..e90de356 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -534,11 +534,12 @@ def clef_feature(na, part, **kwargs): Note that this feature does not return the staff number per note, see staff_feature for this information. """ - notes = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests - numerical_clef_dict ={ + notes = {n.id:n for n in part.notes_tied} + numerical_clef_dict = { 'G':0, 'F':1, 'C':2, 'percussion':3, 'TAB':4, - 'jianpu':5, 'none':6} + 'jianpu':5, 'none':6 + } names = [ "clef_sign", "clef_line", @@ -568,8 +569,8 @@ def clef_feature(na, part, **kwargs): clef_dict[interpolator_key].append(interpolator) W = np.zeros((len(notes), 3)) - - for i, n in enumerate(notes): + for i, na_n in enumerate(na): + n = notes[na_n["id"]] staff = n.staff or 1 time = n.start.t clef_key = "clef_"+str(staff) From d3947ad5abe9c2fbfba1f0f2749e94f1af0df872 Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 14 Oct 2024 11:15:03 +0200 Subject: [PATCH 091/151] articulation feature ordered --- partitura/musicanalysis/note_features.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index e90de356..c807be3a 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -938,9 +938,10 @@ def articulation_feature(na, part, **kwargs): force_size = False feature_by_name = {} - notes = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests + notes = {n.id:n for n in part.notes_tied} N = len(notes) - for i, n in enumerate(notes): + for i, na_n in enumerate(na): + n = notes[na_n["id"]] if n.articulations: for art in n.articulations: if art in names: From 58e9b6af51a3329f626f4a8df1fa1de55f546432 Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 14 Oct 2024 11:15:59 +0200 Subject: [PATCH 092/151] ornament feature ordered --- partitura/musicanalysis/note_features.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index c807be3a..ff0b3cdb 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -996,9 +996,10 @@ def ornament_feature(na, part, **kwargs): "other-ornament", ] feature_by_name = {} - notes = part.notes_tied + notes = {n.id:n for n in part.notes_tied} N = len(notes) - for i, n in enumerate(notes): + for i, na_n in enumerate(na): + n = notes[na_n["id"]] if n.ornaments: for art in n.ornaments: if art in names: From acd88294b47bdc46a172685795bb877032aef4e3 Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 14 Oct 2024 11:21:15 +0200 Subject: [PATCH 093/151] metrical feature ordered --- partitura/musicanalysis/note_features.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index ff0b3cdb..4526b6ed 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -1076,13 +1076,15 @@ def metrical_feature(na, part, **kwargs): non-zero value in the 'metrical_4_4_weak' descriptor. """ - notes = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests + notes_list = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests + notes = {n.id:n for n in notes_list} ts_map = part.time_signature_map bm = part.beat_map feature_by_name = {} eps = 10**-6 - for i, n in enumerate(notes): + for i, na_n in enumerate(na): + n = notes[na_n["id"]] beats, beat_type, mus_beats = ts_map(n.start.t).astype(int) measure = next(n.start.iter_prev(score.Measure, eq=True), None) From d81788ce601f08301bc8fb2914338a4bc650b9df Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 14 Oct 2024 11:35:29 +0200 Subject: [PATCH 094/151] require note ids in tests --- tests/test_clef.py | 2 +- tests/test_note_features.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_clef.py b/tests/test_clef.py index c61e83fc..262c1679 100644 --- a/tests/test_clef.py +++ b/tests/test_clef.py @@ -14,7 +14,7 @@ class TestingClefFeatureExtraction(unittest.TestCase): def test_feature_exctraction(self): for fn in CLEF_TESTFILES: - score = load_musicxml(fn) + score = load_musicxml(fn, force_note_ids = "keep") sna1 = compute_note_array(score.parts[0], feature_functions=["clef_feature"]) sna2 = compute_note_array(score.parts[1], diff --git a/tests/test_note_features.py b/tests/test_note_features.py index 891a254b..859b62b6 100644 --- a/tests/test_note_features.py +++ b/tests/test_note_features.py @@ -18,7 +18,7 @@ class TestingNoteFeatureExtraction(unittest.TestCase): def test_metrical_basis(self): for fn in METRICAL_POSITION_TESTFILES: - score = load_musicxml(fn) + score = load_musicxml(fn, , force_note_ids = "keep") make_note_feats(score[0], ["metrical_feature"]) def test_grace_basis(self): From c77e5db6b302342a5e5abcb23b723f5257bc1a75 Mon Sep 17 00:00:00 2001 From: sildater Date: Mon, 14 Oct 2024 11:38:50 +0200 Subject: [PATCH 095/151] ... --- tests/test_note_features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_note_features.py b/tests/test_note_features.py index 859b62b6..1bafadf3 100644 --- a/tests/test_note_features.py +++ b/tests/test_note_features.py @@ -18,7 +18,7 @@ class TestingNoteFeatureExtraction(unittest.TestCase): def test_metrical_basis(self): for fn in METRICAL_POSITION_TESTFILES: - score = load_musicxml(fn, , force_note_ids = "keep") + score = load_musicxml(fn, force_note_ids = "keep") make_note_feats(score[0], ["metrical_feature"]) def test_grace_basis(self): From b5abc4cd80bb413f3034065eec4dd165a85e5231 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 14 Oct 2024 15:55:10 +0200 Subject: [PATCH 096/151] Added support for stem direction in musicxml import/export --- partitura/io/exportmusicxml.py | 4 ++++ partitura/io/importmusicxml.py | 6 ++++++ partitura/score.py | 8 +++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/partitura/io/exportmusicxml.py b/partitura/io/exportmusicxml.py index 0d93fcc9..824dbcaa 100644 --- a/partitura/io/exportmusicxml.py +++ b/partitura/io/exportmusicxml.py @@ -137,6 +137,10 @@ def make_note_el(note, dur, voice, counter, n_of_staves): if voice not in (None, 0): etree.SubElement(note_e, "voice").text = "{}".format(voice) + if note.stem_direction is not None: + stem_e = etree.SubElement(note_e, "stem") + stem_e.text = note.stem_direction + if note.fermata is not None: notations.append(etree.Element("fermata")) diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index a359fbe3..050c72f6 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -1198,6 +1198,9 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non # initialize beam to None beam = None + # get the stem direction of the note if any + stem_dir = get_value_from_tag(e, "stem", str) or None + # add support of uppercase "ID" tags note_id = ( get_value_from_attribute(e, "id", str) @@ -1270,6 +1273,7 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non ornaments=ornaments, steal_proportion=steal_proportion, doc_order=doc_order, + stem_direction=stem_dir, ) if isinstance(prev_note, score.GraceNote) and prev_note.voice == voice: note.grace_prev = prev_note @@ -1306,6 +1310,7 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non articulations=articulations, ornaments=ornaments, doc_order=doc_order, + stem_direction=stem_dir, ) if isinstance(prev_note, score.GraceNote) and prev_note.voice == voice: @@ -1335,6 +1340,7 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non articulations=articulations, symbolic_duration=symbolic_duration, doc_order=doc_order, + stem_direction=stem_dir, ) else: diff --git a/partitura/score.py b/partitura/score.py index a08066a1..b01833c4 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -1626,6 +1626,9 @@ 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. + stem_direction : str, optional + The stem direction of the note. Can be 'up', 'down', or None. + Defaults to None. """ @@ -1638,10 +1641,13 @@ def __init__( articulations=None, ornaments=None, doc_order=None, + stem_direction=None, **kwargs, ): self._sym_dur = None + super().__init__(**kwargs) + self.voice = voice self.id = id self.staff = staff @@ -1649,7 +1655,7 @@ def __init__( self.articulations = articulations self.ornaments = ornaments self.doc_order = doc_order - + self.stem_direction = stem_direction if stem_direction in ("up", "down") else None # these attributes are set after the instance is constructed self.fermata = None self.tie_prev = None From c85acf95f566f1148926436cc6e511e01be41f79 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 15 Oct 2024 14:28:51 +0200 Subject: [PATCH 097/151] measure feature ordered --- partitura/musicanalysis/note_features.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 92935aca..799d340f 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -1079,7 +1079,8 @@ def measure_feature(na, part, **kwargs): This feature encodes the measure each note is in. """ - notes = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests + notes_list = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests + notes = {n.id:n for n in notes_list} bm = part.beat_map global_start = bm(part.first_point.t) @@ -1093,7 +1094,8 @@ def measure_feature(na, part, **kwargs): ] W = np.zeros((len(notes), 3)) - for i, n in enumerate(notes): + for i, na_n in enumerate(na): + n = notes[na_n["id"]] measure = next(n.start.iter_prev(score.Measure, eq=True), None) if measure: From 115d3a7678fe88375aab05ceea912ccacf4419f5 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 15 Oct 2024 14:46:37 +0200 Subject: [PATCH 098/151] measure test and data --- tests/data/musicxml/test_note_features.xml | 4 ++-- tests/test_note_features.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/data/musicxml/test_note_features.xml b/tests/data/musicxml/test_note_features.xml index 371ed54d..a206da7e 100644 --- a/tests/data/musicxml/test_note_features.xml +++ b/tests/data/musicxml/test_note_features.xml @@ -60,7 +60,7 @@ - + @@ -138,7 +138,7 @@ - + diff --git a/tests/test_note_features.py b/tests/test_note_features.py index 891a254b..b47a2540 100644 --- a/tests/test_note_features.py +++ b/tests/test_note_features.py @@ -59,6 +59,17 @@ def test_slur_grace_art_dyn_orn(self): self.assertTrue(np.all(dyntest), "forte feature does not match") self.assertTrue(np.all(slurtest), "slur feature does not match") + def test_measure_feature(self): + for fn in MUSICXML_NOTE_FEATURES: + score = load_musicxml(fn, force_note_ids=True) + feats = [ + "measure_feature" + ] + na = compute_note_array(score[0], feature_functions=feats) + + + + if __name__ == "__main__": unittest.main() From 5f265a2021091e9ad69ee5dd4cf164a2e081d3a3 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 15 Oct 2024 15:08:26 +0200 Subject: [PATCH 099/151] add measure feature asserts --- tests/test_note_features.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_note_features.py b/tests/test_note_features.py index b47a2540..86fb92bc 100644 --- a/tests/test_note_features.py +++ b/tests/test_note_features.py @@ -67,7 +67,12 @@ def test_measure_feature(self): ] na = compute_note_array(score[0], feature_functions=feats) - + numtest = na["measure_feature.measure_number"] == np.array([1, 1, 1, 2, 2, 2]) + starttest = na["measure_feature.measure_start_beat"] == np.array([0, 0, 0, 4, 4, 4]) + endtest = na["measure_feature.measure_end_beat"] == np.array([4, 4, 4, 8, 8, 8]) + self.assertTrue(np.all(numtest), "measure number feature does not match") + self.assertTrue(np.all(starttest), "measure start feature does not match") + self.assertTrue(np.all(endtest), "measure end feature does not match") From 7ff737ef5e9a78a4bc3598f1f4f751ffa2d6360c Mon Sep 17 00:00:00 2001 From: sildater Date: Thu, 17 Oct 2024 12:14:26 +0200 Subject: [PATCH 100/151] disable cost-based merging of non-note attributes --- partitura/io/exportmusicxml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/partitura/io/exportmusicxml.py b/partitura/io/exportmusicxml.py index 0d93fcc9..ec4b0920 100644 --- a/partitura/io/exportmusicxml.py +++ b/partitura/io/exportmusicxml.py @@ -635,11 +635,11 @@ def merge_measure_contents(notes, other, measure_start): cost[0] = 0 # get the voice for which merging notes and other has lowest cost - merge_voice = sorted(cost.items(), key=itemgetter(1))[0][0] + # merge_voice = sorted(cost.items(), key=itemgetter(1))[0][0] result = [] pos = measure_start for i, voice in enumerate(sorted(notes.keys())): - if voice == merge_voice: + if i == 0: #voice == merge_voice: elements = merged[voice] else: From 2a80fbcaae741f3d64d73d8aebfe0440e0289ba5 Mon Sep 17 00:00:00 2001 From: sildater Date: Thu, 17 Oct 2024 12:22:05 +0200 Subject: [PATCH 101/151] documentation --- partitura/io/exportmusicxml.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/partitura/io/exportmusicxml.py b/partitura/io/exportmusicxml.py index ec4b0920..425d205e 100644 --- a/partitura/io/exportmusicxml.py +++ b/partitura/io/exportmusicxml.py @@ -634,12 +634,20 @@ def merge_measure_contents(notes, other, measure_start): merged[0] = [] cost[0] = 0 + # CHANGE: disabled cost-based merging of non-note elements into stream + # because this led to attributes not being in the beginning of the measure, + # which in turn led to problems with musescore + # fix: add atributes first, then the notes. + # problem: unclear whether this cost-based merging will break anything or + # was just cosmetic to avoid too many forwards and backwards. + # related issue: https://github.com/CPJKU/partitura/issues/390 + # get the voice for which merging notes and other has lowest cost # merge_voice = sorted(cost.items(), key=itemgetter(1))[0][0] result = [] pos = measure_start for i, voice in enumerate(sorted(notes.keys())): - if i == 0: #voice == merge_voice: + if i == 0: # voice == merge_voice: elements = merged[voice] else: From 3f88474709ec98bee16b3a3931bf7cec346e1328 Mon Sep 17 00:00:00 2001 From: sildater Date: Thu, 17 Oct 2024 14:36:31 +0200 Subject: [PATCH 102/151] sign test --- tests/test_clef.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_clef.py b/tests/test_clef.py index 262c1679..3c333fc7 100644 --- a/tests/test_clef.py +++ b/tests/test_clef.py @@ -7,16 +7,22 @@ from tests import ( CLEF_TESTFILES ) +import numpy as np from partitura import load_musicxml from partitura.musicanalysis import compute_note_array from partitura.musicanalysis.note_features import clef_feature class TestingClefFeatureExtraction(unittest.TestCase): - def test_feature_exctraction(self): + def test_clef_feature_exctraction(self): for fn in CLEF_TESTFILES: score = load_musicxml(fn, force_note_ids = "keep") sna1 = compute_note_array(score.parts[0], feature_functions=["clef_feature"]) sna2 = compute_note_array(score.parts[1], feature_functions=["clef_feature"]) + + + sna1test1 = sna1["clef_feature.clef_sign"] == np.array([1., 0., 2., 0., 0., 2., 0., 0., 1., 0.]) + self.assertTrue(np.all(sna1test1), "clef sign does not match") + print(sna1["clef_feature.clef_sign"], sna1["clef_feature.clef_line"], sna1["clef_feature.clef_octave_change"]) From aaaa2426a1e694d079d9632a7a7b06d6346aa165 Mon Sep 17 00:00:00 2001 From: sildater Date: Thu, 17 Oct 2024 14:39:49 +0200 Subject: [PATCH 103/151] all attributes --- tests/test_clef.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_clef.py b/tests/test_clef.py index 3c333fc7..4b99c8a8 100644 --- a/tests/test_clef.py +++ b/tests/test_clef.py @@ -23,6 +23,10 @@ def test_clef_feature_exctraction(self): sna1test1 = sna1["clef_feature.clef_sign"] == np.array([1., 0., 2., 0., 0., 2., 0., 0., 1., 0.]) + sna1test2 = sna1["clef_feature.clef_line"] == np.array([4., 2., 3., 2., 2., 4., 2., 2., 4., 2.]) + sna1test3 = sna1["clef_feature.clef_octave_change"] == np.array([0., 0., 0., 1., 1., 0., -1., 0., 0., 0.]) self.assertTrue(np.all(sna1test1), "clef sign does not match") - print(sna1["clef_feature.clef_sign"], sna1["clef_feature.clef_line"], sna1["clef_feature.clef_octave_change"]) + self.assertTrue(np.all(sna1test1), "clef line does not match") + self.assertTrue(np.all(sna1test1), "clef octave does not match") + print(sna2["clef_feature.clef_sign"], sna2["clef_feature.clef_line"], sna2["clef_feature.clef_octave_change"]) From b2d41060002f6f35abfe955792f065a0dee6733e Mon Sep 17 00:00:00 2001 From: sildater Date: Thu, 17 Oct 2024 14:45:06 +0200 Subject: [PATCH 104/151] full test unmerged --- tests/test_clef.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/test_clef.py b/tests/test_clef.py index 4b99c8a8..24b42704 100644 --- a/tests/test_clef.py +++ b/tests/test_clef.py @@ -11,6 +11,7 @@ from partitura import load_musicxml from partitura.musicanalysis import compute_note_array from partitura.musicanalysis.note_features import clef_feature +from partitura.score import merge_parts class TestingClefFeatureExtraction(unittest.TestCase): def test_clef_feature_exctraction(self): @@ -20,13 +21,26 @@ def test_clef_feature_exctraction(self): feature_functions=["clef_feature"]) sna2 = compute_note_array(score.parts[1], feature_functions=["clef_feature"]) + mpart = merge_parts(score.parts, reassign="staff") + sna3 = compute_note_array(mpart, + feature_functions=["clef_feature"]) - sna1test1 = sna1["clef_feature.clef_sign"] == np.array([1., 0., 2., 0., 0., 2., 0., 0., 1., 0.]) sna1test2 = sna1["clef_feature.clef_line"] == np.array([4., 2., 3., 2., 2., 4., 2., 2., 4., 2.]) sna1test3 = sna1["clef_feature.clef_octave_change"] == np.array([0., 0., 0., 1., 1., 0., -1., 0., 0., 0.]) self.assertTrue(np.all(sna1test1), "clef sign does not match") - self.assertTrue(np.all(sna1test1), "clef line does not match") - self.assertTrue(np.all(sna1test1), "clef octave does not match") - print(sna2["clef_feature.clef_sign"], sna2["clef_feature.clef_line"], sna2["clef_feature.clef_octave_change"]) + self.assertTrue(np.all(sna1test2), "clef line does not match") + self.assertTrue(np.all(sna1test3), "clef octave does not match") + + + sna2test1 = sna2["clef_feature.clef_sign"] == np.array([1., 0.]) + sna2test2 = sna2["clef_feature.clef_line"] == np.array([4., 2.]) + sna2test3 = sna2["clef_feature.clef_octave_change"] == np.array([0., 0.]) + self.assertTrue(np.all(sna2test1), "clef sign does not match") + self.assertTrue(np.all(sna2test2), "clef line does not match") + self.assertTrue(np.all(sna2test3), "clef octave does not match") + + + + print(sna3["clef_feature.clef_sign"], sna3["clef_feature.clef_line"], sna3["clef_feature.clef_octave_change"]) From 24dc6b50bc4d5d66defd0ae578cb883c114ac0e3 Mon Sep 17 00:00:00 2001 From: sildater Date: Thu, 17 Oct 2024 14:53:29 +0200 Subject: [PATCH 105/151] full test including merging merging by staff required, merging by voice discard clefs. --- test.musicxml | 274 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_clef.py | 11 +- 2 files changed, 281 insertions(+), 4 deletions(-) create mode 100644 test.musicxml diff --git a/test.musicxml b/test.musicxml new file mode 100644 index 00000000..8818d351 --- /dev/null +++ b/test.musicxml @@ -0,0 +1,274 @@ + + + + + + + + + + + + + 1 + + 0 + + + 2 + + G + 2 + + + F + 4 + + + + + + G + 4 + + 4 + 1 + whole + 1 + + + 4 + + + + B + 2 + + 4 + 5 + whole + 2 + + + 4 + + + + 4 + 6 + 1 + + + 4 + + + + 4 + 10 + 2 + + + + + + 2 + + G + 2 + 1 + + + C + 3 + + + + + G + 5 + + 2 + 1 + half + 1 + + + + G + 5 + + 2 + 1 + half + 1 + + + 4 + + + + A + 3 + + 4 + 5 + whole + 2 + + + 4 + + + + 4 + 6 + 1 + + + 4 + + + + 4 + 10 + 2 + + + + + + 1 + + G + 2 + -1 + + + C + 4 + + + + + G + 3 + + 2 + 1 + half + 1 + + + + G + 2 + + + + + G + 4 + + 2 + 1 + half + 1 + + + 4 + + + + F + 3 + + 4 + 5 + whole + 2 + + + 4 + + + + B + 2 + + 4 + 6 + whole + 1 + + + 4 + + + + G + 4 + + 4 + 10 + whole + 2 + + + + + + 1 + + F + 4 + + + + + G + 4 + + 4 + 1 + whole + 1 + + + 4 + + + + B + 2 + + 4 + 5 + whole + 2 + + + 4 + + + + 4 + 6 + 1 + + + 4 + + + + 4 + 10 + 2 + + + + diff --git a/tests/test_clef.py b/tests/test_clef.py index 24b42704..b53463e3 100644 --- a/tests/test_clef.py +++ b/tests/test_clef.py @@ -12,6 +12,7 @@ from partitura.musicanalysis import compute_note_array from partitura.musicanalysis.note_features import clef_feature from partitura.score import merge_parts +import partitura class TestingClefFeatureExtraction(unittest.TestCase): def test_clef_feature_exctraction(self): @@ -39,8 +40,10 @@ def test_clef_feature_exctraction(self): self.assertTrue(np.all(sna2test1), "clef sign does not match") self.assertTrue(np.all(sna2test2), "clef line does not match") self.assertTrue(np.all(sna2test3), "clef octave does not match") - - - - print(sna3["clef_feature.clef_sign"], sna3["clef_feature.clef_line"], sna3["clef_feature.clef_octave_change"]) + sna3test1 = sna3["clef_feature.clef_sign"] == np.array([1., 0., 2., 0., 0., 1., 2., 0., 0., 0., 1., 0.]) + sna3test2 = sna3["clef_feature.clef_line"] == np.array([4., 2., 3., 2., 2., 4., 4., 2., 2., 2., 4., 2.]) + sna3test3 = sna3["clef_feature.clef_octave_change"] == np.array([0., 0., 0., 1., 1., 0., 0., -1., 0., 0., 0., 0.]) + self.assertTrue(np.all(sna3test1), "clef sign does not match") + self.assertTrue(np.all(sna3test2), "clef line does not match") + self.assertTrue(np.all(sna3test3), "clef octave does not match") From ea79e9d960fd693bd19944c9184b80c13d692645 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 18 Oct 2024 12:46:22 +0200 Subject: [PATCH 106/151] support for stem direction in MEI. --- partitura/io/exportmei.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index 3a918858..d7285f40 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -277,6 +277,9 @@ def _handle_note(self, note, xml_voice_el): elif note.tie_prev is not None: note_el.set("tie", "t") + if note.stem_direction is not None: + note_el.set("stem.dir", note.stem_direction) + if note.alter is not None: if ( note.step.lower() + ALTER_TO_MEI[note.alter] From 97f79305cc022ed0cd737506fd5b419222977325 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 18 Oct 2024 12:53:33 +0200 Subject: [PATCH 107/151] restricted values for the support of stem direction in MEI. --- partitura/io/exportmei.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index d7285f40..a9754502 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -277,7 +277,7 @@ def _handle_note(self, note, xml_voice_el): elif note.tie_prev is not None: note_el.set("tie", "t") - if note.stem_direction is not None: + if note.stem_direction in ["up", "down"]: note_el.set("stem.dir", note.stem_direction) if note.alter is not None: From fad521c424fb01ef388b29aecb91db485fcf1178 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 18 Oct 2024 12:53:45 +0200 Subject: [PATCH 108/151] Testing musicxml import with stems. --- tests/data/musicxml/test_note_ties.xml | 1 + tests/test_xml.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/tests/data/musicxml/test_note_ties.xml b/tests/data/musicxml/test_note_ties.xml index b34cb50c..285251f0 100644 --- a/tests/data/musicxml/test_note_ties.xml +++ b/tests/data/musicxml/test_note_ties.xml @@ -29,6 +29,7 @@ 1 quarter + up diff --git a/tests/test_xml.py b/tests/test_xml.py index 05959d3f..0de6da8d 100755 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -212,6 +212,11 @@ def test_export_import_slur(self): part1 = make_part_slur() self._pretty_export_import_pretty_test(part1) + def test_stem_direction_import(self): + # test if stem direction is imported correctly for the first note of test_note_ties.xml + part = load_musicxml(MUSICXML_IMPORT_EXPORT_TESTFILES[0])[0] + self.assertEqual(part.notes_tied[0].stem_direction, "up") + def _pretty_export_import_pretty_test(self, part1): # pretty print the part pstring1 = part1.pretty() From 73a96200279051444860d30eaeb99ccc1dbec9cf Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 18 Oct 2024 14:11:08 +0200 Subject: [PATCH 109/151] adding stem information in target test files for unfolding. --- tests/data/musicxml/test_unfold_complex_result.xml | 10 ++++++++++ tests/data/musicxml/test_unfold_dacapo_result.xml | 8 ++++++++ .../musicxml/test_unfold_volta_numbers_result.xml | 11 +++++++++++ 3 files changed, 29 insertions(+) diff --git a/tests/data/musicxml/test_unfold_complex_result.xml b/tests/data/musicxml/test_unfold_complex_result.xml index d17c2190..4aa27621 100644 --- a/tests/data/musicxml/test_unfold_complex_result.xml +++ b/tests/data/musicxml/test_unfold_complex_result.xml @@ -57,6 +57,7 @@ 1 1 + up quarter @@ -66,6 +67,7 @@ 1 1 + up quarter @@ -78,6 +80,7 @@ 2 1 + up half @@ -102,6 +105,7 @@ 2 1 + up half @@ -114,6 +118,7 @@ 2 1 + up half @@ -144,6 +149,7 @@ 2 1 + up half @@ -164,6 +170,7 @@ 2 1 + up half @@ -224,6 +231,7 @@ 1 1 + up quarter @@ -233,6 +241,7 @@ 1 1 + up quarter @@ -245,6 +254,7 @@ 2 1 + up half diff --git a/tests/data/musicxml/test_unfold_dacapo_result.xml b/tests/data/musicxml/test_unfold_dacapo_result.xml index 834ed671..343b405f 100644 --- a/tests/data/musicxml/test_unfold_dacapo_result.xml +++ b/tests/data/musicxml/test_unfold_dacapo_result.xml @@ -81,6 +81,7 @@ 2 1 + up half @@ -105,6 +106,7 @@ 2 1 + up half @@ -129,6 +131,7 @@ 2 1 + up half @@ -147,6 +150,7 @@ 2 1 + up half @@ -159,6 +163,7 @@ 2 1 + up half @@ -177,6 +182,7 @@ 2 1 + up half @@ -285,6 +291,7 @@ 2 1 + up half @@ -309,6 +316,7 @@ 2 1 + up half diff --git a/tests/data/musicxml/test_unfold_volta_numbers_result.xml b/tests/data/musicxml/test_unfold_volta_numbers_result.xml index 20fe39e2..72913027 100644 --- a/tests/data/musicxml/test_unfold_volta_numbers_result.xml +++ b/tests/data/musicxml/test_unfold_volta_numbers_result.xml @@ -57,6 +57,7 @@ 1 1 + up quarter @@ -66,6 +67,7 @@ 1 1 + up quarter @@ -78,6 +80,7 @@ 2 1 + up half @@ -102,6 +105,7 @@ 2 1 + up half @@ -114,6 +118,7 @@ 2 1 + up half @@ -144,6 +149,7 @@ 2 1 + up half @@ -164,6 +170,7 @@ 2 1 + up half @@ -200,6 +207,7 @@ 2 1 + up half @@ -224,6 +232,7 @@ 2 1 + up half @@ -236,6 +245,7 @@ 2 1 + up half @@ -260,6 +270,7 @@ 2 1 + up half From 55daed43b8d2c23709bfd92308765e3f9f7363c5 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 18 Oct 2024 14:11:22 +0200 Subject: [PATCH 110/151] updated the position of stem in test file. --- tests/data/musicxml/test_note_ties.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/musicxml/test_note_ties.xml b/tests/data/musicxml/test_note_ties.xml index 285251f0..0803eb96 100644 --- a/tests/data/musicxml/test_note_ties.xml +++ b/tests/data/musicxml/test_note_ties.xml @@ -27,9 +27,9 @@ 15 1 + up quarter - up From 187e59495d766f64c764b6d50cb015e8ad1f89fc Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 22 Oct 2024 08:29:03 +0000 Subject: [PATCH 111/151] Format code with black (bot) --- partitura/io/exportmusicxml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/exportmusicxml.py b/partitura/io/exportmusicxml.py index 425d205e..01772fc5 100644 --- a/partitura/io/exportmusicxml.py +++ b/partitura/io/exportmusicxml.py @@ -647,7 +647,7 @@ def merge_measure_contents(notes, other, measure_start): result = [] pos = measure_start for i, voice in enumerate(sorted(notes.keys())): - if i == 0: # voice == merge_voice: + if i == 0: # voice == merge_voice: elements = merged[voice] else: From c8d3bff8a025d0a56828bb7f2258fba01eb43436 Mon Sep 17 00:00:00 2001 From: manoskary Date: Tue, 22 Oct 2024 09:15:06 +0000 Subject: [PATCH 112/151] Format code with black (bot) --- partitura/score.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/partitura/score.py b/partitura/score.py index f8bbdefd..ee354fbd 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -3102,7 +3102,9 @@ class ChordSymbol(Harmony): """A harmony element in the score usually for Chord Symbols.""" def __init__(self, root, kind, bass=None): - super().__init__(text=root + (f"/{kind}" if kind else "") + (f"/{bass}" if bass else "")) + super().__init__( + text=root + (f"/{kind}" if kind else "") + (f"/{bass}" if bass else "") + ) self.kind = kind self.root = root self.bass = bass From f0146a7be7de383bddb7f182eb970014fb8b2234 Mon Sep 17 00:00:00 2001 From: sildater Date: Thu, 24 Oct 2024 11:02:50 +0200 Subject: [PATCH 113/151] unique ID warning --- partitura/musicanalysis/note_features.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 4526b6ed..78349547 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -132,6 +132,15 @@ def make_note_features( include_grace_notes=True, include_time_signature=True, ) + + if len(set(na["id"])) != len(na): + warnings.warn( + "Length of note array {0} " + "does not correspond to number of unique IDs {1}. " + "Some feature functions may return spurious values.".format(len(na), + len(set(na["id"]))) + ) + acc = [] if isinstance(feature_functions, str) and feature_functions == "all": feature_functions = list_note_feats_functions() From 36b8423e6062cd12d070615d0c43cc5622d4ecec Mon Sep 17 00:00:00 2001 From: sildater Date: Thu, 24 Oct 2024 11:08:43 +0200 Subject: [PATCH 114/151] unique rest ID warning --- partitura/musicanalysis/note_features.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 78349547..403c42c5 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -265,6 +265,14 @@ def make_rest_features( ) if na.size == 0: return np.array([]) + + if len(set(na["id"])) != len(na): + warnings.warn( + "Length of rest array {0} " + "does not correspond to number of unique IDs {1}. " + "Some feature functions may return spurious values.".format(len(na), + len(set(na["id"]))) + ) acc = [] if isinstance(feature_functions, str) and feature_functions == "all": From 65ec4c58122a60bb3b26694ca78d3c8410ba5940 Mon Sep 17 00:00:00 2001 From: manoskary Date: Fri, 25 Oct 2024 10:37:13 +0000 Subject: [PATCH 115/151] Format code with black (bot) --- partitura/musicanalysis/note_features.py | 85 +++++++++++++----------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 403c42c5..488dd842 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -135,11 +135,12 @@ def make_note_features( if len(set(na["id"])) != len(na): warnings.warn( - "Length of note array {0} " - "does not correspond to number of unique IDs {1}. " - "Some feature functions may return spurious values.".format(len(na), - len(set(na["id"]))) - ) + "Length of note array {0} " + "does not correspond to number of unique IDs {1}. " + "Some feature functions may return spurious values.".format( + len(na), len(set(na["id"])) + ) + ) acc = [] if isinstance(feature_functions, str) and feature_functions == "all": @@ -265,14 +266,15 @@ def make_rest_features( ) if na.size == 0: return np.array([]) - + if len(set(na["id"])) != len(na): warnings.warn( - "Length of rest array {0} " - "does not correspond to number of unique IDs {1}. " - "Some feature functions may return spurious values.".format(len(na), - len(set(na["id"]))) - ) + "Length of rest array {0} " + "does not correspond to number of unique IDs {1}. " + "Some feature functions may return spurious values.".format( + len(na), len(set(na["id"])) + ) + ) acc = [] if isinstance(feature_functions, str) and feature_functions == "all": @@ -551,17 +553,17 @@ def clef_feature(na, part, **kwargs): Note that this feature does not return the staff number per note, see staff_feature for this information. """ - notes = {n.id:n for n in part.notes_tied} + notes = {n.id: n for n in part.notes_tied} numerical_clef_dict = { - 'G':0, 'F':1, 'C':2, - 'percussion':3, 'TAB':4, - 'jianpu':5, 'none':6 - } - names = [ - "clef_sign", - "clef_line", - "clef_octave_change" - ] + "G": 0, + "F": 1, + "C": 2, + "percussion": 3, + "TAB": 4, + "jianpu": 5, + "none": 6, + } + names = ["clef_sign", "clef_line", "clef_octave_change"] clef_dict = defaultdict(list) staff_numbers = set() clef_list = [clef for clef in part.iter_all(score.Clef)] @@ -569,20 +571,23 @@ def clef_feature(na, part, **kwargs): for clef in clef_list: staff = clef.staff or 1 staff_numbers.add(staff) - time_key = "time_"+str(staff) - clef_key = "clef_"+str(staff) + time_key = "time_" + str(staff) + clef_key = "clef_" + str(staff) clef_dict[time_key].append(clef.start.t) clef_dict[clef_key].append(clef) for staff in staff_numbers: - time_key = "time_"+str(staff) - interpolator_key = "interp_"+str(staff) + time_key = "time_" + str(staff) + interpolator_key = "interp_" + str(staff) start_times = np.array(clef_dict[time_key]) clef_indices = np.arange(len(start_times)) - interpolator = interp1d(start_times, clef_indices, - kind = "previous", - bounds_error=False, - fill_value="extrapolate") + interpolator = interp1d( + start_times, + clef_indices, + kind="previous", + bounds_error=False, + fill_value="extrapolate", + ) clef_dict[interpolator_key].append(interpolator) W = np.zeros((len(notes), 3)) @@ -590,21 +595,21 @@ def clef_feature(na, part, **kwargs): n = notes[na_n["id"]] staff = n.staff or 1 time = n.start.t - clef_key = "clef_"+str(staff) - interpolator_key = "interp_"+str(staff) + clef_key = "clef_" + str(staff) + interpolator_key = "interp_" + str(staff) clef_idx = clef_dict[interpolator_key][0](time) clef = clef_dict[clef_key][int(clef_idx)] sign = clef.sign or "none" - W[i,0] = numerical_clef_dict[sign] - W[i,1] = clef.line or 0 - W[i,2] = clef.octave_change or 0 + W[i, 0] = numerical_clef_dict[sign] + W[i, 1] = clef.line or 0 + W[i, 2] = clef.octave_change or 0 else: # add dummy clef W = np.zeros((len(notes), 3)) - W[:,0] = 6 # "none" - W[:,1] = 0 - W[:,2] = 0 + W[:, 0] = 6 # "none" + W[:, 1] = 0 + W[:, 2] = 0 return W, names @@ -955,7 +960,7 @@ def articulation_feature(na, part, **kwargs): force_size = False feature_by_name = {} - notes = {n.id:n for n in part.notes_tied} + notes = {n.id: n for n in part.notes_tied} N = len(notes) for i, na_n in enumerate(na): n = notes[na_n["id"]] @@ -1013,7 +1018,7 @@ def ornament_feature(na, part, **kwargs): "other-ornament", ] feature_by_name = {} - notes = {n.id:n for n in part.notes_tied} + notes = {n.id: n for n in part.notes_tied} N = len(notes) for i, na_n in enumerate(na): n = notes[na_n["id"]] @@ -1094,7 +1099,7 @@ def metrical_feature(na, part, **kwargs): """ notes_list = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests - notes = {n.id:n for n in notes_list} + notes = {n.id: n for n in notes_list} ts_map = part.time_signature_map bm = part.beat_map feature_by_name = {} From 3bed4935a373c28c03ed0646fbab6cd2d0ebe080 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 29 Oct 2024 11:44:07 +0100 Subject: [PATCH 116/151] refactor test --- tests/test_clef.py | 34 ++++++++++++++++++++++++++++++++- tests/test_metrical_position.py | 28 --------------------------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/tests/test_clef.py b/tests/test_clef.py index b53463e3..d236f027 100644 --- a/tests/test_clef.py +++ b/tests/test_clef.py @@ -5,7 +5,8 @@ """ import unittest from tests import ( - CLEF_TESTFILES + CLEF_TESTFILES, + CLEF_MAP_TESTFILES ) import numpy as np from partitura import load_musicxml @@ -47,3 +48,34 @@ def test_clef_feature_exctraction(self): self.assertTrue(np.all(sna3test1), "clef sign does not match") self.assertTrue(np.all(sna3test2), "clef line does not match") self.assertTrue(np.all(sna3test3), "clef octave does not match") + + + + +class TestClefMap(unittest.TestCase): + def test_clef_map(self): + score = load_musicxml(CLEF_MAP_TESTFILES[0]) + for part in score: + # clef = (staff_no, sign_shape, line, octave_shift) + map_fn = part.clef_map + self.assertTrue( + np.all(map_fn(part.first_point.t) == np.array([[1, 0, 2, 0], [2, 1, 4, 0]])) # treble / bass + ) + self.assertTrue( + np.all(map_fn(7) == np.array([[1, 0, 2, -2], [2, 0, 2, 0]])) # treble15vb / treble + ) + self.assertTrue( + np.all(map_fn(8) == np.array([[1, 0, 2, 1], [2, 1, 4, 0]])) # treble8va / bass + ) + self.assertTrue( + np.all(map_fn(11) == np.array([[1, 2, 3, 0], [2, 1, 4, 0]])) # ut3 / bass + ) + self.assertTrue( + np.all(map_fn(12) == np.array([[1, 2, 3, 0], [2, 1, 3, 0]])) # ut3 / bass3 + ) + self.assertTrue( + np.all(map_fn(13) == np.array([[1, 2, 4, 0], [2, 1, 3, 0]])) # ut4 / bass3 + ) + self.assertTrue( + np.all(map_fn(part.last_point.t) == np.array([[1, 2, 4, 0], [2, 1, 3, 0]])) # ut4 / bass3 + ) \ No newline at end of file diff --git a/tests/test_metrical_position.py b/tests/test_metrical_position.py index b2e1b29e..742e7cc8 100644 --- a/tests/test_metrical_position.py +++ b/tests/test_metrical_position.py @@ -14,7 +14,6 @@ from tests import ( METRICAL_POSITION_TESTFILES, TIME_SIGNATURE_MAP_EDGECASES_TESTFILES, - CLEF_MAP_TESTFILES, ) @@ -134,33 +133,6 @@ def test_time_signature_map(self): ) -class TestClefMap(unittest.TestCase): - def test_clef_map(self): - score = load_musicxml(CLEF_MAP_TESTFILES[0]) - for part in score: - # clef = (staff_no, sign_shape, line, octave_shift) - map_fn = part.clef_map - self.assertTrue( - np.all(map_fn(part.first_point.t) == np.array([[1, 0, 2, 0], [2, 1, 4, 0]])) # treble / bass - ) - self.assertTrue( - np.all(map_fn(7) == np.array([[1, 0, 2, -2], [2, 0, 2, 0]])) # treble15vb / treble - ) - self.assertTrue( - np.all(map_fn(8) == np.array([[1, 0, 2, 1], [2, 1, 4, 0]])) # treble8va / bass - ) - self.assertTrue( - np.all(map_fn(11) == np.array([[1, 2, 3, 0], [2, 1, 4, 0]])) # ut3 / bass - ) - self.assertTrue( - np.all(map_fn(12) == np.array([[1, 2, 3, 0], [2, 1, 3, 0]])) # ut3 / bass3 - ) - self.assertTrue( - np.all(map_fn(13) == np.array([[1, 2, 4, 0], [2, 1, 3, 0]])) # ut4 / bass3 - ) - self.assertTrue( - np.all(map_fn(part.last_point.t) == np.array([[1, 2, 4, 0], [2, 1, 3, 0]])) # ut4 / bass3 - ) if __name__ == "__main__": From 865ca5708aa4d0b19cf8a3c277cda2998f7cc479 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 29 Oct 2024 12:46:17 +0100 Subject: [PATCH 117/151] always check for starting clef --- partitura/score.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/partitura/score.py b/partitura/score.py index ac62f264..7dfab6de 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -278,7 +278,8 @@ def clef_map(self) -> Callable[[int], np.ndarray]: elif len(staff_clefs) == 1: # If there is only a single clef staff_clefs = np.array([staff_clefs[0, :], staff_clefs[0, :]]) - elif staff_clefs[0, 0] > self.first_point.t: + + if staff_clefs[0, 0] > self.first_point.t: staff_clefs = np.vstack( ((self.first_point.t, *staff_clefs[0, 1:]), staff_clefs) ) From dc424fae5a854747a663f9579a21ec30de621404 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 29 Oct 2024 12:48:04 +0100 Subject: [PATCH 118/151] default to none --- partitura/score.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/score.py b/partitura/score.py index 7dfab6de..8b2442a3 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -259,7 +259,7 @@ def clef_map(self) -> Callable[[int], np.ndarray]: staff_clefs = clefs[clefs[:, 1] == s] if len(staff_clefs) == 0: # default treble clef - staff, clef, line, octave_change = s, clef_sign_to_int("G"), 2, 0 + staff, clef, line, octave_change = s, clef_sign_to_int("none"), 0, 0 warnings.warn( "No clefs found on staff {}, assuming {} clef.".format(s, clef) From 8cfc0207106a17410f261d2ac1c83efd51621b0d Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 29 Oct 2024 12:52:19 +0100 Subject: [PATCH 119/151] refactor clef feature --- partitura/musicanalysis/note_features.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 488dd842..d4c588e7 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -11,7 +11,11 @@ import types from typing import List, Union, Tuple -from partitura.utils import ensure_notearray, ensure_rest_array +from partitura.utils import ( + ensure_notearray, + ensure_rest_array, + clef_sign_to_int +) from partitura.score import ScoreLike from collections import defaultdict @@ -554,15 +558,6 @@ def clef_feature(na, part, **kwargs): see staff_feature for this information. """ notes = {n.id: n for n in part.notes_tied} - numerical_clef_dict = { - "G": 0, - "F": 1, - "C": 2, - "percussion": 3, - "TAB": 4, - "jianpu": 5, - "none": 6, - } names = ["clef_sign", "clef_line", "clef_octave_change"] clef_dict = defaultdict(list) staff_numbers = set() @@ -600,7 +595,7 @@ def clef_feature(na, part, **kwargs): clef_idx = clef_dict[interpolator_key][0](time) clef = clef_dict[clef_key][int(clef_idx)] sign = clef.sign or "none" - W[i, 0] = numerical_clef_dict[sign] + W[i, 0] = clef_sign_to_int(sign) W[i, 1] = clef.line or 0 W[i, 2] = clef.octave_change or 0 From 733a926fd8bc3631d86ad22085cc8256f4a56484 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 29 Oct 2024 12:56:06 +0100 Subject: [PATCH 120/151] typing --- partitura/score.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index 8b2442a3..1cadd922 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -8,10 +8,9 @@ object). This object serves as a timeline at which musical elements are registered in terms of their start and end times. """ -from collections.abc import Callable from copy import copy, deepcopy from collections import defaultdict -from collections.abc import Iterable +from collections.abc import Iterable, Callable from numbers import Number from partitura.utils.globals import ( MUSICAL_BEATS, @@ -231,7 +230,7 @@ def key_signature_map(self): ) @property - def clef_map(self) -> Callable[[int], np.ndarray]: + def clef_map(self) -> Callable[Union[int, np.ndarray], np.ndarray]: """A function mapping timeline times to the clef in each staff at that time. The function can take scalar values or lists/arrays of values @@ -295,7 +294,7 @@ def clef_map(self) -> Callable[[int], np.ndarray]: ) ) - def collator(time: int) -> np.ndarray: + def collator(time: Union[int, np.ndarray]) -> np.ndarray: return np.array([interpolator(time) for interpolator in interpolators], dtype=int) return collator From 8701d843598d832fb888bfc57559101f9dc00701 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 29 Oct 2024 13:55:59 +0100 Subject: [PATCH 121/151] multipart and missing clef test --- tests/test_clef.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_clef.py b/tests/test_clef.py index d236f027..10032bea 100644 --- a/tests/test_clef.py +++ b/tests/test_clef.py @@ -78,4 +78,28 @@ def test_clef_map(self): ) self.assertTrue( np.all(map_fn(part.last_point.t) == np.array([[1, 2, 4, 0], [2, 1, 3, 0]])) # ut4 / bass3 - ) \ No newline at end of file + ) + + + def test_clef_map_multipart(self): + score = load_musicxml(CLEF_TESTFILES[0]) + p1 = score.parts[0] + p2 = score.parts[1] + + t = np.arange(16) + target_p1_octave_change = np.array([ 0, 0, 0, 0, 1, 1, 1, 1, -1, -1, 0, 0, 0, 0, 0, 0]) + target_p1_line = np.array([4, 4, 4, 4, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4]) + map_fn = p1.clef_map + self.assertTrue(np.all(map_fn(t)[0,:,3] == target_p1_octave_change)) + self.assertTrue(np.all(map_fn(t)[1,:,2] == target_p1_line)) + + target_p2_sign = np.zeros(16) # 16 stepgs G clef, imputed missing clef in the beginning + map_fn = p2.clef_map + self.assertTrue(np.all(map_fn(t)[1,:,1] == target_p2_sign)) + + + p3 = merge_parts(score.parts, reassign="staff") + map_fn = p3.clef_map + self.assertTrue(np.all(map_fn(t)[0,:,3] == target_p1_octave_change)) + self.assertTrue(np.all(map_fn(t)[1,:,2] == target_p1_line)) + self.assertTrue(np.all(map_fn(t)[3,:,1] == target_p2_sign)) From 17a89c57b2dce942ab87ca74e536c7ba29767307 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 29 Oct 2024 14:28:13 +0100 Subject: [PATCH 122/151] error in CI --- partitura/score.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/score.py b/partitura/score.py index 1cadd922..e07090ac 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -230,7 +230,7 @@ def key_signature_map(self): ) @property - def clef_map(self) -> Callable[Union[int, np.ndarray], np.ndarray]: + def clef_map(self) -> Callable[int, np.ndarray]: """A function mapping timeline times to the clef in each staff at that time. The function can take scalar values or lists/arrays of values From fc480bf58881d5c2bd1b9716b5c4028d982224e8 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 29 Oct 2024 14:31:08 +0100 Subject: [PATCH 123/151] remove typing for CI test --- partitura/score.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index e07090ac..287aec76 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -10,7 +10,7 @@ """ from copy import copy, deepcopy from collections import defaultdict -from collections.abc import Iterable, Callable +from collections.abc import Iterable from numbers import Number from partitura.utils.globals import ( MUSICAL_BEATS, @@ -230,7 +230,7 @@ def key_signature_map(self): ) @property - def clef_map(self) -> Callable[int, np.ndarray]: + def clef_map(self): """A function mapping timeline times to the clef in each staff at that time. The function can take scalar values or lists/arrays of values From 725861dc2cf20d46218adcc920245169754a5bf3 Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 29 Oct 2024 13:38:23 +0000 Subject: [PATCH 124/151] Format code with black (bot) --- partitura/musicanalysis/note_features.py | 6 +----- partitura/score.py | 8 +++++--- partitura/utils/globals.py | 2 +- partitura/utils/music.py | 2 ++ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index d4c588e7..fe8bc6cb 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -11,11 +11,7 @@ import types from typing import List, Union, Tuple -from partitura.utils import ( - ensure_notearray, - ensure_rest_array, - clef_sign_to_int -) +from partitura.utils import ensure_notearray, ensure_rest_array, clef_sign_to_int from partitura.score import ScoreLike from collections import defaultdict diff --git a/partitura/score.py b/partitura/score.py index 287aec76..42b84326 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -247,7 +247,7 @@ def clef_map(self): c.staff, clef_sign_to_int(c.sign), c.line, - c.octave_change if c.octave_change is not None else 0 + c.octave_change if c.octave_change is not None else 0, ) for c in self.iter_all(Clef) ] @@ -277,7 +277,7 @@ def clef_map(self): elif len(staff_clefs) == 1: # If there is only a single clef staff_clefs = np.array([staff_clefs[0, :], staff_clefs[0, :]]) - + if staff_clefs[0, 0] > self.first_point.t: staff_clefs = np.vstack( ((self.first_point.t, *staff_clefs[0, 1:]), staff_clefs) @@ -295,7 +295,9 @@ def clef_map(self): ) def collator(time: Union[int, np.ndarray]) -> np.ndarray: - return np.array([interpolator(time) for interpolator in interpolators], dtype=int) + return np.array( + [interpolator(time) for interpolator in interpolators], dtype=int + ) return collator diff --git a/partitura/utils/globals.py b/partitura/utils/globals.py index 2f9cb3f1..b9e9bbbc 100644 --- a/partitura/utils/globals.py +++ b/partitura/utils/globals.py @@ -730,7 +730,7 @@ }, } -#["G", "F", "C", "percussion", "TAB", "jianpu", "none"] +# ["G", "F", "C", "percussion", "TAB", "jianpu", "none"] CLEF_TO_INT = { "G": 0, "F": 1, diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 761dc789..777189ec 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -703,9 +703,11 @@ def key_int_to_mode(mode): def clef_sign_to_int(clef_sign: str) -> int: return CLEF_TO_INT[clef_sign] + def clef_int_to_sign(clef_int: int) -> str: return INT_TO_CLEF[clef_int] + def estimate_symbolic_duration( dur, div, eps=10**-3, return_com_durations=False ) -> Union[Dict[str, Any], Tuple[Dict[str, Any]]]: From 50c52ce708e10f2b8bd56b5a5f3eaa0a9d43363a Mon Sep 17 00:00:00 2001 From: sildater <41552783+sildater@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:55:40 +0100 Subject: [PATCH 125/151] fix typo --- partitura/score.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/score.py b/partitura/score.py index 42b84326..0bfa1051 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -257,7 +257,7 @@ def clef_map(self): for s in range(1, self.number_of_staves + 1): staff_clefs = clefs[clefs[:, 1] == s] if len(staff_clefs) == 0: - # default treble clef + # default "none" clef staff, clef, line, octave_change = s, clef_sign_to_int("none"), 0, 0 warnings.warn( From af0f1cbad185656610c34ca3a2ef9e1e9ea044f6 Mon Sep 17 00:00:00 2001 From: sildater <41552783+sildater@users.noreply.github.com> Date: Wed, 30 Oct 2024 09:42:29 +0100 Subject: [PATCH 126/151] remove extra lines --- partitura/score.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index b01833c4..86fb9998 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -1645,9 +1645,7 @@ def __init__( **kwargs, ): self._sym_dur = None - super().__init__(**kwargs) - self.voice = voice self.id = id self.staff = staff From 37811e0fb511f3da503186050794b2c43c580eac Mon Sep 17 00:00:00 2001 From: manoskary Date: Wed, 30 Oct 2024 10:22:30 +0000 Subject: [PATCH 127/151] Format code with black (bot) --- partitura/score.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/partitura/score.py b/partitura/score.py index 2b2d9668..9d591290 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -1725,7 +1725,9 @@ def __init__( self.articulations = articulations self.ornaments = ornaments self.doc_order = doc_order - self.stem_direction = stem_direction if stem_direction in ("up", "down") else None + self.stem_direction = ( + stem_direction if stem_direction in ("up", "down") else None + ) # these attributes are set after the instance is constructed self.fermata = None self.tie_prev = None From f2a1698156d3bb6f70a789b53127f7b69c93ab58 Mon Sep 17 00:00:00 2001 From: fosfrancesco Date: Wed, 30 Oct 2024 14:56:35 +0100 Subject: [PATCH 128/151] update MEI example to be valid. Previous one had a wrong staff definition due to a musicxml conversion bug --- partitura/assets/score_example.mei | 81 +++++++++++++++++------------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/partitura/assets/score_example.mei b/partitura/assets/score_example.mei index d8267f75..fc384420 100644 --- a/partitura/assets/score_example.mei +++ b/partitura/assets/score_example.mei @@ -1,52 +1,65 @@ - - - - - - - + + + + + + + <respStmt /> </titleStmt> - <pubStmt></pubStmt> + <pubStmt xml:id="p1kxcrok"> + <date isodate="2024-10-30" type="encoding-date">2024-10-30</date> + </pubStmt> </fileDesc> - <encodingDesc xml:id="encodingdesc-2o9bqo"> - <appInfo xml:id="appinfo-j7rtco"> - <application xml:id="application-hc9py4" isodate="2021-12-09T11:27:15" version="3.8.0-dev-45c3f2c"> - <name xml:id="name-symba1">Verovio</name> - <p xml:id="p-roup2t">Transcoded from MusicXML</p> + <encodingDesc xml:id="e1xr2zpp"> + <appInfo xml:id="a1wyxc5n"> + <application xml:id="a1t43ge2" isodate="2024-10-30T10:56:32" version="4.3.1-3b8cc17"> + <name xml:id="n1cp60l4">Verovio</name> + <p xml:id="p13nndma">Transcoded from MusicXML</p> </application> </appInfo> </encodingDesc> </meiHead> <music> <body> - <mdiv xml:id="mhblkrl"> - <score xml:id="ssc72wy"> - <scoreDef xml:id="s3uaoz5"> - <staffGrp xml:id="sjczhy0"> - <staffDef xml:id="P1" n="1" lines="5" ppq="12"> - <label xml:id="lezfcog">Piano</label> - <meterSig xml:id="mhw0sp2" count="4" unit="4" /> - </staffDef> + <mdiv xml:id="muo97v6"> + <score xml:id="suneqlv"> + <scoreDef xml:id="sz00r05"> + <staffGrp xml:id="s1h35kps"> + <staffGrp xml:id="P1" bar.thru="true"> + <grpSym xml:id="g1eji31e" symbol="brace" /> + <label xml:id="l8lirjj">Piano</label> + <instrDef xml:id="iggd40h" midi.channel="0" midi.instrnum="0" midi.volume="78.00%" /> + <staffDef xml:id="sbpks8p" n="1" lines="5" ppq="1"> + <clef xml:id="c1p7nrnw" shape="G" line="2" /> + <keySig xml:id="ki5anqv" sig="0" /> + <meterSig xml:id="m1vpw2w2" count="4" unit="4" /> + </staffDef> + <staffDef xml:id="s1g416nz" n="2" lines="5" ppq="1"> + <clef xml:id="cezkswz" shape="G" line="2" /> + <keySig xml:id="kk41xfz" sig="0" /> + <meterSig xml:id="m7alo7s" count="4" unit="4" /> + </staffDef> + </staffGrp> </staffGrp> </scoreDef> - <sb xml:id="st1gphw" /> - <section xml:id="swgpvx8"> - <pb xml:id="paw6v6b" /> - <measure xml:id="mz87quy" n="1"> - <staff xml:id="sxxu2aq" n="1"> - <layer xml:id="llktcv2" n="1"> - <note xml:id="n01" dur.ppq="48" dur="1" staff="2" oct="4" pname="a" /> - </layer> - <layer xml:id="lgap59p" n="2"> - <rest xml:id="r01" dur.ppq="24" dur="2" /> - <chord xml:id="carc8ao" dur.ppq="24" dur="2"> - <note xml:id="n02" oct="5" pname="c" /> - <note xml:id="n03" oct="5" pname="e" /> + <section xml:id="suu4o7p"> + <measure xml:id="m1e358qx" n="1"> + <staff xml:id="s1ufvigy" n="1"> + <layer xml:id="l1r7cvga" n="1"> + <rest xml:id="ro1o7cb" dur.ppq="2" dur="2" /> + <chord xml:id="c1c1r6b7" dur.ppq="2" dur="2" stem.dir="down"> + <note xml:id="n6dpu2p" oct="5" pname="c" /> + <note xml:id="njfgcwp" oct="5" pname="e" /> </chord> </layer> </staff> + <staff xml:id="s710zw2" n="2"> + <layer xml:id="leffrs2" n="5"> + <note xml:id="n1txt37q" dur.ppq="4" dur="1" oct="4" pname="a" /> + </layer> + </staff> </measure> </section> </score> From c460088f4c920ff754c74985c2b17648609555f3 Mon Sep 17 00:00:00 2001 From: fosfrancesco <foscarin.francesco@gmail.com> Date: Wed, 30 Oct 2024 14:57:51 +0100 Subject: [PATCH 129/151] adding the "auto"mode on merge_parts, which mimic the voice handling of musicxml, with 4 voices per staff --- partitura/score.py | 63 ++++++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index ee354fbd..385f39cc 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -5358,6 +5358,10 @@ def merge_parts(parts, reassign="voice"): If "voice", the new part have only one staff, and as manually voices as the sum of the voices in parts; the voice number get reassigned. + If "both", we reassign all the staves and voices to have unique staff + and voice numbers. According to musicxml standards, we consider 4 voices + per staff. So for example staff 1 will have voices 1,2,3,4, staff 2 will + have voices 5,6,7,8, and so on. Returns ------- @@ -5366,7 +5370,7 @@ def merge_parts(parts, reassign="voice"): """ # check if reassign has valid values - if reassign not in ["staff", "voice"]: + if reassign not in ["staff", "voice","auto"]: raise ValueError( "Only 'staff' and 'voice' are supported ressign values. Found", reassign ) @@ -5404,26 +5408,20 @@ def merge_parts(parts, reassign="voice"): new_part._quarter_durations = [lcm] note_arrays = [part.note_array(include_staff=True) for part in parts] - # find the maximum number of voices for each part (voice number start from 1) - maximum_voices = [ - ( - max(note_array["voice"], default=0) - if max(note_array["voice"], default=0) != 0 - else 1 - ) - for note_array in note_arrays + # find the unique number of voices for each part (voice numbers start from 1) + unique_voices = [ + np.unique(note_array["voice"]) for note_array in note_arrays ] - # find the maximum number of staves for each part (staff number start from 0 but we force them to 1) - maximum_staves = [ - ( - max(note_array["staff"], default=0) - if max(note_array["staff"], default=0) != 0 - else 1 - ) - for note_array in note_arrays + # find the unique number of staves for each part + unique_staves = [ + np.unique(note_array["staff"]) for note_array in note_arrays ] + # find the maximum number of voices for each part (voice numbers start from 1) + maximum_voices = [max(unique_voice, default=1) for unique_voice in unique_voices] + # find the maximum number of staves for each part + maximum_staves = [max(unique_staff, default=1) for unique_staff in unique_staves] - if reassign == "staff": + if reassign in ["staff","auto"]: el_to_discard = ( Barline, Page, @@ -5454,6 +5452,20 @@ def merge_parts(parts, reassign="voice"): ) for p_ind, p in enumerate(parts): + if reassign == "auto": + # find how many staves this part has + n_staves = len(unique_staves[p_ind]) + # find total number of staves in previous parts + if p_ind == 0: + n_previous_staves = 0 + else: + n_previous_staves = sum([len(unique_staff) for unique_staff in unique_staves[:p_ind]]) + # build a mapping between the old staff numbers and the new staff numbers + staff_mapping = dict(zip(unique_staves[p_ind], n_previous_staves+ np.arange(1, n_staves + 1))) + # find how many voices this part has + n_voices = len(unique_voices[p_ind]) + # build a mapping between the old and new voices + voice_mapping = dict(zip(unique_voices[p_ind], n_previous_staves*4 + np.arange(1, n_voices + 1))) for e in p.iter_all(): # full copy the first part and partially copy the others # we don't copy elements like duplicate barlines, clefs or @@ -5473,16 +5485,17 @@ def merge_parts(parts, reassign="voice"): if isinstance(e, GenericNote): e.voice = e.voice + sum(maximum_voices[:p_ind]) elif reassign == "staff": - if isinstance(e, (GenericNote, Words, Direction)): - e.staff = (e.staff if e.staff is not None else 1) + sum( - maximum_staves[:p_ind] - ) - elif isinstance( - e, Clef - ): # TODO: to update if "number" get changed in "staff" + if isinstance(e, (GenericNote, Words, Direction, Clef)): e.staff = (e.staff if e.staff is not None else 1) + sum( maximum_staves[:p_ind] ) + elif reassign == "auto": + # assign based on the voice and staff mappings + if isinstance(e, GenericNote): + # new voice is computed as the sum of voices in staves in previous parts, plus the current + e.voice = voice_mapping[e.voice] + if isinstance(e, (GenericNote, Words, Direction,Clef)): + e.staff = staff_mapping[e.staff] new_part.add(e, start=new_start, end=new_end) # new_part.add(copy.deepcopy(e), start=new_start, end=new_end) From 88fcd5a372a29bb4d9efb2c1e7b3f9bc02632b5a Mon Sep 17 00:00:00 2001 From: fosfrancesco <foscarin.francesco@gmail.com> Date: Wed, 30 Oct 2024 14:59:01 +0100 Subject: [PATCH 130/151] mei import now takes voice and staff number from the score information, and is able to correctly handle cross-staff voices --- partitura/io/importmei.py | 52 ++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/partitura/io/importmei.py b/partitura/io/importmei.py index 2f4ba704..c1dce72a 100644 --- a/partitura/io/importmei.py +++ b/partitura/io/importmei.py @@ -3,6 +3,7 @@ """ This module contains methods for importing MEI files. """ +import os from collections import OrderedDict from lxml import etree from fractions import Fraction @@ -64,6 +65,9 @@ def load_mei(filename: PathLike) -> score.Score: class MeiParser(object): def __init__(self, mei_path: PathLike) -> None: + # check if the file exists. Verovio won't complain if it doesn't + if not os.path.exists(mei_path): + raise FileNotFoundError(f"File {mei_path} not found.") document, ns = self._parse_mei(mei_path, use_verovio=VEROVIO_AVAILABLE) self.document = document self.ns = ns # the namespace in the MEI file @@ -322,11 +326,9 @@ def _handle_clef(self, element, position, part): # find the staff number parent = element.getparent() if parent.tag == self._ns_name("staffDef"): - # number = parent.attrib["n"] - number = 1 + number = parent.attrib["n"] else: # go back another level to staff element - # number = parent.getparent().attrib["n"] - number = 1 + number = parent.getparent().attrib["n"] sign = element.attrib["shape"] line = element.attrib["line"] octave = self._compute_clef_octave( @@ -641,6 +643,10 @@ def _handle_note(self, note_el, position, voice, staff, part) -> int: note_id, duration, symbolic_duration = self._duration_info(note_el, part) # find if it's grace grace_attr = note_el.get("grace") + # find if it has a different staff specification (for staff crossings) + different_staff = note_el.get("staff") + if different_staff is not None: + staff = int(different_staff) if grace_attr is None: # create normal note note = score.Note( @@ -649,7 +655,7 @@ def _handle_note(self, note_el, position, voice, staff, part) -> int: alter=alter, id=note_id, voice=voice, - staff=1, + staff=staff, symbolic_duration=symbolic_duration, articulations=None, # TODO : add articulation ) @@ -668,7 +674,7 @@ def _handle_note(self, note_el, position, voice, staff, part) -> int: alter=alter, id=note_id, voice=voice, - staff=1, + staff=staff, symbolic_duration=symbolic_duration, articulations=None, # TODO : add articulation ) @@ -702,11 +708,15 @@ def _handle_rest(self, rest_el, position, voice, staff, part): """ # find duration info rest_id, duration, symbolic_duration = self._duration_info(rest_el, part) + # find if it has a different staff specification (for staff crossings) + different_staff = rest_el.get("staff") + if different_staff is not None: + staff = int(different_staff) # create rest rest = score.Rest( id=rest_id, voice=voice, - staff=1, + staff=staff, symbolic_duration=symbolic_duration, articulations=None, ) @@ -744,12 +754,16 @@ def _handle_mrest(self, mrest_el, position, voice, staff, part): # find divs per measure ppq = part.quarter_duration_map(position) parts_per_measure = int(ppq * 4 * last_ts.beats / last_ts.beat_type) + # find if it has a different staff specification (for staff crossings) + different_staff = mrest_el.get("staff") + if different_staff is not None: + staff = int(different_staff) # create dummy rest to insert in the timeline rest = score.Rest( id=mrest_id, voice=voice, - staff=1, + staff=staff, symbolic_duration=estimate_symbolic_duration(parts_per_measure, ppq), articulations=None, ) @@ -793,12 +807,16 @@ def _handle_multirest(self, multirest_el, position, voice, staff, part): # find divs per measure ppq = part.quarter_duration_map(position) parts_per_measure = int(ppq * 4 * last_ts.beats / last_ts.beat_type) + # find if it has a different staff specification (for staff crossings) + different_staff = multirest_el.get("staff") + if different_staff is not None: + staff = int(different_staff) # create dummy rest to insert in the timeline rest = score.Rest( id=multirest_id, voice=voice, - staff=1, + staff=staff, symbolic_duration=estimate_symbolic_duration(parts_per_measure, ppq), articulations=None, ) @@ -832,12 +850,22 @@ def _handle_chord(self, chord_el, position, voice, staff, part): """ # find duration info chord_id, duration, symbolic_duration = self._duration_info(chord_el, part) + # find if the entire chord has a different staff specification (for staff crossings) + different_staff = chord_el.get("staff") + if different_staff is not None: + staff = int(different_staff) # find notes info notes_el = chord_el.findall(self._ns_name("note")) for note_el in notes_el: note_id = note_el.attrib[self._ns_name("id", XML_NAMESPACE)] # find pitch info step, octave, alter = self._pitch_info(note_el) + # find if single notes have a different staff specification + different_staff = note_el.get("staff") + if different_staff is not None: + note_staff = int(different_staff) + else: + note_staff = staff # create note note = score.Note( step=step, @@ -845,7 +873,7 @@ def _handle_chord(self, chord_el, position, voice, staff, part): alter=alter, id=note_id, voice=voice, - staff=1, + staff=note_staff, symbolic_duration=symbolic_duration, articulations=None, # TODO : add articulation ) @@ -982,7 +1010,7 @@ def _handle_staff_in_measure( for i_layer, layer_el in enumerate(layers_el): end_positions.append( self._handle_layer_in_staff_in_measure( - layer_el, i_layer + 1, staff_ind, position, part + layer_el, int(layer_el.attrib["n"]), staff_ind, position, part ) ) # check if layers have equal duration (bad encoding, but it often happens) @@ -1063,7 +1091,7 @@ def _handle_section(self, section_el, parts, position: int, measure_number: int) 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, measure_number + staff_el, int(staff_el.attrib["n"]), position, part, measure_number ) ) # handle directives (dir elements) From 89ce10d6c43a2c613c2acf8e5a0d90d154fea3a5 Mon Sep 17 00:00:00 2001 From: fosfrancesco <foscarin.francesco@gmail.com> Date: Wed, 30 Oct 2024 14:59:32 +0100 Subject: [PATCH 131/151] updated tests --- tests/__init__.py | 13 +- tests/data/mei/test_cross_staff_voices.mei | 463 ++++++++++ .../musicxml/test_cross_staff_voices.musicxml | 806 ++++++++++++++++++ tests/test_cross_staff.py | 34 + tests/test_cross_staff_beaming.py | 20 - tests/test_mei.py | 15 +- tests/test_merge_parts.py | 11 +- 7 files changed, 1332 insertions(+), 30 deletions(-) create mode 100644 tests/data/mei/test_cross_staff_voices.mei create mode 100644 tests/data/musicxml/test_cross_staff_voices.musicxml create mode 100644 tests/test_cross_staff.py delete mode 100644 tests/test_cross_staff_beaming.py diff --git a/tests/__init__.py b/tests/__init__.py index 3ed54288..394f9109 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -132,8 +132,11 @@ "test_single_part_change_divs.xml", "test_merge_voices1.xml", "test_merge_voices2.xml", - ] -] + ]] + [ + os.path.join(MEI_PATH, fn) + for fn in [ + "test_merge_voices2.mei", + ]] PIANOROLL_TESTFILES = [ os.path.join(MUSICXML_PATH, fn) @@ -251,3 +254,9 @@ CLEF_TESTFILES = [ os.path.join(DATA_PATH, "musicxml", "test_clef.musicxml") ] + +CROSS_STAFF_TESTFILES = [ + os.path.join(DATA_PATH, "musicxml", "test_cross_staff_beaming.musicxml"), + os.path.join(DATA_PATH, MUSICXML_PATH, "test_cross_staff_voices.musicxml"), + os.path.join(DATA_PATH, MEI_PATH, "test_cross_staff_voices.mei"), +] diff --git a/tests/data/mei/test_cross_staff_voices.mei b/tests/data/mei/test_cross_staff_voices.mei new file mode 100644 index 00000000..cc3247cb --- /dev/null +++ b/tests/data/mei/test_cross_staff_voices.mei @@ -0,0 +1,463 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-model href="https://music-encoding.org/schema/5.0/mei-all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?> +<?xml-model href="https://music-encoding.org/schema/5.0/mei-all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?> +<mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="5.0"> + <meiHead xml:id="m7kmp7y"> + <fileDesc xml:id="fj27vth"> + <titleStmt xml:id="trvhoky"> + <title>Untitled score + + Composer / arranger + + + + 2024-10-29 + + + + + + Verovio +

Transcoded from MusicXML

+
+
+
+
+ + + + + + + Untitled score + Subtitle + Composer / arranger + + + + + + Pno. + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
diff --git a/tests/data/musicxml/test_cross_staff_voices.musicxml b/tests/data/musicxml/test_cross_staff_voices.musicxml new file mode 100644 index 00000000..abab754c --- /dev/null +++ b/tests/data/musicxml/test_cross_staff_voices.musicxml @@ -0,0 +1,806 @@ + + + + + Untitled score + + + Composer / arranger + + MuseScore 4.3.2 + 2024-10-29 + + + + + + + + + + 6.99911 + 40 + + + 1696.94 + 1200.48 + + 85.7252 + 85.7252 + 85.7252 + 85.7252 + + + 85.7252 + 85.7252 + 85.7252 + 85.7252 + + + + 1.8 + 5.5 + 5 + 4.5 + 1 + 1 + 1.1 + 1 + 1.6 + 1.1 + 1.1 + 2.1 + 0.5 + 1.1 + 1 + 2.1 + 0.5 + 1 + 1.2 + 70 + 70 + 49 + + + + + + + title + Untitled score + + + subtitle + Subtitle + + + composer + Composer / arranger + + + + Piano + Pno. + + Piano + keyboard.piano + + + + 1 + 1 + 78.7402 + 0 + + + + + + + + + 50.00 + 0.00 + + 170.00 + + + 65.00 + + + + 2 + + 0 + + + 2 + + G + 2 + + + F + 4 + + + + + B + 4 + + 8 + 1 + whole + 1 + + + 8 + + + + G + 4 + + 2 + 2 + quarter + down + 1 + + + + 2 + 2 + quarter + 1 + + + + 4 + 2 + half + 1 + + + 8 + + + + C + 3 + + 1 + 5 + eighth + up + 2 + begin + + + + G + 3 + + 1 + 5 + eighth + up + 2 + continue + + + + D + 4 + + 1 + 5 + eighth + down + 1 + continue + + + + B + 3 + + 1 + 5 + eighth + up + 2 + end + + + + D + 4 + + 1 + 5 + eighth + down + 1 + begin + + + + E + 4 + + 1 + 5 + eighth + down + 1 + end + + + + 2 + 5 + quarter + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + + -0.00 + 0.00 + + 245.01 + + + 65.00 + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + + -0.00 + 0.00 + + 245.01 + + + 65.00 + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + light-heavy + + + + diff --git a/tests/test_cross_staff.py b/tests/test_cross_staff.py new file mode 100644 index 00000000..7467255f --- /dev/null +++ b/tests/test_cross_staff.py @@ -0,0 +1,34 @@ +import unittest +from partitura import load_musicxml, load_mei +import numpy as np +from tests import CROSS_STAFF_TESTFILES + + +class CrossStaffBeaming(unittest.TestCase): + def test_cross_staff_single_part_musicxml(self): + score = load_musicxml(CROSS_STAFF_TESTFILES[0]) + 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)) + +class CrossStaffVoices(unittest.TestCase): + def test_music_xml(self): + score = load_musicxml(CROSS_STAFF_TESTFILES[1]) + note_array = score.note_array(include_staff=True) + expected_staff = [2,1,1,2,1,2,1,1] + expected_voice = [5,2,1,5,5,5,5,5] + self.assertEqual(note_array["staff"].tolist(), expected_staff) + self.assertEqual(note_array["voice"].tolist(), expected_voice) + + def test_mei(self): + score = load_mei(CROSS_STAFF_TESTFILES[2]) + note_array = score.note_array(include_staff=True) + expected_staff = [2,1,1,2,1,2,1,1] + expected_voice = [5,2,1,5,5,5,5,5] + self.assertEqual(note_array["staff"].tolist(), expected_staff) + self.assertEqual(note_array["voice"].tolist(), expected_voice) \ No newline at end of file diff --git a/tests/test_cross_staff_beaming.py b/tests/test_cross_staff_beaming.py deleted file mode 100644 index 1b8ecd8f..00000000 --- a/tests/test_cross_staff_beaming.py +++ /dev/null @@ -1,20 +0,0 @@ -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_mei.py b/tests/test_mei.py index e5f26b4c..32b03287 100644 --- a/tests/test_mei.py +++ b/tests/test_mei.py @@ -16,6 +16,7 @@ from xmlschema.names import XML_NAMESPACE import os import numpy as np +from partitura.score import merge_parts # class TestSaveMEI(unittest.TestCase): @@ -36,7 +37,8 @@ def test_export_mei_simple(self): ina = import_score.note_array() with TemporaryDirectory() as tmpdir: tmp_mei = os.path.join(tmpdir, "test.mei") - save_mei(import_score, tmp_mei) + merged_mei = merge_parts(import_score.parts, reassign="auto") + save_mei(merged_mei, tmp_mei) export_score = load_mei(tmp_mei) ena = export_score.note_array() self.assertTrue(np.all(ina["onset_beat"] == ena["onset_beat"])) @@ -163,12 +165,12 @@ def test_clef(self): self.assertTrue(clefs2[0].start.t == 0) self.assertTrue(clefs2[0].sign == "C") self.assertTrue(clefs2[0].line == 3) - self.assertTrue(clefs2[0].staff == 1) + self.assertTrue(clefs2[0].staff == 3) self.assertTrue(clefs2[0].octave_change == 0) self.assertTrue(clefs2[1].start.t == 8) self.assertTrue(clefs2[1].sign == "F") self.assertTrue(clefs2[1].line == 4) - self.assertTrue(clefs2[1].staff == 1) + self.assertTrue(clefs2[1].staff == 3) self.assertTrue(clefs2[1].octave_change == 0) # test on part 3 part3 = list(score.iter_parts(part_list))[3] @@ -178,7 +180,7 @@ def test_clef(self): self.assertTrue(clefs3[1].start.t == 4) self.assertTrue(clefs3[1].sign == "G") self.assertTrue(clefs3[1].line == 2) - self.assertTrue(clefs3[1].staff == 1) + self.assertTrue(clefs3[1].staff == 4) self.assertTrue(clefs3[1].octave_change == -1) def test_key_signature1(self): @@ -300,9 +302,8 @@ def test_voice(self): self.assertTrue(np.array_equal(voices, expected_voices)) def test_staff(self): - parts = load_mei(MEI_TESTFILES[15]) - merged_part = score.merge_parts(parts, reassign="staff") - staves = merged_part.note_array(include_staff=True)["staff"] + score = load_mei(MEI_TESTFILES[15]) + staves = score.note_array(include_staff=True)["staff"] expected_staves = [4, 3, 2, 1, 1, 1] self.assertTrue(np.array_equal(staves, expected_staves)) diff --git a/tests/test_merge_parts.py b/tests/test_merge_parts.py index 5496e697..7c9da1c4 100644 --- a/tests/test_merge_parts.py +++ b/tests/test_merge_parts.py @@ -8,7 +8,7 @@ import unittest from pathlib import Path -from partitura import load_musicxml +from partitura import load_musicxml, load_mei from partitura.score import merge_parts, Part, iter_parts from partitura.utils.music import ensure_notearray @@ -100,3 +100,12 @@ def test_reassign_staves2(self): expected_staves = [4, 3, 2, 1, 1, 1] self.assertTrue(note_array["voice"].tolist() == expected_voices) self.assertTrue(note_array["staff"].tolist() == expected_staves) + + def test_reassign_auto(self): + score = load_mei(MERGE_PARTS_TESTFILES[8]) + merged_part = merge_parts(score.parts, reassign="auto") + note_array = merged_part.note_array(include_staff=True) + expected_voices = [13, 9, 5, 2, 1, 1] + expected_staves = [4, 3, 2, 1, 1, 1 ] + self.assertEqual(note_array["voice"].tolist(),expected_voices) + self.assertEqual(note_array["staff"].tolist(),expected_staves) From 471e65f04afbd4a1ecf2241eb79693c0910be3cf Mon Sep 17 00:00:00 2001 From: fosfrancesco Date: Sun, 3 Nov 2024 17:11:45 +0100 Subject: [PATCH 132/151] give default values to staves and voice if they are mising in the MEI, to avoid parsing exceptions. --- partitura/io/importmei.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/partitura/io/importmei.py b/partitura/io/importmei.py index c1dce72a..7bfa492f 100644 --- a/partitura/io/importmei.py +++ b/partitura/io/importmei.py @@ -326,9 +326,9 @@ def _handle_clef(self, element, position, part): # find the staff number parent = element.getparent() if parent.tag == self._ns_name("staffDef"): - number = parent.attrib["n"] + number = parent.attrib.get("n", 1) else: # go back another level to staff element - number = parent.getparent().attrib["n"] + number = parent.getparent().attrib.get("n", 1) sign = element.attrib["shape"] line = element.attrib["line"] octave = self._compute_clef_octave( @@ -1010,7 +1010,7 @@ def _handle_staff_in_measure( for i_layer, layer_el in enumerate(layers_el): end_positions.append( self._handle_layer_in_staff_in_measure( - layer_el, int(layer_el.attrib["n"]), staff_ind, position, part + layer_el, int(layer_el.attrib.get("n", i_layer+1)), staff_ind, position, part ) ) # check if layers have equal duration (bad encoding, but it often happens) @@ -1091,7 +1091,7 @@ def _handle_section(self, section_el, parts, position: int, measure_number: int) for i_s, (part, staff_el) in enumerate(zip(parts, staves_el)): end_positions.append( self._handle_staff_in_measure( - staff_el, int(staff_el.attrib["n"]), position, part, measure_number + staff_el, int(staff_el.attrib.get("n", i_s + 1)), position, part, measure_number ) ) # handle directives (dir elements) From 975d30073fcda4bb98de2e868447dd3d5735ff89 Mon Sep 17 00:00:00 2001 From: manoskary Date: Tue, 5 Nov 2024 10:40:38 +0000 Subject: [PATCH 133/151] Format code with black (bot) --- partitura/io/importmei.py | 12 ++++++++++-- partitura/score.py | 37 ++++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/partitura/io/importmei.py b/partitura/io/importmei.py index 7bfa492f..95606d8b 100644 --- a/partitura/io/importmei.py +++ b/partitura/io/importmei.py @@ -1010,7 +1010,11 @@ def _handle_staff_in_measure( for i_layer, layer_el in enumerate(layers_el): end_positions.append( self._handle_layer_in_staff_in_measure( - layer_el, int(layer_el.attrib.get("n", i_layer+1)), staff_ind, position, part + layer_el, + int(layer_el.attrib.get("n", i_layer + 1)), + staff_ind, + position, + part, ) ) # check if layers have equal duration (bad encoding, but it often happens) @@ -1091,7 +1095,11 @@ def _handle_section(self, section_el, parts, position: int, measure_number: int) for i_s, (part, staff_el) in enumerate(zip(parts, staves_el)): end_positions.append( self._handle_staff_in_measure( - staff_el, int(staff_el.attrib.get("n", i_s + 1)), position, part, measure_number + staff_el, + int(staff_el.attrib.get("n", i_s + 1)), + position, + part, + measure_number, ) ) # handle directives (dir elements) diff --git a/partitura/score.py b/partitura/score.py index a960929d..26fb49f6 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -5448,7 +5448,7 @@ def merge_parts(parts, reassign="voice"): """ # check if reassign has valid values - if reassign not in ["staff", "voice","auto"]: + if reassign not in ["staff", "voice", "auto"]: raise ValueError( "Only 'staff' and 'voice' are supported ressign values. Found", reassign ) @@ -5487,19 +5487,15 @@ def merge_parts(parts, reassign="voice"): note_arrays = [part.note_array(include_staff=True) for part in parts] # find the unique number of voices for each part (voice numbers start from 1) - unique_voices = [ - np.unique(note_array["voice"]) for note_array in note_arrays - ] + unique_voices = [np.unique(note_array["voice"]) for note_array in note_arrays] # find the unique number of staves for each part - unique_staves = [ - np.unique(note_array["staff"]) for note_array in note_arrays - ] + unique_staves = [np.unique(note_array["staff"]) for note_array in note_arrays] # find the maximum number of voices for each part (voice numbers start from 1) maximum_voices = [max(unique_voice, default=1) for unique_voice in unique_voices] - # find the maximum number of staves for each part - maximum_staves = [max(unique_staff, default=1) for unique_staff in unique_staves] + # find the maximum number of staves for each part + maximum_staves = [max(unique_staff, default=1) for unique_staff in unique_staves] - if reassign in ["staff","auto"]: + if reassign in ["staff", "auto"]: el_to_discard = ( Barline, Page, @@ -5537,13 +5533,24 @@ def merge_parts(parts, reassign="voice"): if p_ind == 0: n_previous_staves = 0 else: - n_previous_staves = sum([len(unique_staff) for unique_staff in unique_staves[:p_ind]]) + n_previous_staves = sum( + [len(unique_staff) for unique_staff in unique_staves[:p_ind]] + ) # build a mapping between the old staff numbers and the new staff numbers - staff_mapping = dict(zip(unique_staves[p_ind], n_previous_staves+ np.arange(1, n_staves + 1))) + staff_mapping = dict( + zip( + unique_staves[p_ind], n_previous_staves + np.arange(1, n_staves + 1) + ) + ) # find how many voices this part has n_voices = len(unique_voices[p_ind]) # build a mapping between the old and new voices - voice_mapping = dict(zip(unique_voices[p_ind], n_previous_staves*4 + np.arange(1, n_voices + 1))) + voice_mapping = dict( + zip( + unique_voices[p_ind], + n_previous_staves * 4 + np.arange(1, n_voices + 1), + ) + ) for e in p.iter_all(): # full copy the first part and partially copy the others # we don't copy elements like duplicate barlines, clefs or @@ -5571,8 +5578,8 @@ def merge_parts(parts, reassign="voice"): # assign based on the voice and staff mappings if isinstance(e, GenericNote): # new voice is computed as the sum of voices in staves in previous parts, plus the current - e.voice = voice_mapping[e.voice] - if isinstance(e, (GenericNote, Words, Direction,Clef)): + e.voice = voice_mapping[e.voice] + if isinstance(e, (GenericNote, Words, Direction, Clef)): e.staff = staff_mapping[e.staff] new_part.add(e, start=new_start, end=new_end) From e5bf2e1b1b7809022c76ca98040441f6bf15fb2d Mon Sep 17 00:00:00 2001 From: sildater Date: Tue, 5 Nov 2024 12:04:27 +0000 Subject: [PATCH 134/151] Format code with black (bot) --- partitura/musicanalysis/note_features.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index a922ef4b..121c2c83 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -1156,6 +1156,7 @@ def metrical_strength_feature(na, part, **kwargs): return W, names + def measure_feature(na, part, **kwargs): """Measure feature @@ -1163,12 +1164,12 @@ def measure_feature(na, part, **kwargs): """ notes_list = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests - notes = {n.id:n for n in notes_list} + notes = {n.id: n for n in notes_list} bm = part.beat_map - + global_start = bm(part.first_point.t) global_end = bm(part.last_point.t) - global_number = 0 # default global measure number + global_number = 0 # default global measure number names = [ "measure_number", From f72cf19e7c2bb07c3949b695bdc7bccca3705a07 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 6 Nov 2024 12:15:31 +0100 Subject: [PATCH 135/151] Minor doc correction for nakamura corresp. Aknowledgments to @mimbres for pointing out. --- partitura/io/importnakamura.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/importnakamura.py b/partitura/io/importnakamura.py index f2f32672..b77523eb 100644 --- a/partitura/io/importnakamura.py +++ b/partitura/io/importnakamura.py @@ -35,7 +35,7 @@ def load_nakamuracorresp(filename: PathLike) -> Tuple[Union[np.ndarray, list]]: Parameters ---------- filename : str - The nakamura match.txt-file + The nakamura corresp.txt-file Returns ------- From 46895d9b4069ff528693c0780e44b59bba6f432c Mon Sep 17 00:00:00 2001 From: CarlosCancino-Chacon Date: Mon, 11 Nov 2024 15:54:26 +0000 Subject: [PATCH 136/151] Format code with black (bot) --- partitura/performance.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/partitura/performance.py b/partitura/performance.py index 9bfa6ef4..a04e726b 100644 --- a/partitura/performance.py +++ b/partitura/performance.py @@ -178,8 +178,7 @@ def note_array(self, *args, **kwargs) -> np.ndarray: duration_sec = offset - note_on_sec duration_tick = ( n.get( - seconds_to_midi_ticks( - n["sound_off"], mpq=self.mpq, ppq=self.ppq), + seconds_to_midi_ticks(n["sound_off"], mpq=self.mpq, ppq=self.ppq), seconds_to_midi_ticks(n["note_off"], mpq=self.mpq, ppq=self.ppq), ) - note_on_tick @@ -299,7 +298,7 @@ def adjust_offsets_w_sustain( next_pedal_time = pedal[last_pedal_change_before_off + 1, 0] offs[pedal_down_at_off] = next_pedal_time[pedal_down_at_off] - + # adjust offset times of notes that have a reonset while the sustain pedal is on pitches = np.array([n["midi_pitch"] for n in notes]) note_ons = np.array([n["note_on"] for n in notes]) @@ -311,11 +310,10 @@ def adjust_offsets_w_sustain( sorted_note_ons = note_ons[sorted_indices] sorted_sound_offs = offs[sorted_indices] - adjusted_sound_offs = np.minimum( - sorted_sound_offs[:-1], sorted_note_ons[1:]) + adjusted_sound_offs = np.minimum(sorted_sound_offs[:-1], sorted_note_ons[1:]) offs[sorted_indices[:-1]] = adjusted_sound_offs - + for offset, note in zip(offs, notes): note["sound_off"] = offset From 598af135d33d2bbf781af7247f0577ab9ec2035a Mon Sep 17 00:00:00 2001 From: sildater Date: Thu, 14 Nov 2024 20:11:39 +0100 Subject: [PATCH 137/151] remove spurious musicxml file, add version --- setup.py | 2 +- test.musicxml | 274 -------------------------------------------------- 2 files changed, 1 insertion(+), 275 deletions(-) delete mode 100644 test.musicxml diff --git a/setup.py b/setup.py index 11f9a34f..f1374914 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ EMAIL = "partitura-users@googlegroups.com" AUTHOR = "Maarten Grachten, Carlos Cancino-Chacón, Silvan Peter, Emmanouil Karystinaios, Francesco Foscarin, Thassilo Gadermaier, Patricia Hu" REQUIRES_PYTHON = ">=3.7" -VERSION = "1.5.0" +VERSION = "1.6.0" # What packages are required for this module to be executed? REQUIRED = ["numpy", "scipy", "lxml", "lark-parser", "xmlschema", "mido"] diff --git a/test.musicxml b/test.musicxml deleted file mode 100644 index 8818d351..00000000 --- a/test.musicxml +++ /dev/null @@ -1,274 +0,0 @@ - - - - - - - - - - - - - 1 - - 0 - - - 2 - - G - 2 - - - F - 4 - - - - - - G - 4 - - 4 - 1 - whole - 1 - - - 4 - - - - B - 2 - - 4 - 5 - whole - 2 - - - 4 - - - - 4 - 6 - 1 - - - 4 - - - - 4 - 10 - 2 - - - - - - 2 - - G - 2 - 1 - - - C - 3 - - - - - G - 5 - - 2 - 1 - half - 1 - - - - G - 5 - - 2 - 1 - half - 1 - - - 4 - - - - A - 3 - - 4 - 5 - whole - 2 - - - 4 - - - - 4 - 6 - 1 - - - 4 - - - - 4 - 10 - 2 - - - - - - 1 - - G - 2 - -1 - - - C - 4 - - - - - G - 3 - - 2 - 1 - half - 1 - - - - G - 2 - - - - - G - 4 - - 2 - 1 - half - 1 - - - 4 - - - - F - 3 - - 4 - 5 - whole - 2 - - - 4 - - - - B - 2 - - 4 - 6 - whole - 1 - - - 4 - - - - G - 4 - - 4 - 10 - whole - 2 - - - - - - 1 - - F - 4 - - - - - G - 4 - - 4 - 1 - whole - 1 - - - 4 - - - - B - 2 - - 4 - 5 - whole - 2 - - - 4 - - - - 4 - 6 - 1 - - - 4 - - - - 4 - 10 - 2 - - - - From 578e41d94621f49b1311989c6bd53302c2ea628a Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 2 Dec 2024 17:05:06 +0100 Subject: [PATCH 138/151] Update for load from url. --- partitura/io/__init__.py | 32 +++++++++++++++++++++++++++++++- partitura/io/importrntxt.py | 13 ++----------- tests/test_urlload.py | 26 ++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 tests/test_urlload.py diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index 3e6b9891..11d337fd 100644 --- a/partitura/io/__init__.py +++ b/partitura/io/__init__.py @@ -5,7 +5,9 @@ """ from typing import Union import os - +import urllib.request +from urllib.parse import urlparse +import tempfile from .importmusicxml import load_musicxml from .importmidi import load_score_midi, load_performance_midi from .musescore import load_via_musescore @@ -33,6 +35,32 @@ class NotSupportedFormatError(Exception): pass +def is_url(input): + try: + result = urlparse(input) + return all([result.scheme, result.netloc]) + except ValueError: + return False + + +def download_file(url): + # Send a GET request to the URL + with urllib.request.urlopen(url) as response: + data = response.read() + + # Extract the file extension from the URL + extension = os.path.splitext(url)[-1] + + # Create a temporary file + temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=extension) + + # Write the content to the temporary file + with open(temp_file.name, 'wb') as f: + f.write(data) + + return temp_file.name + + @deprecated_alias(score_fn="filename") @deprecated_parameter("ensure_list") def load_score(filename: PathLike, force_note_ids="keep") -> Score: @@ -58,6 +86,8 @@ def load_score(filename: PathLike, force_note_ids="keep") -> Score: scr: :class:`partitura.score.Score` A score instance. """ + if is_url(filename): + filename = download_file(filename) extension = os.path.splitext(filename)[-1].lower() if extension in (".mxl", ".xml", ".musicxml"): diff --git a/partitura/io/importrntxt.py b/partitura/io/importrntxt.py index a13c5c58..b2828cf5 100644 --- a/partitura/io/importrntxt.py +++ b/partitura/io/importrntxt.py @@ -1,6 +1,6 @@ import re - import partitura.score as spt +import partitura.io as sptio import os.path as osp import numpy as np from urllib.parse import urlparse @@ -9,8 +9,7 @@ def load_rntxt(path: spt.Path, part=None, return_part=False): - - if is_url(path): + if sptio.is_url(path): data = load_data_from_url(path) lines = data.split("\n") else: @@ -41,14 +40,6 @@ def load_data_from_url(url: str): return data -def is_url(input): - try: - result = urlparse(input) - return all([result.scheme, result.netloc]) - except ValueError: - return False - - class RntxtParser: """ A parser for RNtxt format to a partitura Part. diff --git a/tests/test_urlload.py b/tests/test_urlload.py new file mode 100644 index 00000000..91afda6a --- /dev/null +++ b/tests/test_urlload.py @@ -0,0 +1,26 @@ +import unittest +from partitura import load_score +import numpy as np + + +class TestImport(unittest.TestCase): + def test_load_kern(self): + score = load_score("https://raw.githubusercontent.com/CPJKU/partitura/refs/heads/main/partitura/assets/score_example.krn") + note_array = score.note_array() + self.assertTrue(np.all(note_array["pitch"] == [69, 72, 76])) + + def test_load_mei(self): + score = load_score("https://raw.githubusercontent.com/CPJKU/partitura/refs/heads/main/partitura/assets/score_example.mei") + note_array = score.note_array() + self.assertTrue(np.all(note_array["pitch"] == [69, 72, 76])) + + def test_load_midi(self): + score = load_score("https://raw.githubusercontent.com/CPJKU/partitura/refs/heads/main/partitura/assets/score_example.mid") + note_array = score.note_array() + self.assertTrue(np.all(note_array["pitch"] == [69, 72, 76])) + + def test_load_musicxml(self): + score = load_score("https://raw.githubusercontent.com/CPJKU/partitura/refs/heads/main/partitura/assets/score_example.musicxml") + note_array = score.note_array() + self.assertTrue(np.all(note_array["pitch"] == [69, 72, 76])) + From d20990882bb974accc4f1c7f459702fb9dc6214c Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 3 Dec 2024 12:28:17 +0100 Subject: [PATCH 139/151] New version for RNTXT import. --- partitura/io/importrntxt.py | 282 +++++++++++++++++++----------------- 1 file changed, 151 insertions(+), 131 deletions(-) diff --git a/partitura/io/importrntxt.py b/partitura/io/importrntxt.py index b2828cf5..54cebe70 100644 --- a/partitura/io/importrntxt.py +++ b/partitura/io/importrntxt.py @@ -48,169 +48,189 @@ class RntxtParser: https://github.com/MarkGotham/When-in-Rome/blob/master/syntax.md """ def __init__(self, score=None): + # Initialize parser state + self.part = spt.Part(id="rn", part_name="Rn", part_abbreviation="rnp") + self.current_measure = None + self.current_position = 0 + self.measure_beat_position = 1 + self.current_time_signature = spt.TimeSignature(4, 4) + self.time_signature_style = 'Normal' # 'Normal', 'Slow', 'Fast' + self.key = 'C' + self.pedal = None + self.metadata = {} + self.measures = {} + # If a score is provided, copy relevant information if score is not None: - self.ref_part = score.parts[0] - quarter_duration = self.ref_part._quarter_durations[0] - ref_measures = self.ref_part.measures - ref_time_sigs = self.ref_part.time_sigs - ref_keys = self.ref_part.key_sigs + self._initialize_from_score(score) else: - quarter_duration = 4 - ref_measures = [] - ref_time_sigs = [] - ref_keys = [] - self.part = spt.Part(id="rn", part_name="Rn", part_abbreviation="rnp", quarter_duration=quarter_duration) - # include measures - for measure in ref_measures: + # Add default staff + self.part.add(spt.Staff(number=1, lines=5), 0) + + def _initialize_from_score(self, score): + # Copy measures, time signatures, and key signatures from the reference score + self.ref_part = score.parts[0] + for measure in self.ref_part.measures: self.part.add(measure, measure.start.t, measure.end.t) - # include time signatures - for time_sig in ref_time_sigs: + for time_sig in self.ref_part.time_sigs: self.part.add(time_sig, time_sig.start.t) - # include key signatures - for key in ref_keys: - self.part.add(key, key.start.t) + for key_sig in self.ref_part.key_sigs: + self.part.add(key_sig, key_sig.start.t) self.measures = {m.number: m for m in self.part.measures} - self.part.add(spt.Staff(number=1, lines=1), 0) - self.current_measure = None - self.current_position = 0 - self.measure_beat_position = 1 - self.current_voice = None - self.current_note = None - self.current_chord = None - self.current_tie = None - self.num_parsed_romans = 0 - self.key = "C" def parse(self, lines): - # np_lines = np.array(lines) - # potential_measure_lines = np.lines[np.char.startswith(np_lines, "m")] - # for line in potential_measure_lines: - # self._handle_measure(line) - for line in lines: - if line.startswith("Time Signature:"): - self.time_signature = line.split(":")[1].strip() - elif line.startswith("Pedal:"): - self.pedal = line.split(":")[1].strip() - elif line.startswith("m"): - self._handle_measure(line) - - self.currate_ending_times() - - def currate_ending_times(self): - romans = list(self.part.iter_all(spt.RomanNumeral)) - starting_times = [rn.start.t for rn in romans] - argsort_start = np.argsort(starting_times) - for i, rn_idx in enumerate(argsort_start[:-1]): - rn = romans[rn_idx] - if rn.end is None: - rn.end = romans[argsort_start[i+1]].start if rn.start.t < romans[argsort_start[i+1]].start.t else rn.start.t + 1 + for line_num, line in enumerate(lines, 1): + line = line.strip() + if not line or line.startswith('#'): + continue + try: + if ':' in line: + keyword = line.split(':', 1)[0].strip() + if keyword in ('Composer', 'Title', 'Analyst'): + self._handle_metadata(line) + elif keyword == 'Note': + self._handle_note_line(line) + elif keyword == 'Time Signature': + self._handle_time_signature(line) + elif keyword == 'Pedal': + self._handle_pedal(line) + else: + self._handle_line(line) + else: + self._handle_line(line) + except Exception as e: + print(f"Error parsing line {line_num}: {line}") + print(e) + self._calculate_ending_times() + + def _handle_metadata(self, line): + key, value = line.split(':', 1) + self.metadata[key.strip()] = value.strip() + + def _handle_note_line(self, line): + # Notes can be stored or logged as needed + pass - def _handle_measure(self, line): - if not self._validate_measure_line(line): - return - elements = line.split(" ") - measure_number = elements[0].strip("m") - if not measure_number.isnumeric(): - # TODO: complete check for variation measures - if "var" in measure_number: - return - else: + def _handle_pedal(self, line): + # Parse pedal information + pass + + def _handle_line(self, line): + if re.match(r'm\d+(-\d+)?\s*=', line): + self._handle_repeat(line) + elif line.startswith('m'): + self._handle_measure(line) + else: + raise ValueError(f"Unknown line format: {line}") + + def _handle_repeat(self, line): + # Implement repeat logic + pass - raise ValueError(f"Invalid measure number: {measure_number}") - measure_number = int(measure_number) - if measure_number not in self.measures.keys(): + def _handle_measure(self, line): + elements = line.strip().split() + measure_info = elements[0] + measure_match = re.match(r'm(\d+)(?:-(\d+))?', measure_info) + if not measure_match: + raise ValueError(f"Invalid measure number: {measure_info}") + measure_number = int(measure_match.group(1)) + if measure_number not in self.measures: + # Check if previous measure is there + if measure_number - 1 in self.measures: + previous_measure_start = self.measures[measure_number - 1].start.t + # get the current time signature + current_time_signature_beats = self.current_time_signature.beats + self.current_position = self.part.beat_map( + self.part.inv_beat_map(previous_measure_start) + current_time_signature_beats) self.current_measure = spt.Measure(number=measure_number) self.measures[measure_number] = self.current_measure - self.part.add(self.current_measure, int(self.current_position)) + self.part.add(self.current_measure, self.current_position) else: self.current_measure = self.measures[measure_number] - self.current_position = self.current_measure.start.t - # starts counting beats from 1 self.measure_beat_position = 1 for element in elements[1:]: self._handle_element(element) def _handle_element(self, element): - # if element starts with "b" followed by a number ("float" or "int") it is a beat - if element.startswith("b") and element[1:].replace(".", "").isnumeric(): - self.measure_beat_position = float(element[1:]) - if self.current_measure.number == 0: - if (self.current_position == 0 and self.num_parsed_romans == 0): - self.current_position = 0 - else: - self.current_position = self.part.inv_beat_map(self.part.beat_map(self.current_position) + self.measure_beat_position - 1).item() + if element.startswith('b'): + beat_match = re.match(r'b(\d+(\.\d+)?)', element) + if beat_match: + self.measure_beat_position = float(beat_match.group(1)) + self._update_current_position() else: - self.current_position = self.part.inv_beat_map(self.part.beat_map(self.current_measure.start.t) + self.measure_beat_position - 1).item() - - # if element starts with [A-G] and it includes : it is a key - elif len(re.findall(r"[A-Ga-g#b:]", element)) == len(element) and element[-1] == ":": + raise ValueError(f"Invalid beat format: {element}") + elif re.match(r'.*:', element): self._handle_key(element) - # if element only contains "|" or ":" (and combinations) it is a barline elif all(c in "|:" for c in element): self._handle_barline(element) - # else it is a roman numeral else: self._handle_roman_numeral(element) - def _handle_key(self, element): - # key is in the format "C:" or "c:" for C major or c minor - # for alterations use "C#:" or "c#:" for C# major or c# minor - name = element[0] - mode = "major" if name.isupper() else "minor" - step = name.upper() - # handle alterations - alter = element[1:].strip(":") - key_name = f"{step}{alter}{('m' if mode == 'minor' else '')}" - # step and alter to fifths - fifths, mode = key_name_to_fifths_mode(key_name) - ks = spt.KeySignature(fifths=fifths, mode=mode) - self.key = element.strip(":") - self.part.add(ks, int(self.current_position)) + def _update_current_position(self): + self.current_position = self.part.beat_map(self.part.inv_beat_map(self.current_measure.start.t) + self.measure_beat_position) + + def _get_beat_duration(self): + # Calculate beat duration based on the time signature and style + nom, denom = self.current_time_signature.beats, self.current_time_signature.beat_type + quarter_note_duration = 1 # Assuming a quarter note duration of 1 + beat_duration = (4 / denom) * quarter_note_duration + if self.time_signature_style == 'Fast' and nom % 3 == 0 and denom == 8: + beat_duration *= 3 # Compound meter counted in dotted quarters + elif self.time_signature_style == 'Slow' and nom % 3 == 0 and denom == 8: + beat_duration /= 3 # Compound meter counted in eighth notes + return beat_duration + + def _handle_time_signature(self, line): + time_signature = line.split(':', 1)[1].strip() + style = 'Normal' + if 'Slow' in time_signature: + style = 'Slow' + time_signature = time_signature.replace('Slow', '').strip() + elif 'Fast' in time_signature: + style = 'Fast' + time_signature = time_signature.replace('Fast', '').strip() + if time_signature == 'C': + nom, denom = 4, 4 + elif time_signature == 'Cut': + nom, denom = 2, 2 + else: + nom, denom = map(int, time_signature.split('/')) + self.current_time_signature = spt.TimeSignature(nom, denom) + self.time_signature_style = style + self.part.add(self.current_time_signature, self.current_position) def _handle_barline(self, element): + # Implement barline handling if needed pass def _handle_roman_numeral(self, element): - """ - The handling or roman numeral aims to translate rntxt notation to internal partitura notation. - - Parameters - ---------- - element: txt - The element is a rntxt notation string - """ - # Remove line endings and spaces - element = element.strip() - # change strings such as RN6/5 to RN65 but keep RN65/RN for the secondary degree - if "/" in element: - # if all elements between "/" are either digits or one of [o, +] then remove "/" else leave it in place - el_list = element.split("/") - element = el_list[0] - for el in el_list[1:]: - if len(re.findall(r"[1-9\+o]", el)) == len(el): - element += el - else: - element += "/" + el - # Validity checks happen inside the Roman Numeral object - # The checks include 1 & 2 Degree, Root, Bass, Inversion, and Quality extraction. - rn = spt.RomanNumeral(text=element, local_key=self.key) - try: - self.part.add(rn, int(self.current_position)) - except ValueError: - print(f"Could not add roman numeral {element} at position {self.current_position}") - return - # Set the end of the previous roman numeral - # if self.previous_rn is not None: - # self.previous_rn.end = spt.TimePoint(t=self.current_position) - self.num_parsed_romans += 1 - - def _validate_measure_line(self, line): - # does it have elements - if not len(line.split(" ")) > 1: - return False - return True + rn = spt.RomanNumeral(text=element, local_key=self.key) + self.part.add(rn, self.current_position) + except Exception as e: + raise ValueError(f"Error parsing Roman numeral '{element}': {e}") + + def _handle_key(self, element): + match = re.match(r'([A-Ga-g])([#b]*):', element) + if not match: + raise ValueError(f"Invalid key signature: {element}") + note, accidental = match.groups() + mode = 'minor' if note.islower() else 'major' + key_name = note.upper() + accidental + key_str = f"{key_name}{('m' if mode == 'minor' else '')}" + fifths, mode = key_name_to_fifths_mode(key_str) + ks = spt.KeySignature(fifths=fifths, mode=mode) + self.key = element.strip(":") + self.part.add(ks, self.current_position) + + def _calculate_ending_times(self): + romans = sorted(self.part.iter_all(spt.RomanNumeral), key=lambda rn: rn.start.t) + for i, rn in enumerate(romans[:-1]): + rn.end = spt.TimePoint(t=romans[i + 1].start.t) + if romans: + last_rn = romans[-1] + last_rn.end = self.part.end_time or (last_rn.start.t + 1) + From 19cf16472c408a37a15bbd344a9e6d0d644549f2 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 3 Dec 2024 12:31:20 +0100 Subject: [PATCH 140/151] Removed rntxt refs. --- partitura/__init__.py | 1 - partitura/io/__init__.py | 1 - partitura/io/importrntxt.py | 236 ------------------------------------ tests/test_rntxt.py | 39 ------ 4 files changed, 277 deletions(-) delete mode 100644 partitura/io/importrntxt.py delete mode 100644 tests/test_rntxt.py diff --git a/partitura/__init__.py b/partitura/__init__.py index 19da628d..c9efb295 100644 --- a/partitura/__init__.py +++ b/partitura/__init__.py @@ -14,7 +14,6 @@ from .io.exportmusicxml import save_musicxml from .io.importmei import load_mei from .io.importkern import load_kern -from .io.importrntxt import load_rntxt from .io.importmusic21 import load_music21 from .io.importdcml import load_dcml from .io.importmidi import load_score_midi, load_performance_midi, midi_to_notearray diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index 11d337fd..9c3bacb3 100644 --- a/partitura/io/__init__.py +++ b/partitura/io/__init__.py @@ -14,7 +14,6 @@ from .importmatch import load_match from .importmei import load_mei from .importkern import load_kern -from .importrntxt import load_rntxt from .exportkern import save_kern from .importparangonada import load_parangonada_csv from .exportparangonada import save_parangonada_csv diff --git a/partitura/io/importrntxt.py b/partitura/io/importrntxt.py deleted file mode 100644 index 54cebe70..00000000 --- a/partitura/io/importrntxt.py +++ /dev/null @@ -1,236 +0,0 @@ -import re -import partitura.score as spt -import partitura.io as sptio -import os.path as osp -import numpy as np -from urllib.parse import urlparse -import urllib.request -from partitura.utils.music import key_name_to_fifths_mode - - -def load_rntxt(path: spt.Path, part=None, return_part=False): - if sptio.is_url(path): - data = load_data_from_url(path) - lines = data.split("\n") - else: - if not osp.exists(path): - raise FileNotFoundError(f"File not found: {path}") - with open(path, "r") as f: - lines = f.readlines() - assert validate_rntxt(lines) - - # remove empty lines - lines = [line for line in lines if line.strip()] - - parser = RntxtParser(part) - parser.parse(lines) - if return_part or part is None: - return parser.part - return - - -def validate_rntxt(lines): - # TODO: Implement - return True - - -def load_data_from_url(url: str): - with urllib.request.urlopen(url) as response: - data = response.read().decode() - return data - - -class RntxtParser: - """ - A parser for RNtxt format to a partitura Part. - - For full specification of the format visit: - https://github.com/MarkGotham/When-in-Rome/blob/master/syntax.md - """ - def __init__(self, score=None): - # Initialize parser state - self.part = spt.Part(id="rn", part_name="Rn", part_abbreviation="rnp") - self.current_measure = None - self.current_position = 0 - self.measure_beat_position = 1 - self.current_time_signature = spt.TimeSignature(4, 4) - self.time_signature_style = 'Normal' # 'Normal', 'Slow', 'Fast' - self.key = 'C' - self.pedal = None - self.metadata = {} - self.measures = {} - # If a score is provided, copy relevant information - if score is not None: - self._initialize_from_score(score) - else: - # Add default staff - self.part.add(spt.Staff(number=1, lines=5), 0) - - def _initialize_from_score(self, score): - # Copy measures, time signatures, and key signatures from the reference score - self.ref_part = score.parts[0] - for measure in self.ref_part.measures: - self.part.add(measure, measure.start.t, measure.end.t) - for time_sig in self.ref_part.time_sigs: - self.part.add(time_sig, time_sig.start.t) - for key_sig in self.ref_part.key_sigs: - self.part.add(key_sig, key_sig.start.t) - self.measures = {m.number: m for m in self.part.measures} - - def parse(self, lines): - for line_num, line in enumerate(lines, 1): - line = line.strip() - if not line or line.startswith('#'): - continue - try: - if ':' in line: - keyword = line.split(':', 1)[0].strip() - if keyword in ('Composer', 'Title', 'Analyst'): - self._handle_metadata(line) - elif keyword == 'Note': - self._handle_note_line(line) - elif keyword == 'Time Signature': - self._handle_time_signature(line) - elif keyword == 'Pedal': - self._handle_pedal(line) - else: - self._handle_line(line) - else: - self._handle_line(line) - except Exception as e: - print(f"Error parsing line {line_num}: {line}") - print(e) - self._calculate_ending_times() - - def _handle_metadata(self, line): - key, value = line.split(':', 1) - self.metadata[key.strip()] = value.strip() - - def _handle_note_line(self, line): - # Notes can be stored or logged as needed - pass - - def _handle_pedal(self, line): - # Parse pedal information - pass - - def _handle_line(self, line): - if re.match(r'm\d+(-\d+)?\s*=', line): - self._handle_repeat(line) - elif line.startswith('m'): - self._handle_measure(line) - else: - raise ValueError(f"Unknown line format: {line}") - - def _handle_repeat(self, line): - # Implement repeat logic - pass - - def _handle_measure(self, line): - elements = line.strip().split() - measure_info = elements[0] - measure_match = re.match(r'm(\d+)(?:-(\d+))?', measure_info) - if not measure_match: - raise ValueError(f"Invalid measure number: {measure_info}") - measure_number = int(measure_match.group(1)) - if measure_number not in self.measures: - # Check if previous measure is there - if measure_number - 1 in self.measures: - previous_measure_start = self.measures[measure_number - 1].start.t - # get the current time signature - current_time_signature_beats = self.current_time_signature.beats - self.current_position = self.part.beat_map( - self.part.inv_beat_map(previous_measure_start) + current_time_signature_beats) - self.current_measure = spt.Measure(number=measure_number) - self.measures[measure_number] = self.current_measure - self.part.add(self.current_measure, self.current_position) - else: - self.current_measure = self.measures[measure_number] - self.current_position = self.current_measure.start.t - self.measure_beat_position = 1 - for element in elements[1:]: - self._handle_element(element) - - def _handle_element(self, element): - if element.startswith('b'): - beat_match = re.match(r'b(\d+(\.\d+)?)', element) - if beat_match: - self.measure_beat_position = float(beat_match.group(1)) - self._update_current_position() - else: - raise ValueError(f"Invalid beat format: {element}") - elif re.match(r'.*:', element): - self._handle_key(element) - elif all(c in "|:" for c in element): - self._handle_barline(element) - else: - self._handle_roman_numeral(element) - - def _update_current_position(self): - self.current_position = self.part.beat_map(self.part.inv_beat_map(self.current_measure.start.t) + self.measure_beat_position) - - def _get_beat_duration(self): - # Calculate beat duration based on the time signature and style - nom, denom = self.current_time_signature.beats, self.current_time_signature.beat_type - quarter_note_duration = 1 # Assuming a quarter note duration of 1 - beat_duration = (4 / denom) * quarter_note_duration - if self.time_signature_style == 'Fast' and nom % 3 == 0 and denom == 8: - beat_duration *= 3 # Compound meter counted in dotted quarters - elif self.time_signature_style == 'Slow' and nom % 3 == 0 and denom == 8: - beat_duration /= 3 # Compound meter counted in eighth notes - return beat_duration - - def _handle_time_signature(self, line): - time_signature = line.split(':', 1)[1].strip() - style = 'Normal' - if 'Slow' in time_signature: - style = 'Slow' - time_signature = time_signature.replace('Slow', '').strip() - elif 'Fast' in time_signature: - style = 'Fast' - time_signature = time_signature.replace('Fast', '').strip() - if time_signature == 'C': - nom, denom = 4, 4 - elif time_signature == 'Cut': - nom, denom = 2, 2 - else: - nom, denom = map(int, time_signature.split('/')) - self.current_time_signature = spt.TimeSignature(nom, denom) - self.time_signature_style = style - self.part.add(self.current_time_signature, self.current_position) - - def _handle_barline(self, element): - # Implement barline handling if needed - pass - - def _handle_roman_numeral(self, element): - try: - rn = spt.RomanNumeral(text=element, local_key=self.key) - self.part.add(rn, self.current_position) - except Exception as e: - raise ValueError(f"Error parsing Roman numeral '{element}': {e}") - - def _handle_key(self, element): - match = re.match(r'([A-Ga-g])([#b]*):', element) - if not match: - raise ValueError(f"Invalid key signature: {element}") - note, accidental = match.groups() - mode = 'minor' if note.islower() else 'major' - key_name = note.upper() + accidental - key_str = f"{key_name}{('m' if mode == 'minor' else '')}" - fifths, mode = key_name_to_fifths_mode(key_str) - ks = spt.KeySignature(fifths=fifths, mode=mode) - self.key = element.strip(":") - self.part.add(ks, self.current_position) - - def _calculate_ending_times(self): - romans = sorted(self.part.iter_all(spt.RomanNumeral), key=lambda rn: rn.start.t) - for i, rn in enumerate(romans[:-1]): - rn.end = spt.TimePoint(t=romans[i + 1].start.t) - if romans: - last_rn = romans[-1] - last_rn.end = self.part.end_time or (last_rn.start.t + 1) - - - - diff --git a/tests/test_rntxt.py b/tests/test_rntxt.py deleted file mode 100644 index fb8569c7..00000000 --- a/tests/test_rntxt.py +++ /dev/null @@ -1,39 +0,0 @@ -from partitura import load_rntxt, load_kern -from partitura.score import RomanNumeral -from partitura import load_musicxml -import urllib.request -import unittest -import os -from tests import KERN_PATH - - -class TextRNtxtImport(unittest.TestCase): - - def test_chorale_001_from_url(self): - score_path = os.path.join(KERN_PATH, "chor228.krn") - rntxt_url = "https://raw.githubusercontent.com/MarkGotham/When-in-Rome/master/Corpus/Early_Choral/Bach%2C_Johann_Sebastian/Chorales/228/analysis.txt" - score = load_kern(score_path) - rn_part = load_rntxt(rntxt_url, score, return_part=True) - romans = list(rn_part.iter_all(RomanNumeral)) - roots = [r.root for r in romans] - bass = [r.bass_note for r in romans] - primary_degree = [r.primary_degree for r in romans] - secondary_degree = [r.secondary_degree for r in romans] - local_key = [r.local_key for r in romans] - quality = [r.quality for r in romans] - inversion = [r.inversion for r in romans] - expected_roots = ['A', 'A', 'E', 'A', 'A', 'G', 'C', 'C', 'G', 'G', 'A', 'A', 'E', 'E', 'E', 'E', 'A', 'D', 'G#', 'A', 'E', 'A', 'D', 'A', 'B'] - expected_bass = ['A', 'A', 'G#', 'A', 'A', 'B', 'C', 'E', 'G', 'G', 'A', 'A', 'E', 'E', 'E', 'D', 'C', 'C', 'B', 'A', 'E', 'A', 'F', 'E', 'D'] - expected_pdegree = ['i', 'i', 'V', 'i', 'vi', 'V', 'I', 'I', 'V', 'V', 'vi', 'i', 'V', 'V', 'V', 'V', 'i', 'IV', 'viio', 'i', 'V', 'i', 'iv', 'i', 'iio'] - expected_sdegree = ['i', 'i', 'i', 'i', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i'] - expected_lkey = ['a', 'a', 'a', 'a', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'] - expected_quality = ['min', 'min', 'maj', 'min', 'min', 'maj', 'maj', 'maj', 'maj', '7', 'min', 'min', 'maj', 'maj', 'maj', '7', 'min', '7', 'dim', 'min', 'maj', 'min', 'min', 'min', 'dim7'] - expected_inversion = [0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 3, 1, 3, 1, 0, 0, 0, 1, 2, 1] - self.assertEqual(roots, expected_roots) - self.assertEqual(bass, expected_bass) - self.assertEqual(primary_degree, expected_pdegree) - self.assertEqual(secondary_degree, expected_sdegree) - self.assertEqual(local_key, expected_lkey) - self.assertEqual(quality, expected_quality) - self.assertEqual(inversion, expected_inversion) - From 86117e8ffee5693fd3a84cc3e1ab44a481e9dc6e Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 3 Dec 2024 12:34:16 +0100 Subject: [PATCH 141/151] 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, From 03e062b5830fc264c8ec9836f2f349200ca1aaa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20G=C3=A9r=C3=A9?= <71326140+leleogere@users.noreply.github.com> Date: Thu, 19 Dec 2024 11:00:01 +0100 Subject: [PATCH 142/151] Sort exported notes by step when same midi_pitch --- partitura/io/exportmusicxml.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/partitura/io/exportmusicxml.py b/partitura/io/exportmusicxml.py index 9e349abd..e87f15ca 100644 --- a/partitura/io/exportmusicxml.py +++ b/partitura/io/exportmusicxml.py @@ -417,9 +417,10 @@ def linearize_segment_contents(part, start, end, state): for voice in sorted(notes_by_voice.keys()): voice_notes = notes_by_voice[voice] - # sort by pitch + # sort by pitch (then step in case of enharmonic notes) voice_notes.sort( - key=lambda n: n.midi_pitch if hasattr(n, "midi_pitch") else -1, reverse=True + key=lambda n: (n.midi_pitch, n.step) if hasattr(n, "midi_pitch") else (-1, ""), + reverse=True, ) # grace notes should precede other notes at the same onset voice_notes.sort(key=lambda n: not isinstance(n, score.GraceNote)) From 9b824123c1a32ce8bfda86635e9bf0652f0237bf Mon Sep 17 00:00:00 2001 From: fosfrancesco Date: Thu, 19 Dec 2024 14:26:29 +0000 Subject: [PATCH 143/151] Format code with black (bot) --- partitura/io/exportmusicxml.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/partitura/io/exportmusicxml.py b/partitura/io/exportmusicxml.py index e87f15ca..83606988 100644 --- a/partitura/io/exportmusicxml.py +++ b/partitura/io/exportmusicxml.py @@ -419,7 +419,9 @@ def linearize_segment_contents(part, start, end, state): voice_notes = notes_by_voice[voice] # sort by pitch (then step in case of enharmonic notes) voice_notes.sort( - key=lambda n: (n.midi_pitch, n.step) if hasattr(n, "midi_pitch") else (-1, ""), + key=lambda n: ( + (n.midi_pitch, n.step) if hasattr(n, "midi_pitch") else (-1, "") + ), reverse=True, ) # grace notes should precede other notes at the same onset From 9c31c5b24a12782b0df76f85e9bdea5f1aa0ef49 Mon Sep 17 00:00:00 2001 From: fosfrancesco Date: Thu, 19 Dec 2024 14:39:31 +0000 Subject: [PATCH 144/151] Format code with black (bot) --- partitura/io/exportmei.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index c4ae4b15..3479b36f 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -402,7 +402,9 @@ def _handle_beams(self, measure_el: lxml.etree._Element, start: int, end: int): if note_el.getparent() != beam_el: beam_el.append(note_el) - def _handle_clef_changes(self, measure_el: lxml.etree._Element, start: int, end: int): + 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: @@ -556,7 +558,10 @@ def _handle_fingering(self, measure_el: lxml.etree._Element, start: int, end: in for note in self.part.iter_all(spt.Note, start=start, end=end): if note.technical is not None: for technical_notation in note.technical: - if isinstance(technical_notation, score.Fingering) and note.id is not None: + 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) From e1787fc2d53e763931fb1bc0621935f2df60da43 Mon Sep 17 00:00:00 2001 From: manoskary Date: Tue, 24 Dec 2024 10:58:05 +0100 Subject: [PATCH 145/151] set to dlt temp file after url loading --- partitura/io/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index 9c3bacb3..8b661ee7 100644 --- a/partitura/io/__init__.py +++ b/partitura/io/__init__.py @@ -51,7 +51,7 @@ def download_file(url): extension = os.path.splitext(url)[-1] # Create a temporary file - temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=extension) + temp_file = tempfile.NamedTemporaryFile(delete=True, suffix=extension) # Write the content to the temporary file with open(temp_file.name, 'wb') as f: From 59c148c2b4d7524f6fa0c8c2d31aedb96cc8befb Mon Sep 17 00:00:00 2001 From: manoskary Date: Tue, 24 Dec 2024 11:06:30 +0100 Subject: [PATCH 146/151] delete on close fix. --- partitura/io/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index 8b661ee7..6fbe2063 100644 --- a/partitura/io/__init__.py +++ b/partitura/io/__init__.py @@ -51,7 +51,7 @@ def download_file(url): extension = os.path.splitext(url)[-1] # Create a temporary file - temp_file = tempfile.NamedTemporaryFile(delete=True, suffix=extension) + temp_file = tempfile.NamedTemporaryFile(suffix=extension, delete_on_close=True) # Write the content to the temporary file with open(temp_file.name, 'wb') as f: From e41185ff1419d9db76b7329c43f92fb2a0b80783 Mon Sep 17 00:00:00 2001 From: manoskary Date: Tue, 24 Dec 2024 11:21:32 +0100 Subject: [PATCH 147/151] yet another fix for delete on close. delete_on_close=False and delete=True should now delete file upon session end. --- partitura/io/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index 6fbe2063..513e73a3 100644 --- a/partitura/io/__init__.py +++ b/partitura/io/__init__.py @@ -51,7 +51,7 @@ def download_file(url): extension = os.path.splitext(url)[-1] # Create a temporary file - temp_file = tempfile.NamedTemporaryFile(suffix=extension, delete_on_close=True) + temp_file = tempfile.NamedTemporaryFile(suffix=extension, delete_on_close=False, delete=True) # Write the content to the temporary file with open(temp_file.name, 'wb') as f: From 108a4a62e7d379c0076da355c7af3efad3982814 Mon Sep 17 00:00:00 2001 From: manoskary Date: Fri, 27 Dec 2024 11:49:21 +0100 Subject: [PATCH 148/151] fixed temp file delete in url load options for delete on close are not available on python version 3.8 therefore more restructuring on the code was needed. --- partitura/io/__init__.py | 50 +++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index 513e73a3..192eda61 100644 --- a/partitura/io/__init__.py +++ b/partitura/io/__init__.py @@ -42,24 +42,6 @@ def is_url(input): return False -def download_file(url): - # Send a GET request to the URL - with urllib.request.urlopen(url) as response: - data = response.read() - - # Extract the file extension from the URL - extension = os.path.splitext(url)[-1] - - # Create a temporary file - temp_file = tempfile.NamedTemporaryFile(suffix=extension, delete_on_close=False, delete=True) - - # Write the content to the temporary file - with open(temp_file.name, 'wb') as f: - f.write(data) - - return temp_file.name - - @deprecated_alias(score_fn="filename") @deprecated_parameter("ensure_list") def load_score(filename: PathLike, force_note_ids="keep") -> Score: @@ -86,12 +68,28 @@ def load_score(filename: PathLike, force_note_ids="keep") -> Score: A score instance. """ if is_url(filename): - filename = download_file(filename) + url = filename + # Send a GET request to the URL + with urllib.request.urlopen(url) as response: + data = response.read() + + # Extract the file extension from the URL + extension = os.path.splitext(url)[-1] + + # Create a temporary file + temp_file = tempfile.NamedTemporaryFile(suffix=extension, delete=True) + + # Write the content to the temporary file + with open(temp_file.name, 'wb') as f: + f.write(data) + + filename = temp_file.name + else: + extension = os.path.splitext(filename)[-1].lower() - extension = os.path.splitext(filename)[-1].lower() if extension in (".mxl", ".xml", ".musicxml"): # Load MusicXML - return load_musicxml( + score = load_musicxml( filename=filename, force_note_ids=force_note_ids, ) @@ -101,15 +99,15 @@ def load_score(filename: PathLike, force_note_ids="keep") -> Score: assign_note_ids = False else: assign_note_ids = True - return load_score_midi( + score = load_score_midi( filename=filename, assign_note_ids=assign_note_ids, ) elif extension in [".mei"]: # Load MEI - return load_mei(filename=filename) + score = load_mei(filename=filename) elif extension in [".kern", ".krn"]: - return load_kern( + score = load_kern( filename=filename, force_note_ids=force_note_ids, ) @@ -137,7 +135,7 @@ def load_score(filename: PathLike, force_note_ids="keep") -> Score: ".gp", ]: # Load MuseScore - return load_via_musescore( + score = load_via_musescore( filename=filename, force_note_ids=force_note_ids, ) @@ -147,11 +145,11 @@ def load_score(filename: PathLike, force_note_ids="keep") -> Score: filename=filename, create_score=True, ) - return score else: raise NotSupportedFormatError( f"{extension} file extension is not supported. If this should be supported, consider editing partitura/io/__init__.py file" ) + return score def load_score_as_part(filename: PathLike) -> Part: From 356429d593585008d5e7abc13daa352eb69efefb Mon Sep 17 00:00:00 2001 From: manoskary Date: Fri, 10 Jan 2025 16:11:52 +0000 Subject: [PATCH 149/151] Format code with black (bot) --- partitura/io/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index 192eda61..59ca7d50 100644 --- a/partitura/io/__init__.py +++ b/partitura/io/__init__.py @@ -80,7 +80,7 @@ def load_score(filename: PathLike, force_note_ids="keep") -> Score: temp_file = tempfile.NamedTemporaryFile(suffix=extension, delete=True) # Write the content to the temporary file - with open(temp_file.name, 'wb') as f: + with open(temp_file.name, "wb") as f: f.write(data) filename = temp_file.name 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 150/151] 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 From b8b13224a21291ec9e751f11393cc84669e17a0a Mon Sep 17 00:00:00 2001 From: CarlosCancino-Chacon Date: Sun, 19 Jan 2025 07:43:26 +0000 Subject: [PATCH 151/151] Format code with black (bot) --- partitura/io/importmusicxml.py | 11 ++++++++--- partitura/score.py | 2 +- partitura/utils/misc.py | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index 2632432c..4964cb78 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -21,7 +21,12 @@ 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, parse_ints +from partitura.utils.misc import ( + deprecated_alias, + deprecated_parameter, + PathLike, + parse_ints, +) __all__ = ["load_musicxml", "musicxml_to_notearray"] @@ -1679,8 +1684,8 @@ def parse_fingering(e: etree._Element) -> score.Fingering: 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 + # 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: diff --git a/partitura/score.py b/partitura/score.py index 454c59cd..d7ebd2f6 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -3409,7 +3409,7 @@ class Fingering(NoteTechnicalNotation): 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 diff --git a/partitura/utils/misc.py b/partitura/utils/misc.py index c38c13e8..640514c8 100644 --- a/partitura/utils/misc.py +++ b/partitura/utils/misc.py @@ -298,6 +298,6 @@ def parse_ints(input_string: str) -> List[int]: A list of integers extracted from the input string. """ # Regular expression to match numbers - pattern = r'\d+' + 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 + return list(map(int, re.findall(pattern, input_string)))