From acd22f0794715ae4db42fc24e28ea1cba27c2624 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 7 Jul 2023 18:09:48 +0200 Subject: [PATCH 001/197] correct parsing for grace notes. --- partitura/io/importkern.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index da0fecea..722852d4 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -84,6 +84,7 @@ def __init__(self, stream, init_pos, doc_name, part_id, qdivs, barline_dict=None self.parsing = "full" self.stream = stream self.prev_measure_pos = init_pos + self.EDITORIAL_SYMBOLS = ["x", "p", "q", "<", "(", ">", ")"] # Check if part has pickup measure. self.measure_count = ( 0 if np.all(np.char.startswith(stream, "=1-") == False) else 1 @@ -384,7 +385,8 @@ def _handle_note(self, note, note_id, voice=1): grace_attr = "q" in note # or "p" in note # for appoggiatura not sure yet. duration, symbolic_duration, ntype = self._handle_duration(note, grace_attr) # Remove editorial symbols from string, i.e. "x" - ntype = ntype.replace("x", "") + for x in self.EDITORIAL_SYMBOLS: + ntype = ntype.replace(x, "") step, octave = self.KERN_NOTES[ntype[0]] if octave == 4: octave = octave + ntype.count(ntype[0]) - 1 @@ -408,10 +410,12 @@ def _handle_note(self, note, note_id, voice=1): self._handle_fermata(note) else: # create grace note - if "p" in ntype: + if "p" in note: grace_type = "acciaccatura" - elif "q" in ntype: + elif "q" in note: grace_type = "appoggiatura" + else: + raise ValueError("Grace note not recognized") note = score.GraceNote( grace_type=grace_type, step=step, From 002553b4868e4d1f75942791360aa851b48c8bbe Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 10 Jul 2023 12:35:30 +0200 Subject: [PATCH 002/197] Multiple spine expansion fix. --- partitura/io/importkern.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index 722852d4..0e52e909 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -563,6 +563,7 @@ def parse_kern(kern_path: PathLike) -> np.ndarray: striped_parts.append(x) if "*^" in x or "*+": # Accounting for multiple voice ups at the same time. + already_parsed = 0 for i, el in enumerate(x): # Some faulty kerns create an extra part half way through the score. # We choose for the moment to add it to the closest column part. @@ -571,7 +572,10 @@ def parse_kern(kern_path: PathLike) -> np.ndarray: if merge_index: if k < min(merge_index): merge_index = [midx + 1 for midx in merge_index] + else: + k = i + already_parsed merge_index.append(k) + already_parsed += 1 if "*v *v" in x: k = x.index("*v *v") temp = list() @@ -585,6 +589,7 @@ def parse_kern(kern_path: PathLike) -> np.ndarray: # extra empty voice and would mess the parsing. striped_parts = [[el for el in part if el != ""] for part in striped_parts] numpy_parts = np.array(list(zip(striped_parts))).squeeze(1).T + # numpy_parts = np.array(striped_parts).squeeze(1).T return numpy_parts From 769d3d9da0013d3df9647ceedf4ab232ff1d91f3 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 10 Jul 2023 12:56:04 +0200 Subject: [PATCH 003/197] parsing fix for rational durations in kern. --- partitura/io/importkern.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index 0e52e909..f25bf6f7 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -333,12 +333,17 @@ def _search_slurs_and_ties(self, note, note_id): return note def _handle_duration(self, note, isgrace=False): - if isgrace: - _, dur, ntype = re.split("(\d+)", note) - ntype = _ + ntype + foundRational = re.search(r'(\d+)%(\d+)', note) + if foundRational: + ntype = note[foundRational.span()[-1]:] + durationFirst = int(foundRational.group(1)) + durationSecond = float(foundRational.group(2)) + dur = 4 * durationSecond / durationFirst else: _, dur, ntype = re.split("(\d+)", note) - dur = eval(dur) + ntype = _ + ntype if isgrace else ntype + dur = eval(dur) + if dur in self.KERN_DURS.keys(): symbolic_duration = {"type": self.KERN_DURS[dur]} else: @@ -531,7 +536,7 @@ def find_lcm(self, doc): durs, _ = zip(*match) x = np.array(list(map(lambda x: int(x), durs))) divs = np.lcm.reduce(np.unique(x[x != 0])) - return float(divs) / 4.00 + return float(divs) # / 4.00 # functions to initialize the kern parser From ecf360c2456499fa3f0fa832cb96143ec9d44ce5 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 11 Jul 2023 12:41:48 +0200 Subject: [PATCH 004/197] Fixes on loading kern files. --- partitura/io/importkern.py | 76 +++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index f25bf6f7..29adc328 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -203,6 +203,8 @@ def _handle_slurs(self): def _handle_metersig(self, metersig): m = metersig[2:] + if " " in m: + m = m.split(" ")[0] numerator, denominator = map(eval, m.split("/")) new_time_signature = score.TimeSignature(numerator, denominator) self.part.add(new_time_signature, self.position) @@ -383,7 +385,7 @@ def _handle_duration(self, note, isgrace=False): # TODO Handle beams and tuplets. def _handle_note(self, note, note_id, voice=1): - if note == ".": + if note == "." or note == "" or note == " ": return has_fermata = ";" in note note = self._search_slurs_and_ties(note, note_id) @@ -566,7 +568,7 @@ def parse_kern(kern_path: PathLike) -> np.ndarray: striped_parts.append(y) else: striped_parts.append(x) - if "*^" in x or "*+": + if "*^" in x or "*+" in x: # Accounting for multiple voice ups at the same time. already_parsed = 0 for i, el in enumerate(x): @@ -577,10 +579,10 @@ def parse_kern(kern_path: PathLike) -> np.ndarray: if merge_index: if k < min(merge_index): merge_index = [midx + 1 for midx in merge_index] - else: - k = i + already_parsed + k = i + already_parsed merge_index.append(k) already_parsed += 1 + merge_index = sorted(merge_index) if "*v *v" in x: k = x.index("*v *v") temp = list() @@ -590,14 +592,76 @@ def parse_kern(kern_path: PathLike) -> np.ndarray: elif i < k: temp.append(i) merge_index = temp + # Final filter for mistabs and inconsistent tabs that would create # extra empty voice and would mess the parsing. striped_parts = [[el for el in part if el != ""] for part in striped_parts] numpy_parts = np.array(list(zip(striped_parts))).squeeze(1).T - # numpy_parts = np.array(striped_parts).squeeze(1).T return numpy_parts +def parse_kern_v2(kern_path: PathLike) -> np.ndarray: + """ + Parses an KERN file from path to an regular expression. + + Parameters + ---------- + kern_path : PathLike + The path of the KERN document. + Returns + ------- + continuous_parts : numpy character array + non_continuous_parts : list + """ + with open(kern_path, encoding="cp437") as file: + lines = file.read().splitlines() + document_lines = [line.split("\t") for line in lines if not line.startswith("!")] + number_of_voices = len(document_lines[0]) + number_of_lines = len(document_lines) + out = np.empty((number_of_lines, number_of_voices), dtype=" Date: Mon, 17 Jul 2023 12:07:13 +0200 Subject: [PATCH 005/197] Fixes on loading kern files. --- partitura/io/importkern.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index 29adc328..d4a03640 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -84,7 +84,7 @@ def __init__(self, stream, init_pos, doc_name, part_id, qdivs, barline_dict=None self.parsing = "full" self.stream = stream self.prev_measure_pos = init_pos - self.EDITORIAL_SYMBOLS = ["x", "p", "q", "<", "(", ">", ")"] + self.EDITORIAL_SYMBOLS = ["x", "p", "q", "<", "(", ">", ")", "[", "]"] # Check if part has pickup measure. self.measure_count = ( 0 if np.all(np.char.startswith(stream, "=1-") == False) else 1 @@ -417,12 +417,10 @@ def _handle_note(self, note, note_id, voice=1): self._handle_fermata(note) else: # create grace note - if "p" in note: + if "p" in ntype: grace_type = "acciaccatura" - elif "q" in note: + elif "q" in ntype: grace_type = "appoggiatura" - else: - raise ValueError("Grace note not recognized") note = score.GraceNote( grace_type=grace_type, step=step, From a29879e4fb186186d26ee6426edcc3821d554167 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 2 Aug 2023 11:52:16 +0200 Subject: [PATCH 006/197] Moved qdivs computation to the document level. --- partitura/io/importkern.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index d4a03640..8f79b4c8 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -476,6 +476,7 @@ class KernParser: def __init__(self, document, doc_name): self.document = document self.doc_name = doc_name + self.qdivs = self.find_lcm(document.flatten()) # TODO review this code self.DIVS2Q = { 1: 0.25, @@ -522,7 +523,7 @@ def add2part(self, part, unprocessed): def collect(self, doc, pos, id, doc_name): if doc[0] == "**kern": - qdivs = self.find_lcm(doc) + qdivs = self.find_lcm(doc) if self.qdivs is None else self.qdivs x = KernParserPart(doc, pos, id, doc_name, qdivs).part return x From dfeebdfbc5c4d9d59dd2d7e1bd19c3992116add7 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 2 Aug 2023 15:57:51 +0200 Subject: [PATCH 007/197] minor corrections for correct parsing and unit tests. --- partitura/io/importkern.py | 3 +++ partitura/io/musescore.py | 16 ++++++++++------ tests/__init__.py | 2 +- tests/data/musescore/mozart_k265_var1.mscz | Bin 33081 -> 0 bytes tests/test_musescore.py | 6 +++--- tests/test_note_array.py | 6 +++--- 6 files changed, 20 insertions(+), 13 deletions(-) delete mode 100644 tests/data/musescore/mozart_k265_var1.mscz diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index 6cba4d83..34683ef3 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -617,6 +617,9 @@ def parse_kern_v2(kern_path: PathLike) -> np.ndarray: """ with open(kern_path, encoding="cp437") as file: lines = file.read().splitlines() + if lines[0][0] == "<": + # we are using a heuristic to stop the import if we are dealing with a XML file + raise Exception("Invalid Kern file") document_lines = [line.split("\t") for line in lines if not line.startswith("!")] number_of_voices = len(document_lines[0]) number_of_lines = len(document_lines) diff --git a/partitura/io/musescore.py b/partitura/io/musescore.py index 223d2e90..6ea1285e 100644 --- a/partitura/io/musescore.py +++ b/partitura/io/musescore.py @@ -121,17 +121,20 @@ def load_via_musescore( """ # open the file as text and check if the first symbol is "<" to avoid # further processing in case of non-XML files - with open(filename, "r") as f: - if f.read(1) != "<": - raise FileImportException( - "File {} is not a valid XML file.".format(filename) - ) + if filename.endswith(".mscz"): + pass + else: + with open(filename, "r") as f: + if f.read(1) != "<": + raise FileImportException( + "File {} is not a valid XML file.".format(filename) + ) mscore_exec = find_musescore() xml_fh = os.path.splitext(os.path.basename(filename))[0] + ".musicxml" - cmd = [mscore_exec, "-o", xml_fh, filename] + cmd = [mscore_exec, "-o", xml_fh, filename, "-f"] try: ps = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) @@ -210,6 +213,7 @@ def render_musescore( "-o", os.fspath(img_fh), os.fspath(xml_fh), + "-f" ] try: ps = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) diff --git a/tests/__init__.py b/tests/__init__.py index 2a3dad52..ce7bf09c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -187,7 +187,7 @@ MUSESCORE_TESTFILES = [ os.path.join(DATA_PATH, "musescore", fn) - for fn in ["mozart_k265_var1.mscz", "160.03_Pastorale.mscx"] + for fn in ["160.03_Pastorale.mscx"] ] KERN_TIES = [os.path.join(KERN_PATH, fn) for fn in ["tie_mismatch.krn"]] diff --git a/tests/data/musescore/mozart_k265_var1.mscz b/tests/data/musescore/mozart_k265_var1.mscz deleted file mode 100644 index 54a96f328416853774c727c61e7c7e410927a301..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33081 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 diff --git a/tests/test_musescore.py b/tests/test_musescore.py index e99728b4..9797f785 100644 --- a/tests/test_musescore.py +++ b/tests/test_musescore.py @@ -26,17 +26,17 @@ def test_number_of_parts1(self): if platform.system() == "Linux": score = load_via_musescore(MUSESCORE_TESTFILES[0]) self.assertTrue(len(score.parts) == 1) - self.assertTrue(len(score.note_array()) == 218) + self.assertTrue(len(score.note_array()) == 984) else: self.skipTest("MuseScore test can't run on non-linux environment in github actions") def test_epfl_scores(self): # dirty trick, since we can install Musescore only on linux environment in github actions if platform.system() == "Linux": - score = load_via_musescore(MUSESCORE_TESTFILES[1]) + score = load_via_musescore(MUSESCORE_TESTFILES[0]) self.assertTrue(len(score.parts) == 1) # try the generic loading function - score = load_score(MUSESCORE_TESTFILES[1]) + score = load_score(MUSESCORE_TESTFILES[0]) self.assertTrue(len(score.parts) == 1) else: self.skipTest("MuseScore test can't run on non-linux environment in github actions") \ No newline at end of file diff --git a/tests/test_note_array.py b/tests/test_note_array.py index 3f6489d0..54cf5770 100644 --- a/tests/test_note_array.py +++ b/tests/test_note_array.py @@ -175,9 +175,9 @@ def test_ensure_na_different_divs(self): # note_arrays = [p.note_array(include_divs_per_quarter= True) for p in parts] merged_note_array = ensure_notearray(parts) for note in merged_note_array[-4:]: - self.assertTrue(note["onset_div"] == 92) - self.assertTrue(note["duration_div"] == 4) - self.assertTrue(note["divs_pq"] == 4) + self.assertTrue(note["onset_div"] == 368) + self.assertTrue(note["duration_div"] == 16) + self.assertTrue(note["divs_pq"] == 16) def test_score_notearray_method(self): """ From 11cc1193dbb3f6d78bdb55c03e1fc63ae36579c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Sun, 19 Nov 2023 16:04:04 +0100 Subject: [PATCH 008/197] add MetaMessage info to PerformedPart objects --- partitura/io/exportmidi.py | 39 +++++++++++++++++++- partitura/io/importmidi.py | 75 ++++++++++++++++++++++++++++++++++++-- partitura/performance.py | 6 +++ 3 files changed, 115 insertions(+), 5 deletions(-) diff --git a/partitura/io/exportmidi.py b/partitura/io/exportmidi.py index c1639bb1..bda2fbbe 100644 --- a/partitura/io/exportmidi.py +++ b/partitura/io/exportmidi.py @@ -13,7 +13,7 @@ import partitura.score as score from partitura.score import Score, Part, PartGroup, ScoreLike from partitura.performance import Performance, PerformedPart, PerformanceLike -from partitura.utils import partition +from partitura.utils import partition, fifths_mode_to_key_name from partitura.utils.misc import deprecated_alias, PathLike @@ -138,6 +138,43 @@ def save_performance_midi( track_events = defaultdict(lambda: defaultdict(list)) for performed_part in performed_parts: + + for c in performed_part.meta_other: + track = c.get("track", 0) + t = int(np.round(10**6 * ppq * c["time"] / mpq)) + msg_info = dict( + [ + (key, val) + for key, val in c.items() + if key not in ("time", "time_tick", "track") + ] + ) + track_events[track][t].append(MetaMessage(**msg_info)) + + for c in performed_part.key_signatures: + track = c.get("track", 0) + t = int(np.round(10**6 * ppq * c["time"] / mpq)) + track_events[track][t].append( + MetaMessage( + type="key_signature", + key=fifths_mode_to_key_name( + fifths=c.get("fifths", 0), + mode=c.get("mode", None), + ), + ) + ) + + for c in performed_part.time_signatures: + track = c.get("track", 0) + t = int(np.round(10**6 * ppq * c["time"] / mpq)) + track_events[track][t].append( + MetaMessage( + type="time_signature", + numerator=c.get("beats", 4), + denominator=c.get("beat_type", 4), + ), + ) + for c in performed_part.controls: track = c.get("track", 0) ch = c.get("channel", 1) diff --git a/partitura/io/importmidi.py b/partitura/io/importmidi.py index 5843b781..71a37b9d 100644 --- a/partitura/io/importmidi.py +++ b/partitura/io/importmidi.py @@ -127,6 +127,15 @@ def load_performance_midi( notes = [] controls = [] programs = [] + # This information is just for completeness, + # but loading a MIDI file as a performance + # assumes that key and time signature information + # is not reliable (e.g., a performance recorded with + # a MIDI keyboard, without metronome) + key_signatures = [] + time_signatures = [] + # other MetaMessages (not including key and time_signature) + meta_other = [] t = 0 ttick = 0 @@ -138,9 +147,59 @@ def load_performance_midi( t = t + msg.time * time_conversion_factor ttick = ttick + msg.time - if msg.type == "set_tempo": - mpq = msg.tempo - time_conversion_factor = mpq / (ppq * 10**6) + if isinstance(msg, mido.MetaMessage): + # Meta Messages apply to all channels in the track + + # The tempo is set globally in PerformedParts, + # i.e., the tempo_conversion_factor is adjusted + # with every tempo change, rather than creating new + # tempo events. + if msg.type == "set_tempo": + mpq = msg.tempo + time_conversion_factor = mpq / (ppq * 10**6) + + elif msg.type == "time_signature": + time_signatures.append( + dict( + time=t, + time_tick=ttick, + beats=int(msg.numerator), + beat_type=int(msg.denominator), + track=i, + ) + ) + elif msg.type == "key_signature": + key_name = str(msg.key) + fifths, mode = key_name_to_fifths_mode(key_name) + key_signatures.append( + dict( + time=t, + time_tick=ttick, + key_name=str(msg.key), + fifths=fifths, + mode=mode, + track=i, + ) + ) + + else: + # Other MetaMessages + # For more info, see + # https://mido.readthedocs.io/en/latest/meta_message_types.html + msg_dict = dict( + [ + ("time", t), + ("time_tick", ttick), + ("track", i), + ] + + [ + (key, val) + for key, val in msg.__dict__.items() + if key not in ("time", "track", "time_tick") + ] + ) + + meta_other.append(msg_dict) elif msg.type == "control_change": controls.append( @@ -221,7 +280,15 @@ def load_performance_midi( if len(notes) > 0 or len(controls) > 0 or len(programs) > 0: pp = performance.PerformedPart( - notes, controls=controls, programs=programs, ppq=ppq, mpq=mpq, track=i + notes, + controls=controls, + programs=programs, + key_signatures=key_signatures, + time_signatures=time_signatures, + meta_other=meta_other, + ppq=ppq, + mpq=mpq, + track=i, ) pps.append(pp) diff --git a/partitura/performance.py b/partitura/performance.py index d0818d7f..58b89681 100644 --- a/partitura/performance.py +++ b/partitura/performance.py @@ -77,6 +77,9 @@ def __init__( part_name: str = None, controls: List[dict] = None, programs: List[dict] = None, + key_signatures: List[dict] = None, + time_signatures: List[dict] = None, + meta_other: List[dict] = None, sustain_pedal_threshold: int = 64, ppq: int = 480, mpq: int = 500000, @@ -92,6 +95,9 @@ def __init__( ) self.controls = controls or [] self.programs = programs or [] + self.time_signatures = time_signatures or [] + self.key_signature = key_signatures or [] + self.meta_other = meta_other or [] self.ppq = ppq self.mpq = mpq self.track = track From dbfa2fa47d3a408c2e6e61b565b4403828c3fa37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Sun, 19 Nov 2023 16:05:01 +0100 Subject: [PATCH 009/197] add typing annotations (wip) --- partitura/io/importmidi.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/partitura/io/importmidi.py b/partitura/io/importmidi.py index 71a37b9d..8a754f77 100644 --- a/partitura/io/importmidi.py +++ b/partitura/io/importmidi.py @@ -6,7 +6,7 @@ import warnings from collections import defaultdict -from typing import Union, Optional +from typing import Union, Optional, List, Tuple import numpy as np @@ -409,7 +409,13 @@ def load_score_midi( track_names_by_track = {} # notes are indexed by (track, channel) tuples notes_by_track_ch = {} - relevant = {"time_signature", "key_signature", "set_tempo", "note_on", "note_off"} + relevant = { + "time_signature", + "key_signature", + "set_tempo", + "note_on", + "note_off", + } for track_nr, track in enumerate(mid.tracks): time_sigs = [] key_sigs = [] @@ -714,15 +720,15 @@ def assign_group_part_voice(mode, track_ch_combis, track_names): def create_part( - ticks, - notes, - spellings, - voices, - note_ids, - time_sigs, - key_sigs, - part_id=None, - part_name=None, + ticks: int, + notes: List[Tuple[int, int, int]], + spellings: List[Tuple[str, str, int]], + voices: List[int], + note_ids: List[str], + time_sigs: List[Tuple[int, int, int]], + key_sigs: List[Tuple[int, str]], + part_id: Optional[str] = None, + part_name: Optional[str] = None, ) -> score.Part: warnings.warn("create_part", stacklevel=2) @@ -814,7 +820,10 @@ def create_part( return part -def quantize(v, unit): +def quantize( + v: Union[np.ndarray, float, int], + unit: Union[float, int], +) -> Union[np.ndarray, float, int]: """Quantize value `v` to a multiple of `unit`. When `unit` is an integer, the return value will be integer as well, otherwise the function will return a float. From c43ad365900dc65764ae3efcc9b6496f878ff248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Sun, 19 Nov 2023 16:42:58 +0100 Subject: [PATCH 010/197] fix typo --- partitura/performance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/performance.py b/partitura/performance.py index 58b89681..8957491b 100644 --- a/partitura/performance.py +++ b/partitura/performance.py @@ -96,7 +96,7 @@ def __init__( self.controls = controls or [] self.programs = programs or [] self.time_signatures = time_signatures or [] - self.key_signature = key_signatures or [] + self.key_signatures = key_signatures or [] self.meta_other = meta_other or [] self.ppq = ppq self.mpq = mpq From cfe091766c08c7603d44695a84c967cefc26cd40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Sun, 19 Nov 2023 16:43:25 +0100 Subject: [PATCH 011/197] add typing and documentation (wip) --- partitura/io/importmidi.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/partitura/io/importmidi.py b/partitura/io/importmidi.py index 8a754f77..96ee2756 100644 --- a/partitura/io/importmidi.py +++ b/partitura/io/importmidi.py @@ -6,7 +6,7 @@ import warnings from collections import defaultdict -from typing import Union, Optional, List, Tuple +from typing import Union, Optional, List, Tuple, Dict import numpy as np @@ -655,7 +655,11 @@ def make_track_to_part_mapping(tr_ch_keys, group_part_voice_keys): return track_to_part_keys -def assign_group_part_voice(mode, track_ch_combis, track_names): +def assign_group_part_voice( + mode: int, + track_ch_combis: Dict[Tuple[int, int], List], + track_names: Dict[int, str], +) -> Tuple[List[Tuple], Dict, Dict]: """ 0: return one Part per track, with voices assigned by channel 1. return one PartGroup per track, with Parts assigned by channel (no voices) @@ -730,6 +734,29 @@ def create_part( part_id: Optional[str] = None, part_name: Optional[str] = None, ) -> score.Part: + """ + Create score part object + + Parameters + ---------- + ticks: int + Integer unit to represent onset and duration information + in the score in a lossless way. + notes: List[Tuple[int, int, int]] + Note information (onset, pitch, duration) + spellings: List[Tuple[str, str, int]] + voices: List[str] + note_ids: List[str] + time_sigs: List[Tuple[int, int, int]] + key_sigs: + part_id + part_name + + Returns + ------- + part: partitura.score.Part + An object representing a Part in the score + """ warnings.warn("create_part", stacklevel=2) part = score.Part(part_id, part_name=part_name) From 1550426aefa7e9e4eaa09ae03894358126a6deb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Sun, 19 Nov 2023 17:40:58 +0100 Subject: [PATCH 012/197] check whether miditok is installed in tests; remove duplicated test --- tests/test_utils.py | 90 ++++++++++++++++++++++----------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 13b5cc80..87e95bdd 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -9,13 +9,24 @@ from partitura.utils import music from partitura.musicanalysis import performance_codec -from tests import MATCH_IMPORT_EXPORT_TESTFILES, VOSA_TESTFILES, MOZART_VARIATION_FILES, TOKENIZER_TESTFILES +from tests import ( + MATCH_IMPORT_EXPORT_TESTFILES, + VOSA_TESTFILES, + MOZART_VARIATION_FILES, + TOKENIZER_TESTFILES, +) from scipy.interpolate import interp1d as scinterp1d from partitura.utils.generic import interp1d as pinterp1d from partitura.utils.music import tokenize -import miditok -import miditoolkit + +try: + import miditok + import miditoolkit + + HAS_MIDITOK = True +except ImportError: + HAS_MIDITOK = False RNG = np.random.RandomState(1984) @@ -601,45 +612,34 @@ def test_interp1d(self): self.assertTrue(np.all(sinterp(x) == pinterp(x))) -class TestTokenizer(unittest.TestCase): - def test_tokenize1(self): - """ Test the partitura tokenizer""" - tokenizer = miditok.MIDILike() - # produce tokens from the score with partitura - pt_score = partitura.load_score(TOKENIZER_TESTFILES[0]["score"]) - pt_tokens = tokenize(pt_score, tokenizer)[0].tokens - # produce tokens from the manually created MIDI file - mtok_midi = miditoolkit.MidiFile(TOKENIZER_TESTFILES[0]["midi"]) - mtok_tokens = tokenizer(mtok_midi)[0].tokens - # filter out velocity tokens - 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) - - def test_tokenize2(self): - """ Test the partitura tokenizer""" - tokenizer = miditok.REMI() - # produce tokens from the score with partitura - pt_score = partitura.load_score(TOKENIZER_TESTFILES[0]["score"]) - pt_tokens = tokenize(pt_score, tokenizer)[0].tokens - # produce tokens from the manually created MIDI file - mtok_midi = miditoolkit.MidiFile(TOKENIZER_TESTFILES[0]["midi"]) - mtok_tokens = tokenizer(mtok_midi)[0].tokens - # filter out velocity tokens - 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) - - def test_tokenize1(self): - """ Test the partitura tokenizer""" - tokenizer = miditok.MIDILike() - # produce tokens from the score with partitura - pt_score = partitura.load_score(TOKENIZER_TESTFILES[0]["score"]) - pt_tokens = tokenize(pt_score, tokenizer)[0].tokens - # produce tokens from the manually created MIDI file - mtok_midi = miditoolkit.MidiFile(TOKENIZER_TESTFILES[0]["midi"]) - mtok_tokens = tokenizer(mtok_midi)[0].tokens - # filter out velocity tokens - 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) \ No newline at end of file + +if HAS_MIDITOK: + # Only run these tests if miditok is installed + class TestTokenizer(unittest.TestCase): + def test_tokenize1(self): + """Test the partitura tokenizer""" + tokenizer = miditok.MIDILike() + # produce tokens from the score with partitura + pt_score = partitura.load_score(TOKENIZER_TESTFILES[0]["score"]) + pt_tokens = tokenize(pt_score, tokenizer)[0].tokens + # produce tokens from the manually created MIDI file + mtok_midi = miditoolkit.MidiFile(TOKENIZER_TESTFILES[0]["midi"]) + mtok_tokens = tokenizer(mtok_midi)[0].tokens + # filter out velocity tokens + 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) + + def test_tokenize2(self): + """Test the partitura tokenizer""" + tokenizer = miditok.REMI() + # produce tokens from the score with partitura + pt_score = partitura.load_score(TOKENIZER_TESTFILES[0]["score"]) + pt_tokens = tokenize(pt_score, tokenizer)[0].tokens + # produce tokens from the manually created MIDI file + mtok_midi = miditoolkit.MidiFile(TOKENIZER_TESTFILES[0]["midi"]) + mtok_tokens = tokenizer(mtok_midi)[0].tokens + # filter out velocity tokens + 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) From af41cf9e570ed63423b99d7d1e6abfaaee1ae484 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 29 Nov 2023 15:18:42 +0100 Subject: [PATCH 013/197] First Version for reading DCML annotations and score in tsv format. --- partitura/io/__init__.py | 2 + partitura/io/importdcml.py | 107 +++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 partitura/io/importdcml.py diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index 4ad2cd47..ea22d920 100644 --- a/partitura/io/__init__.py +++ b/partitura/io/__init__.py @@ -15,6 +15,8 @@ from .importparangonada import load_parangonada_csv from .exportparangonada import save_parangonada_csv from .importmusic21 import load_music21 +from .importdcml import load_tsv + from partitura.utils.misc import ( deprecated_alias, diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py new file mode 100644 index 00000000..94ab120d --- /dev/null +++ b/partitura/io/importdcml.py @@ -0,0 +1,107 @@ +import numpy as np + +import partitura.score as spt +try: + import pandas as pd +except ImportError: + pd = None + + +def read_note_tsv(note_tsv_path, metadata=None): + data = pd.read_csv(note_tsv_path, sep="\t") + unique_durations = data["duration"].unique() + nominators = [int(qb.split("/")[0]) for qb in unique_durations] + denominators = [int(qb.split("/")[1]) for qb in unique_durations if "/" in qb] + # transform quarter_beats to quarter_divs + qdivs = np.lcm.reduce(denominators) + quarter_durations = data["duration_qb"] + duration_div = np.array([int(qd * qdivs) for qd in quarter_durations]) + onset_div = np.array([int(qd * qdivs) for qd in data["quarterbeats"].apply(eval)]) + flats = data["name"].str.contains("b") + sharps = data["name"].str.contains("#") + double_sharps = data["name"].str.contains("##") + double_flats = data["name"].str.contains("bb") + alter = np.zeros(len(data), dtype=np.int32) + alter[flats] = -1 + alter[sharps] = 1 + alter[double_sharps] = 2 + alter[double_flats] = -2 + data["step"] = data["name"].apply(lambda x: x[0]) + data["onset_div"] = onset_div + data["duration_div"] = duration_div + data["alter"] = alter + data["pitch"] = data["midi"] + grace_mask = ~data["gracenote"].isna() + data["id"] = np.arange(len(data)) + note_array = data[["onset_div", "duration_div", "pitch", "step", "alter", "octave", "id", "staff", "voice"]].to_records(index=False) + part = spt.Part("P0", "Metadata", quarter_duration=qdivs) + + # Add notes + notes = note_array[~grace_mask] + for note in notes: + part.add( + spt.Note( + id=note["id"], + step=note["step"], + octave=note["octave"], + alter=note["alter"], + staff=note["staff"], + voice=note["voice"] + ), start=note["onset_div"], end=note["onset_div"]+note["duration_div"]) + # Add Grace notes + grace_notes = note_array[grace_mask] + for grace_note in grace_notes: + part.add( + spt.GraceNote( + grace_type="grace", + id=grace_note["id"], + step=grace_note["step"], + octave=grace_note["octave"], + alter=grace_note["alter"], + staff=grace_note["staff"], + voice=grace_note["voice"] + ), + start=grace_note["onset_div"], + end=grace_note["onset_div"] + ) + + # Find time signatures + time_signatures_changes = data["timesig"][data["timesig"].shift(1) != data["timesig"]].index + time_signatures = data["timesig"][time_signatures_changes] + start_divs = np.array([int(qd * qdivs) for qd in data["quarterbeats"][time_signatures_changes]]) + end_of_piece = (note_array["onset_div"]+note_array["duration_div"]).max() + end_divs = np.r_[start_divs[1:], end_of_piece] + for ts, start, end in zip(time_signatures, start_divs, end_divs): + part.add(spt.TimeSignature(beats=int(ts.split("/")[0]), beat_type=int(ts.split("/")[1])), start=start, end=end) + + # TODO: Find Ties + tied_notes = data["tied"].dropna() + + return part + + +def read_measure_tsv(measure_tsv_path): + return + + +def read_harmony_tsv(beat_tsv_path, part): + qdivs = part._quarter_durations[0] + data = pd.read_csv(beat_tsv_path, sep="\t") + data["onset_div"] = np.array([int(qd * qdivs) for qd in data["quarterbeats"].apply(eval)]) + data["duration_div"] = np.array([int(qd * qdivs) for qd in data["duration_qb"]]) + for _, row in data.iterrows(): + part.add(spt.RomanNumeral(roman=row["chord"]), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) + return + + +def load_tsv(note_tsv_path, measure_tsv_path=None, harmony_tsv_path=None, metadata=None): + part = read_note_tsv(note_tsv_path, metadata=metadata) + if measure_tsv_path is not None: + read_measure_tsv(measure_tsv_path, part) + else: + spt.add_measures(part) + if harmony_tsv_path is not None: + read_harmony_tsv(harmony_tsv_path, part) + score = spt.Score([part]) + return score + From 97af9f4238cb876046f38c5a820e9750e82d8eb2 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 30 Nov 2023 12:39:56 +0100 Subject: [PATCH 014/197] Added validation for Roman Numeral fields. Added Phrase and Cadence Classes. --- partitura/score.py | 104 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index cbc0f445..95f64ea3 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -18,6 +18,7 @@ from partitura.utils.music import MUSICAL_BEATS, INTERVALCLASSES import warnings, sys import numpy as np +import re from scipy.interpolate import PPoly from typing import Union, List, Optional, Iterator, Iterable as Itertype @@ -2734,15 +2735,114 @@ class RomanNumeral(TimedObject): See parameters """ - def __init__(self, text): + def __init__(self, text, inversion=None, local_key=None, primary_degree=None, secondary_degree=None, quality=None): super().__init__() self.text = text - # assert issubclass(note, GenericNote) + self.inversion = inversion if inversion is not None else self._process_inversion() + self.local_key = local_key if local_key is not None else self._process_local_key() + self.primary_degree = primary_degree if primary_degree is not None else self._process_primary_degree() + self.secondary_degree = secondary_degree if secondary_degree is not None else self._process_secondary_degree() + self.quality = quality if quality is not None else self._process_quality() + + def _process_inversion(self): + """Find the inversion of the roman numeral from the text""" + # The inversion should be right after the roman numeral. + # If there is no inversion, return 0 + numeric_indications_in_text = re.findall(r'\d+', self.text) + if len(numeric_indications_in_text) > 0: + inversion_state = int(numeric_indications_in_text[0]) + if inversion_state == 2: + return 3 + elif inversion_state in [43, 64]: + return 2 + elif inversion_state in [6, 65]: + return 1 + return 0 + + def _process_local_key(self): + """Find the local key of the roman numeral from the text""" + # The local key should be before the roman numeral. + # If there is no local key, return None + local_key = self.text.split(":") + if len(local_key) > 1: + return local_key[0] + return None + + def _process_primary_degree(self): + """Find the primary degree of the roman numeral from the text + + The primary degree should be a roman numeral between 1 and 7. + """ + # The primary degree should be a roman numeral between 1 and 7. + # If there is no primary degree, return None + primary_degree = re.findall(r'[ivIV]+', self.text) + if len(primary_degree) > 0: + return primary_degree[0] + return None + + def _process_secondary_degree(self): + """Find the secondary degree of the roman numeral from the text + + The secondary degree should be a roman numeral between 1 and 7. + If it is not specified in the text, return I (the tonic) when the primary degree is not none. + """ + # The secondary degree should be a roman numeral between 1 and 7. + # If it is not specified in the text, return I (the tonic) when the primary degree is not none. + secondary_degree = re.findall(r'[ivIV]+', self.text) + if len(secondary_degree) > 1: + return secondary_degree[1] + elif self.primary_degree is not None: + return "I" + return None + + def _process_quality(self): + """Find the quality of the roman numeral from the text + + Accepted quality values are M, m, +, o, and None. + """ + # The quality should be M, m, +, o, or None. + # If there is no quality, return None + quality = re.findall(r'[Mm+o]', self.text) + if len(quality) > 0: + return quality[0] + return None + def __str__(self): return f'{super().__str__()} "{self.text}"' +class Cadence(TimedObject): + """A cadence element in the score usually for Cadences.""" + def __init__(self, text, local_key=None): + super().__init__() + self.text = text + self._filter_cadence_type() + self.local_key = local_key + + def _filter_cadence_type(self): + """Cadence should be one of PAC, IAC, HC, DC, EC, PC, or None""" + # capitalize text + self.text = self.text.upper() + if "IAC" in self.text: + self.text = "IAC" + if self.text not in ["PAC", "IAC", "HC", "DC", "EC", "PC"]: + warnings.warn(f"Cadence type {self.text} not found. Setting to None") + self.text = None + + + def __str__(self): + return f'{super().__str__()} "{self.text}"' + + +class Phrase(TimedObject): + def __init__(self): + super().__init__() + + def __str__(self): + return f'{super().__str__()}' + + class ChordSymbol(TimedObject): """A harmony element in the score usually for Chord Symbols.""" From d4d0a9f696ce760fb9028589c3e6de0cc6712454 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 30 Nov 2023 12:40:17 +0100 Subject: [PATCH 015/197] Read Cadences and Phrases from dcml harmony tsv. --- partitura/io/importdcml.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 94ab120d..daa5b1a3 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -10,10 +10,9 @@ def read_note_tsv(note_tsv_path, metadata=None): data = pd.read_csv(note_tsv_path, sep="\t") unique_durations = data["duration"].unique() - nominators = [int(qb.split("/")[0]) for qb in unique_durations] denominators = [int(qb.split("/")[1]) for qb in unique_durations if "/" in qb] # transform quarter_beats to quarter_divs - qdivs = np.lcm.reduce(denominators) + qdivs = np.lcm.reduce(denominators) if len(denominators) > 0 else 4 quarter_durations = data["duration_qb"] duration_div = np.array([int(qd * qdivs) for qd in quarter_durations]) onset_div = np.array([int(qd * qdivs) for qd in data["quarterbeats"].apply(eval)]) @@ -89,8 +88,25 @@ def read_harmony_tsv(beat_tsv_path, part): data = pd.read_csv(beat_tsv_path, sep="\t") data["onset_div"] = np.array([int(qd * qdivs) for qd in data["quarterbeats"].apply(eval)]) data["duration_div"] = np.array([int(qd * qdivs) for qd in data["duration_qb"]]) + # Find Phrase Starts where data["phraseend"] == "{" for _, row in data.iterrows(): - part.add(spt.RomanNumeral(roman=row["chord"]), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) + part.add( + spt.RomanNumeral(roman=row["chord"], + local_key=row["localkey"], + quality=row["chord_type"], + ), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) + if row["cadence"] is not None: + part.add( + spt.Cadence(cadence_type=row["cadence"], + local_key=row["localkey"], + ), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) + + phrase_starts = data[data["phraseend"] == "{"] + phrase_ends = data[data["phraseend"] == "}"] + # Check that the number of phrase starts and ends match + assert len(phrase_starts) == len(phrase_ends), "Number of phrase starts and ends do not match" + for start, end in zip(phrase_starts.iterrows(), phrase_ends.iterrows()): + part.add(spt.Phrase(), start=start[1]["onset_div"], end=end[1]["onset_div"]) return From a6817965ee9f184e673605516df135b2f9c6366e Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 1 Dec 2023 10:32:53 +0100 Subject: [PATCH 016/197] New version of kern for fast parsing. --- partitura/io/importkern_v2.py | 670 ++++++++++++++++++++++++++++++++++ 1 file changed, 670 insertions(+) create mode 100644 partitura/io/importkern_v2.py diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py new file mode 100644 index 00000000..d69c99a8 --- /dev/null +++ b/partitura/io/importkern_v2.py @@ -0,0 +1,670 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains methods for importing Humdrum Kern files. +""" +import re +import warnings + +from typing import Union, Optional + +import numpy as np + +import partitura.score as score +from partitura.utils import PathLike, get_document_name +from partitura.utils.misc import deprecated_alias, deprecated_parameter + + +__all__ = ["load_kern"] + + +class KernGlobalPart(object): + def __init__(self, doc_name, part_id, qdivs): + qdivs = int(1) if int(qdivs) == 0 else int(qdivs) + # super(KernGlobalPart, self).__init__() + self.part = score.Part(doc_name, part_id, quarter_duration=qdivs) + self.default_clef_lines = {"G": 2, "F": 4, "C": 3} + self.SIGN_TO_ACC = { + "n": 0, + "#": 1, + "s": 1, + "ss": 2, + "x": 2, + "##": 2, + "###": 3, + "b": -1, + "f": -1, + "bb": -2, + "ff": -2, + "bbb": -3, + "-": None, + } + + self.KERN_NOTES = { + "C": ("C", 3), + "D": ("D", 3), + "E": ("E", 3), + "F": ("F", 3), + "G": ("G", 3), + "A": ("A", 3), + "B": ("B", 3), + "c": ("C", 4), + "d": ("D", 4), + "e": ("E", 4), + "f": ("F", 4), + "g": ("G", 4), + "a": ("A", 4), + "b": ("B", 4), + } + + self.KERN_DURS = { + # "long": "long", + # "breve": "breve", + 0: "breve", + 1: "whole", + 2: "half", + 4: "quarter", + 8: "eighth", + 16: "16th", + 32: "32nd", + 64: "64th", + 128: "128th", + 256: "256th", + } + + +class KernParserPart(KernGlobalPart): + """ + Class for parsing kern file syntax. + """ + + def __init__(self, stream, init_pos, doc_name, part_id, qdivs, barline_dict=None): + super(KernParserPart, self).__init__(doc_name, part_id, qdivs) + self.position = int(init_pos) + self.parsing = "full" + self.stream = stream + self.prev_measure_pos = init_pos + self.EDITORIAL_SYMBOLS = ["x", "p", "q", "<", "(", ">", ")", "[", "]"] + # Check if part has pickup measure. + self.measure_count = ( + 0 if np.all(np.char.startswith(stream, "=1-") == False) else 1 + ) + self.last_repeat_pos = None + self.mode = None + self.barline_dict = dict() if not barline_dict else barline_dict + self.slur_dict = {"open": [], "close": []} + self.tie_dict = {"open": [], "close": []} + self.process() + + def process(self): + self.staff = None + for index, el in enumerate(self.stream): + self.current_index = index + if el.startswith("*staff"): + self.staff = eval(el[len("*staff") :]) + # elif el.startswith("!!!"): + # self._handle_fileinfo(el) + elif el.startswith("*"): + if self.staff == None: + self.staff = 1 + self._handle_glob_attr(el) + elif el.startswith("="): + self.select_parsing(el) + self._handle_barline(el) + elif " " in el: + self._handle_chord(el, index) + elif "r" in el: + self._handle_rest(el, "r-" + str(index)) + else: + self._handle_note(el, "n-" + str(index)) + self.nid_dict = dict( + [(n.id, n) for n in self.part.iter_all(cls=score.Note)] + + [(n.id, n) for n in self.part.iter_all(cls=score.GraceNote)] + ) + self._handle_slurs() + self._handle_ties() + + # Account for parsing priorities. + def select_parsing(self, el): + if self.parsing == "full": + return el + elif self.parsing == "right": + return el.split()[-1] + else: + return el.split()[0] + + # TODO handle !!!info + def _handle_fileinfo(self, el): + pass + + def _handle_ties(self): + try: + if len(self.tie_dict["open"]) < len(self.tie_dict["close"]): + for index, oid in enumerate(self.tie_dict["open"]): + if ( + self.nid_dict[oid].midi_pitch + != self.nid_dict[self.tie_dict["close"][index]].midi_pitch + ): + dnote = self.nid_dict[self.tie_dict["close"][index]] + m_num = [ + m + for m in self.part.iter_all(score.Measure) + if m.start.t == self.part.measure_map(dnote.start.t)[0] + ][0].number + warnings.warn( + "Dropping Closing Tie of note {} at position {} measure {}".format( + dnote.midi_pitch, dnote.start.t, m_num + ) + ) + self.tie_dict["close"].pop(index) + self._handle_ties() + elif len(self.tie_dict["open"]) > len(self.tie_dict["close"]): + for index, cid in enumerate(self.tie_dict["close"]): + if ( + self.nid_dict[cid].midi_pitch + != self.nid_dict[self.tie_dict["open"][index]].midi_pitch + ): + dnote = self.nid_dict[self.tie_dict["open"][index]] + m_num = [ + m + for m in self.part.iter_all(score.Measure) + if m.start.t == self.part.measure_map(dnote.start.t)[0] + ][0].number + warnings.warn( + "Dropping Opening Tie of note {} at position {} measure {}".format( + dnote.midi_pitch, dnote.start.t, m_num + ) + ) + self.tie_dict["open"].pop(index) + self._handle_ties() + else: + for oid, cid in list( + zip(self.tie_dict["open"], self.tie_dict["close"]) + ): + self.nid_dict[oid].tie_next = self.nid_dict[cid] + self.nid_dict[cid].tie_prev = self.nid_dict[oid] + except Exception: + raise ValueError( + "Tie Mismatch! Uneven amount of closing to open tie brackets." + ) + + def _handle_slurs(self): + if len(self.slur_dict["open"]) != len(self.slur_dict["close"]): + warnings.warn( + "Slur Mismatch! Uneven amount of closing to open slur brackets. Skipping slur parsing.", + ImportWarning, + ) + # raise ValueError( + # "Slur Mismatch! Uneven amount of closing to open slur brackets." + # ) + else: + for oid, cid in list(zip(self.slur_dict["open"], self.slur_dict["close"])): + self.part.add(score.Slur(self.nid_dict[oid], self.nid_dict[cid])) + + def _handle_metersig(self, metersig): + m = metersig[2:] + if " " in m: + m = m.split(" ")[0] + numerator, denominator = map(eval, m.split("/")) + new_time_signature = score.TimeSignature(numerator, denominator) + self.part.add(new_time_signature, self.position) + + def _handle_barline(self, element): + if self.position > self.prev_measure_pos: + indicated_measure = re.findall("=([0-9]+)", element) + if indicated_measure != []: + m = eval(indicated_measure[0]) - 1 + barline = score.Barline(style="normal") + self.part.add(barline, self.position) + self.measure_count = m + self.barline_dict[m] = self.position + else: + m = self.measure_count - 1 + self.part.add(score.Measure(m), self.prev_measure_pos, self.position) + self.prev_measure_pos = self.position + self.measure_count += 1 + if len(element.split()) > 1: + element = element.split()[0] + if element.endswith("!") or element == "==": + barline = score.Fine() + self.part.add(barline, self.position) + if ":|" in element: + barline = score.Repeat() + self.part.add( + barline, + self.position, + self.last_repeat_pos if self.last_repeat_pos else None, + ) + # update position for backward repeat signs + if "|:" in element: + self.last_repeat_pos = self.position + + # TODO maybe also append position for verification. + def _handle_mode(self, element): + if element[1].isupper(): + self.mode = "major" + else: + self.mode = "minor" + + def _handle_keysig(self, element): + keysig_el = element[2:] + fifths = 0 + for c in keysig_el: + if c == "#": + fifths += 1 + if c == "b": + fifths -= 1 + # TODO retrieve the key mode + mode = self.mode if self.mode else "major" + new_key_signature = score.KeySignature(fifths, mode) + self.part.add(new_key_signature, self.position) + + def _compute_clef_octave(self, dis, dis_place): + if dis is not None: + sign = -1 if dis_place == "below" else 1 + octave = sign * int(int(dis) / 8) + else: + octave = 0 + return octave + + def _handle_clef(self, element): + # handle the case where we have clef information + # TODO Compute Clef Octave + if element[5] not in ["G", "F", "C"]: + raise ValueError("Unknown Clef", element[5]) + if len(element) < 7: + line = self.default_clef_lines[element[5]] + else: + line = int(element[6]) if element[6] != "v" else int(element[7]) + new_clef = score.Clef( + staff=self.staff, sign=element[5], line=line, octave_change=0 + ) + self.part.add(new_clef, self.position) + + def _handle_rest(self, el, rest_id): + # find duration info + duration, symbolic_duration, rtype = self._handle_duration(el) + # create rest + rest = score.Rest( + id=rest_id, + voice=1, + staff=1, + symbolic_duration=symbolic_duration, + articulations=None, + ) + # add rest to the part + self.part.add(rest, self.position, self.position + duration) + # return duration to update the position in the layer + self.position += duration + + def _handle_fermata(self, note_instance): + self.part.add(note_instance, self.position) + + def _search_slurs_and_ties(self, note, note_id): + if ")" in note: + x = note.count(")") + if len(self.slur_dict["open"]) == len(self.slur_dict["close"]) + x: + # for _ in range(x): + self.slur_dict["close"].append(note_id) + if note.startswith("("): + # acount for multiple opening brackets + n = note.count("(") + # for _ in range(n): + self.slur_dict["open"].append(note_id) + # Re-order for correct parsing + if len(self.slur_dict["open"]) > len(self.slur_dict["close"]) + 1: + warnings.warn( + "Cannot deal with nested slurs. Dropping Opening slur for note id {}".format( + self.slur_dict["open"][len(self.slur_dict["open"]) - 2] + ) + ) + self.slur_dict["open"].pop(len(self.slur_dict["open"]) - 2) + # x = note_id + # lenc = len(self.slur_dict["open"]) - len(self.slur_dict["close"]) + # self.slur_dict["open"][:lenc - 1] = self.slur_dict["open"][1:lenc] + # self.slur_dict["open"][lenc] = x + note = note[n:] + if "]" in note: + self.tie_dict["close"].append(note_id) + elif "_" in note: + self.tie_dict["open"].append(note_id) + self.tie_dict["close"].append(note_id) + if note.startswith("["): + self.tie_dict["open"].append(note_id) + note = note[1:] + return note + + def _handle_duration(self, note, isgrace=False): + foundRational = re.search(r'(\d+)%(\d+)', note) + if foundRational: + ntype = note[foundRational.span()[-1]:] + durationFirst = int(foundRational.group(1)) + durationSecond = float(foundRational.group(2)) + dur = 4 * durationSecond / durationFirst + else: + _, dur, ntype = re.split("(\d+)", note) + ntype = _ + ntype if isgrace else ntype + dur = eval(dur) + + if dur in self.KERN_DURS.keys(): + symbolic_duration = {"type": self.KERN_DURS[dur]} + else: + diff = dict( + ( + map( + lambda x: (dur - x, x) if dur > x else (dur + x, x), + self.KERN_DURS.keys(), + ) + ) + ) + symbolic_duration = { + "type": self.KERN_DURS[diff[min(list(diff.keys()))]], + "actual_notes": dur / 4, + "normal_notes": diff[min(list(diff.keys()))] / 4, + } + + # calculate duration to divs. + qdivs = self.part._quarter_durations[0] + duration = qdivs * 4 / dur if dur != 0 else qdivs * 8 + if "." in note: + symbolic_duration["dots"] = note.count(".") + ntype = ntype[note.count(".") :] + d = duration + for i in range(symbolic_duration["dots"]): + d = d / 2 + duration += d + else: + symbolic_duration["dots"] = 0 + if isinstance(duration, float): + if not duration.is_integer(): + raise ValueError("Duration divs is not an integer, {}".format(duration)) + # Check that duration is same as int + assert int(duration) == duration + return int(duration), symbolic_duration, ntype + + # TODO Handle beams and tuplets. + + def _handle_note(self, note, note_id, voice=1): + if note == "." or note == "" or note == " ": + return + has_fermata = ";" in note + note = self._search_slurs_and_ties(note, note_id) + grace_attr = "q" in note # or "p" in note # for appoggiatura not sure yet. + duration, symbolic_duration, ntype = self._handle_duration(note, grace_attr) + # Remove editorial symbols from string, i.e. "x" + for x in self.EDITORIAL_SYMBOLS: + ntype = ntype.replace(x, "") + step, octave = self.KERN_NOTES[ntype[0]] + if octave == 4: + octave = octave + ntype.count(ntype[0]) - 1 + elif octave == 3: + octave = octave - ntype.count(ntype[0]) + 1 + alter = ntype.count("#") - ntype.count("-") + # find if it's grace + if not grace_attr: + # create normal note + note = score.Note( + step=step, + octave=octave, + alter=alter, + id=note_id, + voice=int(voice), + staff=self.staff, + symbolic_duration=symbolic_duration, + articulations=None, # TODO : add articulation + ) + if has_fermata: + self._handle_fermata(note) + else: + # create grace note + if "p" in ntype: + grace_type = "acciaccatura" + elif "q" in ntype: + grace_type = "appoggiatura" + note = score.GraceNote( + grace_type=grace_type, + step=step, + octave=octave, + alter=alter, + id=note_id, + voice=1, + staff=self.staff, + symbolic_duration=symbolic_duration, + articulations=None, # TODO : add articulation + ) + duration = 0 + + self.part.add(note, self.position, self.position + duration) + self.position += duration + + def _handle_chord(self, chord, id): + notes = chord.split() + position_history = list() + pos = self.position + for i, note_el in enumerate(notes): + id_new = "c-" + str(i) + "-" + str(id) + self.position = pos + if "r" in note_el: + self._handle_rest(note_el, id_new) + else: + self._handle_note(note_el, id_new, voice=int(i)) + if note_el != ".": + position_history.append(self.position) + # To account for Voice changes and alternate voice order. + self.position = min(position_history) if position_history else self.position + + def _handle_glob_attr(self, el): + if el.startswith("*clef"): + self._handle_clef(el) + elif el.startswith("*k"): + self._handle_keysig(el) + elif el.startswith("*MM"): + pass + elif el.startswith("*M"): + self._handle_metersig(el) + elif el.endswith(":"): + self._handle_mode(el) + elif el.startswith("*S/sic"): + self.parsing = "left" + elif el.startswith("*S/ossia"): + self.parsing = "right" + elif el.startswith("Xstrophe"): + self.parsing = "full" + + +class KernParser: + def __init__(self, document, doc_name): + self.document = document + self.doc_name = doc_name + self.qdivs = self.find_lcm(document.flatten()) + # TODO review this code + self.DIVS2Q = { + 1: 0.25, + 2: 0.5, + 4: 1, + 6: 1.5, + 8: 2, + 16: 4, + 24: 6, + 32: 8, + 48: 12, + 64: 16, + 128: 32, + 256: 64, + } + # self.qdivs = + self.parts = self.process() + + def __getitem__(self, item): + return self.parts[item] + + def process(self): + # TODO handle pickup + # has_pickup = not np.all(np.char.startswith(self.document, "=1-") == False) + # if not has_pickup: + # position = 0 + # else: + # position = self._handle_pickup_position() + position = 0 + # Add for parallel processing + parts = [ + self.collect(self.document[i], position, str(i), self.doc_name) + for i in reversed(range(self.document.shape[0])) + ] + return [p for p in parts if p] + + def add2part(self, part, unprocessed): + flatten = [item for sublist in unprocessed for item in sublist] + if unprocessed: + new_part = KernParserPart( + flatten, 0, self.doc_name, "x", self.qdivs, part.barline_dict + ) + self.parts.append(new_part) + + def collect(self, doc, pos, id, doc_name): + if doc[0] == "**kern": + qdivs = self.find_lcm(doc) if self.qdivs is None else self.qdivs + x = KernParserPart(doc, pos, id, doc_name, qdivs).part + return x + + # TODO handle position of pick-up measure? + def _handle_pickup_position(self): + return 0 + + def find_lcm(self, doc): + kern_string = "-".join([row for row in doc]) + match = re.findall(r"([0-9]+)([a-g]|[A-G]|r|\.)", kern_string) + durs, _ = zip(*match) + x = np.array(list(map(lambda x: int(x), durs))) + divs = np.lcm.reduce(np.unique(x[x != 0])) + return float(divs) # / 4.00 + + +def _handle_kern_with_spine_splitting(kern_path): + file = np.loadtxt(kern_path, dtype=str, delimiter="\n", comments="!", encoding="utf-8") + # Get Main Number of parts and Spline Types + spline_types = file[0].split("\t") + # Decide Parts + + # Find all expansions points + expansion_indices = np.where(np.char.find(file, "*^") != -1)[0] + # For all expansion points find which stream is being expanded + expansion_streams_per_index = [np.argwhere(np.array(line.split("\t")) == "*^")[0] for line in + file[expansion_indices]] + + # Find all Spline Reduction points + reduction_indices = np.where(np.char.find(file, "*v\t*v") != -1)[0] + # For all reduction points find which stream is being reduced + reduction_streams_per_index = [ + np.argwhere(np.char.add(np.array(line.split("\t")[:-1]), np.array(line.split("\t")[1:])) == "*v*v")[0] for line + in file[reduction_indices]] + + # Find all pairs of expansion and reduction points + expansion_reduction_pairs = [] + last_exhaustive_reduction = 0 + for expansion_index in expansion_indices: + for expansion_stream in expansion_index: + # Find the first reduction index that is after the expansion index and has the same index. + for i, reduction_index in enumerate(reduction_indices[last_exhaustive_reduction:]): + for reduction_stream in reduction_streams_per_index[i]: + if expansion_stream == reduction_stream: + expansion_reduction_pairs.append((expansion_index, reduction_index)) + last_exhaustive_reduction = i if i == last_exhaustive_reduction + 1 else last_exhaustive_reduction + break + + +# functions to initialize the kern parser +def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: + """ + Parses an KERN file from path to Part. + + Parameters + ---------- + kern_path : PathLike + The path of the KERN document. + Returns + ------- + continuous_parts : numpy character array + non_continuous_parts : list + """ + try: + # This version of the parser is faster but does not support spine splitting. + file = np.loadtxt(kern_path, dtype=str, delimiter="\t", comments="!", encoding="utf-8") + except: + # This version of the parser supports spine splitting but is slower. + # It adds the splines to with a special character. + file = _handle_kern_with_spine_splitting(kern_path) + + # Get Main Number of parts and Spline Types + spline_types = file[0] + # Decide Parts + + # Get Splines + splines = file.T + for spline in splines: + parse_spline(spline) + + +def parse_spline(spline): + # Remove "-" lines + spline = spline[spline != "-"] + # Find Barline indices, i.e. where spline cells start with "=" + barline_indices = np.where(np.char.startswith(spline, "="))[0] + # Find Chord indices, i.e. where spline cells contain " " + chord_indices = np.where(np.char.find(spline, " ") != -1)[0] + # Find Rest indices, i.e. where spline cells contain "r" + rest_indices = np.where(np.char.find(spline, "r") != -1)[0] + # All the rest are note indices + non_note_indices = np.hstack((chord_indices, barline_indices, rest_indices)) + note_indices = ~np.isin(np.arange(len(spline)), non_note_indices, invert=True) + # Get Notes + notes = spline[note_indices] + + + + +@deprecated_alias(kern_path="filename") +@deprecated_parameter("ensure_list") +def load_kern( + filename: PathLike, + force_note_ids: Optional[Union[bool, str]] = None, + parallel: bool = False, +) -> score.Score: + """Parse a Kern file and build a composite score ontology + structure from it (see also scoreontology.py). + + Parameters + ---------- + filename : PathLike + Path to the Kern file to be parsed + force_note_ids : (bool, 'keep') optional. + When True each Note in the returned Part(s) will have a newly + assigned unique id attribute. Existing note id attributes in + the Kern will be discarded. If 'keep', only notes without + a note id will be assigned one. + + Returns + ------- + scr: :class:`partitura.score.Score` + A `Score` object + """ + # parse kern file + numpy_parts = parse_kern_v2(filename) + # doc_name = os.path.basename(filename[:-4]) + doc_name = get_document_name(filename) + parser = KernParser(numpy_parts, doc_name) + partlist = parser.parts + + score.assign_note_ids( + partlist, keep=(force_note_ids is True or force_note_ids == "keep") + ) + + # TODO: Parse score info (composer, lyricist, etc.) + scr = score.Score(id=doc_name, partlist=partlist) + + return scr + + +if __name__ == "__main__": + kern_path = "/home/manos/.struttura/ScriabinHumdrumDataset/op01/scriabin-op01.krn" + x = parse_kern(kern_path) \ No newline at end of file From 2537e22f40ec3f09e8a1c542fbf1536a3bf05f85 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 1 Dec 2023 16:17:54 +0100 Subject: [PATCH 017/197] additions to the kern parsing workflow. --- partitura/io/importkern_v2.py | 762 ++++++++-------------------------- 1 file changed, 181 insertions(+), 581 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index d69c99a8..4fd1ab7a 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -10,534 +10,57 @@ import numpy as np -import partitura.score as score -from partitura.utils import PathLike, get_document_name -from partitura.utils.misc import deprecated_alias, deprecated_parameter - - -__all__ = ["load_kern"] - - -class KernGlobalPart(object): - def __init__(self, doc_name, part_id, qdivs): - qdivs = int(1) if int(qdivs) == 0 else int(qdivs) - # super(KernGlobalPart, self).__init__() - self.part = score.Part(doc_name, part_id, quarter_duration=qdivs) - self.default_clef_lines = {"G": 2, "F": 4, "C": 3} - self.SIGN_TO_ACC = { - "n": 0, - "#": 1, - "s": 1, - "ss": 2, - "x": 2, - "##": 2, - "###": 3, - "b": -1, - "f": -1, - "bb": -2, - "ff": -2, - "bbb": -3, - "-": None, - } - - self.KERN_NOTES = { - "C": ("C", 3), - "D": ("D", 3), - "E": ("E", 3), - "F": ("F", 3), - "G": ("G", 3), - "A": ("A", 3), - "B": ("B", 3), - "c": ("C", 4), - "d": ("D", 4), - "e": ("E", 4), - "f": ("F", 4), - "g": ("G", 4), - "a": ("A", 4), - "b": ("B", 4), - } - - self.KERN_DURS = { - # "long": "long", - # "breve": "breve", - 0: "breve", - 1: "whole", - 2: "half", - 4: "quarter", - 8: "eighth", - 16: "16th", - 32: "32nd", - 64: "64th", - 128: "128th", - 256: "256th", - } - - -class KernParserPart(KernGlobalPart): - """ - Class for parsing kern file syntax. - """ - - def __init__(self, stream, init_pos, doc_name, part_id, qdivs, barline_dict=None): - super(KernParserPart, self).__init__(doc_name, part_id, qdivs) - self.position = int(init_pos) - self.parsing = "full" - self.stream = stream - self.prev_measure_pos = init_pos - self.EDITORIAL_SYMBOLS = ["x", "p", "q", "<", "(", ">", ")", "[", "]"] - # Check if part has pickup measure. - self.measure_count = ( - 0 if np.all(np.char.startswith(stream, "=1-") == False) else 1 - ) - self.last_repeat_pos = None - self.mode = None - self.barline_dict = dict() if not barline_dict else barline_dict - self.slur_dict = {"open": [], "close": []} - self.tie_dict = {"open": [], "close": []} - self.process() - - def process(self): - self.staff = None - for index, el in enumerate(self.stream): - self.current_index = index - if el.startswith("*staff"): - self.staff = eval(el[len("*staff") :]) - # elif el.startswith("!!!"): - # self._handle_fileinfo(el) - elif el.startswith("*"): - if self.staff == None: - self.staff = 1 - self._handle_glob_attr(el) - elif el.startswith("="): - self.select_parsing(el) - self._handle_barline(el) - elif " " in el: - self._handle_chord(el, index) - elif "r" in el: - self._handle_rest(el, "r-" + str(index)) - else: - self._handle_note(el, "n-" + str(index)) - self.nid_dict = dict( - [(n.id, n) for n in self.part.iter_all(cls=score.Note)] - + [(n.id, n) for n in self.part.iter_all(cls=score.GraceNote)] - ) - self._handle_slurs() - self._handle_ties() - - # Account for parsing priorities. - def select_parsing(self, el): - if self.parsing == "full": - return el - elif self.parsing == "right": - return el.split()[-1] - else: - return el.split()[0] - - # TODO handle !!!info - def _handle_fileinfo(self, el): - pass - - def _handle_ties(self): - try: - if len(self.tie_dict["open"]) < len(self.tie_dict["close"]): - for index, oid in enumerate(self.tie_dict["open"]): - if ( - self.nid_dict[oid].midi_pitch - != self.nid_dict[self.tie_dict["close"][index]].midi_pitch - ): - dnote = self.nid_dict[self.tie_dict["close"][index]] - m_num = [ - m - for m in self.part.iter_all(score.Measure) - if m.start.t == self.part.measure_map(dnote.start.t)[0] - ][0].number - warnings.warn( - "Dropping Closing Tie of note {} at position {} measure {}".format( - dnote.midi_pitch, dnote.start.t, m_num - ) - ) - self.tie_dict["close"].pop(index) - self._handle_ties() - elif len(self.tie_dict["open"]) > len(self.tie_dict["close"]): - for index, cid in enumerate(self.tie_dict["close"]): - if ( - self.nid_dict[cid].midi_pitch - != self.nid_dict[self.tie_dict["open"][index]].midi_pitch - ): - dnote = self.nid_dict[self.tie_dict["open"][index]] - m_num = [ - m - for m in self.part.iter_all(score.Measure) - if m.start.t == self.part.measure_map(dnote.start.t)[0] - ][0].number - warnings.warn( - "Dropping Opening Tie of note {} at position {} measure {}".format( - dnote.midi_pitch, dnote.start.t, m_num - ) - ) - self.tie_dict["open"].pop(index) - self._handle_ties() - else: - for oid, cid in list( - zip(self.tie_dict["open"], self.tie_dict["close"]) - ): - self.nid_dict[oid].tie_next = self.nid_dict[cid] - self.nid_dict[cid].tie_prev = self.nid_dict[oid] - except Exception: - raise ValueError( - "Tie Mismatch! Uneven amount of closing to open tie brackets." - ) - - def _handle_slurs(self): - if len(self.slur_dict["open"]) != len(self.slur_dict["close"]): - warnings.warn( - "Slur Mismatch! Uneven amount of closing to open slur brackets. Skipping slur parsing.", - ImportWarning, - ) - # raise ValueError( - # "Slur Mismatch! Uneven amount of closing to open slur brackets." - # ) - else: - for oid, cid in list(zip(self.slur_dict["open"], self.slur_dict["close"])): - self.part.add(score.Slur(self.nid_dict[oid], self.nid_dict[cid])) - - def _handle_metersig(self, metersig): - m = metersig[2:] - if " " in m: - m = m.split(" ")[0] - numerator, denominator = map(eval, m.split("/")) - new_time_signature = score.TimeSignature(numerator, denominator) - self.part.add(new_time_signature, self.position) - - def _handle_barline(self, element): - if self.position > self.prev_measure_pos: - indicated_measure = re.findall("=([0-9]+)", element) - if indicated_measure != []: - m = eval(indicated_measure[0]) - 1 - barline = score.Barline(style="normal") - self.part.add(barline, self.position) - self.measure_count = m - self.barline_dict[m] = self.position - else: - m = self.measure_count - 1 - self.part.add(score.Measure(m), self.prev_measure_pos, self.position) - self.prev_measure_pos = self.position - self.measure_count += 1 - if len(element.split()) > 1: - element = element.split()[0] - if element.endswith("!") or element == "==": - barline = score.Fine() - self.part.add(barline, self.position) - if ":|" in element: - barline = score.Repeat() - self.part.add( - barline, - self.position, - self.last_repeat_pos if self.last_repeat_pos else None, - ) - # update position for backward repeat signs - if "|:" in element: - self.last_repeat_pos = self.position - - # TODO maybe also append position for verification. - def _handle_mode(self, element): - if element[1].isupper(): - self.mode = "major" - else: - self.mode = "minor" - - def _handle_keysig(self, element): - keysig_el = element[2:] - fifths = 0 - for c in keysig_el: - if c == "#": - fifths += 1 - if c == "b": - fifths -= 1 - # TODO retrieve the key mode - mode = self.mode if self.mode else "major" - new_key_signature = score.KeySignature(fifths, mode) - self.part.add(new_key_signature, self.position) - - def _compute_clef_octave(self, dis, dis_place): - if dis is not None: - sign = -1 if dis_place == "below" else 1 - octave = sign * int(int(dis) / 8) - else: - octave = 0 - return octave - - def _handle_clef(self, element): - # handle the case where we have clef information - # TODO Compute Clef Octave - if element[5] not in ["G", "F", "C"]: - raise ValueError("Unknown Clef", element[5]) - if len(element) < 7: - line = self.default_clef_lines[element[5]] - else: - line = int(element[6]) if element[6] != "v" else int(element[7]) - new_clef = score.Clef( - staff=self.staff, sign=element[5], line=line, octave_change=0 - ) - self.part.add(new_clef, self.position) - - def _handle_rest(self, el, rest_id): - # find duration info - duration, symbolic_duration, rtype = self._handle_duration(el) - # create rest - rest = score.Rest( - id=rest_id, - voice=1, - staff=1, - symbolic_duration=symbolic_duration, - articulations=None, - ) - # add rest to the part - self.part.add(rest, self.position, self.position + duration) - # return duration to update the position in the layer - self.position += duration - - def _handle_fermata(self, note_instance): - self.part.add(note_instance, self.position) - - def _search_slurs_and_ties(self, note, note_id): - if ")" in note: - x = note.count(")") - if len(self.slur_dict["open"]) == len(self.slur_dict["close"]) + x: - # for _ in range(x): - self.slur_dict["close"].append(note_id) - if note.startswith("("): - # acount for multiple opening brackets - n = note.count("(") - # for _ in range(n): - self.slur_dict["open"].append(note_id) - # Re-order for correct parsing - if len(self.slur_dict["open"]) > len(self.slur_dict["close"]) + 1: - warnings.warn( - "Cannot deal with nested slurs. Dropping Opening slur for note id {}".format( - self.slur_dict["open"][len(self.slur_dict["open"]) - 2] - ) - ) - self.slur_dict["open"].pop(len(self.slur_dict["open"]) - 2) - # x = note_id - # lenc = len(self.slur_dict["open"]) - len(self.slur_dict["close"]) - # self.slur_dict["open"][:lenc - 1] = self.slur_dict["open"][1:lenc] - # self.slur_dict["open"][lenc] = x - note = note[n:] - if "]" in note: - self.tie_dict["close"].append(note_id) - elif "_" in note: - self.tie_dict["open"].append(note_id) - self.tie_dict["close"].append(note_id) - if note.startswith("["): - self.tie_dict["open"].append(note_id) - note = note[1:] - return note - - def _handle_duration(self, note, isgrace=False): - foundRational = re.search(r'(\d+)%(\d+)', note) - if foundRational: - ntype = note[foundRational.span()[-1]:] - durationFirst = int(foundRational.group(1)) - durationSecond = float(foundRational.group(2)) - dur = 4 * durationSecond / durationFirst - else: - _, dur, ntype = re.split("(\d+)", note) - ntype = _ + ntype if isgrace else ntype - dur = eval(dur) - - if dur in self.KERN_DURS.keys(): - symbolic_duration = {"type": self.KERN_DURS[dur]} - else: - diff = dict( - ( - map( - lambda x: (dur - x, x) if dur > x else (dur + x, x), - self.KERN_DURS.keys(), - ) - ) - ) - symbolic_duration = { - "type": self.KERN_DURS[diff[min(list(diff.keys()))]], - "actual_notes": dur / 4, - "normal_notes": diff[min(list(diff.keys()))] / 4, - } - - # calculate duration to divs. - qdivs = self.part._quarter_durations[0] - duration = qdivs * 4 / dur if dur != 0 else qdivs * 8 - if "." in note: - symbolic_duration["dots"] = note.count(".") - ntype = ntype[note.count(".") :] - d = duration - for i in range(symbolic_duration["dots"]): - d = d / 2 - duration += d - else: - symbolic_duration["dots"] = 0 - if isinstance(duration, float): - if not duration.is_integer(): - raise ValueError("Duration divs is not an integer, {}".format(duration)) - # Check that duration is same as int - assert int(duration) == duration - return int(duration), symbolic_duration, ntype - - # TODO Handle beams and tuplets. - - def _handle_note(self, note, note_id, voice=1): - if note == "." or note == "" or note == " ": - return - has_fermata = ";" in note - note = self._search_slurs_and_ties(note, note_id) - grace_attr = "q" in note # or "p" in note # for appoggiatura not sure yet. - duration, symbolic_duration, ntype = self._handle_duration(note, grace_attr) - # Remove editorial symbols from string, i.e. "x" - for x in self.EDITORIAL_SYMBOLS: - ntype = ntype.replace(x, "") - step, octave = self.KERN_NOTES[ntype[0]] - if octave == 4: - octave = octave + ntype.count(ntype[0]) - 1 - elif octave == 3: - octave = octave - ntype.count(ntype[0]) + 1 - alter = ntype.count("#") - ntype.count("-") - # find if it's grace - if not grace_attr: - # create normal note - note = score.Note( - step=step, - octave=octave, - alter=alter, - id=note_id, - voice=int(voice), - staff=self.staff, - symbolic_duration=symbolic_duration, - articulations=None, # TODO : add articulation - ) - if has_fermata: - self._handle_fermata(note) - else: - # create grace note - if "p" in ntype: - grace_type = "acciaccatura" - elif "q" in ntype: - grace_type = "appoggiatura" - note = score.GraceNote( - grace_type=grace_type, - step=step, - octave=octave, - alter=alter, - id=note_id, - voice=1, - staff=self.staff, - symbolic_duration=symbolic_duration, - articulations=None, # TODO : add articulation - ) - duration = 0 - - self.part.add(note, self.position, self.position + duration) - self.position += duration - - def _handle_chord(self, chord, id): - notes = chord.split() - position_history = list() - pos = self.position - for i, note_el in enumerate(notes): - id_new = "c-" + str(i) + "-" + str(id) - self.position = pos - if "r" in note_el: - self._handle_rest(note_el, id_new) - else: - self._handle_note(note_el, id_new, voice=int(i)) - if note_el != ".": - position_history.append(self.position) - # To account for Voice changes and alternate voice order. - self.position = min(position_history) if position_history else self.position - - def _handle_glob_attr(self, el): - if el.startswith("*clef"): - self._handle_clef(el) - elif el.startswith("*k"): - self._handle_keysig(el) - elif el.startswith("*MM"): - pass - elif el.startswith("*M"): - self._handle_metersig(el) - elif el.endswith(":"): - self._handle_mode(el) - elif el.startswith("*S/sic"): - self.parsing = "left" - elif el.startswith("*S/ossia"): - self.parsing = "right" - elif el.startswith("Xstrophe"): - self.parsing = "full" - - -class KernParser: - def __init__(self, document, doc_name): - self.document = document - self.doc_name = doc_name - self.qdivs = self.find_lcm(document.flatten()) - # TODO review this code - self.DIVS2Q = { - 1: 0.25, - 2: 0.5, - 4: 1, - 6: 1.5, - 8: 2, - 16: 4, - 24: 6, - 32: 8, - 48: 12, - 64: 16, - 128: 32, - 256: 64, - } - # self.qdivs = - self.parts = self.process() - - def __getitem__(self, item): - return self.parts[item] - - def process(self): - # TODO handle pickup - # has_pickup = not np.all(np.char.startswith(self.document, "=1-") == False) - # if not has_pickup: - # position = 0 - # else: - # position = self._handle_pickup_position() - position = 0 - # Add for parallel processing - parts = [ - self.collect(self.document[i], position, str(i), self.doc_name) - for i in reversed(range(self.document.shape[0])) - ] - return [p for p in parts if p] - - def add2part(self, part, unprocessed): - flatten = [item for sublist in unprocessed for item in sublist] - if unprocessed: - new_part = KernParserPart( - flatten, 0, self.doc_name, "x", self.qdivs, part.barline_dict - ) - self.parts.append(new_part) - - def collect(self, doc, pos, id, doc_name): - if doc[0] == "**kern": - qdivs = self.find_lcm(doc) if self.qdivs is None else self.qdivs - x = KernParserPart(doc, pos, id, doc_name, qdivs).part - return x - - # TODO handle position of pick-up measure? - def _handle_pickup_position(self): - return 0 - - def find_lcm(self, doc): - kern_string = "-".join([row for row in doc]) - match = re.findall(r"([0-9]+)([a-g]|[A-G]|r|\.)", kern_string) - durs, _ = zip(*match) - x = np.array(list(map(lambda x: int(x), durs))) - divs = np.lcm.reduce(np.unique(x[x != 0])) - return float(divs) # / 4.00 +import partitura.score as spt +from partitura.utils import PathLike + + +SIGN_TO_ACC = { + "n": 0, + "#": 1, + "s": 1, + "ss": 2, + "x": 2, + "##": 2, + "###": 3, + "b": -1, + "f": -1, + "bb": -2, + "ff": -2, + "bbb": -3, + "-": None, +} + +KERN_NOTES = { + "C": ("C", 3), + "D": ("D", 3), + "E": ("E", 3), + "F": ("F", 3), + "G": ("G", 3), + "A": ("A", 3), + "B": ("B", 3), + "c": ("C", 4), + "d": ("D", 4), + "e": ("E", 4), + "f": ("F", 4), + "g": ("G", 4), + "a": ("A", 4), + "b": ("B", 4), +} + +KERN_DURS = { + "000": "maxima", + "00": "long", + "0": "breve", + "1": "whole", + "2": "half", + "4": "quarter", + "8": "eighth", + "16": "16th", + "32": "32nd", + "64": "64th", + "128": "128th", + "256": "256th", +} def _handle_kern_with_spine_splitting(kern_path): @@ -590,7 +113,7 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: try: # This version of the parser is faster but does not support spine splitting. file = np.loadtxt(kern_path, dtype=str, delimiter="\t", comments="!", encoding="utf-8") - except: + except ValueError: # This version of the parser supports spine splitting but is slower. # It adds the splines to with a special character. file = _handle_kern_with_spine_splitting(kern_path) @@ -598,73 +121,150 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: # Get Main Number of parts and Spline Types spline_types = file[0] # Decide Parts - + part_number, staff, voice = 0, 0, 0 # Get Splines splines = file.T for spline in splines: - parse_spline(spline) + parser = SplineParser(spline, part_number, staff, voice) + elements = parser.parse() + + +class SplineParser(object): + def init(self, spline, part_number, staff, voice): + self.spline = spline + self.part_number = part_number + self.staff = staff + self.voice = voice + + def parse(self): + spline = self.spline + # Remove "-" lines + spline = spline[spline != "-"] + # Remove "." lines + spline = spline[spline != "."] + # Find Global indices, i.e. where spline cells start with "*" + global_mask = np.char.find(spline, "*") != -1 + # Remove the global indices from the spline + spline = spline[~global_mask] + + # Empty Numpy array with objects + elements = np.empty(len(spline), dtype=object) + + # Find Barline indices, i.e. where spline cells start with "=" + bar_mask = np.char.find(spline, "=") != -1 + # Find Chord indices, i.e. where spline cells contain " " + chord_mask = np.char.find(spline, " ") != -1 + + # All the rest are note indices + note_mask = np.logical_and(~bar_mask, ~chord_mask) + + elements[note_mask] = np.vectorize(self.meta_note_line, otypes=[object])(spline[note_mask]) + elements[chord_mask] = np.vectorize(self.meta_chord_line, otypes=[object])(spline[chord_mask]) + elements[bar_mask] = np.vectorize(self.meta_barline_line, otypes=[object])(spline[bar_mask]) + + return elements + + def _process_kern_pitch(self, pitch): + # find accidentals + alter = re.search(r"([n#\-]+)", pitch) + # remove alter from pitch + pitch = pitch.replace(alter.group(0), "") if alter else pitch + step, octave = KERN_NOTES[pitch[0]] + if octave == 4: + octave = octave + pitch.count(pitch[0]) - 1 + elif octave == 3: + octave = octave - pitch.count(pitch[0]) + 1 + alter = SIGN_TO_ACC[alter.group(0)] if alter else None + return step, octave, alter + + def _process_kern_duration(self, duration): + dur = duration.replace(".", "") + if dur in KERN_DURS.keys(): + symbolic_duration = {"type": KERN_DURS[dur]} + else: + diff = dict( + ( + map( + lambda x: (dur - x, x) if dur > x else (dur + x, x), + KERN_DURS.keys(), + ) + ) + ) + symbolic_duration = { + "type": KERN_DURS[diff[min(list(diff.keys()))]], + "actual_notes": dur / 4, + "normal_notes": diff[min(list(diff.keys()))] / 4, + } + symbolic_duration["dots"] = duration.count(".") + return symbolic_duration + def meta_note_line(self, line): + """ + Grammar Defining a note line. -def parse_spline(spline): - # Remove "-" lines - spline = spline[spline != "-"] - # Find Barline indices, i.e. where spline cells start with "=" - barline_indices = np.where(np.char.startswith(spline, "="))[0] - # Find Chord indices, i.e. where spline cells contain " " - chord_indices = np.where(np.char.find(spline, " ") != -1)[0] - # Find Rest indices, i.e. where spline cells contain "r" - rest_indices = np.where(np.char.find(spline, "r") != -1)[0] - # All the rest are note indices - non_note_indices = np.hstack((chord_indices, barline_indices, rest_indices)) - note_indices = ~np.isin(np.arange(len(spline)), non_note_indices, invert=True) - # Get Notes - notes = spline[note_indices] + A note line is specified by the following grammar: + note_line = symbol | duration | pitch | symbol + Parameters + ---------- + line + Returns + ------- + """ + # extract first occurence of one of the following: a-g A-G r # - n + pitch = re.search(r"([a-gA-Gr\-n#]+)", line).group(0) + # extract duration can be any of the following: 0-9 . + duration = re.search(r"([0-9]+|\.)", line).group(0) + # extract symbol can be any of the following: _()[]{}<>|: + symbol = re.findall(r"([_()[]{}<>|:])", line) + symbolic_duration = self._process_kern_duration(duration) + if pitch == "r": + return spt.Rest(symbolic_duration=symbolic_duration) + step, octave, alter = self._process_kern_pitch(pitch) + return spt.Note(step, octave, alter, symbolic_duration=symbolic_duration) -@deprecated_alias(kern_path="filename") -@deprecated_parameter("ensure_list") -def load_kern( - filename: PathLike, - force_note_ids: Optional[Union[bool, str]] = None, - parallel: bool = False, -) -> score.Score: - """Parse a Kern file and build a composite score ontology - structure from it (see also scoreontology.py). + def meta_barline_line(self, line): + """ + Grammar Defining a barline line. - Parameters - ---------- - filename : PathLike - Path to the Kern file to be parsed - force_note_ids : (bool, 'keep') optional. - When True each Note in the returned Part(s) will have a newly - assigned unique id attribute. Existing note id attributes in - the Kern will be discarded. If 'keep', only notes without - a note id will be assigned one. + A barline line is specified by the following grammar: + barline_line = repeat | barline | number | repeat - Returns - ------- - scr: :class:`partitura.score.Score` - A `Score` object - """ - # parse kern file - numpy_parts = parse_kern_v2(filename) - # doc_name = os.path.basename(filename[:-4]) - doc_name = get_document_name(filename) - parser = KernParser(numpy_parts, doc_name) - partlist = parser.parts + Parameters + ---------- + line + + Returns + ------- + + """ + # find number and keep its index. + number = re.findall(r"([0-9]+)", line) + number_index = line.index(number[0]) if number else line.index("=") + closing_repeat = re.findall(r"[:|]", line[:number_index]) + opening_repeat = re.findall(r"[:|]", line[number_index:]) + return spt.BarLine() + + def meta_chord_line(self, line): + """ + Grammar Defining a chord line. + + A chord line is specified by the following grammar: + chord_line = note | chord - score.assign_note_ids( - partlist, keep=(force_note_ids is True or force_note_ids == "keep") - ) + Parameters + ---------- + line - # TODO: Parse score info (composer, lyricist, etc.) - scr = score.Score(id=doc_name, partlist=partlist) + Returns + ------- - return scr + """ + return ("c", [self.meta_note_line(n) for n in line.split(" ")]) if __name__ == "__main__": - kern_path = "/home/manos/.struttura/ScriabinHumdrumDataset/op01/scriabin-op01.krn" + kern_path = "/home/manos/Desktop/JKU/data/wtc-fugues/wtc1f02.krn" x = parse_kern(kern_path) \ No newline at end of file From 79f6be046093ea165b050b568851f6b18abcfefa Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 4 Dec 2023 17:30:36 +0100 Subject: [PATCH 018/197] additions to the kern parsing workflow. --- partitura/io/importkern_v2.py | 223 ++++++++++++++++++++++++++++++---- 1 file changed, 198 insertions(+), 25 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index 4fd1ab7a..c1f1a141 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -63,6 +63,17 @@ } +def add_durations(a, b): + return a*b / (a + b) + + +def dot_function(duration, dots): + if dots == 0: + return duration + else: + return add_durations((2**dots)*duration, dot_function(duration, dots - 1)) + + def _handle_kern_with_spine_splitting(kern_path): file = np.loadtxt(kern_path, dtype=str, delimiter="\n", comments="!", encoding="utf-8") # Get Main Number of parts and Spline Types @@ -121,49 +132,202 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: # Get Main Number of parts and Spline Types spline_types = file[0] # Decide Parts - part_number, staff, voice = 0, 0, 0 + parts = [] + # Find parsable parts if they start with "**kern" or "**notes" + note_parts = np.char.startswith(spline_types, "**kern") | np.char.startswith(spline_types, "**notes") + # Get Splines - splines = file.T + splines = file[1:].T[note_parts] for spline in splines: - parser = SplineParser(spline, part_number, staff, voice) - elements = parser.parse() + parser = SplineParser(size=spline.shape[-1]) + + if parser.id in [p.id for p in parts]: + warnings.warn("Part {} already exists. Adding to previous Part.".format(parser.id)) + parser.voice += 1 + part = [p for p in parts if p.id == parser.id][0] + has_staff = np.char.startswith(spline, "*staff") + staff = int(spline[has_staff][0][6:]) if np.count_nonzero(has_staff) else 1 + parser.staff = staff + elements = parser.parse(spline) + unique_durs = np.unique(parser.total_duration_values).astype(int) + divs_pq = np.lcm.reduce(unique_durs) + divs_pq = divs_pq if divs_pq > 4 else 4 + part.set_quarter_duration(0, divs_pq) + else: + elements = parser.parse(spline) + unique_durs = np.unique(parser.total_duration_values).astype(int) + divs_pq = np.lcm.reduce(unique_durs) + divs_pq = divs_pq if divs_pq > 4 else 4 + # Initialize Part + part = spt.Part(id=parser.id, quarter_duration=divs_pq, part_name=parser.name) + current_tl_pos = 0 + + for i in range(elements.shape[0]): + element = elements[i] + if element is None: + continue + if isinstance(element, spt.GenericNote): + quarter_duration = 4 / parser.total_duration_values[i] + duration_divs = int(quarter_duration*divs_pq) + el_end = current_tl_pos + duration_divs + part.add(element, start=current_tl_pos, end=el_end) + current_tl_pos = el_end + elif isinstance(element, tuple): + # Chord + quarter_duration = parser.total_duration_values[i] + duration_divs = int(part.inv_quarter_map(quarter_duration)) + el_end = current_tl_pos + duration_divs + for note in element[1]: + part.add(note, start=current_tl_pos, end=el_end) + current_tl_pos = el_end + else: + part.add(element, start=current_tl_pos) + + # For all measures add end time as beginning time of next measure + measures = part.measures + for i in range(len(measures) - 1): + measures[i].end = measures[i + 1].start + measures[-1].end = part.last_point + + if parser.id not in [p.id for p in parts]: + parts.append(part) + return spt.Score(parts) class SplineParser(object): - def init(self, spline, part_number, staff, voice): - self.spline = spline - self.part_number = part_number + def __init__(self, id="P1", staff=1, voice=1, size=1, name=""): + self.id = id + self.name = name self.staff = staff self.voice = voice + self.total_duration_values = [] + self.size = size + self.total_parsed_elements = 0 - def parse(self): - spline = self.spline + def parse(self, spline): # Remove "-" lines spline = spline[spline != "-"] # Remove "." lines spline = spline[spline != "."] - # Find Global indices, i.e. where spline cells start with "*" - global_mask = np.char.find(spline, "*") != -1 - # Remove the global indices from the spline - spline = spline[~global_mask] - # Empty Numpy array with objects elements = np.empty(len(spline), dtype=object) - + self.total_duration_values = np.ones(len(spline)) + # Find Global indices, i.e. where spline cells start with "*" + tandem_mask = np.char.find(spline, "*") != -1 # Find Barline indices, i.e. where spline cells start with "=" bar_mask = np.char.find(spline, "=") != -1 # Find Chord indices, i.e. where spline cells contain " " chord_mask = np.char.find(spline, " ") != -1 - # All the rest are note indices - note_mask = np.logical_and(~bar_mask, ~chord_mask) - + note_mask = np.logical_and(~tandem_mask, np.logical_and(~bar_mask, ~chord_mask)) + elements[tandem_mask] = np.vectorize(self.meta_tandem_line, otypes=[object])(spline[tandem_mask]) + self.total_parsed_elements = -1 + self.note_duration_values = np.ones(len(spline[note_mask])) elements[note_mask] = np.vectorize(self.meta_note_line, otypes=[object])(spline[note_mask]) + self.total_duration_values[note_mask] = self.note_duration_values + self.total_parsed_elements = -1 + self.note_duration_values = np.ones(len(spline[chord_mask])) elements[chord_mask] = np.vectorize(self.meta_chord_line, otypes=[object])(spline[chord_mask]) + self.total_duration_values[chord_mask] = self.note_duration_values elements[bar_mask] = np.vectorize(self.meta_barline_line, otypes=[object])(spline[bar_mask]) return elements + def meta_tandem_line(self, line): + """ + Find all tandem lines + """ + # find number and keep its index. + self.total_parsed_elements += 1 + if line.startswith("*MM"): + rest = line[3:] + return self.process_tempo_line(rest) + elif line.startswith("*I"): + rest = line[2:] + return self.process_istrument_line(rest) + elif line.startswith("*clef"): + rest = line[5:] + return self.process_clef_line(rest) + elif line.startswith("*M"): + rest = line[2:] + return self.process_meter_line(rest) + elif line.startswith("*k"): + rest = line[2:] + return self.process_key_signature_line(rest) + elif line.startswith("*IC"): + rest = line[3:] + return self.process_istrument_class_line(rest) + elif line.startswith("*IG"): + rest = line[3:] + return self.process_istrument_group_line(rest) + elif line.startswith("*tb"): + rest = line[3:] + return self.process_timebase_line(rest) + elif line.startswith("*ITr"): + rest = line[4:] + return self.process_istrument_transpose_line(rest) + elif line.startswith("*staff"): + rest = line[6:] + return self.process_staff_line(rest) + elif line.endswith(":"): + rest = line[1:] + return self.process_key_line(rest) + + def process_tempo_line(self, line): + return spt.Tempo(float(line)) + + def process_istrument_line(self, line): + return + + def process_istrument_class_line(self, line): + return + + def process_istrument_group_line(self, line): + return + + def process_timebase_line(self, line): + return + + def process_istrument_transpose_line(self, line): + return + + def process_key_line(self, line): + find = re.search(r"([a-gA-G])", line).group(0) + # check if the key is major or minor by checking if the key is in lower or upper case. + self.mode = "minor" if find.islower() else "major" + return + + def process_staff_line(self, line): + self.staff = int(line) + return spt.Staff(self.staff) + + def process_clef_line(self, line): + # if the cleff line does not contain any of the following characters, ["G", "F", "C"], raise a ValueError. + if not any(c in line for c in ["G", "F", "C"]): + raise ValueError("Unrecognized clef line: {}".format(line)) + # find the clef + clef = re.search(r"([GFC])", line).group(0) + # find the octave + line = re.search(r"([0-9])", line).group(0) + return spt.Clef(sign=clef, staff=self.staff, line=int(line), octave_change=0) + + def process_key_signature_line(self, line): + fifths = 0 + for c in line: + if c == "#": + fifths += 1 + if c == "b": + fifths -= 1 + # TODO retrieve the key mode + mode = "major" + return spt.KeySignature(fifths, mode) + + def process_meter_line(self, line): + if " " in line: + line = line.split(" ")[0] + numerator, denominator = map(eval, line.split("/")) + return spt.TimeSignature(numerator, denominator) + def _process_kern_pitch(self, pitch): # find accidentals alter = re.search(r"([n#\-]+)", pitch) @@ -196,9 +360,10 @@ def _process_kern_duration(self, duration): "normal_notes": diff[min(list(diff.keys()))] / 4, } symbolic_duration["dots"] = duration.count(".") + self.note_duration_values[self.total_parsed_elements] = dot_function(float(dur), symbolic_duration["dots"]) return symbolic_duration - def meta_note_line(self, line): + def meta_note_line(self, line, voice=None, add=True): """ Grammar Defining a note line. @@ -213,6 +378,8 @@ def meta_note_line(self, line): ------- """ + self.total_parsed_elements += 1 if add else 0 + voice = self.voice if voice is None else voice # extract first occurence of one of the following: a-g A-G r # - n pitch = re.search(r"([a-gA-Gr\-n#]+)", line).group(0) # extract duration can be any of the following: 0-9 . @@ -220,10 +387,11 @@ def meta_note_line(self, line): # extract symbol can be any of the following: _()[]{}<>|: symbol = re.findall(r"([_()[]{}<>|:])", line) symbolic_duration = self._process_kern_duration(duration) + el_id = "{}-s{}-v{}-el{}".format(self.id, self.staff, voice, self.total_parsed_elements) if pitch == "r": - return spt.Rest(symbolic_duration=symbolic_duration) + return spt.Rest(symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) step, octave, alter = self._process_kern_pitch(pitch) - return spt.Note(step, octave, alter, symbolic_duration=symbolic_duration) + return spt.Note(step, octave, alter, symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) def meta_barline_line(self, line): """ @@ -241,11 +409,12 @@ def meta_barline_line(self, line): """ # find number and keep its index. + self.total_parsed_elements += 1 number = re.findall(r"([0-9]+)", line) number_index = line.index(number[0]) if number else line.index("=") closing_repeat = re.findall(r"[:|]", line[:number_index]) - opening_repeat = re.findall(r"[:|]", line[number_index:]) - return spt.BarLine() + opening_repeat = re.findall(r"[|:]", line[number_index:]) + return spt.Measure(number=int(number[0]) if number else None) def meta_chord_line(self, line): """ @@ -262,9 +431,13 @@ def meta_chord_line(self, line): ------- """ - return ("c", [self.meta_note_line(n) for n in line.split(" ")]) + self.total_parsed_elements += 1 + chord = ("c", [self.meta_note_line(n, add=False) for n in line.split(" ")]) + return chord if __name__ == "__main__": kern_path = "/home/manos/Desktop/JKU/data/wtc-fugues/wtc1f02.krn" - x = parse_kern(kern_path) \ No newline at end of file + x = parse_kern(kern_path) + import partitura as pt + pt.save_musicxml(x, "/home/manos/Desktop/test_kern.musicxml") \ No newline at end of file From f7ce8c66e31e3685b4c9a43ba4b0a7613f1d3400 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 5 Dec 2023 15:08:05 +0100 Subject: [PATCH 019/197] A first complete version of faster kern parsing. --- partitura/io/importkern_v2.py | 109 +++++++++++++++++++++++++++------- 1 file changed, 88 insertions(+), 21 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index c1f1a141..f5a94d91 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -127,6 +127,7 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: except ValueError: # This version of the parser supports spine splitting but is slower. # It adds the splines to with a special character. + raise NotImplementedError("Spine splitting is not supported yet.") file = _handle_kern_with_spine_splitting(kern_path) # Get Main Number of parts and Spline Types @@ -140,14 +141,18 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: splines = file[1:].T[note_parts] for spline in splines: parser = SplineParser(size=spline.shape[-1]) - + same_part = False if parser.id in [p.id for p in parts]: + same_part = True warnings.warn("Part {} already exists. Adding to previous Part.".format(parser.id)) parser.voice += 1 part = [p for p in parts if p.id == parser.id][0] has_staff = np.char.startswith(spline, "*staff") staff = int(spline[has_staff][0][6:]) if np.count_nonzero(has_staff) else 1 - parser.staff = staff + if parser.staff != staff: + parser.staff = staff + else: + parser.voice += 1 elements = parser.parse(spline) unique_durs = np.unique(parser.total_duration_values).astype(int) divs_pq = np.lcm.reduce(unique_durs) @@ -174,14 +179,16 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: current_tl_pos = el_end elif isinstance(element, tuple): # Chord - quarter_duration = parser.total_duration_values[i] + quarter_duration = 4 / parser.total_duration_values[i] duration_divs = int(part.inv_quarter_map(quarter_duration)) el_end = current_tl_pos + duration_divs for note in element[1]: part.add(note, start=current_tl_pos, end=el_end) current_tl_pos = el_end else: - part.add(element, start=current_tl_pos) + # Do not repeat structural elements if they are being added to the same part. + if not same_part: + part.add(element, start=current_tl_pos) # For all measures add end time as beginning time of next measure measures = part.measures @@ -203,6 +210,10 @@ def __init__(self, id="P1", staff=1, voice=1, size=1, name=""): self.total_duration_values = [] self.size = size self.total_parsed_elements = 0 + self.tie_prev = None + self.tie_next = None + + def parse(self, spline): # Remove "-" lines @@ -212,27 +223,43 @@ def parse(self, spline): # Empty Numpy array with objects elements = np.empty(len(spline), dtype=object) self.total_duration_values = np.ones(len(spline)) - # Find Global indices, i.e. where spline cells start with "*" + # Find Global indices, i.e. where spline cells start with "*" and process tandem_mask = np.char.find(spline, "*") != -1 + elements[tandem_mask] = np.vectorize(self.meta_tandem_line, otypes=[object])(spline[tandem_mask]) # Find Barline indices, i.e. where spline cells start with "=" bar_mask = np.char.find(spline, "=") != -1 + elements[bar_mask] = np.vectorize(self.meta_barline_line, otypes=[object])(spline[bar_mask]) # Find Chord indices, i.e. where spline cells contain " " chord_mask = np.char.find(spline, " ") != -1 + self.total_parsed_elements = -1 + self.note_duration_values = np.ones(len(spline[chord_mask])) + chord_num = np.count_nonzero(chord_mask) + self.tie_next = np.zeros(chord_num, dtype=bool) + self.tie_prev = np.zeros(chord_num, dtype=bool) + elements[chord_mask] = np.vectorize(self.meta_chord_line, otypes=[object])(spline[chord_mask]) + self.total_duration_values[chord_mask] = self.note_duration_values # All the rest are note indices note_mask = np.logical_and(~tandem_mask, np.logical_and(~bar_mask, ~chord_mask)) - elements[tandem_mask] = np.vectorize(self.meta_tandem_line, otypes=[object])(spline[tandem_mask]) self.total_parsed_elements = -1 self.note_duration_values = np.ones(len(spline[note_mask])) - elements[note_mask] = np.vectorize(self.meta_note_line, otypes=[object])(spline[note_mask]) + note_num = np.count_nonzero(note_mask) + self.tie_next = np.zeros(note_num, dtype=bool) + 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 - self.total_parsed_elements = -1 - self.note_duration_values = np.ones(len(spline[chord_mask])) - elements[chord_mask] = np.vectorize(self.meta_chord_line, otypes=[object])(spline[chord_mask]) - self.total_duration_values[chord_mask] = self.note_duration_values - elements[bar_mask] = np.vectorize(self.meta_barline_line, otypes=[object])(spline[bar_mask]) - + # shift tie_next by one to the right + 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 return elements + + def meta_tandem_line(self, line): """ Find all tandem lines @@ -272,23 +299,33 @@ def meta_tandem_line(self, line): elif line.endswith(":"): rest = line[1:] return self.process_key_line(rest) + elif line.startswith("*-"): + return self.process_fine() def process_tempo_line(self, line): return spt.Tempo(float(line)) + def process_fine(self): + return spt.Fine() + def process_istrument_line(self, line): + #TODO: add support for instrument lines return def process_istrument_class_line(self, line): + # TODO: add support for instrument class lines return def process_istrument_group_line(self, line): + # TODO: add support for instrument group lines return def process_timebase_line(self, line): + # TODO: add support for timebase lines return def process_istrument_transpose_line(self, line): + # TODO: add support for instrument transpose lines return def process_key_line(self, line): @@ -312,12 +349,7 @@ def process_clef_line(self, line): return spt.Clef(sign=clef, staff=self.staff, line=int(line), octave_change=0) def process_key_signature_line(self, line): - fifths = 0 - for c in line: - if c == "#": - fifths += 1 - if c == "b": - fifths -= 1 + fifths = line.count("#") - line.count("-") # TODO retrieve the key mode mode = "major" return spt.KeySignature(fifths, mode) @@ -363,6 +395,38 @@ def _process_kern_duration(self, duration): self.note_duration_values[self.total_parsed_elements] = dot_function(float(dur), symbolic_duration["dots"]) return symbolic_duration + def process_symbol(self, note, symbols): + """ + Process the symbols of a note. + + Parameters + ---------- + note + symbol + + Returns + ------- + + """ + if "[" in symbols: + self.tie_prev[self.total_parsed_elements] = True + # pop symbol and call again + symbols.pop(symbols.index("[")) + self.process_symbol(note, symbols) + if "]" in symbols: + self.tie_next[self.total_parsed_elements] = True + symbols.pop(symbols.index("]")) + self.process_symbol(note, symbols) + if "_" in symbols: + # continuing tie + self.tie_prev[self.total_parsed_elements] = True + self.tie_next[self.total_parsed_elements] = True + symbols.pop(symbols.index("_")) + self.process_symbol(note, symbols) + return + + + def meta_note_line(self, line, voice=None, add=True): """ Grammar Defining a note line. @@ -385,13 +449,16 @@ def meta_note_line(self, line, voice=None, add=True): # extract duration can be any of the following: 0-9 . duration = re.search(r"([0-9]+|\.)", line).group(0) # extract symbol can be any of the following: _()[]{}<>|: - symbol = re.findall(r"([_()[]{}<>|:])", line) + symbols = re.findall(r"([_()\[\]{}<>|:])", line) symbolic_duration = self._process_kern_duration(duration) el_id = "{}-s{}-v{}-el{}".format(self.id, self.staff, voice, self.total_parsed_elements) if pitch == "r": return spt.Rest(symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) step, octave, alter = self._process_kern_pitch(pitch) - return spt.Note(step, octave, alter, symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) + note = spt.Note(step, octave, alter, symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) + if symbols: + self.process_symbol(note, symbols) + return note def meta_barline_line(self, line): """ From 8da824c68fadfad5fa00d24c89b0fb2a9956f722 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 5 Dec 2023 16:46:28 +0100 Subject: [PATCH 020/197] minor styling comments. --- partitura/io/importkern_v2.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index f5a94d91..9d2e30e8 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -128,6 +128,7 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: # This version of the parser supports spine splitting but is slower. # It adds the splines to with a special character. raise NotImplementedError("Spine splitting is not supported yet.") + # TODO add support for spine splitting file = _handle_kern_with_spine_splitting(kern_path) # Get Main Number of parts and Spline Types @@ -213,8 +214,6 @@ def __init__(self, id="P1", staff=1, voice=1, size=1, name=""): self.tie_prev = None self.tie_next = None - - def parse(self, spline): # Remove "-" lines spline = spline[spline != "-"] @@ -238,6 +237,8 @@ def parse(self, spline): self.tie_prev = np.zeros(chord_num, dtype=bool) elements[chord_mask] = np.vectorize(self.meta_chord_line, otypes=[object])(spline[chord_mask]) self.total_duration_values[chord_mask] = self.note_duration_values + # TODO: figure out slurs for chords + # All the rest are note indices note_mask = np.logical_and(~tandem_mask, np.logical_and(~bar_mask, ~chord_mask)) self.total_parsed_elements = -1 @@ -258,8 +259,6 @@ def parse(self, spline): elements[note_mask] = notes return elements - - def meta_tandem_line(self, line): """ Find all tandem lines @@ -425,8 +424,6 @@ def process_symbol(self, note, symbols): self.process_symbol(note, symbols) return - - def meta_note_line(self, line, voice=None, add=True): """ Grammar Defining a note line. From 41ab573406dafed70ae6beb4552dd4bfdefcb02f Mon Sep 17 00:00:00 2001 From: sildater Date: Wed, 10 Jan 2024 16:17:49 +0100 Subject: [PATCH 021/197] add default value for midi score export --- partitura/io/exportmidi.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/partitura/io/exportmidi.py b/partitura/io/exportmidi.py index c1639bb1..195e2880 100644 --- a/partitura/io/exportmidi.py +++ b/partitura/io/exportmidi.py @@ -364,6 +364,9 @@ def to_ppq(t): tempos[to_ppq(tp.start.t)] = MetaMessage( "set_tempo", tempo=tp.microseconds_per_quarter ) + # default tempo + if not tempos: + tempos[0] = MetaMessage("set_tempo", tempo=500000) if anacrusis_behavior == "time_sig_change": # Change time signature to match the duration of the measure From 08e55e37c997f5bc5b670af33656d8401e20ca52 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 11 Jan 2024 15:12:05 +0100 Subject: [PATCH 022/197] Minor styling. --- partitura/score.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index 95f64ea3..010e9c3c 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -2824,13 +2824,11 @@ def _filter_cadence_type(self): """Cadence should be one of PAC, IAC, HC, DC, EC, PC, or None""" # capitalize text self.text = self.text.upper() - if "IAC" in self.text: - self.text = "IAC" + self.text = "IAC" if "IAC" in self.text else self.text if self.text not in ["PAC", "IAC", "HC", "DC", "EC", "PC"]: warnings.warn(f"Cadence type {self.text} not found. Setting to None") self.text = None - def __str__(self): return f'{super().__str__()} "{self.text}"' From cb8baff51e82645196141ae059a719fc9267de85 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 11 Jan 2024 17:42:07 +0100 Subject: [PATCH 023/197] First edition of import kern with stream splitting. --- partitura/io/importkern_v2.py | 125 ++++++++++++++++++++++++---------- 1 file changed, 89 insertions(+), 36 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index 9d2e30e8..5e6ac6d7 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -73,38 +73,87 @@ def dot_function(duration, dots): else: return add_durations((2**dots)*duration, dot_function(duration, dots - 1)) +def parse_by_voice(file, dtype=np.object_): + indices_to_remove = [] + voices = 1 + for i, line in enumerate(file): + try: + if any([line[v] == "*^" for v in range(voices)]): + voices += 1 + elif sum([(line[v] == "*v") for v in range(voices)]): + voices -= sum([line[v] == "*v" for v in range(voices)]) // 2 + else: + for v in range(voices): + indices_to_remove.append([i, v]) + except IndexError: + pass + + + voice_indices = np.array(indices_to_remove) + num_voices = voice_indices[:, 1].max() + 1 + data = np.empty((len(file), num_voices), dtype=dtype) + for line, voice in voice_indices: + data[line, voice] = file[line][voice] + data = data.T + if num_voices > 1: + # Copy global lines from the first voice to all other voices + cp_idx = np.char.startswith(data[0], "*") + 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 + cp_idx = np.char.startswith(data[0], "=") + for i in range(1, num_voices): + data[i][cp_idx] = data[0][cp_idx] + return data, voice_indices + def _handle_kern_with_spine_splitting(kern_path): file = np.loadtxt(kern_path, dtype=str, delimiter="\n", comments="!", encoding="utf-8") # Get Main Number of parts and Spline Types spline_types = file[0].split("\t") - # Decide Parts - - # Find all expansions points - expansion_indices = np.where(np.char.find(file, "*^") != -1)[0] - # For all expansion points find which stream is being expanded - expansion_streams_per_index = [np.argwhere(np.array(line.split("\t")) == "*^")[0] for line in - file[expansion_indices]] - - # Find all Spline Reduction points - reduction_indices = np.where(np.char.find(file, "*v\t*v") != -1)[0] - # For all reduction points find which stream is being reduced - reduction_streams_per_index = [ - np.argwhere(np.char.add(np.array(line.split("\t")[:-1]), np.array(line.split("\t")[1:])) == "*v*v")[0] for line - in file[reduction_indices]] - - # Find all pairs of expansion and reduction points - expansion_reduction_pairs = [] - last_exhaustive_reduction = 0 - for expansion_index in expansion_indices: - for expansion_stream in expansion_index: - # Find the first reduction index that is after the expansion index and has the same index. - for i, reduction_index in enumerate(reduction_indices[last_exhaustive_reduction:]): - for reduction_stream in reduction_streams_per_index[i]: - if expansion_stream == reduction_stream: - expansion_reduction_pairs.append((expansion_index, reduction_index)) - last_exhaustive_reduction = i if i == last_exhaustive_reduction + 1 else last_exhaustive_reduction - break + dtype = file.dtype + data = [] + file = file.tolist() + file = [line.split("\t") for line in file] + continue_parsing = True + for i in range(len(spline_types)): + # Parse by voice + d, voice_indices = parse_by_voice(file, dtype=dtype) + data.append(d) + # Remove all parsed cells from the file + voice_indices = voice_indices[np.lexsort((voice_indices[:, 1]*-1, voice_indices[:, 0]))] + for line, voice in voice_indices: + file[line].pop(voice) + + data = np.vstack(data).T + return data + # + # + # # Find all expansions points + # expansion_indices = np.where(np.char.find(file, "*^") != -1)[0] + # # For all expansion points find which stream is being expanded + # expansion_streams_per_index = [np.argwhere(np.array(line.split("\t")) == "*^")[0] for line in + # file[expansion_indices]] + # + # # Find all Spline Reduction points + # reduction_indices = np.where(np.char.find(file, "*v\t*v") != -1)[0] + # # For all reduction points find which stream is being reduced + # reduction_streams_per_index = [ + # np.argwhere(np.char.add(np.array(line.split("\t")[:-1]), np.array(line.split("\t")[1:])) == "*v*v")[0] for line + # in file[reduction_indices]] + # + # # Find all pairs of expansion and reduction points + # expansion_reduction_pairs = [] + # last_exhaustive_reduction = 0 + # for expansion_index in expansion_indices: + # for expansion_stream in expansion_index: + # # Find the first reduction index that is after the expansion index and has the same index. + # for i, reduction_index in enumerate(reduction_indices[last_exhaustive_reduction:]): + # for reduction_stream in reduction_streams_per_index[i]: + # if expansion_stream == reduction_stream: + # expansion_reduction_pairs.append((expansion_index, reduction_index)) + # last_exhaustive_reduction = i if i == last_exhaustive_reduction + 1 else last_exhaustive_reduction + # break # functions to initialize the kern parser @@ -124,17 +173,17 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: try: # This version of the parser is faster but does not support spine splitting. file = np.loadtxt(kern_path, dtype=str, delimiter="\t", comments="!", encoding="utf-8") + # Decide Parts + parts = [] except ValueError: # This version of the parser supports spine splitting but is slower. - # It adds the splines to with a special character. - raise NotImplementedError("Spine splitting is not supported yet.") - # TODO add support for spine splitting file = _handle_kern_with_spine_splitting(kern_path) + parts = [] + # Get Main Number of parts and Spline Types spline_types = file[0] - # Decide Parts - parts = [] + # Find parsable parts if they start with "**kern" or "**notes" note_parts = np.char.startswith(spline_types, "**kern") | np.char.startswith(spline_types, "**notes") @@ -219,6 +268,8 @@ def parse(self, spline): spline = spline[spline != "-"] # Remove "." lines spline = spline[spline != "."] + # Remove Empty lines + spline = spline[spline != ""] # Empty Numpy array with objects elements = np.empty(len(spline), dtype=object) self.total_duration_values = np.ones(len(spline)) @@ -377,18 +428,20 @@ def _process_kern_duration(self, duration): if dur in KERN_DURS.keys(): symbolic_duration = {"type": KERN_DURS[dur]} else: + dur = eval(dur) diff = dict( ( map( - lambda x: (dur - x, x) if dur > x else (dur + x, x), + lambda x: (str(dur - int(x)), str(int(x))) if dur > int(x) else (str(dur + int(x)), str(int(x))), KERN_DURS.keys(), ) ) ) + symbolic_duration = { "type": KERN_DURS[diff[min(list(diff.keys()))]], "actual_notes": dur / 4, - "normal_notes": diff[min(list(diff.keys()))] / 4, + "normal_notes": int(diff[min(list(diff.keys()))]) / 4, } symbolic_duration["dots"] = duration.count(".") self.note_duration_values[self.total_parsed_elements] = dot_function(float(dur), symbolic_duration["dots"]) @@ -449,7 +502,7 @@ def meta_note_line(self, line, voice=None, add=True): symbols = re.findall(r"([_()\[\]{}<>|:])", line) symbolic_duration = self._process_kern_duration(duration) el_id = "{}-s{}-v{}-el{}".format(self.id, self.staff, voice, self.total_parsed_elements) - if pitch == "r": + if pitch.startswith("r"): return spt.Rest(symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) step, octave, alter = self._process_kern_pitch(pitch) note = spt.Note(step, octave, alter, symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) @@ -501,7 +554,7 @@ def meta_chord_line(self, line): if __name__ == "__main__": - kern_path = "/home/manos/Desktop/JKU/data/wtc-fugues/wtc1f02.krn" + kern_path = "/home/manos/Desktop/test.krn" x = parse_kern(kern_path) import partitura as pt pt.save_musicxml(x, "/home/manos/Desktop/test_kern.musicxml") \ No newline at end of file From afed000447af7a05f07ffe7dae8b1ab52a25a62c Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 12 Jan 2024 17:37:53 +0100 Subject: [PATCH 024/197] Minor corrections and debbugging. --- partitura/io/importkern_v2.py | 67 ++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index 5e6ac6d7..32628c99 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -9,7 +9,7 @@ from typing import Union, Optional import numpy as np - +from math import inf import partitura.score as spt from partitura.utils import PathLike @@ -104,13 +104,14 @@ def parse_by_voice(file, dtype=np.object_): cp_idx = np.char.startswith(data[0], "=") for i in range(1, num_voices): data[i][cp_idx] = data[0][cp_idx] - return data, voice_indices + return data, voice_indices, num_voices def _handle_kern_with_spine_splitting(kern_path): file = np.loadtxt(kern_path, dtype=str, delimiter="\n", comments="!", encoding="utf-8") # Get Main Number of parts and Spline Types spline_types = file[0].split("\t") + parsing_idxs = [] dtype = file.dtype data = [] file = file.tolist() @@ -118,15 +119,17 @@ def _handle_kern_with_spine_splitting(kern_path): continue_parsing = True for i in range(len(spline_types)): # Parse by voice - d, voice_indices = parse_by_voice(file, dtype=dtype) + d, voice_indices, num_voices = parse_by_voice(file, dtype=dtype) data.append(d) + parsing_idxs.append([i for _ in range(num_voices)]) # Remove all parsed cells from the file voice_indices = voice_indices[np.lexsort((voice_indices[:, 1]*-1, voice_indices[:, 0]))] for line, voice in voice_indices: file[line].pop(voice) data = np.vstack(data).T - return data + parsing_idxs = np.hstack(parsing_idxs).T + return data, parsing_idxs # # # # Find all expansions points @@ -173,14 +176,16 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: try: # This version of the parser is faster but does not support spine splitting. file = np.loadtxt(kern_path, dtype=str, delimiter="\t", comments="!", encoding="utf-8") + parsing_idxs = np.arange(file.shape[0]) # Decide Parts - parts = [] + + except ValueError: # This version of the parser supports spine splitting but is slower. - file = _handle_kern_with_spine_splitting(kern_path) - parts = [] + file, parsing_idxs = _handle_kern_with_spine_splitting(kern_path) + parts = [] # Get Main Number of parts and Spline Types spline_types = file[0] @@ -189,13 +194,12 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: # Get Splines splines = file[1:].T[note_parts] - for spline in splines: - parser = SplineParser(size=spline.shape[-1]) + for i, spline in enumerate(splines): + parser = SplineParser(size=spline.shape[-1], id="P{}".format(parsing_idxs[i])) same_part = False if parser.id in [p.id for p in parts]: same_part = True warnings.warn("Part {} already exists. Adding to previous Part.".format(parser.id)) - parser.voice += 1 part = [p for p in parts if p.id == parser.id][0] has_staff = np.char.startswith(spline, "*staff") staff = int(spline[has_staff][0][6:]) if np.count_nonzero(has_staff) else 1 @@ -209,6 +213,10 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: divs_pq = divs_pq if divs_pq > 4 else 4 part.set_quarter_duration(0, divs_pq) else: + has_staff = np.char.startswith(spline, "*staff") + staff = int(spline[has_staff][0][6:]) if np.count_nonzero(has_staff) else 1 + if parser.staff != staff: + parser.staff = staff elements = parser.parse(spline) unique_durs = np.unique(parser.total_duration_values).astype(int) divs_pq = np.lcm.reduce(unique_durs) @@ -217,6 +225,7 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: part = spt.Part(id=parser.id, quarter_duration=divs_pq, part_name=parser.name) current_tl_pos = 0 + 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: @@ -239,15 +248,24 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: # Do not repeat structural elements if they are being added to the same part. if not same_part: part.add(element, start=current_tl_pos) + else: + if isinstance(element, spt.Measure): + current_tl_pos = measure_mapping[element.number] # For all measures add end time as beginning time of next measure measures = part.measures for i in range(len(measures) - 1): measures[i].end = measures[i + 1].start - measures[-1].end = part.last_point + measures[-1].end = part.last_point if parser.id not in [p.id for p in parts]: parts.append(part) + + # currate parts to the same divs per quarter + divs_pq = np.lcm.reduce([p._quarter_durations[0] for p in parts]) + for part in parts: + part.set_quarter_duration(0, divs_pq) + return spt.Score(parts) @@ -270,6 +288,8 @@ def parse(self, spline): spline = spline[spline != "."] # Remove Empty lines spline = spline[spline != ""] + # Remove None lines + spline = spline[spline != None] # Empty Numpy array with objects elements = np.empty(len(spline), dtype=object) self.total_duration_values = np.ones(len(spline)) @@ -423,16 +443,17 @@ def _process_kern_pitch(self, pitch): alter = SIGN_TO_ACC[alter.group(0)] if alter else None return step, octave, alter - def _process_kern_duration(self, duration): + def _process_kern_duration(self, duration, is_grace=False): + dots = duration.count(".") dur = duration.replace(".", "") if dur in KERN_DURS.keys(): symbolic_duration = {"type": KERN_DURS[dur]} else: - dur = eval(dur) + dur = float(dur) diff = dict( ( map( - lambda x: (str(dur - int(x)), str(int(x))) if dur > int(x) else (str(dur + int(x)), str(int(x))), + lambda x: (dur - int(x), str(int(x))) if dur > int(x) else (dur + int(x), str(int(x))), KERN_DURS.keys(), ) ) @@ -440,11 +461,11 @@ def _process_kern_duration(self, duration): symbolic_duration = { "type": KERN_DURS[diff[min(list(diff.keys()))]], - "actual_notes": dur / 4, - "normal_notes": int(diff[min(list(diff.keys()))]) / 4, + "actual_notes": dur // 4, + "normal_notes": int(diff[min(list(diff.keys()))]) // 4, } - symbolic_duration["dots"] = duration.count(".") - self.note_duration_values[self.total_parsed_elements] = dot_function(float(dur), symbolic_duration["dots"]) + symbolic_duration["dots"] = dots + self.note_duration_values[self.total_parsed_elements] = dot_function((float(dur) if isinstance(dur, str) else dur), symbolic_duration["dots"]) if not is_grace else inf return symbolic_duration def process_symbol(self, note, symbols): @@ -497,15 +518,19 @@ def meta_note_line(self, line, voice=None, add=True): # extract first occurence of one of the following: a-g A-G r # - n pitch = re.search(r"([a-gA-Gr\-n#]+)", line).group(0) # extract duration can be any of the following: 0-9 . - duration = re.search(r"([0-9]+|\.)", line).group(0) + duration = re.search(r"([0-9.]+)", line).group(0) # extract symbol can be any of the following: _()[]{}<>|: symbols = re.findall(r"([_()\[\]{}<>|:])", line) - symbolic_duration = self._process_kern_duration(duration) + symbolic_duration = self._process_kern_duration(duration, is_grace="q" in line) el_id = "{}-s{}-v{}-el{}".format(self.id, self.staff, voice, self.total_parsed_elements) if pitch.startswith("r"): return spt.Rest(symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) step, octave, alter = self._process_kern_pitch(pitch) - note = spt.Note(step, octave, alter, symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) + # check if the note is a grace note + if "q" in line: + note = spt.GraceNote(grace_type="grace", step=step, octave=octave, alter=alter, symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) + else: + note = spt.Note(step, octave, alter, symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) if symbols: self.process_symbol(note, symbols) return note From 972712d4d9a225e91ece9bc964f3dd0c38d06697 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 15 Jan 2024 14:22:35 +0100 Subject: [PATCH 025/197] First functional version of kern parsing with spline splitting. --- partitura/io/importkern_v2.py | 78 +++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index 32628c99..7ff029a6 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -3,13 +3,13 @@ """ This module contains methods for importing Humdrum Kern files. """ +import math import re import warnings from typing import Union, Optional - import numpy as np -from math import inf +from math import inf, ceil import partitura.score as spt from partitura.utils import PathLike @@ -27,7 +27,8 @@ "bb": -2, "ff": -2, "bbb": -3, - "-": None, + "-": -1, + "--": -2, } KERN_NOTES = { @@ -77,16 +78,13 @@ def parse_by_voice(file, dtype=np.object_): indices_to_remove = [] voices = 1 for i, line in enumerate(file): - try: - if any([line[v] == "*^" for v in range(voices)]): - voices += 1 - elif sum([(line[v] == "*v") for v in range(voices)]): - voices -= sum([line[v] == "*v" for v in range(voices)]) // 2 - else: - for v in range(voices): - indices_to_remove.append([i, v]) - except IndexError: - pass + for v in range(voices): + indices_to_remove.append([i, v]) + if any([line[v] == "*^" for v in range(voices)]): + voices += 1 + elif sum([(line[v] == "*v") for v in range(voices)]): + sum_vred = sum([line[v] == "*v" for v in range(voices)]) // 2 + voices = voices - sum_vred voice_indices = np.array(indices_to_remove) @@ -108,13 +106,13 @@ def parse_by_voice(file, dtype=np.object_): def _handle_kern_with_spine_splitting(kern_path): - file = np.loadtxt(kern_path, dtype=str, delimiter="\n", comments="!", encoding="utf-8") + org_file = np.loadtxt(kern_path, dtype=str, delimiter="\n", comments="!!", encoding="utf-8") # Get Main Number of parts and Spline Types - spline_types = file[0].split("\t") + spline_types = org_file[0].split("\t") parsing_idxs = [] - dtype = file.dtype + dtype = org_file.dtype data = [] - file = file.tolist() + file = org_file.tolist() file = [line.split("\t") for line in file] continue_parsing = True for i in range(len(spline_types)): @@ -125,8 +123,10 @@ def _handle_kern_with_spine_splitting(kern_path): # Remove all parsed cells from the file voice_indices = voice_indices[np.lexsort((voice_indices[:, 1]*-1, voice_indices[:, 0]))] for line, voice in voice_indices: - file[line].pop(voice) - + if voice < len(file[line]): + file[line].pop(voice) + else: + print("Line {} does not have a voice {} from original line {}".format(line, voice, org_file[line])) data = np.vstack(data).T parsing_idxs = np.hstack(parsing_idxs).T return data, parsing_idxs @@ -194,8 +194,12 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: # Get Splines splines = file[1:].T[note_parts] + + has_instrument = np.char.startswith(splines, "*I") + # if all parts have the same instrument, then they are the same part. + p_same_part = np.all(splines[has_instrument] == splines[has_instrument][0], axis=0) if np.any(has_instrument) else False for i, spline in enumerate(splines): - parser = SplineParser(size=spline.shape[-1], id="P{}".format(parsing_idxs[i])) + parser = SplineParser(size=spline.shape[-1], id="P{}".format(parsing_idxs[i]) if not p_same_part else "P1") same_part = False if parser.id in [p.id for p in parts]: same_part = True @@ -211,6 +215,8 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: unique_durs = np.unique(parser.total_duration_values).astype(int) divs_pq = np.lcm.reduce(unique_durs) divs_pq = divs_pq if divs_pq > 4 else 4 + # compare divs_pq to the divs_pq of the part + divs_pq = np.lcm.reduce([divs_pq, part._quarter_durations[0]]) part.set_quarter_duration(0, divs_pq) else: has_staff = np.char.startswith(spline, "*staff") @@ -232,7 +238,7 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: continue if isinstance(element, spt.GenericNote): quarter_duration = 4 / parser.total_duration_values[i] - duration_divs = int(quarter_duration*divs_pq) + duration_divs = ceil(quarter_duration*divs_pq) el_end = current_tl_pos + duration_divs part.add(element, start=current_tl_pos, end=el_end) current_tl_pos = el_end @@ -257,18 +263,28 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: for i in range(len(measures) - 1): measures[i].end = measures[i + 1].start measures[-1].end = part.last_point + # find and add pickup measure + if part.measures[0].start.t != 0: + part.add(spt.Measure(number=0), start=0, end=part.measures[0].start.t) if parser.id not in [p.id for p in parts]: parts.append(part) # currate parts to the same divs per quarter - divs_pq = np.lcm.reduce([p._quarter_durations[0] for p in parts]) - for part in parts: - part.set_quarter_duration(0, divs_pq) + # divs_pq = np.lcm.reduce([p._quarter_durations[0] for p in parts]) + # for part in parts: + # part.set_quarter_duration(0, divs_pq) return spt.Score(parts) +def rec_divisible_by_two(number): + if number % 2 == 0: + return rec_divisible_by_two(number // 2) + else: + return number + + class SplineParser(object): def __init__(self, id="P1", staff=1, voice=1, size=1, name=""): self.id = id @@ -276,6 +292,7 @@ def __init__(self, id="P1", staff=1, voice=1, size=1, name=""): self.staff = staff self.voice = voice self.total_duration_values = [] + self.alters = [] self.size = size self.total_parsed_elements = 0 self.tie_prev = None @@ -290,6 +307,8 @@ def parse(self, spline): spline = spline[spline != ""] # Remove None lines spline = spline[spline != None] + # Remove lines that start with "!" + spline = spline[np.char.startswith(spline, "!") == False] # Empty Numpy array with objects elements = np.empty(len(spline), dtype=object) self.total_duration_values = np.ones(len(spline)) @@ -420,6 +439,10 @@ def process_clef_line(self, line): def process_key_signature_line(self, line): fifths = line.count("#") - line.count("-") + alters = re.findall(r"([a-gA-G#\-]+)", line) + alters = "".join(alters) + # split alters by two characters + self.alters = [alters[i:i + 2] for i in range(0, len(alters), 2)] # TODO retrieve the key mode mode = "major" return spt.KeySignature(fifths, mode) @@ -432,15 +455,16 @@ def process_meter_line(self, line): def _process_kern_pitch(self, pitch): # find accidentals - alter = re.search(r"([n#\-]+)", pitch) + alter = re.search(r"([n#-]+)", pitch) # remove alter from pitch pitch = pitch.replace(alter.group(0), "") if alter else pitch step, octave = KERN_NOTES[pitch[0]] + # do_alt = (step + alter.group(0)).lower() not in self.alters if alter else False if octave == 4: octave = octave + pitch.count(pitch[0]) - 1 elif octave == 3: octave = octave - pitch.count(pitch[0]) + 1 - alter = SIGN_TO_ACC[alter.group(0)] if alter else None + alter = SIGN_TO_ACC[alter.group(0)] if alter is not None else None return step, octave, alter def _process_kern_duration(self, duration, is_grace=False): @@ -461,7 +485,7 @@ def _process_kern_duration(self, duration, is_grace=False): symbolic_duration = { "type": KERN_DURS[diff[min(list(diff.keys()))]], - "actual_notes": dur // 4, + "actual_notes": int(dur // 4), "normal_notes": int(diff[min(list(diff.keys()))]) // 4, } symbolic_duration["dots"] = dots From 96215347e7aa4a200e2dbcb4e079944a0859da39 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 15 Jan 2024 15:38:24 +0100 Subject: [PATCH 026/197] Minor correction. --- partitura/io/importkern_v2.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index 7ff029a6..c246de68 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -175,7 +175,7 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: """ try: # This version of the parser is faster but does not support spine splitting. - file = np.loadtxt(kern_path, dtype=str, delimiter="\t", comments="!", encoding="utf-8") + file = np.loadtxt(kern_path, dtype=str, delimiter="\t", comments="!!", encoding="utf-8") parsing_idxs = np.arange(file.shape[0]) # Decide Parts @@ -271,9 +271,9 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: parts.append(part) # currate parts to the same divs per quarter - # divs_pq = np.lcm.reduce([p._quarter_durations[0] for p in parts]) - # for part in parts: - # part.set_quarter_duration(0, divs_pq) + divs_pq = np.lcm.reduce([p._quarter_durations[0] for p in parts]) + for part in parts: + part.set_quarter_duration(0, divs_pq) return spt.Score(parts) @@ -542,7 +542,9 @@ def meta_note_line(self, line, voice=None, add=True): # extract first occurence of one of the following: a-g A-G r # - n pitch = re.search(r"([a-gA-Gr\-n#]+)", line).group(0) # extract duration can be any of the following: 0-9 . - duration = re.search(r"([0-9.]+)", line).group(0) + dur_search = re.search(r"([0-9.]+)", line) + # if no duration is found, then the duration is 8 by default (for grace notes with no duration) + duration = dur_search.group(0) if dur_search else "8" # extract symbol can be any of the following: _()[]{}<>|: symbols = re.findall(r"([_()\[\]{}<>|:])", line) symbolic_duration = self._process_kern_duration(duration, is_grace="q" in line) From fe28a58a645b69cb22065afefb36d9710c8dceb2 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 15 Jan 2024 16:04:46 +0100 Subject: [PATCH 027/197] Add measures from dcml measure tsv. --- partitura/io/importdcml.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index daa5b1a3..ccd10c2d 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -79,8 +79,22 @@ def read_note_tsv(note_tsv_path, metadata=None): return part -def read_measure_tsv(measure_tsv_path): - return +def read_measure_tsv(measure_tsv_path, part): + qdivs = part._quarter_durations[0] + data = pd.read_csv(measure_tsv_path, sep="\t") + data["onset_div"] = np.array([int(qd * qdivs) for qd in data["quarterbeats"].apply(eval)]) + data["duration_div"] = np.array([int(qd * qdivs) for qd in data["duration_qb"]]) + repeat_index = 0 + + for idx, row in data.iterrows(): + part.add(spt.Measure(), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) + # if row["repeat"] == "start": + if row["repeat"] == "start" or row["repeat"] == "startend": + repeat_index = idx + elif row["repeat"] == "": + # Find the previous repeat start + start_times = data[repeat_index]["onset_div"] + part.add(spt.Repeat(), start=start_times, end=row["onset_div"]) def read_harmony_tsv(beat_tsv_path, part): From a651864e87e0c77663f74b3f01f639d80c3fb673 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 15 Jan 2024 16:30:24 +0100 Subject: [PATCH 028/197] Import tsv from dcml minor corrections --- partitura/__init__.py | 2 ++ partitura/io/importdcml.py | 49 ++++++++++++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/partitura/__init__.py b/partitura/__init__.py index c624b578..958de256 100644 --- a/partitura/__init__.py +++ b/partitura/__init__.py @@ -15,6 +15,7 @@ from .io.importmei import load_mei from .io.importkern import load_kern from .io.importmusic21 import load_music21 +from .io.importdcml import load_tsv from .io.importmidi import load_score_midi, load_performance_midi, midi_to_notearray from .io.exportmidi import save_score_midi, save_performance_midi from .io.importmatch import load_match @@ -56,6 +57,7 @@ "save_performance_midi", "load_match", "save_match", + "load_tsv", "load_nakamuramatch", "load_nakamuracorresp", "load_parangonada_csv", diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index ccd10c2d..9f106d7a 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -82,16 +82,16 @@ def read_note_tsv(note_tsv_path, metadata=None): def read_measure_tsv(measure_tsv_path, part): qdivs = part._quarter_durations[0] data = pd.read_csv(measure_tsv_path, sep="\t") - data["onset_div"] = np.array([int(qd * qdivs) for qd in data["quarterbeats"].apply(eval)]) + data["onset_div"] = np.array([int(qd * qdivs) for qd in data["quarterbeats"]]) data["duration_div"] = np.array([int(qd * qdivs) for qd in data["duration_qb"]]) repeat_index = 0 for idx, row in data.iterrows(): part.add(spt.Measure(), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) # if row["repeat"] == "start": - if row["repeat"] == "start" or row["repeat"] == "startend": + if row["repeats"] == "start": repeat_index = idx - elif row["repeat"] == "": + elif row["repeats"] == "": # Find the previous repeat start start_times = data[repeat_index]["onset_div"] part.add(spt.Repeat(), start=start_times, end=row["onset_div"]) @@ -102,18 +102,21 @@ def read_harmony_tsv(beat_tsv_path, part): data = pd.read_csv(beat_tsv_path, sep="\t") data["onset_div"] = np.array([int(qd * qdivs) for qd in data["quarterbeats"].apply(eval)]) data["duration_div"] = np.array([int(qd * qdivs) for qd in data["duration_qb"]]) + is_na_cad = data["cadence"].isna() + is_na_roman = data["chord"].isna() # Find Phrase Starts where data["phraseend"] == "{" - for _, row in data.iterrows(): + for idx, row in data[~is_na_roman].iterrows(): part.add( - spt.RomanNumeral(roman=row["chord"], + spt.RomanNumeral(text=row["chord"], local_key=row["localkey"], quality=row["chord_type"], ), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) - if row["cadence"] is not None: - part.add( - spt.Cadence(cadence_type=row["cadence"], - local_key=row["localkey"], - ), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) + + for idx, row in data[~is_na_cad].iterrows(): + part.add( + spt.Cadence(text=row["cadence"], + local_key=row["localkey"], + ), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) phrase_starts = data[data["phraseend"] == "{"] phrase_ends = data[data["phraseend"] == "}"] @@ -125,6 +128,32 @@ def read_harmony_tsv(beat_tsv_path, part): def load_tsv(note_tsv_path, measure_tsv_path=None, harmony_tsv_path=None, metadata=None): + """ + Load a score from tsv files containing the notes, measures and harmony annotations. + + These files are provided by the DCML datasets. + ATTENTION: This functionality requires pandas to be installed, which is not a requirement for partitura. + + Parameters + ---------- + note_tsv_path: str + Path to the tsv file containing the notes + measure_tsv_path: str + Path to the tsv file containing the measures + harmony_tsv_path: + Path to the tsv file containing the harmony annotations + metadata: dict + Metadata to add to the score. This is useful to add the composer, title, etc. + + Returns + ------- + score: :class:`partitura.score.Score` + A `Score` instance. + + """ + if pd is None: + raise ImportError("This functionality requires pandas to be installed") + part = read_note_tsv(note_tsv_path, metadata=metadata) if measure_tsv_path is not None: read_measure_tsv(measure_tsv_path, part) From 8b582fce3c182e6ca9ca93a47bee7b9f196f06b7 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 15 Jan 2024 16:30:33 +0100 Subject: [PATCH 029/197] Added test for import of tsv files. --- tests/__init__.py | 1 + tests/data/tsv/test_harmonies.tsv | 242 +++++ tests/data/tsv/test_measures.tsv | 155 +++ tests/data/tsv/test_notes.tsv | 1694 +++++++++++++++++++++++++++++ tests/test_dcml_import.py | 17 + 5 files changed, 2109 insertions(+) create mode 100644 tests/data/tsv/test_harmonies.tsv create mode 100644 tests/data/tsv/test_measures.tsv create mode 100644 tests/data/tsv/test_notes.tsv create mode 100644 tests/test_dcml_import.py diff --git a/tests/__init__.py b/tests/__init__.py index 712e6a11..ac76a716 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -19,6 +19,7 @@ PARANGONADA_PATH = os.path.join(DATA_PATH, "parangonada") WAV_PATH = os.path.join(DATA_PATH, "wav") PNG_PATH = os.path.join(DATA_PATH, "png") +TSV_PATH = os.path.join(DATA_PATH, "tsv") # this is a list of files for which importing and subsequent exporting should # yield identical MusicXML diff --git a/tests/data/tsv/test_harmonies.tsv b/tests/data/tsv/test_harmonies.tsv new file mode 100644 index 00000000..ff56539c --- /dev/null +++ b/tests/data/tsv/test_harmonies.tsv @@ -0,0 +1,242 @@ +mc mn quarterbeats quarterbeats_all_endings duration_qb mc_onset mn_onset timesig staff voice label alt_label globalkey localkey pedal chord special numeral form figbass changes relativeroot cadence phraseend chord_type globalkey_is_minor localkey_is_minor chord_tones added_tones root bass_note +1 0 0 0 9.0 0 3/4 2/2 2 1 f.i{ f i i i { m 1 1 0, -3, 1 0 0 +4 3 9 9 8.0 0 0 2/2 2 1 V65 f i V65 V 65 Mm7 1 1 5, 2, -1, 1 1 5 +6 5 17 17 4.0 0 0 2/2 2 1 i f i i i m 1 1 0, -3, 1 0 0 +7 6 21 21 4.0 0 0 2/2 2 1 #viio6 f i #viio6 #vii o 6 o 1 1 2, -1, 5 5 2 +8 7 25 25 2.0 0 0 2/2 2 1 i6 f i i6 i 6 m 1 1 -3, 1, 0 0 -3 +8 7 27 27 2.0 1/2 1/2 2/2 2 1 iio6 f i iio6 ii o 6 o 1 1 -1, -4, 2 2 -1 +9 8 29 29 1.0 0 0 2/2 2 1 V(4)} f i V(4) V 4 } M 1 1 1, 0, 2 1 1 +9 8 30 30 2.0 1/4 1/4 2/2 2 1 V|HC f i V V HC M 1 1 1, 5, 2 1 1 +9 8 32 32 9.0 3/4 3/4 2/2 2 1 v{ f i v v { m 1 1 1, -2, 2 1 1 +12 11 41 41 4.0 0 0 2/2 2 1 III.IVM2 ii7(2) f III IVM2 IV M 2 MM7 1 0 4, -1, 3, 0 -1 4 +13 12 45 45 4.0 0 0 2/2 2 1 ii7 f III ii7 ii 7 mm7 1 0 2, -1, 3, 0 2 2 +14 13 49 49 4.0 0 0 2/2 2 1 V43 f III V43 V 43 Mm7 1 0 2, -1, 1, 5 1 2 +15 14 53 53 4.0 0 0 2/2 2 1 I f III I I M 1 0 0, 4, 1 0 0 +16 15 57 57 1.0 0 0 2/2 2 1 ii6(2) f III ii6(2) ii 6 2 m 1 0 -1, 3, 4 2 -1 +16 15 58 58 1.0 1/4 1/4 2/2 2 1 ii6 f III ii6 ii 6 m 1 0 -1, 3, 2 2 -1 +16 15 59 59 2.0 1/2 1/2 2/2 2 1 V65/V f III V65/V V 65 V Mm7 1 0 6, 3, 0, 2 2 6 +17 16 61 61 3.0 0 0 2/2 2 1 V|HC} f III V V HC } M 1 0 1, 5, 2 1 1 +17 16 64 64 1.0 3/4 3/4 2/2 2 1 I6{ f III I6 I 6 { M 1 0 4, 1, 0 0 4 +18 17 65 65 1.0 0 0 2/2 2 1 ii6(2) f III ii6(2) ii 6 2 m 1 0 -1, 3, 4 2 -1 +18 17 66 66 1.0 1/4 1/4 2/2 2 1 ii6 f III ii6 ii 6 m 1 0 -1, 3, 2 2 -1 +18 17 67 67 2.0 1/2 1/2 2/2 2 1 V65/V f III V65/V V 65 V Mm7 1 0 6, 3, 0, 2 2 6 +19 18 69 69 3.0 0 0 2/2 2 1 V|HC} f III V V HC } M 1 0 1, 5, 2 1 1 +19 18 72 72 1.0 3/4 3/4 2/2 2 1 I6{ f III I6 I 6 { M 1 0 4, 1, 0 0 4 +20 19 73 73 1.0 0 0 2/2 2 1 ii6(2) f III ii6(2) ii 6 2 m 1 0 -1, 3, 4 2 -1 +20 19 74 74 1.0 1/4 1/4 2/2 2 1 ii6 f III ii6 ii 6 m 1 0 -1, 3, 2 2 -1 +20 19 75 75 2.0 1/2 1/2 2/2 2 1 V65/V f III V65/V V 65 V Mm7 1 0 6, 3, 0, 2 2 6 +21 20 77 77 3.0 0 0 2/2 2 1 V[V|HC}{ f III V V V HC }{ M 1 0 1, 5, 2 1 1 +21 20 80 80 1.0 3/4 3/4 2/2 2 1 V7(+b9) f III V V7(+b9) V 7 +b9 Mm7 1 0 1, 5, 2, -1 -4 1 1 +22 21 81 81 4.0 0 0 2/2 2 1 V7 f III V V7 V 7 Mm7 1 0 1, 5, 2, -1 1 1 +23 22 85 85 1.5 0 0 2/2 2 2 V7(b9) f III V V7(b9) V 7 b9 Mm7 1 0 1, 5, 2, -1 -4 1 1 +23 22 173/2 173/2 0.5 3/8 3/8 2/2 2 2 V7 f III V V7 V 7 Mm7 1 0 1, 5, 2, -1 1 1 +23 22 87 87 1.0 1/2 1/2 2/2 2 1 I64 f III V I64 I 64 M 1 0 1, 0, 4 0 1 +23 22 88 88 1.0 3/4 3/4 2/2 2 1 V7(+b9) f III V V7(+b9) V 7 +b9 Mm7 1 0 1, 5, 2, -1 -4 1 1 +24 23 89 89 4.0 0 0 2/2 2 1 V7 f III V V7 V 7 Mm7 1 0 1, 5, 2, -1 1 1 +25 24 93 93 1.5 0 0 2/2 2 1 V7(b9) f III V V7(b9) V 7 b9 Mm7 1 0 1, 5, 2, -1 -4 1 1 +25 24 189/2 189/2 0.5 3/8 3/8 2/2 2 1 V7 f III V V7 V 7 Mm7 1 0 1, 5, 2, -1 1 1 +25 24 95 95 1.0 1/2 1/2 2/2 2 1 I64 f III V I64 I 64 M 1 0 1, 0, 4 0 1 +25 24 96 96 1.0 3/4 3/4 2/2 2 1 V7(+b9) f III V V7(+b9) V 7 +b9 Mm7 1 0 1, 5, 2, -1 -4 1 1 +26 25 97 97 2.0 0 0 2/2 2 1 V7] f III V V7 V 7 Mm7 1 0 1, 5, 2, -1 1 1 +26 25 99 99 2.0 1/2 1/2 2/2 2 1 V2 f III V2 V 2 Mm7 1 0 -1, 1, 5, 2 1 -1 +27 26 101 101 2.0 0 0 2/2 2 1 I6 f III I6 I 6 M 1 0 4, 1, 0 0 4 +27 26 103 103 2.0 1/2 1/2 2/2 2 1 V6 f III V6 V 6 M 1 0 5, 2, 1 1 5 +28 27 105 105 2.0 0 0 2/2 2 1 I f III I I M 1 0 0, 4, 1 0 0 +28 27 107 107 2.0 1/2 1/2 2/2 2 1 V43/V viio6/V f III V43/V V 43 V Mm7 1 0 3, 0, 2, 6 2 3 +29 28 109 109 2.0 0 0 2/2 2 1 V f III V V M 1 0 1, 5, 2 1 1 +29 28 111 111 2.0 1/2 1/2 2/2 2 1 V43/V viio6/V f III V43/V V 43 V Mm7 1 0 3, 0, 2, 6 2 3 +30 29 113 113 2.0 0 0 2/2 2 1 V f III V V M 1 0 1, 5, 2 1 1 +30 29 115 115 2.0 1/2 1/2 2/2 2 1 viio43 f III viio43 vii o 43 o7 1 0 -1, -4, 5, 2 5 -1 +31 30 117 117 2.0 0 0 2/2 2 1 I6 f III I6 I 6 M 1 0 4, 1, 0 0 4 +31 30 119 119 2.0 1/2 1/2 2/2 2 1 viio43 f III viio43 vii o 43 o7 1 0 -1, -4, 5, 2 5 -1 +32 31 121 121 2.0 0 0 2/2 2 1 I6 f III I6 I 6 M 1 0 4, 1, 0 0 4 +32 31 123 123 2.0 1/2 1/2 2/2 2 1 V6 f III V6 V 6 M 1 0 5, 2, 1 1 5 +33 32 125 125 2.0 0 0 2/2 2 1 I f III I I M 1 0 0, 4, 1 0 0 +33 32 127 127 2.0 1/2 1/2 2/2 2 1 V43 f III V43 V 43 Mm7 1 0 2, -1, 1, 5 1 2 +34 33 129 129 4.0 0 0 2/2 2 1 I6 f III I6 I 6 M 1 0 4, 1, 0 0 4 +35 34 133 133 4.0 0 0 2/2 2 1 ii6 f III ii6 ii 6 m 1 0 -1, 3, 2 2 -1 +36 35 137 137 4.0 0 0 2/2 2 1 V(64) f III V(64) V 64 M 1 0 1, 0, 4 1 1 +37 36 141 141 4.0 0 0 2/2 2 1 V2 f III V2 V 2 Mm7 1 0 -1, 1, 5, 2 1 -1 +38 37 145 145 4.0 0 0 2/2 2 1 I6 f III I6 I 6 M 1 0 4, 1, 0 0 4 +39 38 149 149 4.0 0 0 2/2 2 1 ii6 f III ii6 ii 6 m 1 0 -1, 3, 2 2 -1 +40 39 153 153 4.0 0 0 2/2 2 1 V(64) f III V(64) V 64 M 1 0 1, 0, 4 1 1 +41 40 157 157 4.0 0 0 2/2 2 1 V7 f III V7 V 7 Mm7 1 0 1, 5, 2, -1 1 1 +42 41 161 161 2.0 0 0 2/2 2 1 I|PAC} f III I I PAC } M 1 0 0, 4, 1 0 0 +42 41 163 163 2.0 1/2 1/2 2/2 2 1 viio7/V{ f III viio7/V vii o 7 V { o7 1 0 6, 3, 0, -3 6 6 +43 42 165 165 2.0 0 0 2/2 2 1 V(64) f III V(64) V 64 M 1 0 1, 0, 4 1 1 +43 42 167 167 2.0 1/2 1/2 2/2 2 1 V7 f III V7 V 7 Mm7 1 0 1, 5, 2, -1 1 1 +44 43 169 169 2.0 0 0 2/2 2 1 I|PAC f III I I PAC M 1 0 0, 4, 1 0 0 +44 43 171 171 2.0 1/2 1/2 2/2 2 1 viio7/V f III viio7/V vii o 7 V o7 1 0 6, 3, 0, -3 6 6 +45 44 173 173 2.0 0 0 2/2 2 1 V(64) f III V(64) V 64 M 1 0 1, 0, 4 1 1 +45 44 175 175 2.0 1/2 1/2 2/2 2 1 V7 f III V7 V 7 Mm7 1 0 1, 5, 2, -1 1 1 +46 45 177 177 2.0 0 0 2/2 2 1 I|PAC f III I I PAC M 1 0 0, 4, 1 0 0 +46 45 179 179 2.0 1/2 1/2 2/2 2 1 viio7/V f III viio7/V vii o 7 V o7 1 0 6, 3, 0, -3 6 6 +47 46 181 181 2.0 0 0 2/2 2 1 V(64) f III V(64) V 64 M 1 0 1, 0, 4 1 1 +47 46 183 183 2.0 1/2 1/2 2/2 2 1 V7 f III V7 V 7 Mm7 1 0 1, 5, 2, -1 1 1 +48 47 185 185 4.0 0 0 2/2 2 1 I(974)} f III I(974) I 974 } M 1 0 0, -1, 1 2, 5 0 0 +49 48 189 189 12.0 0 0 2/2 2 1 I|PAC f III I I PAC M 1 0 0, 4, 1 0 0 +50 48 192 192 0.0 0 3/4 2/2 2 1 { f III { 1 0 +53 51 201 201 8.0 0 0 2/2 2 1 V65 f III V65 V 65 Mm7 1 0 5, 2, -1, 1 1 5 +55 53 209 209 4.0 0 0 2/2 2 1 iv.viio65/V f iv viio65/V vii o 65 V o7 1 1 3, 0, -3, 6 6 3 +56 54 213 213 4.0 0 0 2/2 2 1 Ger6 f iv Ger6 Ger vii o 65 b3 V Ger 1 1 -4, 0, -3, 6 6 -4 +57 55 217 217 3.0 0 0 2/2 2 1 V[V|HC}{ f iv V V V HC }{ M 1 1 1, 5, 2 1 1 +57 55 220 220 1.0 3/4 3/4 2/2 2 1 V7(+b9) f iv V V7(+b9) V 7 +b9 Mm7 1 1 1, 5, 2, -1 -11 1 1 +58 56 221 221 4.0 0 0 2/2 2 1 V7 f iv V V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +59 57 225 225 1.5 0 0 2/2 2 1 V7(b9) f iv V V7(b9) V 7 b9 Mm7 1 1 1, 5, 2, -1 -11 1 1 +59 57 453/2 453/2 0.5 3/8 3/8 2/2 2 1 V7 f iv V V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +59 57 227 227 1.0 1/2 1/2 2/2 2 1 i64 f iv V i64 i 64 m 1 1 1, 0, -3 0 1 +59 57 228 228 1.0 3/4 3/4 2/2 2 1 V7(+b9) f iv V V7(+b9) V 7 +b9 Mm7 1 1 1, 5, 2, -1 -11 1 1 +60 58 229 229 4.0 0 0 2/2 2 1 V7 f iv V V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +61 59 233 233 1.5 0 0 2/2 2 1 V7(b9) f iv V V7(b9) V 7 b9 Mm7 1 1 1, 5, 2, -1 -11 1 1 +61 59 469/2 469/2 0.5 3/8 3/8 2/2 2 1 V7 f iv V V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +61 59 235 235 1.0 1/2 1/2 2/2 2 1 i64 f iv V i64 i 64 m 1 1 1, 0, -3 0 1 +61 59 236 236 3.0 3/4 3/4 2/2 2 1 V7(+b9)] f iv V V7(+b9) V 7 +b9 Mm7 1 1 1, 5, 2, -1 -11 1 1 +62 60 239 239 2.0 1/2 1/2 2/2 2 1 V2 f iv V2 V 2 Mm7 1 1 -1, 1, 5, 2 1 -1 +63 61 241 241 2.0 0 0 2/2 2 1 i6 f iv i6 i 6 m 1 1 -3, 1, 0 0 -3 +63 61 243 243 2.0 1/2 1/2 2/2 2 1 V64 f iv V64 V 64 M 1 1 2, 1, 5 1 2 +64 62 245 245 2.0 0 0 2/2 2 1 i f iv i i m 1 1 0, -3, 1 0 0 +64 62 247 247 2.0 1/2 1/2 2/2 2 1 v.It6 f v It6 It vii o 6 b3 V It 1 1 -4, 0, 6 6 -4 +65 63 249 249 3.0 0 0 2/2 2 1 V[V|HC}{ f v V V V HC }{ M 1 1 1, 5, 2 1 1 +65 63 252 252 1.0 3/4 3/4 2/2 2 1 V7(+b9) f v V V7(+b9) V 7 +b9 Mm7 1 1 1, 5, 2, -1 -11 1 1 +66 64 253 253 4.0 0 0 2/2 2 1 V7 f v V V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +67 65 257 257 1.5 0 0 2/2 2 1 V7(b9) f v V V7(b9) V 7 b9 Mm7 1 1 1, 5, 2, -1 -11 1 1 +67 65 517/2 517/2 0.5 3/8 3/8 2/2 2 1 V7 f v V V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +67 65 259 259 1.0 1/2 1/2 2/2 2 1 i64 f v V i64 i 64 m 1 1 1, 0, -3 0 1 +67 65 260 260 1.0 3/4 3/4 2/2 2 1 V7(+b9) f v V V7(+b9) V 7 +b9 Mm7 1 1 1, 5, 2, -1 -11 1 1 +68 66 261 261 4.0 0 0 2/2 2 1 V7 f v V V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +69 67 265 265 1.5 0 0 2/2 2 1 V7(b9) f v V V7(b9) V 7 b9 Mm7 1 1 1, 5, 2, -1 -11 1 1 +69 67 533/2 533/2 0.5 3/8 3/8 2/2 2 1 V7 f v V V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +69 67 267 267 1.0 1/2 1/2 2/2 2 1 i64] f v V i64 i 64 m 1 1 1, 0, -3 0 1 +69 67 268 268 1.0 3/4 3/4 2/2 2 1 iio64 f v iio64 ii o 64 o 1 1 -4, 2, -1 2 -4 +70 68 269 269 4.0 0 0 2/2 2 1 V7 f v V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +71 69 273 273 1.5 0 0 2/2 2 1 V7(2) f v V7(2) V 7 2 Mm7 1 1 -4, 5, 2, -1 1 -4 +71 69 549/2 549/2 0.5 3/8 3/8 2/2 2 1 V7 f v V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +71 69 275 275 1.0 1/2 1/2 2/2 2 1 i f v i i m 1 1 0, -3, 1 0 0 +71 69 276 276 1.0 3/4 3/4 2/2 2 1 iv.iio64 f iv iio64 ii o 64 o 1 1 -4, 2, -1 2 -4 +72 70 277 277 4.0 0 0 2/2 2 1 V7 f iv V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +73 71 281 281 1.5 0 0 2/2 2 1 V7(2) f iv V7(2) V 7 2 Mm7 1 1 -4, 5, 2, -1 1 -4 +73 71 565/2 565/2 0.5 3/8 3/8 2/2 2 1 V7 f iv V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +73 71 283 283 1.0 1/2 1/2 2/2 2 1 i f iv i i m 1 1 0, -3, 1 0 0 +73 71 284 284 1.0 3/4 3/4 2/2 2 1 III.iio64 f III iio64 ii o 64 o 1 0 -4, 2, -1 2 -4 +74 72 285 285 4.0 0 0 2/2 2 1 V7 f III V7 V 7 Mm7 1 0 1, 5, 2, -1 1 1 +75 73 289 289 1.0 0 0 2/2 2 1 V7(b2) f III V7(b2) V 7 b2 Mm7 1 0 -4, 5, 2, -1 1 -4 +75 73 290 290 3.0 1/4 1/4 2/2 2 1 V7 f III V7 V 7 Mm7 1 0 1, 5, 2, -1 1 1 +76 74 293 293 1.0 0 0 2/2 2 1 I64 f III I64 I 64 M 1 0 1, 0, 4 0 1 +76 74 294 294 3.0 1/4 1/4 2/2 2 1 I6 f III I6 I 6 M 1 0 4, 1, 0 0 4 +77 75 297 297 1.0 0 0 2/2 2 1 IV(0) f III IV(0) IV 0 M 1 0 -1, 3, 0 -1 -1 +77 75 298 298 3.0 1/4 1/4 2/2 2 1 IV f III IV IV M 1 0 -1, 3, 0 -1 -1 +78 76 301 301 1.0 0 0 2/2 2 1 viio64 f III viio64 vii o 64 o 1 0 -1, 5, 2 5 -1 +78 76 302 302 3.0 1/4 1/4 2/2 2 1 viio6 f III viio6 vii o 6 o 1 0 2, -1, 5 5 2 +79 77 305 305 1.0 0 0 2/2 2 1 iii(0) f III iii(0) iii 0 m 1 0 4, 1, 5 4 4 +79 77 306 306 1.0 1/4 1/4 2/2 2 1 iii f III iii iii m 1 0 4, 1, 5 4 4 +79 77 307 307 2.0 1/2 1/2 2/2 2 1 i.V f i V V M 1 1 1, 5, 2 1 1 +80 78 309 309 1.0 0 0 2/2 2 1 i64 f i i64 i 64 m 1 1 1, 0, -3 0 1 +80 78 310 310 3.0 1/4 1/4 2/2 2 1 i6 f i i6 i 6 m 1 1 -3, 1, 0 0 -3 +81 79 313 313 4.0 0 0 2/2 2 1 iv f i iv iv m 1 1 -1, -4, 0 -1 -1 +82 80 317 317 4.0 0 0 2/2 2 1 viio65/V f i viio65/V vii o 65 V o7 1 1 3, 0, -3, 6 6 3 +83 81 321 321 4.0 0 0 2/2 2 1 V[V|HC}{ f i V V V HC }{ M 1 1 1, 5, 2 1 1 +84 82 325 325 4.0 0 0 2/2 2 1 i64 f i V i64 i 64 m 1 1 1, 0, -3 0 1 +85 83 329 329 4.0 0 0 2/2 2 1 V7 f i V V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +86 84 333 333 2.0 0 0 2/2 2 1 i64 f i V i64 i 64 m 1 1 1, 0, -3 0 1 +86 84 335 335 2.0 1/2 1/2 2/2 2 1 viio/V f i V viio/V vii o V o 1 1 6, 3, 0 6 6 +87 85 337 337 4.0 0 0 2/2 2 1 V f i V V V M 1 1 1, 5, 2 1 1 +88 86 341 341 4.0 0 0 2/2 2 1 i64 f i V i64 i 64 m 1 1 1, 0, -3 0 1 +89 87 345 345 4.0 0 0 2/2 2 1 V7 f i V V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +90 88 349 349 2.0 0 0 2/2 2 1 i64 f i V i64 i 64 m 1 1 1, 0, -3 0 1 +90 88 351 351 2.0 1/2 1/2 2/2 2 1 viio/V f i V viio/V vii o V o 1 1 6, 3, 0 6 6 +91 89 353 353 2.0 0 0 2/2 2 1 V f i V V V M 1 1 1, 5, 2 1 1 +91 89 355 355 2.0 1/2 1/2 2/2 2 1 V7(+b9) f i V V7(+b9) V 7 +b9 Mm7 1 1 1, 5, 2, -1 -11 1 1 +92 90 357 357 2.0 0 0 2/2 2 1 i64 f i V i64 i 64 m 1 1 1, 0, -3 0 1 +92 90 359 359 2.0 1/2 1/2 2/2 2 1 viio/V f i V viio/V vii o V o 1 1 6, 3, 0 6 6 +93 91 361 361 2.0 0 0 2/2 2 1 V f i V V V M 1 1 1, 5, 2 1 1 +93 91 363 363 2.0 1/2 1/2 2/2 2 1 V7(+b9) f i V V7(+b9) V 7 +b9 Mm7 1 1 1, 5, 2, -1 -11 1 1 +94 92 365 365 2.0 0 0 2/2 2 1 i64 f i V i64 i 64 m 1 1 1, 0, -3 0 1 +94 92 367 367 2.0 1/2 1/2 2/2 2 1 viio/V f i V viio/V vii o V o 1 1 6, 3, 0 6 6 +95 93 369 369 8.0 0 0 2/2 2 1 V|HC} f i V V V HC } M 1 1 1, 5, 2 1 1 +97 95 377 377 4.0 0 0 2/2 2 1 bII6(4)]{ f i V bII6(4) bII 6 4 { M 1 1 1, -4, -5 -5 1 +98 96 381 381 4.0 0 0 2/2 2 1 bII6 f i bII6 bII 6 M 1 1 -1, -4, -5 -5 -1 +99 97 385 385 4.0 0 0 2/2 2 1 V2 f i V2 V 2 Mm7 1 1 -1, 1, 5, 2 1 -1 +100 98 389 389 4.0 0 0 2/2 2 1 i6 f i i6 i 6 m 1 1 -3, 1, 0 0 -3 +101 99 393 393 4.0 0 0 2/2 2 1 V2/VII f i V2/VII V 2 VII Mm7 1 1 -3, -1, 3, 0 -1 -3 +102 100 397 397 4.0 0 0 2/2 2 1 #viio6 f i #viio6 #vii o 6 o 1 1 2, -1, 5 5 2 +103 101 401 401 8.0 0 0 2/2 2 1 i|IAC}{ f i i i IAC }{ m 1 1 0, -3, 1 0 0 +105 103 409 409 8.0 0 0 2/2 2 1 V65 f i V65 V 65 Mm7 1 1 5, 2, -1, 1 1 5 +107 105 417 417 4.0 0 0 2/2 2 1 i f i i i m 1 1 0, -3, 1 0 0 +108 106 421 421 4.0 0 0 2/2 2 1 #viio6 f i #viio6 #vii o 6 o 1 1 2, -1, 5 5 2 +109 107 425 425 2.0 0 0 2/2 2 1 i6 f i i6 i 6 m 1 1 -3, 1, 0 0 -3 +109 107 427 427 2.0 1/2 1/2 2/2 2 1 iio6 f i iio6 ii o 6 o 1 1 -1, -4, 2 2 -1 +110 108 429 429 1.0 0 0 2/2 2 1 V(4)} f i V(4) V 4 } M 1 1 1, 0, 2 1 1 +110 108 430 430 2.0 1/4 1/4 2/2 2 1 V|HC f i V V HC M 1 1 1, 5, 2 1 1 +110 108 432 432 9.0 3/4 3/4 2/2 2 1 i{ f i i i { m 1 1 0, -3, 1 0 0 +113 111 441 441 4.0 0 0 2/2 2 1 iio6(4)/iv bIIM2 f i iio6(4)/iv ii o 6 4 iv o 1 1 0, -5, 1 1 0 +114 112 445 445 4.0 0 0 2/2 2 1 iio6/iv f i iio6/iv ii o 6 iv o 1 1 -2, -5, 1 1 -2 +115 113 449 449 2.0 0 0 2/2 2 1 V2(b2)/iv f i V2(b2)/iv V 2 b2 iv Mm7 1 1 -2, -12, 4, 1 0 -2 +115 113 451 451 2.0 1/2 1/2 2/2 2 1 V2/iv f i V2/iv V 2 iv Mm7 1 1 -2, 0, 4, 1 0 -2 +116 114 453 453 2.0 0 0 2/2 2 1 iv6(2) f i iv6(2) iv 6 2 m 1 1 -4, 0, 1 -1 -4 +116 114 455 455 2.0 1/2 1/2 2/2 2 1 iv6 f i iv6 iv 6 m 1 1 -4, 0, -1 -1 -4 +117 115 457 457 4.0 0 0 2/2 2 1 viio6/V f i viio6/V vii o 6 V o 1 1 3, 0, 6 6 3 +118 116 461 461 4.0 0 0 2/2 2 1 viio65/V f i viio65/V vii o 65 V o7 1 1 3, 0, -3, 6 6 3 +119 117 465 465 4.0 0 0 2/2 2 1 V|HC} f i V V HC } M 1 1 1, 5, 2 1 1 +120 118 469 469 1.0 0 0 2/2 2 1 V(64) f i V(64) V 64 M 1 1 1, 0, -3 1 1 +120 118 470 470 1.0 1/4 1/4 2/2 2 1 V7 f i V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +120 118 471 471 1.0 1/2 1/2 2/2 2 1 VI f i VI VI M 1 1 -4, 0, -3 -4 -4 +120 118 472 472 1.0 3/4 3/4 2/2 2 1 It6 f i It6 It vii o 6 b3 V It 1 1 -4, 0, 6 6 -4 +121 119 473 473 3.0 0 0 2/2 2 1 V[V|HC{ f i V V V HC { M 1 1 1, 5, 2 1 1 +121 119 476 476 1.0 3/4 3/4 2/2 2 1 V7(+b9) f i V V7(+b9) V 7 +b9 Mm7 1 1 1, 5, 2, -1 -11 1 1 +122 120 477 477 4.0 0 0 2/2 2 1 V7 f i V V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +123 121 481 481 1.5 0 0 2/2 2 2 V7(b9) f i V V7(b9) V 7 b9 Mm7 1 1 1, 5, 2, -1 -11 1 1 +123 121 965/2 965/2 0.5 3/8 3/8 2/2 2 2 V7 f i V V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +123 121 483 483 1.0 1/2 1/2 2/2 2 1 i64 f i V i64 i 64 m 1 1 1, 0, -3 0 1 +123 121 484 484 1.0 3/4 3/4 2/2 2 1 V7(+b9) f i V V7(+b9) V 7 +b9 Mm7 1 1 1, 5, 2, -1 -11 1 1 +124 122 485 485 4.0 0 0 2/2 2 2 V7 f i V V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +125 123 489 489 1.5 0 0 2/2 2 2 V7(b9) f i V V7(b9) V 7 b9 Mm7 1 1 1, 5, 2, -1 -11 1 1 +125 123 981/2 981/2 0.5 3/8 3/8 2/2 2 2 V7 f i V V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +125 123 491 491 1.0 1/2 1/2 2/2 2 1 i64 f i V i64 i 64 m 1 1 1, 0, -3 0 1 +125 123 492 492 3.0 3/4 3/4 2/2 2 1 V7(+b9)] f i V V7(+b9) V 7 +b9 Mm7 1 1 1, 5, 2, -1 -11 1 1 +126 124 495 495 2.0 1/2 1/2 2/2 2 1 V2 f i V2 V 2 Mm7 1 1 -1, 1, 5, 2 1 -1 +127 125 497 497 2.0 0 0 2/2 2 1 i6 f i i6 i 6 m 1 1 -3, 1, 0 0 -3 +127 125 499 499 2.0 1/2 1/2 2/2 2 1 #viio6 f i #viio6 #vii o 6 o 1 1 2, -1, 5 5 2 +128 126 501 501 2.0 0 0 2/2 2 1 i f i i i m 1 1 0, -3, 1 0 0 +128 126 503 503 2.0 1/2 1/2 2/2 2 1 It6 f i It6 It vii o 6 b3 V It 1 1 -4, 0, 6 6 -4 +129 127 505 505 2.0 0 0 2/2 2 1 V f i V V M 1 1 1, 5, 2 1 1 +129 127 507 507 2.0 1/2 1/2 2/2 2 1 It6 f i It6 It vii o 6 b3 V It 1 1 -4, 0, 6 6 -4 +130 128 509 509 2.0 0 0 2/2 2 1 V f i V V M 1 1 1, 5, 2 1 1 +130 128 511 511 2.0 1/2 1/2 2/2 2 1 #viio43 f i #viio43 #vii o 43 o7 1 1 -1, -4, 5, 2 5 -1 +131 129 513 513 2.0 0 0 2/2 2 1 i6 f i i6 i 6 m 1 1 -3, 1, 0 0 -3 +131 129 515 515 2.0 1/2 1/2 2/2 2 1 #viio43 f i #viio43 #vii o 43 o7 1 1 -1, -4, 5, 2 5 -1 +132 130 517 517 2.0 0 0 2/2 2 1 i6 f i i6 i 6 m 1 1 -3, 1, 0 0 -3 +132 130 519 519 2.0 1/2 1/2 2/2 2 1 V6 f i V6 V 6 M 1 1 5, 2, 1 1 5 +133 131 521 521 2.0 0 0 2/2 2 1 i f i i i m 1 1 0, -3, 1 0 0 +133 131 523 523 2.0 1/2 1/2 2/2 2 1 V43 f i V43 V 43 Mm7 1 1 2, -1, 1, 5 1 2 +134 132 525 525 4.0 0 0 2/2 2 1 i6 f i i6 i 6 m 1 1 -3, 1, 0 0 -3 +135 133 529 529 4.0 0 0 2/2 2 1 iio6 f i iio6 ii o 6 o 1 1 -1, -4, 2 2 -1 +136 134 533 533 4.0 0 0 2/2 2 1 V(64) f i V(64) V 64 M 1 1 1, 0, -3 1 1 +137 135 537 537 4.0 0 0 2/2 2 1 V2 f i V2 V 2 Mm7 1 1 -1, 1, 5, 2 1 -1 +138 136 541 541 4.0 0 0 2/2 2 1 i6 f i i6 i 6 m 1 1 -3, 1, 0 0 -3 +139 137 545 545 4.0 0 0 2/2 2 1 iio6 f i iio6 ii o 6 o 1 1 -1, -4, 2 2 -1 +140 138 549 549 4.0 0 0 2/2 2 1 V(64) f i V(64) V 64 M 1 1 1, 0, -3 1 1 +141 139 553 553 4.0 0 0 2/2 2 1 V f i V V M 1 1 1, 5, 2 1 1 +142 140 557 557 2.0 0 0 2/2 2 1 i|PAC} f i i i PAC } m 1 1 0, -3, 1 0 0 +142 140 559 559 2.0 1/2 1/2 2/2 2 1 Ger6{ f i Ger6 Ger vii o 65 b3 V { Ger 1 1 -4, 0, -3, 6 6 -4 +143 141 561 561 2.0 0 0 2/2 2 1 V(64) f i V(64) V 64 M 1 1 1, 0, -3 1 1 +143 141 563 563 2.0 1/2 1/2 2/2 2 1 V7 f i V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +144 142 565 565 2.0 0 0 2/2 2 1 i|PAC f i i i PAC m 1 1 0, -3, 1 0 0 +144 142 567 567 2.0 1/2 1/2 2/2 2 1 Ger6 f i Ger6 Ger vii o 65 b3 V Ger 1 1 -4, 0, -3, 6 6 -4 +145 143 569 569 2.0 0 0 2/2 2 1 V(64) f i V(64) V 64 M 1 1 1, 0, -3 1 1 +145 143 571 571 2.0 1/2 1/2 2/2 2 1 V7 f i V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +146 144 573 573 2.0 0 0 2/2 2 1 i|PAC f i i i PAC m 1 1 0, -3, 1 0 0 +146 144 575 575 2.0 1/2 1/2 2/2 2 1 Ger6 f i Ger6 Ger vii o 65 b3 V Ger 1 1 -4, 0, -3, 6 6 -4 +147 145 577 577 2.0 0 0 2/2 2 1 V(64) f i V(64) V 64 M 1 1 1, 0, -3 1 1 +147 145 579 579 2.0 1/2 1/2 2/2 2 1 V2 f i V2 V 2 Mm7 1 1 -1, 1, 5, 2 1 -1 +148 146 581 581 4.0 0 0 2/2 2 1 V65/iv f i V65/iv V 65 iv Mm7 1 1 4, 1, -2, 0 0 4 +149 147 585 585 4.0 0 0 2/2 2 1 iv f i iv iv m 1 1 -1, -4, 0 -1 -1 +150 148 589 589 4.0 0 0 2/2 2 1 V65/III f i V65/III V 65 III Mm7 1 1 2, -1, -4, -2 -2 2 +151 149 593 593 3.0 0 0 2/2 2 1 III f i III III M 1 1 -3, 1, -2 -3 -3 +151 149 596 596 1.0 3/4 3/4 2/2 2 1 VIM65 f i VIM65 VI M 65 MM7 1 1 0, -3, 1, -4 -4 0 +152 150 597 597 1.0 0 0 2/2 2 1 iio f i iio ii o o 1 1 2, -1, -4 2 2 +152 150 598 598 1.0 1/4 1/4 2/2 2 1 V65 f i V65 V 65 Mm7 1 1 5, 2, -1, 1 1 5 +152 150 599 599 1.0 1/2 1/2 2/2 2 1 i f i i i m 1 1 0, -3, 1 0 0 +152 150 600 600 1.0 3/4 3/4 2/2 2 1 VI f i VI VI M 1 1 -4, 0, -3 -4 -4 +153 151 601 601 2.0 0 0 2/2 2 1 ii%65 f i ii%65 ii % 65 %7 1 1 -1, -4, 0, 2 2 -1 +153 151 603 603 2.0 1/2 1/2 2/2 2 1 V7 f i V7 V 7 Mm7 1 1 1, 5, 2, -1 1 1 +154 152 605 605 3.0 0 0 2/2 2 1 i|PAC} f i i i PAC } m 1 1 0, -3, 1 0 0 diff --git a/tests/data/tsv/test_measures.tsv b/tests/data/tsv/test_measures.tsv new file mode 100644 index 00000000..2d70eb74 --- /dev/null +++ b/tests/data/tsv/test_measures.tsv @@ -0,0 +1,155 @@ +mc mn quarterbeats duration_qb keysig timesig act_dur mc_offset numbering_offset dont_count barline breaks repeats next +1 0 0 1.0 -4 2/2 1/4 3/4 1 firstMeasure 2 +2 1 1 4.0 -4 2/2 1 0 3 +3 2 5 4.0 -4 2/2 1 0 4 +4 3 9 4.0 -4 2/2 1 0 5 +5 4 13 4.0 -4 2/2 1 0 6 +6 5 17 4.0 -4 2/2 1 0 7 +7 6 21 4.0 -4 2/2 1 0 8 +8 7 25 4.0 -4 2/2 1 0 9 +9 8 29 4.0 -4 2/2 1 0 10 +10 9 33 4.0 -4 2/2 1 0 11 +11 10 37 4.0 -4 2/2 1 0 12 +12 11 41 4.0 -4 2/2 1 0 13 +13 12 45 4.0 -4 2/2 1 0 14 +14 13 49 4.0 -4 2/2 1 0 15 +15 14 53 4.0 -4 2/2 1 0 16 +16 15 57 4.0 -4 2/2 1 0 17 +17 16 61 4.0 -4 2/2 1 0 18 +18 17 65 4.0 -4 2/2 1 0 19 +19 18 69 4.0 -4 2/2 1 0 20 +20 19 73 4.0 -4 2/2 1 0 21 +21 20 77 4.0 -4 2/2 1 0 22 +22 21 81 4.0 -4 2/2 1 0 23 +23 22 85 4.0 -4 2/2 1 0 24 +24 23 89 4.0 -4 2/2 1 0 25 +25 24 93 4.0 -4 2/2 1 0 26 +26 25 97 4.0 -4 2/2 1 0 27 +27 26 101 4.0 -4 2/2 1 0 28 +28 27 105 4.0 -4 2/2 1 0 29 +29 28 109 4.0 -4 2/2 1 0 30 +30 29 113 4.0 -4 2/2 1 0 31 +31 30 117 4.0 -4 2/2 1 0 32 +32 31 121 4.0 -4 2/2 1 0 33 +33 32 125 4.0 -4 2/2 1 0 34 +34 33 129 4.0 -4 2/2 1 0 35 +35 34 133 4.0 -4 2/2 1 0 36 +36 35 137 4.0 -4 2/2 1 0 37 +37 36 141 4.0 -4 2/2 1 0 38 +38 37 145 4.0 -4 2/2 1 0 39 +39 38 149 4.0 -4 2/2 1 0 40 +40 39 153 4.0 -4 2/2 1 0 41 +41 40 157 4.0 -4 2/2 1 0 42 +42 41 161 4.0 -4 2/2 1 0 43 +43 42 165 4.0 -4 2/2 1 0 44 +44 43 169 4.0 -4 2/2 1 0 45 +45 44 173 4.0 -4 2/2 1 0 46 +46 45 177 4.0 -4 2/2 1 0 47 +47 46 181 4.0 -4 2/2 1 0 48 +48 47 185 4.0 -4 2/2 1 0 49 +49 48 189 3.0 -4 2/2 3/4 0 line end 1, 50 +50 48 192 1.0 -4 2/2 1/4 3/4 1 start-repeat start 51 +51 49 193 4.0 -4 2/2 1 0 52 +52 50 197 4.0 -4 2/2 1 0 53 +53 51 201 4.0 -4 2/2 1 0 54 +54 52 205 4.0 -4 2/2 1 0 55 +55 53 209 4.0 -4 2/2 1 0 56 +56 54 213 4.0 -4 2/2 1 0 57 +57 55 217 4.0 -4 2/2 1 0 58 +58 56 221 4.0 -4 2/2 1 0 59 +59 57 225 4.0 -4 2/2 1 0 60 +60 58 229 4.0 -4 2/2 1 0 61 +61 59 233 4.0 -4 2/2 1 0 62 +62 60 237 4.0 -4 2/2 1 0 63 +63 61 241 4.0 -4 2/2 1 0 64 +64 62 245 4.0 -4 2/2 1 0 65 +65 63 249 4.0 -4 2/2 1 0 66 +66 64 253 4.0 -4 2/2 1 0 67 +67 65 257 4.0 -4 2/2 1 0 68 +68 66 261 4.0 -4 2/2 1 0 69 +69 67 265 4.0 -4 2/2 1 0 70 +70 68 269 4.0 -4 2/2 1 0 71 +71 69 273 4.0 -4 2/2 1 0 72 +72 70 277 4.0 -4 2/2 1 0 73 +73 71 281 4.0 -4 2/2 1 0 74 +74 72 285 4.0 -4 2/2 1 0 75 +75 73 289 4.0 -4 2/2 1 0 76 +76 74 293 4.0 -4 2/2 1 0 77 +77 75 297 4.0 -4 2/2 1 0 78 +78 76 301 4.0 -4 2/2 1 0 79 +79 77 305 4.0 -4 2/2 1 0 80 +80 78 309 4.0 -4 2/2 1 0 81 +81 79 313 4.0 -4 2/2 1 0 82 +82 80 317 4.0 -4 2/2 1 0 83 +83 81 321 4.0 -4 2/2 1 0 84 +84 82 325 4.0 -4 2/2 1 0 85 +85 83 329 4.0 -4 2/2 1 0 86 +86 84 333 4.0 -4 2/2 1 0 87 +87 85 337 4.0 -4 2/2 1 0 88 +88 86 341 4.0 -4 2/2 1 0 89 +89 87 345 4.0 -4 2/2 1 0 90 +90 88 349 4.0 -4 2/2 1 0 91 +91 89 353 4.0 -4 2/2 1 0 92 +92 90 357 4.0 -4 2/2 1 0 93 +93 91 361 4.0 -4 2/2 1 0 94 +94 92 365 4.0 -4 2/2 1 0 95 +95 93 369 4.0 -4 2/2 1 0 96 +96 94 373 4.0 -4 2/2 1 0 97 +97 95 377 4.0 -4 2/2 1 0 98 +98 96 381 4.0 -4 2/2 1 0 99 +99 97 385 4.0 -4 2/2 1 0 100 +100 98 389 4.0 -4 2/2 1 0 101 +101 99 393 4.0 -4 2/2 1 0 102 +102 100 397 4.0 -4 2/2 1 0 103 +103 101 401 4.0 -4 2/2 1 0 104 +104 102 405 4.0 -4 2/2 1 0 105 +105 103 409 4.0 -4 2/2 1 0 106 +106 104 413 4.0 -4 2/2 1 0 107 +107 105 417 4.0 -4 2/2 1 0 108 +108 106 421 4.0 -4 2/2 1 0 109 +109 107 425 4.0 -4 2/2 1 0 110 +110 108 429 4.0 -4 2/2 1 0 111 +111 109 433 4.0 -4 2/2 1 0 112 +112 110 437 4.0 -4 2/2 1 0 113 +113 111 441 4.0 -4 2/2 1 0 114 +114 112 445 4.0 -4 2/2 1 0 115 +115 113 449 4.0 -4 2/2 1 0 116 +116 114 453 4.0 -4 2/2 1 0 117 +117 115 457 4.0 -4 2/2 1 0 118 +118 116 461 4.0 -4 2/2 1 0 119 +119 117 465 4.0 -4 2/2 1 0 120 +120 118 469 4.0 -4 2/2 1 0 121 +121 119 473 4.0 -4 2/2 1 0 122 +122 120 477 4.0 -4 2/2 1 0 123 +123 121 481 4.0 -4 2/2 1 0 124 +124 122 485 4.0 -4 2/2 1 0 125 +125 123 489 4.0 -4 2/2 1 0 126 +126 124 493 4.0 -4 2/2 1 0 127 +127 125 497 4.0 -4 2/2 1 0 128 +128 126 501 4.0 -4 2/2 1 0 129 +129 127 505 4.0 -4 2/2 1 0 130 +130 128 509 4.0 -4 2/2 1 0 131 +131 129 513 4.0 -4 2/2 1 0 132 +132 130 517 4.0 -4 2/2 1 0 133 +133 131 521 4.0 -4 2/2 1 0 134 +134 132 525 4.0 -4 2/2 1 0 135 +135 133 529 4.0 -4 2/2 1 0 136 +136 134 533 4.0 -4 2/2 1 0 137 +137 135 537 4.0 -4 2/2 1 0 138 +138 136 541 4.0 -4 2/2 1 0 139 +139 137 545 4.0 -4 2/2 1 0 140 +140 138 549 4.0 -4 2/2 1 0 141 +141 139 553 4.0 -4 2/2 1 0 142 +142 140 557 4.0 -4 2/2 1 0 143 +143 141 561 4.0 -4 2/2 1 0 144 +144 142 565 4.0 -4 2/2 1 0 145 +145 143 569 4.0 -4 2/2 1 0 146 +146 144 573 4.0 -4 2/2 1 0 147 +147 145 577 4.0 -4 2/2 1 0 148 +148 146 581 4.0 -4 2/2 1 0 149 +149 147 585 4.0 -4 2/2 1 0 150 +150 148 589 4.0 -4 2/2 1 0 151 +151 149 593 4.0 -4 2/2 1 0 line 152 +152 150 597 4.0 -4 2/2 1 0 153 +153 151 601 4.0 -4 2/2 1 0 154 +154 152 605 3.0 -4 2/2 3/4 0 end 50, -1 diff --git a/tests/data/tsv/test_notes.tsv b/tests/data/tsv/test_notes.tsv new file mode 100644 index 00000000..089174c6 --- /dev/null +++ b/tests/data/tsv/test_notes.tsv @@ -0,0 +1,1694 @@ +mc mn quarterbeats quarterbeats_all_endings duration_qb mc_onset mn_onset timesig staff voice duration gracenote nominal_duration scalar tied tpc midi name octave chord_id +1 0 0 0 1.0 0 3/4 2/2 1 1 1/4 1/4 1 0 60 C4 4 0 +2 1 1 1 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 65 F4 4 1 +2 1 2 2 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 2 +2 1 3 3 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 0 72 C5 5 3 +2 1 4 4 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -1 77 F5 5 4 +3 2 5 5 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -4 80 Ab5 5 5 +3 2 6 6 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 10 +3 2 6 6 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 10 +3 2 6 6 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 10 +3 2 13/2 13/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 1 79 G5 5 6 +3 2 20/3 20/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 -1 77 F5 5 7 +3 2 41/6 41/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 4 76 E5 5 8 +3 2 7 7 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -1 53 F3 3 11 +3 2 7 7 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 11 +3 2 7 7 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 11 +3 2 7 7 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -1 77 F5 5 9 +3 2 8 8 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 12 +3 2 8 8 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 12 +3 2 8 8 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 12 +4 3 9 9 1.0 0 0 2/2 2 1 1/4 1/4 1 4 52 E3 3 17 +4 3 9 9 1.0 0 0 2/2 2 1 1/4 1/4 1 1 55 G3 3 17 +4 3 9 9 1.0 0 0 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 17 +4 3 9 9 1.0 0 0 2/2 2 1 1/4 1/4 1 0 60 C4 4 17 +4 3 9 9 1.0 0 0 2/2 1 1 1/4 1/4 1 1 67 G4 4 13 +4 3 10 10 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 0 72 C5 5 14 +4 3 11 11 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 4 76 E5 5 15 +4 3 12 12 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 1 79 G5 5 16 +5 4 13 13 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -2 82 Bb5 5 18 +5 4 14 14 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 4 52 E3 3 23 +5 4 14 14 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 23 +5 4 14 14 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 23 +5 4 14 14 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 23 +5 4 29/2 29/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 -4 80 Ab5 5 19 +5 4 44/3 44/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 1 79 G5 5 20 +5 4 89/6 89/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 -1 77 F5 5 21 +5 4 15 15 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 4 52 E3 3 24 +5 4 15 15 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 1 55 G3 3 24 +5 4 15 15 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 24 +5 4 15 15 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 24 +5 4 15 15 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 1 79 G5 5 22 +5 4 16 16 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 4 52 E3 3 25 +5 4 16 16 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 25 +5 4 16 16 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 25 +5 4 16 16 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 25 +6 5 17 17 0.0 0 0 2/2 1 1 0 acciaccatura 1/8 1 0 72 C5 5 26 +6 5 17 17 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -4 80 Ab5 5 27 +6 5 18 18 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 32 +6 5 18 18 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 32 +6 5 18 18 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 32 +6 5 37/2 37/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 1 79 G5 5 28 +6 5 56/3 56/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 -1 77 F5 5 29 +6 5 113/6 113/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 4 76 E5 5 30 +6 5 19 19 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -1 53 F3 3 33 +6 5 19 19 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 33 +6 5 19 19 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 33 +6 5 19 19 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -1 77 F5 5 31 +6 5 20 20 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 34 +6 5 20 20 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 34 +6 5 20 20 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 34 +7 6 21 21 0.0 0 0 2/2 1 1 0 acciaccatura 1/8 1 0 72 C5 5 35 +7 6 21 21 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -2 82 Bb5 5 36 +7 6 22 22 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 41 +7 6 22 22 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 41 +7 6 22 22 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 4 64 E4 4 41 +7 6 45/2 45/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 -4 80 Ab5 5 37 +7 6 68/3 68/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 1 79 G5 5 38 +7 6 137/6 137/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 -1 77 F5 5 39 +7 6 23 23 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 1 55 G3 3 42 +7 6 23 23 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 42 +7 6 23 23 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 4 64 E4 4 42 +7 6 23 23 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 1 79 G5 5 40 +7 6 24 24 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 43 +7 6 24 24 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 43 +7 6 24 24 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 4 64 E4 4 43 +8 7 25 25 2.0 0 0 2/2 1 1 1/2 1/2 1 0 72 C5 5 44 +8 7 25 25 2.0 0 0 2/2 1 1 1/2 1/2 1 -1 77 F5 5 44 +8 7 25 25 2.0 0 0 2/2 1 1 1/2 1/2 1 -4 80 Ab5 5 44 +8 7 25 25 2.0 0 0 2/2 1 1 1/2 1/2 1 0 84 C6 6 44 +8 7 26 26 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 49 +8 7 26 26 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 49 +8 7 26 26 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -1 65 F4 4 49 +8 7 27 27 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 -2 82 Bb5 5 45 +8 7 55/2 55/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 -4 80 Ab5 5 46 +8 7 28 28 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 50 +8 7 28 28 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 50 +8 7 28 28 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 67 G4 4 50 +8 7 28 28 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 1 79 G5 5 47 +8 7 57/2 57/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -1 77 F5 5 48 +9 8 29 29 0.0 0 0 2/2 1 1 0 grace16 1/16 1 4 76 E5 5 51 +9 8 29 29 0.0 0 0 2/2 1 1 0 grace16 1/16 1 -1 77 F5 5 52 +9 8 29 29 0.0 0 0 2/2 1 1 0 grace16 1/16 1 1 79 G5 5 53 +9 8 29 29 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 77 F5 5 54 +9 8 30 30 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 56 +9 8 30 30 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 1 67 G4 4 56 +9 8 30 30 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 4 76 E5 5 55 +9 8 32 32 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 43 G2 2 57 +10 9 33 33 1.0 0 0 2/2 2 1 1/4 1/4 1 0 48 C3 3 58 +10 9 34 34 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -3 51 Eb3 3 59 +10 9 35 35 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 1 55 G3 3 60 +10 9 36 36 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 61 +11 10 37 37 1.5 0 0 2/2 2 1 3/8 1/4 3/2 -3 63 Eb4 4 62 +11 10 77/2 77/2 0.16666666666666666 3/8 3/8 2/2 2 1 1/24 1/16 2/3 2 62 D4 4 63 +11 10 116/3 116/3 0.16666666666666666 5/12 5/12 2/2 2 1 1/24 1/16 2/3 0 60 C4 4 64 +11 10 233/6 233/6 0.16666666666666666 11/24 11/24 2/2 2 1 1/24 1/16 2/3 5 59 B3 3 65 +11 10 39 39 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 66 +11 10 40 40 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 67 +12 11 41 41 4.0 0 0 2/2 2 1 1 1 1 0 60 C4 4 74 +12 11 41 41 4.0 0 0 2/2 2 1 1 1 1 -1 65 F4 4 74 +12 11 41 41 4.0 0 0 2/2 1 2 1 1 1 1 -4 68 Ab4 4 73 +12 11 85/2 85/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 -3 75 Eb5 5 68 +12 11 128/3 128/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 -5 73 Db5 5 69 +12 11 257/6 257/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 0 72 C5 5 70 +12 11 43 43 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -5 73 Db5 5 71 +12 11 44 44 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -5 73 Db5 5 72 +13 12 45 45 4.0 0 0 2/2 2 1 1 1 1 -2 58 Bb3 3 82 +13 12 45 45 4.0 0 0 2/2 2 1 1 1 1 -1 65 F4 4 82 +13 12 45 45 1.5 0 0 2/2 1 2 3/8 1/4 3/2 -1 -4 68 Ab4 4 76 +13 12 45 45 4.0 0 0 2/2 1 1 1 1 1 1 -5 73 Db5 5 75 +13 12 93/2 93/2 0.16666666666666666 3/8 3/8 2/2 1 2 1/24 1/16 2/3 -2 70 Bb4 4 77 +13 12 140/3 140/3 0.16666666666666666 5/12 5/12 2/2 1 2 1/24 1/16 2/3 -4 68 Ab4 4 78 +13 12 281/6 281/6 0.16666666666666666 11/24 11/24 2/2 1 2 1/24 1/16 2/3 1 67 G4 4 79 +13 12 47 47 1.0 1/2 1/2 2/2 1 2 1/4 1/4 1 -4 68 Ab4 4 80 +13 12 48 48 1.0 3/4 3/4 2/2 1 2 1/4 1/4 1 -4 68 Ab4 4 81 +14 13 49 49 4.0 0 0 2/2 2 1 1 1 1 -2 58 Bb3 3 90 +14 13 49 49 4.0 0 0 2/2 2 1 1 1 1 -3 63 Eb4 4 90 +14 13 49 49 4.0 0 0 2/2 1 2 1 1 1 1 67 G4 4 89 +14 13 49 49 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -1 -5 73 Db5 5 83 +14 13 101/2 101/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 -3 75 Eb5 5 84 +14 13 152/3 152/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 -5 73 Db5 5 85 +14 13 305/6 305/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 0 72 C5 5 86 +14 13 51 51 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -5 73 Db5 5 87 +14 13 52 52 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -5 73 Db5 5 88 +15 14 53 53 4.0 0 0 2/2 2 1 1 1 1 -4 56 Ab3 3 97 +15 14 53 53 4.0 0 0 2/2 2 1 1 1 1 -3 63 Eb4 4 97 +15 14 53 53 4.0 0 0 2/2 1 2 1 1 1 -4 68 Ab4 4 96 +15 14 109/2 109/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 -5 73 Db5 5 91 +15 14 164/3 164/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 0 72 C5 5 92 +15 14 329/6 329/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 5 71 B4 4 93 +15 14 55 55 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 0 72 C5 5 94 +15 14 56 56 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 0 72 C5 5 95 +16 15 57 57 2.0 0 0 2/2 2 1 1/2 1/2 1 -5 61 Db4 4 102 +16 15 57 57 4.0 0 0 2/2 1 2 1 1 1 -1 65 F4 4 101 +16 15 57 57 1.0 0 0 2/2 1 1 1/4 1/4 1 0 72 C5 5 98 +16 15 58 58 2.0 1/4 1/4 2/2 1 1 1/2 1/2 1 -2 70 Bb4 4 99 +16 15 59 59 2.0 1/2 1/2 2/2 2 1 1/2 1/2 1 2 62 D4 4 103 +16 15 60 60 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 100 +17 16 61 61 1.0 0 0 2/2 2 1 1/4 1/4 1 -3 63 Eb4 4 108 +17 16 61 61 1.0 0 0 2/2 1 1 1/4 1/4 1 1 67 G4 4 104 +17 16 62 62 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -3 75 Eb5 5 105 +17 16 63 63 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -5 73 Db5 5 106 +17 16 64 64 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 109 +17 16 64 64 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -3 63 Eb4 4 109 +17 16 64 64 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 1 0 72 C5 5 107 +18 17 65 65 2.0 0 0 2/2 2 1 1/2 1/2 1 -5 61 Db4 4 113 +18 17 65 65 2.0 0 0 2/2 2 1 1/2 1/2 1 -1 65 F4 4 113 +18 17 65 65 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 0 72 C5 5 110 +18 17 66 66 2.0 1/4 1/4 2/2 1 1 1/2 1/2 1 -2 70 Bb4 4 111 +18 17 67 67 2.0 1/2 1/2 2/2 2 1 1/2 1/2 1 2 62 D4 4 114 +18 17 67 67 2.0 1/2 1/2 2/2 2 1 1/2 1/2 1 -1 65 F4 4 114 +18 17 68 68 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 112 +19 18 69 69 1.0 0 0 2/2 2 1 1/4 1/4 1 -3 63 Eb4 4 119 +19 18 69 69 1.0 0 0 2/2 1 1 1/4 1/4 1 1 67 G4 4 115 +19 18 70 70 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -3 63 Eb4 4 116 +19 18 70 70 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -3 75 Eb5 5 116 +19 18 71 71 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -5 61 Db4 4 117 +19 18 71 71 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -5 73 Db5 5 117 +19 18 72 72 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 48 C3 3 120 +19 18 72 72 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -3 51 Eb3 3 120 +19 18 72 72 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 1 0 60 C4 4 118 +19 18 72 72 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 1 0 72 C5 5 118 +20 19 73 73 2.0 0 0 2/2 2 1 1/2 1/2 1 -5 49 Db3 3 124 +20 19 73 73 2.0 0 0 2/2 2 1 1/2 1/2 1 -1 53 F3 3 124 +20 19 73 73 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 0 60 C4 4 121 +20 19 73 73 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 0 72 C5 5 121 +20 19 74 74 2.0 1/4 1/4 2/2 1 1 1/2 1/2 1 -2 58 Bb3 3 122 +20 19 74 74 2.0 1/4 1/4 2/2 1 1 1/2 1/2 1 -2 70 Bb4 4 122 +20 19 75 75 2.0 1/2 1/2 2/2 2 1 1/2 1/2 1 2 50 D3 3 125 +20 19 75 75 2.0 1/2 1/2 2/2 2 1 1/2 1/2 1 -1 53 F3 3 125 +20 19 76 76 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -4 56 Ab3 3 123 +20 19 76 76 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 123 +21 20 77 77 0.5 0 0 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 128 +21 20 77 77 1.0 0 0 2/2 1 1 1/4 1/4 1 1 55 G3 3 126 +21 20 77 77 1.0 0 0 2/2 1 1 1/4 1/4 1 1 67 G4 4 126 +21 20 155/2 155/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 129 +21 20 78 78 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 130 +21 20 157/2 157/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 131 +21 20 79 79 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 132 +21 20 159/2 159/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 133 +21 20 80 80 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 134 +21 20 80 80 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -8 76 Fb5 5 127 +21 20 161/2 161/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 135 +22 21 81 81 0.5 0 0 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 140 +22 21 81 81 1.0 0 0 2/2 1 1 1/4 1/4 1 -3 75 Eb5 5 136 +22 21 163/2 163/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 141 +22 21 82 82 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 142 +22 21 82 82 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -5 73 Db5 5 137 +22 21 165/2 165/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 143 +22 21 83 83 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 144 +22 21 83 83 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -2 70 Bb4 4 138 +22 21 167/2 167/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 145 +22 21 84 84 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 146 +22 21 84 84 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 1 67 G4 4 139 +22 21 169/2 169/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 147 +23 22 85 85 1.0 0 0 2/2 2 2 1/4 1/4 1 -3 51 Eb3 3 154 +23 22 85 85 2.0 0 0 2/2 2 1 1/2 1/2 1 -5 61 Db4 4 152 +23 22 85 85 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -8 64 Fb4 4 148 +23 22 86 86 1.0 1/4 1/4 2/2 2 2 1/4 1/4 1 -3 51 Eb3 3 155 +23 22 173/2 173/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -3 63 Eb4 4 149 +23 22 87 87 0.5 1/2 1/2 2/2 2 2 1/8 1/8 1 -3 51 Eb3 3 156 +23 22 87 87 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 153 +23 22 87 87 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 150 +23 22 175/2 175/2 0.5 5/8 5/8 2/2 2 2 1/8 1/8 1 -3 63 Eb4 4 157 +23 22 88 88 0.5 3/4 3/4 2/2 2 2 1/8 1/8 1 -3 51 Eb3 3 158 +23 22 88 88 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -8 76 Fb5 5 151 +23 22 177/2 177/2 0.5 7/8 7/8 2/2 2 2 1/8 1/8 1 -3 63 Eb4 4 159 +24 23 89 89 0.5 0 0 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 164 +24 23 89 89 1.0 0 0 2/2 1 1 1/4 1/4 1 -3 75 Eb5 5 160 +24 23 179/2 179/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 165 +24 23 90 90 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 166 +24 23 90 90 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -5 73 Db5 5 161 +24 23 181/2 181/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 167 +24 23 91 91 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 168 +24 23 91 91 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -2 70 Bb4 4 162 +24 23 183/2 183/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 169 +24 23 92 92 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 170 +24 23 92 92 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 1 67 G4 4 163 +24 23 185/2 185/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 171 +25 24 93 93 1.0 0 0 2/2 2 2 1/4 1/4 1 -3 51 Eb3 3 178 +25 24 93 93 2.0 0 0 2/2 2 1 1/2 1/2 1 -5 61 Db4 4 176 +25 24 93 93 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -8 64 Fb4 4 172 +25 24 94 94 1.0 1/4 1/4 2/2 2 2 1/4 1/4 1 -3 51 Eb3 3 179 +25 24 189/2 189/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -3 63 Eb4 4 173 +25 24 95 95 0.5 1/2 1/2 2/2 2 2 1/8 1/8 1 -3 51 Eb3 3 180 +25 24 95 95 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 177 +25 24 95 95 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 174 +25 24 191/2 191/2 0.5 5/8 5/8 2/2 2 2 1/8 1/8 1 -3 63 Eb4 4 181 +25 24 96 96 0.5 3/4 3/4 2/2 2 2 1/8 1/8 1 -3 51 Eb3 3 182 +25 24 96 96 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -8 76 Fb5 5 175 +25 24 193/2 193/2 0.5 7/8 7/8 2/2 2 2 1/8 1/8 1 -3 63 Eb4 4 183 +26 25 97 97 0.5 0 0 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 188 +26 25 97 97 1.0 0 0 2/2 1 1 1/4 1/4 1 -3 75 Eb5 5 184 +26 25 195/2 195/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 189 +26 25 98 98 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 190 +26 25 98 98 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -5 73 Db5 5 185 +26 25 197/2 197/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 191 +26 25 99 99 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -5 61 Db4 4 192 +26 25 99 99 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -2 70 Bb4 4 186 +26 25 199/2 199/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 193 +26 25 100 100 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -5 61 Db4 4 194 +26 25 100 100 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 1 67 G4 4 187 +26 25 201/2 201/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 195 +27 26 101 101 0.5 0 0 2/2 2 1 1/8 1/8 1 0 60 C4 4 202 +27 26 203/2 203/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 203 +27 26 203/2 203/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 1 67 G4 4 196 +27 26 102 102 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 0 60 C4 4 204 +27 26 102 102 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -2 70 Bb4 4 197 +27 26 205/2 205/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 205 +27 26 205/2 205/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -4 68 Ab4 4 198 +27 26 103 103 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 1 55 G3 3 206 +27 26 207/2 207/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 207 +27 26 207/2 207/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 3 69 A4 4 199 +27 26 104 104 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 1 55 G3 3 208 +27 26 104 104 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 0 72 C5 5 200 +27 26 209/2 209/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 209 +27 26 209/2 209/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -2 70 Bb4 4 201 +28 27 105 105 0.5 0 0 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 216 +28 27 211/2 211/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 217 +28 27 211/2 211/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 5 71 B4 4 210 +28 27 106 106 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 218 +28 27 106 106 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -5 73 Db5 5 211 +28 27 213/2 213/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 219 +28 27 213/2 213/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 0 72 C5 5 212 +28 27 107 107 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -1 53 F3 3 220 +28 27 215/2 215/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 2 62 D4 4 221 +28 27 215/2 215/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 2 74 D5 5 213 +28 27 108 108 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 222 +28 27 108 108 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -4 80 Ab5 5 214 +28 27 217/2 217/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 2 62 D4 4 223 +28 27 217/2 217/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 2 74 D5 5 215 +29 28 109 109 0.5 0 0 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 228 +29 28 109 109 1.0 0 0 2/2 1 1 1/4 1/4 1 -3 75 Eb5 5 224 +29 28 219/2 219/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 229 +29 28 110 110 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 1 55 G3 3 230 +29 28 221/2 221/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 231 +29 28 111 111 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -1 53 F3 3 232 +29 28 223/2 223/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 2 62 D4 4 233 +29 28 223/2 223/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 2 74 D5 5 225 +29 28 112 112 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 234 +29 28 112 112 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -4 80 Ab5 5 226 +29 28 225/2 225/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 2 62 D4 4 235 +29 28 225/2 225/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 2 74 D5 5 227 +30 29 113 113 0.5 0 0 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 240 +30 29 113 113 1.0 0 0 2/2 1 1 1/4 1/4 1 -3 75 Eb5 5 236 +30 29 227/2 227/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 241 +30 29 114 114 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 1 55 G3 3 242 +30 29 229/2 229/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 243 +30 29 115 115 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -5 49 Db3 3 244 +30 29 231/2 231/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 245 +30 29 231/2 231/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 1 79 G5 5 237 +30 29 116 116 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -8 52 Fb3 3 246 +30 29 116 116 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -8 88 Fb6 6 238 +30 29 233/2 233/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 247 +30 29 233/2 233/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 1 79 G5 5 239 +31 30 117 117 0.5 0 0 2/2 2 1 1/8 1/8 1 0 48 C3 3 252 +31 30 117 117 1.0 0 0 2/2 1 1 1/4 1/4 1 -4 80 Ab5 5 248 +31 30 235/2 235/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 253 +31 30 118 118 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 254 +31 30 237/2 237/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 255 +31 30 119 119 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -5 49 Db3 3 256 +31 30 239/2 239/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 257 +31 30 239/2 239/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 1 79 G5 5 249 +31 30 120 120 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -8 52 Fb3 3 258 +31 30 120 120 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -8 88 Fb6 6 250 +31 30 241/2 241/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 259 +31 30 241/2 241/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 1 79 G5 5 251 +32 31 121 121 0.5 0 0 2/2 2 1 1/8 1/8 1 0 48 C3 3 266 +32 31 243/2 243/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 267 +32 31 243/2 243/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 1 79 G5 5 260 +32 31 122 122 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 268 +32 31 122 122 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -2 82 Bb5 5 261 +32 31 245/2 245/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 269 +32 31 245/2 245/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -4 80 Ab5 5 262 +32 31 123 123 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 1 43 G2 2 270 +32 31 247/2 247/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 1 55 G3 3 271 +32 31 247/2 247/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 3 81 A5 5 263 +32 31 124 124 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 272 +32 31 124 124 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 0 84 C6 6 264 +32 31 249/2 249/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 1 55 G3 3 273 +32 31 249/2 249/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -2 82 Bb5 5 265 +33 32 125 125 0.5 0 0 2/2 2 1 1/8 1/8 1 -4 44 Ab2 2 280 +33 32 251/2 251/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 281 +33 32 251/2 251/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 5 83 B5 5 274 +33 32 126 126 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 282 +33 32 126 126 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -5 85 Db6 6 275 +33 32 253/2 253/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 283 +33 32 253/2 253/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 0 84 C6 6 276 +33 32 127 127 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -2 46 Bb2 2 284 +33 32 255/2 255/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 285 +33 32 255/2 255/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 0 84 C6 6 277 +33 32 128 128 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -3 51 Eb3 3 286 +33 32 128 128 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -3 87 Eb6 6 278 +33 32 257/2 257/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 287 +33 32 257/2 257/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -5 85 Db6 6 279 +34 33 129 129 1.0 0 0 2/2 2 1 1/4 1/4 1 0 48 C3 3 295 +34 33 259/2 259/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 2 86 D6 6 288 +34 33 130 130 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 -3 51 Eb3 3 296 +34 33 130 130 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -1 89 F6 6 289 +34 33 261/2 261/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -3 87 Eb6 6 290 +34 33 131 131 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 -5 85 Db6 6 291 +34 33 263/2 263/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 0 84 C6 6 292 +34 33 132 132 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 297 +34 33 132 132 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -2 82 Bb5 5 293 +34 33 265/2 265/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -4 80 Ab5 5 294 +35 34 133 133 1.0 0 0 2/2 2 1 1/4 1/4 1 -5 49 Db3 3 306 +35 34 133 133 0.5 0 0 2/2 1 1 1/8 1/8 1 1 79 G5 5 298 +35 34 267/2 267/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 -1 77 F5 5 299 +35 34 134 134 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 -1 53 F3 3 307 +35 34 134 134 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -3 75 Eb5 5 300 +35 34 269/2 269/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -5 73 Db5 5 301 +35 34 135 135 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 0 72 C5 5 302 +35 34 271/2 271/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 -2 70 Bb4 4 303 +35 34 136 136 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 308 +35 34 136 136 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -4 68 Ab4 4 304 +35 34 273/2 273/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 1 67 G4 4 305 +36 35 137 137 1.0 0 0 2/2 2 1 1/4 1/4 1 -3 51 Eb3 3 317 +36 35 137 137 0.5 0 0 2/2 1 1 1/8 1/8 1 -1 65 F4 4 309 +36 35 275/2 275/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 -3 63 Eb4 4 310 +36 35 138 138 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 -4 56 Ab3 3 318 +36 35 138 138 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 2 62 D4 4 311 +36 35 277/2 277/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -3 63 Eb4 4 312 +36 35 139 139 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 2 62 D4 4 313 +36 35 279/2 279/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 -3 63 Eb4 4 314 +36 35 140 140 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 319 +36 35 140 140 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 2 62 D4 4 315 +36 35 281/2 281/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -3 63 Eb4 4 316 +37 36 141 141 1.0 0 0 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 328 +37 36 141 141 0.5 0 0 2/2 1 1 1/8 1/8 1 2 62 D4 4 320 +37 36 283/2 283/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 -3 63 Eb4 4 321 +37 36 142 142 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 329 +37 36 142 142 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -1 65 F4 4 322 +37 36 285/2 285/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -3 63 Eb4 4 323 +37 36 143 143 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 1 55 G3 3 330 +37 36 143 143 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 2 62 D4 4 324 +37 36 287/2 287/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 -3 63 Eb4 4 325 +37 36 144 144 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -5 49 Db3 3 331 +37 36 144 144 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -1 65 F4 4 326 +37 36 289/2 289/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -3 63 Eb4 4 327 +38 37 145 145 1.0 0 0 2/2 2 1 1/4 1/4 1 0 36 C2 2 339 +38 37 291/2 291/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 -4 80 Ab5 5 332 +38 37 146 146 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 -3 39 Eb2 2 340 +38 37 146 146 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -1 89 F6 6 333 +38 37 293/2 293/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -3 87 Eb6 6 334 +38 37 147 147 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 -5 85 Db6 6 335 +38 37 295/2 295/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 0 84 C6 6 336 +38 37 148 148 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 44 Ab2 2 341 +38 37 148 148 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -2 82 Bb5 5 337 +38 37 297/2 297/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -4 80 Ab5 5 338 +39 38 149 149 1.0 0 0 2/2 2 1 1/4 1/4 1 -5 37 Db2 2 350 +39 38 149 149 0.5 0 0 2/2 1 1 1/8 1/8 1 1 79 G5 5 342 +39 38 299/2 299/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 -1 77 F5 5 343 +39 38 150 150 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 -1 41 F2 2 351 +39 38 150 150 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -3 75 Eb5 5 344 +39 38 301/2 301/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -5 73 Db5 5 345 +39 38 151 151 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 0 72 C5 5 346 +39 38 303/2 303/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 -2 70 Bb4 4 347 +39 38 152 152 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 46 Bb2 2 352 +39 38 152 152 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -4 68 Ab4 4 348 +39 38 305/2 305/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 1 67 G4 4 349 +40 39 153 153 1.0 0 0 2/2 2 1 1/4 1/4 1 -3 39 Eb2 2 361 +40 39 153 153 0.5 0 0 2/2 1 1 1/8 1/8 1 -1 65 F4 4 353 +40 39 307/2 307/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 -3 63 Eb4 4 354 +40 39 154 154 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 -4 44 Ab2 2 362 +40 39 154 154 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -5 61 Db4 4 355 +40 39 309/2 309/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 0 60 C4 4 356 +40 39 155 155 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 -2 58 Bb3 3 357 +40 39 311/2 311/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 -4 56 Ab3 3 358 +40 39 156 156 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 48 C3 3 363 +40 39 156 156 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 1 55 G3 3 359 +40 39 313/2 313/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -1 53 F3 3 360 +41 40 157 157 1.0 0 0 2/2 2 1 1/4 1/4 1 -5 49 Db3 3 372 +41 40 157 157 0.5 0 0 2/2 1 1 1/8 1/8 1 -3 51 Eb3 3 364 +41 40 315/2 315/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 -1 53 F3 3 365 +41 40 158 158 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -2 46 Bb2 2 373 +41 40 158 158 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 1 55 G3 3 366 +41 40 317/2 317/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -4 56 Ab3 3 367 +41 40 159 159 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 1 43 G2 2 374 +41 40 159 159 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 -2 58 Bb3 3 368 +41 40 319/2 319/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 0 60 C4 4 369 +41 40 160 160 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -3 39 Eb2 2 375 +41 40 160 160 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -5 61 Db4 4 370 +41 40 321/2 321/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -2 58 Bb3 3 371 +42 41 161 161 1.0 0 0 2/2 2 1 1/4 1/4 1 -4 44 Ab2 2 380 +42 41 161 161 1.0 0 0 2/2 1 1 1/4 1/4 1 -4 56 Ab3 3 376 +42 41 162 162 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 381 +42 41 163 163 0.0 1/2 1/2 2/2 1 1 0 acciaccatura 1/8 1 2 62 D4 4 377 +42 41 163 163 1.5 1/2 1/2 2/2 1 1 3/8 1/4 3/2 -7 71 Cb5 5 378 +42 41 164 164 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 2 50 D3 3 382 +42 41 164 164 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 382 +42 41 164 164 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 382 +42 41 164 164 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -7 59 Cb4 4 382 +42 41 329/2 329/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -2 70 Bb4 4 379 +43 42 165 165 2.0 0 0 2/2 1 1 1/2 1/2 1 -4 68 Ab4 4 383 +43 42 166 166 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -3 51 Eb3 3 386 +43 42 166 166 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 386 +43 42 166 166 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 386 +43 42 167 167 1.5 1/2 1/2 2/2 1 1 3/8 1/4 3/2 1 67 G4 4 384 +43 42 168 168 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -3 51 Eb3 3 387 +43 42 168 168 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 387 +43 42 168 168 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 387 +43 42 337/2 337/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -3 75 Eb5 5 385 +44 43 169 169 1.0 0 0 2/2 1 1 1/4 1/4 1 -3 75 Eb5 5 388 +44 43 170 170 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 393 +44 43 170 170 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 393 +44 43 170 170 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 389 +44 43 171 171 0.0 1/2 1/2 2/2 1 1 0 acciaccatura 1/8 1 2 62 D4 4 390 +44 43 171 171 1.5 1/2 1/2 2/2 1 1 3/8 1/4 3/2 -7 71 Cb5 5 391 +44 43 172 172 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 2 50 D3 3 394 +44 43 172 172 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 394 +44 43 172 172 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 394 +44 43 172 172 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -7 59 Cb4 4 394 +44 43 345/2 345/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -2 70 Bb4 4 392 +45 44 173 173 2.0 0 0 2/2 1 1 1/2 1/2 1 -4 68 Ab4 4 395 +45 44 174 174 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -3 51 Eb3 3 398 +45 44 174 174 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 398 +45 44 174 174 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 398 +45 44 175 175 1.5 1/2 1/2 2/2 1 1 3/8 1/4 3/2 1 67 G4 4 396 +45 44 176 176 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -3 51 Eb3 3 399 +45 44 176 176 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 399 +45 44 176 176 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 399 +45 44 353/2 353/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -3 75 Eb5 5 397 +46 45 177 177 1.0 0 0 2/2 1 1 1/4 1/4 1 -3 75 Eb5 5 400 +46 45 178 178 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 405 +46 45 178 178 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 405 +46 45 178 178 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 401 +46 45 179 179 0.0 1/2 1/2 2/2 1 1 0 acciaccatura 1/8 1 2 74 D5 5 402 +46 45 179 179 1.5 1/2 1/2 2/2 1 1 3/8 1/4 3/2 -7 83 Cb6 6 403 +46 45 180 180 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 2 62 D4 4 406 +46 45 180 180 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -1 65 F4 4 406 +46 45 180 180 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 68 Ab4 4 406 +46 45 180 180 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -7 71 Cb5 5 406 +46 45 361/2 361/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -2 82 Bb5 5 404 +47 46 181 181 2.0 0 0 2/2 1 1 1/2 1/2 1 -4 80 Ab5 5 407 +47 46 182 182 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -3 63 Eb4 4 410 +47 46 182 182 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 68 Ab4 4 410 +47 46 182 182 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 72 C5 5 410 +47 46 183 183 1.5 1/2 1/2 2/2 1 1 3/8 1/4 3/2 1 79 G5 5 408 +47 46 184 184 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -3 63 Eb4 4 411 +47 46 184 184 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 70 Bb4 4 411 +47 46 184 184 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -5 73 Db5 5 411 +47 46 369/2 369/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -3 87 Eb6 6 409 +48 47 185 185 4.0 0 0 2/2 2 1 1 1 1 -4 56 Ab3 3 413 +48 47 185 185 4.0 0 0 2/2 2 1 1 1 1 -2 58 Bb3 3 413 +48 47 185 185 4.0 0 0 2/2 2 1 1 1 1 -5 61 Db4 4 413 +48 47 185 185 4.0 0 0 2/2 2 1 1 1 1 -3 63 Eb4 4 413 +48 47 185 185 4.0 0 0 2/2 2 1 1 1 1 1 67 G4 4 413 +48 47 185 185 4.0 0 0 2/2 1 1 1 1 1 -5 73 Db5 5 412 +48 47 185 185 4.0 0 0 2/2 1 1 1 1 1 1 79 G5 5 412 +48 47 185 185 4.0 0 0 2/2 1 1 1 1 1 -3 87 Eb6 6 412 +49 48 189 189 1.0 0 0 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 415 +49 48 189 189 1.0 0 0 2/2 2 1 1/4 1/4 1 0 60 C4 4 415 +49 48 189 189 1.0 0 0 2/2 2 1 1/4 1/4 1 -3 63 Eb4 4 415 +49 48 189 189 1.0 0 0 2/2 2 1 1/4 1/4 1 -4 68 Ab4 4 415 +49 48 189 189 1.0 0 0 2/2 1 1 1/4 1/4 1 0 72 C5 5 414 +49 48 189 189 1.0 0 0 2/2 1 1 1/4 1/4 1 -3 75 Eb5 5 414 +49 48 189 189 1.0 0 0 2/2 1 1 1/4 1/4 1 -4 80 Ab5 5 414 +50 48 192 192 1.0 0 3/4 2/2 1 1 1/4 1/4 1 -3 63 Eb4 4 416 +51 49 193 193 1.0 0 0 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 417 +51 49 194 194 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 0 72 C5 5 418 +51 49 195 195 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -3 75 Eb5 5 419 +51 49 196 196 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -4 80 Ab5 5 420 +52 50 197 197 1.5 0 0 2/2 1 1 3/8 1/4 3/2 0 84 C6 6 421 +52 50 198 198 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 426 +52 50 198 198 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 426 +52 50 198 198 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -3 63 Eb4 4 426 +52 50 397/2 397/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 -2 82 Bb5 5 422 +52 50 596/3 596/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 -4 80 Ab5 5 423 +52 50 1193/6 1193/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 1 79 G5 5 424 +52 50 199 199 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 427 +52 50 199 199 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 427 +52 50 199 199 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -3 63 Eb4 4 427 +52 50 199 199 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -4 80 Ab5 5 425 +52 50 200 200 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 428 +52 50 200 200 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 428 +52 50 200 200 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -3 63 Eb4 4 428 +53 51 201 201 0.0 0 0 2/2 1 1 0 acciaccatura 1/8 1 -5 73 Db5 5 429 +53 51 201 201 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -5 85 Db6 6 430 +53 51 202 202 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 435 +53 51 202 202 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 435 +53 51 202 202 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 435 +53 51 202 202 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -3 63 Eb4 4 435 +53 51 405/2 405/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 0 84 C6 6 431 +53 51 608/3 608/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 -2 82 Bb5 5 432 +53 51 1217/6 1217/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 3 81 A5 5 433 +53 51 203 203 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 1 55 G3 3 436 +53 51 203 203 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 436 +53 51 203 203 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 436 +53 51 203 203 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -3 63 Eb4 4 436 +53 51 203 203 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -2 82 Bb5 5 434 +53 51 204 204 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 437 +53 51 204 204 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 437 +53 51 204 204 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 437 +53 51 204 204 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -3 63 Eb4 4 437 +54 52 205 205 1.0 0 0 2/2 1 1 1/4 1/4 1 -2 70 Bb4 4 438 +54 52 206 206 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -5 73 Db5 5 439 +54 52 207 207 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 1 79 G5 5 440 +54 52 208 208 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -2 82 Bb5 5 441 +55 53 209 209 0.0 0 0 2/2 1 1 0 acciaccatura 1/8 1 -5 73 Db5 5 442 +55 53 209 209 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -5 85 Db6 6 443 +55 53 210 210 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 448 +55 53 210 210 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 448 +55 53 210 210 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 448 +55 53 210 210 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 4 64 E4 4 448 +55 53 421/2 421/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 0 84 C6 6 444 +55 53 632/3 632/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 -2 82 Bb5 5 445 +55 53 1265/6 1265/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 3 81 A5 5 446 +55 53 211 211 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 1 55 G3 3 449 +55 53 211 211 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 449 +55 53 211 211 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 449 +55 53 211 211 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 4 64 E4 4 449 +55 53 211 211 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -2 82 Bb5 5 447 +55 53 212 212 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 450 +55 53 212 212 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 450 +55 53 212 212 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 450 +55 53 212 212 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 4 64 E4 4 450 +56 54 213 213 0.0 0 0 2/2 1 1 0 acciaccatura 1/8 1 -5 73 Db5 5 451 +56 54 213 213 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -5 85 Db6 6 452 +56 54 214 214 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -6 54 Gb3 3 457 +56 54 214 214 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 457 +56 54 214 214 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 457 +56 54 214 214 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 4 64 E4 4 457 +56 54 429/2 429/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 0 84 C6 6 453 +56 54 644/3 644/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 -2 82 Bb5 5 454 +56 54 1289/6 1289/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 3 81 A5 5 455 +56 54 215 215 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -6 54 Gb3 3 458 +56 54 215 215 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 458 +56 54 215 215 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 458 +56 54 215 215 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 4 64 E4 4 458 +56 54 215 215 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -2 82 Bb5 5 456 +56 54 216 216 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -6 54 Gb3 3 459 +56 54 216 216 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 459 +56 54 216 216 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 459 +56 54 216 216 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 4 64 E4 4 459 +57 55 217 217 0.5 0 0 2/2 2 1 1/8 1/8 1 -1 53 F3 3 462 +57 55 217 217 1.0 0 0 2/2 1 1 1/4 1/4 1 3 81 A5 5 460 +57 55 435/2 435/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 463 +57 55 218 218 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -1 53 F3 3 464 +57 55 437/2 437/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 465 +57 55 219 219 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -1 53 F3 3 466 +57 55 439/2 439/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 467 +57 55 220 220 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -1 53 F3 3 468 +57 55 220 220 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -6 78 Gb5 5 461 +57 55 441/2 441/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 469 +58 56 221 221 0.5 0 0 2/2 2 1 1/8 1/8 1 -1 53 F3 3 474 +58 56 221 221 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 77 F5 5 470 +58 56 443/2 443/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 475 +58 56 222 222 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -1 53 F3 3 476 +58 56 222 222 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -3 75 Eb5 5 471 +58 56 445/2 445/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 477 +58 56 223 223 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -1 53 F3 3 478 +58 56 223 223 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 0 72 C5 5 472 +58 56 447/2 447/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 479 +58 56 224 224 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -1 53 F3 3 480 +58 56 224 224 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 3 69 A4 4 473 +58 56 449/2 449/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 481 +59 57 225 225 1.0 0 0 2/2 2 2 1/4 1/4 1 -1 53 F3 3 488 +59 57 225 225 2.0 0 0 2/2 2 1 1/2 1/2 1 -3 63 Eb4 4 486 +59 57 225 225 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -6 66 Gb4 4 482 +59 57 226 226 1.0 1/4 1/4 2/2 2 2 1/4 1/4 1 -1 53 F3 3 489 +59 57 453/2 453/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -1 65 F4 4 483 +59 57 227 227 0.5 1/2 1/2 2/2 2 2 1/8 1/8 1 -1 53 F3 3 490 +59 57 227 227 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 487 +59 57 227 227 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -2 70 Bb4 4 484 +59 57 455/2 455/2 0.5 5/8 5/8 2/2 2 2 1/8 1/8 1 -1 65 F4 4 491 +59 57 228 228 0.5 3/4 3/4 2/2 2 2 1/8 1/8 1 -1 53 F3 3 492 +59 57 228 228 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -6 78 Gb5 5 485 +59 57 457/2 457/2 0.5 7/8 7/8 2/2 2 2 1/8 1/8 1 -1 65 F4 4 493 +60 58 229 229 0.5 0 0 2/2 2 1 1/8 1/8 1 -1 53 F3 3 498 +60 58 229 229 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 77 F5 5 494 +60 58 459/2 459/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 499 +60 58 230 230 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -1 53 F3 3 500 +60 58 230 230 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -3 75 Eb5 5 495 +60 58 461/2 461/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 501 +60 58 231 231 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -1 53 F3 3 502 +60 58 231 231 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 0 72 C5 5 496 +60 58 463/2 463/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 503 +60 58 232 232 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -1 53 F3 3 504 +60 58 232 232 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 3 69 A4 4 497 +60 58 465/2 465/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 505 +61 59 233 233 1.0 0 0 2/2 2 2 1/4 1/4 1 -1 53 F3 3 512 +61 59 233 233 2.0 0 0 2/2 2 1 1/2 1/2 1 -3 63 Eb4 4 510 +61 59 233 233 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -6 66 Gb4 4 506 +61 59 234 234 1.0 1/4 1/4 2/2 2 2 1/4 1/4 1 -1 53 F3 3 513 +61 59 469/2 469/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -1 65 F4 4 507 +61 59 235 235 0.5 1/2 1/2 2/2 2 2 1/8 1/8 1 -1 53 F3 3 514 +61 59 235 235 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 511 +61 59 235 235 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -2 70 Bb4 4 508 +61 59 471/2 471/2 0.5 5/8 5/8 2/2 2 2 1/8 1/8 1 -1 65 F4 4 515 +61 59 236 236 0.5 3/4 3/4 2/2 2 2 1/8 1/8 1 -1 53 F3 3 516 +61 59 236 236 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -6 78 Gb5 5 509 +61 59 473/2 473/2 0.5 7/8 7/8 2/2 2 2 1/8 1/8 1 -1 65 F4 4 517 +62 60 237 237 0.5 0 0 2/2 2 1 1/8 1/8 1 -1 53 F3 3 522 +62 60 237 237 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 77 F5 5 518 +62 60 475/2 475/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 523 +62 60 238 238 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -1 53 F3 3 524 +62 60 238 238 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -3 75 Eb5 5 519 +62 60 477/2 477/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 525 +62 60 239 239 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 526 +62 60 239 239 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 0 72 C5 5 520 +62 60 479/2 479/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 527 +62 60 240 240 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -3 63 Eb4 4 528 +62 60 240 240 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 3 69 A4 4 521 +62 60 481/2 481/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 529 +63 61 241 241 0.5 0 0 2/2 2 1 1/8 1/8 1 -5 61 Db4 4 536 +63 61 483/2 483/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 537 +63 61 483/2 483/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 3 69 A4 4 530 +63 61 242 242 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -5 61 Db4 4 538 +63 61 242 242 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 0 72 C5 5 531 +63 61 485/2 485/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 539 +63 61 485/2 485/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -2 70 Bb4 4 532 +63 61 243 243 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 0 60 C4 4 540 +63 61 487/2 487/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 541 +63 61 487/2 487/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 -2 70 Bb4 4 533 +63 61 244 244 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 0 60 C4 4 542 +63 61 244 244 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -5 73 Db5 5 534 +63 61 489/2 489/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 543 +63 61 489/2 489/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 0 72 C5 5 535 +64 62 245 245 0.5 0 0 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 550 +64 62 491/2 491/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 551 +64 62 491/2 491/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 0 72 C5 5 544 +64 62 246 246 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 552 +64 62 246 246 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -3 75 Eb5 5 545 +64 62 493/2 493/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 553 +64 62 493/2 493/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -5 73 Db5 5 546 +64 62 247 247 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 554 +64 62 495/2 495/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 6 66 F#4 4 555 +64 62 495/2 495/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 5 71 B4 4 547 +64 62 248 248 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 556 +64 62 248 248 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 2 74 D5 5 548 +64 62 497/2 497/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 6 66 F#4 4 557 +64 62 497/2 497/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 0 72 C5 5 549 +65 63 249 249 0.5 0 0 2/2 2 1 1/8 1/8 1 1 55 G3 3 560 +65 63 249 249 1.0 0 0 2/2 1 1 1/4 1/4 1 5 71 B4 4 558 +65 63 499/2 499/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 1 67 G4 4 561 +65 63 250 250 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 1 55 G3 3 562 +65 63 501/2 501/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 1 67 G4 4 563 +65 63 251 251 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 1 55 G3 3 564 +65 63 503/2 503/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 1 67 G4 4 565 +65 63 252 252 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 1 55 G3 3 566 +65 63 252 252 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -4 80 Ab5 5 559 +65 63 505/2 505/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 1 67 G4 4 567 +66 64 253 253 0.5 0 0 2/2 2 1 1/8 1/8 1 1 55 G3 3 572 +66 64 253 253 1.0 0 0 2/2 1 1 1/4 1/4 1 1 79 G5 5 568 +66 64 507/2 507/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 1 67 G4 4 573 +66 64 254 254 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 1 55 G3 3 574 +66 64 254 254 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -1 77 F5 5 569 +66 64 509/2 509/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 1 67 G4 4 575 +66 64 255 255 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 1 55 G3 3 576 +66 64 255 255 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 2 74 D5 5 570 +66 64 511/2 511/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 1 67 G4 4 577 +66 64 256 256 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 1 55 G3 3 578 +66 64 256 256 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 5 71 B4 4 571 +66 64 513/2 513/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 1 67 G4 4 579 +67 65 257 257 1.0 0 0 2/2 2 2 1/4 1/4 1 1 55 G3 3 586 +67 65 257 257 2.0 0 0 2/2 2 1 1/2 1/2 1 -1 65 F4 4 584 +67 65 257 257 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -4 68 Ab4 4 580 +67 65 258 258 1.0 1/4 1/4 2/2 2 2 1/4 1/4 1 1 55 G3 3 587 +67 65 517/2 517/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 1 67 G4 4 581 +67 65 259 259 0.5 1/2 1/2 2/2 2 2 1/8 1/8 1 1 55 G3 3 588 +67 65 259 259 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -3 63 Eb4 4 585 +67 65 259 259 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 0 72 C5 5 582 +67 65 519/2 519/2 0.5 5/8 5/8 2/2 2 2 1/8 1/8 1 1 67 G4 4 589 +67 65 260 260 0.5 3/4 3/4 2/2 2 2 1/8 1/8 1 1 55 G3 3 590 +67 65 260 260 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -4 80 Ab5 5 583 +67 65 521/2 521/2 0.5 7/8 7/8 2/2 2 2 1/8 1/8 1 1 67 G4 4 591 +68 66 261 261 0.5 0 0 2/2 2 1 1/8 1/8 1 1 55 G3 3 596 +68 66 261 261 1.0 0 0 2/2 1 1 1/4 1/4 1 1 79 G5 5 592 +68 66 523/2 523/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 1 67 G4 4 597 +68 66 262 262 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 1 55 G3 3 598 +68 66 262 262 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -1 77 F5 5 593 +68 66 525/2 525/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 1 67 G4 4 599 +68 66 263 263 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 1 55 G3 3 600 +68 66 263 263 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 2 74 D5 5 594 +68 66 527/2 527/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 1 67 G4 4 601 +68 66 264 264 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 1 55 G3 3 602 +68 66 264 264 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 5 71 B4 4 595 +68 66 529/2 529/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 1 67 G4 4 603 +69 67 265 265 1.0 0 0 2/2 2 2 1/4 1/4 1 1 55 G3 3 612 +69 67 265 265 2.0 0 0 2/2 2 1 1/2 1/2 1 -1 65 F4 4 609 +69 67 265 265 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -4 68 Ab4 4 604 +69 67 266 266 1.0 1/4 1/4 2/2 2 2 1/4 1/4 1 1 55 G3 3 613 +69 67 533/2 533/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 1 67 G4 4 605 +69 67 267 267 1.0 1/2 1/2 2/2 2 2 1/4 1/4 1 1 55 G3 3 614 +69 67 267 267 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -3 63 Eb4 4 610 +69 67 267 267 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 0 72 C5 5 606 +69 67 268 268 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 611 +69 67 268 268 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -1 65 F4 4 607 +69 67 537/2 537/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 2 62 D4 4 608 +70 68 269 269 1.0 0 0 2/2 2 1 1/4 1/4 1 1 55 G3 3 623 +70 68 269 269 0.5 0 0 2/2 1 1 1/8 1/8 1 -1 65 F4 4 615 +70 68 539/2 539/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 2 62 D4 4 616 +70 68 270 270 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 624 +70 68 270 270 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -1 65 F4 4 617 +70 68 541/2 541/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 2 62 D4 4 618 +70 68 271 271 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 2 50 D3 3 625 +70 68 271 271 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 -1 65 F4 4 619 +70 68 543/2 543/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 2 62 D4 4 620 +70 68 272 272 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 5 47 B2 2 626 +70 68 272 272 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -1 65 F4 4 621 +70 68 545/2 545/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 2 62 D4 4 622 +71 69 273 273 1.5 0 0 2/2 2 1 3/8 1/4 3/2 -4 44 Ab2 2 637 +71 69 273 273 0.5 0 0 2/2 1 2 1/8 1/8 1 -1 65 F4 4 629 +71 69 547/2 547/2 0.5 1/8 1/8 2/2 1 2 1/8 1/8 1 2 62 D4 4 630 +71 69 274 274 0.5 1/4 1/4 2/2 1 2 1/8 1/8 1 -1 65 F4 4 631 +71 69 274 274 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 5 71 B4 4 627 +71 69 549/2 549/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 1 43 G2 2 638 +71 69 549/2 549/2 0.5 3/8 3/8 2/2 1 2 1/8 1/8 1 2 62 D4 4 632 +71 69 275 275 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 48 C3 3 639 +71 69 275 275 0.5 1/2 1/2 2/2 1 2 1/8 1/8 1 -3 63 Eb4 4 633 +71 69 275 275 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 0 72 C5 5 628 +71 69 551/2 551/2 0.5 5/8 5/8 2/2 1 2 1/8 1/8 1 0 60 C4 4 634 +71 69 276 276 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -6 54 Gb3 3 640 +71 69 276 276 0.5 3/4 3/4 2/2 1 2 1/8 1/8 1 -3 63 Eb4 4 635 +71 69 553/2 553/2 0.5 7/8 7/8 2/2 1 2 1/8 1/8 1 0 60 C4 4 636 +72 70 277 277 1.0 0 0 2/2 2 1 1/4 1/4 1 -1 53 F3 3 649 +72 70 277 277 0.5 0 0 2/2 1 2 1/8 1/8 1 -3 63 Eb4 4 641 +72 70 555/2 555/2 0.5 1/8 1/8 2/2 1 2 1/8 1/8 1 0 60 C4 4 642 +72 70 278 278 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -3 51 Eb3 3 650 +72 70 278 278 0.5 1/4 1/4 2/2 1 2 1/8 1/8 1 -3 63 Eb4 4 643 +72 70 557/2 557/2 0.5 3/8 3/8 2/2 1 2 1/8 1/8 1 0 60 C4 4 644 +72 70 279 279 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 48 C3 3 651 +72 70 279 279 0.5 1/2 1/2 2/2 1 2 1/8 1/8 1 -3 63 Eb4 4 645 +72 70 559/2 559/2 0.5 5/8 5/8 2/2 1 2 1/8 1/8 1 0 60 C4 4 646 +72 70 280 280 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 3 45 A2 2 652 +72 70 280 280 0.5 3/4 3/4 2/2 1 2 1/8 1/8 1 -3 63 Eb4 4 647 +72 70 561/2 561/2 0.5 7/8 7/8 2/2 1 2 1/8 1/8 1 0 60 C4 4 648 +73 71 281 281 1.5 0 0 2/2 2 1 3/8 1/4 3/2 -6 42 Gb2 2 663 +73 71 281 281 0.5 0 0 2/2 1 2 1/8 1/8 1 -3 63 Eb4 4 655 +73 71 563/2 563/2 0.5 1/8 1/8 2/2 1 2 1/8 1/8 1 0 60 C4 4 656 +73 71 282 282 0.5 1/4 1/4 2/2 1 2 1/8 1/8 1 -3 63 Eb4 4 657 +73 71 282 282 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 3 69 A4 4 653 +73 71 565/2 565/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -1 41 F2 2 664 +73 71 565/2 565/2 0.5 3/8 3/8 2/2 1 2 1/8 1/8 1 0 60 C4 4 658 +73 71 283 283 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -2 46 Bb2 2 665 +73 71 283 283 0.5 1/2 1/2 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 659 +73 71 283 283 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -2 70 Bb4 4 654 +73 71 567/2 567/2 0.5 5/8 5/8 2/2 1 2 1/8 1/8 1 -2 58 Bb3 3 660 +73 71 284 284 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -8 52 Fb3 3 666 +73 71 284 284 0.5 3/4 3/4 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 661 +73 71 569/2 569/2 0.5 7/8 7/8 2/2 1 2 1/8 1/8 1 -2 58 Bb3 3 662 +74 72 285 285 1.0 0 0 2/2 2 1 1/4 1/4 1 -3 51 Eb3 3 675 +74 72 285 285 0.5 0 0 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 667 +74 72 571/2 571/2 0.5 1/8 1/8 2/2 1 2 1/8 1/8 1 -2 58 Bb3 3 668 +74 72 286 286 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -5 49 Db3 3 676 +74 72 286 286 0.5 1/4 1/4 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 669 +74 72 573/2 573/2 0.5 3/8 3/8 2/2 1 2 1/8 1/8 1 -2 58 Bb3 3 670 +74 72 287 287 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -2 46 Bb2 2 677 +74 72 287 287 0.5 1/2 1/2 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 671 +74 72 575/2 575/2 0.5 5/8 5/8 2/2 1 2 1/8 1/8 1 -2 58 Bb3 3 672 +74 72 288 288 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 43 G2 2 678 +74 72 288 288 0.5 3/4 3/4 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 673 +74 72 577/2 577/2 0.5 7/8 7/8 2/2 1 2 1/8 1/8 1 -2 58 Bb3 3 674 +75 73 289 289 1.0 0 0 2/2 2 1 1/4 1/4 1 -8 40 Fb2 2 688 +75 73 289 289 0.5 0 0 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 680 +75 73 579/2 579/2 0.5 1/8 1/8 2/2 1 2 1/8 1/8 1 -2 58 Bb3 3 681 +75 73 290 290 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 -3 39 Eb2 2 689 +75 73 290 290 0.5 1/4 1/4 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 682 +75 73 581/2 581/2 0.5 3/8 3/8 2/2 1 2 1/8 1/8 1 -2 58 Bb3 3 683 +75 73 291 291 0.5 1/2 1/2 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 684 +75 73 291 291 2.0 1/2 1/2 2/2 1 1 1/2 1/2 1 1 67 G4 4 679 +75 73 583/2 583/2 0.5 5/8 5/8 2/2 1 2 1/8 1/8 1 -2 58 Bb3 3 685 +75 73 292 292 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 -3 51 Eb3 3 690 +75 73 292 292 0.5 3/4 3/4 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 686 +75 73 585/2 585/2 0.5 7/8 7/8 2/2 1 2 1/8 1/8 1 -2 58 Bb3 3 687 +76 74 293 293 1.0 0 0 2/2 2 1 1/4 1/4 1 -1 -3 51 Eb3 3 700 +76 74 293 293 0.5 0 0 2/2 1 2 1/8 1/8 1 0 60 C4 4 692 +76 74 587/2 587/2 0.5 1/8 1/8 2/2 1 2 1/8 1/8 1 -4 56 Ab3 3 693 +76 74 294 294 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 0 48 C3 3 701 +76 74 294 294 0.5 1/4 1/4 2/2 1 2 1/8 1/8 1 -3 63 Eb4 4 694 +76 74 589/2 589/2 0.5 3/8 3/8 2/2 1 2 1/8 1/8 1 -4 56 Ab3 3 695 +76 74 295 295 0.5 1/2 1/2 2/2 1 2 1/8 1/8 1 -3 63 Eb4 4 696 +76 74 295 295 2.0 1/2 1/2 2/2 1 1 1/2 1/2 1 -4 68 Ab4 4 691 +76 74 591/2 591/2 0.5 5/8 5/8 2/2 1 2 1/8 1/8 1 -4 56 Ab3 3 697 +76 74 296 296 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 0 36 C2 2 702 +76 74 296 296 0.5 3/4 3/4 2/2 1 2 1/8 1/8 1 -3 63 Eb4 4 698 +76 74 593/2 593/2 0.5 7/8 7/8 2/2 1 2 1/8 1/8 1 -4 56 Ab3 3 699 +77 75 297 297 1.0 0 0 2/2 2 1 1/4 1/4 1 -1 0 36 C2 2 712 +77 75 297 297 0.5 0 0 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 704 +77 75 595/2 595/2 0.5 1/8 1/8 2/2 1 2 1/8 1/8 1 -4 56 Ab3 3 705 +77 75 298 298 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 -5 37 Db2 2 713 +77 75 298 298 0.5 1/4 1/4 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 706 +77 75 597/2 597/2 0.5 3/8 3/8 2/2 1 2 1/8 1/8 1 -4 56 Ab3 3 707 +77 75 299 299 0.5 1/2 1/2 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 708 +77 75 299 299 2.0 1/2 1/2 2/2 1 1 1/2 1/2 1 -1 65 F4 4 703 +77 75 599/2 599/2 0.5 5/8 5/8 2/2 1 2 1/8 1/8 1 -4 56 Ab3 3 709 +77 75 300 300 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 -5 49 Db3 3 714 +77 75 300 300 0.5 3/4 3/4 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 710 +77 75 601/2 601/2 0.5 7/8 7/8 2/2 1 2 1/8 1/8 1 -4 56 Ab3 3 711 +78 76 301 301 1.0 0 0 2/2 2 1 1/4 1/4 1 -1 -5 49 Db3 3 724 +78 76 301 301 0.5 0 0 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 716 +78 76 603/2 603/2 0.5 1/8 1/8 2/2 1 2 1/8 1/8 1 1 55 G3 3 717 +78 76 302 302 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 -2 46 Bb2 2 725 +78 76 302 302 0.5 1/4 1/4 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 718 +78 76 605/2 605/2 0.5 3/8 3/8 2/2 1 2 1/8 1/8 1 1 55 G3 3 719 +78 76 303 303 0.5 1/2 1/2 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 720 +78 76 303 303 2.0 1/2 1/2 2/2 1 1 1/2 1/2 1 1 67 G4 4 715 +78 76 607/2 607/2 0.5 5/8 5/8 2/2 1 2 1/8 1/8 1 1 55 G3 3 721 +78 76 304 304 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 -2 34 Bb1 1 726 +78 76 304 304 0.5 3/4 3/4 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 722 +78 76 609/2 609/2 0.5 7/8 7/8 2/2 1 2 1/8 1/8 1 1 55 G3 3 723 +79 77 305 305 1.0 0 0 2/2 2 1 1/4 1/4 1 -1 -2 34 Bb1 1 736 +79 77 305 305 0.5 0 0 2/2 1 2 1/8 1/8 1 0 60 C4 4 728 +79 77 611/2 611/2 0.5 1/8 1/8 2/2 1 2 1/8 1/8 1 1 55 G3 3 729 +79 77 306 306 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 0 36 C2 2 737 +79 77 306 306 0.5 1/4 1/4 2/2 1 2 1/8 1/8 1 0 60 C4 4 730 +79 77 613/2 613/2 0.5 3/8 3/8 2/2 1 2 1/8 1/8 1 1 55 G3 3 731 +79 77 307 307 0.5 1/2 1/2 2/2 1 2 1/8 1/8 1 0 60 C4 4 732 +79 77 307 307 2.0 1/2 1/2 2/2 1 1 1/2 1/2 1 4 64 E4 4 727 +79 77 615/2 615/2 0.5 5/8 5/8 2/2 1 2 1/8 1/8 1 1 55 G3 3 733 +79 77 308 308 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 0 48 C3 3 738 +79 77 308 308 0.5 3/4 3/4 2/2 1 2 1/8 1/8 1 0 60 C4 4 734 +79 77 617/2 617/2 0.5 7/8 7/8 2/2 1 2 1/8 1/8 1 1 55 G3 3 735 +80 78 309 309 1.0 0 0 2/2 2 1 1/4 1/4 1 -1 0 48 C3 3 748 +80 78 309 309 0.5 0 0 2/2 1 2 1/8 1/8 1 -4 56 Ab3 3 740 +80 78 619/2 619/2 0.5 1/8 1/8 2/2 1 2 1/8 1/8 1 -1 53 F3 3 741 +80 78 310 310 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 -4 44 Ab2 2 749 +80 78 310 310 0.5 1/4 1/4 2/2 1 2 1/8 1/8 1 0 60 C4 4 742 +80 78 621/2 621/2 0.5 3/8 3/8 2/2 1 2 1/8 1/8 1 -1 53 F3 3 743 +80 78 311 311 0.5 1/2 1/2 2/2 1 2 1/8 1/8 1 0 60 C4 4 744 +80 78 311 311 2.0 1/2 1/2 2/2 1 1 1/2 1/2 1 -1 65 F4 4 739 +80 78 623/2 623/2 0.5 5/8 5/8 2/2 1 2 1/8 1/8 1 -1 53 F3 3 745 +80 78 312 312 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 32 Ab1 1 750 +80 78 312 312 0.5 3/4 3/4 2/2 1 2 1/8 1/8 1 0 60 C4 4 746 +80 78 625/2 625/2 0.5 7/8 7/8 2/2 1 2 1/8 1/8 1 -1 53 F3 3 747 +81 79 313 313 0.5 0 0 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 752 +81 79 627/2 627/2 0.5 1/8 1/8 2/2 1 2 1/8 1/8 1 -1 53 F3 3 753 +81 79 314 314 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 -2 34 Bb1 1 760 +81 79 314 314 0.5 1/4 1/4 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 754 +81 79 629/2 629/2 0.5 3/8 3/8 2/2 1 2 1/8 1/8 1 -1 53 F3 3 755 +81 79 315 315 0.5 1/2 1/2 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 756 +81 79 315 315 2.0 1/2 1/2 2/2 1 1 1/2 1/2 1 -1 65 F4 4 751 +81 79 631/2 631/2 0.5 5/8 5/8 2/2 1 2 1/8 1/8 1 -1 53 F3 3 757 +81 79 316 316 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 46 Bb2 2 761 +81 79 316 316 0.5 3/4 3/4 2/2 1 2 1/8 1/8 1 -5 61 Db4 4 758 +81 79 633/2 633/2 0.5 7/8 7/8 2/2 1 2 1/8 1/8 1 -1 53 F3 3 759 +82 80 317 317 0.5 0 0 2/2 1 1 1/8 1/8 1 2 62 D4 4 762 +82 80 635/2 635/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 -1 53 F3 3 763 +82 80 318 318 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 5 35 B1 1 770 +82 80 318 318 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -4 56 Ab3 3 764 +82 80 318 318 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 2 62 D4 4 764 +82 80 318 318 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -1 65 F4 4 764 +82 80 637/2 637/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -1 53 F3 3 765 +82 80 319 319 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 -4 56 Ab3 3 766 +82 80 319 319 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 2 62 D4 4 766 +82 80 319 319 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 -1 65 F4 4 766 +82 80 639/2 639/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 -1 53 F3 3 767 +82 80 320 320 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 5 47 B2 2 771 +82 80 320 320 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -4 56 Ab3 3 768 +82 80 320 320 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 2 62 D4 4 768 +82 80 320 320 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -1 65 F4 4 768 +82 80 641/2 641/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -1 53 F3 3 769 +83 81 321 321 0.5 0 0 2/2 2 1 1/8 1/8 1 0 36 C2 2 774 +83 81 321 321 1.0 0 0 2/2 1 1 1/4 1/4 1 1 55 G3 3 772 +83 81 321 321 1.0 0 0 2/2 1 1 1/4 1/4 1 0 60 C4 4 772 +83 81 321 321 1.0 0 0 2/2 1 1 1/4 1/4 1 4 64 E4 4 772 +83 81 643/2 643/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 775 +83 81 322 322 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 4 52 E3 3 776 +83 81 645/2 645/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 777 +83 81 323 323 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 4 52 E3 3 778 +83 81 647/2 647/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 779 +83 81 324 324 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 4 52 E3 3 780 +83 81 324 324 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 0 72 C5 5 773 +83 81 649/2 649/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 781 +84 82 325 325 0.5 0 0 2/2 2 1 1/8 1/8 1 -1 53 F3 3 784 +84 82 325 325 1.0 0 0 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 782 +84 82 651/2 651/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 785 +84 82 326 326 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -1 53 F3 3 786 +84 82 653/2 653/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 787 +84 82 327 327 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -1 53 F3 3 788 +84 82 655/2 655/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 789 +84 82 328 328 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 790 +84 82 328 328 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -1 77 F5 5 783 +84 82 657/2 657/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 791 +85 83 329 329 0.5 0 0 2/2 2 1 1/8 1/8 1 1 55 G3 3 794 +85 83 329 329 1.0 0 0 2/2 1 1 1/4 1/4 1 4 76 E5 5 792 +85 83 659/2 659/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 795 +85 83 330 330 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 1 55 G3 3 796 +85 83 661/2 661/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 797 +85 83 331 331 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 1 55 G3 3 798 +85 83 663/2 663/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 799 +85 83 332 332 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 800 +85 83 332 332 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 0 84 C6 6 793 +85 83 665/2 665/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 801 +86 84 333 333 0.5 0 0 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 804 +86 84 333 333 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 77 F5 5 802 +86 84 667/2 667/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 805 +86 84 334 334 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 0 60 C4 4 806 +86 84 669/2 669/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 807 +86 84 335 335 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 5 59 B3 3 808 +86 84 335 335 2.0 1/2 1/2 2/2 1 1 1/2 1/2 1 -1 77 F5 5 803 +86 84 335 335 2.0 1/2 1/2 2/2 1 1 1/2 1/2 1 -1 89 F6 6 803 +86 84 671/2 671/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 809 +86 84 336 336 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 2 62 D4 4 810 +86 84 673/2 673/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 811 +87 85 337 337 0.5 0 0 2/2 2 1 1/8 1/8 1 0 60 C4 4 816 +87 85 337 337 1.0 0 0 2/2 1 1 1/4 1/4 1 4 76 E5 5 812 +87 85 337 337 1.0 0 0 2/2 1 1 1/4 1/4 1 4 88 E6 6 812 +87 85 675/2 675/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 817 +87 85 338 338 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 4 52 E3 3 818 +87 85 677/2 677/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 819 +87 85 339 339 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 4 52 E3 3 820 +87 85 679/2 679/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 821 +87 85 340 340 0.0 3/4 3/4 2/2 1 1 0 grace16after 1/16 1 0 72 C5 5 813 +87 85 340 340 0.0 3/4 3/4 2/2 1 1 0 grace16after 1/16 1 5 71 B4 4 814 +87 85 340 340 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 4 52 E3 3 822 +87 85 340 340 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 0 72 C5 5 815 +87 85 681/2 681/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 823 +88 86 341 341 0.5 0 0 2/2 2 1 1/8 1/8 1 -1 53 F3 3 829 +88 86 341 341 1.0 0 0 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 824 +88 86 683/2 683/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 830 +88 86 342 342 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -1 53 F3 3 831 +88 86 342 342 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 825 +88 86 685/2 685/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 832 +88 86 343 343 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -1 53 F3 3 833 +88 86 687/2 687/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 834 +88 86 344 344 0.0 3/4 3/4 2/2 1 1 0 grace16after 1/16 1 -1 77 F5 5 826 +88 86 344 344 0.0 3/4 3/4 2/2 1 1 0 grace16after 1/16 1 4 76 E5 5 827 +88 86 344 344 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 835 +88 86 344 344 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -1 77 F5 5 828 +88 86 689/2 689/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 836 +89 87 345 345 0.5 0 0 2/2 2 1 1/8 1/8 1 1 55 G3 3 842 +89 87 345 345 1.0 0 0 2/2 1 1 1/4 1/4 1 4 76 E5 5 837 +89 87 691/2 691/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 843 +89 87 346 346 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 1 55 G3 3 844 +89 87 346 346 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 4 76 E5 5 838 +89 87 693/2 693/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 845 +89 87 347 347 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 1 55 G3 3 846 +89 87 695/2 695/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 847 +89 87 348 348 0.0 3/4 3/4 2/2 1 1 0 grace16after 1/16 1 0 84 C6 6 839 +89 87 348 348 0.0 3/4 3/4 2/2 1 1 0 grace16after 1/16 1 5 83 B5 5 840 +89 87 348 348 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 848 +89 87 348 348 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 0 84 C6 6 841 +89 87 697/2 697/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 849 +90 88 349 349 0.5 0 0 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 853 +90 88 349 349 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 77 F5 5 850 +90 88 699/2 699/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 854 +90 88 350 350 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 0 60 C4 4 855 +90 88 350 350 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -1 77 F5 5 851 +90 88 701/2 701/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 856 +90 88 351 351 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 5 59 B3 3 857 +90 88 703/2 703/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 858 +90 88 352 352 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 2 62 D4 4 859 +90 88 352 352 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -1 77 F5 5 852 +90 88 352 352 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -1 89 F6 6 852 +90 88 705/2 705/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 860 +91 89 353 353 0.5 0 0 2/2 2 1 1/8 1/8 1 0 60 C4 4 864 +91 89 353 353 1.0 0 0 2/2 1 1 1/4 1/4 1 4 76 E5 5 861 +91 89 353 353 1.0 0 0 2/2 1 1 1/4 1/4 1 4 88 E6 6 861 +91 89 707/2 707/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 865 +91 89 354 354 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 0 60 C4 4 866 +91 89 354 354 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 4 76 E5 5 862 +91 89 354 354 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 4 88 E6 6 862 +91 89 709/2 709/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 867 +91 89 355 355 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 868 +91 89 711/2 711/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 869 +91 89 356 356 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 870 +91 89 356 356 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -5 73 Db5 5 863 +91 89 356 356 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -5 85 Db6 6 863 +91 89 713/2 713/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 871 +92 90 357 357 0.5 0 0 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 875 +92 90 357 357 1.0 0 0 2/2 1 1 1/4 1/4 1 0 72 C5 5 872 +92 90 357 357 1.0 0 0 2/2 1 1 1/4 1/4 1 0 84 C6 6 872 +92 90 715/2 715/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 876 +92 90 358 358 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 0 60 C4 4 877 +92 90 358 358 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 0 72 C5 5 873 +92 90 358 358 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 0 84 C6 6 873 +92 90 717/2 717/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 878 +92 90 359 359 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 5 59 B3 3 879 +92 90 719/2 719/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 880 +92 90 360 360 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 2 62 D4 4 881 +92 90 360 360 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -1 77 F5 5 874 +92 90 360 360 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -1 89 F6 6 874 +92 90 721/2 721/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 882 +93 91 361 361 0.5 0 0 2/2 2 1 1/8 1/8 1 0 60 C4 4 886 +93 91 361 361 1.0 0 0 2/2 1 1 1/4 1/4 1 4 76 E5 5 883 +93 91 361 361 1.0 0 0 2/2 1 1 1/4 1/4 1 4 88 E6 6 883 +93 91 723/2 723/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 887 +93 91 362 362 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 0 60 C4 4 888 +93 91 362 362 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 4 76 E5 5 884 +93 91 362 362 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 4 88 E6 6 884 +93 91 725/2 725/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 889 +93 91 363 363 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 890 +93 91 727/2 727/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 891 +93 91 364 364 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 892 +93 91 364 364 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -5 73 Db5 5 885 +93 91 364 364 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -5 85 Db6 6 885 +93 91 729/2 729/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 893 +94 92 365 365 0.5 0 0 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 897 +94 92 365 365 1.0 0 0 2/2 1 1 1/4 1/4 1 0 72 C5 5 894 +94 92 365 365 1.0 0 0 2/2 1 1 1/4 1/4 1 0 84 C6 6 894 +94 92 731/2 731/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 898 +94 92 366 366 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 0 60 C4 4 899 +94 92 366 366 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 0 72 C5 5 895 +94 92 366 366 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 0 84 C6 6 895 +94 92 733/2 733/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 900 +94 92 367 367 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 5 59 B3 3 901 +94 92 735/2 735/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 902 +94 92 368 368 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 2 62 D4 4 903 +94 92 368 368 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -1 77 F5 5 896 +94 92 368 368 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -1 89 F6 6 896 +94 92 737/2 737/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 0 48 C3 3 904 +95 93 369 369 1.0 0 0 2/2 2 1 1/4 1/4 1 0 60 C4 4 906 +95 93 369 369 1.0 0 0 2/2 1 1 1/4 1/4 1 4 76 E5 5 905 +95 93 369 369 1.0 0 0 2/2 1 1 1/4 1/4 1 4 88 E6 6 905 +95 93 370 370 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 907 +95 93 371 371 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 908 +95 93 372 372 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 909 +96 94 373 373 1.0 0 0 2/2 2 1 1/4 1/4 1 0 60 C4 4 910 +96 94 374 374 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 911 +96 94 375 375 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 912 +96 94 376 376 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 913 +97 95 377 377 1.0 0 0 2/2 2 1 1/4 1/4 1 0 60 C4 4 918 +97 95 377 377 1.0 0 0 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 918 +97 95 378 378 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 919 +97 95 378 378 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 919 +97 95 757/2 757/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 -4 68 Ab4 4 914 +97 95 1136/3 1136/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 -6 66 Gb4 4 915 +97 95 2273/6 2273/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 -1 65 F4 4 916 +97 95 379 379 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 920 +97 95 379 379 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 920 +97 95 379 379 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -6 66 Gb4 4 917 +97 95 380 380 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 921 +97 95 380 380 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 921 +98 96 381 381 1.0 0 0 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 926 +98 96 381 381 1.0 0 0 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 926 +98 96 382 382 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 927 +98 96 382 382 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 927 +98 96 765/2 765/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 -3 87 Eb6 6 922 +98 96 1148/3 1148/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 -5 85 Db6 6 923 +98 96 2297/6 2297/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 0 84 C6 6 924 +98 96 383 383 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 928 +98 96 383 383 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 928 +98 96 383 383 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -5 85 Db6 6 925 +98 96 384 384 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 929 +98 96 384 384 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 929 +99 97 385 385 1.0 0 0 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 934 +99 97 385 385 1.0 0 0 2/2 2 1 1/4 1/4 1 0 60 C4 4 934 +99 97 386 386 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 935 +99 97 386 386 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 935 +99 97 773/2 773/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 -1 65 F4 4 930 +99 97 1160/3 1160/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 4 64 E4 4 931 +99 97 2321/6 2321/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 2 62 D4 4 932 +99 97 387 387 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 936 +99 97 387 387 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 936 +99 97 387 387 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 4 64 E4 4 933 +99 97 388 388 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 937 +99 97 388 388 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 937 +100 98 389 389 1.0 0 0 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 942 +100 98 389 389 1.0 0 0 2/2 2 1 1/4 1/4 1 0 60 C4 4 942 +100 98 390 390 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 943 +100 98 390 390 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 943 +100 98 781/2 781/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 -5 85 Db6 6 938 +100 98 1172/3 1172/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 0 84 C6 6 939 +100 98 2345/6 2345/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 5 83 B5 5 940 +100 98 391 391 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 944 +100 98 391 391 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 944 +100 98 391 391 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 0 84 C6 6 941 +100 98 392 392 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 945 +100 98 392 392 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 945 +101 99 393 393 1.0 0 0 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 950 +101 99 393 393 1.0 0 0 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 950 +101 99 394 394 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 951 +101 99 394 394 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 951 +101 99 789/2 789/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 -3 63 Eb4 4 946 +101 99 1184/3 1184/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 2 62 D4 4 947 +101 99 2369/6 2369/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 0 60 C4 4 948 +101 99 395 395 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 952 +101 99 395 395 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 952 +101 99 395 395 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 2 62 D4 4 949 +101 99 396 396 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 953 +101 99 396 396 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 953 +102 100 397 397 1.0 0 0 2/2 2 1 1/4 1/4 1 1 55 G3 3 958 +102 100 397 397 1.0 0 0 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 958 +102 100 397 397 1.0 0 0 2/2 2 1 1/4 1/4 1 4 64 E4 4 958 +102 100 398 398 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 959 +102 100 398 398 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 959 +102 100 398 398 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 4 64 E4 4 959 +102 100 797/2 797/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 0 84 C6 6 954 +102 100 1196/3 1196/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 -2 82 Bb5 5 955 +102 100 2393/6 2393/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 3 81 A5 5 956 +102 100 399 399 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 1 55 G3 3 960 +102 100 399 399 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 960 +102 100 399 399 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 4 64 E4 4 960 +102 100 399 399 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -2 82 Bb5 5 957 +102 100 400 400 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 961 +102 100 400 400 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 961 +102 100 400 400 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 4 64 E4 4 961 +103 101 401 401 1.0 0 0 2/2 2 1 1/4 1/4 1 -1 53 F3 3 966 +103 101 401 401 1.0 0 0 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 966 +103 101 401 401 1.0 0 0 2/2 2 1 1/4 1/4 1 0 60 C4 4 966 +103 101 401 401 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 65 F4 4 962 +103 101 402 402 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 963 +103 101 403 403 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 0 72 C5 5 964 +103 101 404 404 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -1 77 F5 5 965 +104 102 405 405 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -4 80 Ab5 5 967 +104 102 406 406 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 972 +104 102 406 406 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 972 +104 102 406 406 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 972 +104 102 813/2 813/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 1 79 G5 5 968 +104 102 1220/3 1220/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 -1 77 F5 5 969 +104 102 2441/6 2441/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 4 76 E5 5 970 +104 102 407 407 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -1 53 F3 3 973 +104 102 407 407 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 973 +104 102 407 407 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 973 +104 102 407 407 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -1 77 F5 5 971 +104 102 408 408 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 974 +104 102 408 408 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 974 +104 102 408 408 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 974 +105 103 409 409 1.0 0 0 2/2 2 1 1/4 1/4 1 4 52 E3 3 979 +105 103 409 409 1.0 0 0 2/2 2 1 1/4 1/4 1 1 55 G3 3 979 +105 103 409 409 1.0 0 0 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 979 +105 103 409 409 1.0 0 0 2/2 2 1 1/4 1/4 1 0 60 C4 4 979 +105 103 409 409 1.0 0 0 2/2 1 1 1/4 1/4 1 1 67 G4 4 975 +105 103 410 410 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 0 72 C5 5 976 +105 103 411 411 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 4 76 E5 5 977 +105 103 412 412 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 1 79 G5 5 978 +106 104 413 413 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -2 82 Bb5 5 980 +106 104 414 414 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 4 52 E3 3 985 +106 104 414 414 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 985 +106 104 414 414 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 985 +106 104 414 414 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 985 +106 104 829/2 829/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 -4 80 Ab5 5 981 +106 104 1244/3 1244/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 1 79 G5 5 982 +106 104 2489/6 2489/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 -1 77 F5 5 983 +106 104 415 415 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 4 52 E3 3 986 +106 104 415 415 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 1 55 G3 3 986 +106 104 415 415 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 986 +106 104 415 415 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 986 +106 104 415 415 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 1 79 G5 5 984 +106 104 416 416 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 4 52 E3 3 987 +106 104 416 416 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 987 +106 104 416 416 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 987 +106 104 416 416 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 987 +107 105 417 417 0.0 0 0 2/2 1 1 0 acciaccatura 1/8 1 0 72 C5 5 988 +107 105 417 417 2.0 0 0 2/2 2 1 1/2 1/2 1 -1 53 F3 3 994 +107 105 417 417 2.0 0 0 2/2 2 1 1/2 1/2 1 -4 56 Ab3 3 994 +107 105 417 417 2.0 0 0 2/2 2 1 1/2 1/2 1 0 60 C4 4 994 +107 105 417 417 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -4 80 Ab5 5 989 +107 105 837/2 837/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 1 79 G5 5 990 +107 105 1256/3 1256/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 -1 77 F5 5 991 +107 105 2513/6 2513/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 4 76 E5 5 992 +107 105 419 419 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -1 77 F5 5 993 +108 106 421 421 0.0 0 0 2/2 1 1 0 acciaccatura 1/8 1 0 72 C5 5 995 +108 106 421 421 2.0 0 0 2/2 2 1 1/2 1/2 1 1 55 G3 3 1001 +108 106 421 421 2.0 0 0 2/2 2 1 1/2 1/2 1 -2 58 Bb3 3 1001 +108 106 421 421 2.0 0 0 2/2 2 1 1/2 1/2 1 4 64 E4 4 1001 +108 106 421 421 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -2 82 Bb5 5 996 +108 106 845/2 845/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 -4 80 Ab5 5 997 +108 106 1268/3 1268/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 1 79 G5 5 998 +108 106 2537/6 2537/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 -1 77 F5 5 999 +108 106 423 423 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 1 79 G5 5 1000 +109 107 425 425 1.0 0 0 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 1007 +109 107 425 425 1.0 0 0 2/2 2 1 1/4 1/4 1 0 60 C4 4 1007 +109 107 425 425 1.0 0 0 2/2 2 1 1/4 1/4 1 -1 65 F4 4 1007 +109 107 425 425 2.0 0 0 2/2 1 1 1/2 1/2 1 0 72 C5 5 1002 +109 107 425 425 2.0 0 0 2/2 1 1 1/2 1/2 1 -1 77 F5 5 1002 +109 107 425 425 2.0 0 0 2/2 1 1 1/2 1/2 1 -4 80 Ab5 5 1002 +109 107 425 425 2.0 0 0 2/2 1 1 1/2 1/2 1 0 84 C6 6 1002 +109 107 427 427 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 1008 +109 107 427 427 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 1008 +109 107 427 427 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 1 67 G4 4 1008 +109 107 427 427 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 -2 82 Bb5 5 1003 +109 107 855/2 855/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 -4 80 Ab5 5 1004 +109 107 428 428 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 1 79 G5 5 1005 +109 107 857/2 857/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -1 77 F5 5 1006 +110 108 429 429 0.0 0 0 2/2 1 1 0 grace16 1/16 1 4 76 E5 5 1009 +110 108 429 429 0.0 0 0 2/2 1 1 0 grace16 1/16 1 -1 77 F5 5 1010 +110 108 429 429 0.0 0 0 2/2 1 1 0 grace16 1/16 1 1 79 G5 5 1011 +110 108 429 429 1.0 0 0 2/2 2 1 1/4 1/4 1 0 60 C4 4 1014 +110 108 429 429 1.0 0 0 2/2 2 1 1/4 1/4 1 1 67 G4 4 1014 +110 108 429 429 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 77 F5 5 1012 +110 108 430 430 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 4 76 E5 5 1013 +110 108 432 432 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 48 C3 3 1015 +111 109 433 433 1.0 0 0 2/2 2 1 1/4 1/4 1 -1 53 F3 3 1016 +111 109 434 434 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 1017 +111 109 435 435 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 1018 +111 109 436 436 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -1 65 F4 4 1019 +112 110 437 437 1.5 0 0 2/2 2 1 3/8 1/4 3/2 -4 68 Ab4 4 1020 +112 110 877/2 877/2 0.16666666666666666 3/8 3/8 2/2 2 1 1/24 1/16 2/3 1 67 G4 4 1021 +112 110 1316/3 1316/3 0.16666666666666666 5/12 5/12 2/2 2 1 1/24 1/16 2/3 -1 65 F4 4 1022 +112 110 2633/6 2633/6 0.16666666666666666 11/24 11/24 2/2 2 1 1/24 1/16 2/3 4 64 E4 4 1023 +112 110 439 439 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -1 65 F4 4 1024 +112 110 440 440 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -1 65 F4 4 1025 +113 111 441 441 4.0 0 0 2/2 2 1 1 1 1 -1 65 F4 4 1032 +113 111 441 441 4.0 0 0 2/2 2 1 1 1 1 -6 66 Gb4 4 1032 +113 111 441 441 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -2 82 Bb5 5 1026 +113 111 885/2 885/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 -4 80 Ab5 5 1027 +113 111 1328/3 1328/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 -6 78 Gb5 5 1028 +113 111 2657/6 2657/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 -1 77 F5 5 1029 +113 111 443 443 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -6 78 Gb5 5 1030 +113 111 444 444 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -6 78 Gb5 5 1031 +114 112 445 445 4.0 0 0 2/2 2 1 1 1 1 -3 63 Eb4 4 1039 +114 112 445 445 4.0 0 0 2/2 2 1 1 1 1 -6 66 Gb4 4 1039 +114 112 445 445 4.0 0 0 2/2 1 1 1 1 1 1 -6 78 Gb5 5 1033 +114 112 893/2 893/2 0.16666666666666666 3/8 3/8 2/2 1 2 1/24 1/16 2/3 -5 73 Db5 5 1034 +114 112 1340/3 1340/3 0.16666666666666666 5/12 5/12 2/2 1 2 1/24 1/16 2/3 0 72 C5 5 1035 +114 112 2681/6 2681/6 0.16666666666666666 11/24 11/24 2/2 1 2 1/24 1/16 2/3 5 71 B4 4 1036 +114 112 447 447 1.0 1/2 1/2 2/2 1 2 1/4 1/4 1 0 72 C5 5 1037 +114 112 448 448 1.0 3/4 3/4 2/2 1 2 1/4 1/4 1 0 72 C5 5 1038 +115 113 449 449 4.0 0 0 2/2 2 1 1 1 1 -3 63 Eb4 4 1047 +115 113 449 449 4.0 0 0 2/2 2 1 1 1 1 3 69 A4 4 1047 +115 113 449 449 4.0 0 0 2/2 1 2 1 1 1 1 0 72 C5 5 1046 +115 113 449 449 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -1 -6 78 Gb5 5 1040 +115 113 901/2 901/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 -6 78 Gb5 5 1041 +115 113 1352/3 1352/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 -1 77 F5 5 1042 +115 113 2705/6 2705/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 4 76 E5 5 1043 +115 113 451 451 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -1 77 F5 5 1044 +115 113 452 452 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -1 77 F5 5 1045 +116 114 453 453 4.0 0 0 2/2 2 1 1 1 1 -5 61 Db4 4 1055 +116 114 453 453 4.0 0 0 2/2 2 1 1 1 1 -1 65 F4 4 1055 +116 114 453 453 1.5 0 0 2/2 1 2 3/8 1/4 3/2 -1 0 72 C5 5 1049 +116 114 453 453 4.0 0 0 2/2 1 1 1 1 1 -1 77 F5 5 1048 +116 114 909/2 909/2 0.16666666666666666 3/8 3/8 2/2 1 2 1/24 1/16 2/3 0 72 C5 5 1050 +116 114 1364/3 1364/3 0.16666666666666666 5/12 5/12 2/2 1 2 1/24 1/16 2/3 -2 70 Bb4 4 1051 +116 114 2729/6 2729/6 0.16666666666666666 11/24 11/24 2/2 1 2 1/24 1/16 2/3 3 69 A4 4 1052 +116 114 455 455 1.0 1/2 1/2 2/2 1 2 1/4 1/4 1 -2 70 Bb4 4 1053 +116 114 456 456 1.0 3/4 3/4 2/2 1 2 1/4 1/4 1 -2 70 Bb4 4 1054 +117 115 457 457 4.0 0 0 2/2 2 2 1 1 1 1 2 62 D4 4 1064 +117 115 457 457 3.0 0 0 2/2 2 1 3/4 1/2 3/2 -1 65 F4 4 1062 +117 115 457 457 4.0 0 0 2/2 1 2 1 1 1 5 71 B4 4 1061 +117 115 917/2 917/2 0.16666666666666666 3/8 3/8 2/2 1 1 1/24 1/16 2/3 1 79 G5 5 1056 +117 115 1376/3 1376/3 0.16666666666666666 5/12 5/12 2/2 1 1 1/24 1/16 2/3 -1 77 F5 5 1057 +117 115 2753/6 2753/6 0.16666666666666666 11/24 11/24 2/2 1 1 1/24 1/16 2/3 4 76 E5 5 1058 +117 115 459 459 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -1 77 F5 5 1059 +117 115 460 460 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 4 64 E4 4 1063 +117 115 460 460 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 1 79 G5 5 1060 +118 116 461 461 4.0 0 0 2/2 2 2 1 1 1 -1 2 62 D4 4 1073 +118 116 461 461 1.0 0 0 2/2 2 1 1/4 1/4 1 -1 65 F4 4 1069 +118 116 461 461 1.0 0 0 2/2 1 1 1/4 1/4 1 -4 80 Ab5 5 1065 +118 116 462 462 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 1 67 G4 4 1070 +118 116 462 462 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 4 76 E5 5 1066 +118 116 463 463 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -4 68 Ab4 4 1071 +118 116 463 463 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -1 77 F5 5 1067 +118 116 464 464 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -1 65 F4 4 1072 +118 116 464 464 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 5 71 B4 4 1068 +119 117 465 465 4.0 0 0 2/2 2 2 1 1 1 1 0 60 C4 4 1082 +119 117 465 465 1.0 0 0 2/2 2 1 1/4 1/4 1 4 64 E4 4 1078 +119 117 465 465 1.0 0 0 2/2 1 1 1/4 1/4 1 0 72 C5 5 1074 +119 117 466 466 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 1 67 G4 4 1079 +119 117 466 466 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 4 76 E5 5 1075 +119 117 467 467 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -1 65 F4 4 1080 +119 117 467 467 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -1 77 F5 5 1076 +119 117 468 468 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 4 64 E4 4 1081 +119 117 468 468 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 1 79 G5 5 1077 +120 118 469 469 2.0 0 0 2/2 2 2 1/2 1/2 1 -1 0 60 C4 4 1091 +120 118 469 469 1.0 0 0 2/2 2 1 1/4 1/4 1 -1 65 F4 4 1087 +120 118 469 469 1.0 0 0 2/2 1 1 1/4 1/4 1 -4 80 Ab5 5 1083 +120 118 470 470 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -2 70 Bb4 4 1088 +120 118 470 470 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 4 76 E5 5 1084 +120 118 471 471 2.0 1/2 1/2 2/2 2 2 1/2 1/2 1 -5 61 Db4 4 1092 +120 118 471 471 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -4 68 Ab4 4 1089 +120 118 471 471 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -1 77 F5 5 1085 +120 118 472 472 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -1 65 F4 4 1090 +120 118 472 472 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 5 71 B4 4 1086 +121 119 473 473 0.5 0 0 2/2 2 1 1/8 1/8 1 0 48 C3 3 1095 +121 119 473 473 1.0 0 0 2/2 1 1 1/4 1/4 1 4 64 E4 4 1093 +121 119 473 473 1.0 0 0 2/2 1 1 1/4 1/4 1 0 72 C5 5 1093 +121 119 947/2 947/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1096 +121 119 474 474 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 0 48 C3 3 1097 +121 119 949/2 949/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1098 +121 119 475 475 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 0 48 C3 3 1099 +121 119 951/2 951/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1100 +121 119 476 476 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 0 48 C3 3 1101 +121 119 476 476 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -5 73 Db5 5 1094 +121 119 953/2 953/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1102 +122 120 477 477 0.5 0 0 2/2 2 1 1/8 1/8 1 0 48 C3 3 1107 +122 120 477 477 1.0 0 0 2/2 1 1 1/4 1/4 1 0 72 C5 5 1103 +122 120 955/2 955/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1108 +122 120 478 478 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 0 48 C3 3 1109 +122 120 478 478 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -2 70 Bb4 4 1104 +122 120 957/2 957/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1110 +122 120 479 479 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 0 48 C3 3 1111 +122 120 479 479 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 1 67 G4 4 1105 +122 120 959/2 959/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1112 +122 120 480 480 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 0 48 C3 3 1113 +122 120 480 480 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 4 64 E4 4 1106 +122 120 961/2 961/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1114 +123 121 481 481 1.0 0 0 2/2 2 2 1/4 1/4 1 0 48 C3 3 1121 +123 121 481 481 2.0 0 0 2/2 2 1 1/2 1/2 1 -2 58 Bb3 3 1119 +123 121 481 481 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -5 61 Db4 4 1115 +123 121 482 482 1.0 1/4 1/4 2/2 2 2 1/4 1/4 1 0 48 C3 3 1122 +123 121 965/2 965/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 0 60 C4 4 1116 +123 121 483 483 0.5 1/2 1/2 2/2 2 2 1/8 1/8 1 0 48 C3 3 1123 +123 121 483 483 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 1120 +123 121 483 483 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -1 65 F4 4 1117 +123 121 967/2 967/2 0.5 5/8 5/8 2/2 2 2 1/8 1/8 1 0 60 C4 4 1124 +123 121 484 484 0.5 3/4 3/4 2/2 2 2 1/8 1/8 1 0 48 C3 3 1125 +123 121 484 484 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -5 85 Db6 6 1118 +123 121 969/2 969/2 0.5 7/8 7/8 2/2 2 2 1/8 1/8 1 0 60 C4 4 1126 +124 122 485 485 0.5 0 0 2/2 2 1 1/8 1/8 1 0 48 C3 3 1131 +124 122 485 485 1.0 0 0 2/2 1 1 1/4 1/4 1 0 84 C6 6 1127 +124 122 971/2 971/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1132 +124 122 486 486 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 0 48 C3 3 1133 +124 122 486 486 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -2 82 Bb5 5 1128 +124 122 973/2 973/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1134 +124 122 487 487 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 0 48 C3 3 1135 +124 122 487 487 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 1 79 G5 5 1129 +124 122 975/2 975/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1136 +124 122 488 488 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 0 48 C3 3 1137 +124 122 488 488 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 4 76 E5 5 1130 +124 122 977/2 977/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1138 +125 123 489 489 1.0 0 0 2/2 2 2 1/4 1/4 1 0 48 C3 3 1145 +125 123 489 489 2.0 0 0 2/2 2 1 1/2 1/2 1 -2 58 Bb3 3 1143 +125 123 489 489 1.5 0 0 2/2 1 1 3/8 1/4 3/2 -5 73 Db5 5 1139 +125 123 490 490 1.0 1/4 1/4 2/2 2 2 1/4 1/4 1 0 48 C3 3 1146 +125 123 981/2 981/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 0 72 C5 5 1140 +125 123 491 491 0.5 1/2 1/2 2/2 2 2 1/8 1/8 1 0 48 C3 3 1147 +125 123 491 491 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 1144 +125 123 491 491 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -1 77 F5 5 1141 +125 123 983/2 983/2 0.5 5/8 5/8 2/2 2 2 1/8 1/8 1 0 60 C4 4 1148 +125 123 492 492 0.5 3/4 3/4 2/2 2 2 1/8 1/8 1 0 48 C3 3 1149 +125 123 492 492 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -5 85 Db6 6 1142 +125 123 985/2 985/2 0.5 7/8 7/8 2/2 2 2 1/8 1/8 1 0 60 C4 4 1150 +126 124 493 493 0.5 0 0 2/2 2 1 1/8 1/8 1 0 48 C3 3 1155 +126 124 493 493 1.0 0 0 2/2 1 1 1/4 1/4 1 0 84 C6 6 1151 +126 124 987/2 987/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1156 +126 124 494 494 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 0 48 C3 3 1157 +126 124 494 494 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -2 82 Bb5 5 1152 +126 124 989/2 989/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1158 +126 124 495 495 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 1159 +126 124 495 495 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 1 79 G5 5 1153 +126 124 991/2 991/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1160 +126 124 496 496 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 1161 +126 124 496 496 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 4 76 E5 5 1154 +126 124 993/2 993/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1162 +127 125 497 497 0.5 0 0 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 1169 +127 125 995/2 995/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 1170 +127 125 995/2 995/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 4 76 E5 5 1163 +127 125 498 498 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 0 60 C4 4 1171 +127 125 498 498 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 1 79 G5 5 1164 +127 125 997/2 997/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 1172 +127 125 997/2 997/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -1 77 F5 5 1165 +127 125 499 499 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 1 55 G3 3 1173 +127 125 999/2 999/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 4 64 E4 4 1174 +127 125 999/2 999/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 -1 77 F5 5 1166 +127 125 500 500 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 1175 +127 125 500 500 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -4 80 Ab5 5 1167 +127 125 1001/2 1001/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 4 64 E4 4 1176 +127 125 1001/2 1001/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 1 79 G5 5 1168 +128 126 501 501 0.5 0 0 2/2 2 1 1/8 1/8 1 -1 53 F3 3 1183 +128 126 1003/2 1003/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1184 +128 126 1003/2 1003/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 1 79 G5 5 1177 +128 126 502 502 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 -4 56 Ab3 3 1185 +128 126 502 502 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -2 82 Bb5 5 1178 +128 126 1005/2 1005/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1186 +128 126 1005/2 1005/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -4 80 Ab5 5 1179 +128 126 503 503 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -5 49 Db3 3 1187 +128 126 1007/2 1007/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 5 59 B3 3 1188 +128 126 1007/2 1007/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 5 83 B5 5 1180 +128 126 504 504 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -1 53 F3 3 1189 +128 126 504 504 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -1 89 F6 6 1181 +128 126 1009/2 1009/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 5 59 B3 3 1190 +128 126 1009/2 1009/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 5 83 B5 5 1182 +129 127 505 505 0.5 0 0 2/2 2 1 1/8 1/8 1 0 48 C3 3 1195 +129 127 505 505 1.0 0 0 2/2 1 1 1/4 1/4 1 0 84 C6 6 1191 +129 127 1011/2 1011/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1196 +129 127 506 506 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 4 52 E3 3 1197 +129 127 1013/2 1013/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1198 +129 127 507 507 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -5 49 Db3 3 1199 +129 127 1015/2 1015/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 5 59 B3 3 1200 +129 127 1015/2 1015/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 5 83 B5 5 1192 +129 127 508 508 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -1 53 F3 3 1201 +129 127 508 508 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -1 89 F6 6 1193 +129 127 1017/2 1017/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 5 59 B3 3 1202 +129 127 1017/2 1017/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 5 83 B5 5 1194 +130 128 509 509 0.5 0 0 2/2 2 1 1/8 1/8 1 0 48 C3 3 1207 +130 128 509 509 1.0 0 0 2/2 1 1 1/4 1/4 1 0 84 C6 6 1203 +130 128 1019/2 1019/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1208 +130 128 510 510 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 4 52 E3 3 1209 +130 128 1021/2 1021/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 0 60 C4 4 1210 +130 128 511 511 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -2 46 Bb2 2 1211 +130 128 1023/2 1023/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 1 55 G3 3 1212 +130 128 1023/2 1023/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 4 76 E5 5 1204 +130 128 512 512 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -5 49 Db3 3 1213 +130 128 512 512 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -5 85 Db6 6 1205 +130 128 1025/2 1025/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 1 55 G3 3 1214 +130 128 1025/2 1025/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 4 76 E5 5 1206 +131 129 513 513 0.5 0 0 2/2 2 1 1/8 1/8 1 -4 44 Ab2 2 1219 +131 129 513 513 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 77 F5 5 1215 +131 129 1027/2 1027/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -1 53 F3 3 1220 +131 129 514 514 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 0 48 C3 3 1221 +131 129 1029/2 1029/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -1 53 F3 3 1222 +131 129 515 515 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 -2 46 Bb2 2 1223 +131 129 1031/2 1031/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 1 55 G3 3 1224 +131 129 1031/2 1031/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 4 76 E5 5 1216 +131 129 516 516 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 -5 49 Db3 3 1225 +131 129 516 516 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -5 85 Db6 6 1217 +131 129 1033/2 1033/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 1 55 G3 3 1226 +131 129 1033/2 1033/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 4 76 E5 5 1218 +132 130 517 517 0.5 0 0 2/2 2 1 1/8 1/8 1 -4 44 Ab2 2 1233 +132 130 1035/2 1035/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -1 53 F3 3 1234 +132 130 1035/2 1035/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 4 76 E5 5 1227 +132 130 518 518 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 0 48 C3 3 1235 +132 130 518 518 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 1 79 G5 5 1228 +132 130 1037/2 1037/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -1 53 F3 3 1236 +132 130 1037/2 1037/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -1 77 F5 5 1229 +132 130 519 519 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 4 40 E2 2 1237 +132 130 1039/2 1039/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 4 52 E3 3 1238 +132 130 1039/2 1039/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 -1 77 F5 5 1230 +132 130 520 520 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 0 48 C3 3 1239 +132 130 520 520 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -4 80 Ab5 5 1231 +132 130 1041/2 1041/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 4 52 E3 3 1240 +132 130 1041/2 1041/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 1 79 G5 5 1232 +133 131 521 521 0.5 0 0 2/2 2 1 1/8 1/8 1 -1 41 F2 2 1247 +133 131 1043/2 1043/2 0.5 1/8 1/8 2/2 2 1 1/8 1/8 1 -1 53 F3 3 1248 +133 131 1043/2 1043/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 1 79 G5 5 1241 +133 131 522 522 0.5 1/4 1/4 2/2 2 1 1/8 1/8 1 0 48 C3 3 1249 +133 131 522 522 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -2 82 Bb5 5 1242 +133 131 1045/2 1045/2 0.5 3/8 3/8 2/2 2 1 1/8 1/8 1 -1 53 F3 3 1250 +133 131 1045/2 1045/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -4 80 Ab5 5 1243 +133 131 523 523 0.5 1/2 1/2 2/2 2 1 1/8 1/8 1 1 43 G2 2 1251 +133 131 1047/2 1047/2 0.5 5/8 5/8 2/2 2 1 1/8 1/8 1 1 55 G3 3 1252 +133 131 1047/2 1047/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 3 81 A5 5 1244 +133 131 524 524 0.5 3/4 3/4 2/2 2 1 1/8 1/8 1 0 48 C3 3 1253 +133 131 524 524 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 0 84 C6 6 1245 +133 131 1049/2 1049/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 1 55 G3 3 1254 +133 131 1049/2 1049/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -2 82 Bb5 5 1246 +134 132 525 525 1.0 0 0 2/2 2 1 1/4 1/4 1 -4 44 Ab2 2 1262 +134 132 1051/2 1051/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 0 84 C6 6 1255 +134 132 526 526 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 0 48 C3 3 1263 +134 132 526 526 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -1 89 F6 6 1256 +134 132 1053/2 1053/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -3 87 Eb6 6 1257 +134 132 527 527 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 -5 85 Db6 6 1258 +134 132 1055/2 1055/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 0 84 C6 6 1259 +134 132 528 528 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 1264 +134 132 528 528 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -2 82 Bb5 5 1260 +134 132 1057/2 1057/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -4 80 Ab5 5 1261 +135 133 529 529 1.0 0 0 2/2 2 1 1/4 1/4 1 -2 46 Bb2 2 1273 +135 133 529 529 0.5 0 0 2/2 1 1 1/8 1/8 1 1 79 G5 5 1265 +135 133 1059/2 1059/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 -1 77 F5 5 1266 +135 133 530 530 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 -5 49 Db3 3 1274 +135 133 530 530 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -3 75 Eb5 5 1267 +135 133 1061/2 1061/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -5 73 Db5 5 1268 +135 133 531 531 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 0 72 C5 5 1269 +135 133 1063/2 1063/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 -2 70 Bb4 4 1270 +135 133 532 532 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 1275 +135 133 532 532 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -4 68 Ab4 4 1271 +135 133 1065/2 1065/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 1 67 G4 4 1272 +136 134 533 533 1.0 0 0 2/2 2 1 1/4 1/4 1 0 48 C3 3 1284 +136 134 533 533 0.5 0 0 2/2 1 1 1/8 1/8 1 -1 65 F4 4 1276 +136 134 1067/2 1067/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 4 64 E4 4 1277 +136 134 534 534 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 -1 53 F3 3 1285 +136 134 534 534 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -5 61 Db4 4 1278 +136 134 1069/2 1069/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 0 60 C4 4 1279 +136 134 535 535 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 5 59 B3 3 1280 +136 134 1071/2 1071/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 0 60 C4 4 1281 +136 134 536 536 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 1286 +136 134 536 536 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 5 59 B3 3 1282 +136 134 1073/2 1073/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 0 60 C4 4 1283 +137 135 537 537 1.0 0 0 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 1295 +137 135 537 537 0.5 0 0 2/2 1 1 1/8 1/8 1 5 59 B3 3 1287 +137 135 1075/2 1075/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 0 60 C4 4 1288 +137 135 538 538 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 1296 +137 135 538 538 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -5 61 Db4 4 1289 +137 135 1077/2 1077/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 0 60 C4 4 1290 +137 135 539 539 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 4 52 E3 3 1297 +137 135 539 539 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 5 59 B3 3 1291 +137 135 1079/2 1079/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 0 60 C4 4 1292 +137 135 540 540 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 46 Bb2 2 1298 +137 135 540 540 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -5 61 Db4 4 1293 +137 135 1081/2 1081/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 0 60 C4 4 1294 +138 136 541 541 1.0 0 0 2/2 2 1 1/4 1/4 1 -4 44 Ab2 2 1306 +138 136 1083/2 1083/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 0 84 C6 6 1299 +138 136 542 542 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 0 48 C3 3 1307 +138 136 542 542 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -1 89 F6 6 1300 +138 136 1085/2 1085/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -3 87 Eb6 6 1301 +138 136 543 543 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 -5 85 Db6 6 1302 +138 136 1087/2 1087/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 0 84 C6 6 1303 +138 136 544 544 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 1308 +138 136 544 544 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -2 82 Bb5 5 1304 +138 136 1089/2 1089/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -4 80 Ab5 5 1305 +139 137 545 545 1.0 0 0 2/2 2 1 1/4 1/4 1 -2 46 Bb2 2 1317 +139 137 545 545 0.5 0 0 2/2 1 1 1/8 1/8 1 1 79 G5 5 1309 +139 137 1091/2 1091/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 -1 77 F5 5 1310 +139 137 546 546 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 -5 49 Db3 3 1318 +139 137 546 546 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -3 75 Eb5 5 1311 +139 137 1093/2 1093/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 -5 73 Db5 5 1312 +139 137 547 547 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 0 72 C5 5 1313 +139 137 1095/2 1095/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 -2 70 Bb4 4 1314 +139 137 548 548 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 1319 +139 137 548 548 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -4 68 Ab4 4 1315 +139 137 1097/2 1097/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 1 67 G4 4 1316 +140 138 549 549 1.0 0 0 2/2 2 1 1/4 1/4 1 0 48 C3 3 1328 +140 138 549 549 0.5 0 0 2/2 1 1 1/8 1/8 1 -1 65 F4 4 1320 +140 138 1099/2 1099/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 4 64 E4 4 1321 +140 138 550 550 2.0 1/4 1/4 2/2 2 1 1/2 1/2 1 -1 53 F3 3 1329 +140 138 550 550 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -5 61 Db4 4 1322 +140 138 1101/2 1101/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 0 60 C4 4 1323 +140 138 551 551 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 5 59 B3 3 1324 +140 138 1103/2 1103/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 0 60 C4 4 1325 +140 138 552 552 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 1330 +140 138 552 552 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 5 59 B3 3 1326 +140 138 1105/2 1105/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 0 60 C4 4 1327 +141 139 553 553 0.5 0 0 2/2 1 1 1/8 1/8 1 5 59 B3 3 1331 +141 139 1107/2 1107/2 0.5 1/8 1/8 2/2 1 1 1/8 1/8 1 0 60 C4 4 1332 +141 139 554 554 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 36 C2 2 1339 +141 139 554 554 0.5 1/4 1/4 2/2 1 1 1/8 1/8 1 -5 61 Db4 4 1333 +141 139 1109/2 1109/2 0.5 3/8 3/8 2/2 1 1 1/8 1/8 1 0 60 C4 4 1334 +141 139 555 555 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 36 C2 2 1340 +141 139 555 555 0.5 1/2 1/2 2/2 1 1 1/8 1/8 1 0 60 C4 4 1335 +141 139 1111/2 1111/2 0.5 5/8 5/8 2/2 1 1 1/8 1/8 1 -2 58 Bb3 3 1336 +141 139 556 556 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 36 C2 2 1341 +141 139 556 556 0.5 3/4 3/4 2/2 1 1 1/8 1/8 1 -4 56 Ab3 3 1337 +141 139 1113/2 1113/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 1 55 G3 3 1338 +142 140 557 557 1.0 0 0 2/2 2 1 1/4 1/4 1 -1 41 F2 2 1345 +142 140 557 557 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 53 F3 3 1342 +142 140 558 558 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 1346 +142 140 559 559 1.5 1/2 1/2 2/2 1 1 3/8 1/4 3/2 -4 68 Ab4 4 1343 +142 140 560 560 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -5 49 Db3 3 1347 +142 140 560 560 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 1347 +142 140 560 560 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 1347 +142 140 560 560 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 5 59 B3 3 1347 +142 140 1121/2 1121/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 1 67 G4 4 1344 +143 141 561 561 2.0 0 0 2/2 1 1 1/2 1/2 1 -1 65 F4 4 1348 +143 141 562 562 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 48 C3 3 1351 +143 141 562 562 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 1351 +143 141 562 562 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 1351 +143 141 562 562 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 1351 +143 141 563 563 1.5 1/2 1/2 2/2 1 1 3/8 1/4 3/2 4 64 E4 4 1349 +143 141 564 564 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 48 C3 3 1352 +143 141 564 564 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 1352 +143 141 564 564 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 1352 +143 141 1129/2 1129/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 0 72 C5 5 1350 +144 142 565 565 1.0 0 0 2/2 1 1 1/4 1/4 1 0 72 C5 5 1353 +144 142 566 566 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 1357 +144 142 566 566 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 1357 +144 142 566 566 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -1 65 F4 4 1354 +144 142 567 567 1.5 1/2 1/2 2/2 1 1 3/8 1/4 3/2 -4 68 Ab4 4 1355 +144 142 568 568 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -5 49 Db3 3 1358 +144 142 568 568 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 1358 +144 142 568 568 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 1358 +144 142 568 568 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 5 59 B3 3 1358 +144 142 1137/2 1137/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 1 67 G4 4 1356 +145 143 569 569 2.0 0 0 2/2 1 1 1/2 1/2 1 -1 65 F4 4 1359 +145 143 570 570 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 48 C3 3 1362 +145 143 570 570 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 1362 +145 143 570 570 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 1362 +145 143 570 570 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 1362 +145 143 571 571 1.5 1/2 1/2 2/2 1 1 3/8 1/4 3/2 4 64 E4 4 1360 +145 143 572 572 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 48 C3 3 1363 +145 143 572 572 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 55 G3 3 1363 +145 143 572 572 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 1363 +145 143 1145/2 1145/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 0 72 C5 5 1361 +146 144 573 573 1.0 0 0 2/2 1 1 1/4 1/4 1 0 72 C5 5 1364 +146 144 574 574 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 1369 +146 144 574 574 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 1369 +146 144 574 574 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -1 65 F4 4 1365 +146 144 575 575 0.0 1/2 1/2 2/2 1 1 0 acciaccatura 1/8 1 5 71 B4 4 1366 +146 144 575 575 1.5 1/2 1/2 2/2 1 1 3/8 1/4 3/2 -4 80 Ab5 5 1367 +146 144 576 576 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 1370 +146 144 576 576 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -1 65 F4 4 1370 +146 144 576 576 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -4 68 Ab4 4 1370 +146 144 576 576 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 5 71 B4 4 1370 +146 144 1153/2 1153/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 1 79 G5 5 1368 +147 145 577 577 2.0 0 0 2/2 1 1 1/2 1/2 1 -1 77 F5 5 1371 +147 145 578 578 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 1374 +147 145 578 578 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -1 65 F4 4 1374 +147 145 578 578 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 -4 68 Ab4 4 1374 +147 145 578 578 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 72 C5 5 1374 +147 145 579 579 1.5 1/2 1/2 2/2 1 1 3/8 1/4 3/2 4 76 E5 5 1372 +147 145 580 580 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 1375 +147 145 580 580 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 1375 +147 145 580 580 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 1 67 G4 4 1375 +147 145 1161/2 1161/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 0 84 C6 6 1373 +148 146 581 581 4.0 0 0 2/2 2 1 1 1 1 3 57 A3 3 1377 +148 146 581 581 4.0 0 0 2/2 2 1 1 1 1 0 60 C4 4 1377 +148 146 581 581 4.0 0 0 2/2 2 1 1 1 1 -1 65 F4 4 1377 +148 146 581 581 4.0 0 0 2/2 1 1 1 1 1 0 72 C5 5 1376 +148 146 581 581 4.0 0 0 2/2 1 1 1 1 1 -3 75 Eb5 5 1376 +148 146 581 581 4.0 0 0 2/2 1 1 1 1 1 0 84 C6 6 1376 +149 147 585 585 1.0 0 0 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 1380 +149 147 585 585 1.0 0 0 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 1380 +149 147 585 585 1.0 0 0 2/2 2 1 1/4 1/4 1 -1 65 F4 4 1380 +149 147 585 585 1.0 0 0 2/2 1 1 1/4 1/4 1 -2 70 Bb4 4 1378 +149 147 585 585 1.0 0 0 2/2 1 1 1/4 1/4 1 -5 73 Db5 5 1378 +149 147 585 585 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 77 F5 5 1378 +149 147 1177/2 1177/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 -2 58 Bb3 3 1381 +149 147 1177/2 1177/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 -5 61 Db4 4 1381 +149 147 1177/2 1177/2 0.5 7/8 7/8 2/2 2 1 1/8 1/8 1 -1 65 F4 4 1381 +149 147 1177/2 1177/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -2 70 Bb4 4 1379 +149 147 1177/2 1177/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -5 73 Db5 5 1379 +149 147 1177/2 1177/2 0.5 7/8 7/8 2/2 1 1 1/8 1/8 1 -2 82 Bb5 5 1379 +150 148 589 589 4.0 0 0 2/2 2 1 1 1 1 1 55 G3 3 1383 +150 148 589 589 4.0 0 0 2/2 2 1 1 1 1 -2 58 Bb3 3 1383 +150 148 589 589 4.0 0 0 2/2 2 1 1 1 1 -3 63 Eb4 4 1383 +150 148 589 589 4.0 0 0 2/2 1 1 1 1 1 -2 70 Bb4 4 1382 +150 148 589 589 4.0 0 0 2/2 1 1 1 1 1 -5 73 Db5 5 1382 +150 148 589 589 4.0 0 0 2/2 1 1 1 1 1 -2 82 Bb5 5 1382 +151 149 593 593 1.0 0 0 2/2 2 1 1/4 1/4 1 -4 56 Ab3 3 1386 +151 149 593 593 1.0 0 0 2/2 2 1 1/4 1/4 1 0 60 C4 4 1386 +151 149 593 593 1.0 0 0 2/2 2 1 1/4 1/4 1 -3 63 Eb4 4 1386 +151 149 593 593 1.0 0 0 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 1384 +151 149 593 593 1.0 0 0 2/2 1 1 1/4 1/4 1 0 72 C5 5 1384 +151 149 593 593 1.0 0 0 2/2 1 1 1/4 1/4 1 -3 75 Eb5 5 1384 +151 149 596 596 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -1 53 F3 3 1387 +151 149 596 596 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 1387 +151 149 596 596 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 1385 +151 149 596 596 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 0 72 C5 5 1385 +151 149 596 596 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -4 80 Ab5 5 1385 +152 150 597 597 1.0 0 0 2/2 2 1 1/4 1/4 1 1 55 G3 3 1392 +152 150 597 597 1.0 0 0 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 1392 +152 150 597 597 1.0 0 0 2/2 1 1 1/4 1/4 1 1 67 G4 4 1388 +152 150 597 597 1.0 0 0 2/2 1 1 1/4 1/4 1 -2 70 Bb4 4 1388 +152 150 597 597 1.0 0 0 2/2 1 1 1/4 1/4 1 -5 73 Db5 5 1388 +152 150 598 598 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 4 52 E3 3 1393 +152 150 598 598 1.0 1/4 1/4 2/2 2 1 1/4 1/4 1 0 60 C4 4 1393 +152 150 598 598 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 1 67 G4 4 1389 +152 150 598 598 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 -2 70 Bb4 4 1389 +152 150 598 598 1.0 1/4 1/4 2/2 1 1 1/4 1/4 1 1 79 G5 5 1389 +152 150 599 599 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 -1 53 F3 3 1394 +152 150 599 599 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 1394 +152 150 599 599 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -1 65 F4 4 1390 +152 150 599 599 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 1390 +152 150 599 599 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 0 72 C5 5 1390 +152 150 600 600 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -5 49 Db3 3 1395 +152 150 600 600 1.0 3/4 3/4 2/2 2 1 1/4 1/4 1 -5 61 Db4 4 1395 +152 150 600 600 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -1 65 F4 4 1391 +152 150 600 600 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 1391 +152 150 600 600 1.0 3/4 3/4 2/2 1 1 1/4 1/4 1 -1 77 F5 5 1391 +153 151 601 601 1.0 0 0 2/2 2 1 1/4 1/4 1 -2 46 Bb2 2 1398 +153 151 601 601 1.0 0 0 2/2 2 1 1/4 1/4 1 -5 49 Db3 3 1398 +153 151 601 601 1.0 0 0 2/2 2 1 1/4 1/4 1 1 55 G3 3 1398 +153 151 601 601 1.0 0 0 2/2 2 1 1/4 1/4 1 -2 58 Bb3 3 1398 +153 151 601 601 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 65 F4 4 1396 +153 151 601 601 1.0 0 0 2/2 1 1 1/4 1/4 1 1 67 G4 4 1396 +153 151 601 601 1.0 0 0 2/2 1 1 1/4 1/4 1 -5 73 Db5 5 1396 +153 151 601 601 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 77 F5 5 1396 +153 151 603 603 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 48 C3 3 1399 +153 151 603 603 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 4 52 E3 3 1399 +153 151 603 603 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 1 55 G3 3 1399 +153 151 603 603 1.0 1/2 1/2 2/2 2 1 1/4 1/4 1 0 60 C4 4 1399 +153 151 603 603 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 4 64 E4 4 1397 +153 151 603 603 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 1 67 G4 4 1397 +153 151 603 603 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 -2 70 Bb4 4 1397 +153 151 603 603 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 0 72 C5 5 1397 +153 151 603 603 1.0 1/2 1/2 2/2 1 1 1/4 1/4 1 4 76 E5 5 1397 +154 152 605 605 1.0 0 0 2/2 2 1 1/4 1/4 1 -1 41 F2 2 1401 +154 152 605 605 1.0 0 0 2/2 2 1 1/4 1/4 1 -4 44 Ab2 2 1401 +154 152 605 605 1.0 0 0 2/2 2 1 1/4 1/4 1 0 48 C3 3 1401 +154 152 605 605 1.0 0 0 2/2 2 1 1/4 1/4 1 -1 53 F3 3 1401 +154 152 605 605 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 65 F4 4 1400 +154 152 605 605 1.0 0 0 2/2 1 1 1/4 1/4 1 -4 68 Ab4 4 1400 +154 152 605 605 1.0 0 0 2/2 1 1 1/4 1/4 1 0 72 C5 5 1400 +154 152 605 605 1.0 0 0 2/2 1 1 1/4 1/4 1 -1 77 F5 5 1400 diff --git a/tests/test_dcml_import.py b/tests/test_dcml_import.py new file mode 100644 index 00000000..14068e7a --- /dev/null +++ b/tests/test_dcml_import.py @@ -0,0 +1,17 @@ +import unittest +from partitura import load_tsv +from tests import TSV_PATH +import os + + +class ImportDCMLAnnotations(unittest.TestCase): + def test_tsv_import_from_dcml(self): + note_path = os.path.join(TSV_PATH, "test_notes.tsv") + measure_path = os.path.join(TSV_PATH, "test_measures.tsv") + harmony_path = os.path.join(TSV_PATH, "test_harmonies.tsv") + score = load_tsv(note_path, measure_path, harmony_path) + self.assertEqual(len(score.parts), 1) + + +if __name__ == '__main__': + unittest.main() From 82da68dad620ed8420ebeb2c887b399e1a52c569 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 16 Jan 2024 18:27:30 +0100 Subject: [PATCH 030/197] Naive method to estimate triples. --- partitura/utils/music.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index c6fd9b02..15103c8c 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -939,7 +939,14 @@ def estimate_symbolic_duration(dur, div, eps=10**-3): if np.abs(qdur - DURS[i]) < eps: return SYM_DURS[i].copy() else: - return None + # Guess tuplets (Naive) + type = SYM_DURS[i+3]["type"] + return { + "type": type, + "actual_notes": int(1/qdur), + "normal_notes": 4, + } + def to_quarter_tempo(unit, tempo): From aa2f5d63447993bcaf24ce168f731713a2585b52 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 16 Jan 2024 18:27:37 +0100 Subject: [PATCH 031/197] added symbolic duration. --- partitura/io/importdcml.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 9f106d7a..6180b2e2 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -1,6 +1,7 @@ import numpy as np - +from math import ceil import partitura.score as spt +from partitura.utils.music import estimate_symbolic_duration try: import pandas as pd except ImportError: @@ -14,8 +15,8 @@ def read_note_tsv(note_tsv_path, metadata=None): # transform quarter_beats to quarter_divs qdivs = np.lcm.reduce(denominators) if len(denominators) > 0 else 4 quarter_durations = data["duration_qb"] - duration_div = np.array([int(qd * qdivs) for qd in quarter_durations]) - onset_div = np.array([int(qd * qdivs) for qd in data["quarterbeats"].apply(eval)]) + duration_div = np.array([ceil(qd * qdivs) for qd in quarter_durations]) + onset_div = np.array([ceil(qd * qdivs) for qd in data["quarterbeats"].apply(eval)]) flats = data["name"].str.contains("b") sharps = data["name"].str.contains("#") double_sharps = data["name"].str.contains("##") @@ -38,14 +39,16 @@ def read_note_tsv(note_tsv_path, metadata=None): # Add notes notes = note_array[~grace_mask] for note in notes: + symbolic_duration = estimate_symbolic_duration(note["duration_div"], qdivs) part.add( spt.Note( - id=note["id"], + id="n-{}".format(note["id"]), step=note["step"], octave=note["octave"], alter=note["alter"], staff=note["staff"], - voice=note["voice"] + voice=note["voice"], + symbolic_duration=symbolic_duration ), start=note["onset_div"], end=note["onset_div"]+note["duration_div"]) # Add Grace notes grace_notes = note_array[grace_mask] @@ -53,12 +56,13 @@ def read_note_tsv(note_tsv_path, metadata=None): part.add( spt.GraceNote( grace_type="grace", - id=grace_note["id"], + id="n-{}".format(grace_note["id"]), step=grace_note["step"], octave=grace_note["octave"], alter=grace_note["alter"], staff=grace_note["staff"], - voice=grace_note["voice"] + voice=grace_note["voice"], + symbolic_duration={"type": "eighth"} ), start=grace_note["onset_div"], end=grace_note["onset_div"] From 316d4eb75e56a9acfddff1a9099ba3cfdc580812 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jan 2024 09:49:14 +0100 Subject: [PATCH 032/197] Corrected import for grace notes. --- partitura/io/importdcml.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 6180b2e2..b216e84e 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -49,12 +49,24 @@ def read_note_tsv(note_tsv_path, metadata=None): staff=note["staff"], voice=note["voice"], symbolic_duration=symbolic_duration - ), start=note["onset_div"], end=note["onset_div"]+note["duration_div"]) + ), start=note["onset_div"], end=(note["onset_div"]+note["duration_div"])) # Add Grace notes - grace_notes = note_array[grace_mask] - for grace_note in grace_notes: - part.add( - spt.GraceNote( + grace_note_idxs = np.where(grace_mask)[0] + for grace_idx in grace_note_idxs: + grace_note = note_array[grace_idx] + next_note = note_array[grace_idx + 1] + prev_note = note_array[grace_idx - 1] + next_note_el, prev_note_el = None, None + for note in part.iter_all(spt.Note, grace_note["onset_div"], grace_note["onset_div"]+1): + if note.id == "n-{}".format(next_note["id"]): + next_note_el = note + break + for note in part.iter_all(spt.Note, prev_note["onset_div"], prev_note["onset_div"]+1): + if note.id == "n-{}".format(prev_note["id"]): + prev_note_el = note + break + + grace_el = spt.GraceNote( grace_type="grace", id="n-{}".format(grace_note["id"]), step=grace_note["step"], @@ -62,8 +74,12 @@ def read_note_tsv(note_tsv_path, metadata=None): alter=grace_note["alter"], staff=grace_note["staff"], voice=grace_note["voice"], - symbolic_duration={"type": "eighth"} - ), + symbolic_duration={"type": "eighth"}, + ) + grace_el.grace_next = next_note_el + grace_el.grace_prev = prev_note_el + part.add( + grace_el, start=grace_note["onset_div"], end=grace_note["onset_div"] ) @@ -77,6 +93,9 @@ def read_note_tsv(note_tsv_path, metadata=None): for ts, start, end in zip(time_signatures, start_divs, end_divs): part.add(spt.TimeSignature(beats=int(ts.split("/")[0]), beat_type=int(ts.split("/")[1])), start=start, end=end) + # Add default clefs for piano pieces (Naive) + part.add(spt.Clef(staff=1, sign="G", line=2, octave_change=0), start=0) + part.add(spt.Clef(staff=2, sign="F", line=4, octave_change=0), start=0) # TODO: Find Ties tied_notes = data["tied"].dropna() From 69ccd496c74a787812809532c2b652ab9b34f55f Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jan 2024 11:27:29 +0100 Subject: [PATCH 033/197] Corrected import for grace notes. --- partitura/io/importdcml.py | 46 +++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index b216e84e..2a6993cf 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -52,36 +52,30 @@ def read_note_tsv(note_tsv_path, metadata=None): ), start=note["onset_div"], end=(note["onset_div"]+note["duration_div"])) # Add Grace notes grace_note_idxs = np.where(grace_mask)[0] + grace_note_idxs = [] for grace_idx in grace_note_idxs: grace_note = note_array[grace_idx] - next_note = note_array[grace_idx + 1] - prev_note = note_array[grace_idx - 1] - next_note_el, prev_note_el = None, None - for note in part.iter_all(spt.Note, grace_note["onset_div"], grace_note["onset_div"]+1): - if note.id == "n-{}".format(next_note["id"]): - next_note_el = note - break - for note in part.iter_all(spt.Note, prev_note["onset_div"], prev_note["onset_div"]+1): - if note.id == "n-{}".format(prev_note["id"]): - prev_note_el = note - break - grace_el = spt.GraceNote( - grace_type="grace", - id="n-{}".format(grace_note["id"]), - step=grace_note["step"], - octave=grace_note["octave"], - alter=grace_note["alter"], - staff=grace_note["staff"], - voice=grace_note["voice"], - symbolic_duration={"type": "eighth"}, - ) - grace_el.grace_next = next_note_el - grace_el.grace_prev = prev_note_el + grace_type="grace", + id="n-{}".format(grace_note["id"]), + step=grace_note["step"], + octave=grace_note["octave"], + alter=grace_note["alter"], + staff=grace_note["staff"], + voice=grace_note["voice"], + symbolic_duration={"type": "eighth"}, + ) + + if grace_mask[grace_idx-1]: + prev_note = note_array[grace_idx-1] + for note in part.iter_all(spt.GraceNote, grace_note["onset_div"], grace_note["onset_div"]+1): + if note.id == "n-{}".format(prev_note["id"]): + grace_el.grace_prev = note + note.grace_next = grace_el + break + part.add( - grace_el, - start=grace_note["onset_div"], - end=grace_note["onset_div"] + grace_el, grace_note["onset_div"], grace_note["onset_div"] ) # Find time signatures From e3af96b0c194b157dab426cc109255d5403907d4 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jan 2024 11:27:45 +0100 Subject: [PATCH 034/197] Removed empty list. --- partitura/io/importdcml.py | 1 - 1 file changed, 1 deletion(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 2a6993cf..73520738 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -52,7 +52,6 @@ def read_note_tsv(note_tsv_path, metadata=None): ), start=note["onset_div"], end=(note["onset_div"]+note["duration_div"])) # Add Grace notes grace_note_idxs = np.where(grace_mask)[0] - grace_note_idxs = [] for grace_idx in grace_note_idxs: grace_note = note_array[grace_idx] grace_el = spt.GraceNote( From 84d01c654708660158977f3e98b2db4d6a392b47 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jan 2024 14:37:31 +0100 Subject: [PATCH 035/197] added support for tied notes. --- partitura/io/importdcml.py | 95 ++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 34 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 73520738..a5e5b51d 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -36,47 +36,61 @@ def read_note_tsv(note_tsv_path, metadata=None): note_array = data[["onset_div", "duration_div", "pitch", "step", "alter", "octave", "id", "staff", "voice"]].to_records(index=False) part = spt.Part("P0", "Metadata", quarter_duration=qdivs) - # Add notes - notes = note_array[~grace_mask] - for note in notes: - symbolic_duration = estimate_symbolic_duration(note["duration_div"], qdivs) - part.add( - spt.Note( + # Add notes and grace notes + for n_idx, note in enumerate(note_array): + if grace_mask[n_idx]: + # verify that staff and voice are the same for the grace note and the main note + note_el = spt.GraceNote( + grace_type="grace", id="n-{}".format(note["id"]), step=note["step"], octave=note["octave"], alter=note["alter"], staff=note["staff"], voice=note["voice"], - symbolic_duration=symbolic_duration - ), start=note["onset_div"], end=(note["onset_div"]+note["duration_div"])) - # Add Grace notes + symbolic_duration={"type": "eighth"}, + ) + + if grace_mask[n_idx - 1]: + prev_note = note_array[n_idx - 1] + for note_prev in part.iter_all(spt.GraceNote, note["onset_div"], note["onset_div"] + 1): + if note_prev.id == "n-{}".format(prev_note["id"]): + note_el.grace_prev = note_prev + note_prev.grace_next = note_el + break + else: + symbolic_duration = estimate_symbolic_duration(note["duration_div"], qdivs) + note_el = spt.Note( + id="n-{}".format(note["id"]), + step=note["step"], + octave=note["octave"], + alter=note["alter"], + staff=note["staff"], + voice=note["voice"], + symbolic_duration=symbolic_duration) + part.add(note_el, start=note["onset_div"], end=(note["onset_div"]+note["duration_div"])) + + # Curate grace notes grace_note_idxs = np.where(grace_mask)[0] - for grace_idx in grace_note_idxs: - grace_note = note_array[grace_idx] - grace_el = spt.GraceNote( - grace_type="grace", - id="n-{}".format(grace_note["id"]), - step=grace_note["step"], - octave=grace_note["octave"], - alter=grace_note["alter"], - staff=grace_note["staff"], - voice=grace_note["voice"], - symbolic_duration={"type": "eighth"}, - ) - - if grace_mask[grace_idx-1]: - prev_note = note_array[grace_idx-1] - for note in part.iter_all(spt.GraceNote, grace_note["onset_div"], grace_note["onset_div"]+1): - if note.id == "n-{}".format(prev_note["id"]): - grace_el.grace_prev = note - note.grace_next = grace_el + for i, grace_el in enumerate(part.iter_all(spt.GraceNote)): + grace_idx = grace_note_idxs[i] + note = note_array[grace_idx] + # Find the next note in the same staff and voice + if not grace_mask[grace_idx+1]: + i = 1 + next_note = note_array[grace_idx+i] + while note["staff"] != next_note["staff"] or note["voice"] != next_note["voice"]: + i += 1 + next_note = note_array[grace_idx+i] + assert note["staff"] == next_note["staff"], "Grace note and main note must be in the same staff" + assert note["voice"] == next_note["voice"], "Grace note and main note must be in the same voice" + assert note["onset_div"] == next_note[ + "onset_div"], "Grace note and main note must have the same onset" + for note in part.iter_all(spt.Note, note["onset_div"], note["onset_div"]+1): + if note.id == "n-{}".format(next_note["id"]): + grace_el.grace_next = note break - part.add( - grace_el, grace_note["onset_div"], grace_note["onset_div"] - ) - # Find time signatures time_signatures_changes = data["timesig"][data["timesig"].shift(1) != data["timesig"]].index time_signatures = data["timesig"][time_signatures_changes] @@ -89,8 +103,19 @@ def read_note_tsv(note_tsv_path, metadata=None): # Add default clefs for piano pieces (Naive) part.add(spt.Clef(staff=1, sign="G", line=2, octave_change=0), start=0) part.add(spt.Clef(staff=2, sign="F", line=4, octave_change=0), start=0) + # TODO: Find Ties - tied_notes = data["tied"].dropna() + tied_note_mask = data["tied"] == 1 + for tied_note in note_array[tied_note_mask]: + for note in part.iter_all(spt.Note, tied_note["onset_div"], tied_note["onset_div"]+1): + if note.id == "n-{}".format(tied_note["id"]): + for note_next in part.iter_all(spt.Note, note.end.t, note.end.t+1, mode="starting"): + if note_next.alter == note.alter and note_next.step == note.step and note_next.octave == note.octave: + assert note_next.voice == note.voice, "Tied notes must be in the same voice" + assert note_next.staff == note.staff, "Tied notes must be in the same staff" + note.tie_next = note_next + note_next.tie_prev = note + break return part @@ -103,7 +128,7 @@ def read_measure_tsv(measure_tsv_path, part): repeat_index = 0 for idx, row in data.iterrows(): - part.add(spt.Measure(), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) + part.add(spt.Measure(number=row["mc"], name=row["mn"]), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) # if row["repeat"] == "start": if row["repeats"] == "start": repeat_index = idx @@ -111,6 +136,8 @@ def read_measure_tsv(measure_tsv_path, part): # Find the previous repeat start start_times = data[repeat_index]["onset_div"] part.add(spt.Repeat(), start=start_times, end=row["onset_div"]) + part.add(spt.Fine(), start=part.last_point.t) + return def read_harmony_tsv(beat_tsv_path, part): From 94cc71febc019cea40e7199db85dbfe587cf2e43 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jan 2024 16:42:42 +0100 Subject: [PATCH 036/197] Voice re-indexing for correct musicxml export on single part with multiple staffs. --- partitura/io/importdcml.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index a5e5b51d..129207bb 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -33,6 +33,16 @@ def read_note_tsv(note_tsv_path, metadata=None): data["pitch"] = data["midi"] grace_mask = ~data["gracenote"].isna() data["id"] = np.arange(len(data)) + # Rewrite Voices for correct export + staffs = data["staff"].unique() + re_index_voice_value = 0 + for staff in staffs: + staff_mask = data["staff"] == staff + # add re_index_voice_value to the voice values of the staff + data.loc[staff_mask, "voice"] += re_index_voice_value + # update re_index_voice_value + re_index_voice_value = data.loc[staff_mask, "voice"].max() + note_array = data[["onset_div", "duration_div", "pitch", "step", "alter", "octave", "id", "staff", "voice"]].to_records(index=False) part = spt.Part("P0", "Metadata", quarter_duration=qdivs) From 0fc4122d2e47b224147f6c4cf4d4761a8f37116f Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jan 2024 19:02:18 +0100 Subject: [PATCH 037/197] Made import kern v2 as the main. --- partitura/io/__init__.py | 2 +- partitura/io/importkern_v2.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index 4ad2cd47..f3026a52 100644 --- a/partitura/io/__init__.py +++ b/partitura/io/__init__.py @@ -11,7 +11,7 @@ from .musescore import load_via_musescore from .importmatch import load_match from .importmei import load_mei -from .importkern import load_kern +from .importkern_v2 import load_kern from .importparangonada import load_parangonada_csv from .exportparangonada import save_parangonada_csv from .importmusic21 import load_music21 diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index c246de68..f278c0ab 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -160,7 +160,7 @@ def _handle_kern_with_spine_splitting(kern_path): # functions to initialize the kern parser -def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: +def load_kern(kern_path: PathLike) -> np.ndarray: """ Parses an KERN file from path to Part. @@ -170,8 +170,8 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: The path of the KERN document. Returns ------- - continuous_parts : numpy character array - non_continuous_parts : list + score : Score + The score object containing the parts. """ try: # This version of the parser is faster but does not support spine splitting. @@ -194,7 +194,8 @@ def parse_kern(kern_path: PathLike, num_workers=0) -> np.ndarray: # Get Splines splines = file[1:].T[note_parts] - + # Inverse Order + splines = splines[::-1] has_instrument = np.char.startswith(splines, "*I") # if all parts have the same instrument, then they are the same part. p_same_part = np.all(splines[has_instrument] == splines[has_instrument][0], axis=0) if np.any(has_instrument) else False @@ -606,6 +607,6 @@ def meta_chord_line(self, line): if __name__ == "__main__": kern_path = "/home/manos/Desktop/test.krn" - x = parse_kern(kern_path) + x = load_kern(kern_path) import partitura as pt pt.save_musicxml(x, "/home/manos/Desktop/test_kern.musicxml") \ No newline at end of file From 9b14bd4d9063c6e3fe128508cfc73529b08adecf Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jan 2024 19:02:33 +0100 Subject: [PATCH 038/197] Added a test on spline splitting. --- tests/data/kern/spline_splitting.krn | 39 ++++++++++++++++++++++++++++ tests/test_kern.py | 17 +++++++++--- 2 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 tests/data/kern/spline_splitting.krn diff --git a/tests/data/kern/spline_splitting.krn b/tests/data/kern/spline_splitting.krn new file mode 100644 index 00000000..564fd698 --- /dev/null +++ b/tests/data/kern/spline_splitting.krn @@ -0,0 +1,39 @@ +**kern **kern **kern **kern +*staff2 *staff2 *staff1 *staff1 +*clefF4 *clefF4 *clefG2 *clefG2 +*k[f#c#g#] *k[f#c#g#] *k[f#c#g#] *k[f#c#g#] +*M4/4 *M4/4 *M4/4 *M4/4 +4AA 4c# 4a 4ee +=1 =1 =1 =1 +* * * *^ +8AL 4c# 4a 4ee 8eee +8BJ . . . 8eee +8c#L 4c# 4a 4ee 8ee +8AJ . . . . +8DL 4d 4a 4ff# 8fff# +8EJ . . . 8fff# +8F#L 4d 4a 4ff# 4fff# +8DJ . . . . +=2 =2 =2 =2 =2 +* * * *v *v +2A; 2c#; 2a; 2ee; +4r 4r 4r 4r +4A 4e 4a 4cc# +=3 =3 =3 =3 +4G# 4e 4b 4dd +4A 4e 4a 4cc# +8EL 4e 4g# 4b +8DJ . . . +8C#L 4e [4a 8.cc#L +8AAJ . . . +. . . 16ddJ +=4 =4 =4 =4 +*^ * * *v +2.EE 2E 8eL 8a]L 2b +. . 16d 8f#J . +. . 16c#J . . +. . 4d 4g# . +. 4AA; 4c#; 4e; 4a; +=:|! =:|! =:| =:| =:|! +*v *v * * * +*- *- *- *- diff --git a/tests/test_kern.py b/tests/test_kern.py index 86cfb3f1..228afbdc 100644 --- a/tests/test_kern.py +++ b/tests/test_kern.py @@ -6,7 +6,8 @@ import unittest import partitura -from tests import KERN_TESTFILES, KERN_TIES +import os +from tests import KERN_TESTFILES, KERN_TIES, KERN_PATH from partitura.score import merge_parts from partitura.utils import ensure_notearray from partitura.io.importkern import load_kern @@ -32,17 +33,25 @@ def test_example_kern(self): def test_examples(self): for fn in KERN_TESTFILES: - part = merge_parts(load_kern(fn)) - ka = ensure_notearray(part) + score = load_kern(fn) self.assertTrue(True) def test_tie_mismatch(self): fn = KERN_TIES[0] - part = merge_parts(load_kern(fn)) + score = load_kern(fn) self.assertTrue(True) + def test_spline_splitting(self): + file_path = os.path.join(KERN_PATH, "spline_splitting.krn") + score = load_kern(file_path) + num_parts = 4 + voices_per_part = [2, 1, 1, 2] + self.assertTrue(num_parts == len(score.parts)) + for i, part in enumerate(score.parts): + vn = part.note_array()["voice"].max() + self.assertTrue(voices_per_part[i] == vn) # if __name__ == "__main__": # unittest.main() From de3604ff97be59d518b68d8f662c13f90f88b856 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 18 Jan 2024 12:22:25 +0100 Subject: [PATCH 039/197] Corrections in tests. --- partitura/io/importkern_v2.py | 14 ++++--- tests/data/kern/spline_splitting.krn | 42 +++++++++---------- tests/data/kern/voice_dublifications.krn | 52 ++++++++++++------------ tests/test_kern.py | 2 +- 4 files changed, 57 insertions(+), 53 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index f278c0ab..34848504 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -196,11 +196,13 @@ def load_kern(kern_path: PathLike) -> np.ndarray: 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. p_same_part = np.all(splines[has_instrument] == splines[has_instrument][0], axis=0) if np.any(has_instrument) else False - for i, spline in enumerate(splines): - parser = SplineParser(size=spline.shape[-1], id="P{}".format(parsing_idxs[i]) if not p_same_part else "P1") + for j, spline in enumerate(splines): + parser = SplineParser(size=spline.shape[-1], id="P{}".format(parsing_idxs[j]) if not p_same_part else "P{}".format(j), staff=prev_staff) same_part = False if parser.id in [p.id for p in parts]: same_part = True @@ -222,8 +224,10 @@ def load_kern(kern_path: PathLike) -> np.ndarray: else: has_staff = np.char.startswith(spline, "*staff") staff = int(spline[has_staff][0][6:]) if np.count_nonzero(has_staff) else 1 + # Correction for currating multiple staffs. if parser.staff != staff: parser.staff = staff + prev_staff = staff elements = parser.parse(spline) unique_durs = np.unique(parser.total_duration_values).astype(int) divs_pq = np.lcm.reduce(unique_durs) @@ -301,11 +305,11 @@ def __init__(self, id="P1", staff=1, voice=1, size=1, name=""): def parse(self, spline): # Remove "-" lines - spline = spline[spline != "-"] + spline = spline[spline != '-'] # Remove "." lines - spline = spline[spline != "."] + spline = spline[spline != '.'] # Remove Empty lines - spline = spline[spline != ""] + spline = spline[spline != ''] # Remove None lines spline = spline[spline != None] # Remove lines that start with "!" diff --git a/tests/data/kern/spline_splitting.krn b/tests/data/kern/spline_splitting.krn index 564fd698..a7a5bc5c 100644 --- a/tests/data/kern/spline_splitting.krn +++ b/tests/data/kern/spline_splitting.krn @@ -1,21 +1,21 @@ -**kern **kern **kern **kern +**kern **kern **kern **kern *staff2 *staff2 *staff1 *staff1 *clefF4 *clefF4 *clefG2 *clefG2 *k[f#c#g#] *k[f#c#g#] *k[f#c#g#] *k[f#c#g#] *M4/4 *M4/4 *M4/4 *M4/4 4AA 4c# 4a 4ee =1 =1 =1 =1 -* * * *^ -8AL 4c# 4a 4ee 8eee -8BJ . . . 8eee -8c#L 4c# 4a 4ee 8ee -8AJ . . . . -8DL 4d 4a 4ff# 8fff# -8EJ . . . 8fff# -8F#L 4d 4a 4ff# 4fff# -8DJ . . . . -=2 =2 =2 =2 =2 -* * * *v *v +* * * *^ +8AL 4c# 4a 4ee 8eee +8BJ . . . 8eee +8c#L 4c# 4a 4ee 8ee +8AJ . . . . +8DL 4d 4a 4ff# 8fff# +8EJ . . . 8fff# +8F#L 4d 4a 4ff# 4fff# +8DJ . . . . +=2 =2 =2 =2 =2 +* * * *v *v 2A; 2c#; 2a; 2ee; 4r 4r 4r 4r 4A 4e 4a 4cc# @@ -28,12 +28,12 @@ 8AAJ . . . . . . 16ddJ =4 =4 =4 =4 -*^ * * *v -2.EE 2E 8eL 8a]L 2b -. . 16d 8f#J . -. . 16c#J . . -. . 4d 4g# . -. 4AA; 4c#; 4e; 4a; -=:|! =:|! =:| =:| =:|! -*v *v * * * -*- *- *- *- +*^ * * * +2.EE 2E 8eL 8a]L 2b +. . 16d 8f#J . +. . 16c#J . . +. . 4d 4g# . +. 4AA; 4c#; 4e; 4a; +=:|! =:|! =:| =:| =:|! +*v *v * * * +*- *- *- *- \ No newline at end of file diff --git a/tests/data/kern/voice_dublifications.krn b/tests/data/kern/voice_dublifications.krn index 59386a7c..1a33e233 100644 --- a/tests/data/kern/voice_dublifications.krn +++ b/tests/data/kern/voice_dublifications.krn @@ -1,29 +1,29 @@ -**kern **kern **kern **kern -*staff2 *staff2 *staff1 *staff1 -*clefF4 *clefF4 *clefG2 *clefG2 -*k[f#c#] *k[f#c#] *k[f#c#] *k[f#c#] -*M4/4 *M4/4 *M4/4 *M4/4 -*b: *b: *b: *b: -*MM58 *MM58 *MM58 *MM58 -=1- =1- =1- =1- -[1F# 16A#L 8r 2dd -. 16F# . . -. 16G# 8f# . -. 16A#J . . -. 16BL 8dL . -. 16c# . . -. 16d 8BJ . -. [16BJ . . -. 16B]L 8gL [2cc# -. 16B . . -. 16A# 8f#J . -. 16BJ . . -. 16c#L 8bL . -. 16d . . -. 16e 8a#J . -. 16c#J . . -=2 =2 =2 =2 -*^ * * * +**kern **kern **kern **kern +*staff2 *staff2 *staff1 *staff1 +*clefF4 *clefF4 *clefG2 *clefG2 +*k[f#c#] *k[f#c#] *k[f#c#] *k[f#c#] +*M4/4 *M4/4 *M4/4 *M4/4 +*b: *b: *b: *b: +*MM58 *MM58 *MM58 *MM58 +=1- =1- =1- =1- +[1F# 16A#L 8r 2dd +. 16F# . . +. 16G# 8f# . +. 16A#J . . +. 16BL 8dL . +. 16c# . . +. 16d 8BJ . +. [16BJ . . +. 16B]L 8gL [2cc# +. 16B . . +. 16A# 8f#J . +. 16BJ . . +. 16c#L 8bL . +. 16d . . +. 16e 8a#J . +. 16c#J . . +=2 =2 =2 =2 +*^ * * * 2r 16F#]L 8eL 4f# 4cc#] . 16AA# . . . . 16BB 8d#J . . diff --git a/tests/test_kern.py b/tests/test_kern.py index 228afbdc..2c8c3703 100644 --- a/tests/test_kern.py +++ b/tests/test_kern.py @@ -10,7 +10,7 @@ from tests import KERN_TESTFILES, KERN_TIES, KERN_PATH from partitura.score import merge_parts from partitura.utils import ensure_notearray -from partitura.io.importkern import load_kern +from partitura.io.importkern_v2 import load_kern from partitura import load_musicxml import numpy as np From 91ee92a1a34a72bd239aff4e87bb009ab8a52b32 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 18 Jan 2024 12:29:48 +0100 Subject: [PATCH 040/197] Complete the import kern pipeline. --- partitura/__init__.py | 2 +- partitura/io/importkern_v2.py | 27 ++++++++++++++++++++------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/partitura/__init__.py b/partitura/__init__.py index c624b578..23eebe53 100644 --- a/partitura/__init__.py +++ b/partitura/__init__.py @@ -13,7 +13,7 @@ from .io.importmusicxml import load_musicxml, musicxml_to_notearray from .io.exportmusicxml import save_musicxml from .io.importmei import load_mei -from .io.importkern import load_kern +from .io.importkern_v2 import load_kern from .io.importmusic21 import load_music21 from .io.importmidi import load_score_midi, load_performance_midi, midi_to_notearray from .io.exportmidi import save_score_midi, save_performance_midi diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index 34848504..faa7c8ce 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -11,7 +11,7 @@ import numpy as np from math import inf, ceil import partitura.score as spt -from partitura.utils import PathLike +from partitura.utils import PathLike, get_document_name SIGN_TO_ACC = { @@ -160,29 +160,34 @@ def _handle_kern_with_spine_splitting(kern_path): # functions to initialize the kern parser -def load_kern(kern_path: PathLike) -> np.ndarray: +def load_kern( + filename: PathLike, + force_note_ids: Optional[Union[bool, str]] = None, +) -> spt.Score: """ Parses an KERN file from path to Part. Parameters ---------- - kern_path : PathLike + filename : PathLike The path of the KERN document. + force_note_ids : (None, bool or "keep") + When True each Note in the returned Part(s) will have a newly assigned unique id attribute. Returns ------- - score : Score + score : partitura.score.Score The score object containing the parts. """ try: # This version of the parser is faster but does not support spine splitting. - file = np.loadtxt(kern_path, dtype=str, delimiter="\t", comments="!!", encoding="utf-8") + file = np.loadtxt(filename, dtype=str, delimiter="\t", comments="!!", encoding="utf-8") parsing_idxs = np.arange(file.shape[0]) # Decide Parts except ValueError: # This version of the parser supports spine splitting but is slower. - file, parsing_idxs = _handle_kern_with_spine_splitting(kern_path) + file, parsing_idxs = _handle_kern_with_spine_splitting(filename) parts = [] @@ -280,7 +285,15 @@ def load_kern(kern_path: PathLike) -> np.ndarray: for part in parts: part.set_quarter_duration(0, divs_pq) - return spt.Score(parts) + partlist = parts + + spt.assign_note_ids( + partlist, keep=(force_note_ids is True or force_note_ids == "keep") + ) + + doc_name = get_document_name(filename) + score = spt.Score(partlist=partlist, id=doc_name) + return score def rec_divisible_by_two(number): From a8ddf2c389699dc33da6dd0dd8128db06fa181ce Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 18 Jan 2024 12:42:22 +0100 Subject: [PATCH 041/197] Minor typo correction. --- tests/data/kern/spline_splitting.krn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/kern/spline_splitting.krn b/tests/data/kern/spline_splitting.krn index a7a5bc5c..ee163585 100644 --- a/tests/data/kern/spline_splitting.krn +++ b/tests/data/kern/spline_splitting.krn @@ -1,4 +1,4 @@ -**kern **kern **kern **kern +**kern **kern **kern **kern *staff2 *staff2 *staff1 *staff1 *clefF4 *clefF4 *clefG2 *clefG2 *k[f#c#g#] *k[f#c#g#] *k[f#c#g#] *k[f#c#g#] From 8af8faa43ae334e209e16bee744d540dfeaae300 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 18 Jan 2024 12:42:40 +0100 Subject: [PATCH 042/197] Variable renaming. --- partitura/io/importkern_v2.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index faa7c8ce..3609c5da 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -190,7 +190,7 @@ def load_kern( file, parsing_idxs = _handle_kern_with_spine_splitting(filename) - parts = [] + partlist = [] # Get Main Number of parts and Spline Types spline_types = file[0] @@ -209,10 +209,10 @@ def load_kern( for j, spline in enumerate(splines): parser = SplineParser(size=spline.shape[-1], id="P{}".format(parsing_idxs[j]) if not p_same_part else "P{}".format(j), staff=prev_staff) same_part = False - if parser.id in [p.id for p in parts]: + if parser.id in [p.id for p in partlist]: same_part = True warnings.warn("Part {} already exists. Adding to previous Part.".format(parser.id)) - part = [p for p in parts if p.id == parser.id][0] + part = [p for p in partlist if p.id == parser.id][0] has_staff = np.char.startswith(spline, "*staff") staff = int(spline[has_staff][0][6:]) if np.count_nonzero(has_staff) else 1 if parser.staff != staff: @@ -277,16 +277,14 @@ def load_kern( if part.measures[0].start.t != 0: part.add(spt.Measure(number=0), start=0, end=part.measures[0].start.t) - if parser.id not in [p.id for p in parts]: - parts.append(part) + if parser.id not in [p.id for p in partlist]: + partlist.append(part) # currate parts to the same divs per quarter - divs_pq = np.lcm.reduce([p._quarter_durations[0] for p in parts]) - for part in parts: + divs_pq = np.lcm.reduce([p._quarter_durations[0] for p in partlist]) + for part in partlist: part.set_quarter_duration(0, divs_pq) - partlist = parts - spt.assign_note_ids( partlist, keep=(force_note_ids is True or force_note_ids == "keep") ) From ee0646dcbbdc1606fe6931dd339d8da59749987b Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 18 Jan 2024 14:45:22 +0100 Subject: [PATCH 043/197] Removed curration for different divs among parts. --- partitura/io/importkern_v2.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index 3609c5da..8fecb833 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -234,7 +234,14 @@ def load_kern( parser.staff = staff prev_staff = staff elements = parser.parse(spline) - unique_durs = np.unique(parser.total_duration_values).astype(int) + # Routine to filter out non integer durations + unique_durs = np.unique(parser.total_duration_values) + d_mul = 1 + while not np.all(np.isclose(unique_durs % 1, 0)): + unique_durs = unique_durs * d_mul + d_mul += 1 + unique_durs = unique_durs.astype(int) + divs_pq = np.lcm.reduce(unique_durs) divs_pq = divs_pq if divs_pq > 4 else 4 # Initialize Part @@ -281,9 +288,9 @@ def load_kern( partlist.append(part) # currate parts to the same divs per quarter - divs_pq = np.lcm.reduce([p._quarter_durations[0] for p in partlist]) - for part in partlist: - part.set_quarter_duration(0, divs_pq) + # divs_pq = np.lcm.reduce([p._quarter_durations[0] for p in partlist]) + # for part in partlist: + # part.set_quarter_duration(0, divs_pq) spt.assign_note_ids( partlist, keep=(force_note_ids is True or force_note_ids == "keep") From 0d578ebd568ef101204432bdfbd3578fc2f677f5 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 18 Jan 2024 14:45:38 +0100 Subject: [PATCH 044/197] updated test --- tests/test_note_array.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_note_array.py b/tests/test_note_array.py index 54cf5770..721f3e60 100644 --- a/tests/test_note_array.py +++ b/tests/test_note_array.py @@ -175,9 +175,9 @@ def test_ensure_na_different_divs(self): # note_arrays = [p.note_array(include_divs_per_quarter= True) for p in parts] merged_note_array = ensure_notearray(parts) for note in merged_note_array[-4:]: - self.assertTrue(note["onset_div"] == 368) - self.assertTrue(note["duration_div"] == 16) - self.assertTrue(note["divs_pq"] == 16) + self.assertTrue(note["onset_div"] == 2208) + self.assertTrue(note["duration_div"] == 96) + self.assertTrue(note["divs_pq"] == 96) def test_score_notearray_method(self): """ From 37624886f02083ca82b821b8761f69e8998f8bfa Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 22 Jan 2024 12:17:02 +0100 Subject: [PATCH 045/197] Added partial support for slurs. --- partitura/io/importkern_v2.py | 41 +++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index 8fecb833..f6fb9570 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -196,7 +196,6 @@ def load_kern( # Find parsable parts if they start with "**kern" or "**notes" note_parts = np.char.startswith(spline_types, "**kern") | np.char.startswith(spline_types, "**notes") - # Get Splines splines = file[1:].T[note_parts] # Inverse Order @@ -236,8 +235,10 @@ def load_kern( elements = parser.parse(spline) # Routine to filter out non integer durations unique_durs = np.unique(parser.total_duration_values) - d_mul = 1 - while not np.all(np.isclose(unique_durs % 1, 0)): + # Remove all infinite values + unique_durs = unique_durs[np.isfinite(unique_durs)] + d_mul = 2 + while not np.all(np.isclose(unique_durs % 1, 0.0)): unique_durs = unique_durs * d_mul d_mul += 1 unique_durs = unique_durs.astype(int) @@ -267,6 +268,11 @@ def load_kern( for note in element[1]: part.add(note, start=current_tl_pos, end=el_end) current_tl_pos = el_end + elif isinstance(element, spt.Slur): + start_sl = element.start_note.start.t + end_sl = element.end_note.start.t + part.add(element, start=start_sl, end=end_sl) + else: # Do not repeat structural elements if they are being added to the same part. if not same_part: @@ -320,6 +326,8 @@ def __init__(self, id="P1", staff=1, voice=1, size=1, name=""): self.total_parsed_elements = 0 self.tie_prev = None self.tie_next = None + self.slurs_start = [] + self.slurs_end = [] def parse(self, spline): # Remove "-" lines @@ -368,8 +376,23 @@ def parse(self, spline): 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 ")" + open_slur_mask = np.char.find(spline[note_mask], "(") != -1 + close_slur_mask = np.char.find(spline[note_mask], ")") != -1 + self.slurs_start = np.where(open_slur_mask)[0] + self.slurs_end = np.where(close_slur_mask)[0] + # Only add slur if there is a start and end + if len(self.slurs_start) == len(self.slurs_end): + slurs = np.empty(len(self.slurs_start), dtype=object) + for i, (start, end) in enumerate(zip(self.slurs_start, self.slurs_end)): + slurs[i] = spt.Slur(notes[start], notes[end]) + # Add slurs to elements + elements = np.append(elements, slurs) + else: + warnings.warn("Slurs openings and closings do not match. Skipping parsing slurs for this part {}.".format(self.id)) + return elements def meta_tandem_line(self, line): @@ -627,8 +650,8 @@ def meta_chord_line(self, line): return chord -if __name__ == "__main__": - kern_path = "/home/manos/Desktop/test.krn" - x = load_kern(kern_path) - import partitura as pt - pt.save_musicxml(x, "/home/manos/Desktop/test_kern.musicxml") \ No newline at end of file +# if __name__ == "__main__": +# kern_path = "/home/manos/Desktop/test.krn" +# x = load_kern(kern_path) +# import partitura as pt +# pt.save_musicxml(x, "/home/manos/Desktop/test_kern.musicxml") \ No newline at end of file From 2aa13e3ebb1beef58f04c87809498d72a3ce211c Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 22 Jan 2024 18:16:04 +0100 Subject: [PATCH 046/197] Process primary degree update. --- partitura/score.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index 010e9c3c..4b41a233 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -2775,9 +2775,14 @@ def _process_primary_degree(self): """ # The primary degree should be a roman numeral between 1 and 7. # If there is no primary degree, return None - primary_degree = re.findall(r'[ivIV]+', self.text) - if len(primary_degree) > 0: - return primary_degree[0] + roman_text = self.text.split(":")[-1] + if "7" in roman_text or "65" in roman_text or "43" in roman_text or "2" in roman_text: + add_on = "7" + else: + add_on = "" + primary_degree = re.findall(r'[a-zA-Z+]+', roman_text) + if primary_degree: + return primary_degree.group(0) + add_on return None def _process_secondary_degree(self): From cf81302c0c26d9da895c952e39f5557d80ab5cd8 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 23 Jan 2024 15:29:36 +0100 Subject: [PATCH 047/197] Minor Fixes on accidentals and dotted breve durations. --- partitura/io/importkern_v2.py | 54 +++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index f6fb9570..4d5f61b9 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -11,15 +11,18 @@ import numpy as np from math import inf, ceil import partitura.score as spt -from partitura.utils import PathLike, get_document_name +from partitura.utils import PathLike, get_document_name, symbolic_to_numeric_duration SIGN_TO_ACC = { + "nn": 0, "n": 0, "#": 1, "s": 1, "ss": 2, "x": 2, + "n#": 1, + "#n": 1, "##": 2, "###": 3, "b": -1, @@ -28,6 +31,8 @@ "ff": -2, "bbb": -3, "-": -1, + "n-": -1, + "-n": -1, "--": -2, } @@ -71,6 +76,8 @@ def add_durations(a, b): def dot_function(duration, dots): if dots == 0: return duration + elif duration == 0: + return 0 else: return add_durations((2**dots)*duration, dot_function(duration, dots - 1)) @@ -106,14 +113,14 @@ def parse_by_voice(file, dtype=np.object_): def _handle_kern_with_spine_splitting(kern_path): - org_file = np.loadtxt(kern_path, dtype=str, delimiter="\n", comments="!!", encoding="utf-8") + org_file = np.loadtxt(kern_path, dtype="U", delimiter="\n", comments="!!!", encoding="cp437") # Get Main Number of parts and Spline Types spline_types = org_file[0].split("\t") parsing_idxs = [] dtype = org_file.dtype data = [] file = org_file.tolist() - file = [line.split("\t") for line in file] + file = [line.split("\t") for line in file if not line.startswith("!")] continue_parsing = True for i in range(len(spline_types)): # Parse by voice @@ -180,7 +187,7 @@ def load_kern( """ try: # This version of the parser is faster but does not support spine splitting. - file = np.loadtxt(filename, dtype=str, delimiter="\t", comments="!!", encoding="utf-8") + file = np.loadtxt(filename, dtype="U", delimiter="\t", comments="!!", encoding="cp437") parsing_idxs = np.arange(file.shape[0]) # Decide Parts @@ -255,15 +262,18 @@ def load_kern( if element is None: continue if isinstance(element, spt.GenericNote): - quarter_duration = 4 / parser.total_duration_values[i] - duration_divs = ceil(quarter_duration*divs_pq) + if parser.total_duration_values[i] == 0: + duration_divs = symbolic_to_numeric_duration(element.symbolic_duration, divs_pq) + else: + quarter_duration = 4 / parser.total_duration_values[i] + duration_divs = ceil(quarter_duration*divs_pq) el_end = current_tl_pos + duration_divs part.add(element, start=current_tl_pos, end=el_end) current_tl_pos = el_end elif isinstance(element, tuple): # Chord quarter_duration = 4 / parser.total_duration_values[i] - duration_divs = int(part.inv_quarter_map(quarter_duration)) + duration_divs = ceil(quarter_duration*divs_pq) el_end = current_tl_pos + duration_divs for note in element[1]: part.add(note, start=current_tl_pos, end=el_end) @@ -351,6 +361,7 @@ def parse(self, spline): elements[bar_mask] = np.vectorize(self.meta_barline_line, otypes=[object])(spline[bar_mask]) # Find Chord indices, i.e. where spline cells contain " " chord_mask = np.char.find(spline, " ") != -1 + chord_mask = np.logical_and(chord_mask, np.logical_and(~tandem_mask, ~bar_mask)) self.total_parsed_elements = -1 self.note_duration_values = np.ones(len(spline[chord_mask])) chord_num = np.count_nonzero(chord_mask) @@ -476,12 +487,23 @@ def process_staff_line(self, line): def process_clef_line(self, line): # if the cleff line does not contain any of the following characters, ["G", "F", "C"], raise a ValueError. if not any(c in line for c in ["G", "F", "C"]): - raise ValueError("Unrecognized clef line: {}".format(line)) + raise ValueError("Unrecognized clef: {}".format(line)) # find the clef clef = re.search(r"([GFC])", line).group(0) # find the octave - line = re.search(r"([0-9])", line).group(0) - return spt.Clef(sign=clef, staff=self.staff, line=int(line), octave_change=0) + has_line = re.search(r"([0-9])", line) + if has_line is None: + if clef == "G": + clef_line = 2 + elif clef == "F": + clef_line = 4 + elif clef == "C": + clef_line = 3 + else: + raise ValueError("Unrecognized clef line: {}".format(line)) + else: + clef_line = has_line.group(0) + return spt.Clef(sign=clef, staff=self.staff, line=int(clef_line), octave_change=0) def process_key_signature_line(self, line): fifths = line.count("#") - line.count("-") @@ -496,7 +518,10 @@ def process_key_signature_line(self, line): def process_meter_line(self, line): if " " in line: line = line.split(" ")[0] - numerator, denominator = map(eval, line.split("/")) + numerator, denominator = line.split("/") + # Find digits in numerator and denominator and convert to int + numerator = int(re.search(r"([0-9]+)", numerator).group(0)) + denominator = int(re.search(r"([0-9]+)", denominator).group(0)) return spt.TimeSignature(numerator, denominator) def _process_kern_pitch(self, pitch): @@ -586,7 +611,12 @@ def meta_note_line(self, line, voice=None, add=True): self.total_parsed_elements += 1 if add else 0 voice = self.voice if voice is None else voice # extract first occurence of one of the following: a-g A-G r # - n - pitch = re.search(r"([a-gA-Gr\-n#]+)", line).group(0) + find_pitch = re.search(r"([a-gA-Gr\-n#]+)", line) + if find_pitch is None: + warnings.warn("No pitch found in line: {}, transforming to a rest".format(line)) + pitch = "r" + else: + pitch = find_pitch.group(0) # extract duration can be any of the following: 0-9 . dur_search = re.search(r"([0-9.]+)", line) # if no duration is found, then the duration is 8 by default (for grace notes with no duration) From 0e14e59b6bcdbf012c3ed278f49e97bd405143f8 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 23 Jan 2024 15:46:42 +0100 Subject: [PATCH 048/197] copy musicxml export to new version of mei export. --- partitura/io/mei_export_v2.py | 1100 +++++++++++++++++++++++++++++++++ 1 file changed, 1100 insertions(+) create mode 100644 partitura/io/mei_export_v2.py diff --git a/partitura/io/mei_export_v2.py b/partitura/io/mei_export_v2.py new file mode 100644 index 00000000..e7891c87 --- /dev/null +++ b/partitura/io/mei_export_v2.py @@ -0,0 +1,1100 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains methods for exporting MEI files. +""" +import math +from collections import defaultdict +from lxml import etree +import partitura.score as score +from operator import itemgetter +from typing import Optional +from partitura.utils import partition, iter_current_next, to_quarter_tempo + +from partitura.utils.misc import deprecated_alias, PathLike + + + +__all__ = ["save_mei"] + +DOCTYPE = """""" # noqa: E501 +MEASURE_SEP_COMMENT = "=======================================================" +ARTICULATIONS = [ + "accent", + "breath-mark", + "caesura", + "detached-legato", + "doit", + "falloff", + "plop", + "scoop", + "spiccato", + "staccatissimo", + "staccato", + "stress", + "strong-accent", + "tenuto", + "unstress", +] + +NOTE_TYPE = { + 'whole': '1', + 'half': '2', + 'quarter': '4', + 'eighth': '8', + 'sixteenth': '16', + 'long': 'long', + 'breve': 'breve' + } + +# musicxml integer accidentals to mei +# integer of accidental is array index +ACC = [None, 's', 'ss', 'ff', 'f'] + + +def range_number_from_counter(e, label, counter): + key = (label, e) + number = counter.get(key, None) + + if number is None: + number = 1 + sum(1 for o in counter.keys() if o[0] == label) + assert number is not None + counter[key] = number + + else: + del counter[key] + + return number + + +def filter_string(s): + """ + Make (unicode) string fit for passing it to lxml, which means (at least) + removing null characters. + """ + return s.replace("\x00", "") + + +def make_note_el(note, dur, voice, counter, n_of_staves): + # child order + # | | + # + # + # + # + # + # + + note_e = etree.Element("note") + + if note.id is not None: + note_id = note.id + # make sure note_id is unique by appending _x to the note_id for the + # x-th repetition of the id + counter[note_id] = counter.get(note_id, 0) + 1 + + if counter[note_id] > 1: + note_id += "_{}".format(counter[note_id]) + + note_e.attrib["id"] = filter_string(note_id) + + if isinstance(note, score.Note): + if isinstance(note, score.GraceNote): + if note.grace_type == "acciaccatura": + etree.SubElement(note_e, "grace", slash="yes") + + else: + etree.SubElement(note_e, "grace") + + pitch_e = etree.SubElement(note_e, "pitch") + + etree.SubElement(pitch_e, "step").text = "{}".format(note.step) + + if note.alter not in (None, 0): + etree.SubElement(pitch_e, "alter").text = "{}".format(note.alter) + + etree.SubElement(pitch_e, "octave").text = "{}".format(note.octave) + + elif isinstance(note, score.UnpitchedNote): + unpitch_e = etree.SubElement(note_e, "unpitched") + + etree.SubElement(unpitch_e, "display-step").text = "{}".format(note.step) + + etree.SubElement(unpitch_e, "display-octave").text = "{}".format(note.octave) + + if note.notehead is not None: + nh_e = etree.SubElement(note_e, "notehead") + nh_e.text = "{}".format(note.notehead) + if note.noteheadstyle: + nh_e.attrib["filled"] = "yes" + else: + nh_e.attrib["filled"] = "no" + + elif isinstance(note, score.Rest): + if not note.hidden: + etree.SubElement(note_e, "rest") + + if not isinstance(note, score.GraceNote): + duration_e = etree.SubElement(note_e, "duration") + duration_e.text = "{:d}".format(int(dur)) + + notations = [] + + if note.tie_prev is not None: + etree.SubElement(note_e, "tie", type="stop") + notations.append(etree.Element("tied", type="stop")) + + if note.tie_next is not None: + etree.SubElement(note_e, "tie", type="start") + notations.append(etree.Element("tied", type="start")) + + if voice not in (None, 0): + etree.SubElement(note_e, "voice").text = "{}".format(voice) + + if note.fermata is not None: + notations.append(etree.Element("fermata")) + + if note.articulations: + articulations = [] + for articulation in note.articulations: + if articulation in ARTICULATIONS: + articulations.append(etree.Element(articulation)) + if articulations: + articulations_e = etree.Element("articulations") + articulations_e.extend(articulations) + notations.append(articulations_e) + + sym_dur = note.symbolic_duration or {} + + if sym_dur.get("type") is not None: + etree.SubElement(note_e, "type").text = sym_dur["type"] + + for i in range(sym_dur.get("dots", 0)): + etree.SubElement(note_e, "dot") + + if ( + sym_dur.get("actual_notes") is not None + and sym_dur.get("normal_notes") is not None + ): + time_mod_e = etree.SubElement(note_e, "time-modification") + actual_e = etree.SubElement(time_mod_e, "actual-notes") + actual_e.text = str(sym_dur["actual_notes"]) + normal_e = etree.SubElement(time_mod_e, "normal-notes") + normal_e.text = str(sym_dur["normal_notes"]) + + if note.staff is not None: + if note.staff != 1 or n_of_staves > 1: + etree.SubElement(note_e, "staff").text = "{}".format(note.staff) + + for slur in note.slur_stops: + number = range_number_from_counter(slur, "slur", counter) + + notations.append(etree.Element("slur", number="{}".format(number), type="stop")) + + for slur in note.slur_starts: + number = range_number_from_counter(slur, "slur", counter) + + notations.append( + etree.Element("slur", number="{}".format(number), type="start") + ) + + for tuplet in note.tuplet_stops: + tuplet_key = ("tuplet", tuplet) + number = counter.get(tuplet_key, None) + + if number is None: + number = 1 + counter[tuplet_key] = number + + else: + del counter[tuplet_key] + + notations.append( + etree.Element("tuplet", number="{}".format(number), type="stop") + ) + + for tuplet in note.tuplet_starts: + tuplet_key = ("tuplet", tuplet) + number = counter.get(tuplet_key, None) + + if number is None: + number = 1 + sum(1 for o in counter.keys() if o[0] == "tuplet") + counter[tuplet_key] = number + + else: + del counter[tuplet_key] + + notations.append( + etree.Element("tuplet", number="{}".format(number), type="start") + ) + + if notations: + notations_e = etree.SubElement(note_e, "notations") + notations_e.extend(notations) + + return note_e + + +def do_note(note, measure_end, part, voice, counter, n_of_staves): + if isinstance(note, score.GraceNote): + dur_divs = 0 + + else: + dur_divs = note.end.t - note.start.t + + note_e = make_note_el(note, dur_divs, voice, counter, n_of_staves) + + return (note.start.t, dur_divs, note_e) + + +def linearize_measure_contents(part, start, end, state): + """ + Determine the document order of events starting between `start` (inclusive) + and `end` (exlusive). (notes, directions, divisions, time signatures). This + function finds any mid-measure attribute/divisions and splits up the measure + into segments by divisions, to be linearized separately and + concatenated. The actual linearization is done by + the `linearize_segment_contents` function. + + Parameters + ---------- + start: score.TimePoint + start + end: score.TimePoint + end + part: score.Part + + Returns + ------- + list + The contents of measure in document order + """ + splits = [start] + q_times = part.quarter_durations(start.t, end.t) + if len(q_times) > 0: + quarter = start.quarter + tp = start.next + while tp and tp != end: + if tp.quarter != quarter: + splits.append(tp) + quarter = tp.quarter + tp = tp.next + + splits.append(end) + contents = [] + + for i in range(1, len(splits)): + contents.extend( + linearize_segment_contents(part, splits[i - 1], splits[i], state) + ) + + return contents + + +def remove_voice_polyphony_single(notes, voice_spans): + """ + Test wether a list of notes satisfies the MusicXML constraints on voices that: + - all notes starting at the same time have the same duration + - no is required to specify the voice in document order + whenever a note violates the constraints change its voice + (choosing a new voice that is not currently in use) + + Parameters + ---------- + notes: list + List of notes in a voice + + Returns + ------- + type + Description of return value + """ + + extraneous = defaultdict(list) + + by_onset = defaultdict(list) + for note in notes: + if not isinstance(note, score.GraceNote): + by_onset[note.start.t].append(note) + onsets = sorted(by_onset.keys()) + + for o in onsets: + chord_dur = min(n.duration for n in by_onset[o]) + + for n in by_onset[o]: + if n.duration > chord_dur: + voice = find_free_voice(voice_spans, n.start.t, n.end.t) + voice_spans.append((n.start.t, n.end.t, voice)) + extraneous[voice].append(n) + notes.remove(n) + + # now remove any notes that exceed next onset + by_onset = defaultdict(list) + for note in notes: + by_onset[note.start.t].append(note) + onsets = sorted(by_onset.keys()) + + for o1, o2 in iter_current_next(onsets): + for n in by_onset[o1]: + if o1 + n.duration > o2: + voice = find_free_voice(voice_spans, n.start.t, n.end.t) + voice_spans.append((n.start.t, n.end.t, voice)) + extraneous[voice].append(n) + notes.remove(n) + + return extraneous + + +def find_free_voice(voice_spans, start, end): + free_voice = min(voice for _, _, voice in voice_spans) + 1 + + for vstart, vend, voice in voice_spans: + if (end > vstart) and (start < vend): + free_voice = max(free_voice, voice + 1) + + return free_voice + + +def remove_voice_polyphony(notes_by_voice): + voice_spans = [(-math.inf, math.inf, max(notes_by_voice.keys()))] + extraneous = defaultdict(list) + # n_orig = sum(len(nn) for nn in notes_by_voice.values()) + + for voice, vnotes in notes_by_voice.items(): + v_extr = remove_voice_polyphony_single(vnotes, voice_spans) + + for new_voice, new_vnotes in v_extr.items(): + extraneous[new_voice].extend(new_vnotes) + + # n_1 = sum(len(nn) for nn in notes_by_voice.values()) + # n_2 = sum(len(nn) for nn in extraneous.values()) + # n_new = n_1 + n_2 + # assert n_orig == n_new + # assert len(set(notes_by_voice.keys()).intersection(set(extraneous.keys()))) == 0 + for v, vnotes in extraneous.items(): + notes_by_voice[v] = vnotes + + +# def fill_gaps_with_rests(notes_by_voice, start, end, part): +# for voice, notes in notes_by_voice.items(): +# if len(notes) == 0: +# rest = score.Rest(voice=voice or None) +# part.add(rest, start.t, end.t) +# else: +# t = start.t +# for note in notes: +# if note.start.t > t: +# rest = score.Rest(voice=voice or None) +# part.add(rest, t, note.start.t) +# t = note.end.t +# if note.end.t < end.t: +# rest = score.Rest(voice=voice or None) +# part.add(rest, note.end.t, end.t) + + +def linearize_segment_contents(part, start, end, state): + """ + Determine the document order of events starting between `start` (inclusive) + and `end` (exlusive). + (notes, directions, divisions, time signatures). + """ + + notes = part.iter_all( + score.GenericNote, start=start, end=end, include_subclasses=True + ) + + notes_by_voice = partition(lambda n: n.voice or 0, notes) + if len(notes_by_voice) == 0: + # if there are no notes in this segment, we add a rest + # NOTE: altering the part instance while exporting is bad! + # rest = score.Rest() + # part.add(start.t, rest, end.t) + # notes_by_voice = {0: [rest]} + notes_by_voice[None] = [] + + # make sure there is no polyphony within voices by assigning any violating + # notes to a new (free) voice. + remove_voice_polyphony(notes_by_voice) + + # fill_gaps_with_rests(notes_by_voice, start, end, part) + # # redo + # notes = part.iter_all(score.GenericNote, + # start=start, end=end, + # include_subclasses=True) + # notes_by_voice = partition(lambda n: n.voice or 0, notes) + + voices_e = defaultdict(list) + + for voice in sorted(notes_by_voice.keys()): + voice_notes = notes_by_voice[voice] + # sort by pitch + voice_notes.sort( + key=lambda n: n.midi_pitch 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)) + # voice_notes.sort(key=lambda n: -n.duration) + voice_notes.sort(key=lambda n: n.start.t) + + n_of_staves = part.number_of_staves + + for n in voice_notes: + if isinstance(n, score.GraceNote): + # check if it is the first in its sequence + if not n.grace_prev: + # if so we add the whole grace sequence at once to ensure + # the correct order + for m in n.iter_grace_seq(): + note_e = do_note( + m, end.t, part, voice, state["note_id_counter"], n_of_staves + ) + voices_e[voice].append(note_e) + else: + note_e = do_note( + n, end.t, part, voice, state["note_id_counter"], n_of_staves + ) + voices_e[voice].append(note_e) + + add_chord_tags(voices_e[voice]) + + harmony_e = do_harmony(part, start, end) + attributes_e = do_attributes(part, start, end) + directions_e = do_directions(part, start, end, state["range_counter"]) + prints_e = do_prints(part, start, end) + barline_e = do_barlines(part, start, end) + + other_e = harmony_e + attributes_e + directions_e + barline_e + prints_e + + contents = merge_measure_contents(voices_e, other_e, start.t) + + return contents + + +def do_prints(part, start, end): + pages = part.iter_all(score.Page, start, end) + systems = part.iter_all(score.System, start, end) + by_onset = defaultdict(dict) + for page in pages: + by_onset[page.start.t]["new-page"] = "yes" + for system in systems: + by_onset[system.start.t]["new-system"] = "yes" + result = [] + for onset, attrs in by_onset.items(): + result.append((onset, None, etree.Element("print", **attrs))) + return result + + +def do_barlines(part, start, end): + # all fermata that are not linked to a note (fermata at time end may be part + # of the current or the next measure, depending on the location attribute + # (which is stored in fermata.ref)). + fermata = [ + ferm + for ferm in part.iter_all(score.Fermata, start, end) + if ferm.ref in (None, "left", "middle", "right") + ] + [ + ferm + for ferm in part.iter_all(score.Fermata, end, end.next) + if ferm.ref in (None, "right") + ] + repeat_start = part.iter_all(score.Repeat, start, end) + repeat_end = part.iter_all(score.Repeat, start.next, end.next, mode="ending") + ending_start = part.iter_all(score.Ending, start, end) + ending_end = part.iter_all(score.Ending, start.next, end.next, mode="ending") + by_onset = defaultdict(list) + + for obj in fermata: + by_onset[obj.start.t].append(etree.Element("fermata")) + + for obj in repeat_start: + if obj.start is not None: + by_onset[obj.start.t].append(etree.Element("repeat", direction="forward")) + + for obj in ending_start: + if obj.start is not None: + by_onset[obj.start.t].append( + etree.Element("ending", type="start", number=str(obj.number)) + ) + + for obj in repeat_end: + if obj.end is not None: + by_onset[obj.end.t].append(etree.Element("repeat", direction="backward")) + + for obj in ending_end: + if obj.end is not None: + by_onset[obj.end.t].append( + etree.Element("ending", type="stop", number=str(obj.number)) + ) + + result = [] + + for onset in sorted(by_onset.keys()): + attrib = {} + + if onset == start.t: + attrib["location"] = "left" + + elif onset == end.t: + attrib["location"] = "right" + + else: + attrib["location"] = "middle" + + barline_e = etree.Element("barline", **attrib) + + barline_e.extend(by_onset[onset]) + result.append((onset, None, barline_e)) + + return result + + +def add_chord_tags(notes): + prev_dur = None + prev = None + for onset, dur, note in notes: + if onset == prev: + if dur == prev_dur: + note.insert(0, etree.Element("chord")) + + if any(e.tag == "grace" for e in note): + # if note is a grace note we don't want to trigger a chord for the + # next note + prev = None + else: + prev = onset + prev_dur = dur + + +def forward_backup_if_needed(t, t_prev): + result = [] + gap = 0 + + if t > t_prev: + gap = t - t_prev + e = etree.Element("forward") + ee = etree.SubElement(e, "duration") + ee.text = "{:d}".format(int(gap)) + result.append((t_prev, gap, e)) + + elif t < t_prev: + gap = t_prev - t + e = etree.Element("backup") + ee = etree.SubElement(e, "duration") + ee.text = "{:d}".format(int(gap)) + result.append((t_prev, -gap, e)) + + return result, gap + + +def merge_with_voice(notes, other, measure_start): + by_onset = defaultdict(list) + + for onset, dur, el in notes: + by_onset[onset].append((dur, el)) + + for onset, dur, el in other: + by_onset[onset].append((dur, el)) + + result = [] + last_t = measure_start + fb_cost = 0 + # order to insert simultaneously starting elements; it is important to put + # notes last, since they update the position, and thus would lead to + # needless backup/forward insertions + order = { + "barline": 0, + "attributes": 1, + "direction": 2, + "print": 3, + "sound": 4, + "harmony": 5, + "note": 6, + } + last_note_onset = measure_start + + for onset in sorted(by_onset.keys()): + elems = by_onset[onset] + elems.sort(key=lambda x: order.get(x[1].tag, len(order))) + + for dur, el in elems: + if el.tag == "note": + if el.find("chord") is not None: + last_t = last_note_onset + + last_note_onset = onset + + els, cost = forward_backup_if_needed(onset, last_t) + fb_cost += cost + result.extend(els) + result.append((onset, dur, el)) + last_t = onset + (dur or 0) + + return result, fb_cost + + +def merge_measure_contents(notes, other, measure_start): + merged = {} + # cost (measured as the total forward/backup jumps needed to merge) all + # elements in `other` into each voice + cost = {} + + for voice in sorted(notes.keys()): + # merge `other` with each voice, and keep track of the cost + merged[voice], cost[voice] = merge_with_voice( + notes[voice], other, measure_start + ) + + if not merged: + merged[0] = [] + 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] + result = [] + pos = measure_start + for i, voice in enumerate(sorted(notes.keys())): + if voice == merge_voice: + elements = merged[voice] + + else: + elements = notes[voice] + + # backup/forward when switching voices if necessary + if elements: + gap = elements[0][0] - pos + + if gap < 0: + e = etree.Element("backup") + ee = etree.SubElement(e, "duration") + ee.text = "{:d}".format(-int(gap)) + result.append(e) + + elif gap > 0: + e = etree.Element("forward") + ee = etree.SubElement(e, "duration") + ee.text = "{:d}".format(gap) + result.append(e) + + result.extend([e for _, _, e in elements]) + + # update current position + if elements: + pos = elements[-1][0] + (elements[-1][1] or 0) + + return result + + +def do_directions(part, start, end, counter): + result = [] + + # ending directions + directions = part.iter_all( + score.DynamicDirection, + start.next, + end.next, + include_subclasses=True, + mode="ending", + ) + + for direction in directions: + text = direction.raw_text or direction.text + e0 = etree.Element("direction") + e1 = etree.SubElement(e0, "direction-type") + + if getattr(direction, "wedge", False): + number = range_number_from_counter(direction, "wedge", counter) + e2 = etree.SubElement(e1, "wedge", number="{}".format(number), type="stop") + + else: + number = range_number_from_counter(direction, "wedge", counter) + etree.SubElement(e1, "dashes", number="{}".format(number), type="stop") + + elem = (direction.end.t, None, e0) + result.append(elem) + + tempos = part.iter_all(score.Tempo, start, end) + directions = part.iter_all(score.Direction, start, end, include_subclasses=True) + + for tempo in tempos: + # e0 = etree.Element('direction') + # e1 = etree.SubElement(e0, 'direction-type') + # e2 = etree.SubElement(e1, 'words') + unit = "q" if tempo.unit is None else tempo.unit + # e2.text = '{}={}'.format(unit, tempo.bpm) + # result.append((tempo.start.t, None, e0)) + e3 = etree.Element( + "sound", tempo="{}".format(int(to_quarter_tempo(unit, tempo.bpm))) + ) + result.append((tempo.start.t, None, e3)) + + for direction in directions: + text = direction.raw_text or direction.text + + if text in PEDAL_DIRECTIONS: + # Pedal directions create an element for start + # and an element for ending + + # Use end of the segment as ending of the pedal sign + ped_end = end if direction.end is None else direction.end + + # Create a pedal start element + if direction.start.t >= start.t: + e0s = etree.Element("direction", placement="below") + e1s = etree.SubElement(e0s, "direction-type") + # For sustain pedals + if isinstance(direction, score.SustainPedalDirection): + pedal_kwargs = {} + if direction.line: + pedal_kwargs["line"] = "yes" + # For Flake8 (ignore unused variable), since + # etree.SubElement adds e2s to e1s + e2s = etree.SubElement( # noqa: F841 + e1s, "pedal", type="start", **pedal_kwargs + ) + if direction.staff is not None and direction.staff != 1: + e3s = etree.SubElement(e0s, "staff") + e3s.text = str(direction.staff) + elem = (direction.start.t, None, e0s) + result.append(elem) + if ped_end.t <= end.t: + e0e = etree.Element("direction", placement="below") + e1e = etree.SubElement(e0e, "direction-type") + if isinstance(direction, score.SustainPedalDirection): + pedal_kwargs = {} + if direction.line: + pedal_kwargs["line"] = "yes" + else: + pedal_kwargs["sign"] = "yes" + # For Flake8 (ignore unused variable), since + # etree.SubElement adds e2e to e1e + e2e = etree.SubElement( # noqa: F841 + e1e, "pedal", type="end", **pedal_kwargs + ) + if direction.staff is not None and direction.staff != 1: + e3e = etree.SubElement(e0e, "staff") + e3e.text = str(direction.staff) + elem = (ped_end.t, None, e0e) + result.append(elem) + else: + e0 = etree.Element("direction") + e1 = etree.SubElement(e0, "direction-type") + + if text in DYN_DIRECTIONS: + e2 = etree.SubElement(e1, "dynamics") + etree.SubElement(e2, text) + + elif getattr(direction, "wedge", False): + if isinstance(direction, score.IncreasingLoudnessDirection): + wtype = "crescendo" + else: + wtype = "diminuendo" + + number = range_number_from_counter(direction, "wedge", counter) + e2 = etree.SubElement( + e1, "wedge", number="{}".format(number), type=wtype + ) + + else: + e2 = etree.SubElement(e1, "words") + e2.text = filter_string(text) + + if ( + isinstance(direction, score.DynamicDirection) + and direction.end is not None + ): + e3 = etree.SubElement(e0, "direction-type") + number = range_number_from_counter(direction, "dashes", counter) + etree.SubElement( + e3, "dashes", number="{}".format(number), type="start" + ) + + if direction.staff is not None and direction.staff != 1: + e5 = etree.SubElement(e0, "staff") + e5.text = str(direction.staff) + + elem = (direction.start.t, None, e0) + result.append(elem) + + return result + + +def do_harmony(part, start, end): + """ + Produce xml objects for harmony (Roman Numeral Text) + """ + harmony = part.iter_all(score.RomanNumeral, start, end) + result = [] + for h in harmony: + harmony_e = etree.Element("harmony", print_frame="no") + function = etree.SubElement(harmony_e, "function") + function.text = h.text + kind_e = etree.SubElement(harmony_e, "kind", text="") + kind_e.text = "none" + result.append((h.start.t, None, harmony_e)) + harmony = part.iter_all(score.ChordSymbol, start, end) + for h in harmony: + harmony_e = etree.Element("harmony", print_frame="no") + kind_e = ( + etree.SubElement(harmony_e, "kind", text=h.kind) + if h.kind is not None + else etree.SubElement(harmony_e, "kind", text="") + ) + kind_e.text = "none" + root_e = etree.SubElement(harmony_e, "root") + root_step_e = etree.SubElement(root_e, "root-step") + root_step_e.text = h.root + if h.bass is not None: + bass_e = etree.SubElement(harmony_e, "bass") + bass_step_e = etree.SubElement(bass_e, "bass-step") + bass_step_e.text = h.bass + result.append((h.start.t, None, harmony_e)) + return result + + +def do_attributes(part, start, end): + """ + Produce xml objects for non-note measure content + + Parameters + ---------- + others: type + Description of `others` + + Returns + ------- + type + Description of return value + """ + + by_start = defaultdict(list) + + # for o in part.iter_all(score.Divisions, start, end): + # by_start[o.start.t].append(o) + for t, quarter in part.quarter_durations(start.t, end.t): + by_start[t].append(int(quarter)) + for o in part.iter_all(score.KeySignature, start, end): + by_start[o.start.t].append(o) + for o in part.iter_all(score.TimeSignature, start, end): + by_start[o.start.t].append(o) + for o in part.iter_all(score.Staff, start, end): + by_start[o.start.t].append(o) + + # sort clefs by number before adding them to by_start + clefs_by_start = defaultdict(list) + + for o in part.iter_all(score.Clef, start, end): + clefs_by_start[o.start.t].append(o) + + for t, clefs in clefs_by_start.items(): + clefs.sort(key=lambda clef: getattr(clef, "number", 0)) + by_start[t].extend(clefs) + + result = [] + + # hacky: flag to include staves element before the first clef + staves_included = False + + for t in sorted(by_start.keys()): + attr_e = etree.Element("attributes") + + for o in by_start[t]: + if isinstance(o, int): + etree.SubElement(attr_e, "divisions").text = "{}".format(o) + + elif isinstance(o, score.KeySignature): + ks_e = etree.SubElement(attr_e, "key") + etree.SubElement(ks_e, "fifths").text = "{}".format(o.fifths) + + if o.mode: + etree.SubElement(ks_e, "mode").text = "{}".format(o.mode) + + elif isinstance(o, score.TimeSignature): + ts_e = etree.SubElement(attr_e, "time") + etree.SubElement(ts_e, "beats").text = "{}".format(o.beats) + etree.SubElement(ts_e, "beat-type").text = "{}".format(o.beat_type) + + elif isinstance(o, score.Clef): + if not staves_included: + staves_e = etree.SubElement(attr_e, "staves") + staves_e.text = "{}".format(len(clefs)) + staves_included = True + + clef_e = etree.SubElement(attr_e, "clef") + + if o.staff and o.staff != 1: + clef_e.set("number", "{}".format(o.staff)) + + etree.SubElement(clef_e, "sign").text = "{}".format(o.sign) + etree.SubElement(clef_e, "line").text = "{}".format(o.line) + + if o.octave_change: + etree.SubElement(clef_e, "clef-octave-change").text = "{}".format( + o.octave_change + ) + elif isinstance(o, score.Staff): + staff_e = etree.SubElement(attr_e, "staff-details") + if o.lines: + etree.SubElement(staff_e, "staff-lines").text = "{}".format(o.lines) + + result.append((t, None, attr_e)) + + return result + +@deprecated_alias(parts="score_data") +def save_mei( + score_data: score.ScoreLike, + out: Optional[PathLike] = None, +) -> Optional[str]: + """ + Save a one or more Part or PartGroup instances in MEI format. + + Parameters + ---------- + score_data : Score, list, Part, or PartGroup + The musical score to be saved. A :class:`partitura.score.Score` object, + a :class:`partitura.score.Part`, a :class:`partitura.score.PartGroup` or + a list of these. + out: str, file-like object, or None, optional + Output file + + Returns + ------- + None or str + If no output file is specified using `out` the function returns the + MEI data as a string. Otherwise the function returns None. + """ + + if not isinstance(score_data, score.Score): + score_data = score.Score( + id=None, + partlist=score_data, + ) + + root = etree.Element("score-partwise") + + partlist_e = etree.SubElement(root, "part-list") + state = { + "note_id_counter": {}, + "range_counter": {}, + } + + group_stack = [] + + def close_group_stack(): + while group_stack: + # close group + etree.SubElement( + partlist_e, + "part-group", + number="{}".format(group_stack[-1].number), + type="stop", + ) + # remove from stack + group_stack.pop() + + def handle_parents(part): + # 1. get deepest parent that is in group_stack (keep track of parents to + # add) + pg = part.parent + to_add = [] + while pg: + if pg in group_stack: + break + to_add.append(pg) + pg = pg.parent + + # close groups while not equal to pg + while group_stack: + if pg == group_stack[-1]: + break + else: + # close group + etree.SubElement( + partlist_e, + "part-group", + number="{}".format(group_stack[-1].number), + type="stop", + ) + # remove from stack + group_stack.pop() + + # start all parents in to_add + for pg in reversed(to_add): + # start group + pg_e = etree.SubElement( + partlist_e, "part-group", number="{}".format(pg.number), type="start" + ) + if pg.group_symbol is not None: + symb_e = etree.SubElement(pg_e, "group-symbol") + symb_e.text = pg.group_symbol + if pg.group_name is not None: + name_e = etree.SubElement(pg_e, "group-name") + name_e.text = pg.group_name + + group_stack.append(pg) + + for part in score_data: + handle_parents(part) + + # handle part list entry + scorepart_e = etree.SubElement(partlist_e, "score-part", id=part.id) + + partname_e = etree.SubElement(scorepart_e, "part-name") + if part.part_name: + partname_e.text = filter_string(part.part_name) + + if part.part_abbreviation: + partabbrev_e = etree.SubElement(scorepart_e, "part-abbreviation") + partabbrev_e.text = filter_string(part.part_abbreviation) + + # write the part itself + + part_e = etree.SubElement(root, "part", id=part.id) + + for measure in part.iter_all(score.Measure): + part_e.append(etree.Comment(MEASURE_SEP_COMMENT)) + attrib = {} + + if measure.number is not None: + attrib["number"] = str(measure.number) + + measure_e = etree.SubElement(part_e, "measure", **attrib) + contents = linearize_measure_contents( + part, measure.start, measure.end, state + ) + measure_e.extend(contents) + + close_group_stack() + + if out: + if hasattr(out, "write"): + out.write( + etree.tostring( + root.getroottree(), + encoding="UTF-8", + xml_declaration=True, + pretty_print=True, + doctype=DOCTYPE, + ) + ) + + else: + with open(out, "wb") as f: + f.write( + etree.tostring( + root.getroottree(), + encoding="UTF-8", + xml_declaration=True, + pretty_print=True, + doctype=DOCTYPE, + ) + ) + + else: + return etree.tostring( + root.getroottree(), + encoding="UTF-8", + xml_declaration=True, + pretty_print=True, + doctype=DOCTYPE, + ) From e98ffb41c1aa7e158f8885597956bad6a58113aa Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 23 Jan 2024 16:47:41 +0100 Subject: [PATCH 049/197] Update quality parsing from annotations. --- partitura/io/importdcml.py | 6 +++- partitura/score.py | 65 +++++++++++++++++++++++++++++--------- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 129207bb..6aa87652 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -159,10 +159,14 @@ def read_harmony_tsv(beat_tsv_path, part): is_na_roman = data["chord"].isna() # Find Phrase Starts where data["phraseend"] == "{" for idx, row in data[~is_na_roman].iterrows(): + # row["chord_type"] contains the quality of the chord but it is encoded differently than for other formats + # and datasets. For example, a minor chord is encoded as "m" instead of "min" or "minor" + # Therefore we do not add the quality to the RomanNumeral object. Then it is extracted from the text. part.add( spt.RomanNumeral(text=row["chord"], local_key=row["localkey"], - quality=row["chord_type"], + + # quality=row["chord_type"], ), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) for idx, row in data[~is_na_cad].iterrows(): diff --git a/partitura/score.py b/partitura/score.py index 4b41a233..3aef3870 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -2738,11 +2738,14 @@ class RomanNumeral(TimedObject): def __init__(self, text, inversion=None, local_key=None, primary_degree=None, secondary_degree=None, quality=None): super().__init__() self.text = text + self.accepted_qualities = ('7', 'aug', 'aug6', 'aug7', 'dim', 'dim7', 'hdim7', 'maj', 'maj7', 'min', 'min7') + self.has_seven = "7" in text self.inversion = inversion if inversion is not None else self._process_inversion() self.local_key = local_key if local_key is not None else self._process_local_key() self.primary_degree = primary_degree if primary_degree is not None else self._process_primary_degree() self.secondary_degree = secondary_degree if secondary_degree is not None else self._process_secondary_degree() - self.quality = quality if quality is not None else self._process_quality() + self.quality = quality if quality is not None and quality in self.accepted_qualities else self._process_quality() + def _process_inversion(self): """Find the inversion of the roman numeral from the text""" @@ -2752,10 +2755,19 @@ def _process_inversion(self): if len(numeric_indications_in_text) > 0: inversion_state = int(numeric_indications_in_text[0]) if inversion_state == 2: + self.has_seven = True return 3 - elif inversion_state in [43, 64]: + elif inversion_state == 43: + self.has_seven = True + return 2 + elif inversion_state == 64: + self.has_seven = False return 2 - elif inversion_state in [6, 65]: + elif inversion_state == 6: + self.has_seven = False + return 1 + elif inversion_state == 65: + self.has_seven = True return 1 return 0 @@ -2776,13 +2788,9 @@ def _process_primary_degree(self): # The primary degree should be a roman numeral between 1 and 7. # If there is no primary degree, return None roman_text = self.text.split(":")[-1] - if "7" in roman_text or "65" in roman_text or "43" in roman_text or "2" in roman_text: - add_on = "7" - else: - add_on = "" primary_degree = re.findall(r'[a-zA-Z+]+', roman_text) if primary_degree: - return primary_degree.group(0) + add_on + return primary_degree.group(0) return None def _process_secondary_degree(self): @@ -2803,15 +2811,42 @@ def _process_secondary_degree(self): def _process_quality(self): """Find the quality of the roman numeral from the text - Accepted quality values are M, m, +, o, and None. + Accepted quality values are 7, aug, aug6, aug7, dim, dim7, hdim7, maj, maj7, min, min7. + This format follows the standards from the latest version of the AugmentedNet model. + Found out more here: github.com/napulen/AugmentedNet """ # The quality should be M, m, +, o, or None. - # If there is no quality, return None - quality = re.findall(r'[Mm+o]', self.text) - if len(quality) > 0: - return quality[0] - return None - + aug_cond = "aug" in self.text.lower() or "+" in self.text.lower() + minor_cond = self.primary_degree.islower() if self.primary_degree is not None else False + major_cond = self.primary_degree.isupper() if self.primary_degree is not None else False + dim_cond = "dim" in self.text or "o" in self.text + aug6_cond = "ger" in self.text.lower() or "it" in self.text.lower() or "fr" in self.text.lower() + hdim_cond = "0" in self.text or "%" in self.text or "ø" in self.text + if aug6_cond: + quality = "aug6" + elif "maj7" in self.text.lower(): + quality = "maj7" + elif dim_cond and self.has_seven: + quality = "dim7" + elif dim_cond: + quality = "dim" + elif aug_cond and self.has_seven: + quality = "aug7" + elif aug_cond: + quality = "aug" + elif hdim_cond: + quality = "hdim7" + elif minor_cond and self.has_seven: + quality = "min7" + elif minor_cond: + quality = "min" + elif major_cond and self.has_seven: + quality = "7" + elif major_cond: + quality = "maj" + else: + raise ValueError(f"Quality for {self.text} was not found") + return quality def __str__(self): return f'{super().__str__()} "{self.text}"' From 84258ae4440736f50a7db2aead81fbe549c4c885 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 24 Jan 2024 11:14:36 +0100 Subject: [PATCH 050/197] Added a function to transpose a note only given step and alter (without octave) while maintining a correct pitch spelling. --- partitura/utils/music.py | 48 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 15103c8c..b0932566 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -421,7 +421,7 @@ def ensure_rest_array(restarray_or_part, *args, **kwargs): ) -def transpose_step(step, interval, direction): +def _transpose_step(step, interval, direction): """ Transpose a note by a given interval. Parameters @@ -438,7 +438,7 @@ def transpose_step(step, interval, direction): return step -def _transpose_note(note, interval): +def _transpose_note_inplace(note, interval): """ Transpose a note by a given interval. Parameters @@ -452,7 +452,7 @@ def _transpose_note(note, interval): else: # TODO work for arbitrary octave. prev_step = note.step.capitalize() - note.step = transpose_step(prev_step, interval.number, interval.direction) + note.step = _transpose_step(prev_step, interval.number, interval.direction) if STEPS[note.step] - STEPS[prev_step] < 0 and interval.direction == "up": note.octave += 1 elif STEPS[note.step] - STEPS[prev_step] > 0 and interval.direction == "down": @@ -471,6 +471,46 @@ def _transpose_note(note, interval): ) +def transpose_note(step, alter, interval): + """ + Transpose a note by a given interval without changing the octave or creating a Note Object. + + + Parameters + ---------- + step: str + The step of the pitch, e.g. C, D, E, etc. + alter: int + The alteration of the pitch, e.g. -2, -1, 0, 1, 2 etc. + interval: Interval + The interval to transpose by. + + Returns + ------- + new_step: str + The new step of the pitch, e.g. C, D, E, etc. + new_alter: int + The new alteration of the pitch, e.g. -2, -1, 0, 1, 2 etc. + """ + if interval.quality + str(interval.number) == "P1": + new_step = step + new_alter = alter + else: + prev_step = step.capitalize() + new_step = _transpose_step(prev_step, interval.number, interval.direction) + prev_alter = alter if alter is not None else 0 + prev_pc = MIDI_BASE_CLASS[prev_step.lower()] + prev_alter + tmp_pc = MIDI_BASE_CLASS[new_step.lower()] + if interval.direction == "up": + diff_sm = tmp_pc - prev_pc if tmp_pc >= prev_pc else tmp_pc + 12 - prev_pc + else: + diff_sm = prev_pc - tmp_pc if prev_pc >= tmp_pc else prev_pc + 12 - tmp_pc + new_alter = ( + INTERVAL_TO_SEMITONES[interval.quality + str(interval.number)] - diff_sm + ) + return new_step, new_alter + + def transpose(score: ScoreLike, interval: Interval) -> ScoreLike: """ Transpose a score by a given interval. @@ -502,7 +542,7 @@ def transpose(score: ScoreLike, interval: Interval) -> ScoreLike: transpose(part, interval) elif isinstance(score, s.Part): for note in score.notes_tied: - _transpose_note(note, interval) + _transpose_note_inplace(note, interval) return new_score From e21bc11b241ee6ef120d451f25cd985d1995a431 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 24 Jan 2024 11:14:42 +0100 Subject: [PATCH 051/197] minor corrections. --- partitura/score.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index 3aef3870..e78344ad 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -2746,7 +2746,6 @@ 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() - def _process_inversion(self): """Find the inversion of the roman numeral from the text""" # The inversion should be right after the roman numeral. @@ -2788,7 +2787,7 @@ def _process_primary_degree(self): # The primary degree should be a roman numeral between 1 and 7. # If there is no primary degree, return None roman_text = self.text.split(":")[-1] - primary_degree = re.findall(r'[a-zA-Z+]+', roman_text) + primary_degree = re.search(r'[a-zA-Z+]+', roman_text) if primary_degree: return primary_degree.group(0) return None @@ -2801,11 +2800,14 @@ def _process_secondary_degree(self): """ # The secondary degree should be a roman numeral between 1 and 7. # If it is not specified in the text, return I (the tonic) when the primary degree is not none. - secondary_degree = re.findall(r'[ivIV]+', self.text) - if len(secondary_degree) > 1: - return secondary_degree[1] - elif self.primary_degree is not None: - return "I" + roman_text = self.text.split(":")[-1] + split_pr_sec = roman_text.split("/") + if len(split_pr_sec) > 1: + secondary_degree = re.search(r'[a-zA-Z+]+', split_pr_sec[-1]) + return secondary_degree.group(0) + elif self.primary_degree is not None and self.local_key is not None: + secondary_degree = "I" if self.local_key.isupper() else "i" + return secondary_degree return None def _process_quality(self): From 430f6d06c1240672e5e768c1a35eaf2917e443bd Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 25 Jan 2024 17:28:49 +0100 Subject: [PATCH 052/197] First sketch of Kern Export. --- partitura/io/exportkern.py | 97 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 partitura/io/exportkern.py diff --git a/partitura/io/exportkern.py b/partitura/io/exportkern.py new file mode 100644 index 00000000..00708a54 --- /dev/null +++ b/partitura/io/exportkern.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains methods for exporting Kern files. +""" +import math +from collections import defaultdict +import partitura.score as score +from operator import itemgetter +from typing import Optional +import numpy as np +import warnings +from partitura.utils import partition, iter_current_next, to_quarter_tempo +from partitura.utils.misc import deprecated_alias, PathLike + +__all__ = ["save_kern"] + + +def sym_dur_to_kern(symbolic_duration: dict) -> str: + return "" + +def pitch_to_kern(element: score.GenericNote) -> str: + step, alter, octave = element.step, element.alter, element.octave + return "" + +def markings_to_kern(element: score.GenericNote) -> str: + return "" + +def save_kern( + score_data: score.ScoreLike, + out: Optional[PathLike] = None, + ) -> Optional[str]: + # Header extracts meta information about the score + header = score_data.composer + # Kern can output only from part so first let's merge parts (we need a timewise representation) + if isinstance(score_data, score.Score): + # TODO check that divisions are the same + part = score.merge_parts(score.partlist) + else: + part = score + + + note_array = part.note_array(include_staff=True) + unique_voc_staff = np.unique(note_array[["voice", "staff"]], axis=0) + vocstaff_map_dict = {f"{unique_voc_staff[i][0]}-{unique_voc_staff[i][1]}": i for i in range(unique_voc_staff.shape[0])} + part_elements = list(part.iter_all()) + out_data = np.empty((len(part_elements) + 1, len(unique_voc_staff)), dtype=object) + + # Fill all values with the "." character to filter afterwards + out_data.fill(".") + out_data[0] = "**kern" + prev_note_time = None + prev_note_col_idx = None + prev_note_row_idx = None + for row_idx, el in enumerate(part_elements, start=1): + if isinstance(el, score.GenericNote): + voice = el.voice + staff = el.staff + duration = sym_dur_to_kern(el.symbolic_duration) + pitch = pitch_to_kern(el) + col_idx = vocstaff_map_dict[f"{voice}-{staff}"] + markings = markings_to_kern(el) + kern_el = duration + pitch + markings + if prev_note_time == el.start.t: + if prev_note_col_idx == col_idx: + # Chords in Kern + out_data[prev_note_row_idx, prev_note_col_idx] = out_data[prev_note_row_idx, prev_note_col_idx] + " " + kern_el + else: + # Same row (start.t) other spline + out_data[prev_note_row_idx, col_idx] = kern_el + else: + # New line + out_data[row_idx, col_idx] = kern_el + elif isinstance(el, score.Measure): + # Apply measure to all splines + kern_el = f"={el.number}" + out_data[row_idx] = kern_el + elif isinstance(el, score.TimeSignature): + # Apply element to all splines + kern_el = f"*M{el.beats}/{el.beat_type}" + out_data[row_idx] = kern_el + elif isinstance(el, score.KeySignature): + # Apply element to all splines + alters = "" + kern_el = f"*{alters}" + out_data[row_idx] = kern_el + else: + warnings.warn(f"Element {el} is supported for kern export yet.") + + # if an entire row is filled with "." elements remove it. + out_data = out_data[~np.all(out_data == ".", axis=1)] + + # Use numpy savetxt to save the file + footer = "Encoded using the Partitura Python package, version 1.5.0" + np.savetxt(out, out_data, fmt="utf-8", delimiter="\t", newline="\n", header=header, footer=footer, comments="!!!") + + From 9d019b593cb595d81b6cbb429516d5a46dd90cd3 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 25 Jan 2024 20:13:26 +0100 Subject: [PATCH 053/197] First working version of kern export. --- partitura/io/__init__.py | 1 + partitura/io/exportkern.py | 208 ++++++++++++++++++++++++++++--------- 2 files changed, 159 insertions(+), 50 deletions(-) diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index f3026a52..c6407a94 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_v2 import load_kern +from .exportkern import save_kern from .importparangonada import load_parangonada_csv from .exportparangonada import save_parangonada_csv from .importmusic21 import load_music21 diff --git a/partitura/io/exportkern.py b/partitura/io/exportkern.py index 00708a54..3b428e61 100644 --- a/partitura/io/exportkern.py +++ b/partitura/io/exportkern.py @@ -5,7 +5,7 @@ """ import math from collections import defaultdict -import partitura.score as score +import partitura.score as spt from operator import itemgetter from typing import Optional import numpy as np @@ -16,82 +16,190 @@ __all__ = ["save_kern"] +ACC_TO_SIGN = { + 0: "n", + -1: "-", + 1: "#", + -2: "--", + 2: "##", +} + +KERN_NOTES = { + ('C', 3): 'C', + ('D', 3): 'D', + ('E', 3): 'E', + ('F', 3): 'F', + ('G', 3): 'G', + ('A', 3): 'A', + ('B', 3): 'B', + ('C', 4): 'c', + ('D', 4): 'd', + ('E', 4): 'e', + ('F', 4): 'f', + ('G', 4): 'g', + ('A', 4): 'a', + ('B', 4): 'b' +} + +KERN_DURS = { + 'maxima': '000', + 'long': '00', + 'breve': '0', + 'whole': '1', + 'half': '2', + 'quarter': '4', + 'eighth': '8', + '16th': '16', + '32nd': '32', + '64th': '64', + '128th': '128', + '256th': '256' + } + + def sym_dur_to_kern(symbolic_duration: dict) -> str: - return "" + kern_base = KERN_DURS[symbolic_duration["type"]] + dots = "."*symbolic_duration["dots"] if "dots" in symbolic_duration.keys() else "" + if "actual_notes" in symbolic_duration.keys() and "normal_notes": + kern_base = int(kern_base) * symbolic_duration["actual_notes"] / symbolic_duration["normal_notes"] + kern_base = str(kern_base) + return kern_base + dots -def pitch_to_kern(element: score.GenericNote) -> str: +def duration_to_kern(element: spt.GenericNote) -> str: + if isinstance(element, spt.GraceNote): + if element.grace_type == "acciaccatura": + return "p" + else: + return "q" + else: + return sym_dur_to_kern(element.symbolic_duration) + +def pitch_to_kern(element: spt.GenericNote) -> str: + if isinstance(element, spt.Rest): + return "r" step, alter, octave = element.step, element.alter, element.octave - return "" + if octave > 4: + octave = 4 + multiply_character = octave - 3 + elif octave < 3: + octave = 3 + multiply_character = 4 - octave + else: + multiply_character = 1 + kern_step = KERN_NOTES[(step, octave)] * multiply_character + kern_alter = ACC_TO_SIGN[alter] if alter is not None else "" + return kern_step + kern_alter + + +def markings_to_kern(element: spt.GenericNote) -> str: + symbols = "" + if not isinstance(element, spt.Rest): + if element.tie_next and element.tie_prev: + symbols += "-" + elif element.tie_next: + symbols += "[" + elif element.tie_prev: + symbols += "]" + if element.slur_starts: + symbols += "(" + if element.slur_stops: + symbols += ")" + return symbols -def markings_to_kern(element: score.GenericNote) -> str: - return "" def save_kern( - score_data: score.ScoreLike, + score_data: spt.ScoreLike, out: Optional[PathLike] = None, ) -> Optional[str]: # Header extracts meta information about the score - header = score_data.composer + header = "Here is some random piece" # Kern can output only from part so first let's merge parts (we need a timewise representation) - if isinstance(score_data, score.Score): + if isinstance(score_data, spt.Score): # TODO check that divisions are the same - part = score.merge_parts(score.partlist) + part = spt.merge_parts(score_data.parts) else: - part = score - + part = score_data note_array = part.note_array(include_staff=True) unique_voc_staff = np.unique(note_array[["voice", "staff"]], axis=0) vocstaff_map_dict = {f"{unique_voc_staff[i][0]}-{unique_voc_staff[i][1]}": i for i in range(unique_voc_staff.shape[0])} - part_elements = list(part.iter_all()) - out_data = np.empty((len(part_elements) + 1, len(unique_voc_staff)), dtype=object) - + part_elements = np.array(list(part.iter_all())) + # Part elements is really the maximum number of lines we could have in the kern file + # we add two to account for the **kern and the *- encoding at beginning and end of file + out_data = np.empty((len(part_elements) + 2, len(unique_voc_staff)), dtype=object) + part_el_start_times = np.array([el.start.t for el in part_elements]) + unique_start_times, indices = np.unique(part_el_start_times, return_inverse=True) # Fill all values with the "." character to filter afterwards out_data.fill(".") out_data[0] = "**kern" + out_data[-1] = "*-" prev_note_time = None prev_note_col_idx = None prev_note_row_idx = None - for row_idx, el in enumerate(part_elements, start=1): - if isinstance(el, score.GenericNote): - voice = el.voice - staff = el.staff - duration = sym_dur_to_kern(el.symbolic_duration) - pitch = pitch_to_kern(el) - col_idx = vocstaff_map_dict[f"{voice}-{staff}"] - markings = markings_to_kern(el) - kern_el = duration + pitch + markings - if prev_note_time == el.start.t: - if prev_note_col_idx == col_idx: - # Chords in Kern - out_data[prev_note_row_idx, prev_note_col_idx] = out_data[prev_note_row_idx, prev_note_col_idx] + " " + kern_el + row_idx = 1 + for unique_start_time_idx in range(len(unique_start_times)): + # Get all elements starting at this time + elements_starting = part_elements[indices == unique_start_time_idx] + # Find notes + note_mask = np.array([isinstance(el, spt.GenericNote) for el in elements_starting]) + if np.any(~note_mask): + bar_mask = np.array([isinstance(el, spt.Measure) for el in elements_starting[~note_mask]]) + tandem_mask = ~bar_mask + structural_elements = elements_starting[~note_mask] + structural_elements = np.hstack((structural_elements[tandem_mask], structural_elements[bar_mask])) + else: + structural_elements = elements_starting[~note_mask] + # Put structural elements first (start with tandem elements, then measure elements, then notes and rests) + elements_starting = np.hstack((structural_elements, elements_starting[note_mask])) + for el in elements_starting: + if isinstance(el, spt.GenericNote): + voice = el.voice + staff = el.staff + duration = duration_to_kern(el) + pitch = pitch_to_kern(el) + col_idx = vocstaff_map_dict[f"{voice}-{staff}"] + markings = markings_to_kern(el) + kern_el = duration + pitch + markings + if prev_note_time == el.start.t: + if prev_note_col_idx == col_idx: + # Chords in Kern + out_data[prev_note_row_idx, prev_note_col_idx] = out_data[prev_note_row_idx, prev_note_col_idx] + " " + kern_el + else: + # Same row (start.t) other spline + out_data[prev_note_row_idx, col_idx] = kern_el else: - # Same row (start.t) other spline - out_data[prev_note_row_idx, col_idx] = kern_el + # New line + out_data[row_idx, col_idx] = kern_el + prev_note_row_idx = row_idx + prev_note_col_idx = col_idx + prev_note_time = el.start.t + elif isinstance(el, spt.Measure): + # Apply measure to all splines + kern_el = f"={el.number}" + out_data[row_idx] = kern_el + elif isinstance(el, spt.TimeSignature): + # Apply element to all splines + kern_el = f"*M{el.beats}/{el.beat_type}" + out_data[row_idx] = kern_el + elif isinstance(el, spt.KeySignature): + # Apply element to all splines + alters = "" + kern_el = f"*{alters}" + out_data[row_idx] = kern_el else: - # New line - out_data[row_idx, col_idx] = kern_el - elif isinstance(el, score.Measure): - # Apply measure to all splines - kern_el = f"={el.number}" - out_data[row_idx] = kern_el - elif isinstance(el, score.TimeSignature): - # Apply element to all splines - kern_el = f"*M{el.beats}/{el.beat_type}" - out_data[row_idx] = kern_el - elif isinstance(el, score.KeySignature): - # Apply element to all splines - alters = "" - kern_el = f"*{alters}" - out_data[row_idx] = kern_el - else: - warnings.warn(f"Element {el} is supported for kern export yet.") - + warnings.warn(f"Element {el} is supported for kern export yet.") + row_idx += 1 # if an entire row is filled with "." elements remove it. out_data = out_data[~np.all(out_data == ".", axis=1)] - # Use numpy savetxt to save the file footer = "Encoded using the Partitura Python package, version 1.5.0" - np.savetxt(out, out_data, fmt="utf-8", delimiter="\t", newline="\n", header=header, footer=footer, comments="!!!") + np.savetxt(fname=out, X=out_data, fmt="%1.26s", + delimiter="\t", newline="\n", + header=header, footer=footer, + comments="!!!", encoding="utf-8") +if __name__ == "__main__": + import partitura as pt + score = pt.load_score(pt.EXAMPLE_MUSICXML) + save_kern(score, "C:/Users/melki/Desktop/test.krn") \ No newline at end of file From 83b3c515f074131d1006e4f46e83167e20adb5c7 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 25 Jan 2024 20:23:14 +0100 Subject: [PATCH 054/197] added a test and output. --- partitura/io/exportkern.py | 16 +++++++++++----- tests/test_kern.py | 8 ++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/partitura/io/exportkern.py b/partitura/io/exportkern.py index 3b428e61..da4d3a62 100644 --- a/partitura/io/exportkern.py +++ b/partitura/io/exportkern.py @@ -5,6 +5,9 @@ """ import math from collections import defaultdict + +import numpy + import partitura.score as spt from operator import itemgetter from typing import Optional @@ -110,7 +113,7 @@ def markings_to_kern(element: spt.GenericNote) -> str: def save_kern( score_data: spt.ScoreLike, out: Optional[PathLike] = None, - ) -> Optional[str]: + ) -> Optional[np.ndarray]: # Header extracts meta information about the score header = "Here is some random piece" # Kern can output only from part so first let's merge parts (we need a timewise representation) @@ -193,10 +196,13 @@ def save_kern( out_data = out_data[~np.all(out_data == ".", axis=1)] # Use numpy savetxt to save the file footer = "Encoded using the Partitura Python package, version 1.5.0" - np.savetxt(fname=out, X=out_data, fmt="%1.26s", - delimiter="\t", newline="\n", - header=header, footer=footer, - comments="!!!", encoding="utf-8") + if out is not None: + np.savetxt(fname=out, X=out_data, fmt="%1.26s", + delimiter="\t", newline="\n", + header=header, footer=footer, + comments="!!!", encoding="utf-8") + else: + return out_data if __name__ == "__main__": diff --git a/tests/test_kern.py b/tests/test_kern.py index 2c8c3703..ff09a261 100644 --- a/tests/test_kern.py +++ b/tests/test_kern.py @@ -11,6 +11,7 @@ from partitura.score import merge_parts from partitura.utils import ensure_notearray from partitura.io.importkern_v2 import load_kern +from partitura.io.exportkern import save_kern from partitura import load_musicxml import numpy as np @@ -53,5 +54,12 @@ def test_spline_splitting(self): vn = part.note_array()["voice"].max() self.assertTrue(voices_per_part[i] == vn) + def test_import_export(self): + imported_score = load_kern(partitura.EXAMPLE_KERN) + exported_score = save_kern(imported_score) + x = np.loadtxt(partitura.EXAMPLE_KERN, comments="!", dtype=str, encoding="utf-8", delimiter="\t") + self.assertTrue(np.all(x == exported_score.to_kern())) + + # if __name__ == "__main__": # unittest.main() From b3e5aa217e610e5a7634eb38ee844a54f8c2130a Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 26 Jan 2024 17:28:34 +0100 Subject: [PATCH 055/197] Added fill rests function. The function fills un complete voices with rests either only in the bars they appear or for the whole score. --- partitura/io/exportkern.py | 320 +++++++++++++++++++++++-------------- partitura/score.py | 86 ++++++++++ 2 files changed, 285 insertions(+), 121 deletions(-) diff --git a/partitura/io/exportkern.py b/partitura/io/exportkern.py index da4d3a62..2a709141 100644 --- a/partitura/io/exportkern.py +++ b/partitura/io/exportkern.py @@ -59,61 +59,205 @@ '256th': '256' } +KEYS = ["f", "c", "g", "d", "a", "e", "b"] -def sym_dur_to_kern(symbolic_duration: dict) -> str: - kern_base = KERN_DURS[symbolic_duration["type"]] - dots = "."*symbolic_duration["dots"] if "dots" in symbolic_duration.keys() else "" - if "actual_notes" in symbolic_duration.keys() and "normal_notes": - kern_base = int(kern_base) * symbolic_duration["actual_notes"] / symbolic_duration["normal_notes"] - kern_base = str(kern_base) - return kern_base + dots - -def duration_to_kern(element: spt.GenericNote) -> str: - if isinstance(element, spt.GraceNote): - if element.grace_type == "acciaccatura": - return "p" + + +class KernExporter(object): + """ + Class for exporting a partitura score to Kern format. + + Parameters + ---------- + part: spt.Part + Part to export to Kern format. + """ + def __init__(self, part): + self.part = part + note_array = part.note_array(include_staff=True) + num_measures = len(part.measures) + num_notes = len(part.notes) + num_rests = len(part.rests) + self.unique_voc_staff = np.unique(note_array[["voice", "staff"]], axis=0) + self.vocstaff_map_dict = {f"{self.unique_voc_staff[i][0]}-{self.unique_voc_staff[i][1]}": i for i in + range(self.unique_voc_staff.shape[0])} + # Part elements is really the maximum number of lines we could have in the kern file + # we add some to account for the **kern and the *- encoding at beginning and end of file and also tandem elements + # that might be added. We also add the number of measures to account for the measure encoding + total_elements_ish = num_measures + num_notes + num_rests + 2 + 10 + self.out_data = np.empty((total_elements_ish, len(self.unique_voc_staff)), dtype=object) + self.unique_times = np.array([p.t for p in part._points]) + # Fill all values with the "." character to filter afterwards + self.out_data.fill(".") + self.out_data[0] = "**kern" + self.out_data[-1] = "*-" + # Add the staff element to the second line + for i in range(self.unique_voc_staff.shape[0]): + self.out_data[1, i] = f"*staff{self.unique_voc_staff[i][1]}" + self.prev_note_time = None + self.prev_note_col_idx = None + self.prev_note_row_idx = None + + def parse(self): + row_idx = 2 + for start_time in self.unique_times: + end_time = start_time + 1 + # Get all elements starting at this time + elements_starting = np.array(list(self.part.iter_all(start=start_time, end=end_time)), dtype=object) + # Find notes + note_mask = np.array([isinstance(el, spt.GenericNote) for el in elements_starting]) + if np.any(~note_mask): + bar_mask = np.array([isinstance(el, spt.Measure) for el in elements_starting[~note_mask]]) + tandem_mask = ~bar_mask + structural_elements = elements_starting[~note_mask] + structural_elements = np.hstack((structural_elements[tandem_mask], structural_elements[bar_mask])) + else: + structural_elements = elements_starting[~note_mask] + # Put structural elements first (start with tandem elements, then measure elements, then notes and rests) + elements_starting = np.hstack((structural_elements, elements_starting[note_mask])) + for el in elements_starting: + add_row = True + if isinstance(el, spt.GenericNote): + self._handle_note(el, row_idx) + elif isinstance(el, spt.Clef): + # Apply clef to all voices of the same staff + currect_staff = el.staff + for staff_idx in range(self.unique_voc_staff.shape[0]): + if self.unique_voc_staff[staff_idx][1] == currect_staff: + kern_el = f"*clef{el.sign.upper()}{el.line}" + self.out_data[row_idx, staff_idx] = kern_el + elif isinstance(el, spt.Tempo): + # Apply tempo to all splines + kern_el = f"*MM{to_quarter_tempo(el.qpm)}" + self.out_data[row_idx] = kern_el + elif isinstance(el, spt.Measure): + # Apply measure to all splines + kern_el = f"={el.number}" + self.out_data[row_idx] = kern_el + elif isinstance(el, spt.TimeSignature): + # Apply element to all splines + kern_el = f"*M{el.beats}/{el.beat_type}" + self.out_data[row_idx] = kern_el + elif isinstance(el, spt.KeySignature): + # Apply element to all splines + if el.fifths < 0: + alters = "-".join(KEYS[:el.fifths]) + elif el.fifths > 0: + alters = "#".join(KEYS[:el.fifths]) + else: + alters = "" + kern_el = f"*k[{alters}]" + self.out_data[row_idx] = kern_el + else: + add_row = False + warnings.warn(f"Element {el} is not supported for kern export yet.") + if add_row: + row_idx += 1 + return self.out_data + + def trim(self, data): + # if an entire row is filled with "." elements remove it. + out_data = data[~np.all(data == ".", axis=1)] + return out_data + + def sym_dur_to_kern(self, symbolic_duration: dict) -> str: + kern_base = KERN_DURS[symbolic_duration["type"]] + dots = "." * symbolic_duration["dots"] if "dots" in symbolic_duration.keys() else "" + if "actual_notes" in symbolic_duration.keys() and "normal_notes": + kern_base = int(kern_base) * symbolic_duration["actual_notes"] / symbolic_duration["normal_notes"] + kern_base = str(kern_base) + return kern_base + dots + + def duration_to_kern(self, element: spt.GenericNote) -> str: + if isinstance(element, spt.GraceNote): + if element.grace_type == "acciaccatura": + return "p" + else: + return "q" else: - return "q" - else: - return sym_dur_to_kern(element.symbolic_duration) - -def pitch_to_kern(element: spt.GenericNote) -> str: - if isinstance(element, spt.Rest): - return "r" - step, alter, octave = element.step, element.alter, element.octave - if octave > 4: - octave = 4 - multiply_character = octave - 3 - elif octave < 3: - octave = 3 - multiply_character = 4 - octave - else: - multiply_character = 1 - kern_step = KERN_NOTES[(step, octave)] * multiply_character - kern_alter = ACC_TO_SIGN[alter] if alter is not None else "" - return kern_step + kern_alter - - -def markings_to_kern(element: spt.GenericNote) -> str: - symbols = "" - if not isinstance(element, spt.Rest): - if element.tie_next and element.tie_prev: - symbols += "-" - elif element.tie_next: - symbols += "[" - elif element.tie_prev: - symbols += "]" - if element.slur_starts: - symbols += "(" - if element.slur_stops: - symbols += ")" - return symbols + if "type" not in element.symbolic_duration.keys(): + warnings.warn(f"Element {element} has no symbolic duration type") + return "4" + return self.sym_dur_to_kern(element.symbolic_duration) + + def pitch_to_kern(self, element: spt.GenericNote) -> str: + if isinstance(element, spt.Rest): + return "r" + step, alter, octave = element.step, element.alter, element.octave + if octave > 4: + multiply_character = octave - 3 + octave = 4 + elif octave < 3: + multiply_character = 4 - octave + octave = 3 + else: + multiply_character = 1 + kern_step = KERN_NOTES[(step, octave)] * multiply_character + kern_alter = ACC_TO_SIGN[alter] if alter is not None else "" + return kern_step + kern_alter + + def markings_to_kern(self, element: spt.GenericNote) -> str: + symbols = "" + if not isinstance(element, spt.Rest): + if element.tie_next and element.tie_prev: + symbols += "-" + elif element.tie_next: + symbols += "[" + elif element.tie_prev: + symbols += "]" + if element.slur_starts: + symbols += "(" + if element.slur_stops: + symbols += ")" + if isinstance(element, spt.Note): + if element.beam is not None: + symbols += "L" if element.beam == "begin" else "J" if element.beam == "end" else "K" + return symbols + + def _handle_note(self, el: spt.GenericNote, row_idx) -> str: + voice = el.voice + staff = el.staff + duration = self.duration_to_kern(el) + pitch = self.pitch_to_kern(el) + col_idx = self.vocstaff_map_dict[f"{voice}-{staff}"] + markings = self.markings_to_kern(el) + kern_el = duration + pitch + markings + if self.prev_note_time == el.start.t: + if self.prev_note_col_idx == col_idx: + # Chords in Kern + self.out_data[self.prev_note_row_idx, self.prev_note_col_idx] = self.out_data[ + self.prev_note_row_idx, self.prev_note_col_idx] + " " + kern_el + else: + # Same row (start.t) other spline + self.out_data[self.prev_note_row_idx, col_idx] = kern_el + else: + # New line + self.out_data[row_idx, col_idx] = kern_el + self.prev_note_row_idx = row_idx + self.prev_note_col_idx = col_idx + self.prev_note_time = el.start.t def save_kern( score_data: spt.ScoreLike, out: Optional[PathLike] = None, ) -> Optional[np.ndarray]: + """ + Save a score in Kern format. + + Parameters + ---------- + score_data: spt.ScoreLike + Score to save in Kern format + + out: Optional[PathLike] + Path to save the Kern file. If None, the function returns the Kern file as a numpy array. + + Returns + ------- + Optional[np.ndarray] + If out is None, the Kern file is returned as a numpy array. + """ # Header extracts meta information about the score header = "Here is some random piece" # Kern can output only from part so first let's merge parts (we need a timewise representation) @@ -122,78 +266,12 @@ def save_kern( part = spt.merge_parts(score_data.parts) else: part = score_data - - note_array = part.note_array(include_staff=True) - unique_voc_staff = np.unique(note_array[["voice", "staff"]], axis=0) - vocstaff_map_dict = {f"{unique_voc_staff[i][0]}-{unique_voc_staff[i][1]}": i for i in range(unique_voc_staff.shape[0])} - part_elements = np.array(list(part.iter_all())) - # Part elements is really the maximum number of lines we could have in the kern file - # we add two to account for the **kern and the *- encoding at beginning and end of file - out_data = np.empty((len(part_elements) + 2, len(unique_voc_staff)), dtype=object) - part_el_start_times = np.array([el.start.t for el in part_elements]) - unique_start_times, indices = np.unique(part_el_start_times, return_inverse=True) - # Fill all values with the "." character to filter afterwards - out_data.fill(".") - out_data[0] = "**kern" - out_data[-1] = "*-" - prev_note_time = None - prev_note_col_idx = None - prev_note_row_idx = None - row_idx = 1 - for unique_start_time_idx in range(len(unique_start_times)): - # Get all elements starting at this time - elements_starting = part_elements[indices == unique_start_time_idx] - # Find notes - note_mask = np.array([isinstance(el, spt.GenericNote) for el in elements_starting]) - if np.any(~note_mask): - bar_mask = np.array([isinstance(el, spt.Measure) for el in elements_starting[~note_mask]]) - tandem_mask = ~bar_mask - structural_elements = elements_starting[~note_mask] - structural_elements = np.hstack((structural_elements[tandem_mask], structural_elements[bar_mask])) - else: - structural_elements = elements_starting[~note_mask] - # Put structural elements first (start with tandem elements, then measure elements, then notes and rests) - elements_starting = np.hstack((structural_elements, elements_starting[note_mask])) - for el in elements_starting: - if isinstance(el, spt.GenericNote): - voice = el.voice - staff = el.staff - duration = duration_to_kern(el) - pitch = pitch_to_kern(el) - col_idx = vocstaff_map_dict[f"{voice}-{staff}"] - markings = markings_to_kern(el) - kern_el = duration + pitch + markings - if prev_note_time == el.start.t: - if prev_note_col_idx == col_idx: - # Chords in Kern - out_data[prev_note_row_idx, prev_note_col_idx] = out_data[prev_note_row_idx, prev_note_col_idx] + " " + kern_el - else: - # Same row (start.t) other spline - out_data[prev_note_row_idx, col_idx] = kern_el - else: - # New line - out_data[row_idx, col_idx] = kern_el - prev_note_row_idx = row_idx - prev_note_col_idx = col_idx - prev_note_time = el.start.t - elif isinstance(el, spt.Measure): - # Apply measure to all splines - kern_el = f"={el.number}" - out_data[row_idx] = kern_el - elif isinstance(el, spt.TimeSignature): - # Apply element to all splines - kern_el = f"*M{el.beats}/{el.beat_type}" - out_data[row_idx] = kern_el - elif isinstance(el, spt.KeySignature): - # Apply element to all splines - alters = "" - kern_el = f"*{alters}" - out_data[row_idx] = kern_el - else: - warnings.warn(f"Element {el} is supported for kern export yet.") - row_idx += 1 - # if an entire row is filled with "." elements remove it. - out_data = out_data[~np.all(out_data == ".", axis=1)] + if not part.measures: + spt.add_measures(part) + spt.fill_rests(part, measurewise=False) + exporter = KernExporter(part) + out_data = exporter.parse() + out_data = exporter.trim(out_data) # Use numpy savetxt to save the file footer = "Encoded using the Partitura Python package, version 1.5.0" if out is not None: @@ -207,5 +285,5 @@ def save_kern( if __name__ == "__main__": import partitura as pt - score = pt.load_score(pt.EXAMPLE_MUSICXML) - save_kern(score, "C:/Users/melki/Desktop/test.krn") \ No newline at end of file + score = pt.load_score("/home/manos/Desktop/JKU/data/mozart_piano_sonatas/K279-1.musicxml") + save_kern(score, "/home/manos/Desktop/test.krn") \ No newline at end of file diff --git a/partitura/score.py b/partitura/score.py index cbc0f445..8ceee9b7 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -4802,6 +4802,92 @@ def make_score_variants(part): return svs +def _fill_rests_within_measure(measure: Measure, part: Part) -> None: + start_time = measure.start.t + end_time = measure.end.t + notes = np.array(list(part.iter_all(GenericNote, start_time, end_time))) + voc_staff = np.array([[n.voice, n.staff] for n in notes]) + un_voc_staff, inverse_map = np.unique(voc_staff, axis=0, return_inverse=True) + for i in range(un_voc_staff): + note_mask = inverse_map == i + notes_per_vocstaff = notes[note_mask] + # get note with min start.t + min_start_note = notes_per_vocstaff[np.argmin(notes_per_vocstaff.start.t)] + if min_start_note.start.t > start_time: + sym_dur = estimate_symbolic_duration(min_start_note.start.t - start_time, part._quarter_durations[0]) + rest = Rest(symbolic_duration=sym_dur, staff=min_start_note.staff, voice=min_start_note.voice) + part.add(rest, start_time, min_start_note.start.t) + + +def _fill_rests_global(measure: Measure, part: Part, unique_voc_staff: np.ndarray) -> None: + start_time = measure.start.t + end_time = measure.end.t + notes = np.array(list(part.iter_all(GenericNote, start_time, end_time, include_subclasses=True))) + voc_staff = np.array([[n.voice, n.staff] for n in notes]) + un_voc_staff, inverse_map = np.unique(voc_staff, axis=0, return_inverse=True) + for i in range(un_voc_staff.shape[0]): + note_mask = inverse_map == i + notes_per_vocstaff = notes[note_mask] + # get note with min start.t + min_start_note = notes_per_vocstaff[np.argmin(np.vectorize(lambda x: x.start.t)(notes_per_vocstaff))] + if min_start_note.start.t > start_time: + sym_dur = estimate_symbolic_duration(min_start_note.start.t - start_time, part._quarter_durations[0]) + rest = Rest(symbolic_duration=sym_dur, staff=min_start_note.staff, voice=min_start_note.voice) + part.add(rest, start_time, min_start_note.start.t) + + min_end_note = notes_per_vocstaff[np.argmin(np.vectorize(lambda x: x.end.t)(notes_per_vocstaff))] + if min_end_note.end.t < end_time: + sym_dur = estimate_symbolic_duration(end_time - min_end_note.end.t, part._quarter_durations[0]) + rest = Rest(symbolic_duration=sym_dur, staff=min_end_note.staff, voice=min_end_note.voice) + part.add(rest, min_end_note.end.t, end_time) + + if un_voc_staff.shape[0] != unique_voc_staff.shape[0]: + if un_voc_staff.shape[0] == 0: + diff = unique_voc_staff + else: + # View `un_voc_staff` and `unique_voc_staff` as 1-D structured arrays + x_sa = un_voc_staff.view([('', un_voc_staff.dtype)] * un_voc_staff.shape[1]) + y_sa = unique_voc_staff.view([('', unique_voc_staff.dtype)] * unique_voc_staff.shape[1]) + # Find rows in `unique_voc_staff` that are not in `un_voc_staff` + diff = np.setdiff1d(y_sa, x_sa) + for voice, staff in diff: + sym_dur = estimate_symbolic_duration(end_time - start_time, part._quarter_durations[0]) + rest = Rest(symbolic_duration=sym_dur, staff=staff, voice=voice) + part.add(rest, start_time, end_time) + + +def fill_rests(score_data: ScoreLike, measurewise=True) -> None: + """ + Fill rests in a score when a voice starts in a middle of a measure and no rest precedes. + + When measurewise is True, the voices are searched within a measure. + When measurewise is False, the rests are filled globally in the score for all voices and staffs. + + Parameters + ---------- + score_data: ScoreLike + The score to fill rests + measurewise: bool + If True, fill rests within a measure. If False, fill rests globally in the score. + """ + if isinstance(score_data, Score): + partlist = score_data.parts + else: + partlist = [score_data] + for part in partlist: + measures = part.measures + if measurewise: + for measure in measures: + _fill_rests_within_measure(measure, part) + else: + note_array = part.note_array(include_staff=True) + unique_vocstaff = np.unique( + np.array([note_array["voice"], note_array["staff"]], dtype=np.int64), axis=1 + ) + for measure in measures: + _fill_rests_global(measure, part, unique_vocstaff.T) + + def merge_parts(parts, reassign="voice"): """Merge list of parts or PartGroup into a single part. All parts are expected to have the same time signature From 203e519cacca16a935dfb905b44129e7f0cc5247 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 26 Jan 2024 19:58:43 +0100 Subject: [PATCH 056/197] added Mei exporter object with main structure. --- partitura/io/__init__.py | 2 +- partitura/io/mei_export_v2.py | 1151 +++------------------------------ 2 files changed, 107 insertions(+), 1046 deletions(-) diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index 4ad2cd47..119c6d3e 100644 --- a/partitura/io/__init__.py +++ b/partitura/io/__init__.py @@ -15,7 +15,7 @@ from .importparangonada import load_parangonada_csv from .exportparangonada import save_parangonada_csv from .importmusic21 import load_music21 - +from .mei_export_v2 import save_mei from partitura.utils.misc import ( deprecated_alias, deprecated_parameter, diff --git a/partitura/io/mei_export_v2.py b/partitura/io/mei_export_v2.py index e7891c87..7cb7c22d 100644 --- a/partitura/io/mei_export_v2.py +++ b/partitura/io/mei_export_v2.py @@ -6,943 +6,124 @@ import math from collections import defaultdict from lxml import etree -import partitura.score as score +import partitura.score as spt from operator import itemgetter from typing import Optional from partitura.utils import partition, iter_current_next, to_quarter_tempo - +import numpy as np from partitura.utils.misc import deprecated_alias, PathLike - - __all__ = ["save_mei"] -DOCTYPE = """""" # noqa: E501 -MEASURE_SEP_COMMENT = "=======================================================" -ARTICULATIONS = [ - "accent", - "breath-mark", - "caesura", - "detached-legato", - "doit", - "falloff", - "plop", - "scoop", - "spiccato", - "staccatissimo", - "staccato", - "stress", - "strong-accent", - "tenuto", - "unstress", -] - -NOTE_TYPE = { - 'whole': '1', - 'half': '2', - 'quarter': '4', - 'eighth': '8', - 'sixteenth': '16', - 'long': 'long', - 'breve': 'breve' - } - -# musicxml integer accidentals to mei -# integer of accidental is array index -ACC = [None, 's', 'ss', 'ff', 'f'] - - -def range_number_from_counter(e, label, counter): - key = (label, e) - number = counter.get(key, None) - - if number is None: - number = 1 + sum(1 for o in counter.keys() if o[0] == label) - assert number is not None - counter[key] = number - - else: - del counter[key] - - return number - - -def filter_string(s): - """ - Make (unicode) string fit for passing it to lxml, which means (at least) - removing null characters. - """ - return s.replace("\x00", "") - - -def make_note_el(note, dur, voice, counter, n_of_staves): - # child order - # | | - # - # - # - # - # - # - - note_e = etree.Element("note") - - if note.id is not None: - note_id = note.id - # make sure note_id is unique by appending _x to the note_id for the - # x-th repetition of the id - counter[note_id] = counter.get(note_id, 0) + 1 - - if counter[note_id] > 1: - note_id += "_{}".format(counter[note_id]) - - note_e.attrib["id"] = filter_string(note_id) - - if isinstance(note, score.Note): - if isinstance(note, score.GraceNote): - if note.grace_type == "acciaccatura": - etree.SubElement(note_e, "grace", slash="yes") - - else: - etree.SubElement(note_e, "grace") - - pitch_e = etree.SubElement(note_e, "pitch") - - etree.SubElement(pitch_e, "step").text = "{}".format(note.step) - - if note.alter not in (None, 0): - etree.SubElement(pitch_e, "alter").text = "{}".format(note.alter) - - etree.SubElement(pitch_e, "octave").text = "{}".format(note.octave) - - elif isinstance(note, score.UnpitchedNote): - unpitch_e = etree.SubElement(note_e, "unpitched") - - etree.SubElement(unpitch_e, "display-step").text = "{}".format(note.step) - - etree.SubElement(unpitch_e, "display-octave").text = "{}".format(note.octave) - - if note.notehead is not None: - nh_e = etree.SubElement(note_e, "notehead") - nh_e.text = "{}".format(note.notehead) - if note.noteheadstyle: - nh_e.attrib["filled"] = "yes" - else: - nh_e.attrib["filled"] = "no" - - elif isinstance(note, score.Rest): - if not note.hidden: - etree.SubElement(note_e, "rest") - - if not isinstance(note, score.GraceNote): - duration_e = etree.SubElement(note_e, "duration") - duration_e.text = "{:d}".format(int(dur)) - - notations = [] - - if note.tie_prev is not None: - etree.SubElement(note_e, "tie", type="stop") - notations.append(etree.Element("tied", type="stop")) - - if note.tie_next is not None: - etree.SubElement(note_e, "tie", type="start") - notations.append(etree.Element("tied", type="start")) - - if voice not in (None, 0): - etree.SubElement(note_e, "voice").text = "{}".format(voice) - - if note.fermata is not None: - notations.append(etree.Element("fermata")) - - if note.articulations: - articulations = [] - for articulation in note.articulations: - if articulation in ARTICULATIONS: - articulations.append(etree.Element(articulation)) - if articulations: - articulations_e = etree.Element("articulations") - articulations_e.extend(articulations) - notations.append(articulations_e) - - sym_dur = note.symbolic_duration or {} - - if sym_dur.get("type") is not None: - etree.SubElement(note_e, "type").text = sym_dur["type"] - - for i in range(sym_dur.get("dots", 0)): - etree.SubElement(note_e, "dot") - - if ( - sym_dur.get("actual_notes") is not None - and sym_dur.get("normal_notes") is not None - ): - time_mod_e = etree.SubElement(note_e, "time-modification") - actual_e = etree.SubElement(time_mod_e, "actual-notes") - actual_e.text = str(sym_dur["actual_notes"]) - normal_e = etree.SubElement(time_mod_e, "normal-notes") - normal_e.text = str(sym_dur["normal_notes"]) - - if note.staff is not None: - if note.staff != 1 or n_of_staves > 1: - etree.SubElement(note_e, "staff").text = "{}".format(note.staff) - - for slur in note.slur_stops: - number = range_number_from_counter(slur, "slur", counter) - - notations.append(etree.Element("slur", number="{}".format(number), type="stop")) - - for slur in note.slur_starts: - number = range_number_from_counter(slur, "slur", counter) - - notations.append( - etree.Element("slur", number="{}".format(number), type="start") - ) - - for tuplet in note.tuplet_stops: - tuplet_key = ("tuplet", tuplet) - number = counter.get(tuplet_key, None) - - if number is None: - number = 1 - counter[tuplet_key] = number - - else: - del counter[tuplet_key] - - notations.append( - etree.Element("tuplet", number="{}".format(number), type="stop") - ) - - for tuplet in note.tuplet_starts: - tuplet_key = ("tuplet", tuplet) - number = counter.get(tuplet_key, None) - - if number is None: - number = 1 + sum(1 for o in counter.keys() if o[0] == "tuplet") - counter[tuplet_key] = number - - else: - del counter[tuplet_key] - - notations.append( - etree.Element("tuplet", number="{}".format(number), type="start") - ) - - if notations: - notations_e = etree.SubElement(note_e, "notations") - notations_e.extend(notations) - - return note_e - - -def do_note(note, measure_end, part, voice, counter, n_of_staves): - if isinstance(note, score.GraceNote): - dur_divs = 0 - - else: - dur_divs = note.end.t - note.start.t - - note_e = make_note_el(note, dur_divs, voice, counter, n_of_staves) - - return (note.start.t, dur_divs, note_e) - - -def linearize_measure_contents(part, start, end, state): - """ - Determine the document order of events starting between `start` (inclusive) - and `end` (exlusive). (notes, directions, divisions, time signatures). This - function finds any mid-measure attribute/divisions and splits up the measure - into segments by divisions, to be linearized separately and - concatenated. The actual linearization is done by - the `linearize_segment_contents` function. - - Parameters - ---------- - start: score.TimePoint - start - end: score.TimePoint - end - part: score.Part - - Returns - ------- - list - The contents of measure in document order - """ - splits = [start] - q_times = part.quarter_durations(start.t, end.t) - if len(q_times) > 0: - quarter = start.quarter - tp = start.next - while tp and tp != end: - if tp.quarter != quarter: - splits.append(tp) - quarter = tp.quarter - tp = tp.next - - splits.append(end) - contents = [] - - for i in range(1, len(splits)): - contents.extend( - linearize_segment_contents(part, splits[i - 1], splits[i], state) - ) - - return contents - - -def remove_voice_polyphony_single(notes, voice_spans): - """ - Test wether a list of notes satisfies the MusicXML constraints on voices that: - - all notes starting at the same time have the same duration - - no is required to specify the voice in document order - whenever a note violates the constraints change its voice - (choosing a new voice that is not currently in use) - - Parameters - ---------- - notes: list - List of notes in a voice - - Returns - ------- - type - Description of return value - """ - - extraneous = defaultdict(list) - - by_onset = defaultdict(list) - for note in notes: - if not isinstance(note, score.GraceNote): - by_onset[note.start.t].append(note) - onsets = sorted(by_onset.keys()) - - for o in onsets: - chord_dur = min(n.duration for n in by_onset[o]) - - for n in by_onset[o]: - if n.duration > chord_dur: - voice = find_free_voice(voice_spans, n.start.t, n.end.t) - voice_spans.append((n.start.t, n.end.t, voice)) - extraneous[voice].append(n) - notes.remove(n) - - # now remove any notes that exceed next onset - by_onset = defaultdict(list) - for note in notes: - by_onset[note.start.t].append(note) - onsets = sorted(by_onset.keys()) - - for o1, o2 in iter_current_next(onsets): - for n in by_onset[o1]: - if o1 + n.duration > o2: - voice = find_free_voice(voice_spans, n.start.t, n.end.t) - voice_spans.append((n.start.t, n.end.t, voice)) - extraneous[voice].append(n) - notes.remove(n) - - return extraneous - - -def find_free_voice(voice_spans, start, end): - free_voice = min(voice for _, _, voice in voice_spans) + 1 - - for vstart, vend, voice in voice_spans: - if (end > vstart) and (start < vend): - free_voice = max(free_voice, voice + 1) - - return free_voice - - -def remove_voice_polyphony(notes_by_voice): - voice_spans = [(-math.inf, math.inf, max(notes_by_voice.keys()))] - extraneous = defaultdict(list) - # n_orig = sum(len(nn) for nn in notes_by_voice.values()) - - for voice, vnotes in notes_by_voice.items(): - v_extr = remove_voice_polyphony_single(vnotes, voice_spans) - - for new_voice, new_vnotes in v_extr.items(): - extraneous[new_voice].extend(new_vnotes) - - # n_1 = sum(len(nn) for nn in notes_by_voice.values()) - # n_2 = sum(len(nn) for nn in extraneous.values()) - # n_new = n_1 + n_2 - # assert n_orig == n_new - # assert len(set(notes_by_voice.keys()).intersection(set(extraneous.keys()))) == 0 - for v, vnotes in extraneous.items(): - notes_by_voice[v] = vnotes - - -# def fill_gaps_with_rests(notes_by_voice, start, end, part): -# for voice, notes in notes_by_voice.items(): -# if len(notes) == 0: -# rest = score.Rest(voice=voice or None) -# part.add(rest, start.t, end.t) -# else: -# t = start.t -# for note in notes: -# if note.start.t > t: -# rest = score.Rest(voice=voice or None) -# part.add(rest, t, note.start.t) -# t = note.end.t -# if note.end.t < end.t: -# rest = score.Rest(voice=voice or None) -# part.add(rest, note.end.t, end.t) - - -def linearize_segment_contents(part, start, end, state): - """ - Determine the document order of events starting between `start` (inclusive) - and `end` (exlusive). - (notes, directions, divisions, time signatures). - """ - - notes = part.iter_all( - score.GenericNote, start=start, end=end, include_subclasses=True - ) - - notes_by_voice = partition(lambda n: n.voice or 0, notes) - if len(notes_by_voice) == 0: - # if there are no notes in this segment, we add a rest - # NOTE: altering the part instance while exporting is bad! - # rest = score.Rest() - # part.add(start.t, rest, end.t) - # notes_by_voice = {0: [rest]} - notes_by_voice[None] = [] - - # make sure there is no polyphony within voices by assigning any violating - # notes to a new (free) voice. - remove_voice_polyphony(notes_by_voice) - - # fill_gaps_with_rests(notes_by_voice, start, end, part) - # # redo - # notes = part.iter_all(score.GenericNote, - # start=start, end=end, - # include_subclasses=True) - # notes_by_voice = partition(lambda n: n.voice or 0, notes) - - voices_e = defaultdict(list) - - for voice in sorted(notes_by_voice.keys()): - voice_notes = notes_by_voice[voice] - # sort by pitch - voice_notes.sort( - key=lambda n: n.midi_pitch 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)) - # voice_notes.sort(key=lambda n: -n.duration) - voice_notes.sort(key=lambda n: n.start.t) - - n_of_staves = part.number_of_staves - - for n in voice_notes: - if isinstance(n, score.GraceNote): - # check if it is the first in its sequence - if not n.grace_prev: - # if so we add the whole grace sequence at once to ensure - # the correct order - for m in n.iter_grace_seq(): - note_e = do_note( - m, end.t, part, voice, state["note_id_counter"], n_of_staves - ) - voices_e[voice].append(note_e) - else: - note_e = do_note( - n, end.t, part, voice, state["note_id_counter"], n_of_staves - ) - voices_e[voice].append(note_e) - - add_chord_tags(voices_e[voice]) - - harmony_e = do_harmony(part, start, end) - attributes_e = do_attributes(part, start, end) - directions_e = do_directions(part, start, end, state["range_counter"]) - prints_e = do_prints(part, start, end) - barline_e = do_barlines(part, start, end) - - other_e = harmony_e + attributes_e + directions_e + barline_e + prints_e - - contents = merge_measure_contents(voices_e, other_e, start.t) - - return contents - - -def do_prints(part, start, end): - pages = part.iter_all(score.Page, start, end) - systems = part.iter_all(score.System, start, end) - by_onset = defaultdict(dict) - for page in pages: - by_onset[page.start.t]["new-page"] = "yes" - for system in systems: - by_onset[system.start.t]["new-system"] = "yes" - result = [] - for onset, attrs in by_onset.items(): - result.append((onset, None, etree.Element("print", **attrs))) - return result - - -def do_barlines(part, start, end): - # all fermata that are not linked to a note (fermata at time end may be part - # of the current or the next measure, depending on the location attribute - # (which is stored in fermata.ref)). - fermata = [ - ferm - for ferm in part.iter_all(score.Fermata, start, end) - if ferm.ref in (None, "left", "middle", "right") - ] + [ - ferm - for ferm in part.iter_all(score.Fermata, end, end.next) - if ferm.ref in (None, "right") - ] - repeat_start = part.iter_all(score.Repeat, start, end) - repeat_end = part.iter_all(score.Repeat, start.next, end.next, mode="ending") - ending_start = part.iter_all(score.Ending, start, end) - ending_end = part.iter_all(score.Ending, start.next, end.next, mode="ending") - by_onset = defaultdict(list) - - for obj in fermata: - by_onset[obj.start.t].append(etree.Element("fermata")) - - for obj in repeat_start: - if obj.start is not None: - by_onset[obj.start.t].append(etree.Element("repeat", direction="forward")) - - for obj in ending_start: - if obj.start is not None: - by_onset[obj.start.t].append( - etree.Element("ending", type="start", number=str(obj.number)) - ) - - for obj in repeat_end: - if obj.end is not None: - by_onset[obj.end.t].append(etree.Element("repeat", direction="backward")) - - for obj in ending_end: - if obj.end is not None: - by_onset[obj.end.t].append( - etree.Element("ending", type="stop", number=str(obj.number)) - ) - - result = [] - - for onset in sorted(by_onset.keys()): - attrib = {} - - if onset == start.t: - attrib["location"] = "left" - - elif onset == end.t: - attrib["location"] = "right" - - else: - attrib["location"] = "middle" - - barline_e = etree.Element("barline", **attrib) - - barline_e.extend(by_onset[onset]) - result.append((onset, None, barline_e)) - - return result - - -def add_chord_tags(notes): - prev_dur = None - prev = None - for onset, dur, note in notes: - if onset == prev: - if dur == prev_dur: - note.insert(0, etree.Element("chord")) - - if any(e.tag == "grace" for e in note): - # if note is a grace note we don't want to trigger a chord for the - # next note - prev = None - else: - prev = onset - prev_dur = dur - - -def forward_backup_if_needed(t, t_prev): - result = [] - gap = 0 - - if t > t_prev: - gap = t - t_prev - e = etree.Element("forward") - ee = etree.SubElement(e, "duration") - ee.text = "{:d}".format(int(gap)) - result.append((t_prev, gap, e)) - - elif t < t_prev: - gap = t_prev - t - e = etree.Element("backup") - ee = etree.SubElement(e, "duration") - ee.text = "{:d}".format(int(gap)) - result.append((t_prev, -gap, e)) - - return result, gap - - -def merge_with_voice(notes, other, measure_start): - by_onset = defaultdict(list) - - for onset, dur, el in notes: - by_onset[onset].append((dur, el)) - - for onset, dur, el in other: - by_onset[onset].append((dur, el)) - - result = [] - last_t = measure_start - fb_cost = 0 - # order to insert simultaneously starting elements; it is important to put - # notes last, since they update the position, and thus would lead to - # needless backup/forward insertions - order = { - "barline": 0, - "attributes": 1, - "direction": 2, - "print": 3, - "sound": 4, - "harmony": 5, - "note": 6, - } - last_note_onset = measure_start - - for onset in sorted(by_onset.keys()): - elems = by_onset[onset] - elems.sort(key=lambda x: order.get(x[1].tag, len(order))) - - for dur, el in elems: - if el.tag == "note": - if el.find("chord") is not None: - last_t = last_note_onset - - last_note_onset = onset - - els, cost = forward_backup_if_needed(onset, last_t) - fb_cost += cost - result.extend(els) - result.append((onset, dur, el)) - last_t = onset + (dur or 0) - - return result, fb_cost - - -def merge_measure_contents(notes, other, measure_start): - merged = {} - # cost (measured as the total forward/backup jumps needed to merge) all - # elements in `other` into each voice - cost = {} - - for voice in sorted(notes.keys()): - # merge `other` with each voice, and keep track of the cost - merged[voice], cost[voice] = merge_with_voice( - notes[voice], other, measure_start - ) - - if not merged: - merged[0] = [] - 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] - result = [] - pos = measure_start - for i, voice in enumerate(sorted(notes.keys())): - if voice == merge_voice: - elements = merged[voice] - - else: - elements = notes[voice] - - # backup/forward when switching voices if necessary - if elements: - gap = elements[0][0] - pos - - if gap < 0: - e = etree.Element("backup") - ee = etree.SubElement(e, "duration") - ee.text = "{:d}".format(-int(gap)) - result.append(e) - - elif gap > 0: - e = etree.Element("forward") - ee = etree.SubElement(e, "duration") - ee.text = "{:d}".format(gap) - result.append(e) - - result.extend([e for _, _, e in elements]) - - # update current position - if elements: - pos = elements[-1][0] + (elements[-1][1] or 0) - - return result - - -def do_directions(part, start, end, counter): - result = [] - - # ending directions - directions = part.iter_all( - score.DynamicDirection, - start.next, - end.next, - include_subclasses=True, - mode="ending", - ) - - for direction in directions: - text = direction.raw_text or direction.text - e0 = etree.Element("direction") - e1 = etree.SubElement(e0, "direction-type") - - if getattr(direction, "wedge", False): - number = range_number_from_counter(direction, "wedge", counter) - e2 = etree.SubElement(e1, "wedge", number="{}".format(number), type="stop") - - else: - number = range_number_from_counter(direction, "wedge", counter) - etree.SubElement(e1, "dashes", number="{}".format(number), type="stop") - - elem = (direction.end.t, None, e0) - result.append(elem) - - tempos = part.iter_all(score.Tempo, start, end) - directions = part.iter_all(score.Direction, start, end, include_subclasses=True) - - for tempo in tempos: - # e0 = etree.Element('direction') - # e1 = etree.SubElement(e0, 'direction-type') - # e2 = etree.SubElement(e1, 'words') - unit = "q" if tempo.unit is None else tempo.unit - # e2.text = '{}={}'.format(unit, tempo.bpm) - # result.append((tempo.start.t, None, e0)) - e3 = etree.Element( - "sound", tempo="{}".format(int(to_quarter_tempo(unit, tempo.bpm))) - ) - result.append((tempo.start.t, None, e3)) - - for direction in directions: - text = direction.raw_text or direction.text - - if text in PEDAL_DIRECTIONS: - # Pedal directions create an element for start - # and an element for ending - - # Use end of the segment as ending of the pedal sign - ped_end = end if direction.end is None else direction.end - - # Create a pedal start element - if direction.start.t >= start.t: - e0s = etree.Element("direction", placement="below") - e1s = etree.SubElement(e0s, "direction-type") - # For sustain pedals - if isinstance(direction, score.SustainPedalDirection): - pedal_kwargs = {} - if direction.line: - pedal_kwargs["line"] = "yes" - # For Flake8 (ignore unused variable), since - # etree.SubElement adds e2s to e1s - e2s = etree.SubElement( # noqa: F841 - e1s, "pedal", type="start", **pedal_kwargs - ) - if direction.staff is not None and direction.staff != 1: - e3s = etree.SubElement(e0s, "staff") - e3s.text = str(direction.staff) - elem = (direction.start.t, None, e0s) - result.append(elem) - if ped_end.t <= end.t: - e0e = etree.Element("direction", placement="below") - e1e = etree.SubElement(e0e, "direction-type") - if isinstance(direction, score.SustainPedalDirection): - pedal_kwargs = {} - if direction.line: - pedal_kwargs["line"] = "yes" +ALTER_TO_MEI = { + -2: "ff", + -1: "f", + 0: "n", + 1: "s", + 2: "ss", +} + +DOCTYPE = ' /n \n ' + +class MEIExporter: + def __init__(self, part): + self.part = part + self.element_counter = 0 + + def elc_id(self): + # transforms an integer number to 8-digit string + # The number is right aligned and padded with zeros + return str(self.element_counter).zfill(10) + + def export_to_mei(self): + # Create root MEI element + mei = etree.Element('mei') + + # Create child elements + mei_head = etree.SubElement(mei, 'meiHead') + file_desc = etree.SubElement(mei_head, 'fileDesc') + music = etree.SubElement(mei, 'music') + body = etree.SubElement(music, 'body') + mdiv = etree.SubElement(body, 'mdiv') + score = etree.SubElement(mdiv, 'score') + score_def = etree.SubElement(score, 'scoreDef') + staff_grp = etree.SubElement(score_def, 'staffGrp') + staff_def = etree.SubElement(staff_grp, 'staffDef') + section = etree.SubElement(score, 'section') + + # Iterate over part's timeline + for measure in self.part.measures: + # Create measure element + xml_el = etree.SubElement(section, 'measure') + self._handle_measure(measure, xml_el) + + return mei + + def _handle_measure(self, measure, xml_el): + # Add measure number + xml_el.set('n', str(measure.number)) + xml_el.set('id', "measure-" + self.elc_id()) + note_or_rest_elements = np.array(list(self.part.iter_all(spt.GenericNote, start=measure.start.t, end=measure.end.t, include_subclasses=True))) + # Separate by staff + staffs = np.vectorize(lambda x: x.staff)(note_or_rest_elements) + unique_staffs, staff_inverse_map = np.unique(staffs, return_inverse=True) + for i, staff in enumerate(unique_staffs): + staff_el = etree.SubElement(xml_el, 'staff') + # Add staff number + staff_el.set('n', str(staff)) + staff_el.set('id', "staff-" + self.elc_id()) + staff_notes = note_or_rest_elements[staff_inverse_map == i] + # Separate by voice + voices = np.vectorize(lambda x: x.voice)(staff_notes) + unique_voices, voice_inverse_map = np.unique(voices, return_inverse=True) + for j, voice in enumerate(unique_voices): + voice_el = etree.SubElement(staff_el, 'layer') + voice_el.set('n', str(voice)) + voice_el.set('id', "voice-" + self.elc_id()) + voice_notes = staff_notes[voice_inverse_map == j] + # Sort by onset + voice_notes = sorted(voice_notes, key=lambda x: x.start.t) + # group by start time + for _, group in iter_current_next(voice_notes, key=lambda x: x.start.t): + if len(group) == 1: + self._handle_note_or_rest(group[0], voice_el) else: - pedal_kwargs["sign"] = "yes" - # For Flake8 (ignore unused variable), since - # etree.SubElement adds e2e to e1e - e2e = etree.SubElement( # noqa: F841 - e1e, "pedal", type="end", **pedal_kwargs - ) - if direction.staff is not None and direction.staff != 1: - e3e = etree.SubElement(e0e, "staff") - e3e.text = str(direction.staff) - elem = (ped_end.t, None, e0e) - result.append(elem) - else: - e0 = etree.Element("direction") - e1 = etree.SubElement(e0, "direction-type") - - if text in DYN_DIRECTIONS: - e2 = etree.SubElement(e1, "dynamics") - etree.SubElement(e2, text) - - elif getattr(direction, "wedge", False): - if isinstance(direction, score.IncreasingLoudnessDirection): - wtype = "crescendo" - else: - wtype = "diminuendo" - - number = range_number_from_counter(direction, "wedge", counter) - e2 = etree.SubElement( - e1, "wedge", number="{}".format(number), type=wtype - ) - - else: - e2 = etree.SubElement(e1, "words") - e2.text = filter_string(text) - - if ( - isinstance(direction, score.DynamicDirection) - and direction.end is not None - ): - e3 = etree.SubElement(e0, "direction-type") - number = range_number_from_counter(direction, "dashes", counter) - etree.SubElement( - e3, "dashes", number="{}".format(number), type="start" - ) - - if direction.staff is not None and direction.staff != 1: - e5 = etree.SubElement(e0, "staff") - e5.text = str(direction.staff) - - elem = (direction.start.t, None, e0) - result.append(elem) - - return result - - -def do_harmony(part, start, end): - """ - Produce xml objects for harmony (Roman Numeral Text) - """ - harmony = part.iter_all(score.RomanNumeral, start, end) - result = [] - for h in harmony: - harmony_e = etree.Element("harmony", print_frame="no") - function = etree.SubElement(harmony_e, "function") - function.text = h.text - kind_e = etree.SubElement(harmony_e, "kind", text="") - kind_e.text = "none" - result.append((h.start.t, None, harmony_e)) - harmony = part.iter_all(score.ChordSymbol, start, end) - for h in harmony: - harmony_e = etree.Element("harmony", print_frame="no") - kind_e = ( - etree.SubElement(harmony_e, "kind", text=h.kind) - if h.kind is not None - else etree.SubElement(harmony_e, "kind", text="") - ) - kind_e.text = "none" - root_e = etree.SubElement(harmony_e, "root") - root_step_e = etree.SubElement(root_e, "root-step") - root_step_e.text = h.root - if h.bass is not None: - bass_e = etree.SubElement(harmony_e, "bass") - bass_step_e = etree.SubElement(bass_e, "bass-step") - bass_step_e.text = h.bass - result.append((h.start.t, None, harmony_e)) - return result - - -def do_attributes(part, start, end): - """ - Produce xml objects for non-note measure content - - Parameters - ---------- - others: type - Description of `others` - - Returns - ------- - type - Description of return value - """ - - by_start = defaultdict(list) - - # for o in part.iter_all(score.Divisions, start, end): - # by_start[o.start.t].append(o) - for t, quarter in part.quarter_durations(start.t, end.t): - by_start[t].append(int(quarter)) - for o in part.iter_all(score.KeySignature, start, end): - by_start[o.start.t].append(o) - for o in part.iter_all(score.TimeSignature, start, end): - by_start[o.start.t].append(o) - for o in part.iter_all(score.Staff, start, end): - by_start[o.start.t].append(o) - - # sort clefs by number before adding them to by_start - clefs_by_start = defaultdict(list) - - for o in part.iter_all(score.Clef, start, end): - clefs_by_start[o.start.t].append(o) + self._handle_chord(group, voice_el) - for t, clefs in clefs_by_start.items(): - clefs.sort(key=lambda clef: getattr(clef, "number", 0)) - by_start[t].extend(clefs) + return xml_el - result = [] + def _handle_chord(self, chord, xml_voice_el): + chord_el = etree.SubElement(xml_voice_el, 'chord') + chord_el.set('id', "chord-" + self.elc_id()) + for note in chord: + self._handle_note_or_rest(note, chord_el) - # hacky: flag to include staves element before the first clef - staves_included = False - - for t in sorted(by_start.keys()): - attr_e = etree.Element("attributes") - - for o in by_start[t]: - if isinstance(o, int): - etree.SubElement(attr_e, "divisions").text = "{}".format(o) - - elif isinstance(o, score.KeySignature): - ks_e = etree.SubElement(attr_e, "key") - etree.SubElement(ks_e, "fifths").text = "{}".format(o.fifths) - - if o.mode: - etree.SubElement(ks_e, "mode").text = "{}".format(o.mode) - - elif isinstance(o, score.TimeSignature): - ts_e = etree.SubElement(attr_e, "time") - etree.SubElement(ts_e, "beats").text = "{}".format(o.beats) - etree.SubElement(ts_e, "beat-type").text = "{}".format(o.beat_type) - - elif isinstance(o, score.Clef): - if not staves_included: - staves_e = etree.SubElement(attr_e, "staves") - staves_e.text = "{}".format(len(clefs)) - staves_included = True - - clef_e = etree.SubElement(attr_e, "clef") - - if o.staff and o.staff != 1: - clef_e.set("number", "{}".format(o.staff)) - - etree.SubElement(clef_e, "sign").text = "{}".format(o.sign) - etree.SubElement(clef_e, "line").text = "{}".format(o.line) + def _handle_note_or_rest(self, note, xml_voice_el): + if isinstance(note, spt.Rest): + self._handle_rest(note, xml_voice_el) + else: + self._handle_note(note, xml_voice_el) - if o.octave_change: - etree.SubElement(clef_e, "clef-octave-change").text = "{}".format( - o.octave_change - ) - elif isinstance(o, score.Staff): - staff_e = etree.SubElement(attr_e, "staff-details") - if o.lines: - etree.SubElement(staff_e, "staff-lines").text = "{}".format(o.lines) + def _handle_rest(self, rest, xml_voice_el): + rest_el = etree.SubElement(xml_voice_el, 'rest') + rest_el.set('dur', str(rest.duration)) + rest_el.set('id', "rest-" + self.elc_id()) - result.append((t, None, attr_e)) + def _handle_note(self, note, xml_voice_el): + note_el = etree.SubElement(xml_voice_el, 'note') + note_el.set('dur', str(note.duration)) + note_el.set('id', "note-" + self.elc_id()) + note_el.set('oct', str(note.octave)) + note_el.set('pname', note.step.lower()) + if note.alter is not None: + accidental = etree.SubElement(note_el, 'accid') + accidental.set('id', "accid-" + self.elc_id()) + accidental.set('accid', ALTER_TO_MEI[note.alter]) - return result @deprecated_alias(parts="score_data") def save_mei( - score_data: score.ScoreLike, + score_data: spt.ScoreLike, out: Optional[PathLike] = None, ) -> Optional[str]: """ @@ -964,133 +145,13 @@ def save_mei( MEI data as a string. Otherwise the function returns None. """ - if not isinstance(score_data, score.Score): - score_data = score.Score( - id=None, - partlist=score_data, - ) - - root = etree.Element("score-partwise") - - partlist_e = etree.SubElement(root, "part-list") - state = { - "note_id_counter": {}, - "range_counter": {}, - } - - group_stack = [] - - def close_group_stack(): - while group_stack: - # close group - etree.SubElement( - partlist_e, - "part-group", - number="{}".format(group_stack[-1].number), - type="stop", - ) - # remove from stack - group_stack.pop() - - def handle_parents(part): - # 1. get deepest parent that is in group_stack (keep track of parents to - # add) - pg = part.parent - to_add = [] - while pg: - if pg in group_stack: - break - to_add.append(pg) - pg = pg.parent + if isinstance(score_data, spt.Score): + score_data = spt.merge_parts(score_data.parts) - # close groups while not equal to pg - while group_stack: - if pg == group_stack[-1]: - break - else: - # close group - etree.SubElement( - partlist_e, - "part-group", - number="{}".format(group_stack[-1].number), - type="stop", - ) - # remove from stack - group_stack.pop() - - # start all parents in to_add - for pg in reversed(to_add): - # start group - pg_e = etree.SubElement( - partlist_e, "part-group", number="{}".format(pg.number), type="start" - ) - if pg.group_symbol is not None: - symb_e = etree.SubElement(pg_e, "group-symbol") - symb_e.text = pg.group_symbol - if pg.group_name is not None: - name_e = etree.SubElement(pg_e, "group-name") - name_e.text = pg.group_name - - group_stack.append(pg) - - for part in score_data: - handle_parents(part) - - # handle part list entry - scorepart_e = etree.SubElement(partlist_e, "score-part", id=part.id) - - partname_e = etree.SubElement(scorepart_e, "part-name") - if part.part_name: - partname_e.text = filter_string(part.part_name) - - if part.part_abbreviation: - partabbrev_e = etree.SubElement(scorepart_e, "part-abbreviation") - partabbrev_e.text = filter_string(part.part_abbreviation) - - # write the part itself - - part_e = etree.SubElement(root, "part", id=part.id) - - for measure in part.iter_all(score.Measure): - part_e.append(etree.Comment(MEASURE_SEP_COMMENT)) - attrib = {} - - if measure.number is not None: - attrib["number"] = str(measure.number) - - measure_e = etree.SubElement(part_e, "measure", **attrib) - contents = linearize_measure_contents( - part, measure.start, measure.end, state - ) - measure_e.extend(contents) - - close_group_stack() - - if out: - if hasattr(out, "write"): - out.write( - etree.tostring( - root.getroottree(), - encoding="UTF-8", - xml_declaration=True, - pretty_print=True, - doctype=DOCTYPE, - ) - ) - - else: - with open(out, "wb") as f: - f.write( - etree.tostring( - root.getroottree(), - encoding="UTF-8", - xml_declaration=True, - pretty_print=True, - doctype=DOCTYPE, - ) - ) + exporter = MEIExporter(score_data) + root = exporter.export_to_mei() - else: + if out is None: return etree.tostring( root.getroottree(), encoding="UTF-8", From 0768907b85b6845ceab08a04b1052b75b387fac4 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 26 Jan 2024 20:12:39 +0100 Subject: [PATCH 057/197] changed name from load_tsv to load_dcml --- partitura/__init__.py | 4 ++-- partitura/io/__init__.py | 2 +- partitura/io/importdcml.py | 4 +++- tests/test_dcml_import.py | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/partitura/__init__.py b/partitura/__init__.py index 958de256..ae7efacf 100644 --- a/partitura/__init__.py +++ b/partitura/__init__.py @@ -15,7 +15,7 @@ from .io.importmei import load_mei from .io.importkern import load_kern from .io.importmusic21 import load_music21 -from .io.importdcml import load_tsv +from .io.importdcml import load_dcml from .io.importmidi import load_score_midi, load_performance_midi, midi_to_notearray from .io.exportmidi import save_score_midi, save_performance_midi from .io.importmatch import load_match @@ -57,7 +57,7 @@ "save_performance_midi", "load_match", "save_match", - "load_tsv", + "load_dcml", "load_nakamuramatch", "load_nakamuracorresp", "load_parangonada_csv", diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index ea22d920..320dee5e 100644 --- a/partitura/io/__init__.py +++ b/partitura/io/__init__.py @@ -15,7 +15,7 @@ from .importparangonada import load_parangonada_csv from .exportparangonada import save_parangonada_csv from .importmusic21 import load_music21 -from .importdcml import load_tsv +from .importdcml import load_dcml from partitura.utils.misc import ( diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 6aa87652..216cbe81 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -9,6 +9,8 @@ def read_note_tsv(note_tsv_path, metadata=None): + # data = np.genfromtxt(note_tsv_path, delimiter="\t", dtype=None, names=True, invalid_raise=False) + # unique_durations = np.unique(data["duration"]) data = pd.read_csv(note_tsv_path, sep="\t") unique_durations = data["duration"].unique() denominators = [int(qb.split("/")[1]) for qb in unique_durations if "/" in qb] @@ -184,7 +186,7 @@ def read_harmony_tsv(beat_tsv_path, part): return -def load_tsv(note_tsv_path, measure_tsv_path=None, harmony_tsv_path=None, metadata=None): +def load_dcml(note_tsv_path, measure_tsv_path=None, harmony_tsv_path=None, metadata=None): """ Load a score from tsv files containing the notes, measures and harmony annotations. diff --git a/tests/test_dcml_import.py b/tests/test_dcml_import.py index 14068e7a..fe765a11 100644 --- a/tests/test_dcml_import.py +++ b/tests/test_dcml_import.py @@ -1,5 +1,5 @@ import unittest -from partitura import load_tsv +from partitura import load_dcml from tests import TSV_PATH import os @@ -9,7 +9,7 @@ def test_tsv_import_from_dcml(self): note_path = os.path.join(TSV_PATH, "test_notes.tsv") measure_path = os.path.join(TSV_PATH, "test_measures.tsv") harmony_path = os.path.join(TSV_PATH, "test_harmonies.tsv") - score = load_tsv(note_path, measure_path, harmony_path) + score = load_dcml(note_path, measure_path, harmony_path) self.assertEqual(len(score.parts), 1) From d5d2316dcb41a4384d473637677ee35c605f79ab Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 27 Jan 2024 15:52:36 +0100 Subject: [PATCH 058/197] Minor corrections for adding rests. --- partitura/score.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/partitura/score.py b/partitura/score.py index 8ceee9b7..b502f44a 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -4818,10 +4818,18 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: rest = Rest(symbolic_duration=sym_dur, staff=min_start_note.staff, voice=min_start_note.voice) part.add(rest, start_time, min_start_note.start.t) + min_end_note = notes_per_vocstaff[np.argmin(np.vectorize(lambda x: x.end.t)(notes_per_vocstaff))] + if min_end_note.end.t < end_time: + sym_dur = estimate_symbolic_duration(end_time - min_end_note.end.t, part._quarter_durations[0]) + rest = Rest(symbolic_duration=sym_dur, staff=min_end_note.staff, voice=min_end_note.voice) + part.add(rest, min_end_note.end.t, end_time) + def _fill_rests_global(measure: Measure, part: Part, unique_voc_staff: np.ndarray) -> None: start_time = measure.start.t end_time = measure.end.t + if end_time - start_time == 0: + return notes = np.array(list(part.iter_all(GenericNote, start_time, end_time, include_subclasses=True))) voc_staff = np.array([[n.voice, n.staff] for n in notes]) un_voc_staff, inverse_map = np.unique(voc_staff, axis=0, return_inverse=True) @@ -4835,7 +4843,7 @@ def _fill_rests_global(measure: Measure, part: Part, unique_voc_staff: np.ndarra rest = Rest(symbolic_duration=sym_dur, staff=min_start_note.staff, voice=min_start_note.voice) part.add(rest, start_time, min_start_note.start.t) - min_end_note = notes_per_vocstaff[np.argmin(np.vectorize(lambda x: x.end.t)(notes_per_vocstaff))] + min_end_note = notes_per_vocstaff[np.argmax(np.vectorize(lambda x: x.end.t)(notes_per_vocstaff))] if min_end_note.end.t < end_time: sym_dur = estimate_symbolic_duration(end_time - min_end_note.end.t, part._quarter_durations[0]) rest = Rest(symbolic_duration=sym_dur, staff=min_end_note.staff, voice=min_end_note.voice) From 21a9151250a897c2c52c0bacfec7f29d69d958d4 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 27 Jan 2024 15:52:54 +0100 Subject: [PATCH 059/197] Updated test for kern import/export with temporary directory. --- tests/test_kern.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/test_kern.py b/tests/test_kern.py index ff09a261..e6293930 100644 --- a/tests/test_kern.py +++ b/tests/test_kern.py @@ -8,6 +8,7 @@ import partitura import os from tests import KERN_TESTFILES, KERN_TIES, KERN_PATH +from tempfile import TemporaryDirectory from partitura.score import merge_parts from partitura.utils import ensure_notearray from partitura.io.importkern_v2 import load_kern @@ -56,9 +57,17 @@ def test_spline_splitting(self): def test_import_export(self): imported_score = load_kern(partitura.EXAMPLE_KERN) - exported_score = save_kern(imported_score) - x = np.loadtxt(partitura.EXAMPLE_KERN, comments="!", dtype=str, encoding="utf-8", delimiter="\t") - self.assertTrue(np.all(x == exported_score.to_kern())) + with TemporaryDirectory() as tmpdir: + out = os.path.join(tmpdir, "test.match") + save_kern(imported_score, out) + exported_score = load_kern(out) + im_na = imported_score.note_array(include_staff=True) + ex_na = exported_score.note_array(include_staff=True) + self.assertTrue(np.all(im_na["onset_beat"] == ex_na["onset_beat"])) + self.assertTrue(np.all(im_na["duration_beat"] == ex_na["duration_beat"])) + self.assertTrue(np.all(im_na["pitch"] == ex_na["pitch"])) + self.assertTrue(np.all(im_na["staff"] == ex_na["staff"])) + # NOTE: Voices are not the same because of the way voices are assigned in merge_parts # if __name__ == "__main__": From a74f4a97ebb35fbedfaa8ec1ba6f98ee6da41e6a Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 27 Jan 2024 19:04:50 +0100 Subject: [PATCH 060/197] Fixed group by of notes within voice. --- partitura/io/mei_export_v2.py | 55 ++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/partitura/io/mei_export_v2.py b/partitura/io/mei_export_v2.py index 7cb7c22d..5f9939e7 100644 --- a/partitura/io/mei_export_v2.py +++ b/partitura/io/mei_export_v2.py @@ -8,10 +8,13 @@ from lxml import etree import partitura.score as spt from operator import itemgetter +from itertools import groupby from typing import Optional from partitura.utils import partition, iter_current_next, to_quarter_tempo import numpy as np from partitura.utils.misc import deprecated_alias, PathLike +from partitura.utils.music import MEI_DURS_TO_SYMBOLIC + __all__ = ["save_mei"] @@ -23,7 +26,9 @@ 2: "ss", } -DOCTYPE = ' /n \n ' +SYMBOLIC_TYPES_TO_MEI_DURS = {v: k for k, v in MEI_DURS_TO_SYMBOLIC.items()} + +DOCTYPE = '\n' class MEIExporter: def __init__(self, part): @@ -33,7 +38,9 @@ def __init__(self, part): def elc_id(self): # transforms an integer number to 8-digit string # The number is right aligned and padded with zeros - return str(self.element_counter).zfill(10) + out = str(self.element_counter).zfill(10) + self.element_counter += 1 + return out def export_to_mei(self): # Create root MEI element @@ -82,13 +89,15 @@ def _handle_measure(self, measure, xml_el): voice_el.set('id', "voice-" + self.elc_id()) voice_notes = staff_notes[voice_inverse_map == j] # Sort by onset - voice_notes = sorted(voice_notes, key=lambda x: x.start.t) - # group by start time - for _, group in iter_current_next(voice_notes, key=lambda x: x.start.t): - if len(group) == 1: - self._handle_note_or_rest(group[0], voice_el) + note_start_times = np.vectorize(lambda x: x.start.t)(voice_notes) + unique_onsets = np.unique(note_start_times) + for onset in unique_onsets: + # group by start time + notes = voice_notes[note_start_times == onset] + if len(notes) > 1: + self._handle_chord(notes, voice_el) else: - self._handle_chord(group, voice_el) + self._handle_note_or_rest(notes[0], voice_el) return xml_el @@ -106,12 +115,12 @@ def _handle_note_or_rest(self, note, xml_voice_el): def _handle_rest(self, rest, xml_voice_el): rest_el = etree.SubElement(xml_voice_el, 'rest') - rest_el.set('dur', str(rest.duration)) + rest_el.set('dur', SYMBOLIC_TYPES_TO_MEI_DURS[rest.symbolic_duration["type"]]) rest_el.set('id', "rest-" + self.elc_id()) def _handle_note(self, note, xml_voice_el): note_el = etree.SubElement(xml_voice_el, 'note') - note_el.set('dur', str(note.duration)) + note_el.set('dur', SYMBOLIC_TYPES_TO_MEI_DURS[note.symbolic_duration["type"]]) note_el.set('id', "note-" + self.elc_id()) note_el.set('oct', str(note.octave)) note_el.set('pname', note.step.lower()) @@ -151,7 +160,31 @@ def save_mei( exporter = MEIExporter(score_data) root = exporter.export_to_mei() - if out is None: + if out: + if hasattr(out, "write"): + out.write( + etree.tostring( + root.getroottree(), + encoding="UTF-8", + xml_declaration=True, + pretty_print=True, + doctype=DOCTYPE, + ) + ) + + else: + with open(out, "wb") as f: + f.write( + etree.tostring( + root.getroottree(), + encoding="UTF-8", + xml_declaration=True, + pretty_print=True, + doctype=DOCTYPE, + ) + ) + + else: return etree.tostring( root.getroottree(), encoding="UTF-8", From 81a9b9ef07ed1dd77084b28426d98cd6888d3e50 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 27 Jan 2024 19:37:07 +0100 Subject: [PATCH 061/197] Added staff definition including key signature, time signature and clefs in the begining of the piece. --- partitura/io/mei_export_v2.py | 75 ++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/partitura/io/mei_export_v2.py b/partitura/io/mei_export_v2.py index 5f9939e7..9e555451 100644 --- a/partitura/io/mei_export_v2.py +++ b/partitura/io/mei_export_v2.py @@ -26,6 +26,39 @@ 2: "ss", } +FIFTHS_AND_MODE_TO_PNAME = { + (-7, "major"): "Cb", + (-6, "major"): "Gb", + (-5, "major"): "Db", + (-4, "major"): "Ab", + (-3, "major"): "Eb", + (-2, "major"): "Bb", + (-1, "major"): "F", + (0, "major"): "C", + (1, "major"): "G", + (2, "major"): "D", + (3, "major"): "A", + (4, "major"): "E", + (5, "major"): "B", + (6, "major"): "F#", + (7, "major"): "C#", + (-7, "minor"): "ab", + (-6, "minor"): "eb", + (-5, "minor"): "bb", + (-4, "minor"): "f", + (-3, "minor"): "c", + (-2, "minor"): "g", + (-1, "minor"): "d", + (0, "minor"): "a", + (1, "minor"): "e", + (2, "minor"): "b", + (3, "minor"): "f#", + (4, "minor"): "c#", + (5, "minor"): "g#", + (6, "minor"): "d#", + (7, "minor"): "a#", +} + SYMBOLIC_TYPES_TO_MEI_DURS = {v: k for k, v in MEI_DURS_TO_SYMBOLIC.items()} DOCTYPE = '\n' @@ -55,7 +88,8 @@ def export_to_mei(self): score = etree.SubElement(mdiv, 'score') score_def = etree.SubElement(score, 'scoreDef') staff_grp = etree.SubElement(score_def, 'staffGrp') - staff_def = etree.SubElement(staff_grp, 'staffDef') + self._handle_staffs(staff_grp) + section = etree.SubElement(score, 'section') # Iterate over part's timeline @@ -66,6 +100,45 @@ def export_to_mei(self): return mei + def _handle_staffs(self, xml_el): + clefs = self.part.iter_all(spt.Clef, start=0, end=1) + clefs = {c.staff: c for c in clefs} + key_sigs = list(self.part.iter_all(spt.KeySignature, start=0, end=1)) + keys_sig = key_sigs[0] if len(key_sigs) > 0 else None + time_sigs = list(self.part.iter_all(spt.TimeSignature, start=0, end=1)) + time_sig = time_sigs[0] if len(time_sigs) > 0 else None + for staff_num in range(self.part.number_of_staves): + staff_num += 1 + staff_def = etree.SubElement(xml_el, 'staffDef') + staff_def.set('n', str(staff_num)) + staff_def.set('id', "staffdef-" + self.elc_id()) + staff_def.set('lines', '5') + # Get clef for this staff If no cleff is available for this staff, default to "G2" + clef_def = etree.SubElement(staff_def, 'clef') + clef_def.set('id', "clef-" + self.elc_id()) + clef_shape = clefs[staff_num].step if staff_num in clefs.keys() else "G" + clef_def.set('shape', str(clef_shape)) + clef_def.set('line', str(clefs[staff_num].line)) if staff_num in clefs.keys() else clef_def.set('line', '2') + # Get key signature for this staff + if keys_sig is not None: + ks_def = etree.SubElement(staff_def, 'keySig') + ks_def.set('id', "keysig-" + self.elc_id()) + ks_def.set('mode', keys_sig.mode) + if keys_sig.fifths == 0: + ks_def.set('sig', '0') + elif keys_sig.fifths > 0: + ks_def.set('sig', str(keys_sig.fifths) + 's') + else: + ks_def.set('sig', str(abs(keys_sig.fifths)) + 'f') + # Find the pname from the number of sharps or flats and the mode + ks_def.set('pname', FIFTHS_AND_MODE_TO_PNAME[(keys_sig.fifths, keys_sig.mode)]) + + if time_sig is not None: + ts_def = etree.SubElement(staff_def, 'meterSig') + ts_def.set('id', "msig-" + self.elc_id()) + ts_def.set('count', str(time_sig.beats)) + ts_def.set('unit', str(time_sig.beat_type)) + def _handle_measure(self, measure, xml_el): # Add measure number xml_el.set('n', str(measure.number)) From 30671321b119d2f91ef39537aa7b3fc15394329b Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sun, 28 Jan 2024 16:22:07 +0100 Subject: [PATCH 062/197] Added function fifth and mode to pname. --- partitura/io/mei_export_v2.py | 39 +++-------------------------------- 1 file changed, 3 insertions(+), 36 deletions(-) diff --git a/partitura/io/mei_export_v2.py b/partitura/io/mei_export_v2.py index 9e555451..0a400ef6 100644 --- a/partitura/io/mei_export_v2.py +++ b/partitura/io/mei_export_v2.py @@ -10,7 +10,7 @@ from operator import itemgetter from itertools import groupby from typing import Optional -from partitura.utils import partition, iter_current_next, to_quarter_tempo +from partitura.utils import partition, iter_current_next, to_quarter_tempo, fifths_mode_to_key_name import numpy as np from partitura.utils.misc import deprecated_alias, PathLike from partitura.utils.music import MEI_DURS_TO_SYMBOLIC @@ -26,39 +26,6 @@ 2: "ss", } -FIFTHS_AND_MODE_TO_PNAME = { - (-7, "major"): "Cb", - (-6, "major"): "Gb", - (-5, "major"): "Db", - (-4, "major"): "Ab", - (-3, "major"): "Eb", - (-2, "major"): "Bb", - (-1, "major"): "F", - (0, "major"): "C", - (1, "major"): "G", - (2, "major"): "D", - (3, "major"): "A", - (4, "major"): "E", - (5, "major"): "B", - (6, "major"): "F#", - (7, "major"): "C#", - (-7, "minor"): "ab", - (-6, "minor"): "eb", - (-5, "minor"): "bb", - (-4, "minor"): "f", - (-3, "minor"): "c", - (-2, "minor"): "g", - (-1, "minor"): "d", - (0, "minor"): "a", - (1, "minor"): "e", - (2, "minor"): "b", - (3, "minor"): "f#", - (4, "minor"): "c#", - (5, "minor"): "g#", - (6, "minor"): "d#", - (7, "minor"): "a#", -} - SYMBOLIC_TYPES_TO_MEI_DURS = {v: k for k, v in MEI_DURS_TO_SYMBOLIC.items()} DOCTYPE = '\n' @@ -123,7 +90,7 @@ def _handle_staffs(self, xml_el): if keys_sig is not None: ks_def = etree.SubElement(staff_def, 'keySig') ks_def.set('id', "keysig-" + self.elc_id()) - ks_def.set('mode', keys_sig.mode) + ks_def.set('mode', keys_sig.mode) if keys_sig.mode is not None else ks_def.set('mode', 'major') if keys_sig.fifths == 0: ks_def.set('sig', '0') elif keys_sig.fifths > 0: @@ -131,7 +98,7 @@ def _handle_staffs(self, xml_el): else: ks_def.set('sig', str(abs(keys_sig.fifths)) + 'f') # Find the pname from the number of sharps or flats and the mode - ks_def.set('pname', FIFTHS_AND_MODE_TO_PNAME[(keys_sig.fifths, keys_sig.mode)]) + ks_def.set('pname', fifths_mode_to_key_name(keys_sig.fifths, keys_sig.mode).lower()) if time_sig is not None: ts_def = etree.SubElement(staff_def, 'meterSig') From 2b40d62a7ccddeddb99d1406af111afaa87d54bc Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 29 Jan 2024 14:07:42 +0100 Subject: [PATCH 063/197] Support, for Key Signature changes, Time Signature Changes, Beams, Tuplets, Ties, Grace notes, and correct namespace. --- partitura/io/mei_export_v2.py | 170 ++++++++++++++++++++++++++++++---- 1 file changed, 150 insertions(+), 20 deletions(-) diff --git a/partitura/io/mei_export_v2.py b/partitura/io/mei_export_v2.py index 0a400ef6..f0e2bb53 100644 --- a/partitura/io/mei_export_v2.py +++ b/partitura/io/mei_export_v2.py @@ -18,6 +18,8 @@ __all__ = ["save_mei"] +XMLNS_ID = "{http://www.w3.org/XML/1998/namespace}id" + ALTER_TO_MEI = { -2: "ff", -1: "f", @@ -30,6 +32,7 @@ DOCTYPE = '\n' + class MEIExporter: def __init__(self, part): self.part = part @@ -38,26 +41,34 @@ def __init__(self, part): def elc_id(self): # transforms an integer number to 8-digit string # The number is right aligned and padded with zeros - out = str(self.element_counter).zfill(10) self.element_counter += 1 + out = str(self.element_counter).zfill(10) return out def export_to_mei(self): # Create root MEI element - mei = etree.Element('mei') - + etree.register_namespace("xml", "http://www.w3.org/XML/1998/namespace") + etree.register_namespace( "mei", "http://www.music-encoding.org/ns/mei") + mei = etree.Element('mei', nsmap={'xml': "http://www.w3.org/XML/1998/namespace", + 'mei': "http://www.music-encoding.org/ns/mei"}) + # mei.set('xmlns', "http://www.music-encoding.org/ns/mei") + mei.set('meiversion', "4.0.1") # Create child elements mei_head = etree.SubElement(mei, 'meiHead') file_desc = etree.SubElement(mei_head, 'fileDesc') - music = etree.SubElement(mei, 'music') + music = etree.SubElement(mei, 'music') body = etree.SubElement(music, 'body') mdiv = etree.SubElement(body, 'mdiv') score = etree.SubElement(mdiv, 'score') + score.set(XMLNS_ID, "score-" + self.elc_id()) score_def = etree.SubElement(score, 'scoreDef') + score_def.set(XMLNS_ID, "scoredef-" + self.elc_id()) staff_grp = etree.SubElement(score_def, 'staffGrp') + staff_grp.set(XMLNS_ID, "staffgrp-" + self.elc_id()) self._handle_staffs(staff_grp) section = etree.SubElement(score, 'section') + section.set(XMLNS_ID, "section-" + self.elc_id()) # Iterate over part's timeline for measure in self.part.measures: @@ -78,18 +89,18 @@ def _handle_staffs(self, xml_el): staff_num += 1 staff_def = etree.SubElement(xml_el, 'staffDef') staff_def.set('n', str(staff_num)) - staff_def.set('id', "staffdef-" + self.elc_id()) + staff_def.set(XMLNS_ID, "staffdef-" + self.elc_id()) staff_def.set('lines', '5') # Get clef for this staff If no cleff is available for this staff, default to "G2" clef_def = etree.SubElement(staff_def, 'clef') - clef_def.set('id', "clef-" + self.elc_id()) - clef_shape = clefs[staff_num].step if staff_num in clefs.keys() else "G" + clef_def.set(XMLNS_ID, "clef-" + self.elc_id()) + clef_shape = clefs[staff_num].sign if staff_num in clefs.keys() else "G" clef_def.set('shape', str(clef_shape)) clef_def.set('line', str(clefs[staff_num].line)) if staff_num in clefs.keys() else clef_def.set('line', '2') # Get key signature for this staff if keys_sig is not None: ks_def = etree.SubElement(staff_def, 'keySig') - ks_def.set('id', "keysig-" + self.elc_id()) + ks_def.set(XMLNS_ID, "keysig-" + self.elc_id()) ks_def.set('mode', keys_sig.mode) if keys_sig.mode is not None else ks_def.set('mode', 'major') if keys_sig.fifths == 0: ks_def.set('sig', '0') @@ -102,23 +113,23 @@ def _handle_staffs(self, xml_el): if time_sig is not None: ts_def = etree.SubElement(staff_def, 'meterSig') - ts_def.set('id', "msig-" + self.elc_id()) + ts_def.set(XMLNS_ID, "msig-" + self.elc_id()) ts_def.set('count', str(time_sig.beats)) ts_def.set('unit', str(time_sig.beat_type)) - def _handle_measure(self, measure, xml_el): + def _handle_measure(self, measure, measure_el): # Add measure number - xml_el.set('n', str(measure.number)) - xml_el.set('id', "measure-" + self.elc_id()) + measure_el.set('n', str(measure.number)) + measure_el.set(XMLNS_ID, "measure-" + self.elc_id()) note_or_rest_elements = np.array(list(self.part.iter_all(spt.GenericNote, start=measure.start.t, end=measure.end.t, include_subclasses=True))) # Separate by staff staffs = np.vectorize(lambda x: x.staff)(note_or_rest_elements) unique_staffs, staff_inverse_map = np.unique(staffs, return_inverse=True) for i, staff in enumerate(unique_staffs): - staff_el = etree.SubElement(xml_el, 'staff') + staff_el = etree.SubElement(measure_el, 'staff') # Add staff number staff_el.set('n', str(staff)) - staff_el.set('id', "staff-" + self.elc_id()) + staff_el.set(XMLNS_ID, "staff-" + self.elc_id()) staff_notes = note_or_rest_elements[staff_inverse_map == i] # Separate by voice voices = np.vectorize(lambda x: x.voice)(staff_notes) @@ -126,7 +137,7 @@ def _handle_measure(self, measure, xml_el): for j, voice in enumerate(unique_voices): voice_el = etree.SubElement(staff_el, 'layer') voice_el.set('n', str(voice)) - voice_el.set('id', "voice-" + self.elc_id()) + voice_el.set(XMLNS_ID, "voice-" + self.elc_id()) voice_notes = staff_notes[voice_inverse_map == j] # Sort by onset note_start_times = np.vectorize(lambda x: x.start.t)(voice_notes) @@ -139,11 +150,16 @@ def _handle_measure(self, measure, xml_el): else: self._handle_note_or_rest(notes[0], voice_el) - return xml_el + self._handle_tuplets(measure_el, start=measure.start.t, end=measure.end.t) + self._handle_beams(measure_el, start=measure.start.t, end=measure.end.t) + self._handle_clef_changes(measure_el, start=measure.start.t, end=measure.end.t) + self._handle_ks_changes(measure_el, start=measure.start.t, end=measure.end.t) + self._handle_ts_changes(measure_el, start=measure.start.t, end=measure.end.t) + return measure_el def _handle_chord(self, chord, xml_voice_el): chord_el = etree.SubElement(xml_voice_el, 'chord') - chord_el.set('id', "chord-" + self.elc_id()) + chord_el.set(XMLNS_ID, "chord-" + self.elc_id()) for note in chord: self._handle_note_or_rest(note, chord_el) @@ -156,19 +172,133 @@ def _handle_note_or_rest(self, note, xml_voice_el): def _handle_rest(self, rest, xml_voice_el): rest_el = etree.SubElement(xml_voice_el, 'rest') rest_el.set('dur', SYMBOLIC_TYPES_TO_MEI_DURS[rest.symbolic_duration["type"]]) - rest_el.set('id', "rest-" + self.elc_id()) + rest_el.set(XMLNS_ID, "rest-" + self.elc_id()) def _handle_note(self, note, xml_voice_el): note_el = etree.SubElement(xml_voice_el, 'note') note_el.set('dur', SYMBOLIC_TYPES_TO_MEI_DURS[note.symbolic_duration["type"]]) - note_el.set('id', "note-" + self.elc_id()) + note_el.set(XMLNS_ID, "note-" + self.elc_id()) if note.id is None else note_el.set(XMLNS_ID, note.id) note_el.set('oct', str(note.octave)) note_el.set('pname', note.step.lower()) + if note.tie_next is not None and note.tie_prev is not None: + note_el.set('tie', 'm') + elif note.tie_next is not None: + note_el.set('tie', 'i') + elif note.tie_prev is not None: + note_el.set('tie', 't') + if note.alter is not None: accidental = etree.SubElement(note_el, 'accid') - accidental.set('id', "accid-" + self.elc_id()) + accidental.set(XMLNS_ID, "accid-" + self.elc_id()) accidental.set('accid', ALTER_TO_MEI[note.alter]) + if isinstance(note, spt.GraceNote): + note_el.set('grace', 'acc') + + def _handle_tuplets(self, measure_el, start, end): + for tuplet in self.part.iter_all(spt.Tuplet, start=start, end=end): + start_note = tuplet.start_note + end_note = tuplet.end_note + # Find the note element corresponding to the start note i.e. has the same id value + start_note_el = measure_el.xpath(f".//*[@xml:id='{start_note.id}']")[0] + # Find the note element corresponding to the end note i.e. has the same id value + end_note_el = measure_el.xpath(f".//*[@xml:id='{end_note.id}']")[0] + # Create the tuplet element as parent of the start and end note elements + # Make it start at the same index as the start note element + tuplet_el = etree.Element('tuplet') + layer_el = start_note_el.getparent() + layer_el.insert(layer_el.index(start_note_el), tuplet_el) + tuplet_el.set(XMLNS_ID, "tuplet-" + self.elc_id()) + tuplet_el.set('num', str(start_note.symbolic_duration["actual_notes"])) + tuplet_el.set('numbase', str(start_note.symbolic_duration["normal_notes"])) + # Add all elements between the start and end note elements to the tuplet element as childen + # Find them from the xml tree + start_note_index = start_note_el.getparent().index(start_note_el) + end_note_index = end_note_el.getparent().index(end_note_el) + xml_el_within_tuplet = [start_note_el.getparent()[i] for i in range(start_note_index, end_note_index + 1)] + for el in xml_el_within_tuplet: + tuplet_el.append(el) + + def _handle_beams(self, measure_el, start, end): + for beam in self.part.iter_all(spt.Beam, start=start, end=end): + start_note = beam.notes[np.argmin([n.start.t for n in beam.notes])] + # Beam element is parent of the note element + note_el = measure_el.xpath(f".//*[@xml:id='{start_note.id}']")[0] + layer_el = note_el.getparent() + insert_index = layer_el.index(note_el) + # If the parent is a tuplet, the beam element should be added as parent of the tuplet element + if layer_el.tag == 'tuplet': + parent_el = layer_el.getparent() + insert_index = parent_el.index(layer_el) + layer_el = parent_el + # Create the beam element + beam_el = etree.Element('beam') + layer_el.insert(insert_index, beam_el) + beam_el.set(XMLNS_ID, "beam-" + self.elc_id()) + for note in beam.notes: + # Find the note element corresponding to the start note i.e. has the same id value + note_el = measure_el.xpath(f".//*[@xml:id='{note.id}']") + if len(note_el) > 0: + note_el = note_el[0] + beam_el.append(note_el) + + def _handle_clef_changes(self, measure_el, start, end): + 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: + continue + # Find the note element corresponding to the start note i.e. has the same id value + for note in self.part.iter_all(spt.GenericNote, start=clef.start.t, end=clef.start.t): + note_el = measure_el.xpath(f".//*[@xml:id='{note.id}']") + if len(note_el) > 0: + note_el = note_el[0] + layer_el = note_el.getparent() + insert_index = layer_el.index(note_el) + # Create the clef element + clef_el = etree.Element('clef') + layer_el.insert(insert_index, clef_el) + clef_el.set(XMLNS_ID, "clef-" + self.elc_id()) + clef_el.set('shape', str(clef.sign)) + clef_el.set('line', str(clef.line)) + + def _handle_ks_changes(self, measure_el, start, end): + # 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): + if key_sig.start.t == 0: + continue + # Create the scoreDef element + score_def_el = etree.Element('scoreDef') + score_def_el.set(XMLNS_ID, "scoredef-" + self.elc_id()) + score_def_el.set('mode', key_sig.mode) if key_sig.mode is not None else score_def_el.set('mode', 'major') + if key_sig.fifths == 0: + score_def_el.set('sig', '0') + elif key_sig.fifths > 0: + score_def_el.set('sig', str(key_sig.fifths) + 's') + else: + score_def_el.set('sig', str(abs(key_sig.fifths)) + 'f') + # Find the pname from the number of sharps or flats and the mode + score_def_el.set('pname', fifths_mode_to_key_name(key_sig.fifths, key_sig.mode).lower()) + # Add the scoreDef element at before the measure element starts + parent = measure_el.getparent() + parent.insert(parent.index(measure_el), score_def_el) + + def _handle_ts_changes(self, measure_el, start, end): + # 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): + if time_sig.start.t == 0: + continue + # Create the scoreDef element + score_def_el = etree.Element('scoreDef') + score_def_el.set(XMLNS_ID, "scoredef-" + self.elc_id()) + + # Add the scoreDef element at before the measure element starts + parent = measure_el.getparent() + parent.insert(parent.index(measure_el), score_def_el) + score_def_el.set('count', str(time_sig.beats)) + score_def_el.set('unit', str(time_sig.beat_type)) + @deprecated_alias(parts="score_data") def save_mei( From b10e4a861c67a0d9919b980b1749067b898dfaad Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 29 Jan 2024 14:37:14 +0100 Subject: [PATCH 064/197] Added namespace empty. --- partitura/__init__.py | 1 + partitura/io/mei_export_v2.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/partitura/__init__.py b/partitura/__init__.py index c624b578..2dbcadac 100644 --- a/partitura/__init__.py +++ b/partitura/__init__.py @@ -23,6 +23,7 @@ from .io.importparangonada import load_parangonada_csv from .io.exportparangonada import save_parangonada_csv, save_csv_for_parangonada from .io.exportaudio import save_wav +from .io.mei_export_v2 import save_mei from .display import render from . import musicanalysis from .musicanalysis import make_note_features, compute_note_array, full_note_array diff --git a/partitura/io/mei_export_v2.py b/partitura/io/mei_export_v2.py index f0e2bb53..a803e7d0 100644 --- a/partitura/io/mei_export_v2.py +++ b/partitura/io/mei_export_v2.py @@ -50,7 +50,7 @@ def export_to_mei(self): etree.register_namespace("xml", "http://www.w3.org/XML/1998/namespace") etree.register_namespace( "mei", "http://www.music-encoding.org/ns/mei") mei = etree.Element('mei', nsmap={'xml': "http://www.w3.org/XML/1998/namespace", - 'mei': "http://www.music-encoding.org/ns/mei"}) + None: "http://www.music-encoding.org/ns/mei"}) # mei.set('xmlns', "http://www.music-encoding.org/ns/mei") mei.set('meiversion', "4.0.1") # Create child elements From ddb445c6e7470cc9f521062aa08bd48dd3c20d77 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 29 Jan 2024 14:40:30 +0100 Subject: [PATCH 065/197] Return and set duration for chords. --- partitura/io/mei_export_v2.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/partitura/io/mei_export_v2.py b/partitura/io/mei_export_v2.py index a803e7d0..2c16e5ce 100644 --- a/partitura/io/mei_export_v2.py +++ b/partitura/io/mei_export_v2.py @@ -161,22 +161,27 @@ def _handle_chord(self, chord, xml_voice_el): chord_el = etree.SubElement(xml_voice_el, 'chord') chord_el.set(XMLNS_ID, "chord-" + self.elc_id()) for note in chord: - self._handle_note_or_rest(note, chord_el) + duration = self._handle_note_or_rest(note, chord_el) + chord_el.set('dur', duration) def _handle_note_or_rest(self, note, xml_voice_el): if isinstance(note, spt.Rest): - self._handle_rest(note, xml_voice_el) + duration = self._handle_rest(note, xml_voice_el) else: - self._handle_note(note, xml_voice_el) + duration = self._handle_note(note, xml_voice_el) + return duration def _handle_rest(self, rest, xml_voice_el): rest_el = etree.SubElement(xml_voice_el, 'rest') - rest_el.set('dur', SYMBOLIC_TYPES_TO_MEI_DURS[rest.symbolic_duration["type"]]) + duration = SYMBOLIC_TYPES_TO_MEI_DURS[rest.symbolic_duration["type"]] + rest_el.set('dur', duration) rest_el.set(XMLNS_ID, "rest-" + self.elc_id()) + return duration def _handle_note(self, note, xml_voice_el): note_el = etree.SubElement(xml_voice_el, 'note') - note_el.set('dur', SYMBOLIC_TYPES_TO_MEI_DURS[note.symbolic_duration["type"]]) + duration = SYMBOLIC_TYPES_TO_MEI_DURS[note.symbolic_duration["type"]] + note_el.set('dur', duration) note_el.set(XMLNS_ID, "note-" + self.elc_id()) if note.id is None else note_el.set(XMLNS_ID, note.id) note_el.set('oct', str(note.octave)) note_el.set('pname', note.step.lower()) @@ -194,6 +199,7 @@ def _handle_note(self, note, xml_voice_el): if isinstance(note, spt.GraceNote): note_el.set('grace', 'acc') + return duration def _handle_tuplets(self, measure_el, start, end): for tuplet in self.part.iter_all(spt.Tuplet, start=start, end=end): From 9819f143294cb2758568ecab5140bba861079161 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 29 Jan 2024 14:40:37 +0100 Subject: [PATCH 066/197] Added first test for export. --- tests/test_mei.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/test_mei.py b/tests/test_mei.py index 782c7976..ebdfcca1 100644 --- a/tests/test_mei.py +++ b/tests/test_mei.py @@ -7,15 +7,15 @@ import unittest from tests import MEI_TESTFILES -from partitura import load_musicxml, load_mei, EXAMPLE_MEI +from partitura import load_musicxml, load_mei, EXAMPLE_MEI, save_mei import partitura.score as score from partitura.io.importmei import MeiParser from partitura.utils import compute_pianoroll from lxml import etree +from tempfile import TemporaryDirectory from xmlschema.names import XML_NAMESPACE - +import os import numpy as np -from pathlib import Path # class TestSaveMEI(unittest.TestCase): @@ -30,6 +30,21 @@ # self.assertTrue(mei.decode('utf-8') == target_mei, msg) +class TestExportMEI(unittest.TestCase): + def test_export_mei(self): + import_score = load_mei(EXAMPLE_MEI) + ina = import_score.note_array() + with TemporaryDirectory() as tmpdir: + tmp_mei = os.path.join(tmpdir, "test.mei") + save_mei(import_score, tmp_mei) + export_score = load_mei(tmp_mei) + ena = export_score.note_array() + self.assertTrue(np.all(ina["onset_beat"] == ena["onset_beat"])) + self.assertTrue(np.all(ina["duration_beat"] == ena["duration_beat"])) + self.assertTrue(np.all(ina["pitch"] == ena["pitch"])) + self.assertTrue(np.all(ina["voice"] == ena["voice"])) + self.assertTrue(np.all(ina["id"] == ena["id"])) + class TestImportMEI(unittest.TestCase): def test_main_part_group1(self): From 476d953f5debb6d0948b4678a55db5ac83d1c000 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 30 Jan 2024 10:31:16 +0100 Subject: [PATCH 067/197] Updated clef change and symbolic durations. --- partitura/io/importkern_v2.py | 49 ++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index 4d5f61b9..be923552 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -54,18 +54,19 @@ } KERN_DURS = { - "000": "maxima", - "00": "long", - "0": "breve", - "1": "whole", - "2": "half", - "4": "quarter", - "8": "eighth", - "16": "16th", - "32": "32nd", - "64": "64th", - "128": "128th", - "256": "256th", + "3%2": {"type": "whole", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + "000": {"type": "maxima"}, + "00": {"type": "long"}, + "0": {"type": "breve"}, + "1": {"type": "whole"}, + "2": {"type": "half"}, + "4": {"type": "quarter"}, + "8": {"type": "eighth"}, + "16": {"type": "16th"}, + "32": {"type": "32nd"}, + "64": {"type": "64th"}, + "128": {"type": "128th"}, + "256": {"type": "256th"}, } @@ -492,6 +493,7 @@ def process_clef_line(self, line): clef = re.search(r"([GFC])", line).group(0) # find the octave has_line = re.search(r"([0-9])", line) + octave_change = "v" in line if has_line is None: if clef == "G": clef_line = 2 @@ -503,7 +505,15 @@ def process_clef_line(self, line): raise ValueError("Unrecognized clef line: {}".format(line)) else: clef_line = has_line.group(0) - return spt.Clef(sign=clef, staff=self.staff, line=int(clef_line), octave_change=0) + if octave_change and clef_line == 2 and clef == "G": + octave = -1 + elif octave_change: + warnings.warn("Octave change not supported for clef: {}".format(line)) + octave = 0 + else: + octave = 0 + + return spt.Clef(sign=clef, staff=self.staff, line=int(clef_line), octave_change=octave) def process_key_signature_line(self, line): fifths = line.count("#") - line.count("-") @@ -542,7 +552,7 @@ def _process_kern_duration(self, duration, is_grace=False): dots = duration.count(".") dur = duration.replace(".", "") if dur in KERN_DURS.keys(): - symbolic_duration = {"type": KERN_DURS[dur]} + symbolic_duration = KERN_DURS[dur] else: dur = float(dur) diff = dict( @@ -553,12 +563,9 @@ def _process_kern_duration(self, duration, is_grace=False): ) ) ) - - symbolic_duration = { - "type": KERN_DURS[diff[min(list(diff.keys()))]], - "actual_notes": int(dur // 4), - "normal_notes": int(diff[min(list(diff.keys()))]) // 4, - } + symbolic_duration = KERN_DURS[diff[min(list(diff.keys()))]] + symbolic_duration["actual_notes"] = int(dur // 4) + symbolic_duration["normal_notes"] = int(diff[min(list(diff.keys()))]) // 4 symbolic_duration["dots"] = dots self.note_duration_values[self.total_parsed_elements] = dot_function((float(dur) if isinstance(dur, str) else dur), symbolic_duration["dots"]) if not is_grace else inf return symbolic_duration @@ -618,7 +625,7 @@ def meta_note_line(self, line, voice=None, add=True): else: pitch = find_pitch.group(0) # extract duration can be any of the following: 0-9 . - dur_search = re.search(r"([0-9.]+)", line) + dur_search = re.search(r"([0-9.%]+)", line) # if no duration is found, then the duration is 8 by default (for grace notes with no duration) duration = dur_search.group(0) if dur_search else "8" # extract symbol can be any of the following: _()[]{}<>|: From f122f36c0cf48fe554ace936462b2846f9a0346c Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 30 Jan 2024 10:44:30 +0100 Subject: [PATCH 068/197] Minor fix and update on Kern Durations with renaissance specials. --- partitura/io/importkern_v2.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index be923552..e8140b1f 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -55,6 +55,7 @@ KERN_DURS = { "3%2": {"type": "whole", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + "2%3": {"type": "whole", "dots": 1}, "000": {"type": "maxima"}, "00": {"type": "long"}, "0": {"type": "breve"}, @@ -555,14 +556,16 @@ def _process_kern_duration(self, duration, is_grace=False): symbolic_duration = KERN_DURS[dur] else: dur = float(dur) + key_loolup = [2 ** i for i in range(0, 9)] diff = dict( ( map( - lambda x: (dur - int(x), str(int(x))) if dur > int(x) else (dur + int(x), str(int(x))), - KERN_DURS.keys(), + lambda x: (dur - x, str(x)) if dur > x else (dur + x, str(x)), + key_loolup, ) ) ) + symbolic_duration = KERN_DURS[diff[min(list(diff.keys()))]] symbolic_duration["actual_notes"] = int(dur // 4) symbolic_duration["normal_notes"] = int(diff[min(list(diff.keys()))]) // 4 @@ -685,10 +688,3 @@ def meta_chord_line(self, line): self.total_parsed_elements += 1 chord = ("c", [self.meta_note_line(n, add=False) for n in line.split(" ")]) return chord - - -# if __name__ == "__main__": -# kern_path = "/home/manos/Desktop/test.krn" -# x = load_kern(kern_path) -# import partitura as pt -# pt.save_musicxml(x, "/home/manos/Desktop/test_kern.musicxml") \ No newline at end of file From 3e2cbab6f1b652af86c48b8241c8c4d69b1676d9 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 30 Jan 2024 11:03:45 +0100 Subject: [PATCH 069/197] Minor fix for Kern import: spline-splitting version of kern creates conflicts with numpy version 1.24 when the delimiter of readtxt is `\n`. It has now been changed to `np.genfromtxt` --- partitura/io/importkern_v2.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index e8140b1f..6b13a323 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -115,7 +115,8 @@ def parse_by_voice(file, dtype=np.object_): def _handle_kern_with_spine_splitting(kern_path): - org_file = np.loadtxt(kern_path, dtype="U", delimiter="\n", comments="!!!", encoding="cp437") + # org_file = np.loadtxt(kern_path, dtype="U", delimiter="\n", comments="!!!", encoding="cp437") + org_file = np.genfromtxt(kern_path, dtype="U", delimiter="\n", comments="!!!", encoding="cp437") # Get Main Number of parts and Spline Types spline_types = org_file[0].split("\t") parsing_idxs = [] From 6ca49536918996610b3666dda1685d4353daf83b Mon Sep 17 00:00:00 2001 From: Emmanouil Karystinaios Date: Tue, 30 Jan 2024 11:46:18 +0100 Subject: [PATCH 070/197] Update partitura_unittests.yml Updated optional dependencies to include pandas. --- .github/workflows/partitura_unittests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/partitura_unittests.yml b/.github/workflows/partitura_unittests.yml index a0c2dd08..93bb2043 100644 --- a/.github/workflows/partitura_unittests.yml +++ b/.github/workflows/partitura_unittests.yml @@ -28,7 +28,7 @@ jobs: - name: Install Optional dependencies run: | pip install music21==8.3.0 Pillow==9.5.0 musescore==0.0.1 - pip install miditok==2.0.6 tokenizers==0.13.3 + pip install miditok==2.0.6 tokenizers==0.13.3 pandas==2.0.3 - name: Run Tests run: | pip install coverage From 3d7de334e2c7d904cc66937d573c48f52463dbec Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 30 Jan 2024 12:10:32 +0100 Subject: [PATCH 071/197] Minor fix for estimate symbolic duration. --- partitura/utils/music.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index b0932566..76a07cf5 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -981,14 +981,14 @@ def estimate_symbolic_duration(dur, div, eps=10**-3): else: # Guess tuplets (Naive) type = SYM_DURS[i+3]["type"] + normal_notes = 2 return { "type": type, - "actual_notes": int(1/qdur), - "normal_notes": 4, + "actual_notes": int(normal_notes/qdur), + "normal_notes": normal_notes, } - def to_quarter_tempo(unit, tempo): """Given a string `unit` (e.g. 'q', 'q.' or 'h') and a number `tempo`, return the corresponding tempo in quarter notes. This is From 5c50193e2b2319be4effeb2eaef2be53a282cf65 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 30 Jan 2024 12:12:51 +0100 Subject: [PATCH 072/197] Removed Value error raise from RomanNumeral quality search. Now set to warning. It will be fixed with introduced Vocabulary on chords. --- partitura/score.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/partitura/score.py b/partitura/score.py index e78344ad..e8c4adfd 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -2847,7 +2847,8 @@ def _process_quality(self): elif major_cond: quality = "maj" else: - raise ValueError(f"Quality for {self.text} was not found") + warnings.warn(f"Quality for {self.text} was not found, could be a special case. Setting to None.") + quality = None return quality def __str__(self): From 62b297efef8e77bd11c04de7c3584fb2adb11b03 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 30 Jan 2024 12:25:45 +0100 Subject: [PATCH 073/197] Caught corner cases that led to errors with symbolic durations. --- partitura/utils/music.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 76a07cf5..3f221e3e 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -5,6 +5,7 @@ """ from __future__ import annotations import copy +import math from collections import defaultdict import re import warnings @@ -975,16 +976,18 @@ def estimate_symbolic_duration(dur, div, eps=10**-3): """ global DURS, SYM_DURS qdur = dur / div + if qdur == 0: + return i = find_nearest(DURS, qdur) if np.abs(qdur - DURS[i]) < eps: return SYM_DURS[i].copy() else: - # Guess tuplets (Naive) + # NOTE: Guess tuplets (Naive) it doesn't cover composite durations from tied notes. type = SYM_DURS[i+3]["type"] normal_notes = 2 return { "type": type, - "actual_notes": int(normal_notes/qdur), + "actual_notes": math.ceil(normal_notes/qdur), "normal_notes": normal_notes, } From 7061d660fe854a0c0edad58a03d0fa8bbca09439 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 30 Jan 2024 19:21:16 +0100 Subject: [PATCH 074/197] included export for Roman Numerals in a very simple form. --- partitura/io/mei_export_v2.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/partitura/io/mei_export_v2.py b/partitura/io/mei_export_v2.py index 2c16e5ce..ac5ccfe0 100644 --- a/partitura/io/mei_export_v2.py +++ b/partitura/io/mei_export_v2.py @@ -155,6 +155,7 @@ def _handle_measure(self, measure, measure_el): self._handle_clef_changes(measure_el, start=measure.start.t, end=measure.end.t) self._handle_ks_changes(measure_el, start=measure.start.t, end=measure.end.t) self._handle_ts_changes(measure_el, start=measure.start.t, end=measure.end.t) + self._handle_harmony(measure_el, start=measure.start.t, end=measure.end.t) return measure_el def _handle_chord(self, chord, xml_voice_el): @@ -305,6 +306,18 @@ 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): + # 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 harmony in self.part.iter_all(spt.RomanNumeral, start=start, end=end): + harm_el = etree.SubElement(measure_el, 'harm') + harm_el.set(XMLNS_ID, "harm-" + self.elc_id()) + harm_el.set("staff", str(self.part.number_of_staves)) + harm_el.set("tstamp", str(np.diff(self.part.quarter_map([start, harmony.start.t]))[0]+1)) + harm_el.set("place", "below") + # text is a child element of harmony but not a xml element + harm_el.text = harmony.text + @deprecated_alias(parts="score_data") def save_mei( From 3155044648498d5b820d693637860a7c31ec9bcd Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 30 Jan 2024 19:26:44 +0100 Subject: [PATCH 075/197] Added a test for harmony. --- tests/test_mei.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_mei.py b/tests/test_mei.py index ebdfcca1..d360b0c2 100644 --- a/tests/test_mei.py +++ b/tests/test_mei.py @@ -6,7 +6,7 @@ import unittest -from tests import MEI_TESTFILES +from tests import MEI_TESTFILES, MUSICXML_PATH from partitura import load_musicxml, load_mei, EXAMPLE_MEI, save_mei import partitura.score as score from partitura.io.importmei import MeiParser @@ -45,6 +45,13 @@ def test_export_mei(self): self.assertTrue(np.all(ina["voice"] == ena["voice"])) self.assertTrue(np.all(ina["id"] == ena["id"])) + def test_export_with_harmony(self): + score_fn = os.path.join(MUSICXML_PATH, "test_harmony.musicxml") + import_score = load_musicxml(score_fn) + with TemporaryDirectory() as tmpdir: + tmp_mei = os.path.join(tmpdir, "test.mei") + save_mei(import_score, tmp_mei) + class TestImportMEI(unittest.TestCase): def test_main_part_group1(self): From ea16e5bd046135fa2345fa70b69f7d191bd74534 Mon Sep 17 00:00:00 2001 From: manoskary Date: Thu, 1 Feb 2024 08:43:51 +0000 Subject: [PATCH 076/197] Format code with black (bot) --- partitura/io/importmusic21.py | 24 +++++++++++++++--------- partitura/io/matchlines_v1.py | 14 ++++++++------ partitura/score.py | 17 ++++++++++------- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/partitura/io/importmusic21.py b/partitura/io/importmusic21.py index f0ad2038..363dc9fe 100644 --- a/partitura/io/importmusic21.py +++ b/partitura/io/importmusic21.py @@ -100,14 +100,18 @@ def fill_part_notes(self, m21_part, pt_part, part_idx): for i_pitch, pitch in enumerate(generic_note.pitches): if generic_note.duration.isGrace: note = pt.score.GraceNote( - grace_type="acciaccatura" - if generic_note.duration.slash - else "appoggiatura", + grace_type=( + "acciaccatura" + if generic_note.duration.slash + else "appoggiatura" + ), step=pitch.step, octave=pitch.octave, - alter=pitch.accidental.alter - if pitch.accidental is not None - else None, + alter=( + pitch.accidental.alter + if pitch.accidental is not None + else None + ), # id="{}_{}".format(generic_note.id, i_pitch), id=generic_note.id, voice=self.find_voice(generic_note), @@ -119,9 +123,11 @@ def fill_part_notes(self, m21_part, pt_part, part_idx): note = pt.score.Note( step=pitch.step, octave=pitch.octave, - alter=pitch.accidental.alter - if pitch.accidental is not None - else None, + alter=( + pitch.accidental.alter + if pitch.accidental is not None + else None + ), # id="{}_{}".format(generic_note.id, i_pitch), id=generic_note.id, voice=self.find_voice(generic_note), diff --git a/partitura/io/matchlines_v1.py b/partitura/io/matchlines_v1.py index 1fdcfe69..a794ee33 100644 --- a/partitura/io/matchlines_v1.py +++ b/partitura/io/matchlines_v1.py @@ -1150,9 +1150,11 @@ def from_instance( version=version, anchor=instance.Anchor, note=MatchNote.from_instance(instance.note, version=version), - ornament_type=["trill"] - if instance.version < Version(1, 0, 0) - else instance.OrnamentType, + ornament_type=( + ["trill"] + if instance.version < Version(1, 0, 0) + else instance.OrnamentType + ), ) @@ -1347,9 +1349,9 @@ def make_section( start_in_beats_original=start_in_beats_original, end_in_beats_unfolded=end_in_beats_unfolded, end_in_beats_original=end_in_beats_original, - repeat_end_type=[repeat_end_type] - if isinstance(repeat_end_type, str) - else repeat_end_type, + repeat_end_type=( + [repeat_end_type] if isinstance(repeat_end_type, str) else repeat_end_type + ), ) return ml diff --git a/partitura/score.py b/partitura/score.py index cbc0f445..ca69cbd9 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -1227,7 +1227,6 @@ def use_notated_beat(self): class TimePoint(ComparableMixin): - """A TimePoint represents a temporal position within a :class:`Part`. @@ -4872,16 +4871,20 @@ def merge_parts(parts, reassign="voice"): 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 + ( + max(note_array["voice"], default=0) + if max(note_array["voice"], default=0) != 0 + else 1 + ) 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 + ( + max(note_array["staff"], default=0) + if max(note_array["staff"], default=0) != 0 + else 1 + ) for note_array in note_arrays ] From d70f4c9f36f0b588d88321a50a12a5c17a98bf14 Mon Sep 17 00:00:00 2001 From: manoskary Date: Thu, 1 Feb 2024 13:26:24 +0000 Subject: [PATCH 077/197] Format code with black (bot) --- partitura/io/exportmidi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/exportmidi.py b/partitura/io/exportmidi.py index 61759385..fda24f74 100644 --- a/partitura/io/exportmidi.py +++ b/partitura/io/exportmidi.py @@ -403,7 +403,7 @@ def to_ppq(t): ) # default tempo if not tempos: - tempos[0] = MetaMessage("set_tempo", tempo=500000) + tempos[0] = MetaMessage("set_tempo", tempo=500000) if anacrusis_behavior == "time_sig_change": # Change time signature to match the duration of the measure From 726a167cd7f67fec55a3618a8131d78419616498 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 3 Feb 2024 19:26:04 +0100 Subject: [PATCH 078/197] updating matching condition for finding ties. --- partitura/io/importdcml.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 216cbe81..40b3adac 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -1,3 +1,5 @@ +import warnings + import numpy as np from math import ceil import partitura.score as spt @@ -121,13 +123,18 @@ def read_note_tsv(note_tsv_path, metadata=None): for tied_note in note_array[tied_note_mask]: for note in part.iter_all(spt.Note, tied_note["onset_div"], tied_note["onset_div"]+1): if note.id == "n-{}".format(tied_note["id"]): + found_next = False for note_next in part.iter_all(spt.Note, note.end.t, note.end.t+1, mode="starting"): - if note_next.alter == note.alter and note_next.step == note.step and note_next.octave == note.octave: - assert note_next.voice == note.voice, "Tied notes must be in the same voice" - assert note_next.staff == note.staff, "Tied notes must be in the same staff" + condition = note_next.alter == note.alter and note_next.step == note.step and \ + note_next.octave == note.octave and note.voice == note_next.voice and \ + note.staff == note_next.staff + if condition: note.tie_next = note_next note_next.tie_prev = note + found_next = condition break + if not found_next: + warnings.warn("Opening tie, but no matching note found.") return part From 39a5f03928169052d728663129fc83d4fa087889 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 3 Feb 2024 19:52:38 +0100 Subject: [PATCH 079/197] Fixes for dcml_import and mix with object, string and float types. --- partitura/io/importdcml.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 40b3adac..1b084c1d 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -142,6 +142,7 @@ def read_note_tsv(note_tsv_path, metadata=None): def read_measure_tsv(measure_tsv_path, part): qdivs = part._quarter_durations[0] data = pd.read_csv(measure_tsv_path, sep="\t") + data["quarterbeats"] = data["quarterbeats"].apply(eval) if data.dtypes["quarterbeats"] == str or data.dtypes["quarterbeats"] == object else data["quarterbeats"] data["onset_div"] = np.array([int(qd * qdivs) for qd in data["quarterbeats"]]) data["duration_div"] = np.array([int(qd * qdivs) for qd in data["duration_qb"]]) repeat_index = 0 @@ -162,7 +163,9 @@ def read_measure_tsv(measure_tsv_path, part): def read_harmony_tsv(beat_tsv_path, part): qdivs = part._quarter_durations[0] data = pd.read_csv(beat_tsv_path, sep="\t") - data["onset_div"] = np.array([int(qd * qdivs) for qd in data["quarterbeats"].apply(eval)]) + data["quarterbeats"] = data["quarterbeats"].apply(eval) if data.dtypes["quarterbeats"] == str or data.dtypes[ + "quarterbeats"] == object else data["quarterbeats"] + data["onset_div"] = np.array([int(qd * qdivs) for qd in data["quarterbeats"]]) data["duration_div"] = np.array([int(qd * qdivs) for qd in data["duration_qb"]]) is_na_cad = data["cadence"].isna() is_na_roman = data["chord"].isna() From a8651cb844eb5f74a54448ac583cf4d3a45a86c5 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 3 Feb 2024 20:26:52 +0100 Subject: [PATCH 080/197] more fixes for dcml_import and mix with object, string and float types. --- partitura/io/importdcml.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 1b084c1d..aade0823 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -14,6 +14,8 @@ def read_note_tsv(note_tsv_path, metadata=None): # data = np.genfromtxt(note_tsv_path, delimiter="\t", dtype=None, names=True, invalid_raise=False) # unique_durations = np.unique(data["duration"]) data = pd.read_csv(note_tsv_path, sep="\t") + data["quarterbeats"] = data["quarterbeats"].apply(eval) if data.dtypes["quarterbeats"] == str or data.dtypes[ + "quarterbeats"] == object else data["quarterbeats"] unique_durations = data["duration"].unique() denominators = [int(qb.split("/")[1]) for qb in unique_durations if "/" in qb] # transform quarter_beats to quarter_divs From 3e68c3369435d122d3072293de2c5081e3764e91 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 3 Feb 2024 20:29:23 +0100 Subject: [PATCH 081/197] minor correction. --- partitura/io/importdcml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index aade0823..2bb9523d 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -22,7 +22,7 @@ def read_note_tsv(note_tsv_path, metadata=None): qdivs = np.lcm.reduce(denominators) if len(denominators) > 0 else 4 quarter_durations = data["duration_qb"] duration_div = np.array([ceil(qd * qdivs) for qd in quarter_durations]) - onset_div = np.array([ceil(qd * qdivs) for qd in data["quarterbeats"].apply(eval)]) + onset_div = np.array([ceil(qd * qdivs) for qd in data["quarterbeats"]]) flats = data["name"].str.contains("b") sharps = data["name"].str.contains("#") double_sharps = data["name"].str.contains("##") From ef4a4b1d11e7803900b3f7d1543d3e49c582c2b9 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 5 Feb 2024 11:40:48 +0100 Subject: [PATCH 082/197] Fixes and hacks for various minor problems loading dcml scores. --- partitura/io/importdcml.py | 41 ++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 2bb9523d..ae669bb5 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -14,6 +14,10 @@ def read_note_tsv(note_tsv_path, metadata=None): # data = np.genfromtxt(note_tsv_path, delimiter="\t", dtype=None, names=True, invalid_raise=False) # unique_durations = np.unique(data["duration"]) data = pd.read_csv(note_tsv_path, sep="\t") + # Hack for empty values in quarterbeats, to investigate. + # (It happens with voltas when the second volta has a different number of measures) + if not np.all(data["quarterbeats"].isna() == False): + data = data[~data["quarterbeats"].isna()] data["quarterbeats"] = data["quarterbeats"].apply(eval) if data.dtypes["quarterbeats"] == str or data.dtypes[ "quarterbeats"] == object else data["quarterbeats"] unique_durations = data["duration"].unique() @@ -37,7 +41,7 @@ def read_note_tsv(note_tsv_path, metadata=None): data["duration_div"] = duration_div data["alter"] = alter data["pitch"] = data["midi"] - grace_mask = ~data["gracenote"].isna() + grace_mask = ~data["gracenote"].isna().to_numpy() if "gracenote" in data.columns else np.zeros(len(data), dtype=bool) data["id"] = np.arange(len(data)) # Rewrite Voices for correct export staffs = data["staff"].unique() @@ -144,20 +148,26 @@ def read_note_tsv(note_tsv_path, metadata=None): def read_measure_tsv(measure_tsv_path, part): qdivs = part._quarter_durations[0] data = pd.read_csv(measure_tsv_path, sep="\t") + # Hack for empty values in quarterbeats, to investigate. + # (It happens with voltas when the second volta has a different number of measures) + if not np.all(data["quarterbeats"].isna() == False): + data = data[~data["quarterbeats"].isna()] data["quarterbeats"] = data["quarterbeats"].apply(eval) if data.dtypes["quarterbeats"] == str or data.dtypes["quarterbeats"] == object else data["quarterbeats"] data["onset_div"] = np.array([int(qd * qdivs) for qd in data["quarterbeats"]]) data["duration_div"] = np.array([int(qd * qdivs) for qd in data["duration_qb"]]) - repeat_index = 0 + # Get first index + repeat_index, _ = next(data.iterrows()) for idx, row in data.iterrows(): part.add(spt.Measure(number=row["mc"], name=row["mn"]), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) - # if row["repeat"] == "start": + if row["repeats"] == "start": repeat_index = idx - elif row["repeats"] == "": + elif row["repeats"] == "end": # Find the previous repeat start - start_times = data[repeat_index]["onset_div"] + start_times = data.iloc[repeat_index]["onset_div"] part.add(spt.Repeat(), start=start_times, end=row["onset_div"]) + part.add(spt.Fine(), start=part.last_point.t) return @@ -165,6 +175,10 @@ def read_measure_tsv(measure_tsv_path, part): def read_harmony_tsv(beat_tsv_path, part): qdivs = part._quarter_durations[0] data = pd.read_csv(beat_tsv_path, sep="\t") + # Hack for empty values in quarterbeats, to investigate. + # (It happens with voltas when the second volta has a different number of measures) + if not np.all(data["quarterbeats"].isna() == False): + data = data[~data["quarterbeats"].isna()] data["quarterbeats"] = data["quarterbeats"].apply(eval) if data.dtypes["quarterbeats"] == str or data.dtypes[ "quarterbeats"] == object else data["quarterbeats"] data["onset_div"] = np.array([int(qd * qdivs) for qd in data["quarterbeats"]]) @@ -189,12 +203,19 @@ def read_harmony_tsv(beat_tsv_path, part): local_key=row["localkey"], ), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) - phrase_starts = data[data["phraseend"] == "{"] - phrase_ends = data[data["phraseend"] == "}"] + # Check if phrase information is available. + if np.all(data["phraseend"].isna()): + return + # search if character "{, }" in present in values of column phraseend + phrase_starts = data[data["phraseend"].str.contains("{") == True] + phrase_ends = data[data["phraseend"].str.contains("}") == True] # Check that the number of phrase starts and ends match - assert len(phrase_starts) == len(phrase_ends), "Number of phrase starts and ends do not match" - for start, end in zip(phrase_starts.iterrows(), phrase_ends.iterrows()): - part.add(spt.Phrase(), start=start[1]["onset_div"], end=end[1]["onset_div"]) + if len(phrase_starts) == len(phrase_ends): + for start, end in zip(phrase_starts.iterrows(), phrase_ends.iterrows()): + part.add(spt.Phrase(), start=start[1]["onset_div"], end=end[1]["onset_div"]) + else: + # TODO: account for unfoldings and repeats. + warnings.warn("Number of phrase starts and ends do not match, skipping parsing phrases") return From 1c44183fa8ee42ac556923bcef5c5064cc242fad Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 5 Feb 2024 11:45:01 +0100 Subject: [PATCH 083/197] Include cadence, phrases, harmony properties in part. --- partitura/score.py | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index e8c4adfd..64186a0c 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -628,6 +628,41 @@ def tempo_directions(self): """ return [e for e in self.iter_all(TempoDirection, include_subclasses=True)] + @property + def cadences(self): + """Return a list of all cadences in the part + + Returns + ------- + list + List of Cadence objects + + """ + return [e for e in self.iter_all(Cadence, include_subclasses=False)] + + @property + def harmony(self): + """Return a list of all harmony in the part + + Returns + ------- + list + List of Harmony objects + + """ + return [e for e in self.iter_all(Harmony, include_subclasses=True)] + + @property + def phrases(self): + """Return a list of all phrases in the part + + Returns + ------- + list + List of Phrase objects + """ + return [e for e in self.iter_all(Phrase, include_subclasses=False)] + @property def articulations(self): """Return a list of all Articulation markings in the part @@ -2721,7 +2756,7 @@ def __str__(self): return f'{super().__str__()} "{self.text}"' -class RomanNumeral(TimedObject): +class RomanNumeral(Harmony): """A harmony element in the score usually for Roman Numerals. Parameters @@ -2736,7 +2771,7 @@ class RomanNumeral(TimedObject): """ def __init__(self, text, inversion=None, local_key=None, primary_degree=None, secondary_degree=None, quality=None): - super().__init__() + super().__init__(text) self.text = text self.accepted_qualities = ('7', 'aug', 'aug6', 'aug7', 'dim', 'dim7', 'hdim7', 'maj', 'maj7', 'min', 'min7') self.has_seven = "7" in text @@ -2884,11 +2919,11 @@ def __str__(self): return f'{super().__str__()}' -class ChordSymbol(TimedObject): +class ChordSymbol(Harmony): """A harmony element in the score usually for Chord Symbols.""" def __init__(self, root, kind, bass=None): - super().__init__() + super().__init__(text=root + kind + (f"/{bass}" if bass else "")) self.kind = kind self.root = root self.bass = bass From d5b560776fa7e20800318e7f9b8e8a46e6927702 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 12 Feb 2024 15:14:45 +0100 Subject: [PATCH 084/197] added property to interval class, to return the number of semitones. --- partitura/score.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/partitura/score.py b/partitura/score.py index 64186a0c..50aabef1 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -15,7 +15,7 @@ from numbers import Number # import copy -from partitura.utils.music import MUSICAL_BEATS, INTERVALCLASSES +from partitura.utils.music import MUSICAL_BEATS, INTERVALCLASSES, INTERVAL_TO_SEMITONES import warnings, sys import numpy as np import re @@ -2963,6 +2963,10 @@ def validate(self): "down", ], f"Interval direction {self.direction} not found" + @property + def semitones(self): + return INTERVAL_TO_SEMITONES[self.quality + str(self.number)] + def __str__(self): return f'{super().__str__()} "{self.number}{self.quality}"' From ac52b063b784ccdc00c94d42a94e865c99a51a8e Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 12 Feb 2024 18:35:24 +0100 Subject: [PATCH 085/197] Renaming mei export file and removing old version. --- partitura/__init__.py | 2 +- partitura/io/__init__.py | 2 +- partitura/io/exportmei.py | 2479 +++++---------------------------- partitura/io/mei_export_v2.py | 383 ----- 4 files changed, 385 insertions(+), 2481 deletions(-) delete mode 100644 partitura/io/mei_export_v2.py diff --git a/partitura/__init__.py b/partitura/__init__.py index 2dbcadac..3d54e093 100644 --- a/partitura/__init__.py +++ b/partitura/__init__.py @@ -23,7 +23,7 @@ from .io.importparangonada import load_parangonada_csv from .io.exportparangonada import save_parangonada_csv, save_csv_for_parangonada from .io.exportaudio import save_wav -from .io.mei_export_v2 import save_mei +from .io.exportmei import save_mei from .display import render from . import musicanalysis from .musicanalysis import make_note_features, compute_note_array, full_note_array diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index 119c6d3e..b9e39bee 100644 --- a/partitura/io/__init__.py +++ b/partitura/io/__init__.py @@ -15,7 +15,7 @@ from .importparangonada import load_parangonada_csv from .exportparangonada import save_parangonada_csv from .importmusic21 import load_music21 -from .mei_export_v2 import save_mei +from .exportmei import save_mei from partitura.utils.misc import ( deprecated_alias, deprecated_parameter, diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index 9de25a7d..ac5ccfe0 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -1,2096 +1,383 @@ -# import partitura -# import partitura.score as score -# from lxml import etree -# from partitura.utils.generic import partition -# from partitura.utils.music import estimate_symbolic_duration -# from copy import copy - - -# name_space = "http://www.music-encoding.org/ns/mei" - -# xml_id_string = "{http://www.w3.org/XML/1998/namespace}id" - - -# def extend_key(dict_of_lists, key, value): -# """extend or create a list at the given key in the given dictionary - -# Parameters -# ---------- -# dict_of_lists: dictionary -# where all values are lists -# key: self explanatory -# value: self explanatory - -# """ - -# if key in dict_of_lists.keys(): -# if isinstance(value, list): -# dict_of_lists[key].extend(value) -# else: -# dict_of_lists[key].append(value) -# else: -# dict_of_lists[key] = value if isinstance(value, list) else [value] - - -# def calc_dur_dots_split_notes_first_temp_dur(note, measure, num_to_numbase_ratio=1): -# """ -# Notes have to be represented as a string of elemental notes (there is no notation for arbitrary durations) -# This function calculates this string (the durations of the elemental notes and their dot counts), -# whether the note crosses the measure and the temporal duration of the first elemental note - -# Parameters -# ---------- -# note: score.GenericNote -# The note whose representation as a string of elemental notes is calculated -# measure: score.Measure -# The measure which contains note -# num_to_numbase_ratio: float, optional -# scales the duration of note according to whether or not it belongs to a tuplet and which one - - -# Returns -# ------- -# dur_dots: list of int pairs -# this describes the string of elemental notes that represent the note notationally -# every pair in the list contains the duration and the dot count of an elemental note and -# the list is ordered by duration in decreasing order -# split_notes: list or None -# an empty list if note crosses measure -# None if it doesn't -# first_temp_dur: int or None -# duration of first elemental note in partitura time -# """ - -# if measure == "pad": -# return [], None, None - -# if isinstance(note, score.GraceNote): -# main_note = note.main_note -# # HACK: main note should actually be always not None for a proper GraceNote -# if main_note != None: -# dur_dots, _, _ = calc_dur_dots_split_notes_first_temp_dur( -# main_note, measure -# ) -# dur_dots = [(2 * dur_dots[0][0], dur_dots[0][1])] -# else: -# dur_dots = [(8, 0)] -# note.id += "_missing_main_note" -# return dur_dots, None, None - -# note_duration = note.duration - -# split_notes = None - -# if note.start.t + note.duration > measure.end.t: -# note_duration = measure.end.t - note.start.t -# split_notes = [] - -# quarter_dur = measure.start.quarter -# fraction = num_to_numbase_ratio * note_duration / quarter_dur - -# int_part = int(fraction) -# frac_part = fraction - int_part - -# # calc digits of fraction in base2 -# untied_durations = [] -# pow_of_2 = 1 - -# while int_part > 0: -# bit = int_part % 2 -# untied_durations.insert(0, bit * pow_of_2) -# int_part = int_part // 2 -# pow_of_2 *= 2 - -# pow_of_2 = 1 / 2 - -# while frac_part > 0: -# frac_part *= 2 -# bit = int(frac_part) -# frac_part -= bit -# untied_durations.append(bit * pow_of_2) -# pow_of_2 /= 2 - -# dur_dots = [] - -# curr_dur = 0 -# curr_dots = 0 - -# def add_dd(dur_dots, dur, dots): -# dur_dots.append((int(4 / dur), dots)) - -# for untied_dur in untied_durations: -# if curr_dur != 0: -# if untied_dur == 0: -# add_dd(dur_dots, curr_dur, curr_dots) -# curr_dots = 0 -# curr_dur = 0 -# else: -# curr_dots += 1 -# else: -# curr_dur = untied_dur - -# if curr_dur != 0: -# add_dd(dur_dots, curr_dur, curr_dots) - -# first_temp_dur = int(untied_durations[0] * quarter_dur) - -# return dur_dots, split_notes, first_temp_dur - - -# def insert_elem_check(t, inbetween_notes_elems): -# """Check if something like a clef etc appears before time t - -# Parameters -# ---------- -# t: int -# time from a Timepoint -# inbetween_notes_elems: list of InbetweenNotesElements -# a list of objects describing things like clefs etc - -# Returns -# ------- -# True if something like a clef etc appears before time t -# """ - -# for ine in inbetween_notes_elems: -# if ine.elem != None and ine.elem.start.t <= t: -# return True - -# return False - - -# def partition_handle_none(func, iter, partition_attrib): -# p = partition(func, iter) -# newKey = None - -# if None in p.keys(): -# raise KeyError( -# 'PARTITION ERROR: some elements of set do not have partition attribute "' -# + partition_attrib -# + '"' -# ) - -# return p - - -# def add_child(parent, child_name): -# return etree.SubElement(parent, child_name) - - -# def set_attributes(elem, *list_attrib_val): -# for attrib_val in list_attrib_val: -# elem.set(attrib_val[0], str(attrib_val[1])) - - -# def attribs_of_key_sig(ks): -# """ -# Returns values of a score.KeySignature object necessary for a MEI document - -# Parameters -# ---------- -# ks: score.KeySignature - -# Returns -# ------- -# fifths: string -# describes the circle of fifths -# mode: string -# "major" or "minor" -# pname: string -# pitch letter -# """ - -# key = ks.name -# pname = key[0].lower() -# mode = "major" - -# if len(key) == 2: -# mode = "minor" - -# fifths = str(abs(ks.fifths)) - -# if ks.fifths < 0: -# fifths += "f" -# elif ks.fifths > 0: -# fifths += "s" - -# return fifths, mode, pname - - -# def first_instances_per_part( -# cls, parts, start=score.TimePoint(0), end=score.TimePoint(1) -# ): -# """ -# Returns the first instances of a class (multiple objects with same start time are possible) in each part - -# Parameters -# ---------- -# cls: class -# parts: list of score.Part -# start: score.TimePoint, optional -# start of the range to search in -# end: score.TimePoint, optional -# end of the range to search in - -# Returns -# ------- -# instances_per_part: list of list of instances of cls -# sublists might be empty -# if all sublists are empty, instances_per_part is empty -# """ -# if not isinstance(start, list): -# start = [start] * len(parts) -# elif not len(parts) == len(start): -# raise ValueError( -# "ERROR at first_instances_per_part: start times are given as list with different size to parts list" -# ) - -# if not isinstance(end, list): -# end = [end] * len(parts) -# elif not len(parts) == len(end): -# raise ValueError( -# "ERROR at first_instances_per_part: end times are given as list with different size to parts list" -# ) - -# for i in range(len(parts)): -# if start[i] == None and end[i] != None or start[i] != None and end[i] == None: -# raise ValueError( -# "ERROR at first_instances_per_part: (start==None) != (end==None) (None elements in start have to be at same position as in end and vice versa)" -# ) - -# instances_per_part = [] - -# non_empty = False - -# for i, p in enumerate(parts): -# s = start[i] -# e = end[i] - -# if s == None: -# instances_per_part.append([]) -# continue - -# instances = list(p.iter_all(cls, s, e)) - -# if len(instances) == 0: -# instances_per_part.append([]) -# continue - -# non_empty = True -# t = min(instances, key=lambda i: i.start.t).start.t -# instances_per_part.append([i for i in instances if t == i.start.t]) - -# if non_empty: -# return instances_per_part - -# return [] - - -# def first_instance_per_part( -# cls, parts, start=score.TimePoint(0), end=score.TimePoint(1) -# ): -# """ -# Reduce the result of first_instances_per_part, a 2D list, to a 1D list -# If there are multiple first instances then program aborts with error message - -# Parameters -# ---------- -# cls: class -# parts: list of score.Part -# start: score.TimePoint, optional -# start of the range to search in -# end: score.TimePoint, optional -# end of the range to search in - -# Returns -# ------- -# fipp: list of instances of cls -# elements might be None -# """ -# fispp = first_instances_per_part(cls, parts, start, end) - -# fipp = [] - -# for i, fis in enumerate(fispp): -# if len(fis) == 0: -# fipp.append(None) -# elif len(fis) == 1: -# fipp.append(fis[0]) -# else: -# raise ValueError( -# "Part " + parts[i].name, -# "ID " + parts[i].id, -# "has more than one instance of " -# + str(cls) -# + " at beginning t=0, but there should only be a single one", -# ) - -# return fipp - - -# def first_instances(cls, part, start=score.TimePoint(0), end=score.TimePoint(1)): -# """ -# Returns the first instances of a class (multiple objects with same start time are possible) in the part - -# Parameters -# ---------- -# cls: class -# part: score.Part -# start: score.TimePoint, optional -# start of the range to search in -# end: score.TimePoint, optional -# end of the range to search in - -# Returns -# ------- -# fis: list of instances of cls -# might be empty -# """ -# fis = first_instances_per_part(cls, [part], start, end) - -# if len(fis) == 0: -# return [] - -# return fis[0] - - -# def first_instance(cls, part, start=score.TimePoint(0), end=score.TimePoint(1)): -# """ -# Reduce the result of first_instance_per_part, a 1D list, to an element -# If there are multiple first instances then program aborts with error message - -# Parameters -# ---------- -# cls: class -# part: score.Part -# start: score.TimePoint, optional -# start of the range to search in -# end: score.TimePoint, optional -# end of the range to search in - -# Returns -# ------- -# fi: instance of cls or None -# """ -# fi = first_instance_per_part(cls, [part], start, end) - -# if len(fi) == 0: -# return None - -# return fi[0] - - -# def common_signature(cls, sig_eql, parts, current_measures=None): -# """ -# Calculate whether a list of parts has a common signature (as in key or time signature) - -# Parameters -# ---------- -# cls: score.KeySignature or score.TimeSignature -# sig_eql: function -# takes 2 signature objects as input and returns whether they are equivalent (in some sense) -# parts: list of score.Part -# current_measures: list of score.Measure, optional -# current as in the measures of the parts that are played at the same time and are processed - -# Returns -# ------- -# common_sig: instance of cls -# might be None if there is no commonality between parts -# """ -# sigs = None -# if current_measures != None: -# # HACK: measures should probably not contain "pad" at this point, but an actual dummy measure with start and end times? -# sigs = first_instance_per_part( -# cls, -# parts, -# start=[cm.start if cm != "pad" else None for cm in current_measures], -# end=[cm.end if cm != "pad" else None for cm in current_measures], -# ) -# else: -# sigs = first_instance_per_part(cls, parts) - -# if sigs == None or len(sigs) == 0 or None in sigs: -# return None - -# common_sig = sigs.pop() - -# for sig in sigs: -# if sig.start.t != common_sig.start.t or not sig_eql(sig, common_sig): -# return None - -# return common_sig - - -# def vertical_slice(list_2d, index): -# """ -# Returns elements of the sublists at index in a 1D list -# all sublists of list_2d have to have len > index -# """ -# vslice = [] - -# for list_1d in list_2d: -# vslice.append(list_1d[index]) - -# return vslice - - -# def time_sig_eql(ts1, ts2): -# """ -# equivalence function for score.TimeSignature objects -# """ -# return ts1.beats == ts2.beats and ts1.beat_type == ts2.beat_type - - -# def key_sig_eql(ks1, ks2): -# """ -# equivalence function for score.KeySignature objects -# """ -# return ks1.name == ks2.name and ks1.fifths == ks2.fifths - - -# def idx(len_obj): -# return range(len(len_obj)) - - -# def attribs_of_clef(clef): -# """ -# Returns values of a score.Clef object necessary for a MEI document - -# Parameters -# ---------- -# clef: score.Clef - -# Returns -# ------- -# sign: string -# shape of clef (F,G, etc) -# line: -# which line to place clef on -# """ -# sign = clef.sign - -# if sign == "percussion": -# sign = "perc" - -# if clef.octave_change != None and clef.octave_change != 0: -# place = "above" - -# if clef.octave_change < 0: -# place = "below" - -# return sign, clef.line, 1 + 7 * abs(clef.octave_change), place - -# return sign, clef.line - - -# def create_staff_def(staff_grp, clef): -# """ - -# Parameters -# ---------- -# staff_grp: etree.SubElement -# clef: score.Clef -# """ -# staff_def = add_child(staff_grp, "staffDef") - -# attribs = attribs_of_clef(clef) -# set_attributes( -# staff_def, -# ("n", clef.number), -# ("lines", 5), -# ("clef.shape", attribs[0]), -# ("clef.line", attribs[1]), -# ) -# if len(attribs) == 4: -# set_attributes( -# staff_def, ("clef.dis", attribs[2]), ("clef.dis.place", attribs[3]) -# ) - - -# def pad_measure(s, measure_per_staff, notes_within_measure_per_staff, auto_rest_count): -# """ -# Adds a fake measure ("pad") to the measures of the staff s and a score.Rest object to the notes - -# Parameters -# ---------- -# s: int -# staff number -# measure_per_staff: dict of score.Measure objects -# notes_within_measure_per_staff: dict of lists of score.GenericNote objects -# auto_rest_count: int -# a counter for all the score.Rest objects that are created automatically - -# Returns -# ------- -# incremented auto rest counter -# """ - -# measure_per_staff[s] = "pad" -# r = score.Rest(id="pR" + str(auto_rest_count), voice=1) -# r.start = score.TimePoint(0) -# r.end = r.start - -# extend_key(notes_within_measure_per_staff, s, r) -# return auto_rest_count + 1 - - -# class InbetweenNotesElement: -# """ -# InbetweenNotesElements contain information on objects like clefs, keysignatures, etc -# within the score and how to process them - -# Parameters -# ---------- -# name: string -# name of the element used in MEI -# attrib_names: list of strings -# names of the attributes of the MEI element -# attrib_vals_of: function -# a function that returns the attribute values of elem -# container_dict: dict of lists of partitura objects -# the container containing the required elements is at staff -# staff: int -# staff number -# skip_index: int -# init value for the cursor i (might skip 0) - -# Attributes -# ---------- -# name: string -# name of the element used in MEI -# attrib_names: list of strings -# names of the attributes of the MEI element -# elem: instance of partitura object -# attrib_vals_of: function -# a function that returns the attribute values of elem -# container: list of partitura objects -# the container where elem gets its values from -# i: int -# cursor that keeps track of position in container -# """ - -# __slots__ = ["name", "attrib_names", "attrib_vals_of", "container", "i", "elem"] - -# def __init__( -# self, name, attrib_names, attrib_vals_of, container_dict, staff, skip_index -# ): -# self.name = name -# self.attrib_names = attrib_names -# self.attrib_vals_of = attrib_vals_of - -# self.i = 0 -# self.elem = None - -# if staff in container_dict.keys(): -# self.container = container_dict[staff] -# if len(self.container) > skip_index: -# self.elem = self.container[skip_index] -# self.i = skip_index -# else: -# self.container = [] - - -# def chord_rep(chords, chord_i): -# return chords[chord_i][0] - - -# def handle_beam(open_up, parents): -# """ -# Using a stack of MEI elements, opens and closes beams - -# Parameters -# ---------- -# open_up: boolean -# flag that indicates whether to open or close recent beam -# parents: list of etree.SubElement -# stack of MEI elements that contain the beam element - -# Returns -# ------- -# unchanged open_up value -# """ -# if open_up: -# parents.append(add_child(parents[-1], "beam")) -# else: -# parents.pop() - -# return open_up - - -# def is_chord_in_tuplet(chord_i, tuplet_indices): -# """ -# check if chord falls in the range of a tuplet - -# Parameters -# ---------- -# chord_i: int -# index of chord within chords array -# tuplet_indices: list of int pairs -# contains the index ranges of all the tuplets in a measure of a staff - -# Returns -# ------- -# whether chord falls in the range of a tuplet -# """ -# for start, stop in tuplet_indices: -# if start <= chord_i and chord_i <= stop: -# return True - -# return False - - -# def calc_num_to_numbase_ratio(chord_i, chords, tuplet_indices): -# """ -# calculates how to scale a notes duration with regard to the tuplet it is in - -# Parameters -# ---------- -# chord_i: int -# index of chord within chords array -# chords: list of list of score.GenericNote -# array of chords (which are lists of notes) -# tuplet_indices: list of int pairs -# contains the index ranges of all the tuplets in a measure of a staff - -# Returns -# ------- -# the num to numbase ratio of a tuplet (eg. 3 in 2 tuplet is 1.5) -# """ -# rep = chords[chord_i][0] -# if not isinstance(rep, score.GraceNote) and is_chord_in_tuplet( -# chord_i, tuplet_indices -# ): -# return ( -# rep.symbolic_duration["actual_notes"] -# / rep.symbolic_duration["normal_notes"] -# ) -# return 1 - - -# def process_chord( -# chord_i, -# chords, -# inbetween_notes_elements, -# open_beam, -# auto_beaming, -# parents, -# dur_dots, -# split_notes, -# first_temp_dur, -# tuplet_indices, -# ties, -# measure, -# layer, -# tuplet_id_counter, -# open_tuplet, -# last_key_sig, -# note_alterations, -# notes_next_measure_per_staff, -# next_dur_dots=None, -# ): -# """ -# creates , , , etc elements from chords -# also creates , , etc elements if necessary for chords objects -# also creates , , etc elements before chord objects from inbetween_notes_elements - -# Parameters -# ---------- -# chord_i: int -# index of chord within chords array -# chords: list of list of score.GenericNote -# chord array -# inbetween_notes_elements: list of InbetweenNotesElements -# check this to see if something like clef needs to get inserted before chord -# open_beam: boolean -# flag that indicates whether a beam is currently open -# auto_beaming: boolean -# flag that determines if automatic beams should be created or if it is kept manual -# parents: list of etree.SubElement -# stack of MEI elements that contain the most recent beam element -# dur_dots: list of int pairs -# describes how the chord actually gets notated via tied notes, each pair contains the duration of the notated note and its dot count -# split_notes: list -# this is either empty or None -# if None, nothing is done with this -# if an empty list, that means this chord crosses into the next measure and a chord is created for the next measure which is tied to this one -# first_temp_dur: int -# amount of ticks (as in partitura) of the first notated note -# tuplet_indices: list of int pairs -# the ranges of tuplets within the chords array -# ties: dict -# out parameter, contains pairs of IDs which need to be connected via ties -# this function also adds to that -# measure: score.Measure - -# layer: etree.SubElement -# the parent element of the elements created here -# tuplet_id_counter: int - -# open_tuplet: boolean -# describes if a tuplet is open or not -# last_key_sig: score.KeySignature -# the key signature this chord should be interpeted in -# note_alterations: dict -# contains the alterations of staff positions (notes) that are relevant for this chord -# notes_next_measure_per_staff: dict of lists of score.GenericNote -# out parameter, add the result of split_notes into this -# next_dur_dots: list of int pairs, optional -# needed for proper beaming - -# Returns -# ------- -# tuplet_id_counter: int -# incremented if tuplet created -# open_beam: boolean -# eventually modified if beam opened or closed -# open_tuplet: boolean -# eventually modified if tuplet opened or closed -# """ - -# chord_notes = chords[chord_i] -# rep = chord_notes[0] - -# for ine in inbetween_notes_elements: -# if insert_elem_check(rep.start.t, [ine]): -# # note should maybe be split according to keysig or clef etc insertion time, right now only beaming is disrupted -# if open_beam and auto_beaming: -# open_beam = handle_beam(False, parents) - -# xml_elem = add_child(parents[-1], ine.name) -# attrib_vals = ine.attrib_vals_of(ine.elem) - -# if ine.name == "keySig": -# last_key_sig = ine.elem - -# if len(ine.attrib_names) < len(attrib_vals): -# raise ValueError( -# "ERROR at insertion of inbetween_notes_elements: there are more attribute values than there are attribute names for xml element " -# + ine.name -# ) - -# for nv in zip(ine.attrib_names[: len(attrib_vals)], attrib_vals): -# set_attributes(xml_elem, nv) - -# if ine.i + 1 >= len(ine.container): -# ine.elem = None -# else: -# ine.i += 1 -# ine.elem = ine.container[ine.i] - -# if is_chord_in_tuplet(chord_i, tuplet_indices): -# if not open_tuplet: -# parents.append(add_child(parents[-1], "tuplet")) -# num = rep.symbolic_duration["actual_notes"] -# numbase = rep.symbolic_duration["normal_notes"] -# set_attributes( -# parents[-1], -# (xml_id_string, "t" + str(tuplet_id_counter)), -# ("num", num), -# ("numbase", numbase), -# ) -# tuplet_id_counter += 1 -# open_tuplet = True -# elif open_tuplet: -# parents.pop() -# open_tuplet = False - -# def set_dur_dots(elem, dur_dots): -# dur, dots = dur_dots -# set_attributes(elem, ("dur", dur)) - -# if dots > 0: -# set_attributes(elem, ("dots", dots)) - -# if isinstance(rep, score.Note): -# if auto_beaming: -# # for now all notes are beamed, however some rules should be obeyed there, see Note Beaming and Grouping - -# # check to close beam -# if open_beam and ( -# dur_dots[0][0] < 8 -# or chord_i - 1 >= 0 -# and type(rep) != type(chord_rep(chords, chord_i - 1)) -# ): -# open_beam = handle_beam(False, parents) - -# # check to open beam (maybe again) -# if not open_beam and dur_dots[0][0] >= 8: -# # open beam if there are multiple "consecutive notes" which don't get interrupted by some element -# if len(dur_dots) > 1 and not insert_elem_check( -# rep.start.t + first_temp_dur, inbetween_notes_elements -# ): -# open_beam = handle_beam(True, parents) - -# # open beam if there is just a single note that is not the last one in measure and next note in measure is of same type and fits in beam as well, without getting interrupted by some element -# elif ( -# len(dur_dots) <= 1 -# and chord_i + 1 < len(chords) -# and next_dur_dots[0][0] >= 8 -# and type(rep) == type(chord_rep(chords, chord_i + 1)) -# and not insert_elem_check( -# chord_rep(chords, chord_i + 1).start.t, inbetween_notes_elements -# ) -# ): -# open_beam = handle_beam(True, parents) -# elif ( -# open_beam -# and chord_i > 0 -# and rep.beam != chord_rep(chords, chord_i - 1).beam -# ): -# open_beam = handle_beam(False, parents) - -# if not auto_beaming and not open_beam and rep.beam != None: -# open_beam = handle_beam(True, parents) - -# def conditional_gracify(elem, rep, chord_i, chords): -# if isinstance(rep, score.GraceNote): -# grace = "unacc" - -# if rep.grace_type == "appoggiatura": -# grace = "acc" - -# set_attributes(elem, ("grace", grace)) - -# if rep.steal_proportion != None: -# set_attributes( -# elem, ("grace.time", str(rep.steal_proportion * 100) + "%") -# ) - -# if chord_i == 0 or not isinstance( -# chord_rep(chords, chord_i - 1), score.GraceNote -# ): -# chords[chord_i] = [copy(n) for n in chords[chord_i]] - -# for n in chords[chord_i]: -# n.tie_next = n.main_note - -# def create_note(parent, n, id, last_key_sig, note_alterations): -# note = add_child(parent, "note") - -# step = n.step.lower() -# set_attributes( -# note, (xml_id_string, id), ("pname", step), ("oct", n.octave) -# ) - -# if n.articulations != None and len(n.articulations) > 0: -# artics = [] - -# translation = { -# "accent": "acc", -# "staccato": "stacc", -# "tenuto": "ten", -# "staccatissimo": "stacciss", -# "spiccato": "spicc", -# "scoop": "scoop", -# "plop": "plop", -# "doit": "doit", -# } - -# for a in n.articulations: -# if a in translation.keys(): -# artics.append(translation[a]) -# set_attributes(note, ("artic", " ".join(artics))) - -# sharps = ["f", "c", "g", "d", "a", "e", "b"] -# flats = list(reversed(sharps)) - -# staff_pos = step + str(n.octave) - -# alter = n.alter or 0 - -# def set_accid(note, acc, note_alterations, staff_pos, alter): -# if ( -# staff_pos in note_alterations.keys() -# and alter == note_alterations[staff_pos] -# ): -# return -# set_attributes(note, ("accid", acc)) -# note_alterations[staff_pos] = alter - -# # sharpen note if: is sharp, is not sharpened by key or prev alt -# # flatten note if: is flat, is not flattened by key or prev alt -# # neutralize note if: is neutral, is sharpened/flattened by key or prev alt - -# # check if note is sharpened/flattened by prev alt or key -# if ( -# staff_pos in note_alterations.keys() -# and note_alterations[staff_pos] != 0 -# or last_key_sig.fifths > 0 -# and step in sharps[: last_key_sig.fifths] -# or last_key_sig.fifths < 0 -# and step in flats[: -last_key_sig.fifths] -# ): -# if alter == 0: -# set_accid(note, "n", note_alterations, staff_pos, alter) -# elif alter > 0: -# set_accid(note, "s", note_alterations, staff_pos, alter) -# elif alter < 0: -# set_accid(note, "f", note_alterations, staff_pos, alter) - -# return note - -# if len(chord_notes) > 1: -# chord = add_child(parents[-1], "chord") - -# set_dur_dots(chord, dur_dots[0]) - -# conditional_gracify(chord, rep, chord_i, chords) - -# for n in chord_notes: -# create_note(chord, n, n.id, last_key_sig, note_alterations) - -# else: -# note = create_note(parents[-1], rep, rep.id, last_key_sig, note_alterations) -# set_dur_dots(note, dur_dots[0]) - -# conditional_gracify(note, rep, chord_i, chords) - -# if len(dur_dots) > 1: -# for n in chord_notes: -# ties[n.id] = [n.id] - -# def create_split_up_notes(chord_notes, i, parents, dur_dots, ties, rep): -# if len(chord_notes) > 1: -# chord = add_child(parents[-1], "chord") -# set_dur_dots(chord, dur_dots[i]) - -# for n in chord_notes: -# id = n.id + "-" + str(i) - -# ties[n.id].append(id) -# create_note(chord, n, id, last_key_sig, note_alterations) -# else: -# id = rep.id + "-" + str(i) - -# ties[rep.id].append(id) - -# note = create_note( -# parents[-1], rep, id, last_key_sig, note_alterations -# ) - -# set_dur_dots(note, dur_dots[i]) - -# for i in range(1, len(dur_dots) - 1): -# if not open_beam and dur_dots[i][0] >= 8: -# open_beam = handle_beam(True, parents) - -# create_split_up_notes(chord_notes, i, parents, dur_dots, ties, rep) - -# create_split_up_notes( -# chord_notes, len(dur_dots) - 1, parents, dur_dots, ties, rep -# ) - -# if split_notes != None: - -# for n in chord_notes: -# split_notes.append(score.Note(n.step, n.octave, id=n.id + "s")) - -# if len(dur_dots) > 1: -# for n in chord_notes: -# ties[n.id].append(n.id + "s") -# else: -# for n in chord_notes: -# ties[n.id] = [n.id, n.id + "s"] - -# for n in chord_notes: -# if n.tie_next != None: -# if n.id in ties.keys(): -# ties[n.id].append(n.tie_next.id) -# else: -# ties[n.id] = [n.id, n.tie_next.id] - -# elif isinstance(rep, score.Rest): -# if split_notes != None: -# split_notes.append(score.Rest(id=rep.id + "s")) - -# if ( -# measure == "pad" -# or measure.start.t == rep.start.t -# and measure.end.t == rep.end.t -# ): -# rest = add_child(layer, "mRest") - -# set_attributes(rest, (xml_id_string, rep.id)) -# else: -# rest = add_child(layer, "rest") - -# set_attributes(rest, (xml_id_string, rep.id)) - -# set_dur_dots(rest, dur_dots[0]) - -# for i in range(1, len(dur_dots)): -# rest = add_child(layer, "rest") - -# id = rep.id + str(i) - -# set_attributes(rest, (xml_id_string, id)) -# set_dur_dots(rest, dur_dots[i]) - -# if split_notes != None: -# for sn in split_notes: -# sn.voice = rep.voice -# sn.start = measure.end -# sn.end = score.TimePoint(rep.start.t + rep.duration) - -# extend_key(notes_next_measure_per_staff, s, sn) - -# return tuplet_id_counter, open_beam, open_tuplet - - -# def create_score_def(measures, measure_i, parts, parent): -# """ -# creates - -# Parameters -# ---------- -# measures: list of score.Measure -# measure_i: int -# index of measure currently processed within measures -# parts: list of score.Part -# parent: etree.SubElement -# parent of -# """ -# reference_measures = vertical_slice(measures, measure_i) - -# common_key_sig = common_signature( -# score.KeySignature, key_sig_eql, parts, reference_measures -# ) -# common_time_sig = common_signature( -# score.TimeSignature, time_sig_eql, parts, reference_measures -# ) - -# score_def = None - -# if common_key_sig != None or common_time_sig != None: -# score_def = add_child(parent, "scoreDef") - -# if common_key_sig != None: -# fifths, mode, pname = attribs_of_key_sig(common_key_sig) - -# set_attributes( -# score_def, ("key.sig", fifths), ("key.mode", mode), ("key.pname", pname) -# ) - -# if common_time_sig != None: -# set_attributes( -# score_def, -# ("meter.count", common_time_sig.beats), -# ("meter.unit", common_time_sig.beat_type), -# ) - -# return score_def - - -# class MeasureContent: -# """ -# Simply a bundle for all the data of a measure that needs to be processed for a MEI document - -# Attributes -# ---------- -# ties_per_staff: dict of lists -# clefs_per_staff: dict of lists -# key_sigs_per_staff: dict of lists -# time_sigs_per_staff: dict of lists -# measure_per_staff: dict of lists -# tuplets_per_staff: dict of lists -# slurs: list -# dirs: list -# dynams: list -# tempii: list -# fermatas: list -# """ - -# __slots__ = [ -# "ties_per_staff", -# "clefs_per_staff", -# "key_sigs_per_staff", -# "time_sigs_per_staff", -# "measure_per_staff", -# "tuplets_per_staff", -# "slurs", -# "dirs", -# "dynams", -# "tempii", -# "fermatas", -# ] - -# def __init__(self): -# self.ties_per_staff = {} -# self.clefs_per_staff = {} -# self.key_sigs_per_staff = {} -# self.time_sigs_per_staff = {} -# self.measure_per_staff = {} -# self.tuplets_per_staff = {} - -# self.slurs = [] -# self.dirs = [] -# self.dynams = [] -# self.tempii = [] -# self.fermatas = [] - - -# def extract_from_measures( -# parts, -# measures, -# measure_i, -# staves_per_part, -# auto_rest_count, -# notes_within_measure_per_staff, -# ): -# """ -# Returns a bundle of data regarding the measure currently processed, things like notes, key signatures, etc -# Also creates padding measures, necessary for example, for staves of instruments which do not play in the current measure - -# Parameters -# ---------- -# parts: list of score.Part -# measures: list of score.Measure -# measure_i: int -# index of current measure within measures -# staves_per_part: dict of list of ints -# staff enumeration partitioned by part -# auto_rest_count: int -# counter for the IDs of automatically generated rests -# notes_within_measure_per_staff: dict of lists of score.GenericNote -# in and out parameter, might contain note objects that have crossed from previous measure into current one - -# Returns -# ------- -# auto_rest_count: int -# incremented if score.Rest created -# current_measure_content: MeasureContent -# bundle for all the data that is extracted from the currently processed measure -# """ -# current_measure_content = MeasureContent() - -# for part_i, part in enumerate(parts): -# m = measures[part_i][measure_i] - -# if m == "pad": -# for s in staves_per_part[part_i]: -# auto_rest_count = pad_measure( -# s, -# current_measure_content.measure_per_staff, -# notes_within_measure_per_staff, -# auto_rest_count, -# ) - -# continue - -# def cls_within_measure(part, cls, measure, incl_subcls=False): -# return part.iter_all( -# cls, measure.start, measure.end, include_subclasses=incl_subcls -# ) - -# def cls_within_measure_list(part, cls, measure, incl_subcls=False): -# return list(cls_within_measure(part, cls, measure, incl_subcls)) - -# clefs_within_measure_per_staff_per_part = partition_handle_none( -# lambda c: c.number, cls_within_measure(part, score.Clef, m), "number" -# ) -# key_sigs_within_measure = cls_within_measure_list(part, score.KeySignature, m) -# time_sigs_within_measure = cls_within_measure_list(part, score.TimeSignature, m) -# current_measure_content.slurs.extend(cls_within_measure(part, score.Slur, m)) -# tuplets_within_measure = cls_within_measure_list(part, score.Tuplet, m) - -# beat_map = part.beat_map - -# def calc_tstamp(beat_map, t, measure): -# return beat_map(t) - beat_map(measure.start.t) + 1 - -# for w in cls_within_measure(part, score.Words, m): -# tstamp = calc_tstamp(beat_map, w.start.t, m) -# current_measure_content.dirs.append((tstamp, w)) - -# for tempo in cls_within_measure(part, score.Tempo, m): -# tstamp = calc_tstamp(beat_map, tempo.start.t, m) -# current_measure_content.tempii.append( -# (tstamp, staves_per_part[part_i][0], tempo) -# ) - -# for fermata in cls_within_measure(part, score.Fermata, m): -# tstamp = calc_tstamp(beat_map, fermata.start.t, m) -# current_measure_content.fermatas.append((tstamp, fermata.ref.staff)) - -# for dynam in cls_within_measure(part, score.Direction, m, True): -# tstamp = calc_tstamp(beat_map, dynam.start.t, m) -# tstamp2 = None - -# if dynam.end != None: -# measure_counter = measure_i -# while True: -# if dynam.end.t <= measures[part_i][measure_counter].end.t: -# tstamp2 = calc_tstamp( -# beat_map, dynam.end.t, measures[part_i][measure_counter] -# ) - -# tstamp2 = str(measure_counter - measure_i) + "m+" + str(tstamp2) - -# break -# elif ( -# measure_counter + 1 >= len(measures[part_i]) -# or measures[part_i][measure_counter + 1] == "pad" -# ): -# raise ValueError( -# "A score.Direction instance has an end time that exceeds actual non-padded measures" -# ) -# else: -# measure_counter += 1 - -# current_measure_content.dynams.append((tstamp, tstamp2, dynam)) - -# notes_within_measure_per_staff_per_part = partition_handle_none( -# lambda n: n.staff, -# cls_within_measure(part, score.GenericNote, m, True), -# "staff", -# ) - -# for s in staves_per_part[part_i]: -# current_measure_content.key_sigs_per_staff[s] = key_sigs_within_measure -# current_measure_content.time_sigs_per_staff[s] = time_sigs_within_measure -# current_measure_content.tuplets_per_staff[s] = tuplets_within_measure - -# if s not in notes_within_measure_per_staff_per_part.keys(): -# auto_rest_count = pad_measure( -# s, -# current_measure_content.measure_per_staff, -# notes_within_measure_per_staff, -# auto_rest_count, -# ) - -# for s, nwp in notes_within_measure_per_staff_per_part.items(): -# extend_key(notes_within_measure_per_staff, s, nwp) -# current_measure_content.measure_per_staff[s] = m - -# for s, cwp in clefs_within_measure_per_staff_per_part.items(): -# current_measure_content.clefs_per_staff[s] = cwp - -# return auto_rest_count, current_measure_content - - -# def create_measure( -# section, -# measure_i, -# staves_sorted, -# notes_within_measure_per_staff, -# score_def, -# tuplet_id_counter, -# auto_beaming, -# last_key_sig_per_staff, -# current_measure_content, -# ): -# """ -# creates a element within
-# also returns an updated id counter for tuplets and a dictionary of notes that cross into the next measure - -# Parameters -# ---------- -# section: etree.SubElement -# measure_i: int -# index of the measure created -# staves_sorted: list of ints -# a sorted list of the proper staff enumeration of the score -# notes_within_measure_per_staff: dict of lists of score.GenericNote -# contains score.Note, score.Rest, etc objects of the current measure, partitioned by staff enumeration -# will be further partitioned and sorted by voice, time and type (score.GraceNote) and eventually gathered into -# a list of equivalence classes called chords -# score_def: etree.SubElement -# tuplet_id_counter: int -# tuplets usually don't come with IDs, so an automatic counter takes care of that -# auto_beaming: boolean -# enables automatic beaming -# last_key_sig_per_staff: dict of score.KeySignature -# keeps track of the keysignature each staff is currently in -# current_measure_content: MeasureContent -# contains all sorts of data for the measure like tuplets, slurs, etc - -# Returns -# ------- -# tuplet_id_counter: int -# incremented if tuplet created -# notes_next_measure_per_staff: dict of lists of score.GenericNote -# score.GenericNote objects that cross into the next measure -# """ -# measure = add_child(section, "measure") -# set_attributes(measure, ("n", measure_i + 1)) - -# ties_per_staff = {} - -# for s in staves_sorted: -# note_alterations = {} - -# staff = add_child(measure, "staff") - -# set_attributes(staff, ("n", s)) - -# notes_within_measure_per_staff_per_voice = partition_handle_none( -# lambda n: n.voice, notes_within_measure_per_staff[s], "voice" -# ) - -# ties_per_staff_per_voice = {} - -# m = current_measure_content.measure_per_staff[s] - -# tuplets = [] -# if s in current_measure_content.tuplets_per_staff.keys(): -# tuplets = current_measure_content.tuplets_per_staff[s] - -# last_key_sig = last_key_sig_per_staff[s] - -# for voice, notes in notes_within_measure_per_staff_per_voice.items(): -# layer = add_child(staff, "layer") - -# set_attributes(layer, ("n", voice)) - -# ties = {} - -# notes_partition = partition_handle_none( -# lambda n: n.start.t, notes, "start.t" -# ) - -# chords = [] - -# for t in sorted(notes_partition.keys()): -# ns = notes_partition[t] - -# if len(ns) > 1: -# type_partition = partition_handle_none( -# lambda n: isinstance(n, score.GraceNote), ns, "isGraceNote" -# ) - -# if True in type_partition.keys(): -# gns = type_partition[True] - -# gn_chords = [] - -# def scan_backwards(gns): -# start = gns[0] - -# while isinstance(start.grace_prev, score.GraceNote): -# start = start.grace_prev - -# return start - -# start = scan_backwards(gns) - -# def process_grace_note(n, gns): -# if not n in gns: -# raise ValueError( -# "Error at forward scan of GraceNotes: a grace_next has either different staff, voice or starting time than GraceNote chain" -# ) -# gns.remove(n) -# return n.grace_next - -# while isinstance(start, score.GraceNote): -# gn_chords.append([start]) -# start = process_grace_note(start, gns) - -# while len(gns) > 0: -# start = scan_backwards(gns) - -# i = 0 -# while isinstance(start, score.GraceNote): -# if i >= len(gn_chords): -# raise IndexError( -# "ERROR at GraceNote-forward scanning: Difference in lengths of grace note sequences for different chord notes" -# ) -# gn_chords[i].append(start) -# start = process_grace_note(start, gns) -# i += 1 - -# if not i == len(gn_chords): -# raise IndexError( -# "ERROR at GraceNote-forward scanning: Difference in lengths of grace note sequences for different chord notes" -# ) - -# for gnc in gn_chords: -# chords.append(gnc) - -# if not False in type_partition.keys(): -# raise KeyError( -# "ERROR at ChordNotes-grouping: GraceNotes detected without additional regular Notes at same time; staff " -# + str(s) -# ) - -# reg_notes = type_partition[False] - -# rep = reg_notes[0] - -# for i in range(1, len(reg_notes)): -# n = reg_notes[i] - -# if n.duration != rep.duration: -# raise ValueError( -# "In staff " + str(s) + ",", -# "in measure " + str(m.number) + ",", -# "for voice " + str(voice) + ",", -# "2 notes start at time " + str(n.start.t) + ",", -# "but have different durations, namely " -# + n.id -# + " has duration " -# + str(n.duration) -# + " and " -# + rep.id -# + " has duration " -# + str(rep.duration), -# "change to same duration for a chord or change voice of one of the notes for something else", -# ) -# # HACK: unpitched notes are treated as Rests right now -# elif not isinstance(rep, score.Rest) and not isinstance( -# n, score.Rest -# ): -# if rep.beam != n.beam: -# print( -# "WARNING: notes within chords don't share the same beam", -# "specifically note " -# + str(rep) -# + " has beam " -# + str(rep.beam), -# "and note " + str(n) + " has beam " + str(n.beam), -# "export still continues though", -# ) -# elif set(rep.tuplet_starts) != set(n.tuplet_starts) and set( -# rep.tuplet_stops -# ) != set(n.tuplet_stops): -# print( -# "WARNING: notes within chords don't share same tuplets, export still continues though" -# ) -# chords.append(reg_notes) -# else: -# chords.append(ns) - -# tuplet_indices = [] -# for tuplet in tuplets: -# ci = 0 -# start = -1 -# stop = -1 -# while ci < len(chords): -# for n in chords[ci]: -# if tuplet in n.tuplet_starts: -# start = ci -# break -# for n in chords[ci]: -# if tuplet in n.tuplet_stops: -# stop = ci -# break - -# if start >= 0 and stop >= 0: -# if not start <= stop: -# raise ValueError( -# "In measure " + str(measure_i + 1) + ",", -# "in staff " + str(s) + ",", -# "[" + str(tuplet) + "] stops before it starts?", -# "start=" + str(start + 1) + "; stop=" + str(stop + 1), -# ) -# tuplet_indices.append((start, stop)) -# break - -# ci += 1 - -# parents = [layer] -# open_beam = False - -# ( -# next_dur_dots, -# next_split_notes, -# next_first_temp_dur, -# ) = calc_dur_dots_split_notes_first_temp_dur( -# chords[0][0], m, calc_num_to_numbase_ratio(0, chords, tuplet_indices) -# ) - -# inbetween_notes_elements = [ -# InbetweenNotesElement( -# "clef", -# ["shape", "line", "dis", "dis.place"], -# attribs_of_clef, -# current_measure_content.clefs_per_staff, -# s, -# int(measure_i == 0), -# ), -# InbetweenNotesElement( -# "keySig", -# ["sig", "mode", "pname", "sig.showchange"], -# (lambda ks: attribs_of_key_sig(ks) + ("true",)), -# current_measure_content.key_sigs_per_staff, -# s, -# int(score_def != None), -# ), -# InbetweenNotesElement( -# "meterSig", -# ["count", "unit"], -# lambda ts: (ts.beats, ts.beat_type), -# current_measure_content.time_sigs_per_staff, -# s, -# int(score_def != None), -# ), -# ] - -# open_tuplet = False - -# notes_next_measure_per_staff = {} - -# for chord_i in range(len(chords) - 1): -# dur_dots, split_notes, first_temp_dur = ( -# next_dur_dots, -# next_split_notes, -# next_first_temp_dur, -# ) -# ( -# next_dur_dots, -# next_split_notes, -# next_first_temp_dur, -# ) = calc_dur_dots_split_notes_first_temp_dur( -# chord_rep(chords, chord_i + 1), -# m, -# calc_num_to_numbase_ratio(chord_i + 1, chords, tuplet_indices), -# ) -# tuplet_id_counter, open_beam, open_tuplet = process_chord( -# chord_i, -# chords, -# inbetween_notes_elements, -# open_beam, -# auto_beaming, -# parents, -# dur_dots, -# split_notes, -# first_temp_dur, -# tuplet_indices, -# ties, -# m, -# layer, -# tuplet_id_counter, -# open_tuplet, -# last_key_sig, -# note_alterations, -# notes_next_measure_per_staff, -# next_dur_dots, -# ) - -# tuplet_id_counter, _, _ = process_chord( -# len(chords) - 1, -# chords, -# inbetween_notes_elements, -# open_beam, -# auto_beaming, -# parents, -# next_dur_dots, -# next_split_notes, -# next_first_temp_dur, -# tuplet_indices, -# ties, -# m, -# layer, -# tuplet_id_counter, -# open_tuplet, -# last_key_sig, -# note_alterations, -# notes_next_measure_per_staff, -# ) - -# ties_per_staff_per_voice[voice] = ties - -# ties_per_staff[s] = ties_per_staff_per_voice - -# for fermata in current_measure_content.fermatas: -# tstamp = fermata[0] -# fermata_staff = fermata[1] - -# f = add_child(measure, "fermata") -# set_attributes(f, ("staff", fermata_staff), ("tstamp", tstamp)) - -# for slur in current_measure_content.slurs: -# s = add_child(measure, "slur") -# if slur.start_note == None or slur.end_note == None: -# raise ValueError("Slur is missing start or end") -# set_attributes( -# s, -# ("staff", slur.start_note.staff), -# ("startid", "#" + slur.start_note.id), -# ("endid", "#" + slur.end_note.id), -# ) - -# for tstamp, word in current_measure_content.dirs: -# d = add_child(measure, "dir") -# set_attributes(d, ("staff", word.staff), ("tstamp", tstamp)) -# d.text = word.text - -# # smufl individual notes start with E1 -# # these are the last 2 digits of the codes -# metronome_codes = { -# "breve": "D0", -# "whole": "D2", -# "half": "D3", -# "h": "D3", -# "quarter": "D5", -# "q": "D5", -# "eighth": "D7", -# "e": "D5", -# "16th": "D9", -# "32nd": "DB", -# "64th": "DD", -# "128th": "DF", -# "256th": "E1", -# } - -# for tstamp, staff, tempo in current_measure_content.tempii: -# t = add_child(measure, "tempo") -# set_attributes(t, ("staff", staff), ("tstamp", tstamp)) - -# unit = str(tempo.unit) - -# dots = unit.count(".") - -# unit = unit[:-dots] - -# string_to_build = [ -# ' á', -# metronome_codes[unit or "q"], -# ";", -# ] - -# for i in range(dots): -# string_to_build.append("") - -# string_to_build.append(" = ") -# string_to_build.append(str(tempo.bpm)) - -# t.text = "".join(string_to_build) - -# for tstamp, tstamp2, dynam in current_measure_content.dynams: -# if isinstance(dynam, score.DynamicLoudnessDirection): -# d = add_child(measure, "hairpin") -# form = ( -# "cres" -# if isinstance(dynam, score.IncreasingLoudnessDirection) -# else "dim" -# ) -# set_attributes(d, ("form", form)) - -# # duration can also matter for other dynamics, might want to move this out of branch -# if tstamp2 != None: -# set_attributes(d, ("tstamp2", tstamp2)) -# else: -# d = add_child(measure, "dynam") -# d.text = dynam.text - -# set_attributes(d, ("staff", dynam.staff), ("tstamp", tstamp)) - -# for s, tps in ties_per_staff.items(): - -# for v, tpspv in tps.items(): - -# for ties in tpspv.values(): - -# for i in range(len(ties) - 1): -# tie = add_child(measure, "tie") - -# set_attributes( -# tie, -# ("staff", s), -# ("startid", "#" + ties[i]), -# ("endid", "#" + ties[i + 1]), -# ) - -# for s, k in current_measure_content.key_sigs_per_staff.items(): -# if len(k) > 0: -# last_key_sig_per_staff[s] = max(k, key=lambda k: k.start.t) - -# return tuplet_id_counter, notes_next_measure_per_staff - - -# def unpack_part_group(part_grp, parts=[]): -# """ -# Recursively gather individual parts into a list, flattening the tree of parts so to say - -# Parameters -# ---------- -# part_grp: score.PartGroup -# parts: list of score.Part, optional - -# Returns -# ------- -# parts: list of score.Part -# """ -# for c in part_grp.children: -# if isinstance(c, score.PartGroup): -# unpack_part_group(c, parts) -# else: -# parts.append(c) - -# return parts - - -# def save_mei( -# parts, -# auto_beaming=True, -# file_name="testResult", -# title_text=None, -# proper_staff_grp=False, -# ): -# """ -# creates an MEI document based on the parts provided -# So far only is used and not which means all the parts are gathered in one whole score and -# no individual scores are defined for individual parts - -# Parameters -# ---------- -# parts: score.Part, score.PartGroup or list of score.Part -# auto_beaming: boolean, optional -# if all beaming has been done manually then set to False -# otherwise this flag can be used to enable automatic beaming (beaming rules are still in progess) -# file_name: string, optional -# should not contain file extension, .mei will be added automatically -# title_text: string, optional -# name of the piece, e.g. "Klaviersonate Nr. 14" or "WAP" -# if not provided, a title will be derived from file_name -# proper_staff_grp: boolean, optional -# if true, group staves per part -# else group all staves together -# default is false because Verovio doesn't seem to render multiple staff groups correctly (but that just might be because multiple staff groups are not generated correctly in this function) -# """ - -# if isinstance(parts, score.PartGroup): -# parts = unpack_part_group(parts) -# elif isinstance(parts, score.Part): -# parts = [parts] - -# for p in parts: -# score.sanitize_part(p) - -# mei = etree.Element("mei") - -# mei_head = add_child(mei, "meiHead") -# music = add_child(mei, "music") - -# mei_head.set("xmlns", name_space) -# file_desc = add_child(mei_head, "fileDesc") -# title_stmt = add_child(file_desc, "titleStmt") -# pub_stmt = add_child(file_desc, "pubStmt") -# title = add_child(title_stmt, "title") -# title.set("type", "main") - -# # derive a title for the piece from the file_name -# if title_text == None: -# cursor = len(file_name) - 1 -# while cursor >= 0 and file_name[cursor] != "/": -# cursor -= 1 - -# tmp = file_name[cursor + 1 :].split("_") -# tmp = [s[:1].upper() + s[1:] for s in tmp] -# title.text = " ".join(tmp) -# else: -# title.text = title_text - -# body = add_child(music, "body") -# mdiv = add_child(body, "mdiv") -# mei_score = add_child(mdiv, "score") - -# classes_with_staff = [score.GenericNote, score.Words, score.Direction] - -# staves_per_part = [] - -# staves_are_valid = True - -# for p in parts: -# tmp = { -# staffed_obj.staff -# for cls in classes_with_staff -# for staffed_obj in p.iter_all(cls, include_subclasses=True) -# } -# tmp = tmp.union({clef.number for clef in p.iter_all(score.Clef)}) -# staves_per_part.append(list(tmp)) - -# if None in staves_per_part[-1]: -# staves_are_valid = False -# staves_per_part[-1].remove(None) - -# staves_per_part[-1].append( -# (max(staves_per_part[-1]) if len(staves_per_part[-1]) > 0 else 0) + 1 -# ) - -# staves_per_part[-1].sort() - -# if staves_are_valid: -# staves_sorted = sorted([s for staves in staves_per_part for s in staves]) - -# i = 0 - -# while i + 1 < len(staves_sorted): -# if staves_sorted[i] == staves_sorted[i + 1]: -# staves_are_valid = False -# break - -# i += 1 - -# if not staves_are_valid: -# staves_per_part_backup = staves_per_part - -# staves_sorted = [] -# staves_per_part = [] - -# # ASSUMPTION: staves are >0 -# max_staff = 0 -# for staves in staves_per_part_backup: -# if len(staves) == 0: -# staves_per_part.append([]) -# else: -# shift = [s + max_staff for s in staves] - -# max_staff += max(staves) - -# staves_sorted.extend(shift) -# staves_per_part.append(shift) - -# # staves_sorted.sort() - -# max_staff = 0 -# for i, p in enumerate(parts): -# for cls in classes_with_staff: -# for staff_obj in p.iter_all(cls, include_subclasses=True): -# staff_obj.staff = max_staff + ( -# staff_obj.staff -# if staff_obj.staff != None -# else max(staves_per_part_backup[i]) -# ) - -# for clef in p.iter_all(score.Clef): -# clef.number = max_staff + ( -# clef.number -# if clef.number != None -# else max(staves_per_part_backup[i]) -# ) - -# max_staff += ( -# max(staves_per_part_backup[i]) -# if len(staves_per_part_backup[i]) > 0 -# else 0 -# ) - -# measures = [list(parts[0].iter_all(score.Measure))] -# padding_required = False -# max_length = len(measures[0]) -# for i in range(1, len(parts)): -# m = list(parts[i].iter_all(score.Measure)) - -# if len(m) > max_length: -# max_length = len(m) - -# if not padding_required: -# padding_required = len(m) != len(measures[0]) - -# measures.append(m) - -# score_def = create_score_def(measures, 0, parts, mei_score) - -# score_def_setup = score_def - -# if score_def == None: -# score_def_setup = add_child(mei_score, "scoreDef") - -# clefs_per_part = first_instances_per_part(score.Clef, parts) - -# for i in idx(clefs_per_part): -# clefs_per_part[i] = partition_handle_none( -# lambda c: c.number, clefs_per_part[i], "number" -# ) - -# if len(clefs_per_part) == 0: -# create_staff_def( -# staff_grp, score.Clef(sign="G", line=2, number=1, octave_change=0) -# ) -# else: -# staff_grp = add_child(score_def_setup, "staffGrp") -# for staves in staves_per_part: -# if proper_staff_grp: -# staff_grp = add_child(score_def_setup, "staffGrp") - -# for s in staves: -# clefs = None - -# for clefs_per_staff in clefs_per_part: -# if s in clefs_per_staff.keys(): -# clefs = clefs_per_staff[s] -# break - -# if clefs != None: -# clef = clefs[0] -# if len(clefs) != 1: -# raise ValueError( -# "ERROR at staff_def creation: Staff " -# + str(clef.number) -# + " starts with more than 1 clef at t=0" -# ) -# create_staff_def(staff_grp, clef) -# else: -# create_staff_def( -# staff_grp, -# score.Clef(sign="G", line=2, number=s, octave_change=0), -# ) - -# section = add_child(mei_score, "section") - -# measures_are_aligned = True -# if padding_required: -# cursors = [0] * len(measures) -# tempii = [None] * len(measures) - -# while measures_are_aligned: -# compare_measures = {} -# for i, m in enumerate(measures): -# if cursors[i] < len(m): -# compare_measures[i] = m[cursors[i]] -# cursors[i] += 1 - -# if len(compare_measures) == 0: -# break - -# compm_keys = list(compare_measures.keys()) - -# new_tempii = first_instance_per_part( -# score.Tempo, -# [p for i, p in enumerate(parts) if i in compm_keys], -# start=[cm.start for cm in compare_measures.values()], -# end=[cm.end for cm in compare_measures.values()], -# ) - -# if len(new_tempii) == 0: -# for k in compm_keys: -# new_tempii.append(tempii[k]) -# else: -# for i, nt in enumerate(new_tempii): -# if nt == None: -# new_tempii[i] = tempii[compm_keys[i]] -# else: -# tempii[compm_keys[i]] = nt - -# def norm_dur(m): -# return (m.end.t - m.start.t) // m.start.quarter - -# rep_i = 0 -# while rep_i < len(new_tempii) and new_tempii[rep_i] == None: -# rep_i += 1 - -# if rep_i == len(new_tempii): -# continue - -# rep_dur = ( -# norm_dur(compare_measures[compm_keys[rep_i]]) * new_tempii[rep_i].bpm -# ) - -# for i in range(rep_i + 1, len(compm_keys)): -# nt = new_tempii[i] - -# if nt == None: -# continue - -# m = compare_measures[compm_keys[i]] -# dur = norm_dur(m) * new_tempii[i].bpm - -# if dur != rep_dur: -# measures_are_aligned = False -# break - -# tuplet_id_counter = 0 - -# if measures_are_aligned: -# time_offset = [0] * len(measures) - -# if padding_required: -# for i, mp in enumerate(measures): -# ii = len(mp) -# time_offset[i] = mp[ii - 1].end.t -# while ii < max_length: -# mp.append("pad") -# ii += 1 - -# notes_last_measure_per_staff = {} -# auto_rest_count = 0 - -# notes_within_measure_per_staff = notes_last_measure_per_staff - -# auto_rest_count, current_measure_content = extract_from_measures( -# parts, -# measures, -# 0, -# staves_per_part, -# auto_rest_count, -# notes_within_measure_per_staff, -# ) - -# last_key_sig_per_staff = {} - -# for s, k in current_measure_content.key_sigs_per_staff.items(): -# last_key_sig_per_staff[s] = ( -# min(k, key=lambda k: k.start.t) if len(k) > 0 else None -# ) - -# tuplet_id_counter, notes_last_measure_per_staff = create_measure( -# section, -# 0, -# staves_sorted, -# notes_within_measure_per_staff, -# score_def, -# tuplet_id_counter, -# auto_beaming, -# last_key_sig_per_staff, -# current_measure_content, -# ) - -# for measure_i in range(1, len(measures[0])): -# notes_within_measure_per_staff = notes_last_measure_per_staff - -# auto_rest_count, current_measure_content = extract_from_measures( -# parts, -# measures, -# measure_i, -# staves_per_part, -# auto_rest_count, -# notes_within_measure_per_staff, -# ) - -# score_def = create_score_def(measures, measure_i, parts, section) - -# tuplet_id_counter, notes_last_measure_per_staff = create_measure( -# section, -# measure_i, -# staves_sorted, -# notes_within_measure_per_staff, -# score_def, -# tuplet_id_counter, -# auto_beaming, -# last_key_sig_per_staff, -# current_measure_content, -# ) - -# (etree.ElementTree(mei)).write(file_name + ".mei", pretty_print=True) - -# # post processing step necessary -# # etree won't write <,> and & into an element's text -# with open(file_name + ".mei") as result: -# text = list(result.read()) -# new_text = [] - -# i = 0 -# while i < len(text): -# ch = text[i] -# if ch == "&": -# if text[i + 1 : i + 4] == ["l", "t", ";"]: -# ch = "<" -# i += 4 -# elif text[i + 1 : i + 4] == ["g", "t", ";"]: -# ch = ">" -# i += 4 -# elif text[i + 1 : i + 5] == ["a", "m", "p", ";"]: -# i += 5 -# else: -# i += 1 -# else: -# i += 1 - -# new_text.append(ch) - -# new_text = "".join(new_text) - -# with open(file_name + ".mei", "w") as result: -# result.write(new_text) +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains methods for exporting MEI files. +""" +import math +from collections import defaultdict +from lxml import etree +import partitura.score as spt +from operator import itemgetter +from itertools import groupby +from typing import Optional +from partitura.utils import partition, iter_current_next, to_quarter_tempo, fifths_mode_to_key_name +import numpy as np +from partitura.utils.misc import deprecated_alias, PathLike +from partitura.utils.music import MEI_DURS_TO_SYMBOLIC + + +__all__ = ["save_mei"] + +XMLNS_ID = "{http://www.w3.org/XML/1998/namespace}id" + +ALTER_TO_MEI = { + -2: "ff", + -1: "f", + 0: "n", + 1: "s", + 2: "ss", +} + +SYMBOLIC_TYPES_TO_MEI_DURS = {v: k for k, v in MEI_DURS_TO_SYMBOLIC.items()} + +DOCTYPE = '\n' + + +class MEIExporter: + def __init__(self, part): + self.part = part + self.element_counter = 0 + + def elc_id(self): + # transforms an integer number to 8-digit string + # The number is right aligned and padded with zeros + self.element_counter += 1 + out = str(self.element_counter).zfill(10) + return out + + def export_to_mei(self): + # Create root MEI element + etree.register_namespace("xml", "http://www.w3.org/XML/1998/namespace") + etree.register_namespace( "mei", "http://www.music-encoding.org/ns/mei") + mei = etree.Element('mei', nsmap={'xml': "http://www.w3.org/XML/1998/namespace", + None: "http://www.music-encoding.org/ns/mei"}) + # mei.set('xmlns', "http://www.music-encoding.org/ns/mei") + mei.set('meiversion', "4.0.1") + # Create child elements + mei_head = etree.SubElement(mei, 'meiHead') + file_desc = etree.SubElement(mei_head, 'fileDesc') + music = etree.SubElement(mei, 'music') + body = etree.SubElement(music, 'body') + mdiv = etree.SubElement(body, 'mdiv') + score = etree.SubElement(mdiv, 'score') + score.set(XMLNS_ID, "score-" + self.elc_id()) + score_def = etree.SubElement(score, 'scoreDef') + score_def.set(XMLNS_ID, "scoredef-" + self.elc_id()) + staff_grp = etree.SubElement(score_def, 'staffGrp') + staff_grp.set(XMLNS_ID, "staffgrp-" + self.elc_id()) + self._handle_staffs(staff_grp) + + section = etree.SubElement(score, 'section') + section.set(XMLNS_ID, "section-" + self.elc_id()) + + # Iterate over part's timeline + for measure in self.part.measures: + # Create measure element + xml_el = etree.SubElement(section, 'measure') + self._handle_measure(measure, xml_el) + + return mei + + def _handle_staffs(self, xml_el): + clefs = self.part.iter_all(spt.Clef, start=0, end=1) + clefs = {c.staff: c for c in clefs} + key_sigs = list(self.part.iter_all(spt.KeySignature, start=0, end=1)) + keys_sig = key_sigs[0] if len(key_sigs) > 0 else None + time_sigs = list(self.part.iter_all(spt.TimeSignature, start=0, end=1)) + time_sig = time_sigs[0] if len(time_sigs) > 0 else None + for staff_num in range(self.part.number_of_staves): + staff_num += 1 + staff_def = etree.SubElement(xml_el, 'staffDef') + staff_def.set('n', str(staff_num)) + staff_def.set(XMLNS_ID, "staffdef-" + self.elc_id()) + staff_def.set('lines', '5') + # Get clef for this staff If no cleff is available for this staff, default to "G2" + clef_def = etree.SubElement(staff_def, 'clef') + clef_def.set(XMLNS_ID, "clef-" + self.elc_id()) + clef_shape = clefs[staff_num].sign if staff_num in clefs.keys() else "G" + clef_def.set('shape', str(clef_shape)) + clef_def.set('line', str(clefs[staff_num].line)) if staff_num in clefs.keys() else clef_def.set('line', '2') + # Get key signature for this staff + if keys_sig is not None: + ks_def = etree.SubElement(staff_def, 'keySig') + ks_def.set(XMLNS_ID, "keysig-" + self.elc_id()) + ks_def.set('mode', keys_sig.mode) if keys_sig.mode is not None else ks_def.set('mode', 'major') + if keys_sig.fifths == 0: + ks_def.set('sig', '0') + elif keys_sig.fifths > 0: + ks_def.set('sig', str(keys_sig.fifths) + 's') + else: + ks_def.set('sig', str(abs(keys_sig.fifths)) + 'f') + # Find the pname from the number of sharps or flats and the mode + ks_def.set('pname', fifths_mode_to_key_name(keys_sig.fifths, keys_sig.mode).lower()) + + if time_sig is not None: + ts_def = etree.SubElement(staff_def, 'meterSig') + ts_def.set(XMLNS_ID, "msig-" + self.elc_id()) + ts_def.set('count', str(time_sig.beats)) + ts_def.set('unit', str(time_sig.beat_type)) + + def _handle_measure(self, measure, measure_el): + # Add measure number + measure_el.set('n', str(measure.number)) + measure_el.set(XMLNS_ID, "measure-" + self.elc_id()) + note_or_rest_elements = np.array(list(self.part.iter_all(spt.GenericNote, start=measure.start.t, end=measure.end.t, include_subclasses=True))) + # Separate by staff + staffs = np.vectorize(lambda x: x.staff)(note_or_rest_elements) + unique_staffs, staff_inverse_map = np.unique(staffs, return_inverse=True) + for i, staff in enumerate(unique_staffs): + staff_el = etree.SubElement(measure_el, 'staff') + # Add staff number + staff_el.set('n', str(staff)) + staff_el.set(XMLNS_ID, "staff-" + self.elc_id()) + staff_notes = note_or_rest_elements[staff_inverse_map == i] + # Separate by voice + voices = np.vectorize(lambda x: x.voice)(staff_notes) + unique_voices, voice_inverse_map = np.unique(voices, return_inverse=True) + for j, voice in enumerate(unique_voices): + voice_el = etree.SubElement(staff_el, 'layer') + voice_el.set('n', str(voice)) + voice_el.set(XMLNS_ID, "voice-" + self.elc_id()) + voice_notes = staff_notes[voice_inverse_map == j] + # Sort by onset + note_start_times = np.vectorize(lambda x: x.start.t)(voice_notes) + unique_onsets = np.unique(note_start_times) + for onset in unique_onsets: + # group by start time + notes = voice_notes[note_start_times == onset] + if len(notes) > 1: + self._handle_chord(notes, voice_el) + else: + self._handle_note_or_rest(notes[0], voice_el) + + self._handle_tuplets(measure_el, start=measure.start.t, end=measure.end.t) + self._handle_beams(measure_el, start=measure.start.t, end=measure.end.t) + self._handle_clef_changes(measure_el, start=measure.start.t, end=measure.end.t) + self._handle_ks_changes(measure_el, start=measure.start.t, end=measure.end.t) + self._handle_ts_changes(measure_el, start=measure.start.t, end=measure.end.t) + self._handle_harmony(measure_el, start=measure.start.t, end=measure.end.t) + return measure_el + + def _handle_chord(self, chord, xml_voice_el): + chord_el = etree.SubElement(xml_voice_el, 'chord') + chord_el.set(XMLNS_ID, "chord-" + self.elc_id()) + for note in chord: + duration = self._handle_note_or_rest(note, chord_el) + chord_el.set('dur', duration) + + def _handle_note_or_rest(self, note, xml_voice_el): + if isinstance(note, spt.Rest): + duration = self._handle_rest(note, xml_voice_el) + else: + duration = self._handle_note(note, xml_voice_el) + return duration + + def _handle_rest(self, rest, xml_voice_el): + rest_el = etree.SubElement(xml_voice_el, 'rest') + duration = SYMBOLIC_TYPES_TO_MEI_DURS[rest.symbolic_duration["type"]] + rest_el.set('dur', duration) + rest_el.set(XMLNS_ID, "rest-" + self.elc_id()) + return duration + + def _handle_note(self, note, xml_voice_el): + note_el = etree.SubElement(xml_voice_el, 'note') + duration = SYMBOLIC_TYPES_TO_MEI_DURS[note.symbolic_duration["type"]] + note_el.set('dur', duration) + note_el.set(XMLNS_ID, "note-" + self.elc_id()) if note.id is None else note_el.set(XMLNS_ID, note.id) + note_el.set('oct', str(note.octave)) + note_el.set('pname', note.step.lower()) + if note.tie_next is not None and note.tie_prev is not None: + note_el.set('tie', 'm') + elif note.tie_next is not None: + note_el.set('tie', 'i') + elif note.tie_prev is not None: + note_el.set('tie', 't') + + if note.alter is not None: + accidental = etree.SubElement(note_el, 'accid') + accidental.set(XMLNS_ID, "accid-" + self.elc_id()) + accidental.set('accid', ALTER_TO_MEI[note.alter]) + + if isinstance(note, spt.GraceNote): + note_el.set('grace', 'acc') + return duration + + def _handle_tuplets(self, measure_el, start, end): + for tuplet in self.part.iter_all(spt.Tuplet, start=start, end=end): + start_note = tuplet.start_note + end_note = tuplet.end_note + # Find the note element corresponding to the start note i.e. has the same id value + start_note_el = measure_el.xpath(f".//*[@xml:id='{start_note.id}']")[0] + # Find the note element corresponding to the end note i.e. has the same id value + end_note_el = measure_el.xpath(f".//*[@xml:id='{end_note.id}']")[0] + # Create the tuplet element as parent of the start and end note elements + # Make it start at the same index as the start note element + tuplet_el = etree.Element('tuplet') + layer_el = start_note_el.getparent() + layer_el.insert(layer_el.index(start_note_el), tuplet_el) + tuplet_el.set(XMLNS_ID, "tuplet-" + self.elc_id()) + tuplet_el.set('num', str(start_note.symbolic_duration["actual_notes"])) + tuplet_el.set('numbase', str(start_note.symbolic_duration["normal_notes"])) + # Add all elements between the start and end note elements to the tuplet element as childen + # Find them from the xml tree + start_note_index = start_note_el.getparent().index(start_note_el) + end_note_index = end_note_el.getparent().index(end_note_el) + xml_el_within_tuplet = [start_note_el.getparent()[i] for i in range(start_note_index, end_note_index + 1)] + for el in xml_el_within_tuplet: + tuplet_el.append(el) + + def _handle_beams(self, measure_el, start, end): + for beam in self.part.iter_all(spt.Beam, start=start, end=end): + start_note = beam.notes[np.argmin([n.start.t for n in beam.notes])] + # Beam element is parent of the note element + note_el = measure_el.xpath(f".//*[@xml:id='{start_note.id}']")[0] + layer_el = note_el.getparent() + insert_index = layer_el.index(note_el) + # If the parent is a tuplet, the beam element should be added as parent of the tuplet element + if layer_el.tag == 'tuplet': + parent_el = layer_el.getparent() + insert_index = parent_el.index(layer_el) + layer_el = parent_el + # Create the beam element + beam_el = etree.Element('beam') + layer_el.insert(insert_index, beam_el) + beam_el.set(XMLNS_ID, "beam-" + self.elc_id()) + for note in beam.notes: + # Find the note element corresponding to the start note i.e. has the same id value + note_el = measure_el.xpath(f".//*[@xml:id='{note.id}']") + if len(note_el) > 0: + note_el = note_el[0] + beam_el.append(note_el) + + def _handle_clef_changes(self, measure_el, start, end): + 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: + continue + # Find the note element corresponding to the start note i.e. has the same id value + for note in self.part.iter_all(spt.GenericNote, start=clef.start.t, end=clef.start.t): + note_el = measure_el.xpath(f".//*[@xml:id='{note.id}']") + if len(note_el) > 0: + note_el = note_el[0] + layer_el = note_el.getparent() + insert_index = layer_el.index(note_el) + # Create the clef element + clef_el = etree.Element('clef') + layer_el.insert(insert_index, clef_el) + clef_el.set(XMLNS_ID, "clef-" + self.elc_id()) + clef_el.set('shape', str(clef.sign)) + clef_el.set('line', str(clef.line)) + + def _handle_ks_changes(self, measure_el, start, end): + # 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): + if key_sig.start.t == 0: + continue + # Create the scoreDef element + score_def_el = etree.Element('scoreDef') + score_def_el.set(XMLNS_ID, "scoredef-" + self.elc_id()) + score_def_el.set('mode', key_sig.mode) if key_sig.mode is not None else score_def_el.set('mode', 'major') + if key_sig.fifths == 0: + score_def_el.set('sig', '0') + elif key_sig.fifths > 0: + score_def_el.set('sig', str(key_sig.fifths) + 's') + else: + score_def_el.set('sig', str(abs(key_sig.fifths)) + 'f') + # Find the pname from the number of sharps or flats and the mode + score_def_el.set('pname', fifths_mode_to_key_name(key_sig.fifths, key_sig.mode).lower()) + # Add the scoreDef element at before the measure element starts + parent = measure_el.getparent() + parent.insert(parent.index(measure_el), score_def_el) + + def _handle_ts_changes(self, measure_el, start, end): + # 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): + if time_sig.start.t == 0: + continue + # Create the scoreDef element + score_def_el = etree.Element('scoreDef') + score_def_el.set(XMLNS_ID, "scoredef-" + self.elc_id()) + + # Add the scoreDef element at before the measure element starts + parent = measure_el.getparent() + parent.insert(parent.index(measure_el), score_def_el) + 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): + # 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 harmony in self.part.iter_all(spt.RomanNumeral, start=start, end=end): + harm_el = etree.SubElement(measure_el, 'harm') + harm_el.set(XMLNS_ID, "harm-" + self.elc_id()) + harm_el.set("staff", str(self.part.number_of_staves)) + harm_el.set("tstamp", str(np.diff(self.part.quarter_map([start, harmony.start.t]))[0]+1)) + harm_el.set("place", "below") + # text is a child element of harmony but not a xml element + harm_el.text = harmony.text + + +@deprecated_alias(parts="score_data") +def save_mei( + score_data: spt.ScoreLike, + out: Optional[PathLike] = None, +) -> Optional[str]: + """ + Save a one or more Part or PartGroup instances in MEI format. + + Parameters + ---------- + score_data : Score, list, Part, or PartGroup + The musical score to be saved. A :class:`partitura.score.Score` object, + a :class:`partitura.score.Part`, a :class:`partitura.score.PartGroup` or + a list of these. + out: str, file-like object, or None, optional + Output file + + Returns + ------- + None or str + If no output file is specified using `out` the function returns the + MEI data as a string. Otherwise the function returns None. + """ + + if isinstance(score_data, spt.Score): + score_data = spt.merge_parts(score_data.parts) + + exporter = MEIExporter(score_data) + root = exporter.export_to_mei() + + if out: + if hasattr(out, "write"): + out.write( + etree.tostring( + root.getroottree(), + encoding="UTF-8", + xml_declaration=True, + pretty_print=True, + doctype=DOCTYPE, + ) + ) + + else: + with open(out, "wb") as f: + f.write( + etree.tostring( + root.getroottree(), + encoding="UTF-8", + xml_declaration=True, + pretty_print=True, + doctype=DOCTYPE, + ) + ) + + else: + return etree.tostring( + root.getroottree(), + encoding="UTF-8", + xml_declaration=True, + pretty_print=True, + doctype=DOCTYPE, + ) diff --git a/partitura/io/mei_export_v2.py b/partitura/io/mei_export_v2.py deleted file mode 100644 index ac5ccfe0..00000000 --- a/partitura/io/mei_export_v2.py +++ /dev/null @@ -1,383 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -This module contains methods for exporting MEI files. -""" -import math -from collections import defaultdict -from lxml import etree -import partitura.score as spt -from operator import itemgetter -from itertools import groupby -from typing import Optional -from partitura.utils import partition, iter_current_next, to_quarter_tempo, fifths_mode_to_key_name -import numpy as np -from partitura.utils.misc import deprecated_alias, PathLike -from partitura.utils.music import MEI_DURS_TO_SYMBOLIC - - -__all__ = ["save_mei"] - -XMLNS_ID = "{http://www.w3.org/XML/1998/namespace}id" - -ALTER_TO_MEI = { - -2: "ff", - -1: "f", - 0: "n", - 1: "s", - 2: "ss", -} - -SYMBOLIC_TYPES_TO_MEI_DURS = {v: k for k, v in MEI_DURS_TO_SYMBOLIC.items()} - -DOCTYPE = '\n' - - -class MEIExporter: - def __init__(self, part): - self.part = part - self.element_counter = 0 - - def elc_id(self): - # transforms an integer number to 8-digit string - # The number is right aligned and padded with zeros - self.element_counter += 1 - out = str(self.element_counter).zfill(10) - return out - - def export_to_mei(self): - # Create root MEI element - etree.register_namespace("xml", "http://www.w3.org/XML/1998/namespace") - etree.register_namespace( "mei", "http://www.music-encoding.org/ns/mei") - mei = etree.Element('mei', nsmap={'xml': "http://www.w3.org/XML/1998/namespace", - None: "http://www.music-encoding.org/ns/mei"}) - # mei.set('xmlns', "http://www.music-encoding.org/ns/mei") - mei.set('meiversion', "4.0.1") - # Create child elements - mei_head = etree.SubElement(mei, 'meiHead') - file_desc = etree.SubElement(mei_head, 'fileDesc') - music = etree.SubElement(mei, 'music') - body = etree.SubElement(music, 'body') - mdiv = etree.SubElement(body, 'mdiv') - score = etree.SubElement(mdiv, 'score') - score.set(XMLNS_ID, "score-" + self.elc_id()) - score_def = etree.SubElement(score, 'scoreDef') - score_def.set(XMLNS_ID, "scoredef-" + self.elc_id()) - staff_grp = etree.SubElement(score_def, 'staffGrp') - staff_grp.set(XMLNS_ID, "staffgrp-" + self.elc_id()) - self._handle_staffs(staff_grp) - - section = etree.SubElement(score, 'section') - section.set(XMLNS_ID, "section-" + self.elc_id()) - - # Iterate over part's timeline - for measure in self.part.measures: - # Create measure element - xml_el = etree.SubElement(section, 'measure') - self._handle_measure(measure, xml_el) - - return mei - - def _handle_staffs(self, xml_el): - clefs = self.part.iter_all(spt.Clef, start=0, end=1) - clefs = {c.staff: c for c in clefs} - key_sigs = list(self.part.iter_all(spt.KeySignature, start=0, end=1)) - keys_sig = key_sigs[0] if len(key_sigs) > 0 else None - time_sigs = list(self.part.iter_all(spt.TimeSignature, start=0, end=1)) - time_sig = time_sigs[0] if len(time_sigs) > 0 else None - for staff_num in range(self.part.number_of_staves): - staff_num += 1 - staff_def = etree.SubElement(xml_el, 'staffDef') - staff_def.set('n', str(staff_num)) - staff_def.set(XMLNS_ID, "staffdef-" + self.elc_id()) - staff_def.set('lines', '5') - # Get clef for this staff If no cleff is available for this staff, default to "G2" - clef_def = etree.SubElement(staff_def, 'clef') - clef_def.set(XMLNS_ID, "clef-" + self.elc_id()) - clef_shape = clefs[staff_num].sign if staff_num in clefs.keys() else "G" - clef_def.set('shape', str(clef_shape)) - clef_def.set('line', str(clefs[staff_num].line)) if staff_num in clefs.keys() else clef_def.set('line', '2') - # Get key signature for this staff - if keys_sig is not None: - ks_def = etree.SubElement(staff_def, 'keySig') - ks_def.set(XMLNS_ID, "keysig-" + self.elc_id()) - ks_def.set('mode', keys_sig.mode) if keys_sig.mode is not None else ks_def.set('mode', 'major') - if keys_sig.fifths == 0: - ks_def.set('sig', '0') - elif keys_sig.fifths > 0: - ks_def.set('sig', str(keys_sig.fifths) + 's') - else: - ks_def.set('sig', str(abs(keys_sig.fifths)) + 'f') - # Find the pname from the number of sharps or flats and the mode - ks_def.set('pname', fifths_mode_to_key_name(keys_sig.fifths, keys_sig.mode).lower()) - - if time_sig is not None: - ts_def = etree.SubElement(staff_def, 'meterSig') - ts_def.set(XMLNS_ID, "msig-" + self.elc_id()) - ts_def.set('count', str(time_sig.beats)) - ts_def.set('unit', str(time_sig.beat_type)) - - def _handle_measure(self, measure, measure_el): - # Add measure number - measure_el.set('n', str(measure.number)) - measure_el.set(XMLNS_ID, "measure-" + self.elc_id()) - note_or_rest_elements = np.array(list(self.part.iter_all(spt.GenericNote, start=measure.start.t, end=measure.end.t, include_subclasses=True))) - # Separate by staff - staffs = np.vectorize(lambda x: x.staff)(note_or_rest_elements) - unique_staffs, staff_inverse_map = np.unique(staffs, return_inverse=True) - for i, staff in enumerate(unique_staffs): - staff_el = etree.SubElement(measure_el, 'staff') - # Add staff number - staff_el.set('n', str(staff)) - staff_el.set(XMLNS_ID, "staff-" + self.elc_id()) - staff_notes = note_or_rest_elements[staff_inverse_map == i] - # Separate by voice - voices = np.vectorize(lambda x: x.voice)(staff_notes) - unique_voices, voice_inverse_map = np.unique(voices, return_inverse=True) - for j, voice in enumerate(unique_voices): - voice_el = etree.SubElement(staff_el, 'layer') - voice_el.set('n', str(voice)) - voice_el.set(XMLNS_ID, "voice-" + self.elc_id()) - voice_notes = staff_notes[voice_inverse_map == j] - # Sort by onset - note_start_times = np.vectorize(lambda x: x.start.t)(voice_notes) - unique_onsets = np.unique(note_start_times) - for onset in unique_onsets: - # group by start time - notes = voice_notes[note_start_times == onset] - if len(notes) > 1: - self._handle_chord(notes, voice_el) - else: - self._handle_note_or_rest(notes[0], voice_el) - - self._handle_tuplets(measure_el, start=measure.start.t, end=measure.end.t) - self._handle_beams(measure_el, start=measure.start.t, end=measure.end.t) - self._handle_clef_changes(measure_el, start=measure.start.t, end=measure.end.t) - self._handle_ks_changes(measure_el, start=measure.start.t, end=measure.end.t) - self._handle_ts_changes(measure_el, start=measure.start.t, end=measure.end.t) - self._handle_harmony(measure_el, start=measure.start.t, end=measure.end.t) - return measure_el - - def _handle_chord(self, chord, xml_voice_el): - chord_el = etree.SubElement(xml_voice_el, 'chord') - chord_el.set(XMLNS_ID, "chord-" + self.elc_id()) - for note in chord: - duration = self._handle_note_or_rest(note, chord_el) - chord_el.set('dur', duration) - - def _handle_note_or_rest(self, note, xml_voice_el): - if isinstance(note, spt.Rest): - duration = self._handle_rest(note, xml_voice_el) - else: - duration = self._handle_note(note, xml_voice_el) - return duration - - def _handle_rest(self, rest, xml_voice_el): - rest_el = etree.SubElement(xml_voice_el, 'rest') - duration = SYMBOLIC_TYPES_TO_MEI_DURS[rest.symbolic_duration["type"]] - rest_el.set('dur', duration) - rest_el.set(XMLNS_ID, "rest-" + self.elc_id()) - return duration - - def _handle_note(self, note, xml_voice_el): - note_el = etree.SubElement(xml_voice_el, 'note') - duration = SYMBOLIC_TYPES_TO_MEI_DURS[note.symbolic_duration["type"]] - note_el.set('dur', duration) - note_el.set(XMLNS_ID, "note-" + self.elc_id()) if note.id is None else note_el.set(XMLNS_ID, note.id) - note_el.set('oct', str(note.octave)) - note_el.set('pname', note.step.lower()) - if note.tie_next is not None and note.tie_prev is not None: - note_el.set('tie', 'm') - elif note.tie_next is not None: - note_el.set('tie', 'i') - elif note.tie_prev is not None: - note_el.set('tie', 't') - - if note.alter is not None: - accidental = etree.SubElement(note_el, 'accid') - accidental.set(XMLNS_ID, "accid-" + self.elc_id()) - accidental.set('accid', ALTER_TO_MEI[note.alter]) - - if isinstance(note, spt.GraceNote): - note_el.set('grace', 'acc') - return duration - - def _handle_tuplets(self, measure_el, start, end): - for tuplet in self.part.iter_all(spt.Tuplet, start=start, end=end): - start_note = tuplet.start_note - end_note = tuplet.end_note - # Find the note element corresponding to the start note i.e. has the same id value - start_note_el = measure_el.xpath(f".//*[@xml:id='{start_note.id}']")[0] - # Find the note element corresponding to the end note i.e. has the same id value - end_note_el = measure_el.xpath(f".//*[@xml:id='{end_note.id}']")[0] - # Create the tuplet element as parent of the start and end note elements - # Make it start at the same index as the start note element - tuplet_el = etree.Element('tuplet') - layer_el = start_note_el.getparent() - layer_el.insert(layer_el.index(start_note_el), tuplet_el) - tuplet_el.set(XMLNS_ID, "tuplet-" + self.elc_id()) - tuplet_el.set('num', str(start_note.symbolic_duration["actual_notes"])) - tuplet_el.set('numbase', str(start_note.symbolic_duration["normal_notes"])) - # Add all elements between the start and end note elements to the tuplet element as childen - # Find them from the xml tree - start_note_index = start_note_el.getparent().index(start_note_el) - end_note_index = end_note_el.getparent().index(end_note_el) - xml_el_within_tuplet = [start_note_el.getparent()[i] for i in range(start_note_index, end_note_index + 1)] - for el in xml_el_within_tuplet: - tuplet_el.append(el) - - def _handle_beams(self, measure_el, start, end): - for beam in self.part.iter_all(spt.Beam, start=start, end=end): - start_note = beam.notes[np.argmin([n.start.t for n in beam.notes])] - # Beam element is parent of the note element - note_el = measure_el.xpath(f".//*[@xml:id='{start_note.id}']")[0] - layer_el = note_el.getparent() - insert_index = layer_el.index(note_el) - # If the parent is a tuplet, the beam element should be added as parent of the tuplet element - if layer_el.tag == 'tuplet': - parent_el = layer_el.getparent() - insert_index = parent_el.index(layer_el) - layer_el = parent_el - # Create the beam element - beam_el = etree.Element('beam') - layer_el.insert(insert_index, beam_el) - beam_el.set(XMLNS_ID, "beam-" + self.elc_id()) - for note in beam.notes: - # Find the note element corresponding to the start note i.e. has the same id value - note_el = measure_el.xpath(f".//*[@xml:id='{note.id}']") - if len(note_el) > 0: - note_el = note_el[0] - beam_el.append(note_el) - - def _handle_clef_changes(self, measure_el, start, end): - 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: - continue - # Find the note element corresponding to the start note i.e. has the same id value - for note in self.part.iter_all(spt.GenericNote, start=clef.start.t, end=clef.start.t): - note_el = measure_el.xpath(f".//*[@xml:id='{note.id}']") - if len(note_el) > 0: - note_el = note_el[0] - layer_el = note_el.getparent() - insert_index = layer_el.index(note_el) - # Create the clef element - clef_el = etree.Element('clef') - layer_el.insert(insert_index, clef_el) - clef_el.set(XMLNS_ID, "clef-" + self.elc_id()) - clef_el.set('shape', str(clef.sign)) - clef_el.set('line', str(clef.line)) - - def _handle_ks_changes(self, measure_el, start, end): - # 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): - if key_sig.start.t == 0: - continue - # Create the scoreDef element - score_def_el = etree.Element('scoreDef') - score_def_el.set(XMLNS_ID, "scoredef-" + self.elc_id()) - score_def_el.set('mode', key_sig.mode) if key_sig.mode is not None else score_def_el.set('mode', 'major') - if key_sig.fifths == 0: - score_def_el.set('sig', '0') - elif key_sig.fifths > 0: - score_def_el.set('sig', str(key_sig.fifths) + 's') - else: - score_def_el.set('sig', str(abs(key_sig.fifths)) + 'f') - # Find the pname from the number of sharps or flats and the mode - score_def_el.set('pname', fifths_mode_to_key_name(key_sig.fifths, key_sig.mode).lower()) - # Add the scoreDef element at before the measure element starts - parent = measure_el.getparent() - parent.insert(parent.index(measure_el), score_def_el) - - def _handle_ts_changes(self, measure_el, start, end): - # 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): - if time_sig.start.t == 0: - continue - # Create the scoreDef element - score_def_el = etree.Element('scoreDef') - score_def_el.set(XMLNS_ID, "scoredef-" + self.elc_id()) - - # Add the scoreDef element at before the measure element starts - parent = measure_el.getparent() - parent.insert(parent.index(measure_el), score_def_el) - 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): - # 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 harmony in self.part.iter_all(spt.RomanNumeral, start=start, end=end): - harm_el = etree.SubElement(measure_el, 'harm') - harm_el.set(XMLNS_ID, "harm-" + self.elc_id()) - harm_el.set("staff", str(self.part.number_of_staves)) - harm_el.set("tstamp", str(np.diff(self.part.quarter_map([start, harmony.start.t]))[0]+1)) - harm_el.set("place", "below") - # text is a child element of harmony but not a xml element - harm_el.text = harmony.text - - -@deprecated_alias(parts="score_data") -def save_mei( - score_data: spt.ScoreLike, - out: Optional[PathLike] = None, -) -> Optional[str]: - """ - Save a one or more Part or PartGroup instances in MEI format. - - Parameters - ---------- - score_data : Score, list, Part, or PartGroup - The musical score to be saved. A :class:`partitura.score.Score` object, - a :class:`partitura.score.Part`, a :class:`partitura.score.PartGroup` or - a list of these. - out: str, file-like object, or None, optional - Output file - - Returns - ------- - None or str - If no output file is specified using `out` the function returns the - MEI data as a string. Otherwise the function returns None. - """ - - if isinstance(score_data, spt.Score): - score_data = spt.merge_parts(score_data.parts) - - exporter = MEIExporter(score_data) - root = exporter.export_to_mei() - - if out: - if hasattr(out, "write"): - out.write( - etree.tostring( - root.getroottree(), - encoding="UTF-8", - xml_declaration=True, - pretty_print=True, - doctype=DOCTYPE, - ) - ) - - else: - with open(out, "wb") as f: - f.write( - etree.tostring( - root.getroottree(), - encoding="UTF-8", - xml_declaration=True, - pretty_print=True, - doctype=DOCTYPE, - ) - ) - - else: - return etree.tostring( - root.getroottree(), - encoding="UTF-8", - xml_declaration=True, - pretty_print=True, - doctype=DOCTYPE, - ) From 273fd325fcbc97a9a6fbbd3b74792e5efb3f6d42 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 12 Feb 2024 18:40:00 +0100 Subject: [PATCH 086/197] Fixing harmony doc string. --- partitura/io/exportmei.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index ac5ccfe0..7463281d 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -307,8 +307,11 @@ def _handle_ts_changes(self, measure_el, start, end): score_def_el.set('unit', str(time_sig.beat_type)) def _handle_harmony(self, measure_el, start, end): - # 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 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 + The harmonies will be displayed below the lowest staff. + """ for harmony in self.part.iter_all(spt.RomanNumeral, start=start, end=end): harm_el = etree.SubElement(measure_el, 'harm') harm_el.set(XMLNS_ID, "harm-" + self.elc_id()) From 957df357accb8f1246e273afb8dfd2de592db4ef Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 12 Feb 2024 18:44:05 +0100 Subject: [PATCH 087/197] Restricting support to scores with a single part. --- partitura/io/exportmei.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index 7463281d..b1ef6725 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -347,7 +347,16 @@ def save_mei( """ if isinstance(score_data, spt.Score): - score_data = spt.merge_parts(score_data.parts) + parts = score_data.parts + elif isinstance(score_data, list): + parts = score_data + else: + parts = [score_data] + + if len(parts) > 1: + raise ValueError("Partitura supports only one part or PartGroup per MEI file.") + + score_data = parts[0] exporter = MEIExporter(score_data) root = exporter.export_to_mei() From e108dc9cbdb41470ab4288ab6e9546b79c099175 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 12 Feb 2024 18:53:36 +0100 Subject: [PATCH 088/197] Update with more complex mei export test case. NOTE: Handling ids in MEI import is weird and non-consistent. --- tests/test_mei.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/test_mei.py b/tests/test_mei.py index d360b0c2..e5f26b4c 100644 --- a/tests/test_mei.py +++ b/tests/test_mei.py @@ -31,7 +31,7 @@ # self.assertTrue(mei.decode('utf-8') == target_mei, msg) class TestExportMEI(unittest.TestCase): - def test_export_mei(self): + def test_export_mei_simple(self): import_score = load_mei(EXAMPLE_MEI) ina = import_score.note_array() with TemporaryDirectory() as tmpdir: @@ -45,6 +45,18 @@ def test_export_mei(self): self.assertTrue(np.all(ina["voice"] == ena["voice"])) self.assertTrue(np.all(ina["id"] == ena["id"])) + def test_export_mei(self): + import_score = load_musicxml(os.path.join(MUSICXML_PATH, "test_chew_vosa_example.xml"), force_note_ids=True) + ina = import_score.note_array() + with TemporaryDirectory() as tmpdir: + tmp_mei = os.path.join(tmpdir, "test.mei") + save_mei(import_score, tmp_mei) + export_score = load_mei(tmp_mei) + ena = export_score.note_array() + self.assertTrue(np.all(ina["onset_beat"] == ena["onset_beat"])) + self.assertTrue(np.all(ina["duration_beat"] == ena["duration_beat"])) + self.assertTrue(np.all(ina["pitch"] == ena["pitch"])) + def test_export_with_harmony(self): score_fn = os.path.join(MUSICXML_PATH, "test_harmony.musicxml") import_score = load_musicxml(score_fn) From 600504b384d9569547a58123809dd7e62c7ce05b Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 13 Feb 2024 15:36:32 +0100 Subject: [PATCH 089/197] Moved globals to new file. --- partitura/directions.py | 23 +- partitura/musicanalysis/key_identification.py | 70 +-- partitura/musicanalysis/meter.py | 18 +- partitura/utils/globals.py | 433 ++++++++++++++++++ partitura/utils/music.py | 282 +----------- partitura/utils/normalize.py | 3 +- partitura/utils/synth.py | 38 +- 7 files changed, 447 insertions(+), 420 deletions(-) create mode 100644 partitura/utils/globals.py diff --git a/partitura/directions.py b/partitura/directions.py index b52024f6..9c8081d4 100644 --- a/partitura/directions.py +++ b/partitura/directions.py @@ -13,6 +13,7 @@ import re import warnings +from partitura.utils.globals import UNABBREVS try: from lark import Lark @@ -51,27 +52,7 @@ def join_items(items): ) -UNABBREVS = [ - (re.compile(r"(crescendo|cresc\.?)"), "crescendo"), - (re.compile(r"(smorzando|smorz\.?)"), "smorzando"), - (re.compile(r"(decrescendo|(decresc|decr|dimin|dim)\.?)"), "diminuendo"), - (re.compile(r"((acceler|accel|acc)\.?)"), "accelerando"), - (re.compile(r"(ritenente|riten\.?)"), "ritenuto"), - (re.compile(r"((ritard|rit)\.?)"), "ritardando"), - (re.compile(r"((rallent|rall)\.?)"), "rallentando"), - (re.compile(r"(dolciss\.?)"), "dolcissimo"), - (re.compile(r"((sosten|sost)\.?)"), "sostenuto"), - (re.compile(r"(delicatiss\.?)"), "delicatissimo"), - (re.compile(r"(leggieramente|leggiermente|leggiero|legg\.?)"), "leggiero"), - (re.compile(r"(leggierissimo|(leggieriss\.?))"), "leggierissimo"), - (re.compile(r"(scherz\.?)"), "scherzando"), - (re.compile(r"(tenute|ten\.?)"), "tenuto"), - (re.compile(r"(allegretto)"), "allegro"), - (re.compile(r"(espress\.?)"), "espressivo"), - (re.compile(r"(ligato)"), "legato"), - (re.compile(r"(ligatissimo)"), "legatissimo"), - (re.compile(r"((rinforz|rinf|rfz|rf)\.?)"), "rinforzando"), -] + def unabbreviate(s): diff --git a/partitura/musicanalysis/key_identification.py b/partitura/musicanalysis/key_identification.py index 1fc5fa35..62bb1ea7 100644 --- a/partitura/musicanalysis/key_identification.py +++ b/partitura/musicanalysis/key_identification.py @@ -11,6 +11,10 @@ import numpy as np from scipy.linalg import circulant from partitura.utils.music import ensure_notearray +from partitura.utils.globals import ( + KEYS, key_prof_maj_kk, key_prof_min_kk, key_prof_maj_cbms, key_prof_min_cbms, + key_prof_maj_kp, key_prof_min_kp, VALID_KEY_PROFILES +) __all__ = ["estimate_key"] @@ -18,73 +22,7 @@ # Each tuple is (key root name, mode, fifths) # The key root name is equal to that with the smallest fifths in # the circle of fifths. -KEYS = [ - ("C", "major", 0), - ("Db", "major", -5), - ("D", "major", 2), - ("Eb", "major", -3), - ("E", "major", 4), - ("F", "major", -1), - ("F#", "major", 6), - ("G", "major", 1), - ("Ab", "major", -4), - ("A", "major", 3), - ("Bb", "major", -2), - ("B", "major", 5), - ("C", "minor", -3), - ("C#", "minor", 4), - ("D", "minor", -1), - ("D#", "minor", 6), - ("E", "minor", 1), - ("F", "minor", -4), - ("F#", "minor", 3), - ("G", "minor", -2), - ("G#", "minor", 5), - ("A", "minor", 0), - ("Bb", "minor", -5), - ("B", "minor", 2), -] - -VALID_KEY_PROFILES = [ - "krumhansl_kessler", - "kk", - "temperley", - "tp", - "kostka_payne", - "kp", -] - - -# Krumhansl--Kessler Key Profiles - -# From Krumhansl's "Cognitive Foundations of Musical Pitch" pp.30 -key_prof_maj_kk = np.array( - [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88] -) - -key_prof_min_kk = np.array( - [6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17] -) - -# Temperley Key Profiles - -# CBMS (from "Music and Probability" Table 6.1, pp. 86) -key_prof_maj_cbms = np.array( - [5.0, 2.0, 3.5, 2.0, 4.5, 4.0, 2.0, 4.5, 2.0, 3.5, 1.5, 4.0] -) - -key_prof_min_cbms = np.array( - [5.0, 2.0, 3.5, 4.5, 2.0, 4.0, 2.0, 4.5, 3.5, 2.0, 1.5, 4.0] -) -# Kostka-Payne (from "Music and Probability" Table 6.1, pp. 86) -key_prof_maj_kp = np.array( - [0.748, 0.060, 0.488, 0.082, 0.670, 0.460, 0.096, 0.715, 0.104, 0.366, 0.057, 0.400] -) - -key_prof_min_kp = np.array( - [0.712, 0.048, 0.474, 0.618, 0.049, 0.460, 0.105, 0.747, 0.404, 0.067, 0.133, 0.330] -) def build_key_profile_matrix(key_prof_maj, key_prof_min): diff --git a/partitura/musicanalysis/meter.py b/partitura/musicanalysis/meter.py index db70be27..1c337d5c 100644 --- a/partitura/musicanalysis/meter.py +++ b/partitura/musicanalysis/meter.py @@ -21,22 +21,12 @@ # from scipy.interpolate import interp1d from partitura.utils import get_time_units_from_note_array, ensure_notearray, add_field +from partitura.utils.globals import ( + CHORD_SPREAD_TIME, MIN_INTERVAL, MAX_INTERVAL, CLUSTER_WIDTH, N_CLUSTERS, + INIT_DURATION, TIMEOUT, TOLERANCE_PRE, TOLERANCE_POST, TOLERANCE_INNER, CORRECTION_FACTOR, MAX_AGENTS +) -# Scaling factors -MAX = 9999999999999 -MIN_INTERVAL = 0.01 -MAX_INTERVAL = 2 # in seconds -CLUSTER_WIDTH = 1 / 12 # in seconds -N_CLUSTERS = 100 -INIT_DURATION = 10 # in seconds -TIMEOUT = 10 # in seconds -TOLERANCE_POST = 0.4 # propotion of beat_interval -TOLERANCE_PRE = 0.2 # proportion of beat_interval -TOLERANCE_INNER = 1 / 12 -CORRECTION_FACTOR = 1 / 4 # higher => more correction (speed changes) -MAX_AGENTS = 100 # delete low-scoring agents when there are more than MAX_AGENTS -CHORD_SPREAD_TIME = 1 / 12 # for onset aggregation class MultipleAgents: diff --git a/partitura/utils/globals.py b/partitura/utils/globals.py new file mode 100644 index 00000000..3660686a --- /dev/null +++ b/partitura/utils/globals.py @@ -0,0 +1,433 @@ +import re +import numpy as np + + +MIDI_BASE_CLASS = {"c": 0, "d": 2, "e": 4, "f": 5, "g": 7, "a": 9, "b": 11} +# _MORPHETIC_BASE_CLASS = {'c': 0, 'd': 1, 'e': 2, 'f': 3, 'g': 4, 'a': 5, 'b': 6} +# _MORPHETIC_OCTAVE = {0: 32, 1: 39, 2: 46, 3: 53, 4: 60, 5: 67, 6: 74, 7: 81, 8: 89} +ALTER_SIGNS = {None: "", 0: "", 1: "#", 2: "x", -1: "b", -2: "bb"} + +DUMMY_PS_BASE_CLASS = { + 0: ("c", 0), + 1: ("c", 1), + 2: ("d", 0), + 3: ("d", 1), + 4: ("e", 0), + 5: ("f", 0), + 6: ("f", 1), + 7: ("g", 0), + 8: ("g", 1), + 9: ("a", 0), + 10: ("a", 1), + 11: ("b", 0), +} + +MEI_DURS_TO_SYMBOLIC = { + "long": "long", + "0": "breve", + "breve": "breve", + "1": "whole", + "2": "half", + "4": "quarter", + "8": "eighth", + "16": "16th", + "32": "32nd", + "64": "64th", + "128": "128th", + "256": "256th", +} + +SYMBOLIC_TO_INT_DURS = { + "long": 0.25, + "breve": 0.5, + "whole": 1, + "half": 2, + "quarter": 4, + "eighth": 8, + "16th": 16, + "32nd": 32, + "64th": 64, + "128th": 128, + "256th": 256, +} + +LABEL_DURS = { + "long": 16, + "breve": 8, + "whole": 4, + "half": 2, + "h": 2, + "quarter": 1, + "q": 1, + "eighth": 1 / 2, + "e": 1 / 2, + "16th": 1 / 4, + "32nd": 1 / 8.0, + "64th": 1 / 16, + "128th": 1 / 32, + "256th": 1 / 64, +} +DOT_MULTIPLIERS = (1, 1 + 1 / 2, 1 + 3 / 4, 1 + 7 / 8) +# DURS and SYM_DURS encode the same information as _LABEL_DURS and +# _DOT_MULTIPLIERS, but they allow for faster estimation of symbolic duration +# (estimate_symbolic duration). At some point we will probably do away with +# _LABEL_DURS and _DOT_MULTIPLIERS. +DURS = np.array( + [ + 1.5625000e-02, + 2.3437500e-02, + 2.7343750e-02, + 2.9296875e-02, + 3.1250000e-02, + 4.6875000e-02, + 5.4687500e-02, + 5.8593750e-02, + 6.2500000e-02, + 9.3750000e-02, + 1.0937500e-01, + 1.1718750e-01, + 1.2500000e-01, + 1.8750000e-01, + 2.1875000e-01, + 2.3437500e-01, + 2.5000000e-01, + 3.7500000e-01, + 4.3750000e-01, + 4.6875000e-01, + 5.0000000e-01, + 5.0000000e-01, + 7.5000000e-01, + 7.5000000e-01, + 8.7500000e-01, + 8.7500000e-01, + 9.3750000e-01, + 9.3750000e-01, + 1.0000000e00, + 1.0000000e00, + 1.5000000e00, + 1.5000000e00, + 1.7500000e00, + 1.7500000e00, + 1.8750000e00, + 1.8750000e00, + 2.0000000e00, + 2.0000000e00, + 3.0000000e00, + 3.0000000e00, + 3.5000000e00, + 3.5000000e00, + 3.7500000e00, + 3.7500000e00, + 4.0000000e00, + 6.0000000e00, + 7.0000000e00, + 7.5000000e00, + 8.0000000e00, + 1.2000000e01, + 1.4000000e01, + 1.5000000e01, + 1.6000000e01, + 2.4000000e01, + 2.8000000e01, + 3.0000000e01, + ] +) + +SYM_DURS = [ + {"type": "256th", "dots": 0}, + {"type": "256th", "dots": 1}, + {"type": "256th", "dots": 2}, + {"type": "256th", "dots": 3}, + {"type": "128th", "dots": 0}, + {"type": "128th", "dots": 1}, + {"type": "128th", "dots": 2}, + {"type": "128th", "dots": 3}, + {"type": "64th", "dots": 0}, + {"type": "64th", "dots": 1}, + {"type": "64th", "dots": 2}, + {"type": "64th", "dots": 3}, + {"type": "32nd", "dots": 0}, + {"type": "32nd", "dots": 1}, + {"type": "32nd", "dots": 2}, + {"type": "32nd", "dots": 3}, + {"type": "16th", "dots": 0}, + {"type": "16th", "dots": 1}, + {"type": "16th", "dots": 2}, + {"type": "16th", "dots": 3}, + {"type": "eighth", "dots": 0}, + {"type": "e", "dots": 0}, + {"type": "eighth", "dots": 1}, + {"type": "e", "dots": 1}, + {"type": "eighth", "dots": 2}, + {"type": "e", "dots": 2}, + {"type": "eighth", "dots": 3}, + {"type": "e", "dots": 3}, + {"type": "quarter", "dots": 0}, + {"type": "q", "dots": 0}, + {"type": "quarter", "dots": 1}, + {"type": "q", "dots": 1}, + {"type": "quarter", "dots": 2}, + {"type": "q", "dots": 2}, + {"type": "quarter", "dots": 3}, + {"type": "q", "dots": 3}, + {"type": "half", "dots": 0}, + {"type": "h", "dots": 0}, + {"type": "half", "dots": 1}, + {"type": "h", "dots": 1}, + {"type": "half", "dots": 2}, + {"type": "h", "dots": 2}, + {"type": "half", "dots": 3}, + {"type": "h", "dots": 3}, + {"type": "whole", "dots": 0}, + {"type": "whole", "dots": 1}, + {"type": "whole", "dots": 2}, + {"type": "whole", "dots": 3}, + {"type": "breve", "dots": 0}, + {"type": "breve", "dots": 1}, + {"type": "breve", "dots": 2}, + {"type": "breve", "dots": 3}, + {"type": "long", "dots": 0}, + {"type": "long", "dots": 1}, + {"type": "long", "dots": 2}, + {"type": "long", "dots": 3}, +] + +MAJOR_KEYS = [ + "Cb", + "Gb", + "Db", + "Ab", + "Eb", + "Bb", + "F", + "C", + "G", + "D", + "A", + "E", + "B", + "F#", + "C#", +] +MINOR_KEYS = [ + "Ab", + "Eb", + "Bb", + "F", + "C", + "G", + "D", + "A", + "E", + "B", + "F#", + "C#", + "G#", + "D#", + "A#", +] + +TIME_UNITS = ["beat", "quarter", "sec", "div"] + +NOTE_NAME_PATT = re.compile(r"([A-G]{1})([xb\#]*)(\d+)") + +INTERVALCLASSES = [ + f"{specific}{generic}" + for generic in [2, 3, 6, 7] + for specific in ["dd", "d", "m", "M", "A", "AA"] +] + [ + f"{specific}{generic}" + for generic in [1, 4, 5] + for specific in ["dd", "d", "P", "A", "AA"] +] + +INTERVAL_TO_SEMITONES = dict( + zip( + INTERVALCLASSES, + [ + generic + specific + for generic in [1, 3, 8, 10] + for specific in [-2, -1, 0, 1, 2, 3] + ] + + [ + generic + specific + for generic in [0, 5, 7] + for specific in [-2, -1, 0, 1, 2] + ], + ) +) + + +STEPS = { + "C": 0, + "D": 1, + "E": 2, + "F": 3, + "G": 4, + "A": 5, + "B": 6, + 0: "C", + 1: "D", + 2: "E", + 3: "F", + 4: "G", + 5: "A", + 6: "B", +} + + +MUSICAL_BEATS = {6: 2, 9: 3, 12: 4} + +# Standard tuning frequency of A4 in Hz +A4 = 440.0 + + +UNABBREVS = [ + (re.compile(r"(crescendo|cresc\.?)"), "crescendo"), + (re.compile(r"(smorzando|smorz\.?)"), "smorzando"), + (re.compile(r"(decrescendo|(decresc|decr|dimin|dim)\.?)"), "diminuendo"), + (re.compile(r"((acceler|accel|acc)\.?)"), "accelerando"), + (re.compile(r"(ritenente|riten\.?)"), "ritenuto"), + (re.compile(r"((ritard|rit)\.?)"), "ritardando"), + (re.compile(r"((rallent|rall)\.?)"), "rallentando"), + (re.compile(r"(dolciss\.?)"), "dolcissimo"), + (re.compile(r"((sosten|sost)\.?)"), "sostenuto"), + (re.compile(r"(delicatiss\.?)"), "delicatissimo"), + (re.compile(r"(leggieramente|leggiermente|leggiero|legg\.?)"), "leggiero"), + (re.compile(r"(leggierissimo|(leggieriss\.?))"), "leggierissimo"), + (re.compile(r"(scherz\.?)"), "scherzando"), + (re.compile(r"(tenute|ten\.?)"), "tenuto"), + (re.compile(r"(allegretto)"), "allegro"), + (re.compile(r"(espress\.?)"), "espressivo"), + (re.compile(r"(ligato)"), "legato"), + (re.compile(r"(ligatissimo)"), "legatissimo"), + (re.compile(r"((rinforz|rinf|rfz|rf)\.?)"), "rinforzando"), +] + + + +TWO_PI = 2 * np.pi +SAMPLE_RATE = 44100 +DTYPE = float + +NATURAL_INTERVAL_RATIOS = { + 0: 1, + 1: 16 / 15, # 15/14, 11/10 + 2: 8 / 7, # 9/8, 10/9, 12/11, 13/14 + 3: 6 / 5, # 7/6, + 4: 5 / 4, + 5: 4 / 3, + 6: 7 / 5, # 13/9, + 7: 3 / 2, + 8: 8 / 5, + 9: 5 / 3, + 10: 7 / 4, # 13/7 + 11: 15 / 8, + 12: 2, +} + +# symmetric five limit temperament with supertonic = 10:9 +FIVE_LIMIT_INTERVAL_RATIOS = { + 0: 1, + 1: 16 / 15, + 2: 10 / 9, + 3: 6 / 5, + 4: 5 / 4, + 5: 4 / 3, + 6: 7 / 5, + 7: 3 / 2, + 8: 8 / 5, + 9: 5 / 3, + 10: 9 / 5, + 11: 15 / 8, + 12: 2, +} + + +EPSILON = 0.0001 + +KEYS = [ + ("C", "major", 0), + ("Db", "major", -5), + ("D", "major", 2), + ("Eb", "major", -3), + ("E", "major", 4), + ("F", "major", -1), + ("F#", "major", 6), + ("G", "major", 1), + ("Ab", "major", -4), + ("A", "major", 3), + ("Bb", "major", -2), + ("B", "major", 5), + ("C", "minor", -3), + ("C#", "minor", 4), + ("D", "minor", -1), + ("D#", "minor", 6), + ("E", "minor", 1), + ("F", "minor", -4), + ("F#", "minor", 3), + ("G", "minor", -2), + ("G#", "minor", 5), + ("A", "minor", 0), + ("Bb", "minor", -5), + ("B", "minor", 2), +] + +VALID_KEY_PROFILES = [ + "krumhansl_kessler", + "kk", + "temperley", + "tp", + "kostka_payne", + "kp", +] + + +# Krumhansl--Kessler Key Profiles + +# From Krumhansl's "Cognitive Foundations of Musical Pitch" pp.30 +key_prof_maj_kk = np.array( + [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88] +) + +key_prof_min_kk = np.array( + [6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17] +) + +# Temperley Key Profiles + +# CBMS (from "Music and Probability" Table 6.1, pp. 86) +key_prof_maj_cbms = np.array( + [5.0, 2.0, 3.5, 2.0, 4.5, 4.0, 2.0, 4.5, 2.0, 3.5, 1.5, 4.0] +) + +key_prof_min_cbms = np.array( + [5.0, 2.0, 3.5, 4.5, 2.0, 4.0, 2.0, 4.5, 3.5, 2.0, 1.5, 4.0] +) + +# Kostka-Payne (from "Music and Probability" Table 6.1, pp. 86) +key_prof_maj_kp = np.array( + [0.748, 0.060, 0.488, 0.082, 0.670, 0.460, 0.096, 0.715, 0.104, 0.366, 0.057, 0.400] +) + +key_prof_min_kp = np.array( + [0.712, 0.048, 0.474, 0.618, 0.049, 0.460, 0.105, 0.747, 0.404, 0.067, 0.133, 0.330] +) + + +# Scaling factors +MAX = 9999999999999 +MIN_INTERVAL = 0.01 +MAX_INTERVAL = 2 # in seconds +CLUSTER_WIDTH = 1 / 12 # in seconds +N_CLUSTERS = 100 +INIT_DURATION = 10 # in seconds +TIMEOUT = 10 # in seconds +TOLERANCE_POST = 0.4 # propotion of beat_interval +TOLERANCE_PRE = 0.2 # proportion of beat_interval +TOLERANCE_INNER = 1 / 12 +CORRECTION_FACTOR = 1 / 4 # higher => more correction (speed changes) +MAX_AGENTS = 100 # delete low-scoring agents when there are more than MAX_AGENTS +CHORD_SPREAD_TIME = 1 / 12 # for onset aggregation + + diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 3f221e3e..9bd825c5 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -14,10 +14,12 @@ from scipy.sparse import csc_matrix from typing import Union, Callable, Optional, TYPE_CHECKING from partitura.utils.generic import find_nearest, search, iter_current_next +from partitura.utils.globals import * import partitura from tempfile import TemporaryDirectory import os + try: import miditok from miditok.midi_tokenizer import MIDITokenizer @@ -41,286 +43,6 @@ class MIDITokenizer(object): from partitura.performance import PerformanceLike, Performance, PerformedPart -MIDI_BASE_CLASS = {"c": 0, "d": 2, "e": 4, "f": 5, "g": 7, "a": 9, "b": 11} -# _MORPHETIC_BASE_CLASS = {'c': 0, 'd': 1, 'e': 2, 'f': 3, 'g': 4, 'a': 5, 'b': 6} -# _MORPHETIC_OCTAVE = {0: 32, 1: 39, 2: 46, 3: 53, 4: 60, 5: 67, 6: 74, 7: 81, 8: 89} -ALTER_SIGNS = {None: "", 0: "", 1: "#", 2: "x", -1: "b", -2: "bb"} - -DUMMY_PS_BASE_CLASS = { - 0: ("c", 0), - 1: ("c", 1), - 2: ("d", 0), - 3: ("d", 1), - 4: ("e", 0), - 5: ("f", 0), - 6: ("f", 1), - 7: ("g", 0), - 8: ("g", 1), - 9: ("a", 0), - 10: ("a", 1), - 11: ("b", 0), -} - -MEI_DURS_TO_SYMBOLIC = { - "long": "long", - "0": "breve", - "breve": "breve", - "1": "whole", - "2": "half", - "4": "quarter", - "8": "eighth", - "16": "16th", - "32": "32nd", - "64": "64th", - "128": "128th", - "256": "256th", -} - -SYMBOLIC_TO_INT_DURS = { - "long": 0.25, - "breve": 0.5, - "whole": 1, - "half": 2, - "quarter": 4, - "eighth": 8, - "16th": 16, - "32nd": 32, - "64th": 64, - "128th": 128, - "256th": 256, -} - -LABEL_DURS = { - "long": 16, - "breve": 8, - "whole": 4, - "half": 2, - "h": 2, - "quarter": 1, - "q": 1, - "eighth": 1 / 2, - "e": 1 / 2, - "16th": 1 / 4, - "32nd": 1 / 8.0, - "64th": 1 / 16, - "128th": 1 / 32, - "256th": 1 / 64, -} -DOT_MULTIPLIERS = (1, 1 + 1 / 2, 1 + 3 / 4, 1 + 7 / 8) -# DURS and SYM_DURS encode the same information as _LABEL_DURS and -# _DOT_MULTIPLIERS, but they allow for faster estimation of symbolic duration -# (estimate_symbolic duration). At some point we will probably do away with -# _LABEL_DURS and _DOT_MULTIPLIERS. -DURS = np.array( - [ - 1.5625000e-02, - 2.3437500e-02, - 2.7343750e-02, - 2.9296875e-02, - 3.1250000e-02, - 4.6875000e-02, - 5.4687500e-02, - 5.8593750e-02, - 6.2500000e-02, - 9.3750000e-02, - 1.0937500e-01, - 1.1718750e-01, - 1.2500000e-01, - 1.8750000e-01, - 2.1875000e-01, - 2.3437500e-01, - 2.5000000e-01, - 3.7500000e-01, - 4.3750000e-01, - 4.6875000e-01, - 5.0000000e-01, - 5.0000000e-01, - 7.5000000e-01, - 7.5000000e-01, - 8.7500000e-01, - 8.7500000e-01, - 9.3750000e-01, - 9.3750000e-01, - 1.0000000e00, - 1.0000000e00, - 1.5000000e00, - 1.5000000e00, - 1.7500000e00, - 1.7500000e00, - 1.8750000e00, - 1.8750000e00, - 2.0000000e00, - 2.0000000e00, - 3.0000000e00, - 3.0000000e00, - 3.5000000e00, - 3.5000000e00, - 3.7500000e00, - 3.7500000e00, - 4.0000000e00, - 6.0000000e00, - 7.0000000e00, - 7.5000000e00, - 8.0000000e00, - 1.2000000e01, - 1.4000000e01, - 1.5000000e01, - 1.6000000e01, - 2.4000000e01, - 2.8000000e01, - 3.0000000e01, - ] -) - -SYM_DURS = [ - {"type": "256th", "dots": 0}, - {"type": "256th", "dots": 1}, - {"type": "256th", "dots": 2}, - {"type": "256th", "dots": 3}, - {"type": "128th", "dots": 0}, - {"type": "128th", "dots": 1}, - {"type": "128th", "dots": 2}, - {"type": "128th", "dots": 3}, - {"type": "64th", "dots": 0}, - {"type": "64th", "dots": 1}, - {"type": "64th", "dots": 2}, - {"type": "64th", "dots": 3}, - {"type": "32nd", "dots": 0}, - {"type": "32nd", "dots": 1}, - {"type": "32nd", "dots": 2}, - {"type": "32nd", "dots": 3}, - {"type": "16th", "dots": 0}, - {"type": "16th", "dots": 1}, - {"type": "16th", "dots": 2}, - {"type": "16th", "dots": 3}, - {"type": "eighth", "dots": 0}, - {"type": "e", "dots": 0}, - {"type": "eighth", "dots": 1}, - {"type": "e", "dots": 1}, - {"type": "eighth", "dots": 2}, - {"type": "e", "dots": 2}, - {"type": "eighth", "dots": 3}, - {"type": "e", "dots": 3}, - {"type": "quarter", "dots": 0}, - {"type": "q", "dots": 0}, - {"type": "quarter", "dots": 1}, - {"type": "q", "dots": 1}, - {"type": "quarter", "dots": 2}, - {"type": "q", "dots": 2}, - {"type": "quarter", "dots": 3}, - {"type": "q", "dots": 3}, - {"type": "half", "dots": 0}, - {"type": "h", "dots": 0}, - {"type": "half", "dots": 1}, - {"type": "h", "dots": 1}, - {"type": "half", "dots": 2}, - {"type": "h", "dots": 2}, - {"type": "half", "dots": 3}, - {"type": "h", "dots": 3}, - {"type": "whole", "dots": 0}, - {"type": "whole", "dots": 1}, - {"type": "whole", "dots": 2}, - {"type": "whole", "dots": 3}, - {"type": "breve", "dots": 0}, - {"type": "breve", "dots": 1}, - {"type": "breve", "dots": 2}, - {"type": "breve", "dots": 3}, - {"type": "long", "dots": 0}, - {"type": "long", "dots": 1}, - {"type": "long", "dots": 2}, - {"type": "long", "dots": 3}, -] - -MAJOR_KEYS = [ - "Cb", - "Gb", - "Db", - "Ab", - "Eb", - "Bb", - "F", - "C", - "G", - "D", - "A", - "E", - "B", - "F#", - "C#", -] -MINOR_KEYS = [ - "Ab", - "Eb", - "Bb", - "F", - "C", - "G", - "D", - "A", - "E", - "B", - "F#", - "C#", - "G#", - "D#", - "A#", -] - -TIME_UNITS = ["beat", "quarter", "sec", "div"] - -NOTE_NAME_PATT = re.compile(r"([A-G]{1})([xb\#]*)(\d+)") - -INTERVALCLASSES = [ - f"{specific}{generic}" - for generic in [2, 3, 6, 7] - for specific in ["dd", "d", "m", "M", "A", "AA"] -] + [ - f"{specific}{generic}" - for generic in [1, 4, 5] - for specific in ["dd", "d", "P", "A", "AA"] -] - -INTERVAL_TO_SEMITONES = dict( - zip( - INTERVALCLASSES, - [ - generic + specific - for generic in [1, 3, 8, 10] - for specific in [-2, -1, 0, 1, 2, 3] - ] - + [ - generic + specific - for generic in [0, 5, 7] - for specific in [-2, -1, 0, 1, 2] - ], - ) -) - - -STEPS = { - "C": 0, - "D": 1, - "E": 2, - "F": 3, - "G": 4, - "A": 5, - "B": 6, - 0: "C", - 1: "D", - 2: "E", - 3: "F", - 4: "G", - 5: "A", - 6: "B", -} - - -MUSICAL_BEATS = {6: 2, 9: 3, 12: 4} - -# Standard tuning frequency of A4 in Hz -A4 = 440.0 - - def ensure_notearray(notearray_or_part, *args, **kwargs): """ Ensures to get a structured note array from the input. diff --git a/partitura/utils/normalize.py b/partitura/utils/normalize.py index 5da9e188..72fb9d59 100644 --- a/partitura/utils/normalize.py +++ b/partitura/utils/normalize.py @@ -4,10 +4,9 @@ This module contains normalization utilities """ import numpy as np +from partitura.utils.globals import EPSILON -EPSILON = 0.0001 - def range_normalize( array, diff --git a/partitura/utils/synth.py b/partitura/utils/synth.py index 507280c7..e2ede4fe 100644 --- a/partitura/utils/synth.py +++ b/partitura/utils/synth.py @@ -22,43 +22,7 @@ midi_pitch_to_frequency, performance_notearray_from_score_notearray, ) - -TWO_PI = 2 * np.pi -SAMPLE_RATE = 44100 -DTYPE = float - -NATURAL_INTERVAL_RATIOS = { - 0: 1, - 1: 16 / 15, # 15/14, 11/10 - 2: 8 / 7, # 9/8, 10/9, 12/11, 13/14 - 3: 6 / 5, # 7/6, - 4: 5 / 4, - 5: 4 / 3, - 6: 7 / 5, # 13/9, - 7: 3 / 2, - 8: 8 / 5, - 9: 5 / 3, - 10: 7 / 4, # 13/7 - 11: 15 / 8, - 12: 2, -} - -# symmetric five limit temperament with supertonic = 10:9 -FIVE_LIMIT_INTERVAL_RATIOS = { - 0: 1, - 1: 16 / 15, - 2: 10 / 9, - 3: 6 / 5, - 4: 5 / 4, - 5: 4 / 3, - 6: 7 / 5, - 7: 3 / 2, - 8: 8 / 5, - 9: 5 / 3, - 10: 9 / 5, - 11: 15 / 8, - 12: 2, -} +from partitura.utils.globals import DTYPE, SAMPLE_RATE, TWO_PI, FIVE_LIMIT_INTERVAL_RATIOS, A4, NATURAL_INTERVAL_RATIOS def midi_pitch_to_natural_frequency( From 112bf50e45be49c3634b60736d7ae69cba487b5a Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 13 Feb 2024 16:48:04 +0100 Subject: [PATCH 090/197] Update globals. --- partitura/utils/globals.py | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/partitura/utils/globals.py b/partitura/utils/globals.py index 3660686a..57d176f0 100644 --- a/partitura/utils/globals.py +++ b/partitura/utils/globals.py @@ -431,3 +431,50 @@ CHORD_SPREAD_TIME = 1 / 12 # for onset aggregation + +Voc_majmin = [ + "Cad64", "V", "viio", "V7", "N", "It", "Fr7", "Ger7" +] + +Voc_maj_only = [ + "I", "ii", "iii", "IV", "vi", "I7", "ii7", "iii7", "IV7", "vi7", "viio7", "V+" +] + +Voc_min_only = [ + "i", "iio", "III+", "iv", "VI", "i7", "iio7", "III+7", "iv7", "VI7", "viio7" +] + +Voc_maj = Voc_majmin + Voc_maj_only +Voc_min = Voc_majmin + Voc_min_only + +Voc_T_degree = [ + "I", "II", "III", "IV", "V", "VI", "VII", + "i", "ii", "iii", "iv", "v", "vi", "vii", +] + + +BASE_PC = { + "C": 0, + "D": 2, + "E": 4, + "F": 5, + "G": 7, + "A": 9, + "B": 11, +} + +ALT_TO_INT = { + "--": -2, + "-": -1, + "": 0, + "#": 1, + "##": 2, +} + +INT_TO_ALT = { + -2: "--", + -1: "-", + 0: "", + 1: "#", + 2: "##", +} \ No newline at end of file From 14d56994d95fa64a5ed5e97b18dc790cfd9effd8 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 13 Feb 2024 16:48:43 +0100 Subject: [PATCH 091/197] Compute localkey from globalkey --- partitura/io/importdcml.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index ae669bb5..bc12c0b3 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -1,9 +1,10 @@ import warnings - +import re import numpy as np from math import ceil import partitura.score as spt -from partitura.utils.music import estimate_symbolic_duration +from partitura.utils.music import estimate_symbolic_duration, transpose_note +from partitura.utils.globals import ALT_TO_INT, INT_TO_ALT try: import pandas as pd except ImportError: @@ -190,17 +191,33 @@ def read_harmony_tsv(beat_tsv_path, part): # row["chord_type"] contains the quality of the chord but it is encoded differently than for other formats # and datasets. For example, a minor chord is encoded as "m" instead of "min" or "minor" # Therefore we do not add the quality to the RomanNumeral object. Then it is extracted from the text. + # Local key is in relation to the global key. + if row["globalkey"].islower(): + transposition_interval = spt.Roman2Interval_Min[row["localkey"]] + else: + transposition_interval = spt.Roman2Interval_Maj[row["localkey"]] + + key_step = re.search(r"[a-gA-G]", row["globalkey"]).group(0) + key_alter = re.search(r"[#b]", row["globalkey"]).group(0) if re.search(r"[#b]", row["globalkey"]) else "" + key_alter = ALT_TO_INT[key_alter] + key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) + local_key = key_step + INT_TO_ALT[key_alter] part.add( spt.RomanNumeral(text=row["chord"], - local_key=row["localkey"], + local_key=local_key, # quality=row["chord_type"], ), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) for idx, row in data[~is_na_cad].iterrows(): + key_step = re.search(r"[a-gA-G]", row["localkey"]).group(0) + key_alter = re.search(r"[#b]", row["localkey"]).group(0) if re.search(r"[#b]", row["localkey"]) else "" + key_alter = ALT_TO_INT[key_alter] + key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) + local_key = key_step + INT_TO_ALT[key_alter] part.add( spt.Cadence(text=row["cadence"], - local_key=row["localkey"], + local_key=local_key, ), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) # Check if phrase information is available. From 2396253cb80d088867a0a7317379961614a243c5 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 13 Feb 2024 16:48:54 +0100 Subject: [PATCH 092/197] Update functions in music. --- partitura/utils/music.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 9bd825c5..b79be409 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -3164,6 +3164,27 @@ def tokenize( return tokens +def step2pc(step, alter): + """ + Convert a step to a pitch class. + + Parameters + ---------- + step: str + The step of the pitch, e.g. C, D, E, etc. + alter: int + The alteration of the pitch, e.g. -2, -1, 0, 1, 2 etc. + + Returns + ------- + pc: int + The pitch class of the step. + """ + base_pc = BASE_PC[step] + pc = (base_pc + alter) % 12 + return pc + + if __name__ == "__main__": import doctest From d5a431c0b32c417b8846d37c47e7e3faf7952ee6 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 13 Feb 2024 16:49:14 +0100 Subject: [PATCH 093/197] Update Roman Numeral with bass and root. --- partitura/score.py | 89 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index 50aabef1..d2c82716 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -15,7 +15,7 @@ from numbers import Number # import copy -from partitura.utils.music import MUSICAL_BEATS, INTERVALCLASSES, INTERVAL_TO_SEMITONES +from partitura.utils.globals import MUSICAL_BEATS, INTERVALCLASSES, INTERVAL_TO_SEMITONES import warnings, sys import numpy as np import re @@ -45,8 +45,9 @@ _OrderedSet, update_note_ids_after_unfolding, ) - from partitura.utils.generic import interp1d +from partitura.utils.music import transpose_note, step2pc +from partitura.utils.globals import (INT_TO_ALT, ALT_TO_INT) class Part(object): @@ -2780,6 +2781,10 @@ def __init__(self, text, inversion=None, local_key=None, primary_degree=None, se self.primary_degree = primary_degree if primary_degree is not None else self._process_primary_degree() 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: + self.root = self.find_root_note() + self.bass_note = self.find_bass_note() def _process_inversion(self): """Find the inversion of the roman numeral from the text""" @@ -2824,7 +2829,8 @@ def _process_primary_degree(self): roman_text = self.text.split(":")[-1] primary_degree = re.search(r'[a-zA-Z+]+', roman_text) if primary_degree: - return primary_degree.group(0) + # remove o from the text + return primary_degree.group(0).replace("o", "") return None def _process_secondary_degree(self): @@ -2886,6 +2892,47 @@ def _process_quality(self): quality = None return quality + def find_root_note(self): + """ + Find the root note of a chord. + + Returns + ------- + number: int + The number of the chord. + """ + # Corrected step after degree2 + interval = Roman2Interval_Min[self.secondary_degree] if self.secondary_degree.islower() else Roman2Interval_Maj[self.secondary_degree] + key_step = re.search(r"[a-gA-G]", self.local_key).group(0) + key_alter = re.search(r"[#b]", self.local_key).group(0) if re.search(r"[#b]", self.local_key) else "" + key_alter = ALT_TO_INT[key_alter] + step, alter = transpose_note(key_step, key_alter, interval) + # Corrected step after degree1 + # TODO add support for diminished and augmented chords + interval = Roman2Interval_Min[self.primary_degree] if self.primary_degree.islower() else Roman2Interval_Maj[self.primary_degree] + step, alter = transpose_note(step, alter, interval) + root = step + INT_TO_ALT[alter] + return root + + def find_bass_note(self): + # TODO add support for diminished and augmented chords + step = re.search(r"[a-gA-G]", self.root).group(0) + alter = re.search(r"[#b]", self.root) + alter = ALT_TO_INT[alter.group(0)] if alter else 0 + + if self.inversion == 1: + if self.primary_degree.islower(): + step, alter = transpose_note(step, alter, Interval(3, "m")) + else: + step, alter = transpose_note(step, alter, Interval(3, "M")) + elif self.inversion == 2: + step, alter = transpose_note(step, alter, Interval(5, "P")) + elif self.inversion == 3: + step, alter = transpose_note(step, alter, Interval(7, "m")) + + bass_note_name = step + INT_TO_ALT[alter] + return bass_note_name + def __str__(self): return f'{super().__str__()} "{self.text}"' @@ -5163,6 +5210,42 @@ def is_a_within_b(a, b, wholly=False): return contained +Roman2Interval_Maj = { + "I": Interval(1, "P"), + "II": Interval(2, "M"), + "III": Interval(3, "M"), + "IV": Interval(4, "P"), + "V": Interval(5, "P"), + "VI": Interval(6, "M"), + "VII": Interval(7, "M"), + "i": Interval(1, "P"), + "ii": Interval(2, "M"), + "iii": Interval(3, "m"), + "iv": Interval(4, "P"), + "v": Interval(5, "P"), + "vi": Interval(6, "M"), + "vii": Interval(7, "M"), +} + +Roman2Interval_Min = { + "I": Interval(1, "P"), + "II": Interval(2, "M"), + "III": Interval(3, "m"), + "IV": Interval(4, "P"), + "V": Interval(5, "P"), + "VI": Interval(6, "m"), + "VII": Interval(7, "m"), + "i": Interval(1, "P"), + "ii": Interval(2, "M"), + "iii": Interval(3, "m"), + "iv": Interval(4, "P"), + "v": Interval(5, "P"), + "vi": Interval(6, "m"), + "vii": Interval(7, "m"), +} + + + class InvalidTimePointException(Exception): """Raised when a time point is instantiated with an invalid number.""" From d8317ae3213d761bfbde13478a522138b6772862 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 13 Feb 2024 17:14:16 +0100 Subject: [PATCH 094/197] minor fixes for correct harmony parsing. --- partitura/io/importdcml.py | 4 ++-- partitura/score.py | 27 ++++++++++++++++++++++----- partitura/utils/globals.py | 4 +++- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index bc12c0b3..11dc4e90 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -210,8 +210,8 @@ def read_harmony_tsv(beat_tsv_path, part): ), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) for idx, row in data[~is_na_cad].iterrows(): - key_step = re.search(r"[a-gA-G]", row["localkey"]).group(0) - key_alter = re.search(r"[#b]", row["localkey"]).group(0) if re.search(r"[#b]", row["localkey"]) else "" + key_step = re.search(r"[a-gA-G]", row["globalkey"]).group(0) + key_alter = re.search(r"[#b]", row["globalkey"]).group(0) if re.search(r"[#b]", row["globalkey"]) else "" key_alter = ALT_TO_INT[key_alter] key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) local_key = key_step + INT_TO_ALT[key_alter] diff --git a/partitura/score.py b/partitura/score.py index d2c82716..d2cb6fee 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -21,7 +21,7 @@ import re from scipy.interpolate import PPoly from typing import Union, List, Optional, Iterator, Iterable as Itertype - +import difflib from partitura.utils import ( ComparableMixin, ReplaceRefMixin, @@ -47,7 +47,7 @@ ) from partitura.utils.generic import interp1d from partitura.utils.music import transpose_note, step2pc -from partitura.utils.globals import (INT_TO_ALT, ALT_TO_INT) +from partitura.utils.globals import (INT_TO_ALT, ALT_TO_INT, ACCEPTED_ROMANS) class Part(object): @@ -2829,8 +2829,12 @@ def _process_primary_degree(self): roman_text = self.text.split(":")[-1] primary_degree = re.search(r'[a-zA-Z+]+', roman_text) if primary_degree: - # remove o from the text - return primary_degree.group(0).replace("o", "") + prim_d = primary_degree.group(0) + # if the primary degree is not in accepted values, return the closest one + if prim_d in ACCEPTED_ROMANS: + return prim_d + else: + return difflib.get_close_matches(prim_d, ACCEPTED_ROMANS, n=1, cutoff=0.5)[0] return None def _process_secondary_degree(self): @@ -2909,7 +2913,7 @@ def find_root_note(self): step, alter = transpose_note(key_step, key_alter, interval) # Corrected step after degree1 # TODO add support for diminished and augmented chords - interval = Roman2Interval_Min[self.primary_degree] if self.primary_degree.islower() else Roman2Interval_Maj[self.primary_degree] + interval = Roman2Interval_Min[self.primary_degree] if key_step.islower() else Roman2Interval_Maj[self.primary_degree] step, alter = transpose_note(step, alter, interval) root = step + INT_TO_ALT[alter] return root @@ -5225,12 +5229,19 @@ def is_a_within_b(a, b, wholly=False): "v": Interval(5, "P"), "vi": Interval(6, "M"), "vii": Interval(7, "M"), + "viio": Interval(7, "M"), + "N": Interval(2, "m"), + "iio": Interval(2, "M"), + "Ger7": Interval(4, "A"), + "Fr7": Interval(4, "A"), + "It": Interval(4, "A"), } Roman2Interval_Min = { "I": Interval(1, "P"), "II": Interval(2, "M"), "III": Interval(3, "m"), + "III+": Interval(3, "m"), "IV": Interval(4, "P"), "V": Interval(5, "P"), "VI": Interval(6, "m"), @@ -5242,6 +5253,12 @@ def is_a_within_b(a, b, wholly=False): "v": Interval(5, "P"), "vi": Interval(6, "m"), "vii": Interval(7, "m"), + "viio": Interval(7, "M"), + "N": Interval(2, "m"), + "iio": Interval(2, "M"), + "Ger7": Interval(4, "A"), + "Fr7": Interval(4, "A"), + "It": Interval(4, "A"), } diff --git a/partitura/utils/globals.py b/partitura/utils/globals.py index 57d176f0..06f0bdf9 100644 --- a/partitura/utils/globals.py +++ b/partitura/utils/globals.py @@ -433,7 +433,7 @@ Voc_majmin = [ - "Cad64", "V", "viio", "V7", "N", "It", "Fr7", "Ger7" + "Cad64", "V", "viio", "V7", "N", "It", "Fr7", "Ger7", "v" ] Voc_maj_only = [ @@ -447,6 +447,8 @@ Voc_maj = Voc_majmin + Voc_maj_only Voc_min = Voc_majmin + Voc_min_only +ACCEPTED_ROMANS = list(set(Voc_maj + Voc_min)) + Voc_T_degree = [ "I", "II", "III", "IV", "V", "VI", "VII", "i", "ii", "iii", "iv", "v", "vi", "vii", From 0d694819d8516f1c6614b28e4f2dc82ded17bf89 Mon Sep 17 00:00:00 2001 From: manoskary Date: Tue, 13 Feb 2024 17:56:54 +0000 Subject: [PATCH 095/197] Format code with black (bot) --- partitura/io/exportmei.py | 192 ++++++++++++++++++++++++-------------- 1 file changed, 120 insertions(+), 72 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index b1ef6725..66e92117 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -10,7 +10,12 @@ from operator import itemgetter from itertools import groupby from typing import Optional -from partitura.utils import partition, iter_current_next, to_quarter_tempo, fifths_mode_to_key_name +from partitura.utils import ( + partition, + iter_current_next, + to_quarter_tempo, + fifths_mode_to_key_name, +) import numpy as np from partitura.utils.misc import deprecated_alias, PathLike from partitura.utils.music import MEI_DURS_TO_SYMBOLIC @@ -48,32 +53,37 @@ def elc_id(self): def export_to_mei(self): # Create root MEI element etree.register_namespace("xml", "http://www.w3.org/XML/1998/namespace") - etree.register_namespace( "mei", "http://www.music-encoding.org/ns/mei") - mei = etree.Element('mei', nsmap={'xml': "http://www.w3.org/XML/1998/namespace", - None: "http://www.music-encoding.org/ns/mei"}) + etree.register_namespace("mei", "http://www.music-encoding.org/ns/mei") + mei = etree.Element( + "mei", + nsmap={ + "xml": "http://www.w3.org/XML/1998/namespace", + None: "http://www.music-encoding.org/ns/mei", + }, + ) # mei.set('xmlns', "http://www.music-encoding.org/ns/mei") - mei.set('meiversion', "4.0.1") + mei.set("meiversion", "4.0.1") # Create child elements - mei_head = etree.SubElement(mei, 'meiHead') - file_desc = etree.SubElement(mei_head, 'fileDesc') - music = etree.SubElement(mei, 'music') - body = etree.SubElement(music, 'body') - mdiv = etree.SubElement(body, 'mdiv') - score = etree.SubElement(mdiv, 'score') + mei_head = etree.SubElement(mei, "meiHead") + file_desc = etree.SubElement(mei_head, "fileDesc") + music = etree.SubElement(mei, "music") + body = etree.SubElement(music, "body") + mdiv = etree.SubElement(body, "mdiv") + score = etree.SubElement(mdiv, "score") score.set(XMLNS_ID, "score-" + self.elc_id()) - score_def = etree.SubElement(score, 'scoreDef') + score_def = etree.SubElement(score, "scoreDef") score_def.set(XMLNS_ID, "scoredef-" + self.elc_id()) - staff_grp = etree.SubElement(score_def, 'staffGrp') + staff_grp = etree.SubElement(score_def, "staffGrp") staff_grp.set(XMLNS_ID, "staffgrp-" + self.elc_id()) self._handle_staffs(staff_grp) - section = etree.SubElement(score, 'section') + section = etree.SubElement(score, "section") section.set(XMLNS_ID, "section-" + self.elc_id()) # Iterate over part's timeline for measure in self.part.measures: # Create measure element - xml_el = etree.SubElement(section, 'measure') + xml_el = etree.SubElement(section, "measure") self._handle_measure(measure, xml_el) return mei @@ -87,56 +97,76 @@ def _handle_staffs(self, xml_el): time_sig = time_sigs[0] if len(time_sigs) > 0 else None for staff_num in range(self.part.number_of_staves): staff_num += 1 - staff_def = etree.SubElement(xml_el, 'staffDef') - staff_def.set('n', str(staff_num)) + staff_def = etree.SubElement(xml_el, "staffDef") + staff_def.set("n", str(staff_num)) staff_def.set(XMLNS_ID, "staffdef-" + self.elc_id()) - staff_def.set('lines', '5') + staff_def.set("lines", "5") # Get clef for this staff If no cleff is available for this staff, default to "G2" - clef_def = etree.SubElement(staff_def, 'clef') + clef_def = etree.SubElement(staff_def, "clef") clef_def.set(XMLNS_ID, "clef-" + self.elc_id()) clef_shape = clefs[staff_num].sign if staff_num in clefs.keys() else "G" - clef_def.set('shape', str(clef_shape)) - clef_def.set('line', str(clefs[staff_num].line)) if staff_num in clefs.keys() else clef_def.set('line', '2') + clef_def.set("shape", str(clef_shape)) + ( + clef_def.set("line", str(clefs[staff_num].line)) + if staff_num in clefs.keys() + else clef_def.set("line", "2") + ) # Get key signature for this staff if keys_sig is not None: - ks_def = etree.SubElement(staff_def, 'keySig') + ks_def = etree.SubElement(staff_def, "keySig") ks_def.set(XMLNS_ID, "keysig-" + self.elc_id()) - ks_def.set('mode', keys_sig.mode) if keys_sig.mode is not None else ks_def.set('mode', 'major') + ( + ks_def.set("mode", keys_sig.mode) + if keys_sig.mode is not None + else ks_def.set("mode", "major") + ) if keys_sig.fifths == 0: - ks_def.set('sig', '0') + ks_def.set("sig", "0") elif keys_sig.fifths > 0: - ks_def.set('sig', str(keys_sig.fifths) + 's') + ks_def.set("sig", str(keys_sig.fifths) + "s") else: - ks_def.set('sig', str(abs(keys_sig.fifths)) + 'f') + ks_def.set("sig", str(abs(keys_sig.fifths)) + "f") # Find the pname from the number of sharps or flats and the mode - ks_def.set('pname', fifths_mode_to_key_name(keys_sig.fifths, keys_sig.mode).lower()) + ks_def.set( + "pname", + fifths_mode_to_key_name(keys_sig.fifths, keys_sig.mode).lower(), + ) if time_sig is not None: - ts_def = etree.SubElement(staff_def, 'meterSig') + ts_def = etree.SubElement(staff_def, "meterSig") ts_def.set(XMLNS_ID, "msig-" + self.elc_id()) - ts_def.set('count', str(time_sig.beats)) - ts_def.set('unit', str(time_sig.beat_type)) + ts_def.set("count", str(time_sig.beats)) + ts_def.set("unit", str(time_sig.beat_type)) def _handle_measure(self, measure, measure_el): # Add measure number - measure_el.set('n', str(measure.number)) + measure_el.set("n", str(measure.number)) measure_el.set(XMLNS_ID, "measure-" + self.elc_id()) - note_or_rest_elements = np.array(list(self.part.iter_all(spt.GenericNote, start=measure.start.t, end=measure.end.t, include_subclasses=True))) + note_or_rest_elements = np.array( + list( + self.part.iter_all( + spt.GenericNote, + start=measure.start.t, + end=measure.end.t, + include_subclasses=True, + ) + ) + ) # Separate by staff staffs = np.vectorize(lambda x: x.staff)(note_or_rest_elements) unique_staffs, staff_inverse_map = np.unique(staffs, return_inverse=True) for i, staff in enumerate(unique_staffs): - staff_el = etree.SubElement(measure_el, 'staff') + staff_el = etree.SubElement(measure_el, "staff") # Add staff number - staff_el.set('n', str(staff)) + staff_el.set("n", str(staff)) staff_el.set(XMLNS_ID, "staff-" + self.elc_id()) staff_notes = note_or_rest_elements[staff_inverse_map == i] # Separate by voice voices = np.vectorize(lambda x: x.voice)(staff_notes) unique_voices, voice_inverse_map = np.unique(voices, return_inverse=True) for j, voice in enumerate(unique_voices): - voice_el = etree.SubElement(staff_el, 'layer') - voice_el.set('n', str(voice)) + voice_el = etree.SubElement(staff_el, "layer") + voice_el.set("n", str(voice)) voice_el.set(XMLNS_ID, "voice-" + self.elc_id()) voice_notes = staff_notes[voice_inverse_map == j] # Sort by onset @@ -159,11 +189,11 @@ def _handle_measure(self, measure, measure_el): return measure_el def _handle_chord(self, chord, xml_voice_el): - chord_el = etree.SubElement(xml_voice_el, 'chord') + chord_el = etree.SubElement(xml_voice_el, "chord") chord_el.set(XMLNS_ID, "chord-" + self.elc_id()) for note in chord: duration = self._handle_note_or_rest(note, chord_el) - chord_el.set('dur', duration) + chord_el.set("dur", duration) def _handle_note_or_rest(self, note, xml_voice_el): if isinstance(note, spt.Rest): @@ -173,33 +203,37 @@ def _handle_note_or_rest(self, note, xml_voice_el): return duration def _handle_rest(self, rest, xml_voice_el): - rest_el = etree.SubElement(xml_voice_el, 'rest') + rest_el = etree.SubElement(xml_voice_el, "rest") duration = SYMBOLIC_TYPES_TO_MEI_DURS[rest.symbolic_duration["type"]] - rest_el.set('dur', duration) + rest_el.set("dur", duration) rest_el.set(XMLNS_ID, "rest-" + self.elc_id()) return duration def _handle_note(self, note, xml_voice_el): - note_el = etree.SubElement(xml_voice_el, 'note') + note_el = etree.SubElement(xml_voice_el, "note") duration = SYMBOLIC_TYPES_TO_MEI_DURS[note.symbolic_duration["type"]] - note_el.set('dur', duration) - note_el.set(XMLNS_ID, "note-" + self.elc_id()) if note.id is None else note_el.set(XMLNS_ID, note.id) - note_el.set('oct', str(note.octave)) - note_el.set('pname', note.step.lower()) + note_el.set("dur", duration) + ( + note_el.set(XMLNS_ID, "note-" + self.elc_id()) + if note.id is None + else note_el.set(XMLNS_ID, note.id) + ) + note_el.set("oct", str(note.octave)) + note_el.set("pname", note.step.lower()) if note.tie_next is not None and note.tie_prev is not None: - note_el.set('tie', 'm') + note_el.set("tie", "m") elif note.tie_next is not None: - note_el.set('tie', 'i') + note_el.set("tie", "i") elif note.tie_prev is not None: - note_el.set('tie', 't') + note_el.set("tie", "t") if note.alter is not None: - accidental = etree.SubElement(note_el, 'accid') + accidental = etree.SubElement(note_el, "accid") accidental.set(XMLNS_ID, "accid-" + self.elc_id()) - accidental.set('accid', ALTER_TO_MEI[note.alter]) + accidental.set("accid", ALTER_TO_MEI[note.alter]) if isinstance(note, spt.GraceNote): - note_el.set('grace', 'acc') + note_el.set("grace", "acc") return duration def _handle_tuplets(self, measure_el, start, end): @@ -212,17 +246,20 @@ def _handle_tuplets(self, measure_el, start, end): end_note_el = measure_el.xpath(f".//*[@xml:id='{end_note.id}']")[0] # Create the tuplet element as parent of the start and end note elements # Make it start at the same index as the start note element - tuplet_el = etree.Element('tuplet') + tuplet_el = etree.Element("tuplet") layer_el = start_note_el.getparent() layer_el.insert(layer_el.index(start_note_el), tuplet_el) tuplet_el.set(XMLNS_ID, "tuplet-" + self.elc_id()) - tuplet_el.set('num', str(start_note.symbolic_duration["actual_notes"])) - tuplet_el.set('numbase', str(start_note.symbolic_duration["normal_notes"])) + tuplet_el.set("num", str(start_note.symbolic_duration["actual_notes"])) + tuplet_el.set("numbase", str(start_note.symbolic_duration["normal_notes"])) # Add all elements between the start and end note elements to the tuplet element as childen # Find them from the xml tree start_note_index = start_note_el.getparent().index(start_note_el) end_note_index = end_note_el.getparent().index(end_note_el) - xml_el_within_tuplet = [start_note_el.getparent()[i] for i in range(start_note_index, end_note_index + 1)] + xml_el_within_tuplet = [ + start_note_el.getparent()[i] + for i in range(start_note_index, end_note_index + 1) + ] for el in xml_el_within_tuplet: tuplet_el.append(el) @@ -234,12 +271,12 @@ def _handle_beams(self, measure_el, start, end): layer_el = note_el.getparent() insert_index = layer_el.index(note_el) # If the parent is a tuplet, the beam element should be added as parent of the tuplet element - if layer_el.tag == 'tuplet': + if layer_el.tag == "tuplet": parent_el = layer_el.getparent() insert_index = parent_el.index(layer_el) layer_el = parent_el # Create the beam element - beam_el = etree.Element('beam') + beam_el = etree.Element("beam") layer_el.insert(insert_index, beam_el) beam_el.set(XMLNS_ID, "beam-" + self.elc_id()) for note in beam.notes: @@ -255,18 +292,20 @@ def _handle_clef_changes(self, measure_el, start, end): if clef.start.t == 0: continue # Find the note element corresponding to the start note i.e. has the same id value - for note in self.part.iter_all(spt.GenericNote, start=clef.start.t, end=clef.start.t): + for note in self.part.iter_all( + spt.GenericNote, start=clef.start.t, end=clef.start.t + ): note_el = measure_el.xpath(f".//*[@xml:id='{note.id}']") if len(note_el) > 0: note_el = note_el[0] layer_el = note_el.getparent() insert_index = layer_el.index(note_el) # Create the clef element - clef_el = etree.Element('clef') + clef_el = etree.Element("clef") layer_el.insert(insert_index, clef_el) clef_el.set(XMLNS_ID, "clef-" + self.elc_id()) - clef_el.set('shape', str(clef.sign)) - clef_el.set('line', str(clef.line)) + clef_el.set("shape", str(clef.sign)) + clef_el.set("line", str(clef.line)) def _handle_ks_changes(self, measure_el, start, end): # For key signature changes, we add a new scoreDef element at the beginning of the measure @@ -275,17 +314,23 @@ def _handle_ks_changes(self, measure_el, start, end): if key_sig.start.t == 0: continue # Create the scoreDef element - score_def_el = etree.Element('scoreDef') + score_def_el = etree.Element("scoreDef") score_def_el.set(XMLNS_ID, "scoredef-" + self.elc_id()) - score_def_el.set('mode', key_sig.mode) if key_sig.mode is not None else score_def_el.set('mode', 'major') + ( + score_def_el.set("mode", key_sig.mode) + if key_sig.mode is not None + else score_def_el.set("mode", "major") + ) if key_sig.fifths == 0: - score_def_el.set('sig', '0') + score_def_el.set("sig", "0") elif key_sig.fifths > 0: - score_def_el.set('sig', str(key_sig.fifths) + 's') + score_def_el.set("sig", str(key_sig.fifths) + "s") else: - score_def_el.set('sig', str(abs(key_sig.fifths)) + 'f') + score_def_el.set("sig", str(abs(key_sig.fifths)) + "f") # Find the pname from the number of sharps or flats and the mode - score_def_el.set('pname', fifths_mode_to_key_name(key_sig.fifths, key_sig.mode).lower()) + score_def_el.set( + "pname", fifths_mode_to_key_name(key_sig.fifths, key_sig.mode).lower() + ) # Add the scoreDef element at before the measure element starts parent = measure_el.getparent() parent.insert(parent.index(measure_el), score_def_el) @@ -297,14 +342,14 @@ def _handle_ts_changes(self, measure_el, start, end): if time_sig.start.t == 0: continue # Create the scoreDef element - score_def_el = etree.Element('scoreDef') + score_def_el = etree.Element("scoreDef") score_def_el.set(XMLNS_ID, "scoredef-" + self.elc_id()) # Add the scoreDef element at before the measure element starts parent = measure_el.getparent() parent.insert(parent.index(measure_el), score_def_el) - score_def_el.set('count', str(time_sig.beats)) - score_def_el.set('unit', str(time_sig.beat_type)) + 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): """ @@ -313,10 +358,13 @@ def _handle_harmony(self, measure_el, start, end): The harmonies will be displayed below the lowest staff. """ for harmony in self.part.iter_all(spt.RomanNumeral, start=start, end=end): - harm_el = etree.SubElement(measure_el, 'harm') + harm_el = etree.SubElement(measure_el, "harm") harm_el.set(XMLNS_ID, "harm-" + self.elc_id()) harm_el.set("staff", str(self.part.number_of_staves)) - harm_el.set("tstamp", str(np.diff(self.part.quarter_map([start, harmony.start.t]))[0]+1)) + harm_el.set( + "tstamp", + str(np.diff(self.part.quarter_map([start, harmony.start.t]))[0] + 1), + ) harm_el.set("place", "below") # text is a child element of harmony but not a xml element harm_el.text = harmony.text From ebb37361e20030d190e892ee0793e13ac8f00269 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 14 Feb 2024 11:23:43 +0100 Subject: [PATCH 096/197] minor changes, not yet final. --- partitura/utils/music.py | 13 +++++++++++-- tests/test_kern.py | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index c6fd9b02..a6ae644b 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -15,7 +15,7 @@ from partitura.utils.generic import find_nearest, search, iter_current_next import partitura from tempfile import TemporaryDirectory -import os +import os, math try: import miditok @@ -935,11 +935,20 @@ def estimate_symbolic_duration(dur, div, eps=10**-3): """ global DURS, SYM_DURS qdur = dur / div + if qdur == 0: + return {} i = find_nearest(DURS, qdur) if np.abs(qdur - DURS[i]) < eps: return SYM_DURS[i].copy() else: - return None + # NOTE: Guess tuplets (Naive) it doesn't cover composite durations from tied notes. + type = SYM_DURS[i+3]["type"] + normal_notes = 2 + return { + "type": type, + "actual_notes": math.ceil(normal_notes/qdur), + "normal_notes": normal_notes, + } def to_quarter_tempo(unit, tempo): diff --git a/tests/test_kern.py b/tests/test_kern.py index ff09a261..b6b44c74 100644 --- a/tests/test_kern.py +++ b/tests/test_kern.py @@ -57,8 +57,8 @@ def test_spline_splitting(self): def test_import_export(self): imported_score = load_kern(partitura.EXAMPLE_KERN) exported_score = save_kern(imported_score) - x = np.loadtxt(partitura.EXAMPLE_KERN, comments="!", dtype=str, encoding="utf-8", delimiter="\t") - self.assertTrue(np.all(x == exported_score.to_kern())) + x = np.loadtxt(partitura.EXAMPLE_KERN, comments="!!", dtype=str, encoding="utf-8", delimiter="\t") + self.assertTrue(np.all(x == exported_score)) # if __name__ == "__main__": From cea00a9be4750d5c152be2275c6a52c23f11924d Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 15 Feb 2024 13:37:18 +0100 Subject: [PATCH 097/197] added Cadence text to musicxml export. --- partitura/io/exportmusicxml.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/partitura/io/exportmusicxml.py b/partitura/io/exportmusicxml.py index 0abe508b..dbdd9880 100644 --- a/partitura/io/exportmusicxml.py +++ b/partitura/io/exportmusicxml.py @@ -834,6 +834,17 @@ def do_harmony(part, start, end): bass_step_e = etree.SubElement(bass_e, "bass-step") bass_step_e.text = h.bass result.append((h.start.t, None, harmony_e)) + + # Does harmony annotation for cadences + # TODO: Merge with existing Roman Numeral and ChordSymbol annotations if they exist. + harmony = part.iter_all(score.Cadence, start, end) + for h in harmony: + harmony_e = etree.Element("harmony", print_frame="no") + function = etree.SubElement(harmony_e, "function") + function.text = h.text + kind_e = etree.SubElement(harmony_e, "kind", text="") + kind_e.text = "none" + result.append((h.start.t, None, harmony_e)) return result From 424a989c56addd467d42cff0d01302e19f7b00a9 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 15 Feb 2024 15:40:56 +0100 Subject: [PATCH 098/197] added Cadence text to musicxml export and import. --- partitura/io/exportmusicxml.py | 2 +- partitura/io/importmusicxml.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/partitura/io/exportmusicxml.py b/partitura/io/exportmusicxml.py index dbdd9880..168fd430 100644 --- a/partitura/io/exportmusicxml.py +++ b/partitura/io/exportmusicxml.py @@ -841,7 +841,7 @@ def do_harmony(part, start, end): for h in harmony: harmony_e = etree.Element("harmony", print_frame="no") function = etree.SubElement(harmony_e, "function") - function.text = h.text + function.text = "|" + h.text kind_e = etree.SubElement(harmony_e, "kind", text="") kind_e.text = "none" result.append((h.start.t, None, harmony_e)) diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index b586b9f6..08f03b28 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -645,6 +645,10 @@ def _handle_harmony(e, position, part): if e.find("function") is not None: text = e.find("function").text if text is not None: + if "|" in text: + text = text.split("|")[0] + cadence_annotation = text = text.split("|")[1] + part.add(score.Cadence(cadence_annotation), position) part.add(score.RomanNumeral(text), position) elif e.find("kind") is not None and e.find("root") is not None: # TODO: handle kind text which is other kind of annotation also root From a897101229b586a3a7d1c42500c36c32784dfaea Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 16 Feb 2024 12:13:44 +0100 Subject: [PATCH 099/197] Fix for equalizing quarter division among parts. --- partitura/io/importkern_v2.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index 6b13a323..e4149a98 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -3,6 +3,7 @@ """ This module contains methods for importing Humdrum Kern files. """ +import copy import math import re import warnings @@ -307,16 +308,28 @@ def load_kern( partlist.append(part) # currate parts to the same divs per quarter - # divs_pq = np.lcm.reduce([p._quarter_durations[0] for p in partlist]) - # for part in partlist: - # part.set_quarter_duration(0, divs_pq) + # TODO: do this during parsing + divs_pq = np.lcm.reduce([p._quarter_durations[0] for p in partlist]) + new_partlist = list() + for part in partlist: + if part._quarter_durations[0] != divs_pq: + new_part = spt.Part(part.id, part.part_name, part.part_abbreviation, quarter_duration=divs_pq) + multiplier = divs_pq // part._quarter_durations[0] + for el in part.iter_all(start=0, end=part.last_point): + new_el = copy.copy(el) + new_el.start.t = new_el.start.t * multiplier + if new_el.end is not None: + new_el.end.t = new_el.end.t * multiplier + new_part.add(new_el) + else: + new_partlist.append(part) spt.assign_note_ids( - partlist, keep=(force_note_ids is True or force_note_ids == "keep") + new_partlist, keep=(force_note_ids is True or force_note_ids == "keep") ) doc_name = get_document_name(filename) - score = spt.Score(partlist=partlist, id=doc_name) + score = spt.Score(partlist=new_partlist, id=doc_name) return score From 5c3c32ba0be7b1ea785895eb7f41babcfc14a116 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 16 Feb 2024 12:43:00 +0100 Subject: [PATCH 100/197] Fix for equalizing quarter division among parts during parsing. --- partitura/io/importkern_v2.py | 117 ++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 56 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index e4149a98..9ca0a4e4 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -4,10 +4,8 @@ This module contains methods for importing Humdrum Kern files. """ import copy -import math -import re +import re, sys import warnings - from typing import Union, Optional import numpy as np from math import inf, ceil @@ -170,6 +168,45 @@ def _handle_kern_with_spine_splitting(kern_path): # break +def element_parsing(part, elements, total_duration_values, same_part): + divs_pq = part._quarter_durations[0] + current_tl_pos = 0 + 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: + continue + if isinstance(element, spt.GenericNote): + if total_duration_values[i] == 0: + duration_divs = symbolic_to_numeric_duration(element.symbolic_duration, divs_pq) + else: + quarter_duration = 4 / total_duration_values[i] + duration_divs = ceil(quarter_duration * divs_pq) + el_end = current_tl_pos + duration_divs + part.add(element, start=current_tl_pos, end=el_end) + current_tl_pos = el_end + elif isinstance(element, tuple): + # Chord + quarter_duration = 4 / total_duration_values[i] + duration_divs = ceil(quarter_duration * divs_pq) + el_end = current_tl_pos + duration_divs + for note in element[1]: + part.add(note, start=current_tl_pos, end=el_end) + current_tl_pos = el_end + elif isinstance(element, spt.Slur): + start_sl = element.start_note.start.t + end_sl = element.end_note.start.t + part.add(element, start=start_sl, end=end_sl) + + else: + # Do not repeat structural elements if they are being added to the same part. + if not same_part: + part.add(element, start=current_tl_pos) + else: + if isinstance(element, spt.Measure): + current_tl_pos = measure_mapping[element.number] + + # functions to initialize the kern parser def load_kern( filename: PathLike, @@ -216,6 +253,10 @@ def load_kern( has_instrument = np.char.startswith(splines, "*I") # if all parts have the same instrument, then they are the same part. p_same_part = np.all(splines[has_instrument] == splines[has_instrument][0], axis=0) if np.any(has_instrument) else False + total_durations_list = list() + elements_list = list() + part_assignments = list() + copy_partlist = list() for j, spline in enumerate(splines): parser = SplineParser(size=spline.shape[-1], id="P{}".format(parsing_idxs[j]) if not p_same_part else "P{}".format(j), staff=prev_staff) same_part = False @@ -258,43 +299,23 @@ def load_kern( divs_pq = divs_pq if divs_pq > 4 else 4 # Initialize Part part = spt.Part(id=parser.id, quarter_duration=divs_pq, part_name=parser.name) - current_tl_pos = 0 - - 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: - continue - if isinstance(element, spt.GenericNote): - if parser.total_duration_values[i] == 0: - duration_divs = symbolic_to_numeric_duration(element.symbolic_duration, divs_pq) - else: - quarter_duration = 4 / parser.total_duration_values[i] - duration_divs = ceil(quarter_duration*divs_pq) - el_end = current_tl_pos + duration_divs - part.add(element, start=current_tl_pos, end=el_end) - current_tl_pos = el_end - elif isinstance(element, tuple): - # Chord - quarter_duration = 4 / parser.total_duration_values[i] - duration_divs = ceil(quarter_duration*divs_pq) - el_end = current_tl_pos + duration_divs - for note in element[1]: - part.add(note, start=current_tl_pos, end=el_end) - current_tl_pos = el_end - elif isinstance(element, spt.Slur): - start_sl = element.start_note.start.t - end_sl = element.end_note.start.t - part.add(element, start=start_sl, end=end_sl) - else: - # Do not repeat structural elements if they are being added to the same part. - if not same_part: - part.add(element, start=current_tl_pos) - else: - if isinstance(element, spt.Measure): - current_tl_pos = measure_mapping[element.number] + part_assignments.append(same_part) + total_durations_list.append(parser.total_duration_values) + elements_list.append(elements) + copy_partlist.append(part) + + # Currate parts to the same divs per quarter + divs_pq = np.lcm.reduce([p._quarter_durations[0] for p in copy_partlist]) + 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): + element_parsing(part, elements, total_duration_values, same_part) + + for i, part in enumerate(copy_partlist): + if part_assignments[i]: + continue # For all measures add end time as beginning time of next measure measures = part.measures for i in range(len(measures) - 1): @@ -307,29 +328,13 @@ def load_kern( if parser.id not in [p.id for p in partlist]: partlist.append(part) - # currate parts to the same divs per quarter - # TODO: do this during parsing - divs_pq = np.lcm.reduce([p._quarter_durations[0] for p in partlist]) - new_partlist = list() - for part in partlist: - if part._quarter_durations[0] != divs_pq: - new_part = spt.Part(part.id, part.part_name, part.part_abbreviation, quarter_duration=divs_pq) - multiplier = divs_pq // part._quarter_durations[0] - for el in part.iter_all(start=0, end=part.last_point): - new_el = copy.copy(el) - new_el.start.t = new_el.start.t * multiplier - if new_el.end is not None: - new_el.end.t = new_el.end.t * multiplier - new_part.add(new_el) - else: - new_partlist.append(part) spt.assign_note_ids( - new_partlist, keep=(force_note_ids is True or force_note_ids == "keep") + partlist, keep=(force_note_ids is True or force_note_ids == "keep") ) doc_name = get_document_name(filename) - score = spt.Score(partlist=new_partlist, id=doc_name) + score = spt.Score(partlist=partlist, id=doc_name) return score From 256ef693055f821e97882d00fcc7f4da24ebf598 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 16 Feb 2024 12:59:28 +0100 Subject: [PATCH 101/197] Minor fix for parsing correct duplicate parts. --- partitura/io/importkern_v2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index 9ca0a4e4..be4ae47a 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -260,10 +260,10 @@ def load_kern( for j, spline in enumerate(splines): parser = SplineParser(size=spline.shape[-1], id="P{}".format(parsing_idxs[j]) if not p_same_part else "P{}".format(j), staff=prev_staff) same_part = False - if parser.id in [p.id for p in partlist]: + if parser.id in [p.id for p in copy_partlist]: same_part = True warnings.warn("Part {} already exists. Adding to previous Part.".format(parser.id)) - part = [p for p in partlist if p.id == parser.id][0] + part = [p for p in copy_partlist if p.id == parser.id][0] has_staff = np.char.startswith(spline, "*staff") staff = int(spline[has_staff][0][6:]) if np.count_nonzero(has_staff) else 1 if parser.staff != staff: From c43df035a14b298237c4ecee129ef4950d3e1cf9 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 16 Feb 2024 23:23:15 +0100 Subject: [PATCH 102/197] Update transpose note with simpler function for ascending intervals + some assertions. --- partitura/utils/music.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index b79be409..67e16c08 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -194,7 +194,7 @@ def _transpose_note_inplace(note, interval): ) -def transpose_note(step, alter, interval): +def transpose_note_old(step, alter, interval): """ Transpose a note by a given interval without changing the octave or creating a Note Object. @@ -234,6 +234,42 @@ def transpose_note(step, alter, interval): return new_step, new_alter +def transpose_note(step, alter, interval): + """ + Transpose a note by a given interval without changing the octave or creating a Note Object. + + + Parameters + ---------- + step: str + The step of the pitch, e.g. C, D, E, etc. + alter: int + The alteration of the pitch, e.g. -2, -1, 0, 1, 2 etc. + interval: Interval + The interval to transpose by. Only interval direction "up" is supported. + + Returns + ------- + new_step: str + The new step of the pitch, e.g. C, D, E, etc. + new_alter: int + The new alteration of the pitch, e.g. -2, -1, 0, 1, 2 etc. + """ + prev_step = step.capitalize() + assert interval.direction == "up", "Only interval direction 'up' is supported." + assert -3 < alter < 3, f"Input Alteration {alter} is not in the range -2 to 2." + assert interval.number < 8, f"Input Interval {interval.number} is not in the range 1 to 7." + assert prev_step in BASE_PC.keys(), f"Input Step {prev_step} is must be one of: {BASE_PC.keys()}." + new_step = STEPS[(STEPS[prev_step] + interval.number - 1) % 7] + prev_alter = alter if alter is not None else 0 + pc_prev = step2pc(prev_step, prev_alter) + pc_new = step2pc(new_step, prev_alter) + new_alter = interval.semitones - (pc_new - pc_prev) % 12 + prev_alter + # add test to check if the new alteration is correct (i.e. accept maximum of 2 flats or sharps) + assert -3 < new_alter < 3, f"New alteration {new_alter} is not in the range -2 to 2." + return new_step, new_alter + + def transpose(score: ScoreLike, interval: Interval) -> ScoreLike: """ Transpose a score by a given interval. From 0792473eb466a8aafbd4ef5d6c0ee24fdf6286ba Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 17 Feb 2024 17:41:17 +0100 Subject: [PATCH 103/197] Minor fixes on harmony parsing. --- partitura/io/importmusicxml.py | 3 +-- partitura/score.py | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index 08f03b28..0ab230d0 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -646,8 +646,7 @@ def _handle_harmony(e, position, part): text = e.find("function").text if text is not None: if "|" in text: - text = text.split("|")[0] - cadence_annotation = text = text.split("|")[1] + text, cadence_annotation = text.split("|") part.add(score.Cadence(cadence_annotation), position) part.add(score.RomanNumeral(text), position) elif e.find("kind") is not None and e.find("root") is not None: diff --git a/partitura/score.py b/partitura/score.py index ccb6cfb1..9f7657e3 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -2825,7 +2825,9 @@ def _process_primary_degree(self): """ # The primary degree should be a roman numeral between 1 and 7. # If there is no primary degree, return None + # Remove any key information roman_text = self.text.split(":")[-1] + roman_text = roman_text.split(".")[-1] if "." in roman_text else roman_text primary_degree = re.search(r'[a-zA-Z+]+', roman_text) if primary_degree: prim_d = primary_degree.group(0) @@ -2905,7 +2907,7 @@ def find_root_note(self): The number of the chord. """ # Corrected step after degree2 - interval = Roman2Interval_Min[self.secondary_degree] if self.secondary_degree.islower() else Roman2Interval_Maj[self.secondary_degree] + interval = Roman2Interval_Min[self.secondary_degree] if self.local_key.islower() else Roman2Interval_Maj[self.secondary_degree] key_step = re.search(r"[a-gA-G]", self.local_key).group(0) key_alter = re.search(r"[#b]", self.local_key).group(0) if re.search(r"[#b]", self.local_key) else "" key_alter = ALT_TO_INT[key_alter] From 78d22e86721f1776685cbec5b45c4b7f3e8d74f3 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 17 Feb 2024 17:42:29 +0100 Subject: [PATCH 104/197] Updated vocabulary of Roman2Interval for root estimation. --- partitura/score.py | 1 + 1 file changed, 1 insertion(+) diff --git a/partitura/score.py b/partitura/score.py index 9f7657e3..be8357fd 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -5223,6 +5223,7 @@ def is_a_within_b(a, b, wholly=False): "I": Interval(1, "P"), "II": Interval(2, "M"), "III": Interval(3, "M"), + "III+": Interval(3, "M"), "IV": Interval(4, "P"), "V": Interval(5, "P"), "VI": Interval(6, "M"), From ef751afc7f9ba0851366f12474de337b863a8cf9 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 19 Feb 2024 11:37:50 +0100 Subject: [PATCH 105/197] Fixes for correct symbolic duration parsing. --- partitura/io/importkern_v2.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py index be4ae47a..81397b02 100644 --- a/partitura/io/importkern_v2.py +++ b/partitura/io/importkern_v2.py @@ -572,7 +572,7 @@ def _process_kern_duration(self, duration, is_grace=False): dots = duration.count(".") dur = duration.replace(".", "") if dur in KERN_DURS.keys(): - symbolic_duration = KERN_DURS[dur] + symbolic_duration = copy.deepcopy(KERN_DURS[dur]) else: dur = float(dur) key_loolup = [2 ** i for i in range(0, 9)] @@ -585,11 +585,12 @@ def _process_kern_duration(self, duration, is_grace=False): ) ) - symbolic_duration = KERN_DURS[diff[min(list(diff.keys()))]] + symbolic_duration = copy.deepcopy(KERN_DURS[diff[min(list(diff.keys()))]]) symbolic_duration["actual_notes"] = int(dur // 4) symbolic_duration["normal_notes"] = int(diff[min(list(diff.keys()))]) // 4 - symbolic_duration["dots"] = dots - self.note_duration_values[self.total_parsed_elements] = dot_function((float(dur) if isinstance(dur, str) else dur), symbolic_duration["dots"]) if not is_grace else inf + if dots: + symbolic_duration["dots"] = dots + self.note_duration_values[self.total_parsed_elements] = dot_function((float(dur) if isinstance(dur, str) else dur), dots) if not is_grace else inf return symbolic_duration def process_symbol(self, note, symbols): From f5ac28944103c687618c2c0c7ff0f94b661384f7 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 20 Feb 2024 12:57:18 +0100 Subject: [PATCH 106/197] Filter non valid symbols from cadence text. --- partitura/score.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/partitura/score.py b/partitura/score.py index be8357fd..28386b4a 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -2954,6 +2954,8 @@ def _filter_cadence_type(self): """Cadence should be one of PAC, IAC, HC, DC, EC, PC, or None""" # capitalize text self.text = self.text.upper() + # Filter alphabet characters only. + self.text = re.findall(r'[A-Z]+', self.text)[0] self.text = "IAC" if "IAC" in self.text else self.text if self.text not in ["PAC", "IAC", "HC", "DC", "EC", "PC"]: warnings.warn(f"Cadence type {self.text} not found. Setting to None") From 37729a9c5706c9e933eec16c4846b00500e3acc5 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 20 Feb 2024 12:58:14 +0100 Subject: [PATCH 107/197] Minor fix for correcting typing for flats in key. --- partitura/io/importdcml.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 11dc4e90..081c042f 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -199,6 +199,7 @@ def read_harmony_tsv(beat_tsv_path, part): key_step = re.search(r"[a-gA-G]", row["globalkey"]).group(0) key_alter = re.search(r"[#b]", row["globalkey"]).group(0) if re.search(r"[#b]", row["globalkey"]) else "" + key_alter = key_alter.replace("b", "-") key_alter = ALT_TO_INT[key_alter] key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) local_key = key_step + INT_TO_ALT[key_alter] @@ -212,6 +213,7 @@ def read_harmony_tsv(beat_tsv_path, part): for idx, row in data[~is_na_cad].iterrows(): key_step = re.search(r"[a-gA-G]", row["globalkey"]).group(0) key_alter = re.search(r"[#b]", row["globalkey"]).group(0) if re.search(r"[#b]", row["globalkey"]) else "" + key_alter = key_alter.replace("b", "-") key_alter = ALT_TO_INT[key_alter] key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) local_key = key_step + INT_TO_ALT[key_alter] From 20f799ed914b9fe5e682f641d6bc4f5aaa5428d2 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 23 Feb 2024 16:54:24 +0100 Subject: [PATCH 108/197] Solved issue with dotted durations and empty symbolic duration. --- partitura/io/exportmei.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index 66e92117..abe8b859 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -18,7 +18,7 @@ ) import numpy as np from partitura.utils.misc import deprecated_alias, PathLike -from partitura.utils.music import MEI_DURS_TO_SYMBOLIC +from partitura.utils.music import MEI_DURS_TO_SYMBOLIC, estimate_symbolic_duration __all__ = ["save_mei"] @@ -41,6 +41,7 @@ class MEIExporter: def __init__(self, part): self.part = part + self.qdivs = part._quarter_durations[0] self.element_counter = 0 def elc_id(self): @@ -204,8 +205,12 @@ def _handle_note_or_rest(self, note, xml_voice_el): def _handle_rest(self, rest, xml_voice_el): rest_el = etree.SubElement(xml_voice_el, "rest") + if "type" not in rest.symbolic_duration: + rest.symbolic_duration = estimate_symbolic_duration(rest.end.t - rest.start.t, div=self.qdivs) duration = SYMBOLIC_TYPES_TO_MEI_DURS[rest.symbolic_duration["type"]] rest_el.set("dur", duration) + if "dots" in rest.symbolic_duration: + rest_el.set("dots", str(rest.symbolic_duration["dots"])) rest_el.set(XMLNS_ID, "rest-" + self.elc_id()) return duration @@ -218,6 +223,8 @@ def _handle_note(self, note, xml_voice_el): if note.id is None else note_el.set(XMLNS_ID, note.id) ) + if "dots" in note.symbolic_duration: + note_el.set("dots", str(note.symbolic_duration["dots"])) note_el.set("oct", str(note.octave)) note_el.set("pname", note.step.lower()) if note.tie_next is not None and note.tie_prev is not None: From de6009044ba1b87a934dd990e940541240fd7cce Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 23 Feb 2024 16:55:54 +0100 Subject: [PATCH 109/197] Added export for cadences. --- partitura/io/exportmei.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index abe8b859..2ca58e4a 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -376,6 +376,18 @@ 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 + for harmony in self.part.iter_all(spt.Cadence, start=start, end=end): + harm_el = etree.SubElement(measure_el, "harm") + harm_el.set(XMLNS_ID, "harm-" + self.elc_id()) + harm_el.set("staff", str(self.part.number_of_staves)) + harm_el.set( + "tstamp", + str(np.diff(self.part.quarter_map([start, harmony.start.t]))[0] + 1), + ) + harm_el.set("place", "below") + # text is a child element of harmony but not a xml element + harm_el.text = "|" + harmony.text + @deprecated_alias(parts="score_data") def save_mei( From 229fb49fb459f7749c32b17fea26b358eb5f833a Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 23 Feb 2024 16:56:57 +0100 Subject: [PATCH 110/197] Added Cadence Timed Object class. --- partitura/score.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/partitura/score.py b/partitura/score.py index ca69cbd9..17713379 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -2696,6 +2696,29 @@ def __str__(self): return f'{super().__str__()} "{self.shift_type}"' +class Cadence(TimedObject): + """A cadence element in the score usually for Cadences.""" + def __init__(self, text, local_key=None): + super().__init__() + self.text = text + self._filter_cadence_type() + self.local_key = local_key + + def _filter_cadence_type(self): + """Cadence should be one of PAC, IAC, HC, DC, EC, PC, or None""" + # capitalize text + self.text = self.text.upper() + # Filter alphabet characters only. + self.text = re.findall(r'[A-Z]+', self.text)[0] + self.text = "IAC" if "IAC" in self.text else self.text + if self.text not in ["PAC", "IAC", "HC", "DC", "EC", "PC"]: + warnings.warn(f"Cadence type {self.text} not found. Setting to None") + self.text = None + + def __str__(self): + return f'{super().__str__()} "{self.text}"' + + class Harmony(TimedObject): """A harmony element in the score not currently used. From 15c269e334453066aa4d1461de229a45ecfe7a42 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 23 Feb 2024 17:36:46 +0100 Subject: [PATCH 111/197] Added Beam support for musicxml import. --- partitura/io/importmusicxml.py | 37 ++++++++++++++++++++++++++++++---- partitura/score.py | 9 ++++++--- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index b586b9f6..1c7461d1 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -517,6 +517,8 @@ def _handle_measure(measure_el, position, part, ongoing, doc_order, measure_coun # add the start of the measure to the time line part.add(measure, position) + # Initialize Beams in Measure + prev_beam = None # keep track of the position within the measure # measure_pos = 0 measure_start = position @@ -575,8 +577,8 @@ def _handle_measure(measure_el, position, part, ongoing, doc_order, measure_coun _handle_sound(e, position, part) elif e.tag == "note": - (position, prev_note) = _handle_note( - e, position, part, ongoing, prev_note, doc_order + (position, prev_note, prev_beam) = _handle_note( + e, position, part, ongoing, prev_note, doc_order, prev_beam ) doc_order += 1 measure_maxtime = max(measure_maxtime, position) @@ -1175,13 +1177,15 @@ def _handle_sound(e, position, part): (position, part, tempo) -def _handle_note(e, position, part, ongoing, prev_note, doc_order): +def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=None): # get some common features of element if available duration = get_value_from_tag(e, "duration", int) or 0 # elements may have an explicit temporal offset # offset = get_value_from_tag(e, 'offset', int) or 0 staff = get_value_from_tag(e, "staff", int) or 1 voice = get_value_from_tag(e, "voice", int) or 1 + # initialize beam to None + beam = None # add support of uppercase "ID" tags note_id = ( @@ -1254,6 +1258,27 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order): if isinstance(prev_note, score.GraceNote) and prev_note.voice == voice: note.grace_prev = prev_note else: + beam = e.find("beam") + if beam is not None: + if "number" in beam.attrib.keys(): + beam_num = beam.attrib["number"] + beam = beam.text if beam_num == "1" else None + else: + beam = beam.text + + if beam == "begin": + prev_beam = score.Beam() + part.add(prev_beam, position) + beam = prev_beam + elif beam == "continue": + beam = prev_beam + elif beam == "end": + beam = prev_beam + prev_beam = None + else: + beam = None + prev_beam = None + note = score.Note( step=step, octave=octave, @@ -1309,6 +1334,10 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order): part.add(note, position, position + duration) + # After note is assigned to part we can assign the beam to the note if it exists + if isinstance(beam, score.Beam): + note.assign_beam(beam) + ties = e.findall("tie") if len(ties) > 0: tie_key = ("tie", getattr(note, "midi_pitch", "rest")) @@ -1353,7 +1382,7 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order): new_position = position + duration - return new_position, note + return new_position, note, prev_beam def handle_tuplets(notations, ongoing, note): diff --git a/partitura/score.py b/partitura/score.py index 17713379..874da6cf 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -1582,9 +1582,10 @@ def __init__( articulations=None, ornaments=None, doc_order=None, + **kwargs ): self._sym_dur = None - super().__init__() + super().__init__(**kwargs) self.voice = voice self.id = id self.staff = staff @@ -1871,13 +1872,15 @@ class Note(GenericNote): """ - def __init__(self, step, octave, alter=None, beam=None, **kwargs): + def __init__(self, step, octave, alter=None, **kwargs): super().__init__(**kwargs) self.step = step.upper() self.octave = octave self.alter = alter - self.beam = beam + self.beam = None + def assign_beam(self, beam): + self.beam = beam if self.beam is not None: self.beam.append(self) From facf6a0754c5053788180a99da66360bb6ca9f7c Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 23 Feb 2024 18:52:24 +0100 Subject: [PATCH 112/197] Only displaying accidentals if they do not belong to the key signature. --- partitura/io/exportmei.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index 2ca58e4a..af484bef 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -43,6 +43,9 @@ def __init__(self, part): self.part = part self.qdivs = part._quarter_durations[0] self.element_counter = 0 + self.current_key_signature = [] + self.flats = ["bf", "ef", "af", "df", "gf", "cf", "ff"] + self.sharps = ["fs", "cs", "gs", "ds", "as", "es", "bs"] def elc_id(self): # transforms an integer number to 8-digit string @@ -125,8 +128,10 @@ def _handle_staffs(self, xml_el): ks_def.set("sig", "0") elif keys_sig.fifths > 0: ks_def.set("sig", str(keys_sig.fifths) + "s") + self.current_key_signature = self.sharps[: keys_sig.fifths] else: ks_def.set("sig", str(abs(keys_sig.fifths)) + "f") + self.current_key_signature = self.flats[: abs(keys_sig.fifths)] # Find the pname from the number of sharps or flats and the mode ks_def.set( "pname", @@ -235,9 +240,12 @@ def _handle_note(self, note, xml_voice_el): note_el.set("tie", "t") if note.alter is not None: - accidental = etree.SubElement(note_el, "accid") - accidental.set(XMLNS_ID, "accid-" + self.elc_id()) - accidental.set("accid", ALTER_TO_MEI[note.alter]) + if note.step.lower() + ALTER_TO_MEI[note.alter] in self.current_key_signature: + note_el.set("accid.ges", ALTER_TO_MEI[note.alter]) + else: + accidental = etree.SubElement(note_el, "accid") + accidental.set(XMLNS_ID, "accid-" + self.elc_id()) + accidental.set("accid", ALTER_TO_MEI[note.alter]) if isinstance(note, spt.GraceNote): note_el.set("grace", "acc") @@ -332,8 +340,10 @@ def _handle_ks_changes(self, measure_el, start, end): score_def_el.set("sig", "0") elif key_sig.fifths > 0: score_def_el.set("sig", str(key_sig.fifths) + "s") + self.current_key_signature = self.sharps[: key_sig.fifths] else: score_def_el.set("sig", str(abs(key_sig.fifths)) + "f") + self.current_key_signature = self.flats[: abs(key_sig.fifths)] # Find the pname from the number of sharps or flats and the mode score_def_el.set( "pname", fifths_mode_to_key_name(key_sig.fifths, key_sig.mode).lower() From b68340106799cae19ff0b96e136137b623c2e17a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Sat, 24 Feb 2024 14:20:19 +0100 Subject: [PATCH 113/197] fix computing time in ticks for slice_ppart_by_time --- partitura/utils/music.py | 65 ++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index c6fd9b02..c68507c1 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -3265,10 +3265,12 @@ def slice_ppart_by_time( # get ppq if PerformedPart contains it, # else skip time_tick info when e.g. created with 'load_performance_midi' - try: - ppq = ppart.ppq - except AttributeError: - ppq = None + # try: + # ppq = ppart.ppq + # except AttributeError: + # ppq = None + ppq = getattr(ppart, "ppq", None) + mpq = getattr(ppart, "mpq", None) controls_slice = [] if ppart.controls: @@ -3276,8 +3278,12 @@ def slice_ppart_by_time( if cc["time"] >= start_time and cc["time"] <= end_time: new_cc = cc.copy() new_cc["time"] -= start_time - if ppq: - new_cc["time_tick"] = int(2 * ppq * cc["time"]) + if ppq is not None and mpq is not None: + new_cc["time_tick"] = seconds_to_midi_ticks( + time_in_seconds=new_cc["time"], + mpq=mpq, + ppq=ppq, + ) controls_slice.append(new_cc) programs_slice = [] @@ -3286,10 +3292,18 @@ def slice_ppart_by_time( if pr["time"] >= start_time and pr["time"] <= end_time: new_pr = pr.copy() new_pr["time"] -= start_time - if ppq: - new_pr["time_tick"] = int(2 * ppq * pr["time"]) + if ppq is not None and mpq is not None: + new_pr["time_tick"] = seconds_to_midi_ticks( + time_in_seconds=new_pr["time"], + mpq=mpq, + ppq=ppq, + ) programs_slice.append(new_pr) + time_signatures = [] + key_signatures = [] + meta_other = [] + notes_slice = [] note_id = 0 for note in ppart.notes: @@ -3303,9 +3317,13 @@ def slice_ppart_by_time( ) else: new_note["note_off"] = note["note_off"] - start_time - if ppq: + if ppq is not None and mpq is not None: new_note["note_on_tick"] = 0 - new_note["note_off_tick"] = int(2 * ppq * new_note["note_off"]) + new_note["note_off_tick"] = seconds_to_midi_ticks( + time_in_seconds=new_note["note_off"], + mpq=mpq, + ppq=ppq, + ) if reindex_notes: new_note["id"] = f"n{note_id}" note_id += 1 @@ -3321,11 +3339,19 @@ def slice_ppart_by_time( ) else: new_note["note_off"] = note["note_off"] - start_time - if ppq: - new_note["note_on_tick"] = int(2 * ppq * new_note["note_on"]) - new_note["note_off_tick"] = int(2 * ppq * new_note["note_off"]) + if ppq is not None and mpq is not None: + new_note["note_on_tick"] = seconds_to_midi_ticks( + time_in_seconds=new_note["note_on"], + mpq=mpq, + ppq=ppq, + ) + new_note["note_off_tick"] = seconds_to_midi_ticks( + time_in_seconds=new_note["note_off"], + mpq=mpq, + ppq=ppq, + ) if reindex_notes: - new_note["id"] = "n" + str(note_id) + new_note["id"] = f"n{note_id}" note_id += 1 notes_slice.append(new_note) # assumes notes in list are sorted by onset time @@ -3334,14 +3360,21 @@ def slice_ppart_by_time( # Create slice PerformedPart ppart_slice = PerformedPart( - notes=notes_slice, programs=programs_slice, controls=controls_slice, ppq=ppq + notes=notes_slice, + programs=programs_slice, + controls=controls_slice, + ppq=ppq, + mpq=mpq, + key_signatures=key_signatures, + time_signatures=time_signatures, + meta_other=meta_other, ) # set threshold property after creating notes list to update 'sound_offset' values ppart_slice.sustain_pedal_threshold = ppart.sustain_pedal_threshold if ppart.id: - ppart_slice.id = ppart.id + "_slice_{}s_to_{}s".format(start_time, end_time) + ppart_slice.id = f"{ppart.id}_slice_{start_time}s_to_{end_time}s" if ppart.part_name: ppart_slice.part_name = ppart.part_name From 2478d968777222b90bfdadbaaf7ed12089840b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Sat, 24 Feb 2024 14:35:16 +0100 Subject: [PATCH 114/197] add missing messates to slice (wip) --- partitura/utils/music.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index c68507c1..d584f8f8 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -11,7 +11,7 @@ import numpy as np from scipy.interpolate import interp1d from scipy.sparse import csc_matrix -from typing import Union, Callable, Optional, TYPE_CHECKING +from typing import Union, Callable, Optional, TYPE_CHECKING, List from partitura.utils.generic import find_nearest, search, iter_current_next import partitura from tempfile import TemporaryDirectory @@ -3256,7 +3256,7 @@ def slice_ppart_by_time( raise ValueError("Input is not an instance of PerformedPart!") if start_time > end_time: - raise ValueError("Start time not less than end time!") + raise ValueError("Start time must be smaller than end time!") # create a new (empty) instance of a PerformedPart # single dummy note added to be able to set sustain_pedal_threshold in __init__ @@ -3272,8 +3272,24 @@ def slice_ppart_by_time( ppq = getattr(ppart, "ppq", None) mpq = getattr(ppart, "mpq", None) + def add_info_to_list(input_list: List[dict], output_list: List[dict]) -> None: + + for elem in input_list: + if elem["time"] >= start_time and elem["time"] <= end_time: + new_elem = elem.copy() + new_elem["time"] -= start_time + if ppq is not None and mpq is not None: + new_elem["time_tick"] = seconds_to_midi_ticks( + time_in_seconds=new_elem["time"], + mpq=mpq, + ppq=ppq, + ) + output_list.append(new_elem) + controls_slice = [] if ppart.controls: + # TODO + # * Keep previous pedal value for cc in ppart.controls: if cc["time"] >= start_time and cc["time"] <= end_time: new_cc = cc.copy() @@ -3288,6 +3304,8 @@ def slice_ppart_by_time( programs_slice = [] if ppart.programs: + # TODO + # * Keep previous programs for pr in ppart.programs: if pr["time"] >= start_time and pr["time"] <= end_time: new_pr = pr.copy() @@ -3301,6 +3319,11 @@ def slice_ppart_by_time( programs_slice.append(new_pr) time_signatures = [] + if ppart.time_signatures: + for ts in ppart.time_signatures: + if ts["time"] >= start_time and ts["time"] <= end_time: + new_ts = ts.copy() + new_ts["time"] -= start_time key_signatures = [] meta_other = [] From fb965ea8c951a461892e9667efeb11fa50f56c9b Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 26 Feb 2024 10:25:42 +0100 Subject: [PATCH 115/197] Added function to infer beams from score. --- partitura/score.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/partitura/score.py b/partitura/score.py index 874da6cf..d6758365 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -4980,6 +4980,66 @@ def merge_parts(parts, reassign="voice"): return new_part +def infer_beaming(part: ScoreLike): + """ + Infer beaming from the metrical position of notes in a part. + + This function infers the beaming based on the time signature for all notes. + It separates the notes into groups based on their staff and voice. + + Parameters + ---------- + part: ScoreLike + The part to infer beaming for. This can be a part or a score. + If a score is given, the function will infer beaming for all parts in the score. + + """ + + if isinstance(part, Score): + for p in part.parts: + infer_beaming(p) + else: + note_array = part.note_array(include_metrical_position=True, include_staff=True, include_time_signature=True) + beat_ends = note_array["onset_beat"] + note_array["duration_beat"] + # split note_array into groups based on staff and voice + unique_vocstaff = np.unique(note_array[['voice', 'staff']], axis=0) + for v, s in unique_vocstaff: + mask = (note_array['voice'] == v) & (note_array['staff'] == s) + # get the metrical position of the notes + na_vocstaff = note_array[mask] + # get the beat ends of the notes + beat_end = beat_ends[mask] + # get notes + beam_start_mask = (na_vocstaff["is_downbeat"] == 1) & (na_vocstaff["duration_beat"] <= 0.5) + mus_beats = na_vocstaff["ts_mus_beats"] * (na_vocstaff["ts_beat_type"] < 4) + mus_beats = np.where(mus_beats == 0, 1, mus_beats) + beam_end_mask = np.isclose(np.mod(beat_end, mus_beats), 0.0) & (na_vocstaff[ + "duration_beat"] <= 0.5) + beam_between = (na_vocstaff["duration_beat"] <= 0.5) & ~beam_start_mask & ~beam_end_mask + id_beam_start = na_vocstaff["id"][beam_start_mask] + id_beam_end = na_vocstaff["id"][beam_end_mask] + id_beam_between = na_vocstaff["id"][beam_between] + start_time = na_vocstaff["onset_div"].min() + end_time = na_vocstaff["onset_div"].max() + 1 + prev_beam = None + for note in part.iter_all(Note, start_time, end_time): + if note.beam is not None: + continue + if note.id in id_beam_start: + beam = Beam() + note.assign_beam(beam) + prev_beam = beam + elif note.id in id_beam_end: + if prev_beam is not None: + note.assign_beam(prev_beam) + part.add(prev_beam, prev_beam.start.t, prev_beam.end.t) + prev_beam = None + elif note.id in id_beam_between: + if prev_beam is None: + prev_beam = Beam() + note.assign_beam(prev_beam) + + def is_a_within_b(a, b, wholly=False): """ Returns a boolean indicating whether a is (wholly) within b. From d344de85af6c1d787ea3b4e6f7017ce5d42a222f Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 26 Feb 2024 12:49:40 +0100 Subject: [PATCH 116/197] Corrections for exporting beams when parent of note element is chord. --- partitura/io/exportmei.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index af484bef..4f6a4e18 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -70,6 +70,10 @@ def export_to_mei(self): # Create child elements mei_head = etree.SubElement(mei, "meiHead") file_desc = etree.SubElement(mei_head, "fileDesc") + # write the title + title_stmt = etree.SubElement(file_desc, "titleStmt") + title = etree.SubElement(title_stmt, "title") + title.text = self.part.id if self.part.id is not None else "Untitled" music = etree.SubElement(mei, "music") body = etree.SubElement(music, "body") mdiv = etree.SubElement(body, "mdiv") @@ -135,7 +139,7 @@ def _handle_staffs(self, xml_el): # Find the pname from the number of sharps or flats and the mode ks_def.set( "pname", - fifths_mode_to_key_name(keys_sig.fifths, keys_sig.mode).lower(), + fifths_mode_to_key_name(keys_sig.fifths, keys_sig.mode).lower()[0], # only the first letter ) if time_sig is not None: @@ -290,6 +294,12 @@ def _handle_beams(self, measure_el, start, end): parent_el = layer_el.getparent() insert_index = parent_el.index(layer_el) layer_el = parent_el + # If the parent is a chord, the beam element should be added as parent of the chord element + if layer_el.tag == "chord": + parent_el = layer_el.getparent() + insert_index = parent_el.index(layer_el) + layer_el = parent_el + # Create the beam element beam_el = etree.Element("beam") layer_el.insert(insert_index, beam_el) @@ -299,7 +309,16 @@ def _handle_beams(self, measure_el, start, end): note_el = measure_el.xpath(f".//*[@xml:id='{note.id}']") if len(note_el) > 0: note_el = note_el[0] - beam_el.append(note_el) + # Add the note element to the beam element but if the parent is a tuplet, the note element should be added as child of the tuplet element + if note_el.getparent().tag == "tuplet": + beam_el.append(note_el.getparent()) + elif note_el.getparent().tag == "chord": + beam_el.append(note_el.getparent()) + else: + # verify that the note element is not already a child of the beam element + if note_el.getparent() != beam_el: + beam_el.append(note_el) + def _handle_clef_changes(self, measure_el, start, end): for clef in self.part.iter_all(spt.Clef, start=start, end=end): From 0317060abc2cca7b19e469235a0f316d76907ecb Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 26 Feb 2024 17:11:34 +0100 Subject: [PATCH 117/197] Fill rests function. --- partitura/score.py | 107 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/partitura/score.py b/partitura/score.py index d6758365..c1c2af31 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -4980,6 +4980,113 @@ def merge_parts(parts, reassign="voice"): return new_part + + +def _fill_rests_within_measure(measure: Measure, part: Part) -> None: + start_time = measure.start.t + end_time = measure.end.t + notes = np.array(list(part.iter_all(GenericNote, start_time, end_time, include_subclasses=True))) + voc_staff = np.array([[n.voice, n.staff] for n in notes]) + un_voc_staff, inverse_map = np.unique(voc_staff, axis=0, return_inverse=True) + for i in range(len(un_voc_staff)): + note_mask = inverse_map == i + notes_per_vocstaff = notes[note_mask] + sort_note_start = np.argsort(np.vectorize(lambda x: x.start.t)(notes_per_vocstaff)) + sort_note_end = np.argsort(np.vectorize(lambda x: x.end.t)(notes_per_vocstaff)) + # get note with min start.t + min_start_note = notes_per_vocstaff[sort_note_start[0]] + if min_start_note.start.t > start_time: + sym_dur = estimate_symbolic_duration(min_start_note.start.t - start_time, part._quarter_durations[0]) + rest = Rest(symbolic_duration=sym_dur, staff=min_start_note.staff, voice=min_start_note.voice) + part.add(rest, start_time, min_start_note.start.t) + + min_end_note = notes_per_vocstaff[sort_note_end[-1]] + if min_end_note.end.t < end_time: + sym_dur = estimate_symbolic_duration(end_time - min_end_note.end.t, part._quarter_durations[0]) + rest = Rest(symbolic_duration=sym_dur, staff=min_end_note.staff, voice=min_end_note.voice) + part.add(rest, min_end_note.end.t, end_time) + + if len(sort_note_start) <= 1: + continue + + for i in range(1, len(sort_note_start)): + if notes_per_vocstaff[sort_note_start[i]].start.t > notes_per_vocstaff[sort_note_end[i-1]].end.t: + sym_dur = estimate_symbolic_duration(notes_per_vocstaff[sort_note_start[i]].start.t - notes_per_vocstaff[sort_note_end[i-1]].end.t, part._quarter_durations[0]) + rest = Rest(symbolic_duration=sym_dur, staff=notes_per_vocstaff[sort_note_end[i-1]].staff, voice=notes_per_vocstaff[sort_note_end[i-1]].voice) + part.add(rest, notes_per_vocstaff[sort_note_end[i-1]].end.t, notes_per_vocstaff[sort_note_start[i]].start.t) + + +def _fill_rests_global(measure: Measure, part: Part, unique_voc_staff: np.ndarray) -> None: + start_time = measure.start.t + end_time = measure.end.t + if end_time - start_time == 0: + return + notes = np.array(list(part.iter_all(GenericNote, start_time, end_time, include_subclasses=True))) + voc_staff = np.array([[n.voice, n.staff] for n in notes]) + un_voc_staff, inverse_map = np.unique(voc_staff, axis=0, return_inverse=True) + for i in range(un_voc_staff.shape[0]): + note_mask = inverse_map == i + notes_per_vocstaff = notes[note_mask] + # get note with min start.t + min_start_note = notes_per_vocstaff[np.argmin(np.vectorize(lambda x: x.start.t)(notes_per_vocstaff))] + if min_start_note.start.t > start_time: + sym_dur = estimate_symbolic_duration(min_start_note.start.t - start_time, part._quarter_durations[0]) + rest = Rest(symbolic_duration=sym_dur, staff=min_start_note.staff, voice=min_start_note.voice) + part.add(rest, start_time, min_start_note.start.t) + + min_end_note = notes_per_vocstaff[np.argmax(np.vectorize(lambda x: x.end.t)(notes_per_vocstaff))] + if min_end_note.end.t < end_time: + sym_dur = estimate_symbolic_duration(end_time - min_end_note.end.t, part._quarter_durations[0]) + rest = Rest(symbolic_duration=sym_dur, staff=min_end_note.staff, voice=min_end_note.voice) + part.add(rest, min_end_note.end.t, end_time) + + if un_voc_staff.shape[0] != unique_voc_staff.shape[0]: + if un_voc_staff.shape[0] == 0: + diff = unique_voc_staff + else: + # View `un_voc_staff` and `unique_voc_staff` as 1-D structured arrays + x_sa = un_voc_staff.view([('', un_voc_staff.dtype)] * un_voc_staff.shape[1]) + y_sa = unique_voc_staff.view([('', unique_voc_staff.dtype)] * unique_voc_staff.shape[1]) + # Find rows in `unique_voc_staff` that are not in `un_voc_staff` + diff = np.setdiff1d(y_sa, x_sa) + for voice, staff in diff: + sym_dur = estimate_symbolic_duration(end_time - start_time, part._quarter_durations[0]) + rest = Rest(symbolic_duration=sym_dur, staff=staff, voice=voice) + part.add(rest, start_time, end_time) + + +def fill_rests(score_data: ScoreLike, measurewise=True) -> None: + """ + Fill rests in a score when a voice starts in a middle of a measure and no rest precedes. + + When measurewise is True, the voices are searched within a measure. + When measurewise is False, the rests are filled globally in the score for all voices and staffs. + + Parameters + ---------- + score_data: ScoreLike + The score to fill rests + measurewise: bool + If True, fill rests within a measure. If False, fill rests globally in the score. + """ + if isinstance(score_data, Score): + partlist = score_data.parts + else: + partlist = [score_data] + for part in partlist: + measures = part.measures + if measurewise: + for measure in measures: + _fill_rests_within_measure(measure, part) + else: + note_array = part.note_array(include_staff=True) + unique_vocstaff = np.unique( + np.array([note_array["voice"], note_array["staff"]], dtype=np.int64), axis=1 + ) + for measure in measures: + _fill_rests_global(measure, part, unique_vocstaff.T) + + def infer_beaming(part: ScoreLike): """ Infer beaming from the metrical position of notes in a part. From d47ec7d8625e50206e8eb178bd03eac86def438a Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 26 Feb 2024 17:13:06 +0100 Subject: [PATCH 118/197] Updated infer beaming. --- partitura/score.py | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index c1c2af31..574760f9 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -5117,9 +5117,11 @@ def infer_beaming(part: ScoreLike): # get the beat ends of the notes beat_end = beat_ends[mask] # get notes - beam_start_mask = (na_vocstaff["is_downbeat"] == 1) & (na_vocstaff["duration_beat"] <= 0.5) mus_beats = na_vocstaff["ts_mus_beats"] * (na_vocstaff["ts_beat_type"] < 4) mus_beats = np.where(mus_beats == 0, 1, mus_beats) + max_mus_beat = mus_beats.max() + beam_start_mask = np.isclose(np.mod(na_vocstaff["onset_beat"], mus_beats), 0.0) & (na_vocstaff[ + "duration_beat"] <= 0.5) beam_end_mask = np.isclose(np.mod(beat_end, mus_beats), 0.0) & (na_vocstaff[ "duration_beat"] <= 0.5) beam_between = (na_vocstaff["duration_beat"] <= 0.5) & ~beam_start_mask & ~beam_end_mask @@ -5129,22 +5131,40 @@ def infer_beaming(part: ScoreLike): start_time = na_vocstaff["onset_div"].min() end_time = na_vocstaff["onset_div"].max() + 1 prev_beam = None - for note in part.iter_all(Note, start_time, end_time): + notes_in_beam = [] + notes_in_vs = list(part.iter_all(Note, start_time, end_time)) + notes_in_vs.sort(key=lambda x: x.start.t) + prev_start = 0 + for note in notes_in_vs: + if note.voice != v or note.staff != s: + continue if note.beam is not None: continue + + if part.beat_map(note.start.t) - part.beat_map(prev_start) > max_mus_beat: + prev_beam = None + notes_in_beam = [] + prev_start = note.start.t if note.id in id_beam_start: - beam = Beam() - note.assign_beam(beam) - prev_beam = beam + notes_in_beam = [] + prev_beam = Beam() + prev_start = note.start.t + notes_in_beam.append(note) elif note.id in id_beam_end: if prev_beam is not None: - note.assign_beam(prev_beam) - part.add(prev_beam, prev_beam.start.t, prev_beam.end.t) + notes_in_beam.append(note) + if len(notes_in_beam) > 1: + for n in notes_in_beam: + n.assign_beam(prev_beam) + part.add(prev_beam, prev_beam.start.t, prev_beam.end.t) + notes_in_beam = [] prev_beam = None elif note.id in id_beam_between: if prev_beam is None: + prev_start = note.start.t + notes_in_beam = [] prev_beam = Beam() - note.assign_beam(prev_beam) + notes_in_beam.append(note) def is_a_within_b(a, b, wholly=False): From 7b879d8115ec24e9cad737eced1c458dec1c9776 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 26 Feb 2024 17:14:41 +0100 Subject: [PATCH 119/197] updated handle beams for export to skip beams with less than 2 notes. --- partitura/io/exportmei.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index 4f6a4e18..2f10d0ca 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -285,6 +285,9 @@ def _handle_tuplets(self, measure_el, start, end): def _handle_beams(self, measure_el, start, end): for beam in self.part.iter_all(spt.Beam, start=start, end=end): start_note = beam.notes[np.argmin([n.start.t for n in beam.notes])] + # If the beam has only one note, skip it + if len(beam.notes) < 2: + continue # Beam element is parent of the note element note_el = measure_el.xpath(f".//*[@xml:id='{start_note.id}']")[0] layer_el = note_el.getparent() From 7959123004cec3ca00fe49129d415f31f916a522 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 28 Feb 2024 11:01:13 +0100 Subject: [PATCH 120/197] corrections for mei export with cross-staff beaming. --- partitura/io/exportmei.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index 2f10d0ca..1184d716 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -164,7 +164,10 @@ def _handle_measure(self, measure, measure_el): ) # Separate by staff staffs = np.vectorize(lambda x: x.staff)(note_or_rest_elements) + voices = np.vectorize(lambda x: x.voice)(note_or_rest_elements) unique_staffs, staff_inverse_map = np.unique(staffs, return_inverse=True) + unique_voices_par = np.unique(voices) + voice_staff_map = {v : {"mask": voices == v, "staff": np.bincount(staffs[voices == v], minlength=len(unique_staffs)).argmax()} for v in unique_voices_par} for i, staff in enumerate(unique_staffs): staff_el = etree.SubElement(measure_el, "staff") # Add staff number @@ -178,7 +181,10 @@ def _handle_measure(self, measure, measure_el): voice_el = etree.SubElement(staff_el, "layer") voice_el.set("n", str(voice)) voice_el.set(XMLNS_ID, "voice-" + self.elc_id()) - voice_notes = staff_notes[voice_inverse_map == j] + # try to handle cross-staff beaming + if voice_staff_map[voice]["staff"] != staff: + continue + voice_notes = note_or_rest_elements[voice_staff_map[voice]["mask"]] # Sort by onset note_start_times = np.vectorize(lambda x: x.start.t)(voice_notes) unique_onsets = np.unique(note_start_times) @@ -216,6 +222,9 @@ def _handle_rest(self, rest, xml_voice_el): rest_el = etree.SubElement(xml_voice_el, "rest") if "type" not in rest.symbolic_duration: rest.symbolic_duration = estimate_symbolic_duration(rest.end.t - rest.start.t, div=self.qdivs) + if rest.symbolic_duration["type"] not in SYMBOLIC_TYPES_TO_MEI_DURS.keys(): + # TODO: handle other types of rests + rest.symbolic_duration["type"] = "quarter" duration = SYMBOLIC_TYPES_TO_MEI_DURS[rest.symbolic_duration["type"]] rest_el.set("dur", duration) if "dots" in rest.symbolic_duration: @@ -236,6 +245,7 @@ def _handle_note(self, note, xml_voice_el): note_el.set("dots", str(note.symbolic_duration["dots"])) note_el.set("oct", str(note.octave)) note_el.set("pname", note.step.lower()) + note_el.set("staff", str(note.staff)) if note.tie_next is not None and note.tie_prev is not None: note_el.set("tie", "m") elif note.tie_next is not None: @@ -284,10 +294,10 @@ def _handle_tuplets(self, measure_el, start, end): def _handle_beams(self, measure_el, start, end): for beam in self.part.iter_all(spt.Beam, start=start, end=end): - start_note = beam.notes[np.argmin([n.start.t for n in beam.notes])] # If the beam has only one note, skip it if len(beam.notes) < 2: continue + start_note = beam.notes[np.argmin([n.start.t for n in beam.notes])] # Beam element is parent of the note element note_el = measure_el.xpath(f".//*[@xml:id='{start_note.id}']")[0] layer_el = note_el.getparent() From 3678135f6a938633d25fc8597490735960b492b8 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 28 Feb 2024 11:04:07 +0100 Subject: [PATCH 121/197] minor change for rest infilling. --- partitura/score.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/partitura/score.py b/partitura/score.py index 574760f9..36cc191e 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -4986,7 +4986,9 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: start_time = measure.start.t end_time = measure.end.t notes = np.array(list(part.iter_all(GenericNote, start_time, end_time, include_subclasses=True))) - voc_staff = np.array([[n.voice, n.staff] for n in notes]) + # voc_staff = np.array([[n.voice, n.staff] for n in notes]) + # voc_staff is now transformed to only voice + voc_staff = np.array([n.voice for n in notes]) un_voc_staff, inverse_map = np.unique(voc_staff, axis=0, return_inverse=True) for i in range(len(un_voc_staff)): note_mask = inverse_map == i From 84ff1f3aef67d1ddb67dda2064f2b868d642e060 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 28 Feb 2024 11:19:08 +0100 Subject: [PATCH 122/197] changed infer beaming based only on the voice. --- partitura/score.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index 36cc191e..31878362 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -5111,9 +5111,12 @@ def infer_beaming(part: ScoreLike): note_array = part.note_array(include_metrical_position=True, include_staff=True, include_time_signature=True) beat_ends = note_array["onset_beat"] + note_array["duration_beat"] # split note_array into groups based on staff and voice - unique_vocstaff = np.unique(note_array[['voice', 'staff']], axis=0) - for v, s in unique_vocstaff: - mask = (note_array['voice'] == v) & (note_array['staff'] == s) + # unique_vocstaff = np.unique(note_array[['voice', 'staff']], axis=0) + # for v, s in unique_vocstaff: + # mask = (note_array['voice'] == v) & (note_array['staff'] == s) + unique_vocstaff = np.unique(note_array["voice"]) + for v in unique_vocstaff: + mask = note_array["voice"] == v # get the metrical position of the notes na_vocstaff = note_array[mask] # get the beat ends of the notes @@ -5138,7 +5141,8 @@ def infer_beaming(part: ScoreLike): notes_in_vs.sort(key=lambda x: x.start.t) prev_start = 0 for note in notes_in_vs: - if note.voice != v or note.staff != s: + # if note.voice != v or note.staff != s: + if note.voice != v: continue if note.beam is not None: continue From 7018e9d61790c42eadf30f4d61eae8d9718b7380 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 28 Feb 2024 11:26:03 +0100 Subject: [PATCH 123/197] Added extra abrieviated symbolic types. --- partitura/io/exportmei.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index 1184d716..3e350fee 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -34,6 +34,9 @@ } SYMBOLIC_TYPES_TO_MEI_DURS = {v: k for k, v in MEI_DURS_TO_SYMBOLIC.items()} +SYMBOLIC_TYPES_TO_MEI_DURS["h"] = "2" +SYMBOLIC_TYPES_TO_MEI_DURS["e"] = "8" +SYMBOLIC_TYPES_TO_MEI_DURS["q"] = "4" DOCTYPE = '\n' @@ -42,6 +45,7 @@ class MEIExporter: def __init__(self, part): self.part = part self.qdivs = part._quarter_durations[0] + self.num_staves = part.number_of_staves self.element_counter = 0 self.current_key_signature = [] self.flats = ["bf", "ef", "af", "df", "gf", "cf", "ff"] @@ -167,12 +171,15 @@ def _handle_measure(self, measure, measure_el): voices = np.vectorize(lambda x: x.voice)(note_or_rest_elements) unique_staffs, staff_inverse_map = np.unique(staffs, return_inverse=True) unique_voices_par = np.unique(voices) - voice_staff_map = {v : {"mask": voices == v, "staff": np.bincount(staffs[voices == v], minlength=len(unique_staffs)).argmax()} for v in unique_voices_par} - for i, staff in enumerate(unique_staffs): + voice_staff_map = {v : {"mask": voices == v, "staff": np.bincount(staffs[voices == v], minlength=len(self.num_staves)).argmax()} for v in unique_voices_par} + for i in range(self.num_staves): + staff = i + 1 staff_el = etree.SubElement(measure_el, "staff") # Add staff number staff_el.set("n", str(staff)) staff_el.set(XMLNS_ID, "staff-" + self.elc_id()) + if staff not in unique_staffs: + continue staff_notes = note_or_rest_elements[staff_inverse_map == i] # Separate by voice voices = np.vectorize(lambda x: x.voice)(staff_notes) @@ -222,9 +229,6 @@ def _handle_rest(self, rest, xml_voice_el): rest_el = etree.SubElement(xml_voice_el, "rest") if "type" not in rest.symbolic_duration: rest.symbolic_duration = estimate_symbolic_duration(rest.end.t - rest.start.t, div=self.qdivs) - if rest.symbolic_duration["type"] not in SYMBOLIC_TYPES_TO_MEI_DURS.keys(): - # TODO: handle other types of rests - rest.symbolic_duration["type"] = "quarter" duration = SYMBOLIC_TYPES_TO_MEI_DURS[rest.symbolic_duration["type"]] rest_el.set("dur", duration) if "dots" in rest.symbolic_duration: From 0f34336849c026f3ef0bd93f8e681c5467dbdf8d Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 28 Feb 2024 11:34:41 +0100 Subject: [PATCH 124/197] Minor correction. --- partitura/io/exportmei.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index 3e350fee..2c74f5a5 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -171,7 +171,7 @@ def _handle_measure(self, measure, measure_el): voices = np.vectorize(lambda x: x.voice)(note_or_rest_elements) unique_staffs, staff_inverse_map = np.unique(staffs, return_inverse=True) unique_voices_par = np.unique(voices) - voice_staff_map = {v : {"mask": voices == v, "staff": np.bincount(staffs[voices == v], minlength=len(self.num_staves)).argmax()} for v in unique_voices_par} + voice_staff_map = {v : {"mask": voices == v, "staff": np.bincount(staffs[voices == v], minlength=self.num_staves).argmax()} for v in unique_voices_par} for i in range(self.num_staves): staff = i + 1 staff_el = etree.SubElement(measure_el, "staff") @@ -227,7 +227,7 @@ def _handle_note_or_rest(self, note, xml_voice_el): def _handle_rest(self, rest, xml_voice_el): rest_el = etree.SubElement(xml_voice_el, "rest") - if "type" not in rest.symbolic_duration: + if rest.symbolic_duration is None: rest.symbolic_duration = estimate_symbolic_duration(rest.end.t - rest.start.t, div=self.qdivs) duration = SYMBOLIC_TYPES_TO_MEI_DURS[rest.symbolic_duration["type"]] rest_el.set("dur", duration) From a30f2dd85252e2b5cd60573e9c50480299438aca Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 28 Feb 2024 11:38:37 +0100 Subject: [PATCH 125/197] Minor edits. --- partitura/io/exportmei.py | 2 +- partitura/utils/music.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index 2c74f5a5..5c4596d3 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -227,7 +227,7 @@ def _handle_note_or_rest(self, note, xml_voice_el): def _handle_rest(self, rest, xml_voice_el): rest_el = etree.SubElement(xml_voice_el, "rest") - if rest.symbolic_duration is None: + if "type" not in rest.symbolic_duration.keys(): rest.symbolic_duration = estimate_symbolic_duration(rest.end.t - rest.start.t, div=self.qdivs) duration = SYMBOLIC_TYPES_TO_MEI_DURS[rest.symbolic_duration["type"]] rest_el.set("dur", duration) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index c6fd9b02..34b2b691 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -935,11 +935,21 @@ def estimate_symbolic_duration(dur, div, eps=10**-3): """ global DURS, SYM_DURS qdur = dur / div + if qdur == 0: + return i = find_nearest(DURS, qdur) if np.abs(qdur - DURS[i]) < eps: return SYM_DURS[i].copy() else: return None + # NOTE: Guess tuplets (Naive) it doesn't cover composite durations from tied notes. + type = SYM_DURS[i + 3]["type"] + normal_notes = 2 + return { + "type": type, + "actual_notes": math.ceil(normal_notes / qdur), + "normal_notes": normal_notes, + } def to_quarter_tempo(unit, tempo): From 5b3889b4e51196b92536a409595332abd770d7db Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 28 Feb 2024 11:47:59 +0100 Subject: [PATCH 126/197] Updated rest infilling to account for empty staffs within measures. --- partitura/score.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index 31878362..f1ca15b0 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -4980,28 +4980,36 @@ def merge_parts(parts, reassign="voice"): return new_part - - def _fill_rests_within_measure(measure: Measure, part: Part) -> None: start_time = measure.start.t end_time = measure.end.t notes = np.array(list(part.iter_all(GenericNote, start_time, end_time, include_subclasses=True))) - # voc_staff = np.array([[n.voice, n.staff] for n in notes]) + # voc_staff is now transformed to only voice - voc_staff = np.array([n.voice for n in notes]) - un_voc_staff, inverse_map = np.unique(voc_staff, axis=0, return_inverse=True) - for i in range(len(un_voc_staff)): + voc_staff = np.array([[n.voice, n.staff] for n in notes]) + un_voice, inverse_map = np.unique(voc_staff[:, 0], axis=0, return_inverse=True) + # Check if a staff is empty and fill it with rests + unique_staff = np.unique(voc_staff[:, 1]) + if len(unique_staff) < part.number_of_staves: + for staff in range(1, part.number_of_staves + 1): + if staff not in unique_staff: + sym_dur = estimate_symbolic_duration(end_time - start_time, part._quarter_durations[0]) + rest = Rest(symbolic_duration=sym_dur, staff=staff, voice=1) + part.add(rest, start_time, end_time) + # Now we fill the rests for each voice + for i in range(len(un_voice)): note_mask = inverse_map == i notes_per_vocstaff = notes[note_mask] sort_note_start = np.argsort(np.vectorize(lambda x: x.start.t)(notes_per_vocstaff)) sort_note_end = np.argsort(np.vectorize(lambda x: x.end.t)(notes_per_vocstaff)) - # get note with min start.t + # get note with min start.t and fill the rest before it if needed min_start_note = notes_per_vocstaff[sort_note_start[0]] if min_start_note.start.t > start_time: sym_dur = estimate_symbolic_duration(min_start_note.start.t - start_time, part._quarter_durations[0]) rest = Rest(symbolic_duration=sym_dur, staff=min_start_note.staff, voice=min_start_note.voice) part.add(rest, start_time, min_start_note.start.t) + # get note with max end.t and fill the rest after it if needed min_end_note = notes_per_vocstaff[sort_note_end[-1]] if min_end_note.end.t < end_time: sym_dur = estimate_symbolic_duration(end_time - min_end_note.end.t, part._quarter_durations[0]) @@ -5010,7 +5018,7 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: if len(sort_note_start) <= 1: continue - + # fill the rests between notes if needed (i.e. if there is a gap between notes) for i in range(1, len(sort_note_start)): if notes_per_vocstaff[sort_note_start[i]].start.t > notes_per_vocstaff[sort_note_end[i-1]].end.t: sym_dur = estimate_symbolic_duration(notes_per_vocstaff[sort_note_start[i]].start.t - notes_per_vocstaff[sort_note_end[i-1]].end.t, part._quarter_durations[0]) From fb32a7936c574c1b5f1b55178e49810b2762bec3 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 28 Feb 2024 11:52:45 +0100 Subject: [PATCH 127/197] Update for rest infilling in empty staff within measure to correct voice. --- partitura/score.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/score.py b/partitura/score.py index f1ca15b0..037e31bb 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -4994,7 +4994,7 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: for staff in range(1, part.number_of_staves + 1): if staff not in unique_staff: sym_dur = estimate_symbolic_duration(end_time - start_time, part._quarter_durations[0]) - rest = Rest(symbolic_duration=sym_dur, staff=staff, voice=1) + rest = Rest(symbolic_duration=sym_dur, staff=staff, voice=un_voice.max() + 1) part.add(rest, start_time, end_time) # Now we fill the rests for each voice for i in range(len(un_voice)): From d7dddf7e9da00a52ca0a2b1239bfc34cfe1451cc Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 28 Feb 2024 12:18:11 +0100 Subject: [PATCH 128/197] add cadence label to Roman numeral label if available at the same time position. --- partitura/io/exportmei.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index 5c4596d3..f615f0eb 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -423,16 +423,23 @@ def _handle_harmony(self, measure_el, start, end): harm_el.text = harmony.text for harmony in self.part.iter_all(spt.Cadence, start=start, end=end): - harm_el = etree.SubElement(measure_el, "harm") - harm_el.set(XMLNS_ID, "harm-" + self.elc_id()) - harm_el.set("staff", str(self.part.number_of_staves)) - harm_el.set( - "tstamp", - str(np.diff(self.part.quarter_map([start, harmony.start.t]))[0] + 1), - ) - harm_el.set("place", "below") - # text is a child element of harmony but not a xml element - harm_el.text = "|" + harmony.text + # if there is already a harmony at the same position, add the cadence to the text of the harmony + harm_els = measure_el.xpath(f".//harm[@tstamp='{harmony.start.t}']") + if len(harm_els) > 0: + harm_el = harm_els[0] + harm_el.text += " |" + harmony.text + else: + + harm_el = etree.SubElement(measure_el, "harm") + harm_el.set(XMLNS_ID, "harm-" + self.elc_id()) + harm_el.set("staff", str(self.part.number_of_staves)) + harm_el.set( + "tstamp", + str(np.diff(self.part.quarter_map([start, harmony.start.t]))[0] + 1), + ) + harm_el.set("place", "below") + # text is a child element of harmony but not a xml element + harm_el.text = "|" + harmony.text @deprecated_alias(parts="score_data") From 4fefd098ffe02c6093e8e31d567ce720124517aa Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 28 Feb 2024 12:21:42 +0100 Subject: [PATCH 129/197] add cadence label to Roman numeral label if available at the same time position corrected. --- 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 f615f0eb..f15cc572 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -424,7 +424,7 @@ def _handle_harmony(self, measure_el, start, end): for harmony in self.part.iter_all(spt.Cadence, start=start, end=end): # if there is already a harmony at the same position, add the cadence to the text of the harmony - harm_els = measure_el.xpath(f".//harm[@tstamp='{harmony.start.t}']") + harm_els = measure_el.xpath(f".//harm[@tstamp='{np.diff(self.part.quarter_map([start, harmony.start.t]))[0] + 1}']") if len(harm_els) > 0: harm_el = harm_els[0] harm_el.text += " |" + harmony.text From 68083d373fa220bd30a602a239702eff2113a2f9 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 28 Feb 2024 14:50:56 +0100 Subject: [PATCH 130/197] Minor edits for tuplet export starting with rest or including chords. --- partitura/io/exportmei.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index f15cc572..0e4d97cd 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -233,7 +233,9 @@ def _handle_rest(self, rest, xml_voice_el): rest_el.set("dur", duration) if "dots" in rest.symbolic_duration: rest_el.set("dots", str(rest.symbolic_duration["dots"])) - rest_el.set(XMLNS_ID, "rest-" + self.elc_id()) + if rest.id is None: + rest.id = "rest-" + self.elc_id() + rest_el.set(XMLNS_ID, rest.id) return duration def _handle_note(self, note, xml_voice_el): @@ -277,6 +279,9 @@ def _handle_tuplets(self, measure_el, start, end): start_note_el = measure_el.xpath(f".//*[@xml:id='{start_note.id}']")[0] # Find the note element corresponding to the end note i.e. has the same id value end_note_el = measure_el.xpath(f".//*[@xml:id='{end_note.id}']")[0] + # if start or note element parents are chords, tuplet element should be added as parent of the chord element + start_note_el = start_note_el.getparent() if start_note_el.getparent().tag == "chord" else start_note_el + end_note_el = end_note_el.getparent() if end_note_el.getparent().tag == "chord" else end_note_el # Create the tuplet element as parent of the start and end note elements # Make it start at the same index as the start note element tuplet_el = etree.Element("tuplet") From 3af8604b69268862ec1ee87f1c0350bd3ad9eef8 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 28 Feb 2024 16:24:05 +0100 Subject: [PATCH 131/197] Minor updates on infer beaming for musical beats. --- partitura/score.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index 037e31bb..f60d97f4 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -5130,14 +5130,15 @@ def infer_beaming(part: ScoreLike): # get the beat ends of the notes beat_end = beat_ends[mask] # get notes - mus_beats = na_vocstaff["ts_mus_beats"] * (na_vocstaff["ts_beat_type"] < 4) + beat_multiplier = 4 / na_vocstaff["ts_beat_type"] + mus_beats = na_vocstaff["ts_beats"] / na_vocstaff["ts_mus_beats"] * (na_vocstaff["ts_beat_type"] > 4) mus_beats = np.where(mus_beats == 0, 1, mus_beats) max_mus_beat = mus_beats.max() beam_start_mask = np.isclose(np.mod(na_vocstaff["onset_beat"], mus_beats), 0.0) & (na_vocstaff[ - "duration_beat"] <= 0.5) + "duration_beat"] * beat_multiplier <= 0.5) beam_end_mask = np.isclose(np.mod(beat_end, mus_beats), 0.0) & (na_vocstaff[ - "duration_beat"] <= 0.5) - beam_between = (na_vocstaff["duration_beat"] <= 0.5) & ~beam_start_mask & ~beam_end_mask + "duration_beat"] * beat_multiplier <= 0.5) + beam_between = (na_vocstaff["duration_beat"] * beat_multiplier <= 0.5) & ~beam_start_mask & ~beam_end_mask id_beam_start = na_vocstaff["id"][beam_start_mask] id_beam_end = na_vocstaff["id"][beam_end_mask] id_beam_between = na_vocstaff["id"][beam_between] From db0e713baeda896f805b25562e54420ef6c0b74b Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 1 Mar 2024 16:10:01 +0100 Subject: [PATCH 132/197] added support for repeat, ending and fermata. --- partitura/io/exportmei.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index 0e4d97cd..62a99bae 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -87,6 +87,7 @@ def export_to_mei(self): score_def.set(XMLNS_ID, "scoredef-" + self.elc_id()) staff_grp = etree.SubElement(score_def, "staffGrp") staff_grp.set(XMLNS_ID, "staffgrp-" + self.elc_id()) + staff_grp.set("bar.thru", "true") self._handle_staffs(staff_grp) section = etree.SubElement(score, "section") @@ -209,6 +210,8 @@ def _handle_measure(self, measure, measure_el): self._handle_ks_changes(measure_el, start=measure.start.t, end=measure.end.t) self._handle_ts_changes(measure_el, start=measure.start.t, end=measure.end.t) 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) return measure_el def _handle_chord(self, chord, xml_voice_el): @@ -341,7 +344,6 @@ 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): for clef in self.part.iter_all(spt.Clef, start=start, end=end): # Clef element is parent of the note element @@ -446,6 +448,27 @@ 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): + for fermata in self.part.iter_all(spt.Fermata, start=start, end=end): + if fermata.ref is not None: + note = fermata.ref + note_el = measure_el.xpath(f".//*[@xml:id='{note.id}']")[0] + note_el.set("fermata", "above") + else: + fermata_el = etree.SubElement(measure_el, "fermata") + fermata_el.set(XMLNS_ID, "fermata-" + self.elc_id()) + fermata_el.set("tstamp", str(np.diff(self.part.quarter_map([start, fermata.start.t]))[0] + 1)) + # Set the fermata to be above the staff (the highest staff) + fermata_el.set("staff", "1") + + def _handle_barline(self, measure_el, start, end): + for end_barline in self.part.iter_all(spt.Ending, start=end, end=end+1): + measure_el.set("right", "end") + for end_repeat in self.part.iter_all(spt.Repeat, start=end, end=end+1, mode="ending"): + measure_el.set("right", "rptend") + for start_repeat in self.part.iter_all(spt.Repeat, start=start, end=start+1, mode="starting"): + measure_el.set("left", "rptstart") + @deprecated_alias(parts="score_data") def save_mei( From f6d6d2c7afc2eb4076a5f5874dcccaf2b8c317e5 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 1 Mar 2024 16:12:48 +0100 Subject: [PATCH 133/197] correction for ending barline. --- 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 62a99bae..4dfdfec1 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -462,7 +462,7 @@ def _handle_fermata(self, measure_el, start, end): fermata_el.set("staff", "1") def _handle_barline(self, measure_el, start, end): - for end_barline in self.part.iter_all(spt.Ending, start=end, end=end+1): + for end_barline in self.part.iter_all(spt.Ending, start=end, end=end+1, mode="ending"): measure_el.set("right", "end") for end_repeat in self.part.iter_all(spt.Repeat, start=end, end=end+1, mode="ending"): measure_el.set("right", "rptend") From ebd05da669159f3730f65bee20ec89b19f00f6fa Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 1 Mar 2024 16:18:43 +0100 Subject: [PATCH 134/197] Added title support for export. --- partitura/io/exportmei.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index 4dfdfec1..06952797 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -42,10 +42,11 @@ class MEIExporter: - def __init__(self, part): + def __init__(self, part, title=None): self.part = part self.qdivs = part._quarter_durations[0] self.num_staves = part.number_of_staves + self.title = title self.element_counter = 0 self.current_key_signature = [] self.flats = ["bf", "ef", "af", "df", "gf", "cf", "ff"] @@ -77,7 +78,10 @@ def export_to_mei(self): # write the title title_stmt = etree.SubElement(file_desc, "titleStmt") title = etree.SubElement(title_stmt, "title") - title.text = self.part.id if self.part.id is not None else "Untitled" + if self.title is not None: + title.text = self.title + else: + title.text = self.part.id if self.part.id is not None else "Untitled" music = etree.SubElement(mei, "music") body = etree.SubElement(music, "body") mdiv = etree.SubElement(body, "mdiv") @@ -468,12 +472,16 @@ def _handle_barline(self, measure_el, start, end): measure_el.set("right", "rptend") for start_repeat in self.part.iter_all(spt.Repeat, start=start, end=start+1, mode="starting"): measure_el.set("left", "rptstart") + for end_barline in self.part.iter_all(spt.Barline, start=end, end=end+1, mode="starting"): + if end_barline.style == "double": + measure_el.set("right", "end") @deprecated_alias(parts="score_data") def save_mei( score_data: spt.ScoreLike, out: Optional[PathLike] = None, + title: Optional[str] = None, ) -> Optional[str]: """ Save a one or more Part or PartGroup instances in MEI format. @@ -506,7 +514,7 @@ def save_mei( score_data = parts[0] - exporter = MEIExporter(score_data) + exporter = MEIExporter(score_data, title=title) root = exporter.export_to_mei() if out: From 09785e67a37e552c4d4b204f6ab0b4e680ac8c23 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 1 Mar 2024 16:21:08 +0100 Subject: [PATCH 135/197] Correction for ending barline. --- 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 06952797..b01579b8 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -473,7 +473,7 @@ def _handle_barline(self, measure_el, start, end): for start_repeat in self.part.iter_all(spt.Repeat, start=start, end=start+1, mode="starting"): measure_el.set("left", "rptstart") for end_barline in self.part.iter_all(spt.Barline, start=end, end=end+1, mode="starting"): - if end_barline.style == "double": + if end_barline.style == "light-heavy": measure_el.set("right", "end") From ec970573d18eec5702c2f6ff296fdd6228e85a08 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 1 Mar 2024 16:22:09 +0100 Subject: [PATCH 136/197] Correction for ending barline. --- partitura/io/exportmei.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index b01579b8..9158a5dd 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -468,13 +468,13 @@ def _handle_fermata(self, measure_el, start, end): def _handle_barline(self, measure_el, start, end): for end_barline in self.part.iter_all(spt.Ending, start=end, end=end+1, mode="ending"): measure_el.set("right", "end") + for end_barline in self.part.iter_all(spt.Barline, start=end, end=end+1, mode="starting"): + if end_barline.style == "light-heavy": + measure_el.set("right", "end") for end_repeat in self.part.iter_all(spt.Repeat, start=end, end=end+1, mode="ending"): measure_el.set("right", "rptend") for start_repeat in self.part.iter_all(spt.Repeat, start=start, end=start+1, mode="starting"): measure_el.set("left", "rptstart") - for end_barline in self.part.iter_all(spt.Barline, start=end, end=end+1, mode="starting"): - if end_barline.style == "light-heavy": - measure_el.set("right", "end") @deprecated_alias(parts="score_data") From 13298b7c107813247b761e04410f7af5c457c1d1 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 1 Mar 2024 16:29:20 +0100 Subject: [PATCH 137/197] minor styling change. --- partitura/io/exportmei.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index 9158a5dd..be6c3e12 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -93,10 +93,8 @@ def export_to_mei(self): staff_grp.set(XMLNS_ID, "staffgrp-" + self.elc_id()) staff_grp.set("bar.thru", "true") self._handle_staffs(staff_grp) - section = etree.SubElement(score, "section") section.set(XMLNS_ID, "section-" + self.elc_id()) - # Iterate over part's timeline for measure in self.part.measures: # Create measure element From 34245579a363403f14bbc6b2b61665dfc2f4b1df Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 4 Mar 2024 12:36:29 +0100 Subject: [PATCH 138/197] import musicxml addition of writen accidentals. --- partitura/io/importmusicxml.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index 1c7461d1..7eb88674 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -78,6 +78,13 @@ OCTAVE_SHIFTS = {8: 1, 15: 2, 22: 3} +ACCIDENTAL_MAP = { + "sharp": 1, + "natural": 0, + "flat": -1, + "double-sharp": 2, + "double-flat": -2, +} def validate_musicxml(xml, debug=False): """ @@ -1236,6 +1243,11 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non step = get_value_from_tag(pitch, "step", str) alter = get_value_from_tag(pitch, "alter", int) octave = get_value_from_tag(pitch, "octave", int) + # When step is none check for accidental attribute + if alter is None: + alter = get_value_from_tag(e, "accidental", str) + if alter is not None: + alter = ACCIDENTAL_MAP[alter] grace = e.find("grace") From 8de77aab3c46ef41fe564d66d9509ee190781405 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 9 Mar 2024 19:20:05 +0100 Subject: [PATCH 139/197] minor corrections. --- partitura/score.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/partitura/score.py b/partitura/score.py index f60d97f4..4d3edae8 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -13,7 +13,7 @@ from collections import defaultdict from collections.abc import Iterable from numbers import Number - +import re # import copy from partitura.utils.music import MUSICAL_BEATS, INTERVALCLASSES import warnings, sys @@ -567,6 +567,18 @@ def rests(self): """ return [e for e in self.iter_all(Rest, include_subclasses=False)] + @property + def cadences(self): + """Return a list of all cadence objects in the part + + Returns + ------- + list + List of Cadence objects + + """ + return [e for e in self.iter_all(Cadence)] + @property def repeats(self): """Return a list of all Repeat objects in the part From c074a5891fa3920a249277a3cd5db5456e1f2977 Mon Sep 17 00:00:00 2001 From: manoskary Date: Mon, 11 Mar 2024 14:25:19 +0000 Subject: [PATCH 140/197] Format code with black (bot) --- partitura/io/exportmei.py | 66 ++++++++++++--- partitura/io/importmusicxml.py | 1 + partitura/score.py | 147 +++++++++++++++++++++++++-------- 3 files changed, 167 insertions(+), 47 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index be6c3e12..a16261e5 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -146,7 +146,9 @@ def _handle_staffs(self, xml_el): # Find the pname from the number of sharps or flats and the mode ks_def.set( "pname", - fifths_mode_to_key_name(keys_sig.fifths, keys_sig.mode).lower()[0], # only the first letter + fifths_mode_to_key_name(keys_sig.fifths, keys_sig.mode).lower()[ + 0 + ], # only the first letter ) if time_sig is not None: @@ -174,7 +176,15 @@ def _handle_measure(self, measure, measure_el): voices = np.vectorize(lambda x: x.voice)(note_or_rest_elements) unique_staffs, staff_inverse_map = np.unique(staffs, return_inverse=True) unique_voices_par = np.unique(voices) - voice_staff_map = {v : {"mask": voices == v, "staff": np.bincount(staffs[voices == v], minlength=self.num_staves).argmax()} for v in unique_voices_par} + voice_staff_map = { + v: { + "mask": voices == v, + "staff": np.bincount( + staffs[voices == v], minlength=self.num_staves + ).argmax(), + } + for v in unique_voices_par + } for i in range(self.num_staves): staff = i + 1 staff_el = etree.SubElement(measure_el, "staff") @@ -233,7 +243,9 @@ def _handle_note_or_rest(self, note, xml_voice_el): def _handle_rest(self, rest, xml_voice_el): rest_el = etree.SubElement(xml_voice_el, "rest") if "type" not in rest.symbolic_duration.keys(): - rest.symbolic_duration = estimate_symbolic_duration(rest.end.t - rest.start.t, div=self.qdivs) + rest.symbolic_duration = estimate_symbolic_duration( + rest.end.t - rest.start.t, div=self.qdivs + ) duration = SYMBOLIC_TYPES_TO_MEI_DURS[rest.symbolic_duration["type"]] rest_el.set("dur", duration) if "dots" in rest.symbolic_duration: @@ -265,7 +277,10 @@ def _handle_note(self, note, xml_voice_el): note_el.set("tie", "t") if note.alter is not None: - if note.step.lower() + ALTER_TO_MEI[note.alter] in self.current_key_signature: + if ( + note.step.lower() + ALTER_TO_MEI[note.alter] + in self.current_key_signature + ): note_el.set("accid.ges", ALTER_TO_MEI[note.alter]) else: accidental = etree.SubElement(note_el, "accid") @@ -285,8 +300,16 @@ def _handle_tuplets(self, measure_el, start, end): # Find the note element corresponding to the end note i.e. has the same id value end_note_el = measure_el.xpath(f".//*[@xml:id='{end_note.id}']")[0] # if start or note element parents are chords, tuplet element should be added as parent of the chord element - start_note_el = start_note_el.getparent() if start_note_el.getparent().tag == "chord" else start_note_el - end_note_el = end_note_el.getparent() if end_note_el.getparent().tag == "chord" else end_note_el + start_note_el = ( + start_note_el.getparent() + if start_note_el.getparent().tag == "chord" + else start_note_el + ) + end_note_el = ( + end_note_el.getparent() + if end_note_el.getparent().tag == "chord" + else end_note_el + ) # Create the tuplet element as parent of the start and end note elements # Make it start at the same index as the start note element tuplet_el = etree.Element("tuplet") @@ -433,7 +456,9 @@ def _handle_harmony(self, measure_el, start, end): for harmony in self.part.iter_all(spt.Cadence, start=start, end=end): # if there is already a harmony at the same position, add the cadence to the text of the harmony - harm_els = measure_el.xpath(f".//harm[@tstamp='{np.diff(self.part.quarter_map([start, harmony.start.t]))[0] + 1}']") + harm_els = measure_el.xpath( + f".//harm[@tstamp='{np.diff(self.part.quarter_map([start, harmony.start.t]))[0] + 1}']" + ) if len(harm_els) > 0: harm_el = harm_els[0] harm_el.text += " |" + harmony.text @@ -444,7 +469,9 @@ def _handle_harmony(self, measure_el, start, end): harm_el.set("staff", str(self.part.number_of_staves)) harm_el.set( "tstamp", - str(np.diff(self.part.quarter_map([start, harmony.start.t]))[0] + 1), + str( + np.diff(self.part.quarter_map([start, harmony.start.t]))[0] + 1 + ), ) harm_el.set("place", "below") # text is a child element of harmony but not a xml element @@ -459,19 +486,32 @@ def _handle_fermata(self, measure_el, start, end): else: fermata_el = etree.SubElement(measure_el, "fermata") fermata_el.set(XMLNS_ID, "fermata-" + self.elc_id()) - fermata_el.set("tstamp", str(np.diff(self.part.quarter_map([start, fermata.start.t]))[0] + 1)) + fermata_el.set( + "tstamp", + str( + np.diff(self.part.quarter_map([start, fermata.start.t]))[0] + 1 + ), + ) # Set the fermata to be above the staff (the highest staff) fermata_el.set("staff", "1") def _handle_barline(self, measure_el, start, end): - for end_barline in self.part.iter_all(spt.Ending, start=end, end=end+1, mode="ending"): + for end_barline in self.part.iter_all( + spt.Ending, start=end, end=end + 1, mode="ending" + ): measure_el.set("right", "end") - for end_barline in self.part.iter_all(spt.Barline, start=end, end=end+1, mode="starting"): + for end_barline in self.part.iter_all( + spt.Barline, start=end, end=end + 1, mode="starting" + ): if end_barline.style == "light-heavy": measure_el.set("right", "end") - for end_repeat in self.part.iter_all(spt.Repeat, start=end, end=end+1, mode="ending"): + for end_repeat in self.part.iter_all( + spt.Repeat, start=end, end=end + 1, mode="ending" + ): measure_el.set("right", "rptend") - for start_repeat in self.part.iter_all(spt.Repeat, start=start, end=start+1, mode="starting"): + for start_repeat in self.part.iter_all( + spt.Repeat, start=start, end=start + 1, mode="starting" + ): measure_el.set("left", "rptstart") diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index 7eb88674..1b3a8b95 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -86,6 +86,7 @@ "double-flat": -2, } + def validate_musicxml(xml, debug=False): """ Validate an XML file against an XSD. diff --git a/partitura/score.py b/partitura/score.py index 4d3edae8..582925b2 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -14,6 +14,7 @@ from collections.abc import Iterable from numbers import Number import re + # import copy from partitura.utils.music import MUSICAL_BEATS, INTERVALCLASSES import warnings, sys @@ -1594,7 +1595,7 @@ def __init__( articulations=None, ornaments=None, doc_order=None, - **kwargs + **kwargs, ): self._sym_dur = None super().__init__(**kwargs) @@ -2713,6 +2714,7 @@ def __str__(self): class Cadence(TimedObject): """A cadence element in the score usually for Cadences.""" + def __init__(self, text, local_key=None): super().__init__() self.text = text @@ -2724,7 +2726,7 @@ def _filter_cadence_type(self): # capitalize text self.text = self.text.upper() # Filter alphabet characters only. - self.text = re.findall(r'[A-Z]+', self.text)[0] + self.text = re.findall(r"[A-Z]+", self.text)[0] self.text = "IAC" if "IAC" in self.text else self.text if self.text not in ["PAC", "IAC", "HC", "DC", "EC", "PC"]: warnings.warn(f"Cadence type {self.text} not found. Setting to None") @@ -4995,7 +4997,9 @@ def merge_parts(parts, reassign="voice"): def _fill_rests_within_measure(measure: Measure, part: Part) -> None: start_time = measure.start.t end_time = measure.end.t - notes = np.array(list(part.iter_all(GenericNote, start_time, end_time, include_subclasses=True))) + notes = np.array( + list(part.iter_all(GenericNote, start_time, end_time, include_subclasses=True)) + ) # voc_staff is now transformed to only voice voc_staff = np.array([[n.voice, n.staff] for n in notes]) @@ -5005,61 +5009,114 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: if len(unique_staff) < part.number_of_staves: for staff in range(1, part.number_of_staves + 1): if staff not in unique_staff: - sym_dur = estimate_symbolic_duration(end_time - start_time, part._quarter_durations[0]) - rest = Rest(symbolic_duration=sym_dur, staff=staff, voice=un_voice.max() + 1) + sym_dur = estimate_symbolic_duration( + end_time - start_time, part._quarter_durations[0] + ) + rest = Rest( + symbolic_duration=sym_dur, staff=staff, voice=un_voice.max() + 1 + ) part.add(rest, start_time, end_time) # Now we fill the rests for each voice for i in range(len(un_voice)): note_mask = inverse_map == i notes_per_vocstaff = notes[note_mask] - sort_note_start = np.argsort(np.vectorize(lambda x: x.start.t)(notes_per_vocstaff)) + sort_note_start = np.argsort( + np.vectorize(lambda x: x.start.t)(notes_per_vocstaff) + ) sort_note_end = np.argsort(np.vectorize(lambda x: x.end.t)(notes_per_vocstaff)) # get note with min start.t and fill the rest before it if needed min_start_note = notes_per_vocstaff[sort_note_start[0]] if min_start_note.start.t > start_time: - sym_dur = estimate_symbolic_duration(min_start_note.start.t - start_time, part._quarter_durations[0]) - rest = Rest(symbolic_duration=sym_dur, staff=min_start_note.staff, voice=min_start_note.voice) + sym_dur = estimate_symbolic_duration( + min_start_note.start.t - start_time, part._quarter_durations[0] + ) + rest = Rest( + symbolic_duration=sym_dur, + staff=min_start_note.staff, + voice=min_start_note.voice, + ) part.add(rest, start_time, min_start_note.start.t) # get note with max end.t and fill the rest after it if needed min_end_note = notes_per_vocstaff[sort_note_end[-1]] if min_end_note.end.t < end_time: - sym_dur = estimate_symbolic_duration(end_time - min_end_note.end.t, part._quarter_durations[0]) - rest = Rest(symbolic_duration=sym_dur, staff=min_end_note.staff, voice=min_end_note.voice) + sym_dur = estimate_symbolic_duration( + end_time - min_end_note.end.t, part._quarter_durations[0] + ) + rest = Rest( + symbolic_duration=sym_dur, + staff=min_end_note.staff, + voice=min_end_note.voice, + ) part.add(rest, min_end_note.end.t, end_time) if len(sort_note_start) <= 1: continue # fill the rests between notes if needed (i.e. if there is a gap between notes) for i in range(1, len(sort_note_start)): - if notes_per_vocstaff[sort_note_start[i]].start.t > notes_per_vocstaff[sort_note_end[i-1]].end.t: - sym_dur = estimate_symbolic_duration(notes_per_vocstaff[sort_note_start[i]].start.t - notes_per_vocstaff[sort_note_end[i-1]].end.t, part._quarter_durations[0]) - rest = Rest(symbolic_duration=sym_dur, staff=notes_per_vocstaff[sort_note_end[i-1]].staff, voice=notes_per_vocstaff[sort_note_end[i-1]].voice) - part.add(rest, notes_per_vocstaff[sort_note_end[i-1]].end.t, notes_per_vocstaff[sort_note_start[i]].start.t) + if ( + notes_per_vocstaff[sort_note_start[i]].start.t + > notes_per_vocstaff[sort_note_end[i - 1]].end.t + ): + sym_dur = estimate_symbolic_duration( + notes_per_vocstaff[sort_note_start[i]].start.t + - notes_per_vocstaff[sort_note_end[i - 1]].end.t, + part._quarter_durations[0], + ) + rest = Rest( + symbolic_duration=sym_dur, + staff=notes_per_vocstaff[sort_note_end[i - 1]].staff, + voice=notes_per_vocstaff[sort_note_end[i - 1]].voice, + ) + part.add( + rest, + notes_per_vocstaff[sort_note_end[i - 1]].end.t, + notes_per_vocstaff[sort_note_start[i]].start.t, + ) -def _fill_rests_global(measure: Measure, part: Part, unique_voc_staff: np.ndarray) -> None: +def _fill_rests_global( + measure: Measure, part: Part, unique_voc_staff: np.ndarray +) -> None: start_time = measure.start.t end_time = measure.end.t if end_time - start_time == 0: return - notes = np.array(list(part.iter_all(GenericNote, start_time, end_time, include_subclasses=True))) + notes = np.array( + list(part.iter_all(GenericNote, start_time, end_time, include_subclasses=True)) + ) voc_staff = np.array([[n.voice, n.staff] for n in notes]) un_voc_staff, inverse_map = np.unique(voc_staff, axis=0, return_inverse=True) for i in range(un_voc_staff.shape[0]): note_mask = inverse_map == i notes_per_vocstaff = notes[note_mask] # get note with min start.t - min_start_note = notes_per_vocstaff[np.argmin(np.vectorize(lambda x: x.start.t)(notes_per_vocstaff))] + min_start_note = notes_per_vocstaff[ + np.argmin(np.vectorize(lambda x: x.start.t)(notes_per_vocstaff)) + ] if min_start_note.start.t > start_time: - sym_dur = estimate_symbolic_duration(min_start_note.start.t - start_time, part._quarter_durations[0]) - rest = Rest(symbolic_duration=sym_dur, staff=min_start_note.staff, voice=min_start_note.voice) + sym_dur = estimate_symbolic_duration( + min_start_note.start.t - start_time, part._quarter_durations[0] + ) + rest = Rest( + symbolic_duration=sym_dur, + staff=min_start_note.staff, + voice=min_start_note.voice, + ) part.add(rest, start_time, min_start_note.start.t) - min_end_note = notes_per_vocstaff[np.argmax(np.vectorize(lambda x: x.end.t)(notes_per_vocstaff))] + min_end_note = notes_per_vocstaff[ + np.argmax(np.vectorize(lambda x: x.end.t)(notes_per_vocstaff)) + ] if min_end_note.end.t < end_time: - sym_dur = estimate_symbolic_duration(end_time - min_end_note.end.t, part._quarter_durations[0]) - rest = Rest(symbolic_duration=sym_dur, staff=min_end_note.staff, voice=min_end_note.voice) + sym_dur = estimate_symbolic_duration( + end_time - min_end_note.end.t, part._quarter_durations[0] + ) + rest = Rest( + symbolic_duration=sym_dur, + staff=min_end_note.staff, + voice=min_end_note.voice, + ) part.add(rest, min_end_note.end.t, end_time) if un_voc_staff.shape[0] != unique_voc_staff.shape[0]: @@ -5067,12 +5124,16 @@ def _fill_rests_global(measure: Measure, part: Part, unique_voc_staff: np.ndarra diff = unique_voc_staff else: # View `un_voc_staff` and `unique_voc_staff` as 1-D structured arrays - x_sa = un_voc_staff.view([('', un_voc_staff.dtype)] * un_voc_staff.shape[1]) - y_sa = unique_voc_staff.view([('', unique_voc_staff.dtype)] * unique_voc_staff.shape[1]) + x_sa = un_voc_staff.view([("", un_voc_staff.dtype)] * un_voc_staff.shape[1]) + y_sa = unique_voc_staff.view( + [("", unique_voc_staff.dtype)] * unique_voc_staff.shape[1] + ) # Find rows in `unique_voc_staff` that are not in `un_voc_staff` diff = np.setdiff1d(y_sa, x_sa) for voice, staff in diff: - sym_dur = estimate_symbolic_duration(end_time - start_time, part._quarter_durations[0]) + sym_dur = estimate_symbolic_duration( + end_time - start_time, part._quarter_durations[0] + ) rest = Rest(symbolic_duration=sym_dur, staff=staff, voice=voice) part.add(rest, start_time, end_time) @@ -5103,7 +5164,8 @@ def fill_rests(score_data: ScoreLike, measurewise=True) -> None: else: note_array = part.note_array(include_staff=True) unique_vocstaff = np.unique( - np.array([note_array["voice"], note_array["staff"]], dtype=np.int64), axis=1 + np.array([note_array["voice"], note_array["staff"]], dtype=np.int64), + axis=1, ) for measure in measures: _fill_rests_global(measure, part, unique_vocstaff.T) @@ -5128,7 +5190,11 @@ def infer_beaming(part: ScoreLike): for p in part.parts: infer_beaming(p) else: - note_array = part.note_array(include_metrical_position=True, include_staff=True, include_time_signature=True) + note_array = part.note_array( + include_metrical_position=True, + include_staff=True, + include_time_signature=True, + ) beat_ends = note_array["onset_beat"] + note_array["duration_beat"] # split note_array into groups based on staff and voice # unique_vocstaff = np.unique(note_array[['voice', 'staff']], axis=0) @@ -5143,14 +5209,24 @@ def infer_beaming(part: ScoreLike): beat_end = beat_ends[mask] # get notes beat_multiplier = 4 / na_vocstaff["ts_beat_type"] - mus_beats = na_vocstaff["ts_beats"] / na_vocstaff["ts_mus_beats"] * (na_vocstaff["ts_beat_type"] > 4) + mus_beats = ( + na_vocstaff["ts_beats"] + / na_vocstaff["ts_mus_beats"] + * (na_vocstaff["ts_beat_type"] > 4) + ) mus_beats = np.where(mus_beats == 0, 1, mus_beats) max_mus_beat = mus_beats.max() - beam_start_mask = np.isclose(np.mod(na_vocstaff["onset_beat"], mus_beats), 0.0) & (na_vocstaff[ - "duration_beat"] * beat_multiplier <= 0.5) - beam_end_mask = np.isclose(np.mod(beat_end, mus_beats), 0.0) & (na_vocstaff[ - "duration_beat"] * beat_multiplier <= 0.5) - beam_between = (na_vocstaff["duration_beat"] * beat_multiplier <= 0.5) & ~beam_start_mask & ~beam_end_mask + beam_start_mask = np.isclose( + np.mod(na_vocstaff["onset_beat"], mus_beats), 0.0 + ) & (na_vocstaff["duration_beat"] * beat_multiplier <= 0.5) + beam_end_mask = np.isclose(np.mod(beat_end, mus_beats), 0.0) & ( + na_vocstaff["duration_beat"] * beat_multiplier <= 0.5 + ) + beam_between = ( + (na_vocstaff["duration_beat"] * beat_multiplier <= 0.5) + & ~beam_start_mask + & ~beam_end_mask + ) id_beam_start = na_vocstaff["id"][beam_start_mask] id_beam_end = na_vocstaff["id"][beam_end_mask] id_beam_between = na_vocstaff["id"][beam_between] @@ -5168,7 +5244,10 @@ def infer_beaming(part: ScoreLike): if note.beam is not None: continue - if part.beat_map(note.start.t) - part.beat_map(prev_start) > max_mus_beat: + if ( + part.beat_map(note.start.t) - part.beat_map(prev_start) + > max_mus_beat + ): prev_beam = None notes_in_beam = [] prev_start = note.start.t From 6374927972fda7980c1b60e71f03b5690c2c6231 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 15 Mar 2024 17:03:09 +0100 Subject: [PATCH 141/197] update the symbolic duration function. --- partitura/utils/music.py | 48 +++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 34b2b691..87f96456 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -6,12 +6,12 @@ from __future__ import annotations import copy from collections import defaultdict -import re +import re, math import warnings import numpy as np from scipy.interpolate import interp1d from scipy.sparse import csc_matrix -from typing import Union, Callable, Optional, TYPE_CHECKING +from typing import Union, Callable, Optional, TYPE_CHECKING, Tuple, Dict, Any from partitura.utils.generic import find_nearest, search, iter_current_next import partitura from tempfile import TemporaryDirectory @@ -171,6 +171,8 @@ class MIDITokenizer(object): ] ) + + SYM_DURS = [ {"type": "256th", "dots": 0}, {"type": "256th", "dots": 1}, @@ -319,6 +321,16 @@ class MIDITokenizer(object): # Standard tuning frequency of A4 in Hz A4 = 440.0 +COMPOSITE_DURS = np.array( + [1 + 4/32, 2+1/16, 2+1/32] +) + +SYM_COMPOSITE_DURS = [ + ({"type": "quarter", "dots": 0}, {"type": "32nd", "dots": 1}), + ({"type": "half", "dots": 1}, {"type": "16th", "dots": 0}), + ({"type": "half", "dots": 1}, {"type": "32nd", "dots": 0}) + ] + def ensure_notearray(notearray_or_part, *args, **kwargs): """ @@ -894,7 +906,7 @@ def key_int_to_mode(mode): raise ValueError("Unknown mode {}".format(mode)) -def estimate_symbolic_duration(dur, div, eps=10**-3): +def estimate_symbolic_duration(dur, div, eps=10**-3) -> Union[Union[Dict[str, Any], Tuple[Dict[str, Any]]], None]: """Given a numeric duration, a divisions value (specifiying the number of units per quarter note) and optionally a tolerance `eps` for numerical imprecisions, estimate corresponding the symbolic @@ -918,7 +930,10 @@ def estimate_symbolic_duration(dur, div, eps=10**-3): Returns ------- - + out: Union[Union[Dict[str, Any], Tuple[Dict[str, Any]]], None] + Symbolic duration as a dictionary, or None if no matching + duration is found. When a composite duration is found, then it returns a tuple of symbolic durations. + The returned tuple should be tied notes. Examples -------- @@ -941,15 +956,22 @@ def estimate_symbolic_duration(dur, div, eps=10**-3): if np.abs(qdur - DURS[i]) < eps: return SYM_DURS[i].copy() else: - return None - # NOTE: Guess tuplets (Naive) it doesn't cover composite durations from tied notes. - type = SYM_DURS[i + 3]["type"] - normal_notes = 2 - return { - "type": type, - "actual_notes": math.ceil(normal_notes / qdur), - "normal_notes": normal_notes, - } + # Note when the duration is not found, the we are left with two solutions: + # 1. The duration is a tuplet + # 2. The duration is a composite duration + # For composite duration. We can use the following approach: + i = find_nearest(COMPOSITE_DURS, qdur) + if np.abs(qdur - COMPOSITE_DURS[i]) < eps: + return SYM_DURS[i].copy() + else: + # NOTE: Guess tuplets (Naive) it doesn't cover composite durations from tied notes. + type = SYM_DURS[i + 3]["type"] + normal_notes = 2 + return { + "type": type, + "actual_notes": math.ceil(normal_notes / qdur), + "normal_notes": normal_notes, + } def to_quarter_tempo(unit, tempo): From 3c34fbc77610350590ce4fddc385ccf3e1f4bafd Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 15 Mar 2024 17:15:25 +0100 Subject: [PATCH 142/197] update the rest infilling function for composite rest durations. --- partitura/score.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index 582925b2..3394871a 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -5009,13 +5009,23 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: if len(unique_staff) < part.number_of_staves: for staff in range(1, part.number_of_staves + 1): if staff not in unique_staff: + # solution when estimation returns composite durations. sym_dur = estimate_symbolic_duration( - end_time - start_time, part._quarter_durations[0] + end_time - start_time, part._quarter_durations[0], return_com_durations=True ) - rest = Rest( - symbolic_duration=sym_dur, staff=staff, voice=un_voice.max() + 1 - ) - part.add(rest, start_time, end_time) + if isinstance(sym_dur, tuple): + for i, sd in enumerate(sym_dur): + end_time = start_time + symbolic_to_numeric_duration(sd, part._quarter_durations[0]) + rest = Rest( + symbolic_duration=sd, staff=staff, voice=un_voice.max() + 1 + ) + part.add(rest, start_time, end_time) + start_time = end_time + else: + rest = Rest( + symbolic_duration=sym_dur, staff=staff, voice=un_voice.max() + 1 + ) + part.add(rest, start_time, end_time) # Now we fill the rests for each voice for i in range(len(un_voice)): note_mask = inverse_map == i From c3026b8ee38b77ee7a449cec8175d89a8b9c739e Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 15 Mar 2024 17:15:45 +0100 Subject: [PATCH 143/197] Makes composite duration an option for duration estimation. --- partitura/utils/music.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 87f96456..a8933eb7 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -906,7 +906,7 @@ def key_int_to_mode(mode): raise ValueError("Unknown mode {}".format(mode)) -def estimate_symbolic_duration(dur, div, eps=10**-3) -> Union[Union[Dict[str, Any], Tuple[Dict[str, Any]]], None]: +def estimate_symbolic_duration(dur, div, eps=10**-3, return_com_durations=False) -> Union[Union[Dict[str, Any], Tuple[Dict[str, Any]]], None]: """Given a numeric duration, a divisions value (specifiying the number of units per quarter note) and optionally a tolerance `eps` for numerical imprecisions, estimate corresponding the symbolic @@ -927,6 +927,8 @@ def estimate_symbolic_duration(dur, div, eps=10**-3) -> Union[Union[Dict[str, An Number of units per quarter note eps : float, optional (default: 10**-3) Tolerance in case of imprecise matches + return_com_durations : bool, optional (default: False) + If True, return composite durations as well. Returns ------- @@ -961,7 +963,7 @@ def estimate_symbolic_duration(dur, div, eps=10**-3) -> Union[Union[Dict[str, An # 2. The duration is a composite duration # For composite duration. We can use the following approach: i = find_nearest(COMPOSITE_DURS, qdur) - if np.abs(qdur - COMPOSITE_DURS[i]) < eps: + if np.abs(qdur - COMPOSITE_DURS[i]) < eps and return_com_durations: return SYM_DURS[i].copy() else: # NOTE: Guess tuplets (Naive) it doesn't cover composite durations from tied notes. From d7b1108650c80106fd1e0a9e3ca6b9d48b2e039c Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 19 Mar 2024 17:41:40 +0100 Subject: [PATCH 144/197] Some small correction on exporting tuplets. --- partitura/io/exportmei.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index a16261e5..efff9667 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -295,6 +295,8 @@ def _handle_tuplets(self, measure_el, start, end): for tuplet in self.part.iter_all(spt.Tuplet, start=start, end=end): start_note = tuplet.start_note end_note = tuplet.end_note + if start_note.start.t < start or end_note.end.t > end: + continue # Find the note element corresponding to the start note i.e. has the same id value start_note_el = measure_el.xpath(f".//*[@xml:id='{start_note.id}']")[0] # Find the note element corresponding to the end note i.e. has the same id value @@ -322,6 +324,9 @@ def _handle_tuplets(self, measure_el, start, end): # Find them from the xml tree start_note_index = start_note_el.getparent().index(start_note_el) end_note_index = end_note_el.getparent().index(end_note_el) + # If the start and end note elements are not in order skip (it a weird bug that happens sometimes) + if start_note_index > end_note_index: + continue xml_el_within_tuplet = [ start_note_el.getparent()[i] for i in range(start_note_index, end_note_index + 1) @@ -481,8 +486,9 @@ def _handle_fermata(self, measure_el, start, end): for fermata in self.part.iter_all(spt.Fermata, start=start, end=end): if fermata.ref is not None: note = fermata.ref - note_el = measure_el.xpath(f".//*[@xml:id='{note.id}']")[0] - note_el.set("fermata", "above") + note_el = measure_el.xpath(f".//*[@xml:id='{note.id}']") + if len(note_el) > 0: + note_el[0].set("fermata", "above") else: fermata_el = etree.SubElement(measure_el, "fermata") fermata_el.set(XMLNS_ID, "fermata-" + self.elc_id()) From bd2a7d41723184900b716be8028c887ae68c2986 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 22 Mar 2024 14:32:51 +0100 Subject: [PATCH 145/197] Added some additional composite durations. --- partitura/utils/music.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index a8933eb7..58fe8906 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -322,13 +322,15 @@ class MIDITokenizer(object): A4 = 440.0 COMPOSITE_DURS = np.array( - [1 + 4/32, 2+1/16, 2+1/32] + [1 + 4/16, 1 + 4/32, 2+4/8, 2+4/16, 2+4/32] ) SYM_COMPOSITE_DURS = [ - ({"type": "quarter", "dots": 0}, {"type": "32nd", "dots": 1}), - ({"type": "half", "dots": 1}, {"type": "16th", "dots": 0}), - ({"type": "half", "dots": 1}, {"type": "32nd", "dots": 0}) + ({"type": "quarter", "dots": 0}, {"type": "16nd", "dots": 0}), + ({"type": "quarter", "dots": 0}, {"type": "32nd", "dots": 0}), + ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0}), + ({"type": "half", "dots": 0}, {"type": "16th", "dots": 0}), + ({"type": "half", "dots": 0}, {"type": "32nd", "dots": 0}) ] @@ -962,9 +964,9 @@ def estimate_symbolic_duration(dur, div, eps=10**-3, return_com_durations=False) # 1. The duration is a tuplet # 2. The duration is a composite duration # For composite duration. We can use the following approach: - i = find_nearest(COMPOSITE_DURS, qdur) - if np.abs(qdur - COMPOSITE_DURS[i]) < eps and return_com_durations: - return SYM_DURS[i].copy() + 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]) else: # NOTE: Guess tuplets (Naive) it doesn't cover composite durations from tied notes. type = SYM_DURS[i + 3]["type"] From 3d42ef3c91f0099cf135dc7994785321d2d9df5c Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 22 Mar 2024 14:33:15 +0100 Subject: [PATCH 146/197] Changes for composite durations in fill rests. --- partitura/score.py | 83 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 25 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index 3394871a..b3ba5eda 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -3846,6 +3846,7 @@ def find_tuplets(part): start_note = note_tuplet[0] stop_note = note_tuplet[-1] tuplet = Tuplet(start_note, stop_note) + assert start_note.start.t <= stop_note.start.t, "The start note of a Tuplet should be before the stop note" part.add(tuplet, start_note.start.t, stop_note.end.t) tup_start += actual_notes @@ -5038,27 +5039,48 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: min_start_note = notes_per_vocstaff[sort_note_start[0]] if min_start_note.start.t > start_time: sym_dur = estimate_symbolic_duration( - min_start_note.start.t - start_time, part._quarter_durations[0] - ) - rest = Rest( - symbolic_duration=sym_dur, - staff=min_start_note.staff, - voice=min_start_note.voice, + min_start_note.start.t - start_time, part._quarter_durations[0], return_com_durations=True ) - part.add(rest, start_time, min_start_note.start.t) + # solution when estimation returns composite durations. + if isinstance(sym_dur, tuple): + for i, sd in enumerate(sym_dur): + end_time = start_time + symbolic_to_numeric_duration(sd, part._quarter_durations[0]) + rest = Rest( + symbolic_duration=sd, staff=min_start_note.staff, voice=min_start_note.voice + ) + part.add(rest, start_time, end_time) + start_time = end_time + else: + rest = Rest( + symbolic_duration=sym_dur, + staff=min_start_note.staff, + voice=min_start_note.voice, + ) + part.add(rest, start_time, min_start_note.start.t) # get note with max end.t and fill the rest after it if needed min_end_note = notes_per_vocstaff[sort_note_end[-1]] if min_end_note.end.t < end_time: sym_dur = estimate_symbolic_duration( - end_time - min_end_note.end.t, part._quarter_durations[0] + end_time - min_end_note.end.t, part._quarter_durations[0], return_com_durations=True ) - rest = Rest( - symbolic_duration=sym_dur, - staff=min_end_note.staff, - voice=min_end_note.voice, - ) - part.add(rest, min_end_note.end.t, end_time) + # solution when estimation returns composite durations. + if isinstance(sym_dur, tuple): + start_time = min_end_note.end.t + for i, sd in enumerate(sym_dur): + end_time = start_time + symbolic_to_numeric_duration(sd, part._quarter_durations[0]) + rest = Rest( + symbolic_duration=sd, staff=min_end_note.staff, voice=min_end_note.voice + ) + part.add(rest, start_time, end_time) + start_time = end_time + else: + rest = Rest( + symbolic_duration=sym_dur, + staff=min_end_note.staff, + voice=min_end_note.voice, + ) + part.add(rest, min_end_note.end.t, end_time) if len(sort_note_start) <= 1: continue @@ -5071,18 +5093,29 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: sym_dur = estimate_symbolic_duration( notes_per_vocstaff[sort_note_start[i]].start.t - notes_per_vocstaff[sort_note_end[i - 1]].end.t, - part._quarter_durations[0], - ) - rest = Rest( - symbolic_duration=sym_dur, - staff=notes_per_vocstaff[sort_note_end[i - 1]].staff, - voice=notes_per_vocstaff[sort_note_end[i - 1]].voice, - ) - part.add( - rest, - notes_per_vocstaff[sort_note_end[i - 1]].end.t, - notes_per_vocstaff[sort_note_start[i]].start.t, + part._quarter_durations[0], return_com_durations=True ) + if isinstance(sym_dur, tuple): + start_time = notes_per_vocstaff[sort_note_end[i - 1]].end.t + for i, sd in enumerate(sym_dur): + end_time = start_time + symbolic_to_numeric_duration(sd, part._quarter_durations[0]) + rest = Rest( + symbolic_duration=sd, staff=notes_per_vocstaff[sort_note_end[i - 1]].staff, + voice=notes_per_vocstaff[sort_note_end[i - 1]].voice + ) + part.add(rest, start_time, end_time) + start_time = end_time + else: + rest = Rest( + symbolic_duration=sym_dur, + staff=notes_per_vocstaff[sort_note_end[i - 1]].staff, + voice=notes_per_vocstaff[sort_note_end[i - 1]].voice, + ) + part.add( + rest, + notes_per_vocstaff[sort_note_end[i - 1]].end.t, + notes_per_vocstaff[sort_note_start[i]].start.t, + ) def _fill_rests_global( From b12511a7e21eaae4e34dc365dc163feeb8f8a793 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 22 Mar 2024 14:33:29 +0100 Subject: [PATCH 147/197] Added some assertions. --- partitura/io/importmusicxml.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index 1b3a8b95..f19f0d8e 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -1447,6 +1447,9 @@ def handle_tuplets(notations, ongoing, note): stopping_tuplets.append(tuplet) + # 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, "Tuplet start time is after tuplet stop time" return starting_tuplets, stopping_tuplets From 1bd24a74415bb50f45934d957ec40814a5ea521a Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 22 Mar 2024 14:33:45 +0100 Subject: [PATCH 148/197] Added warnings for tuplet export in mei. --- partitura/io/exportmei.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index efff9667..3a918858 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -17,6 +17,7 @@ fifths_mode_to_key_name, ) import numpy as np +import warnings from partitura.utils.misc import deprecated_alias, PathLike from partitura.utils.music import MEI_DURS_TO_SYMBOLIC, estimate_symbolic_duration @@ -296,6 +297,20 @@ def _handle_tuplets(self, measure_el, start, end): start_note = tuplet.start_note end_note = tuplet.end_note if start_note.start.t < start or end_note.end.t > end: + warnings.warn( + "Tuplet start or end note is outside of the measure. Skipping tuplet element." + ) + continue + if start_note.start.t > end_note.start.t: + warnings.warn( + "Tuplet start note is after end note. Skipping tuplet element." + ) + continue + # Skip if start and end notes are in different voices or staves + if start_note.voice != end_note.voice or start_note.staff != end_note.staff: + warnings.warn( + "Tuplet start and end notes are in different voices or staves. Skipping tuplet element." + ) continue # Find the note element corresponding to the start note i.e. has the same id value start_note_el = measure_el.xpath(f".//*[@xml:id='{start_note.id}']")[0] @@ -352,8 +367,13 @@ def _handle_beams(self, measure_el, start, end): # If the parent is a chord, the beam element should be added as parent of the chord element if layer_el.tag == "chord": parent_el = layer_el.getparent() - insert_index = parent_el.index(layer_el) - layer_el = parent_el + if parent_el.tag == "tuplet": + parent_el = parent_el.getparent() + insert_index = parent_el.index(layer_el.getparent()) + layer_el = parent_el + else: + insert_index = parent_el.index(layer_el) + layer_el = parent_el # Create the beam element beam_el = etree.Element("beam") @@ -368,7 +388,10 @@ def _handle_beams(self, measure_el, start, end): if note_el.getparent().tag == "tuplet": beam_el.append(note_el.getparent()) elif note_el.getparent().tag == "chord": - beam_el.append(note_el.getparent()) + if note_el.getparent().getparent().tag == "tuplet": + beam_el.append(note_el.getparent().getparent()) + else: + beam_el.append(note_el.getparent()) else: # verify that the note element is not already a child of the beam element if note_el.getparent() != beam_el: From 711b5c3db5f7fe44699216842b24e1a0fed175bb Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 26 Mar 2024 11:07:52 +0100 Subject: [PATCH 149/197] correction for overwritten variables. --- partitura/score.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index b3ba5eda..98aa4dad 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -5015,13 +5015,14 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: end_time - start_time, part._quarter_durations[0], return_com_durations=True ) if isinstance(sym_dur, tuple): + st = start_time for i, sd in enumerate(sym_dur): - end_time = start_time + symbolic_to_numeric_duration(sd, part._quarter_durations[0]) + et = start_time + symbolic_to_numeric_duration(sd, part._quarter_durations[0]) rest = Rest( symbolic_duration=sd, staff=staff, voice=un_voice.max() + 1 ) - part.add(rest, start_time, end_time) - start_time = end_time + part.add(rest, st, et) + st = et else: rest = Rest( symbolic_duration=sym_dur, staff=staff, voice=un_voice.max() + 1 @@ -5043,13 +5044,14 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: ) # solution when estimation returns composite durations. if isinstance(sym_dur, tuple): + st = start_time for i, sd in enumerate(sym_dur): - end_time = start_time + symbolic_to_numeric_duration(sd, part._quarter_durations[0]) + et = st + symbolic_to_numeric_duration(sd, part._quarter_durations[0]) rest = Rest( symbolic_duration=sd, staff=min_start_note.staff, voice=min_start_note.voice ) - part.add(rest, start_time, end_time) - start_time = end_time + part.add(rest, st, et) + st = et else: rest = Rest( symbolic_duration=sym_dur, @@ -5066,14 +5068,14 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: ) # solution when estimation returns composite durations. if isinstance(sym_dur, tuple): - start_time = min_end_note.end.t + st = min_end_note.end.t for i, sd in enumerate(sym_dur): - end_time = start_time + symbolic_to_numeric_duration(sd, part._quarter_durations[0]) + et = st + symbolic_to_numeric_duration(sd, part._quarter_durations[0]) rest = Rest( symbolic_duration=sd, staff=min_end_note.staff, voice=min_end_note.voice ) - part.add(rest, start_time, end_time) - start_time = end_time + part.add(rest, st, et) + st = et else: rest = Rest( symbolic_duration=sym_dur, @@ -5096,15 +5098,15 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: part._quarter_durations[0], return_com_durations=True ) if isinstance(sym_dur, tuple): - start_time = notes_per_vocstaff[sort_note_end[i - 1]].end.t + st = notes_per_vocstaff[sort_note_end[i - 1]].end.t for i, sd in enumerate(sym_dur): - end_time = start_time + symbolic_to_numeric_duration(sd, part._quarter_durations[0]) + et = st + symbolic_to_numeric_duration(sd, part._quarter_durations[0]) rest = Rest( symbolic_duration=sd, staff=notes_per_vocstaff[sort_note_end[i - 1]].staff, voice=notes_per_vocstaff[sort_note_end[i - 1]].voice ) - part.add(rest, start_time, end_time) - start_time = end_time + part.add(rest, st, et) + st = et else: rest = Rest( symbolic_duration=sym_dur, From 76fbdcee59afac88bdd5c61e6e6189ce817a2905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Thu, 28 Mar 2024 10:17:04 +0100 Subject: [PATCH 150/197] synthesize using fluidsynth (as an optional requirement) --- partitura/io/exportaudio.py | 67 ++++- partitura/io/exportmatch.py | 2 +- partitura/io/importmatch.py | 2 +- partitura/musicanalysis/performance_codec.py | 5 +- partitura/utils/fluidsynth.py | 265 +++++++++++++++++++ partitura/utils/misc.py | 26 ++ partitura/utils/synth.py | 16 +- 7 files changed, 374 insertions(+), 9 deletions(-) create mode 100644 partitura/utils/fluidsynth.py diff --git a/partitura/io/exportaudio.py b/partitura/io/exportaudio.py index 563b3009..c9742622 100644 --- a/partitura/io/exportaudio.py +++ b/partitura/io/exportaudio.py @@ -1,8 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- """ -This module contains methods to synthesize Partitura object to wav using -additive synthesis +This module contains methods to synthesize Partitura object to wav. """ from typing import Union, Optional, Callable, Dict, Any import numpy as np @@ -13,10 +12,18 @@ from partitura.performance import PerformanceLike from partitura.utils.synth import synthesize, SAMPLE_RATE, A4 +from partitura.utils.fluidsynth import ( + synthesize_fluidsynth, + DEFAULT_SOUNDFONT, + HAS_FLUIDSYNTH, +) from partitura.utils.misc import PathLike -__all__ = ["save_wav"] +__all__ = [ + "save_wav", + "save_wav_fluidsynth", +] def save_wav( @@ -94,3 +101,57 @@ def save_wav( wavfile.write(out, samplerate, audio_signal) else: return audio_signal + + +def save_wav_fluidsynth( + input_data: Union[ScoreLike, PerformanceLike, np.ndarray], + out: Optional[PathLike] = None, + samplerate: int = SAMPLE_RATE, + soundfont: PathLike = DEFAULT_SOUNDFONT, + bpm: Union[float, np.ndarray, Callable] = 60, +) -> Optional[np.ndarray]: + """ + Export a score (a `Score`, `Part`, `PartGroup` or list of `Part` instances), + a performance (`Performance`, `PerformedPart` or list of `PerformedPart` instances) + as a WAV file using fluidsynth + + Parameters + ---------- + input_data : ScoreLike, PerformanceLike or np.ndarray + A partitura object with note information. + out : PathLike or None + Path of the output Wave file. If None, the method outputs + the audio signal as an array (see `audio_signal` below). + samplerate: int + The sample rate of the audio file in Hz. The default is 44100Hz. + soundfont : PathLike + Path to the soundfont in SF2 format for fluidsynth. + bpm : float, np.ndarray, callable + The bpm to render the output (if the input is a score-like object). + See `partitura.utils.music.performance_notearray_from_score_notearray` + for more information on this parameter. + + Returns + ------- + audio_signal : np.ndarray + Audio signal as a 1D array. Only returned if `out` is None. + """ + audio_signal = synthesize_fluidsynth( + note_info=input_data, + samplerate=samplerate, + soundfont=soundfont, + bpm=bpm, + ) + + if out is not None: + # Write audio signal + + # convert to 16bit integers (save as PCM 16 bit) + amplitude = np.iinfo(np.int16).max + if abs(audio_signal).max() <= 1: + # convert to 16bit integers (save as PCM 16 bit) + amplitude = np.iinfo(np.int16).max + audio_signal *= amplitude + wavfile.write(out, samplerate, audio_signal.astype(np.int16)) + else: + return audio_signal diff --git a/partitura/io/exportmatch.py b/partitura/io/exportmatch.py index 3311c75b..72435956 100644 --- a/partitura/io/exportmatch.py +++ b/partitura/io/exportmatch.py @@ -395,7 +395,7 @@ def matchfile_from_alignment( onset=onset, offset=offset, velocity=pnote["velocity"], - channel=pnote.get("channel", 1), + channel=pnote.get("channel", 0), track=pnote.get("track", 0), ) pnote_sort_info[pnote["id"]] = ( diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index dcb95723..bb1f061a 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -394,7 +394,7 @@ def performed_part_from_match( sound_off=midi_ticks_to_seconds(note.Offset, mpq, ppq), velocity=note.Velocity, track=getattr(note, "Track", 0), - channel=getattr(note, "Channel", 1), + channel=getattr(note, "Channel", 0), ) ) # Set first note_on to zero in ticks and seconds if first_note_at_zero diff --git a/partitura/musicanalysis/performance_codec.py b/partitura/musicanalysis/performance_codec.py index 9da86785..2eac40c2 100644 --- a/partitura/musicanalysis/performance_codec.py +++ b/partitura/musicanalysis/performance_codec.py @@ -819,12 +819,13 @@ def get_matched_notes(spart_note_array, ppart_note_array, alignment): else: p_id = al["performance_id"] - p_idx = int(np.where(ppart_note_array["id"] == p_id)[0]) + p_idx = np.where(ppart_note_array["id"] == p_id)[0] s_idx = np.where(spart_note_array["id"] == al["score_id"])[0] - if len(s_idx) > 0: + if len(s_idx) > 0 and len(p_idx) > 0: s_idx = int(s_idx) + p_idx = int(p_idx) matched_idxs.append((s_idx, p_idx)) return np.array(matched_idxs) diff --git a/partitura/utils/fluidsynth.py b/partitura/utils/fluidsynth.py new file mode 100644 index 00000000..072ad3e3 --- /dev/null +++ b/partitura/utils/fluidsynth.py @@ -0,0 +1,265 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" +This module contains methods for synthesizing score- or performance-like +objects using fluidsynth. Fluidsynth is an optional dependency. + +""" + +import os +from collections import defaultdict +from typing import Callable, Optional, Union + +import numpy as np +import partitura as pt + +try: + from fluidsynth import Synth + + HAS_FLUIDSYNTH = True +except ImportError: + Synth = None + HAS_FLUIDSYNTH = False + +from partitura.io.exportaudio import SAMPLE_RATE +from partitura.performance import PerformanceLike +from partitura.score import ScoreLike +from partitura.utils.misc import PathLike, download_file +from partitura.utils.music import ( + ensure_notearray, + get_time_units_from_note_array, + performance_notearray_from_score_notearray, +) +from scipy.io import wavfile + +# MuseScore's soundfont distributed under the License. +DEFAULT_SOUNDFONT_URL = "ftp://ftp.osuosl.org/pub/musescore/soundfont/MuseScore_General/MuseScore_General.sf2" + +DEFAULT_SOUNDFONT = os.path.join(pt.__path__, "assets", "MuseScore_General.sf2") + +if not os.path.exists(DEFAULT_SOUNDFONT) and HAS_FLUIDSYNTH: + + download_file( + url=DEFAULT_SOUNDFONT_URL, + out=DEFAULT_SOUNDFONT, + ) + + +def synthesize_fluidsynth( + note_info: Union[ScoreLike, PerformanceLike, np.ndarray], + samplerate: int = SAMPLE_RATE, + soundfont: str = DEFAULT_SOUNDFONT, + bpm: Union[float, np.ndarray, Callable] = 60, +) -> np.ndarray: + + if not HAS_FLUIDSYNTH: + raise ImportError("Fluidsynth is not installed!") + + if isinstance(note_info, pt.performance.Performance): + for ppart in note_info: + ppart.sustain_pedal_threshold = 127 + + if isinstance(note_info, pt.performance.PerformedPart): + note_info.sustain_pedal_threshold = 127 + note_array = ensure_notearray(note_info) + + onset_unit, _ = get_time_units_from_note_array(note_array) + if np.min(note_array[onset_unit]) <= 0: + note_array[onset_unit] = note_array[onset_unit] + np.min(note_array[onset_unit]) + + pitch = note_array["pitch"] + # If the input is a score, convert score time to seconds + if onset_unit != "onset_sec": + pnote_array = performance_notearray_from_score_notearray( + snote_array=note_array, + bpm=bpm, + ) + onsets = pnote_array["onset_sec"] + offsets = pnote_array["onset_sec"] + pnote_array["duration_sec"] + # duration = pnote_array["duration_sec"] + channel = pnote_array["channel"] + track = pnote_array["track"] + velocity = pnote_array["velocity"] + else: + onsets = note_array["onset_sec"] + offsets = note_array["onset_sec"] + note_array["duration_sec"] + # duration = note_array["duration_sec"] + + if "velocity" in note_array.dtype.names: + velocity = note_array["velocity"] + else: + velocity = np.ones(len(onsets), dtype=int) * 64 + if "channel" in note_array.dtype.names: + channel = note_array["channel"] + else: + channel = np.zeros(len(onsets), dtype=int) + + if "track" in note_array.dtype.names: + track = note_array["track"] + else: + track = np.zeros(len(onsets), dtype=int) + + controls = [] + if isinstance(note_info, pt.performance.Performance): + + for ppart in note_info: + controls += ppart.controls + + unique_tracks = list( + set(list(np.unique(track)) + list(set([c["track"] for c in controls]))) + ) + + track_dict = defaultdict(lambda: defaultdict(list)) + + for tn in unique_tracks: + track_idxs = np.where(track == tn)[0] + + track_channels = channel[track_idxs] + track_pitch = pitch[track_idxs] + track_onsets = onsets[track_idxs] + track_offsets = offsets[track_idxs] + track_velocity = velocity[track_idxs] + + unique_channels = np.unique(track_channels) + + track_controls = [c for c in controls if c["track"] == tn] + + for chn in unique_channels: + + channel_idxs = np.where(track_channels == chn)[0] + + channel_pitch = track_pitch[channel_idxs] + channel_onset = track_onsets[channel_idxs] + channel_offset = track_offsets[channel_idxs] + channel_velocity = track_velocity[channel_idxs] + + channel_controls = [c for c in track_controls if c["channel"] == chn] + + track_dict[tn][chn] = [ + channel_pitch, + channel_onset, + channel_offset, + channel_velocity, + channel_controls, + ] + + # set to mono + synthesizer = Synth(samplerate=SAMPLE_RATE) + sf_id = synthesizer.sfload(soundfont) + + audio_signals = [] + for tn, channel_info in track_dict.items(): + + for chn, (pi, on, off, vel, ctrls) in channel_info.items(): + + audio_signal = synth_note_info( + pitch=pi, + onsets=on, + offsets=off, + velocities=vel, + controls=ctrls, + program=None, + synthesizer=synthesizer, + sf_id=sf_id, + channel=chn, + samplerate=samplerate, + ) + audio_signals.append(audio_signal) + + # pad audio signals: + + signal_lengths = [len(signal) for signal in audio_signals] + max_len = np.max(signal_lengths) + + output_audio_signal = np.zeros(max_len) + + for sl, audio_signal in zip(signal_lengths, audio_signals): + + output_audio_signal[:sl] += audio_signal + + # normalization term + norm_term = max(audio_signal.max(), abs(audio_signal.min())) + output_audio_signal /= norm_term + + return output_audio_signal + + +def synth_note_info( + pitch: np.ndarray, + onsets: np.ndarray, + offsets: np.ndarray, + velocities: np.ndarray, + controls: Optional[list], + program: Optional[int], + synthesizer: Synth, + sf_id: int, + channel: int, + samplerate: int = SAMPLE_RATE, +) -> np.ndarray: + + # set program + synthesizer.program_select(channel, sf_id, 0, program or 0) + + # TODO: extend piece duration to account for pedal info. + if len(controls) > 0 and len(offsets) > 0: + piece_duration = max(offsets.max(), np.max([c["time"] for c in controls])) + elif len(controls) > 0 and len(offsets) == 0: + piece_duration = np.max([c["time"] for c in controls]) + elif len(controls) == 0 and len(offsets) > 0: + piece_duration = offsets.max() + else: + # return a single zero + audio_signal = np.zeros(1) + return audio_signal + + num_frames = int(np.round(piece_duration * samplerate)) + + # Initialize array containing audio + audio_signal = np.zeros(num_frames, dtype="float") + + # Initialize the time axis + x = np.linspace(0, piece_duration, num=num_frames) + + # onsets in frames (i.e., indices of the `audio_signal` array) + onsets_in_frames = np.searchsorted(x, onsets, side="left") + offsets_in_frames = np.searchsorted(x, offsets, side="left") + + messages = [] + for ctrl in controls or []: + + messages.append( + ( + "cc", + channel, + ctrl["number"], + ctrl["value"], + np.searchsorted(x, ctrl["time"], side="left"), + ) + ) + + for pi, vel, oif, ofif in zip( + pitch, velocities, onsets_in_frames, offsets_in_frames + ): + + messages += [ + ("noteon", channel, pi, vel, oif), + ("noteoff", channel, pi, ofif), + ] + + # sort messages + messages.sort(key=lambda x: x[-1]) + + delta_times = [ + int(nm[-1] - cm[-1]) for nm, cm in zip(messages[1:], messages[:-1]) + ] + [0] + + for dt, msg in zip(delta_times, messages): + + msg_type = msg[0] + msg_time = msg[-1] + getattr(synthesizer, msg_type)(*msg[1:-1]) + + samples = synthesizer.get_samples(dt)[::2] + audio_signal[msg_time : msg_time + dt] = samples + + return audio_signal diff --git a/partitura/utils/misc.py b/partitura/utils/misc.py index 843f8c11..3695e189 100644 --- a/partitura/utils/misc.py +++ b/partitura/utils/misc.py @@ -6,6 +6,8 @@ import functools import os import warnings +from urllib.request import urlopen +from shutil import copyfileobj from typing import Union, Callable, Dict, Any, Iterable, Optional @@ -254,3 +256,27 @@ def concatenate_images( else: return new_image + + +def download_file( + url: str, + out: str, +) -> None: + """ + Download a file from a specified URL and save it to a local file path. + + Parameters + ---------- + url : str + The URL of the file to download. + out : str + The local file path where the downloaded file should be saved. + + Notes + ----- + This method was adapted from a Stack Overflow answer + (https://stackoverflow.com/a/15035466), and is distributed under the + CC BY-SA 4.0 license (https://creativecommons.org/licenses/by-sa/4.0/). + """ + with urlopen(url) as in_stream, open(out, "wb") as out_file: + copyfileobj(in_stream, out_file) diff --git a/partitura/utils/synth.py b/partitura/utils/synth.py index 507280c7..6a433669 100644 --- a/partitura/utils/synth.py +++ b/partitura/utils/synth.py @@ -8,7 +8,8 @@ ---- * Add other tuning systems? """ -from typing import Union, Tuple, Dict, Optional, Any, Callable +from __future__ import annotations +from typing import Union, Tuple, Dict, Optional, Any, Callable, TYPE_CHECKING import numpy as np @@ -23,6 +24,15 @@ performance_notearray_from_score_notearray, ) +if TYPE_CHECKING: + # Import typing info for typing annotations. + # For this to work we need to import annotations from __future__ + # Solution from + # https://medium.com/quick-code/python-type-hinting-eliminating-importerror-due-to-circular-imports-265dfb0580f8 + from partitura.score import ScoreLike, Interval + from partitura.performance import PerformanceLike, Performance, PerformedPart + + TWO_PI = 2 * np.pi SAMPLE_RATE = 44100 DTYPE = float @@ -61,6 +71,8 @@ } + + def midi_pitch_to_natural_frequency( midi_pitch: Union[int, float, np.ndarray], a4: Union[int, float] = A4, @@ -374,7 +386,7 @@ def max_f(self, freq: Union[float, np.ndarray]) -> Union[float, np.ndarray]: def synthesize( - note_info, + note_info: Union[ScoreLike, PerformanceLike, np.ndarray], samplerate: int = SAMPLE_RATE, envelope_fun: str = "linear", tuning: Union[str, Callable] = "equal_temperament", From f9c1e8afef23aec95dc431156fb6fea6f08104fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Thu, 28 Mar 2024 10:25:52 +0100 Subject: [PATCH 151/197] fixed typo --- partitura/utils/fluidsynth.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/partitura/utils/fluidsynth.py b/partitura/utils/fluidsynth.py index 072ad3e3..b1bb4250 100644 --- a/partitura/utils/fluidsynth.py +++ b/partitura/utils/fluidsynth.py @@ -3,7 +3,6 @@ """ This module contains methods for synthesizing score- or performance-like objects using fluidsynth. Fluidsynth is an optional dependency. - """ import os @@ -35,10 +34,10 @@ # MuseScore's soundfont distributed under the License. DEFAULT_SOUNDFONT_URL = "ftp://ftp.osuosl.org/pub/musescore/soundfont/MuseScore_General/MuseScore_General.sf2" -DEFAULT_SOUNDFONT = os.path.join(pt.__path__, "assets", "MuseScore_General.sf2") +DEFAULT_SOUNDFONT = os.path.join(pt.__path__[0], "assets", "MuseScore_General.sf2") if not os.path.exists(DEFAULT_SOUNDFONT) and HAS_FLUIDSYNTH: - + print(f"Downloading soundfont from {DEFAULT_SOUNDFONT_URL}...") download_file( url=DEFAULT_SOUNDFONT_URL, out=DEFAULT_SOUNDFONT, @@ -48,9 +47,31 @@ def synthesize_fluidsynth( note_info: Union[ScoreLike, PerformanceLike, np.ndarray], samplerate: int = SAMPLE_RATE, - soundfont: str = DEFAULT_SOUNDFONT, + soundfont: PathLike = DEFAULT_SOUNDFONT, bpm: Union[float, np.ndarray, Callable] = 60, ) -> np.ndarray: + """ + Synthesize partitura object with note information using + fluidsynth. + + Parameters + ---------- + note_info : ScoreLike, PerformanceLike or np.ndarray + A partitura object with note information. + samplerate: int + The sample rate of the audio file in Hz. + soundfont: PathLike + The path to the soundfont (in SF2 format). + bpm : float, np.ndarray or callable + The bpm to render the output (if the input is a score-like object). + See `partitura.utils.music.performance_notearray_from_score_notearray` + for more information on this parameter. + + Returns + ------- + output_audio_signal : np.ndarray + Audio signal as a 1D array. + """ if not HAS_FLUIDSYNTH: raise ImportError("Fluidsynth is not installed!") @@ -83,7 +104,6 @@ def synthesize_fluidsynth( else: onsets = note_array["onset_sec"] offsets = note_array["onset_sec"] + note_array["duration_sec"] - # duration = note_array["duration_sec"] if "velocity" in note_array.dtype.names: velocity = note_array["velocity"] From 2922b15540022d93a3c25d02861f564aabc9428f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Thu, 28 Mar 2024 11:28:08 +0100 Subject: [PATCH 152/197] add tests to fluidsynth export --- .gitignore | 3 ++ partitura/io/exportaudio.py | 14 ++++- partitura/utils/fluidsynth.py | 27 ++++++---- tests/test_fluidsynth.py | 97 +++++++++++++++++++++++++++++++++++ tests/test_synth.py | 2 +- 5 files changed, 129 insertions(+), 14 deletions(-) create mode 100644 tests/test_fluidsynth.py diff --git a/.gitignore b/.gitignore index 1b28c046..3546d9ee 100644 --- a/.gitignore +++ b/.gitignore @@ -152,3 +152,6 @@ static # phdocs phdocs.txt + +# fluidsynth default soundfont +partitura/assets/MuseScore_General.sf2 diff --git a/partitura/io/exportaudio.py b/partitura/io/exportaudio.py index c9742622..0d81beb4 100644 --- a/partitura/io/exportaudio.py +++ b/partitura/io/exportaudio.py @@ -97,8 +97,16 @@ def save_wav( ) if out is not None: - # Write audio signal - wavfile.write(out, samplerate, audio_signal) + # convert to 16bit integers (save as PCM 16 bit) + # (some DAWs cannot load audio files that are float64, + # e.g., Logic) + amplitude = np.iinfo(np.int16).max + if abs(audio_signal).max() <= 1: + # convert to 16bit integers (save as PCM 16 bit) + amplitude = np.iinfo(np.int16).max + audio_signal *= amplitude + wavfile.write(out, samplerate, audio_signal.astype(np.int16)) + else: return audio_signal @@ -147,6 +155,8 @@ def save_wav_fluidsynth( # Write audio signal # convert to 16bit integers (save as PCM 16 bit) + # (some DAWs cannot load audio files that are float64, + # e.g., Logic) amplitude = np.iinfo(np.int16).max if abs(audio_signal).max() <= 1: # convert to 16bit integers (save as PCM 16 bit) diff --git a/partitura/utils/fluidsynth.py b/partitura/utils/fluidsynth.py index b1bb4250..3c3a595b 100644 --- a/partitura/utils/fluidsynth.py +++ b/partitura/utils/fluidsynth.py @@ -16,11 +16,11 @@ from fluidsynth import Synth HAS_FLUIDSYNTH = True -except ImportError: - Synth = None - HAS_FLUIDSYNTH = False +except ImportError: # pragma: no cover + Synth = None # pragma: no cover + HAS_FLUIDSYNTH = False # pragma: no cover -from partitura.io.exportaudio import SAMPLE_RATE +from partitura.utils.synth import SAMPLE_RATE from partitura.performance import PerformanceLike from partitura.score import ScoreLike from partitura.utils.misc import PathLike, download_file @@ -29,19 +29,22 @@ get_time_units_from_note_array, performance_notearray_from_score_notearray, ) -from scipy.io import wavfile # MuseScore's soundfont distributed under the License. DEFAULT_SOUNDFONT_URL = "ftp://ftp.osuosl.org/pub/musescore/soundfont/MuseScore_General/MuseScore_General.sf2" -DEFAULT_SOUNDFONT = os.path.join(pt.__path__[0], "assets", "MuseScore_General.sf2") +DEFAULT_SOUNDFONT = os.path.join( + pt.__path__[0], + "assets", + "MuseScore_General.sf2", +) -if not os.path.exists(DEFAULT_SOUNDFONT) and HAS_FLUIDSYNTH: - print(f"Downloading soundfont from {DEFAULT_SOUNDFONT_URL}...") +if not os.path.exists(DEFAULT_SOUNDFONT) and HAS_FLUIDSYNTH: # pragma: no cover + print(f"Downloading soundfont from {DEFAULT_SOUNDFONT_URL}...") # pragma: no cover download_file( url=DEFAULT_SOUNDFONT_URL, out=DEFAULT_SOUNDFONT, - ) + ) # pragma: no cover def synthesize_fluidsynth( @@ -58,10 +61,13 @@ def synthesize_fluidsynth( ---------- note_info : ScoreLike, PerformanceLike or np.ndarray A partitura object with note information. + samplerate: int The sample rate of the audio file in Hz. + soundfont: PathLike The path to the soundfont (in SF2 format). + bpm : float, np.ndarray or callable The bpm to render the output (if the input is a score-like object). See `partitura.utils.music.performance_notearray_from_score_notearray` @@ -74,7 +80,7 @@ def synthesize_fluidsynth( """ if not HAS_FLUIDSYNTH: - raise ImportError("Fluidsynth is not installed!") + raise ImportError("Fluidsynth is not installed!") # pragma: no cover if isinstance(note_info, pt.performance.Performance): for ppart in note_info: @@ -220,7 +226,6 @@ def synth_note_info( # set program synthesizer.program_select(channel, sf_id, 0, program or 0) - # TODO: extend piece duration to account for pedal info. if len(controls) > 0 and len(offsets) > 0: piece_duration = max(offsets.max(), np.max([c["time"] for c in controls])) elif len(controls) > 0 and len(offsets) == 0: diff --git a/tests/test_fluidsynth.py b/tests/test_fluidsynth.py new file mode 100644 index 00000000..c6923ff9 --- /dev/null +++ b/tests/test_fluidsynth.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains tests for the fluidsynth methods. +""" +import unittest + +import numpy as np +from scipy.io import wavfile +import tempfile + +from partitura.utils.fluidsynth import ( + synthesize_fluidsynth, + HAS_FLUIDSYNTH, + SAMPLE_RATE, +) + +from partitura import EXAMPLE_MUSICXML, load_score, load_performance_midi + +from partitura.io.exportaudio import save_wav_fluidsynth + +from tests import MOZART_VARIATION_FILES + +RNG = np.random.RandomState(1984) + +if HAS_FLUIDSYNTH: + + class TestSynthesize(unittest.TestCase): + + score = load_score(EXAMPLE_MUSICXML) + + def test_synthesize(self): + + score_na = self.score.note_array() + + duration_beats = ( + score_na["onset_beat"] + score_na["duration_beat"] + ).max() - score_na["onset_beat"].min() + + for bpm in RNG.randint(30, 200, size=10): + + for samplerate in [12000, 16000, 22000, SAMPLE_RATE]: + + duration_sec = duration_beats * 60 / bpm + y = synthesize_fluidsynth( + note_info=self.score, + samplerate=samplerate, + bpm=bpm, + ) + + expected_length = np.round(duration_sec * samplerate) + + self.assertTrue(len(y) == expected_length) + + self.assertTrue(isinstance(y, np.ndarray)) + + class TestSynthExport(unittest.TestCase): + + test_files = [ + load_score(MOZART_VARIATION_FILES["musicxml"]), + load_performance_midi(MOZART_VARIATION_FILES["midi"]), + ] + + def export(self, note_info): + + y = synthesize_fluidsynth( + note_info=note_info, + samplerate=SAMPLE_RATE, + bpm=60, + ) + + with tempfile.TemporaryFile(suffix=".wav") as filename: + + save_wav_fluidsynth( + input_data=note_info, + out=filename, + samplerate=SAMPLE_RATE, + bpm=60, + ) + + sr_rec, rec_audio = wavfile.read(filename) + + self.assertTrue(sr_rec == SAMPLE_RATE) + self.assertTrue(len(rec_audio) == len(y)) + self.assertTrue( + np.allclose( + rec_audio / rec_audio.max(), + y / y.max(), + atol=1e-4, + ) + ) + + def test_export(self): + + for note_info in self.test_files: + + self.export(note_info) diff --git a/tests/test_synth.py b/tests/test_synth.py index b4ea8522..9cc26c9c 100644 --- a/tests/test_synth.py +++ b/tests/test_synth.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -This module contains tests for the synthesis methods. +This module contains tests for the additive synthesis methods. """ import unittest From 3fd555d31c84048b87ef42c24605051d06e8167b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Thu, 28 Mar 2024 11:30:18 +0100 Subject: [PATCH 153/197] minor --- partitura/io/exportaudio.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/partitura/io/exportaudio.py b/partitura/io/exportaudio.py index 0d81beb4..25e4a35f 100644 --- a/partitura/io/exportaudio.py +++ b/partitura/io/exportaudio.py @@ -144,6 +144,10 @@ def save_wav_fluidsynth( audio_signal : np.ndarray Audio signal as a 1D array. Only returned if `out` is None. """ + + if not HAS_FLUIDSYNTH: + raise ImportError("Fluidsynth is not installed!") + audio_signal = synthesize_fluidsynth( note_info=input_data, samplerate=samplerate, From de9a4ef0edc9ec345fc3bb1507281df555d839d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Thu, 28 Mar 2024 11:32:56 +0100 Subject: [PATCH 154/197] update __init__ --- partitura/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/partitura/__init__.py b/partitura/__init__.py index 3d54e093..4598dd53 100644 --- a/partitura/__init__.py +++ b/partitura/__init__.py @@ -22,7 +22,7 @@ from .io.importnakamura import load_nakamuramatch, load_nakamuracorresp from .io.importparangonada import load_parangonada_csv from .io.exportparangonada import save_parangonada_csv, save_csv_for_parangonada -from .io.exportaudio import save_wav +from .io.exportaudio import save_wav, save_wav_fluidsynth from .io.exportmei import save_mei from .display import render from . import musicanalysis @@ -61,5 +61,7 @@ "load_nakamuracorresp", "load_parangonada_csv", "save_parangonada_csv", + "save_wav", + "save_wav_fluidsynth", "render", ] From e9208346cce5e5cc7a252204c4f607e2eb5c60d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Thu, 28 Mar 2024 12:12:03 +0100 Subject: [PATCH 155/197] update default soundfont to sf3 --- .gitignore | 2 +- partitura/utils/fluidsynth.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 3546d9ee..6a035d51 100644 --- a/.gitignore +++ b/.gitignore @@ -154,4 +154,4 @@ static phdocs.txt # fluidsynth default soundfont -partitura/assets/MuseScore_General.sf2 +partitura/assets/MuseScore_General.sf* diff --git a/partitura/utils/fluidsynth.py b/partitura/utils/fluidsynth.py index 3c3a595b..4ef66091 100644 --- a/partitura/utils/fluidsynth.py +++ b/partitura/utils/fluidsynth.py @@ -30,13 +30,14 @@ performance_notearray_from_score_notearray, ) -# MuseScore's soundfont distributed under the License. -DEFAULT_SOUNDFONT_URL = "ftp://ftp.osuosl.org/pub/musescore/soundfont/MuseScore_General/MuseScore_General.sf2" +# MuseScore's soundfont distributed under the MIT License. +# https://ftp.osuosl.org/pub/musescore/soundfont/MuseScore_General/MuseScore_General_License.md +DEFAULT_SOUNDFONT_URL = "ftp://ftp.osuosl.org/pub/musescore/soundfont/MuseScore_General/MuseScore_General.sf3" DEFAULT_SOUNDFONT = os.path.join( pt.__path__[0], "assets", - "MuseScore_General.sf2", + "MuseScore_General.sf3", ) if not os.path.exists(DEFAULT_SOUNDFONT) and HAS_FLUIDSYNTH: # pragma: no cover From df83cc03b1dc002bb3a2aee511468a1ecc268bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Thu, 28 Mar 2024 12:21:25 +0100 Subject: [PATCH 156/197] add missing docstrings --- partitura/io/exportaudio.py | 2 +- partitura/utils/fluidsynth.py | 37 ++++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/partitura/io/exportaudio.py b/partitura/io/exportaudio.py index 25e4a35f..6b8cb335 100644 --- a/partitura/io/exportaudio.py +++ b/partitura/io/exportaudio.py @@ -133,7 +133,7 @@ def save_wav_fluidsynth( samplerate: int The sample rate of the audio file in Hz. The default is 44100Hz. soundfont : PathLike - Path to the soundfont in SF2 format for fluidsynth. + Path to the soundfont in SF2/SF3 format for fluidsynth. bpm : float, np.ndarray, callable The bpm to render the output (if the input is a score-like object). See `partitura.utils.music.performance_notearray_from_score_notearray` diff --git a/partitura/utils/fluidsynth.py b/partitura/utils/fluidsynth.py index 4ef66091..5452c817 100644 --- a/partitura/utils/fluidsynth.py +++ b/partitura/utils/fluidsynth.py @@ -67,7 +67,7 @@ def synthesize_fluidsynth( The sample rate of the audio file in Hz. soundfont: PathLike - The path to the soundfont (in SF2 format). + The path to the soundfont (in SF2/SF3 format). bpm : float, np.ndarray or callable The bpm to render the output (if the input is a score-like object). @@ -223,6 +223,41 @@ def synth_note_info( channel: int, samplerate: int = SAMPLE_RATE, ) -> np.ndarray: + """ + Synthesize note information with Fluidsynth. + This method is designed to synthesize the notes in a + single track and channel. + + Parameters + ---------- + pitch : np.ndarray + An array with pitch information for each note. + onsets : np.ndarray + An array with onset time in seconds for each note. + offsets : np.ndarray + An array with offset times in seconds for each note. + velocities : np.ndarray + An array with MIDI velocities for each note. + controls : Optional[list] + A list of MIDI controls (e.g., pedals). + (as the `controls` attribute in `PerformedPart` objects) + program : Optional[int] + A list of MIDI programs as dictionaries + (as the `program` attribute in `PerformedPart` objects). + synthesizer : Synth + An instance of a fluidsynth Synth object. + sf_id : int + The id of the synthesizer object + channel : int + Channel for the the notes. + samplerate : int, optional + Sample rate, by default SAMPLE_RATE + + Returns + ------- + audio_signal : np.ndarray + A 1D array with the synthesized audio signal. + """ # set program synthesizer.program_select(channel, sf_id, 0, program or 0) From faf74ac6208c73d671e0f5847ea2ead309139151 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 11 Apr 2024 16:39:38 +0200 Subject: [PATCH 157/197] corrected local key computation from dcml. --- partitura/io/importdcml.py | 37 +++++++++++++++++++++++++++++++------ partitura/score.py | 18 ++++++++++++++++-- partitura/utils/globals.py | 2 ++ 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 081c042f..7b44ecd3 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -11,6 +11,28 @@ pd = None + +LOCAL_KEY_TRASPOSITIONS_DCML = { + "minor": { + "i": spt.Interval(1, "P"), + "ii": spt.Interval(2, "M"), + "iii": spt.Interval(3, "m"), + "iv": spt.Interval(4, "P"), + "v": spt.Interval(5, "P"), + "vi": spt.Interval(6, "m"), + "vii": spt.Interval(7, "m"), + }, + "major": { + "i": spt.Interval(1, "P"), + "ii": spt.Interval(2, "M"), + "iii": spt.Interval(3, "M"), + "iv": spt.Interval(4, "P"), + "v": spt.Interval(5, "P"), + "vi": spt.Interval(6, "M"), + "vii": spt.Interval(7, "M"), + }, +} + def read_note_tsv(note_tsv_path, metadata=None): # data = np.genfromtxt(note_tsv_path, delimiter="\t", dtype=None, names=True, invalid_raise=False) # unique_durations = np.unique(data["duration"]) @@ -192,17 +214,20 @@ def read_harmony_tsv(beat_tsv_path, part): # and datasets. For example, a minor chord is encoded as "m" instead of "min" or "minor" # Therefore we do not add the quality to the RomanNumeral object. Then it is extracted from the text. # Local key is in relation to the global key. - if row["globalkey"].islower(): - transposition_interval = spt.Roman2Interval_Min[row["localkey"]] - else: - transposition_interval = spt.Roman2Interval_Maj[row["localkey"]] - + local_key_sharps = row["localkey"].count("#") + local_key_flats = row["localkey"].count("b") + local_key = row["localkey"].replace("#", "").replace("b", "") + local_key_is_minor = local_key.islower() + local_key = local_key.lower() + global_key = "minor" if row["globalkey"].islower() else "major" + transposition_interval = LOCAL_KEY_TRASPOSITIONS_DCML[global_key][local_key] + transposition_interval = transposition_interval.change_quality(local_key_sharps - local_key_flats) key_step = re.search(r"[a-gA-G]", row["globalkey"]).group(0) key_alter = re.search(r"[#b]", row["globalkey"]).group(0) if re.search(r"[#b]", row["globalkey"]) else "" key_alter = key_alter.replace("b", "-") key_alter = ALT_TO_INT[key_alter] key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) - local_key = key_step + INT_TO_ALT[key_alter] + local_key = (key_step.lower() if local_key_is_minor else key_step.upper()) + INT_TO_ALT[key_alter] part.add( spt.RomanNumeral(text=row["chord"], local_key=local_key, diff --git a/partitura/score.py b/partitura/score.py index 28386b4a..06fe7041 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -3021,6 +3021,22 @@ def validate(self): def semitones(self): return INTERVAL_TO_SEMITONES[self.quality + str(self.number)] + def change_quality(self, num): + change_direction_c = ["AA", "A", "P", "d", "dd"] + change_direction_d = ["AA", "A", "m", "M", "d", "dd"] + + prev_quality = self.quality + if num == 0: + pass + else: + change_dir = change_direction_c if self.number in [1, 4, 5, 8] else change_direction_d + cur_index = change_dir.index(prev_quality) + new_index = cur_index + num + if new_index >= len(change_dir) or new_index < 0: + raise ValueError("Interval quality cannot be changed to that extent") + self.quality = change_dir[new_index] + return self + def __str__(self): return f'{super().__str__()} "{self.number}{self.quality}"' @@ -5269,8 +5285,6 @@ def is_a_within_b(a, b, wholly=False): "It": Interval(4, "A"), } - - class InvalidTimePointException(Exception): """Raised when a time point is instantiated with an invalid number.""" diff --git a/partitura/utils/globals.py b/partitura/utils/globals.py index 06f0bdf9..db7edcb7 100644 --- a/partitura/utils/globals.py +++ b/partitura/utils/globals.py @@ -468,6 +468,8 @@ ALT_TO_INT = { "--": -2, "-": -1, + "b": -1, + "bb": -2, "": 0, "#": 1, "##": 2, From e81ee2179c953d2d4155c4fe920c67645d540582 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 22 Apr 2024 17:18:06 +0200 Subject: [PATCH 158/197] Invertion of direction for interval quality change. --- partitura/score.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index 06fe7041..6de965f2 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -3022,8 +3022,8 @@ def semitones(self): return INTERVAL_TO_SEMITONES[self.quality + str(self.number)] def change_quality(self, num): - change_direction_c = ["AA", "A", "P", "d", "dd"] - change_direction_d = ["AA", "A", "m", "M", "d", "dd"] + change_direction_c = ["dd", "d", "P", "A", "AA"] + change_direction_d = ["dd", "d", "m", "M", "A", "AA"] prev_quality = self.quality if num == 0: From aeb705cea33e5fb4dfddeca26aa80f5457f5aef0 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 22 Apr 2024 17:18:29 +0200 Subject: [PATCH 159/197] Corrections for better parsing of local keys. --- partitura/io/importdcml.py | 89 +++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 7b44ecd3..df551713 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -14,22 +14,22 @@ LOCAL_KEY_TRASPOSITIONS_DCML = { "minor": { - "i": spt.Interval(1, "P"), - "ii": spt.Interval(2, "M"), - "iii": spt.Interval(3, "m"), - "iv": spt.Interval(4, "P"), - "v": spt.Interval(5, "P"), - "vi": spt.Interval(6, "m"), - "vii": spt.Interval(7, "m"), + "i": (1, "P"), + "ii": (2, "M"), + "iii": (3, "m"), + "iv": (4, "P"), + "v": (5, "P"), + "vi": (6, "m"), + "vii": (7, "m"), }, "major": { - "i": spt.Interval(1, "P"), - "ii": spt.Interval(2, "M"), - "iii": spt.Interval(3, "M"), - "iv": spt.Interval(4, "P"), - "v": spt.Interval(5, "P"), - "vi": spt.Interval(6, "M"), - "vii": spt.Interval(7, "M"), + "i": (1, "P"), + "ii": (2, "M"), + "iii": (3, "M"), + "iv": (4, "P"), + "v": (5, "P"), + "vi": (6, "M"), + "vii": (7, "M"), }, } @@ -194,6 +194,27 @@ def read_measure_tsv(measure_tsv_path, part): part.add(spt.Fine(), start=part.last_point.t) return +def process_local_key(loc_k, glob_k): + local_key_sharps = loc_k.count("#") + local_key_flats = loc_k.count("b") + local_key = loc_k.replace("#", "").replace("b", "") + local_key_is_minor = local_key.islower() + local_key = local_key.lower() + global_key_is_minor = glob_k.islower() + if local_key_is_minor == global_key_is_minor and local_key == "i": + return glob_k + g_key = "minor" if glob_k.islower() else "major" + num, qual = LOCAL_KEY_TRASPOSITIONS_DCML[g_key][local_key] + transposition_interval = spt.Interval(num, qual) + transposition_interval = transposition_interval.change_quality(local_key_sharps - local_key_flats) + key_step = re.search(r"[a-gA-G]", glob_k).group(0) + key_alter = re.search(r"[#b]", glob_k).group(0) if re.search(r"[#b]", glob_k) else "" + key_alter = key_alter.replace("b", "-") + key_alter = ALT_TO_INT[key_alter] + key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) + local_key = (key_step.lower() if local_key_is_minor else key_step.upper()) + INT_TO_ALT[key_alter] + return local_key + def read_harmony_tsv(beat_tsv_path, part): qdivs = part._quarter_durations[0] @@ -214,34 +235,32 @@ def read_harmony_tsv(beat_tsv_path, part): # and datasets. For example, a minor chord is encoded as "m" instead of "min" or "minor" # Therefore we do not add the quality to the RomanNumeral object. Then it is extracted from the text. # Local key is in relation to the global key. - local_key_sharps = row["localkey"].count("#") - local_key_flats = row["localkey"].count("b") - local_key = row["localkey"].replace("#", "").replace("b", "") - local_key_is_minor = local_key.islower() - local_key = local_key.lower() - global_key = "minor" if row["globalkey"].islower() else "major" - transposition_interval = LOCAL_KEY_TRASPOSITIONS_DCML[global_key][local_key] - transposition_interval = transposition_interval.change_quality(local_key_sharps - local_key_flats) - key_step = re.search(r"[a-gA-G]", row["globalkey"]).group(0) - key_alter = re.search(r"[#b]", row["globalkey"]).group(0) if re.search(r"[#b]", row["globalkey"]) else "" - key_alter = key_alter.replace("b", "-") - key_alter = ALT_TO_INT[key_alter] - key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) - local_key = (key_step.lower() if local_key_is_minor else key_step.upper()) + INT_TO_ALT[key_alter] + if "/" in row["localkey"]: + # if the local key has a secondary degree (e.g. "V/IV") we need to process it differently + inter_key = process_local_key(row["localkey"].split("/")[-1], row["globalkey"]) + local_key = process_local_key(row["localkey"].split("/")[0], inter_key) + else: + local_key = process_local_key(row["localkey"], row["globalkey"]) + part.add( spt.RomanNumeral(text=row["chord"], local_key=local_key, - # quality=row["chord_type"], ), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) for idx, row in data[~is_na_cad].iterrows(): - key_step = re.search(r"[a-gA-G]", row["globalkey"]).group(0) - key_alter = re.search(r"[#b]", row["globalkey"]).group(0) if re.search(r"[#b]", row["globalkey"]) else "" - key_alter = key_alter.replace("b", "-") - key_alter = ALT_TO_INT[key_alter] - key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) - local_key = key_step + INT_TO_ALT[key_alter] + if "/" in row["localkey"]: + # if the local key has a secondary degree (e.g. "V/IV") we need to process it differently + inter_key = process_local_key(row["localkey"].split("/")[-1], row["globalkey"]) + local_key = process_local_key(row["localkey"].split("/")[0], inter_key) + else: + local_key = process_local_key(row["localkey"], row["globalkey"]) + # key_step = re.search(r"[a-gA-G]", row["globalkey"]).group(0) + # key_alter = re.search(r"[#b]", row["globalkey"]).group(0) if re.search(r"[#b]", row["globalkey"]) else "" + # key_alter = key_alter.replace("b", "-") + # key_alter = ALT_TO_INT[key_alter] + # key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) + # local_key = key_step + INT_TO_ALT[key_alter] part.add( spt.Cadence(text=row["cadence"], local_key=local_key, From 1fea7d2f42607cd85fe87da02dd49efb10c849a2 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 23 Apr 2024 16:30:47 +0200 Subject: [PATCH 160/197] Filter primary degree and allow None values. --- partitura/score.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/partitura/score.py b/partitura/score.py index 6de965f2..429dde5a 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -2835,7 +2835,9 @@ def _process_primary_degree(self): if prim_d in ACCEPTED_ROMANS: return prim_d else: - return difflib.get_close_matches(prim_d, ACCEPTED_ROMANS, n=1, cutoff=0.5)[0] + matches = difflib.get_close_matches(prim_d, ACCEPTED_ROMANS, n=1, cutoff=0.5) + if matches: + return matches[0] return None def _process_secondary_degree(self): From 7ddbc59e331ab5a9f62feef11eaef1176219577b Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 23 Apr 2024 17:25:32 +0200 Subject: [PATCH 161/197] function for processing local_key relative to global. --- partitura/score.py | 75 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index 429dde5a..01a0a8b5 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -47,7 +47,7 @@ ) from partitura.utils.generic import interp1d from partitura.utils.music import transpose_note, step2pc -from partitura.utils.globals import (INT_TO_ALT, ALT_TO_INT, ACCEPTED_ROMANS) +from partitura.utils.globals import (INT_TO_ALT, ALT_TO_INT, ACCEPTED_ROMANS, LOCAL_KEY_TRASPOSITIONS_DCML) class Part(object): @@ -2909,16 +2909,28 @@ def find_root_note(self): The number of the chord. """ # Corrected step after degree2 - interval = Roman2Interval_Min[self.secondary_degree] if self.local_key.islower() else Roman2Interval_Maj[self.secondary_degree] key_step = re.search(r"[a-gA-G]", self.local_key).group(0) key_alter = re.search(r"[#b]", self.local_key).group(0) if re.search(r"[#b]", self.local_key) else "" key_alter = ALT_TO_INT[key_alter] - step, alter = transpose_note(key_step, key_alter, interval) + try: + interval = Roman2Interval_Min[self.secondary_degree] if self.local_key.islower() else Roman2Interval_Maj[self.secondary_degree] + step, alter = transpose_note(key_step, key_alter, interval) + except KeyError: + loc_k = self.secondary_degree + glob_k = self.local_key + step, alter = process_local_key(loc_k, glob_k, return_step_alter=True) # Corrected step after degree1 # TODO add support for diminished and augmented chords - interval = Roman2Interval_Min[self.primary_degree] if key_step.islower() else Roman2Interval_Maj[self.primary_degree] - step, alter = transpose_note(step, alter, interval) - root = step + INT_TO_ALT[alter] + try: + interval = Roman2Interval_Min[self.primary_degree] if self.secondary_degree.islower() else Roman2Interval_Maj[self.primary_degree] + step, alter = transpose_note(step, alter, interval) + root = step + INT_TO_ALT[alter] + except KeyError: + loc_k = self.primary_degree + glob_k = step.lower() if self.secondary_degree.islower() else step.upper() + root = step + INT_TO_ALT[alter] + root = process_local_key(loc_k, glob_k) + return root def find_bass_note(self): @@ -5239,6 +5251,30 @@ def is_a_within_b(a, b, wholly=False): return contained +def process_local_key(loc_k, glob_k, return_step_alter=False): + local_key_sharps = loc_k.count("#") + local_key_flats = loc_k.count("b") + local_key = loc_k.replace("#", "").replace("b", "") + local_key_is_minor = local_key.islower() + local_key = local_key.lower() + global_key_is_minor = glob_k.islower() + if local_key_is_minor == global_key_is_minor and local_key == "i" and local_key_sharps - local_key_flats == 0 and (not return_step_alter): + return glob_k + g_key = "minor" if glob_k.islower() else "major" + num, qual = LOCAL_KEY_TRASPOSITIONS_DCML[g_key][local_key] + transposition_interval = Interval(num, qual) + transposition_interval = transposition_interval.change_quality(local_key_sharps - local_key_flats) + key_step = re.search(r"[a-gA-G]", glob_k).group(0) + key_alter = re.search(r"[#b]", glob_k).group(0) if re.search(r"[#b]", glob_k) else "" + key_alter = key_alter.replace("b", "-") + key_alter = ALT_TO_INT[key_alter] + key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) + if return_step_alter: + return key_step, key_alter + local_key = (key_step.lower() if local_key_is_minor else key_step.upper()) + INT_TO_ALT[key_alter] + return local_key + + Roman2Interval_Maj = { "I": Interval(1, "P"), "II": Interval(2, "M"), @@ -5287,6 +5323,33 @@ def is_a_within_b(a, b, wholly=False): "It": Interval(4, "A"), } + +def process_local_key(loc_k, glob_k, return_step_alter=False): + local_key_sharps = loc_k.count("#") + local_key_flats = loc_k.count("b") + local_key = loc_k.replace("#", "").replace("b", "") + local_key_is_minor = local_key.islower() + local_key = local_key.lower() + global_key_is_minor = glob_k.islower() + if local_key_is_minor == global_key_is_minor and local_key == "i" and local_key_sharps - local_key_flats == 0 and (not return_step_alter): + return glob_k + g_key = "minor" if glob_k.islower() else "major" + # keep only letters in local_key + local_key = re.sub(r"[^a-zA-Z]", "", local_key) + num, qual = LOCAL_KEY_TRASPOSITIONS_DCML[g_key][local_key] + transposition_interval = Interval(num, qual) + transposition_interval = transposition_interval.change_quality(local_key_sharps - local_key_flats) + key_step = re.search(r"[a-gA-G]", glob_k).group(0) + key_alter = re.search(r"[#b]", glob_k).group(0) if re.search(r"[#b]", glob_k) else "" + key_alter = key_alter.replace("b", "-") + key_alter = ALT_TO_INT[key_alter] + key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) + if return_step_alter: + return key_step, key_alter + local_key = (key_step.lower() if local_key_is_minor else key_step.upper()) + INT_TO_ALT[key_alter] + return local_key + + class InvalidTimePointException(Exception): """Raised when a time point is instantiated with an invalid number.""" From 4ac526fe42b71f76cd28d2264055524647cce44d Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 23 Apr 2024 17:25:54 +0200 Subject: [PATCH 162/197] Moved DCML globals from import script. --- partitura/utils/globals.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/partitura/utils/globals.py b/partitura/utils/globals.py index db7edcb7..7ce87baa 100644 --- a/partitura/utils/globals.py +++ b/partitura/utils/globals.py @@ -481,4 +481,26 @@ 0: "", 1: "#", 2: "##", +} + + +LOCAL_KEY_TRASPOSITIONS_DCML = { + "minor": { + "i": (1, "P"), + "ii": (2, "M"), + "iii": (3, "m"), + "iv": (4, "P"), + "v": (5, "P"), + "vi": (6, "m"), + "vii": (7, "m"), + }, + "major": { + "i": (1, "P"), + "ii": (2, "M"), + "iii": (3, "M"), + "iv": (4, "P"), + "v": (5, "P"), + "vi": (6, "M"), + "vii": (7, "M"), + }, } \ No newline at end of file From 16bf465c3e652e4491375fdc3583e046b4c66ccd Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 23 Apr 2024 17:26:18 +0200 Subject: [PATCH 163/197] cleanup. --- partitura/io/importdcml.py | 47 ++------------------------------------ 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index df551713..b6db4a5e 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -3,36 +3,14 @@ import numpy as np from math import ceil import partitura.score as spt -from partitura.utils.music import estimate_symbolic_duration, transpose_note -from partitura.utils.globals import ALT_TO_INT, INT_TO_ALT +from partitura.score import process_local_key +from partitura.utils.music import estimate_symbolic_duration try: import pandas as pd except ImportError: pd = None - -LOCAL_KEY_TRASPOSITIONS_DCML = { - "minor": { - "i": (1, "P"), - "ii": (2, "M"), - "iii": (3, "m"), - "iv": (4, "P"), - "v": (5, "P"), - "vi": (6, "m"), - "vii": (7, "m"), - }, - "major": { - "i": (1, "P"), - "ii": (2, "M"), - "iii": (3, "M"), - "iv": (4, "P"), - "v": (5, "P"), - "vi": (6, "M"), - "vii": (7, "M"), - }, -} - def read_note_tsv(note_tsv_path, metadata=None): # data = np.genfromtxt(note_tsv_path, delimiter="\t", dtype=None, names=True, invalid_raise=False) # unique_durations = np.unique(data["duration"]) @@ -194,27 +172,6 @@ def read_measure_tsv(measure_tsv_path, part): part.add(spt.Fine(), start=part.last_point.t) return -def process_local_key(loc_k, glob_k): - local_key_sharps = loc_k.count("#") - local_key_flats = loc_k.count("b") - local_key = loc_k.replace("#", "").replace("b", "") - local_key_is_minor = local_key.islower() - local_key = local_key.lower() - global_key_is_minor = glob_k.islower() - if local_key_is_minor == global_key_is_minor and local_key == "i": - return glob_k - g_key = "minor" if glob_k.islower() else "major" - num, qual = LOCAL_KEY_TRASPOSITIONS_DCML[g_key][local_key] - transposition_interval = spt.Interval(num, qual) - transposition_interval = transposition_interval.change_quality(local_key_sharps - local_key_flats) - key_step = re.search(r"[a-gA-G]", glob_k).group(0) - key_alter = re.search(r"[#b]", glob_k).group(0) if re.search(r"[#b]", glob_k) else "" - key_alter = key_alter.replace("b", "-") - key_alter = ALT_TO_INT[key_alter] - key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) - local_key = (key_step.lower() if local_key_is_minor else key_step.upper()) + INT_TO_ALT[key_alter] - return local_key - def read_harmony_tsv(beat_tsv_path, part): qdivs = part._quarter_durations[0] From 8918f38fd7ba018d7db1d6f1e15883c55262d9f5 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Thu, 25 Apr 2024 12:29:46 +0200 Subject: [PATCH 164/197] extremely minor addition for "none" type mode. --- partitura/utils/music.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 34b2b691..6286b6b6 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -866,7 +866,7 @@ def key_mode_to_int(mode): """ if mode in ("minor", -1): return -1 - elif mode in ("major", None, 1): + elif mode in ("major", None, "none", 1): return 1 else: raise ValueError("Unknown mode {}".format(mode)) @@ -888,7 +888,7 @@ def key_int_to_mode(mode): """ if mode in ("minor", -1): return "minor" - elif mode in ("major", None, 1): + elif mode in ("major", None, "none", 1): return "major" else: raise ValueError("Unknown mode {}".format(mode)) From 3a67b0dbcb63d8619c98746494f1e04f448a4cb2 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sun, 12 May 2024 10:25:40 +0200 Subject: [PATCH 165/197] Removed old version of kern and replaced name to new one. --- partitura/__init__.py | 2 +- partitura/io/__init__.py | 2 +- partitura/io/importkern.py | 1336 +++++++++++++++++---------------- partitura/io/importkern_v2.py | 710 ------------------ tests/test_kern.py | 2 +- 5 files changed, 673 insertions(+), 1379 deletions(-) delete mode 100644 partitura/io/importkern_v2.py diff --git a/partitura/__init__.py b/partitura/__init__.py index 70d30e96..3d54e093 100644 --- a/partitura/__init__.py +++ b/partitura/__init__.py @@ -13,7 +13,7 @@ from .io.importmusicxml import load_musicxml, musicxml_to_notearray from .io.exportmusicxml import save_musicxml from .io.importmei import load_mei -from .io.importkern_v2 import load_kern +from .io.importkern import load_kern from .io.importmusic21 import load_music21 from .io.importmidi import load_score_midi, load_performance_midi, midi_to_notearray from .io.exportmidi import save_score_midi, save_performance_midi diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index 91e26ec3..bc71d7ad 100644 --- a/partitura/io/__init__.py +++ b/partitura/io/__init__.py @@ -11,7 +11,7 @@ from .musescore import load_via_musescore from .importmatch import load_match from .importmei import load_mei -from .importkern_v2 import load_kern +from .importkern import load_kern from .exportkern import save_kern from .importparangonada import load_parangonada_csv from .exportparangonada import save_parangonada_csv diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index 760992fe..81397b02 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -3,704 +3,708 @@ """ This module contains methods for importing Humdrum Kern files. """ -import re +import copy +import re, sys import warnings - from typing import Union, Optional - import numpy as np +from math import inf, ceil +import partitura.score as spt +from partitura.utils import PathLike, get_document_name, symbolic_to_numeric_duration + + +SIGN_TO_ACC = { + "nn": 0, + "n": 0, + "#": 1, + "s": 1, + "ss": 2, + "x": 2, + "n#": 1, + "#n": 1, + "##": 2, + "###": 3, + "b": -1, + "f": -1, + "bb": -2, + "ff": -2, + "bbb": -3, + "-": -1, + "n-": -1, + "-n": -1, + "--": -2, +} + +KERN_NOTES = { + "C": ("C", 3), + "D": ("D", 3), + "E": ("E", 3), + "F": ("F", 3), + "G": ("G", 3), + "A": ("A", 3), + "B": ("B", 3), + "c": ("C", 4), + "d": ("D", 4), + "e": ("E", 4), + "f": ("F", 4), + "g": ("G", 4), + "a": ("A", 4), + "b": ("B", 4), +} + +KERN_DURS = { + "3%2": {"type": "whole", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + "2%3": {"type": "whole", "dots": 1}, + "000": {"type": "maxima"}, + "00": {"type": "long"}, + "0": {"type": "breve"}, + "1": {"type": "whole"}, + "2": {"type": "half"}, + "4": {"type": "quarter"}, + "8": {"type": "eighth"}, + "16": {"type": "16th"}, + "32": {"type": "32nd"}, + "64": {"type": "64th"}, + "128": {"type": "128th"}, + "256": {"type": "256th"}, +} + + +def add_durations(a, b): + return a*b / (a + b) + + +def dot_function(duration, dots): + if dots == 0: + return duration + elif duration == 0: + return 0 + else: + return add_durations((2**dots)*duration, dot_function(duration, dots - 1)) + +def parse_by_voice(file, dtype=np.object_): + indices_to_remove = [] + voices = 1 + for i, line in enumerate(file): + for v in range(voices): + indices_to_remove.append([i, v]) + if any([line[v] == "*^" for v in range(voices)]): + voices += 1 + elif sum([(line[v] == "*v") for v in range(voices)]): + sum_vred = sum([line[v] == "*v" for v in range(voices)]) // 2 + voices = voices - sum_vred + + + voice_indices = np.array(indices_to_remove) + num_voices = voice_indices[:, 1].max() + 1 + data = np.empty((len(file), num_voices), dtype=dtype) + for line, voice in voice_indices: + data[line, voice] = file[line][voice] + data = data.T + if num_voices > 1: + # Copy global lines from the first voice to all other voices + cp_idx = np.char.startswith(data[0], "*") + 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 + cp_idx = np.char.startswith(data[0], "=") + for i in range(1, num_voices): + data[i][cp_idx] = data[0][cp_idx] + return data, voice_indices, num_voices + + +def _handle_kern_with_spine_splitting(kern_path): + # org_file = np.loadtxt(kern_path, dtype="U", delimiter="\n", comments="!!!", encoding="cp437") + org_file = np.genfromtxt(kern_path, dtype="U", delimiter="\n", comments="!!!", encoding="cp437") + # Get Main Number of parts and Spline Types + spline_types = org_file[0].split("\t") + parsing_idxs = [] + dtype = org_file.dtype + data = [] + file = org_file.tolist() + file = [line.split("\t") for line in file if not line.startswith("!")] + continue_parsing = True + for i in range(len(spline_types)): + # Parse by voice + d, voice_indices, num_voices = parse_by_voice(file, dtype=dtype) + data.append(d) + parsing_idxs.append([i for _ in range(num_voices)]) + # Remove all parsed cells from the file + voice_indices = voice_indices[np.lexsort((voice_indices[:, 1]*-1, voice_indices[:, 0]))] + for line, voice in voice_indices: + if voice < len(file[line]): + file[line].pop(voice) + else: + print("Line {} does not have a voice {} from original line {}".format(line, voice, org_file[line])) + data = np.vstack(data).T + parsing_idxs = np.hstack(parsing_idxs).T + return data, parsing_idxs + # + # + # # Find all expansions points + # expansion_indices = np.where(np.char.find(file, "*^") != -1)[0] + # # For all expansion points find which stream is being expanded + # expansion_streams_per_index = [np.argwhere(np.array(line.split("\t")) == "*^")[0] for line in + # file[expansion_indices]] + # + # # Find all Spline Reduction points + # reduction_indices = np.where(np.char.find(file, "*v\t*v") != -1)[0] + # # For all reduction points find which stream is being reduced + # reduction_streams_per_index = [ + # np.argwhere(np.char.add(np.array(line.split("\t")[:-1]), np.array(line.split("\t")[1:])) == "*v*v")[0] for line + # in file[reduction_indices]] + # + # # Find all pairs of expansion and reduction points + # expansion_reduction_pairs = [] + # last_exhaustive_reduction = 0 + # for expansion_index in expansion_indices: + # for expansion_stream in expansion_index: + # # Find the first reduction index that is after the expansion index and has the same index. + # for i, reduction_index in enumerate(reduction_indices[last_exhaustive_reduction:]): + # for reduction_stream in reduction_streams_per_index[i]: + # if expansion_stream == reduction_stream: + # expansion_reduction_pairs.append((expansion_index, reduction_index)) + # last_exhaustive_reduction = i if i == last_exhaustive_reduction + 1 else last_exhaustive_reduction + # break + + +def element_parsing(part, elements, total_duration_values, same_part): + divs_pq = part._quarter_durations[0] + current_tl_pos = 0 + 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: + continue + if isinstance(element, spt.GenericNote): + if total_duration_values[i] == 0: + duration_divs = symbolic_to_numeric_duration(element.symbolic_duration, divs_pq) + else: + quarter_duration = 4 / total_duration_values[i] + duration_divs = ceil(quarter_duration * divs_pq) + el_end = current_tl_pos + duration_divs + part.add(element, start=current_tl_pos, end=el_end) + current_tl_pos = el_end + elif isinstance(element, tuple): + # Chord + quarter_duration = 4 / total_duration_values[i] + duration_divs = ceil(quarter_duration * divs_pq) + el_end = current_tl_pos + duration_divs + for note in element[1]: + part.add(note, start=current_tl_pos, end=el_end) + current_tl_pos = el_end + elif isinstance(element, spt.Slur): + start_sl = element.start_note.start.t + end_sl = element.end_note.start.t + part.add(element, start=start_sl, end=end_sl) -import partitura.score as score -from partitura.utils import PathLike, get_document_name -from partitura.utils.misc import deprecated_alias, deprecated_parameter - - -__all__ = ["load_kern"] - - -class KernGlobalPart(object): - def __init__(self, doc_name, part_id, qdivs): - qdivs = int(1) if int(qdivs) == 0 else int(qdivs) - # super(KernGlobalPart, self).__init__() - self.part = score.Part(doc_name, part_id, quarter_duration=qdivs) - self.default_clef_lines = {"G": 2, "F": 4, "C": 3} - self.SIGN_TO_ACC = { - "n": 0, - "#": 1, - "s": 1, - "ss": 2, - "x": 2, - "##": 2, - "###": 3, - "b": -1, - "f": -1, - "bb": -2, - "ff": -2, - "bbb": -3, - "-": None, - } - - self.KERN_NOTES = { - "C": ("C", 3), - "D": ("D", 3), - "E": ("E", 3), - "F": ("F", 3), - "G": ("G", 3), - "A": ("A", 3), - "B": ("B", 3), - "c": ("C", 4), - "d": ("D", 4), - "e": ("E", 4), - "f": ("F", 4), - "g": ("G", 4), - "a": ("A", 4), - "b": ("B", 4), - } - - self.KERN_DURS = { - # "long": "long", - # "breve": "breve", - 0: "breve", - 1: "whole", - 2: "half", - 4: "quarter", - 8: "eighth", - 16: "16th", - 32: "32nd", - 64: "64th", - 128: "128th", - 256: "256th", - } - - -class KernParserPart(KernGlobalPart): - """ - Class for parsing kern file syntax. + else: + # Do not repeat structural elements if they are being added to the same part. + if not same_part: + part.add(element, start=current_tl_pos) + else: + if isinstance(element, spt.Measure): + current_tl_pos = measure_mapping[element.number] + + +# functions to initialize the kern parser +def load_kern( + filename: PathLike, + force_note_ids: Optional[Union[bool, str]] = None, +) -> spt.Score: """ + Parses an KERN file from path to Part. - def __init__(self, stream, init_pos, doc_name, part_id, qdivs, barline_dict=None): - super(KernParserPart, self).__init__(doc_name, part_id, qdivs) - self.position = int(init_pos) - self.parsing = "full" - self.stream = stream - self.prev_measure_pos = init_pos - self.EDITORIAL_SYMBOLS = ["x", "p", "q", "<", "(", ">", ")", "[", "]"] - # Check if part has pickup measure. - self.measure_count = ( - 0 if np.all(np.char.startswith(stream, "=1-") == False) else 1 - ) - self.last_repeat_pos = None - self.mode = None - self.barline_dict = dict() if not barline_dict else barline_dict - self.slur_dict = {"open": [], "close": []} - self.tie_dict = {"open": [], "close": []} - self.process() - - def process(self): - self.staff = None - for index, el in enumerate(self.stream): - self.current_index = index - if el.startswith("*staff"): - self.staff = eval(el[len("*staff") :]) - # elif el.startswith("!!!"): - # self._handle_fileinfo(el) - elif el.startswith("*"): - if self.staff == None: - self.staff = 1 - self._handle_glob_attr(el) - elif el.startswith("="): - self.select_parsing(el) - self._handle_barline(el) - elif " " in el: - self._handle_chord(el, index) - elif "r" in el: - self._handle_rest(el, "r-" + str(index)) + Parameters + ---------- + filename : PathLike + The path of the KERN document. + force_note_ids : (None, bool or "keep") + When True each Note in the returned Part(s) will have a newly assigned unique id attribute. + Returns + ------- + score : partitura.score.Score + The score object containing the parts. + """ + try: + # This version of the parser is faster but does not support spine splitting. + file = np.loadtxt(filename, dtype="U", delimiter="\t", comments="!!", encoding="cp437") + parsing_idxs = np.arange(file.shape[0]) + # Decide Parts + + + except ValueError: + # This version of the parser supports spine splitting but is slower. + file, parsing_idxs = _handle_kern_with_spine_splitting(filename) + + + partlist = [] + # Get Main Number of parts and Spline Types + spline_types = file[0] + + # Find parsable parts if they start with "**kern" or "**notes" + note_parts = np.char.startswith(spline_types, "**kern") | np.char.startswith(spline_types, "**notes") + # 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. + p_same_part = np.all(splines[has_instrument] == splines[has_instrument][0], axis=0) if np.any(has_instrument) else False + total_durations_list = list() + elements_list = list() + part_assignments = list() + copy_partlist = list() + for j, spline in enumerate(splines): + parser = SplineParser(size=spline.shape[-1], id="P{}".format(parsing_idxs[j]) if not p_same_part else "P{}".format(j), staff=prev_staff) + same_part = False + if parser.id in [p.id for p in copy_partlist]: + same_part = True + warnings.warn("Part {} already exists. Adding to previous Part.".format(parser.id)) + part = [p for p in copy_partlist if p.id == parser.id][0] + has_staff = np.char.startswith(spline, "*staff") + staff = int(spline[has_staff][0][6:]) if np.count_nonzero(has_staff) else 1 + if parser.staff != staff: + parser.staff = staff else: - self._handle_note(el, "n-" + str(index)) - self.nid_dict = dict( - [(n.id, n) for n in self.part.iter_all(cls=score.Note)] - + [(n.id, n) for n in self.part.iter_all(cls=score.GraceNote)] - ) - self._handle_slurs() - self._handle_ties() - - # Account for parsing priorities. - def select_parsing(self, el): - if self.parsing == "full": - return el - elif self.parsing == "right": - return el.split()[-1] + parser.voice += 1 + elements = parser.parse(spline) + unique_durs = np.unique(parser.total_duration_values).astype(int) + divs_pq = np.lcm.reduce(unique_durs) + divs_pq = divs_pq if divs_pq > 4 else 4 + # compare divs_pq to the divs_pq of the part + divs_pq = np.lcm.reduce([divs_pq, part._quarter_durations[0]]) + part.set_quarter_duration(0, divs_pq) else: - return el.split()[0] - - # TODO handle !!!info - def _handle_fileinfo(self, el): - pass - - def _handle_ties(self): - try: - if len(self.tie_dict["open"]) < len(self.tie_dict["close"]): - for index, oid in enumerate(self.tie_dict["open"]): - if ( - self.nid_dict[oid].midi_pitch - != self.nid_dict[self.tie_dict["close"][index]].midi_pitch - ): - dnote = self.nid_dict[self.tie_dict["close"][index]] - m_num = [ - m - for m in self.part.iter_all(score.Measure) - if m.start.t == self.part.measure_map(dnote.start.t)[0] - ][0].number - warnings.warn( - "Dropping Closing Tie of note {} at position {} measure {}".format( - dnote.midi_pitch, dnote.start.t, m_num - ) - ) - self.tie_dict["close"].pop(index) - self._handle_ties() - elif len(self.tie_dict["open"]) > len(self.tie_dict["close"]): - for index, cid in enumerate(self.tie_dict["close"]): - if ( - self.nid_dict[cid].midi_pitch - != self.nid_dict[self.tie_dict["open"][index]].midi_pitch - ): - dnote = self.nid_dict[self.tie_dict["open"][index]] - m_num = [ - m - for m in self.part.iter_all(score.Measure) - if m.start.t == self.part.measure_map(dnote.start.t)[0] - ][0].number - warnings.warn( - "Dropping Opening Tie of note {} at position {} measure {}".format( - dnote.midi_pitch, dnote.start.t, m_num - ) - ) - self.tie_dict["open"].pop(index) - self._handle_ties() - else: - for oid, cid in list( - zip(self.tie_dict["open"], self.tie_dict["close"]) - ): - self.nid_dict[oid].tie_next = self.nid_dict[cid] - self.nid_dict[cid].tie_prev = self.nid_dict[oid] - except Exception: - raise ValueError( - "Tie Mismatch! Uneven amount of closing to open tie brackets." - ) + has_staff = np.char.startswith(spline, "*staff") + staff = int(spline[has_staff][0][6:]) if np.count_nonzero(has_staff) else 1 + # Correction for currating multiple staffs. + if parser.staff != staff: + parser.staff = staff + prev_staff = staff + elements = parser.parse(spline) + # Routine to filter out non integer durations + unique_durs = np.unique(parser.total_duration_values) + # Remove all infinite values + unique_durs = unique_durs[np.isfinite(unique_durs)] + d_mul = 2 + while not np.all(np.isclose(unique_durs % 1, 0.0)): + unique_durs = unique_durs * d_mul + d_mul += 1 + unique_durs = unique_durs.astype(int) + + divs_pq = np.lcm.reduce(unique_durs) + divs_pq = divs_pq if divs_pq > 4 else 4 + # Initialize Part + part = spt.Part(id=parser.id, quarter_duration=divs_pq, part_name=parser.name) + + part_assignments.append(same_part) + total_durations_list.append(parser.total_duration_values) + elements_list.append(elements) + copy_partlist.append(part) + + # Currate parts to the same divs per quarter + divs_pq = np.lcm.reduce([p._quarter_durations[0] for p in copy_partlist]) + 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): + element_parsing(part, elements, total_duration_values, same_part) + + for i, part in enumerate(copy_partlist): + if part_assignments[i]: + continue + # For all measures add end time as beginning time of next measure + measures = part.measures + for i in range(len(measures) - 1): + measures[i].end = measures[i + 1].start + measures[-1].end = part.last_point + # find and add pickup measure + if part.measures[0].start.t != 0: + part.add(spt.Measure(number=0), start=0, end=part.measures[0].start.t) - def _handle_slurs(self): - if len(self.slur_dict["open"]) != len(self.slur_dict["close"]): - warnings.warn( - "Slur Mismatch! Uneven amount of closing to open slur brackets. Skipping slur parsing.", - ImportWarning, - ) - # raise ValueError( - # "Slur Mismatch! Uneven amount of closing to open slur brackets." - # ) + if parser.id not in [p.id for p in partlist]: + partlist.append(part) + + + spt.assign_note_ids( + partlist, keep=(force_note_ids is True or force_note_ids == "keep") + ) + + doc_name = get_document_name(filename) + score = spt.Score(partlist=partlist, id=doc_name) + return score + + +def rec_divisible_by_two(number): + if number % 2 == 0: + return rec_divisible_by_two(number // 2) + else: + return number + + +class SplineParser(object): + def __init__(self, id="P1", staff=1, voice=1, size=1, name=""): + self.id = id + self.name = name + self.staff = staff + self.voice = voice + self.total_duration_values = [] + self.alters = [] + self.size = size + self.total_parsed_elements = 0 + self.tie_prev = None + self.tie_next = None + self.slurs_start = [] + self.slurs_end = [] + + def parse(self, spline): + # Remove "-" lines + spline = spline[spline != '-'] + # Remove "." lines + spline = spline[spline != '.'] + # Remove Empty lines + spline = spline[spline != ''] + # Remove None lines + spline = spline[spline != None] + # Remove lines that start with "!" + spline = spline[np.char.startswith(spline, "!") == False] + # Empty Numpy array with objects + elements = np.empty(len(spline), dtype=object) + self.total_duration_values = np.ones(len(spline)) + # Find Global indices, i.e. where spline cells start with "*" and process + tandem_mask = np.char.find(spline, "*") != -1 + elements[tandem_mask] = np.vectorize(self.meta_tandem_line, otypes=[object])(spline[tandem_mask]) + # Find Barline indices, i.e. where spline cells start with "=" + bar_mask = np.char.find(spline, "=") != -1 + elements[bar_mask] = np.vectorize(self.meta_barline_line, otypes=[object])(spline[bar_mask]) + # Find Chord indices, i.e. where spline cells contain " " + chord_mask = np.char.find(spline, " ") != -1 + chord_mask = np.logical_and(chord_mask, np.logical_and(~tandem_mask, ~bar_mask)) + self.total_parsed_elements = -1 + self.note_duration_values = np.ones(len(spline[chord_mask])) + chord_num = np.count_nonzero(chord_mask) + self.tie_next = np.zeros(chord_num, dtype=bool) + self.tie_prev = np.zeros(chord_num, dtype=bool) + elements[chord_mask] = np.vectorize(self.meta_chord_line, otypes=[object])(spline[chord_mask]) + self.total_duration_values[chord_mask] = self.note_duration_values + # TODO: figure out slurs for chords + + # All the rest are note indices + note_mask = np.logical_and(~tandem_mask, np.logical_and(~bar_mask, ~chord_mask)) + self.total_parsed_elements = -1 + self.note_duration_values = np.ones(len(spline[note_mask])) + note_num = np.count_nonzero(note_mask) + self.tie_next = np.zeros(note_num, dtype=bool) + 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 + 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 ")" + open_slur_mask = np.char.find(spline[note_mask], "(") != -1 + close_slur_mask = np.char.find(spline[note_mask], ")") != -1 + self.slurs_start = np.where(open_slur_mask)[0] + self.slurs_end = np.where(close_slur_mask)[0] + # Only add slur if there is a start and end + if len(self.slurs_start) == len(self.slurs_end): + slurs = np.empty(len(self.slurs_start), dtype=object) + for i, (start, end) in enumerate(zip(self.slurs_start, self.slurs_end)): + slurs[i] = spt.Slur(notes[start], notes[end]) + # Add slurs to elements + elements = np.append(elements, slurs) else: - for oid, cid in list(zip(self.slur_dict["open"], self.slur_dict["close"])): - self.part.add(score.Slur(self.nid_dict[oid], self.nid_dict[cid])) - - def _handle_metersig(self, metersig): - m = metersig[2:] - if " " in m: - m = m.split(" ")[0] - numerator, denominator = map(eval, m.split("/")) - new_time_signature = score.TimeSignature(numerator, denominator) - self.part.add(new_time_signature, self.position) - - def _handle_barline(self, element): - if self.position > self.prev_measure_pos: - indicated_measure = re.findall("=([0-9]+)", element) - if indicated_measure != []: - m = eval(indicated_measure[0]) - 1 - barline = score.Barline(style="normal") - self.part.add(barline, self.position) - self.measure_count = m - self.barline_dict[m] = self.position + warnings.warn("Slurs openings and closings do not match. Skipping parsing slurs for this part {}.".format(self.id)) + + return elements + + def meta_tandem_line(self, line): + """ + Find all tandem lines + """ + # find number and keep its index. + self.total_parsed_elements += 1 + if line.startswith("*MM"): + rest = line[3:] + return self.process_tempo_line(rest) + elif line.startswith("*I"): + rest = line[2:] + return self.process_istrument_line(rest) + elif line.startswith("*clef"): + rest = line[5:] + return self.process_clef_line(rest) + elif line.startswith("*M"): + rest = line[2:] + return self.process_meter_line(rest) + elif line.startswith("*k"): + rest = line[2:] + return self.process_key_signature_line(rest) + elif line.startswith("*IC"): + rest = line[3:] + return self.process_istrument_class_line(rest) + elif line.startswith("*IG"): + rest = line[3:] + return self.process_istrument_group_line(rest) + elif line.startswith("*tb"): + rest = line[3:] + return self.process_timebase_line(rest) + elif line.startswith("*ITr"): + rest = line[4:] + return self.process_istrument_transpose_line(rest) + elif line.startswith("*staff"): + rest = line[6:] + return self.process_staff_line(rest) + elif line.endswith(":"): + rest = line[1:] + return self.process_key_line(rest) + elif line.startswith("*-"): + return self.process_fine() + + def process_tempo_line(self, line): + return spt.Tempo(float(line)) + + def process_fine(self): + return spt.Fine() + + def process_istrument_line(self, line): + #TODO: add support for instrument lines + return + + def process_istrument_class_line(self, line): + # TODO: add support for instrument class lines + return + + def process_istrument_group_line(self, line): + # TODO: add support for instrument group lines + return + + def process_timebase_line(self, line): + # TODO: add support for timebase lines + return + + def process_istrument_transpose_line(self, line): + # TODO: add support for instrument transpose lines + return + + def process_key_line(self, line): + find = re.search(r"([a-gA-G])", line).group(0) + # check if the key is major or minor by checking if the key is in lower or upper case. + self.mode = "minor" if find.islower() else "major" + return + + def process_staff_line(self, line): + self.staff = int(line) + return spt.Staff(self.staff) + + def process_clef_line(self, line): + # if the cleff line does not contain any of the following characters, ["G", "F", "C"], raise a ValueError. + if not any(c in line for c in ["G", "F", "C"]): + raise ValueError("Unrecognized clef: {}".format(line)) + # find the clef + clef = re.search(r"([GFC])", line).group(0) + # find the octave + has_line = re.search(r"([0-9])", line) + octave_change = "v" in line + if has_line is None: + if clef == "G": + clef_line = 2 + elif clef == "F": + clef_line = 4 + elif clef == "C": + clef_line = 3 else: - m = self.measure_count - 1 - self.part.add(score.Measure(m), self.prev_measure_pos, self.position) - self.prev_measure_pos = self.position - self.measure_count += 1 - if len(element.split()) > 1: - element = element.split()[0] - if element.endswith("!") or element == "==": - barline = score.Fine() - self.part.add(barline, self.position) - if ":|" in element: - barline = score.Repeat() - self.part.add( - barline, - self.position, - self.last_repeat_pos if self.last_repeat_pos else None, - ) - # update position for backward repeat signs - if "|:" in element: - self.last_repeat_pos = self.position - - # TODO maybe also append position for verification. - def _handle_mode(self, element): - if element[1].isupper(): - self.mode = "major" - else: - self.mode = "minor" - - def _handle_keysig(self, element): - keysig_el = element[2:] - fifths = 0 - for c in keysig_el: - if c == "#": - fifths += 1 - if c == "b": - fifths -= 1 - # TODO retrieve the key mode - mode = self.mode if self.mode else "major" - new_key_signature = score.KeySignature(fifths, mode) - self.part.add(new_key_signature, self.position) - - def _compute_clef_octave(self, dis, dis_place): - if dis is not None: - sign = -1 if dis_place == "below" else 1 - octave = sign * int(int(dis) / 8) + raise ValueError("Unrecognized clef line: {}".format(line)) else: + clef_line = has_line.group(0) + if octave_change and clef_line == 2 and clef == "G": + octave = -1 + elif octave_change: + warnings.warn("Octave change not supported for clef: {}".format(line)) octave = 0 - return octave - - def _handle_clef(self, element): - # handle the case where we have clef information - # TODO Compute Clef Octave - if element[5] not in ["G", "F", "C"]: - raise ValueError("Unknown Clef", element[5]) - if len(element) < 7: - line = self.default_clef_lines[element[5]] else: - line = int(element[6]) if element[6] != "v" else int(element[7]) - new_clef = score.Clef( - staff=self.staff, sign=element[5], line=line, octave_change=0 - ) - self.part.add(new_clef, self.position) - - def _handle_rest(self, el, rest_id): - # find duration info - duration, symbolic_duration, rtype = self._handle_duration(el) - # create rest - rest = score.Rest( - id=rest_id, - voice=1, - staff=1, - symbolic_duration=symbolic_duration, - articulations=None, - ) - # add rest to the part - self.part.add(rest, self.position, self.position + duration) - # return duration to update the position in the layer - self.position += duration - - def _handle_fermata(self, note_instance): - self.part.add(note_instance, self.position) - - def _search_slurs_and_ties(self, note, note_id): - if ")" in note: - x = note.count(")") - if len(self.slur_dict["open"]) == len(self.slur_dict["close"]) + x: - # for _ in range(x): - self.slur_dict["close"].append(note_id) - if note.startswith("("): - # acount for multiple opening brackets - n = note.count("(") - # for _ in range(n): - self.slur_dict["open"].append(note_id) - # Re-order for correct parsing - if len(self.slur_dict["open"]) > len(self.slur_dict["close"]) + 1: - warnings.warn( - "Cannot deal with nested slurs. Dropping Opening slur for note id {}".format( - self.slur_dict["open"][len(self.slur_dict["open"]) - 2] - ) - ) - self.slur_dict["open"].pop(len(self.slur_dict["open"]) - 2) - # x = note_id - # lenc = len(self.slur_dict["open"]) - len(self.slur_dict["close"]) - # self.slur_dict["open"][:lenc - 1] = self.slur_dict["open"][1:lenc] - # self.slur_dict["open"][lenc] = x - note = note[n:] - if "]" in note: - self.tie_dict["close"].append(note_id) - elif "_" in note: - self.tie_dict["open"].append(note_id) - self.tie_dict["close"].append(note_id) - if note.startswith("["): - self.tie_dict["open"].append(note_id) - note = note[1:] - return note + octave = 0 - def _handle_duration(self, note, isgrace=False): - foundRational = re.search(r'(\d+)%(\d+)', note) - if foundRational: - ntype = note[foundRational.span()[-1]:] - durationFirst = int(foundRational.group(1)) - durationSecond = float(foundRational.group(2)) - dur = 4 * durationSecond / durationFirst - else: - _, dur, ntype = re.split("(\d+)", note) - ntype = _ + ntype if isgrace else ntype - dur = eval(dur) + return spt.Clef(sign=clef, staff=self.staff, line=int(clef_line), octave_change=octave) - if dur in self.KERN_DURS.keys(): - symbolic_duration = {"type": self.KERN_DURS[dur]} + def process_key_signature_line(self, line): + fifths = line.count("#") - line.count("-") + alters = re.findall(r"([a-gA-G#\-]+)", line) + alters = "".join(alters) + # split alters by two characters + self.alters = [alters[i:i + 2] for i in range(0, len(alters), 2)] + # TODO retrieve the key mode + mode = "major" + return spt.KeySignature(fifths, mode) + + def process_meter_line(self, line): + if " " in line: + line = line.split(" ")[0] + numerator, denominator = line.split("/") + # Find digits in numerator and denominator and convert to int + numerator = int(re.search(r"([0-9]+)", numerator).group(0)) + denominator = int(re.search(r"([0-9]+)", denominator).group(0)) + return spt.TimeSignature(numerator, denominator) + + def _process_kern_pitch(self, pitch): + # find accidentals + alter = re.search(r"([n#-]+)", pitch) + # remove alter from pitch + pitch = pitch.replace(alter.group(0), "") if alter else pitch + step, octave = KERN_NOTES[pitch[0]] + # do_alt = (step + alter.group(0)).lower() not in self.alters if alter else False + if octave == 4: + octave = octave + pitch.count(pitch[0]) - 1 + elif octave == 3: + octave = octave - pitch.count(pitch[0]) + 1 + alter = SIGN_TO_ACC[alter.group(0)] if alter is not None else None + return step, octave, alter + + def _process_kern_duration(self, duration, is_grace=False): + dots = duration.count(".") + dur = duration.replace(".", "") + if dur in KERN_DURS.keys(): + symbolic_duration = copy.deepcopy(KERN_DURS[dur]) else: + dur = float(dur) + key_loolup = [2 ** i for i in range(0, 9)] diff = dict( ( map( - lambda x: (dur - x, x) if dur > x else (dur + x, x), - self.KERN_DURS.keys(), + lambda x: (dur - x, str(x)) if dur > x else (dur + x, str(x)), + key_loolup, ) ) ) - symbolic_duration = { - "type": self.KERN_DURS[diff[min(list(diff.keys()))]], - "actual_notes": dur / 4, - "normal_notes": diff[min(list(diff.keys()))] / 4, - } - - # calculate duration to divs. - qdivs = self.part._quarter_durations[0] - duration = qdivs * 4 / dur if dur != 0 else qdivs * 8 - if "." in note: - symbolic_duration["dots"] = note.count(".") - ntype = ntype[note.count(".") :] - d = duration - for i in range(symbolic_duration["dots"]): - d = d / 2 - duration += d + + symbolic_duration = copy.deepcopy(KERN_DURS[diff[min(list(diff.keys()))]]) + symbolic_duration["actual_notes"] = int(dur // 4) + symbolic_duration["normal_notes"] = int(diff[min(list(diff.keys()))]) // 4 + if dots: + symbolic_duration["dots"] = dots + self.note_duration_values[self.total_parsed_elements] = dot_function((float(dur) if isinstance(dur, str) else dur), dots) if not is_grace else inf + return symbolic_duration + + def process_symbol(self, note, symbols): + """ + Process the symbols of a note. + + Parameters + ---------- + note + symbol + + Returns + ------- + + """ + if "[" in symbols: + self.tie_prev[self.total_parsed_elements] = True + # pop symbol and call again + symbols.pop(symbols.index("[")) + self.process_symbol(note, symbols) + if "]" in symbols: + self.tie_next[self.total_parsed_elements] = True + symbols.pop(symbols.index("]")) + self.process_symbol(note, symbols) + if "_" in symbols: + # continuing tie + self.tie_prev[self.total_parsed_elements] = True + self.tie_next[self.total_parsed_elements] = True + symbols.pop(symbols.index("_")) + self.process_symbol(note, symbols) + return + + def meta_note_line(self, line, voice=None, add=True): + """ + Grammar Defining a note line. + + A note line is specified by the following grammar: + note_line = symbol | duration | pitch | symbol + + Parameters + ---------- + line + + Returns + ------- + + """ + self.total_parsed_elements += 1 if add else 0 + voice = self.voice if voice is None else voice + # extract first occurence of one of the following: a-g A-G r # - n + find_pitch = re.search(r"([a-gA-Gr\-n#]+)", line) + if find_pitch is None: + warnings.warn("No pitch found in line: {}, transforming to a rest".format(line)) + pitch = "r" else: - symbolic_duration["dots"] = 0 - if isinstance(duration, float): - if not duration.is_integer(): - raise ValueError("Duration divs is not an integer, {}".format(duration)) - # Check that duration is same as int - assert int(duration) == duration - return int(duration), symbolic_duration, ntype - - # TODO Handle beams and tuplets. - - def _handle_note(self, note, note_id, voice=1): - if note == "." or note == "" or note == " ": - return - has_fermata = ";" in note - note = self._search_slurs_and_ties(note, note_id) - grace_attr = "q" in note # or "p" in note # for appoggiatura not sure yet. - duration, symbolic_duration, ntype = self._handle_duration(note, grace_attr) - # Remove editorial symbols from string, i.e. "x" - for x in self.EDITORIAL_SYMBOLS: - ntype = ntype.replace(x, "") - step, octave = self.KERN_NOTES[ntype[0]] - if octave == 4: - octave = octave + ntype.count(ntype[0]) - 1 - elif octave == 3: - octave = octave - ntype.count(ntype[0]) + 1 - alter = ntype.count("#") - ntype.count("-") - # find if it's grace - if not grace_attr: - # create normal note - note = score.Note( - step=step, - octave=octave, - alter=alter, - id=note_id, - voice=int(voice), - staff=self.staff, - symbolic_duration=symbolic_duration, - articulations=None, # TODO : add articulation - ) - if has_fermata: - self._handle_fermata(note) + pitch = find_pitch.group(0) + # extract duration can be any of the following: 0-9 . + dur_search = re.search(r"([0-9.%]+)", line) + # if no duration is found, then the duration is 8 by default (for grace notes with no duration) + duration = dur_search.group(0) if dur_search else "8" + # extract symbol can be any of the following: _()[]{}<>|: + symbols = re.findall(r"([_()\[\]{}<>|:])", line) + symbolic_duration = self._process_kern_duration(duration, is_grace="q" in line) + el_id = "{}-s{}-v{}-el{}".format(self.id, self.staff, voice, self.total_parsed_elements) + if pitch.startswith("r"): + return spt.Rest(symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) + step, octave, alter = self._process_kern_pitch(pitch) + # check if the note is a grace note + if "q" in line: + note = spt.GraceNote(grace_type="grace", step=step, octave=octave, alter=alter, symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) else: - # create grace note - if "p" in ntype: - grace_type = "acciaccatura" - elif "q" in ntype: - grace_type = "appoggiatura" - note = score.GraceNote( - grace_type=grace_type, - step=step, - octave=octave, - alter=alter, - id=note_id, - voice=1, - staff=self.staff, - symbolic_duration=symbolic_duration, - articulations=None, # TODO : add articulation - ) - duration = 0 - - self.part.add(note, self.position, self.position + duration) - self.position += duration - - def _handle_chord(self, chord, id): - notes = chord.split() - position_history = list() - pos = self.position - for i, note_el in enumerate(notes): - id_new = "c-" + str(i) + "-" + str(id) - self.position = pos - if "r" in note_el: - self._handle_rest(note_el, id_new) - else: - self._handle_note(note_el, id_new, voice=int(i)) - if note_el != ".": - position_history.append(self.position) - # To account for Voice changes and alternate voice order. - self.position = min(position_history) if position_history else self.position - - def _handle_glob_attr(self, el): - if el.startswith("*clef"): - self._handle_clef(el) - elif el.startswith("*k"): - self._handle_keysig(el) - elif el.startswith("*MM"): - pass - elif el.startswith("*M"): - self._handle_metersig(el) - elif el.endswith(":"): - self._handle_mode(el) - elif el.startswith("*S/sic"): - self.parsing = "left" - elif el.startswith("*S/ossia"): - self.parsing = "right" - elif el.startswith("Xstrophe"): - self.parsing = "full" - - -class KernParser: - def __init__(self, document, doc_name): - self.document = document - self.doc_name = doc_name - self.qdivs = self.find_lcm(document.flatten()) - # TODO review this code - self.DIVS2Q = { - 1: 0.25, - 2: 0.5, - 4: 1, - 6: 1.5, - 8: 2, - 16: 4, - 24: 6, - 32: 8, - 48: 12, - 64: 16, - 128: 32, - 256: 64, - } - # self.qdivs = - self.parts = self.process() - - def __getitem__(self, item): - return self.parts[item] - - def process(self): - # TODO handle pickup - # has_pickup = not np.all(np.char.startswith(self.document, "=1-") == False) - # if not has_pickup: - # position = 0 - # else: - # position = self._handle_pickup_position() - position = 0 - # Add for parallel processing - parts = [ - self.collect(self.document[i], position, str(i), self.doc_name) - for i in reversed(range(self.document.shape[0])) - ] - return [p for p in parts if p] - - def add2part(self, part, unprocessed): - flatten = [item for sublist in unprocessed for item in sublist] - if unprocessed: - new_part = KernParserPart( - flatten, 0, self.doc_name, "x", self.qdivs, part.barline_dict - ) - self.parts.append(new_part) + note = spt.Note(step, octave, alter, symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) + if symbols: + self.process_symbol(note, symbols) + return note - def collect(self, doc, pos, id, doc_name): - if doc[0] == "**kern": - qdivs = self.find_lcm(doc) if self.qdivs is None else self.qdivs - x = KernParserPart(doc, pos, id, doc_name, qdivs).part - return x + def meta_barline_line(self, line): + """ + Grammar Defining a barline line. - # TODO handle position of pick-up measure? - def _handle_pickup_position(self): - return 0 + A barline line is specified by the following grammar: + barline_line = repeat | barline | number | repeat - def find_lcm(self, doc): - kern_string = "-".join([row for row in doc]) - match = re.findall(r"([0-9]+)([a-g]|[A-G]|r|\.)", kern_string) - durs, _ = zip(*match) - x = np.array(list(map(lambda x: int(x), durs))) - divs = np.lcm.reduce(np.unique(x[x != 0])) - return float(divs) # / 4.00 + Parameters + ---------- + line + Returns + ------- -# functions to initialize the kern parser -def parse_kern(kern_path: PathLike) -> np.ndarray: - """ - Parses an KERN file from path to an regular expression. + """ + # find number and keep its index. + self.total_parsed_elements += 1 + number = re.findall(r"([0-9]+)", line) + number_index = line.index(number[0]) if number else line.index("=") + closing_repeat = re.findall(r"[:|]", line[:number_index]) + opening_repeat = re.findall(r"[|:]", line[number_index:]) + return spt.Measure(number=int(number[0]) if number else None) - Parameters - ---------- - kern_path : PathLike - The path of the KERN document. - Returns - ------- - continuous_parts : numpy character array - non_continuous_parts : list - """ - with open(kern_path, encoding="cp437") as file: - lines = file.read().splitlines() - d = [line.split("\t") for line in lines if not line.startswith("!")] - striped_parts = list() - merge_index = [] - for x in d: - if merge_index: - for midx in merge_index: - x[midx] = x[midx] + " " + x[midx + 1] - y = [el for i, el in enumerate(x) if i - 1 not in merge_index] - striped_parts.append(y) - else: - striped_parts.append(x) - if "*^" in x or "*+" in x: - # Accounting for multiple voice ups at the same time. - already_parsed = 0 - for i, el in enumerate(x): - # Some faulty kerns create an extra part half way through the score. - # We choose for the moment to add it to the closest column part. - if el == "*^" or el == "*+": - k = i - if merge_index: - if k < min(merge_index): - merge_index = [midx + 1 for midx in merge_index] - k = i + already_parsed - merge_index.append(k) - already_parsed += 1 - merge_index = sorted(merge_index) - if "*v *v" in x: - k = x.index("*v *v") - temp = list() - for i in merge_index: - if i > k: - temp.append(i - 1) - elif i < k: - temp.append(i) - merge_index = temp - - # Final filter for mistabs and inconsistent tabs that would create - # extra empty voice and would mess the parsing. - striped_parts = [[el for el in part if el != ""] for part in striped_parts] - numpy_parts = np.array(list(zip(striped_parts))).squeeze(1).T - return numpy_parts - - -def parse_kern_v2(kern_path: PathLike) -> np.ndarray: - """ - Parses an KERN file from path to an regular expression. + def meta_chord_line(self, line): + """ + Grammar Defining a chord line. - Parameters - ---------- - kern_path : PathLike - The path of the KERN document. - Returns - ------- - continuous_parts : numpy character array - non_continuous_parts : list - """ - with open(kern_path, encoding="cp437") as file: - lines = file.read().splitlines() - if lines[0][0] == "<": - # we are using a heuristic to stop the import if we are dealing with a XML file - raise Exception("Invalid Kern file") - document_lines = [line.split("\t") for line in lines if not line.startswith("!")] - number_of_voices = len(document_lines[0]) - number_of_lines = len(document_lines) - out = np.empty((number_of_lines, number_of_voices), dtype=" score.Score: - """Parse a Kern file and build a composite score ontology - structure from it (see also scoreontology.py). + A chord line is specified by the following grammar: + chord_line = note | chord - Parameters - ---------- - filename : PathLike - Path to the Kern file to be parsed - force_note_ids : (bool, 'keep') optional. - When True each Note in the returned Part(s) will have a newly - assigned unique id attribute. Existing note id attributes in - the Kern will be discarded. If 'keep', only notes without - a note id will be assigned one. - - Returns - ------- - scr: :class:`partitura.score.Score` - A `Score` object - """ - # parse kern file - numpy_parts = parse_kern_v2(filename) - # doc_name = os.path.basename(filename[:-4]) - doc_name = get_document_name(filename) - parser = KernParser(numpy_parts, doc_name) - partlist = parser.parts - - score.assign_note_ids( - partlist, keep=(force_note_ids is True or force_note_ids == "keep") - ) + Parameters + ---------- + line - # TODO: Parse score info (composer, lyricist, etc.) - scr = score.Score(id=doc_name, partlist=partlist) + Returns + ------- - return scr + """ + self.total_parsed_elements += 1 + chord = ("c", [self.meta_note_line(n, add=False) for n in line.split(" ")]) + return chord diff --git a/partitura/io/importkern_v2.py b/partitura/io/importkern_v2.py deleted file mode 100644 index 81397b02..00000000 --- a/partitura/io/importkern_v2.py +++ /dev/null @@ -1,710 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -This module contains methods for importing Humdrum Kern files. -""" -import copy -import re, sys -import warnings -from typing import Union, Optional -import numpy as np -from math import inf, ceil -import partitura.score as spt -from partitura.utils import PathLike, get_document_name, symbolic_to_numeric_duration - - -SIGN_TO_ACC = { - "nn": 0, - "n": 0, - "#": 1, - "s": 1, - "ss": 2, - "x": 2, - "n#": 1, - "#n": 1, - "##": 2, - "###": 3, - "b": -1, - "f": -1, - "bb": -2, - "ff": -2, - "bbb": -3, - "-": -1, - "n-": -1, - "-n": -1, - "--": -2, -} - -KERN_NOTES = { - "C": ("C", 3), - "D": ("D", 3), - "E": ("E", 3), - "F": ("F", 3), - "G": ("G", 3), - "A": ("A", 3), - "B": ("B", 3), - "c": ("C", 4), - "d": ("D", 4), - "e": ("E", 4), - "f": ("F", 4), - "g": ("G", 4), - "a": ("A", 4), - "b": ("B", 4), -} - -KERN_DURS = { - "3%2": {"type": "whole", "dots": 0, "actual_notes": 3, "normal_notes": 2}, - "2%3": {"type": "whole", "dots": 1}, - "000": {"type": "maxima"}, - "00": {"type": "long"}, - "0": {"type": "breve"}, - "1": {"type": "whole"}, - "2": {"type": "half"}, - "4": {"type": "quarter"}, - "8": {"type": "eighth"}, - "16": {"type": "16th"}, - "32": {"type": "32nd"}, - "64": {"type": "64th"}, - "128": {"type": "128th"}, - "256": {"type": "256th"}, -} - - -def add_durations(a, b): - return a*b / (a + b) - - -def dot_function(duration, dots): - if dots == 0: - return duration - elif duration == 0: - return 0 - else: - return add_durations((2**dots)*duration, dot_function(duration, dots - 1)) - -def parse_by_voice(file, dtype=np.object_): - indices_to_remove = [] - voices = 1 - for i, line in enumerate(file): - for v in range(voices): - indices_to_remove.append([i, v]) - if any([line[v] == "*^" for v in range(voices)]): - voices += 1 - elif sum([(line[v] == "*v") for v in range(voices)]): - sum_vred = sum([line[v] == "*v" for v in range(voices)]) // 2 - voices = voices - sum_vred - - - voice_indices = np.array(indices_to_remove) - num_voices = voice_indices[:, 1].max() + 1 - data = np.empty((len(file), num_voices), dtype=dtype) - for line, voice in voice_indices: - data[line, voice] = file[line][voice] - data = data.T - if num_voices > 1: - # Copy global lines from the first voice to all other voices - cp_idx = np.char.startswith(data[0], "*") - 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 - cp_idx = np.char.startswith(data[0], "=") - for i in range(1, num_voices): - data[i][cp_idx] = data[0][cp_idx] - return data, voice_indices, num_voices - - -def _handle_kern_with_spine_splitting(kern_path): - # org_file = np.loadtxt(kern_path, dtype="U", delimiter="\n", comments="!!!", encoding="cp437") - org_file = np.genfromtxt(kern_path, dtype="U", delimiter="\n", comments="!!!", encoding="cp437") - # Get Main Number of parts and Spline Types - spline_types = org_file[0].split("\t") - parsing_idxs = [] - dtype = org_file.dtype - data = [] - file = org_file.tolist() - file = [line.split("\t") for line in file if not line.startswith("!")] - continue_parsing = True - for i in range(len(spline_types)): - # Parse by voice - d, voice_indices, num_voices = parse_by_voice(file, dtype=dtype) - data.append(d) - parsing_idxs.append([i for _ in range(num_voices)]) - # Remove all parsed cells from the file - voice_indices = voice_indices[np.lexsort((voice_indices[:, 1]*-1, voice_indices[:, 0]))] - for line, voice in voice_indices: - if voice < len(file[line]): - file[line].pop(voice) - else: - print("Line {} does not have a voice {} from original line {}".format(line, voice, org_file[line])) - data = np.vstack(data).T - parsing_idxs = np.hstack(parsing_idxs).T - return data, parsing_idxs - # - # - # # Find all expansions points - # expansion_indices = np.where(np.char.find(file, "*^") != -1)[0] - # # For all expansion points find which stream is being expanded - # expansion_streams_per_index = [np.argwhere(np.array(line.split("\t")) == "*^")[0] for line in - # file[expansion_indices]] - # - # # Find all Spline Reduction points - # reduction_indices = np.where(np.char.find(file, "*v\t*v") != -1)[0] - # # For all reduction points find which stream is being reduced - # reduction_streams_per_index = [ - # np.argwhere(np.char.add(np.array(line.split("\t")[:-1]), np.array(line.split("\t")[1:])) == "*v*v")[0] for line - # in file[reduction_indices]] - # - # # Find all pairs of expansion and reduction points - # expansion_reduction_pairs = [] - # last_exhaustive_reduction = 0 - # for expansion_index in expansion_indices: - # for expansion_stream in expansion_index: - # # Find the first reduction index that is after the expansion index and has the same index. - # for i, reduction_index in enumerate(reduction_indices[last_exhaustive_reduction:]): - # for reduction_stream in reduction_streams_per_index[i]: - # if expansion_stream == reduction_stream: - # expansion_reduction_pairs.append((expansion_index, reduction_index)) - # last_exhaustive_reduction = i if i == last_exhaustive_reduction + 1 else last_exhaustive_reduction - # break - - -def element_parsing(part, elements, total_duration_values, same_part): - divs_pq = part._quarter_durations[0] - current_tl_pos = 0 - 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: - continue - if isinstance(element, spt.GenericNote): - if total_duration_values[i] == 0: - duration_divs = symbolic_to_numeric_duration(element.symbolic_duration, divs_pq) - else: - quarter_duration = 4 / total_duration_values[i] - duration_divs = ceil(quarter_duration * divs_pq) - el_end = current_tl_pos + duration_divs - part.add(element, start=current_tl_pos, end=el_end) - current_tl_pos = el_end - elif isinstance(element, tuple): - # Chord - quarter_duration = 4 / total_duration_values[i] - duration_divs = ceil(quarter_duration * divs_pq) - el_end = current_tl_pos + duration_divs - for note in element[1]: - part.add(note, start=current_tl_pos, end=el_end) - current_tl_pos = el_end - elif isinstance(element, spt.Slur): - start_sl = element.start_note.start.t - end_sl = element.end_note.start.t - part.add(element, start=start_sl, end=end_sl) - - else: - # Do not repeat structural elements if they are being added to the same part. - if not same_part: - part.add(element, start=current_tl_pos) - else: - if isinstance(element, spt.Measure): - current_tl_pos = measure_mapping[element.number] - - -# functions to initialize the kern parser -def load_kern( - filename: PathLike, - force_note_ids: Optional[Union[bool, str]] = None, -) -> spt.Score: - """ - Parses an KERN file from path to Part. - - Parameters - ---------- - filename : PathLike - The path of the KERN document. - force_note_ids : (None, bool or "keep") - When True each Note in the returned Part(s) will have a newly assigned unique id attribute. - Returns - ------- - score : partitura.score.Score - The score object containing the parts. - """ - try: - # This version of the parser is faster but does not support spine splitting. - file = np.loadtxt(filename, dtype="U", delimiter="\t", comments="!!", encoding="cp437") - parsing_idxs = np.arange(file.shape[0]) - # Decide Parts - - - except ValueError: - # This version of the parser supports spine splitting but is slower. - file, parsing_idxs = _handle_kern_with_spine_splitting(filename) - - - partlist = [] - # Get Main Number of parts and Spline Types - spline_types = file[0] - - # Find parsable parts if they start with "**kern" or "**notes" - note_parts = np.char.startswith(spline_types, "**kern") | np.char.startswith(spline_types, "**notes") - # 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. - p_same_part = np.all(splines[has_instrument] == splines[has_instrument][0], axis=0) if np.any(has_instrument) else False - total_durations_list = list() - elements_list = list() - part_assignments = list() - copy_partlist = list() - for j, spline in enumerate(splines): - parser = SplineParser(size=spline.shape[-1], id="P{}".format(parsing_idxs[j]) if not p_same_part else "P{}".format(j), staff=prev_staff) - same_part = False - if parser.id in [p.id for p in copy_partlist]: - same_part = True - warnings.warn("Part {} already exists. Adding to previous Part.".format(parser.id)) - part = [p for p in copy_partlist if p.id == parser.id][0] - has_staff = np.char.startswith(spline, "*staff") - staff = int(spline[has_staff][0][6:]) if np.count_nonzero(has_staff) else 1 - if parser.staff != staff: - parser.staff = staff - else: - parser.voice += 1 - elements = parser.parse(spline) - unique_durs = np.unique(parser.total_duration_values).astype(int) - divs_pq = np.lcm.reduce(unique_durs) - divs_pq = divs_pq if divs_pq > 4 else 4 - # compare divs_pq to the divs_pq of the part - divs_pq = np.lcm.reduce([divs_pq, part._quarter_durations[0]]) - part.set_quarter_duration(0, divs_pq) - else: - has_staff = np.char.startswith(spline, "*staff") - staff = int(spline[has_staff][0][6:]) if np.count_nonzero(has_staff) else 1 - # Correction for currating multiple staffs. - if parser.staff != staff: - parser.staff = staff - prev_staff = staff - elements = parser.parse(spline) - # Routine to filter out non integer durations - unique_durs = np.unique(parser.total_duration_values) - # Remove all infinite values - unique_durs = unique_durs[np.isfinite(unique_durs)] - d_mul = 2 - while not np.all(np.isclose(unique_durs % 1, 0.0)): - unique_durs = unique_durs * d_mul - d_mul += 1 - unique_durs = unique_durs.astype(int) - - divs_pq = np.lcm.reduce(unique_durs) - divs_pq = divs_pq if divs_pq > 4 else 4 - # Initialize Part - part = spt.Part(id=parser.id, quarter_duration=divs_pq, part_name=parser.name) - - part_assignments.append(same_part) - total_durations_list.append(parser.total_duration_values) - elements_list.append(elements) - copy_partlist.append(part) - - # Currate parts to the same divs per quarter - divs_pq = np.lcm.reduce([p._quarter_durations[0] for p in copy_partlist]) - 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): - element_parsing(part, elements, total_duration_values, same_part) - - for i, part in enumerate(copy_partlist): - if part_assignments[i]: - continue - # For all measures add end time as beginning time of next measure - measures = part.measures - for i in range(len(measures) - 1): - measures[i].end = measures[i + 1].start - measures[-1].end = part.last_point - # find and add pickup measure - if part.measures[0].start.t != 0: - part.add(spt.Measure(number=0), start=0, end=part.measures[0].start.t) - - if parser.id not in [p.id for p in partlist]: - partlist.append(part) - - - spt.assign_note_ids( - partlist, keep=(force_note_ids is True or force_note_ids == "keep") - ) - - doc_name = get_document_name(filename) - score = spt.Score(partlist=partlist, id=doc_name) - return score - - -def rec_divisible_by_two(number): - if number % 2 == 0: - return rec_divisible_by_two(number // 2) - else: - return number - - -class SplineParser(object): - def __init__(self, id="P1", staff=1, voice=1, size=1, name=""): - self.id = id - self.name = name - self.staff = staff - self.voice = voice - self.total_duration_values = [] - self.alters = [] - self.size = size - self.total_parsed_elements = 0 - self.tie_prev = None - self.tie_next = None - self.slurs_start = [] - self.slurs_end = [] - - def parse(self, spline): - # Remove "-" lines - spline = spline[spline != '-'] - # Remove "." lines - spline = spline[spline != '.'] - # Remove Empty lines - spline = spline[spline != ''] - # Remove None lines - spline = spline[spline != None] - # Remove lines that start with "!" - spline = spline[np.char.startswith(spline, "!") == False] - # Empty Numpy array with objects - elements = np.empty(len(spline), dtype=object) - self.total_duration_values = np.ones(len(spline)) - # Find Global indices, i.e. where spline cells start with "*" and process - tandem_mask = np.char.find(spline, "*") != -1 - elements[tandem_mask] = np.vectorize(self.meta_tandem_line, otypes=[object])(spline[tandem_mask]) - # Find Barline indices, i.e. where spline cells start with "=" - bar_mask = np.char.find(spline, "=") != -1 - elements[bar_mask] = np.vectorize(self.meta_barline_line, otypes=[object])(spline[bar_mask]) - # Find Chord indices, i.e. where spline cells contain " " - chord_mask = np.char.find(spline, " ") != -1 - chord_mask = np.logical_and(chord_mask, np.logical_and(~tandem_mask, ~bar_mask)) - self.total_parsed_elements = -1 - self.note_duration_values = np.ones(len(spline[chord_mask])) - chord_num = np.count_nonzero(chord_mask) - self.tie_next = np.zeros(chord_num, dtype=bool) - self.tie_prev = np.zeros(chord_num, dtype=bool) - elements[chord_mask] = np.vectorize(self.meta_chord_line, otypes=[object])(spline[chord_mask]) - self.total_duration_values[chord_mask] = self.note_duration_values - # TODO: figure out slurs for chords - - # All the rest are note indices - note_mask = np.logical_and(~tandem_mask, np.logical_and(~bar_mask, ~chord_mask)) - self.total_parsed_elements = -1 - self.note_duration_values = np.ones(len(spline[note_mask])) - note_num = np.count_nonzero(note_mask) - self.tie_next = np.zeros(note_num, dtype=bool) - 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 - 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 ")" - open_slur_mask = np.char.find(spline[note_mask], "(") != -1 - close_slur_mask = np.char.find(spline[note_mask], ")") != -1 - self.slurs_start = np.where(open_slur_mask)[0] - self.slurs_end = np.where(close_slur_mask)[0] - # Only add slur if there is a start and end - if len(self.slurs_start) == len(self.slurs_end): - slurs = np.empty(len(self.slurs_start), dtype=object) - for i, (start, end) in enumerate(zip(self.slurs_start, self.slurs_end)): - slurs[i] = spt.Slur(notes[start], notes[end]) - # Add slurs to elements - elements = np.append(elements, slurs) - else: - warnings.warn("Slurs openings and closings do not match. Skipping parsing slurs for this part {}.".format(self.id)) - - return elements - - def meta_tandem_line(self, line): - """ - Find all tandem lines - """ - # find number and keep its index. - self.total_parsed_elements += 1 - if line.startswith("*MM"): - rest = line[3:] - return self.process_tempo_line(rest) - elif line.startswith("*I"): - rest = line[2:] - return self.process_istrument_line(rest) - elif line.startswith("*clef"): - rest = line[5:] - return self.process_clef_line(rest) - elif line.startswith("*M"): - rest = line[2:] - return self.process_meter_line(rest) - elif line.startswith("*k"): - rest = line[2:] - return self.process_key_signature_line(rest) - elif line.startswith("*IC"): - rest = line[3:] - return self.process_istrument_class_line(rest) - elif line.startswith("*IG"): - rest = line[3:] - return self.process_istrument_group_line(rest) - elif line.startswith("*tb"): - rest = line[3:] - return self.process_timebase_line(rest) - elif line.startswith("*ITr"): - rest = line[4:] - return self.process_istrument_transpose_line(rest) - elif line.startswith("*staff"): - rest = line[6:] - return self.process_staff_line(rest) - elif line.endswith(":"): - rest = line[1:] - return self.process_key_line(rest) - elif line.startswith("*-"): - return self.process_fine() - - def process_tempo_line(self, line): - return spt.Tempo(float(line)) - - def process_fine(self): - return spt.Fine() - - def process_istrument_line(self, line): - #TODO: add support for instrument lines - return - - def process_istrument_class_line(self, line): - # TODO: add support for instrument class lines - return - - def process_istrument_group_line(self, line): - # TODO: add support for instrument group lines - return - - def process_timebase_line(self, line): - # TODO: add support for timebase lines - return - - def process_istrument_transpose_line(self, line): - # TODO: add support for instrument transpose lines - return - - def process_key_line(self, line): - find = re.search(r"([a-gA-G])", line).group(0) - # check if the key is major or minor by checking if the key is in lower or upper case. - self.mode = "minor" if find.islower() else "major" - return - - def process_staff_line(self, line): - self.staff = int(line) - return spt.Staff(self.staff) - - def process_clef_line(self, line): - # if the cleff line does not contain any of the following characters, ["G", "F", "C"], raise a ValueError. - if not any(c in line for c in ["G", "F", "C"]): - raise ValueError("Unrecognized clef: {}".format(line)) - # find the clef - clef = re.search(r"([GFC])", line).group(0) - # find the octave - has_line = re.search(r"([0-9])", line) - octave_change = "v" in line - if has_line is None: - if clef == "G": - clef_line = 2 - elif clef == "F": - clef_line = 4 - elif clef == "C": - clef_line = 3 - else: - raise ValueError("Unrecognized clef line: {}".format(line)) - else: - clef_line = has_line.group(0) - if octave_change and clef_line == 2 and clef == "G": - octave = -1 - elif octave_change: - warnings.warn("Octave change not supported for clef: {}".format(line)) - octave = 0 - else: - octave = 0 - - return spt.Clef(sign=clef, staff=self.staff, line=int(clef_line), octave_change=octave) - - def process_key_signature_line(self, line): - fifths = line.count("#") - line.count("-") - alters = re.findall(r"([a-gA-G#\-]+)", line) - alters = "".join(alters) - # split alters by two characters - self.alters = [alters[i:i + 2] for i in range(0, len(alters), 2)] - # TODO retrieve the key mode - mode = "major" - return spt.KeySignature(fifths, mode) - - def process_meter_line(self, line): - if " " in line: - line = line.split(" ")[0] - numerator, denominator = line.split("/") - # Find digits in numerator and denominator and convert to int - numerator = int(re.search(r"([0-9]+)", numerator).group(0)) - denominator = int(re.search(r"([0-9]+)", denominator).group(0)) - return spt.TimeSignature(numerator, denominator) - - def _process_kern_pitch(self, pitch): - # find accidentals - alter = re.search(r"([n#-]+)", pitch) - # remove alter from pitch - pitch = pitch.replace(alter.group(0), "") if alter else pitch - step, octave = KERN_NOTES[pitch[0]] - # do_alt = (step + alter.group(0)).lower() not in self.alters if alter else False - if octave == 4: - octave = octave + pitch.count(pitch[0]) - 1 - elif octave == 3: - octave = octave - pitch.count(pitch[0]) + 1 - alter = SIGN_TO_ACC[alter.group(0)] if alter is not None else None - return step, octave, alter - - def _process_kern_duration(self, duration, is_grace=False): - dots = duration.count(".") - dur = duration.replace(".", "") - if dur in KERN_DURS.keys(): - symbolic_duration = copy.deepcopy(KERN_DURS[dur]) - else: - dur = float(dur) - key_loolup = [2 ** i for i in range(0, 9)] - diff = dict( - ( - map( - lambda x: (dur - x, str(x)) if dur > x else (dur + x, str(x)), - key_loolup, - ) - ) - ) - - symbolic_duration = copy.deepcopy(KERN_DURS[diff[min(list(diff.keys()))]]) - symbolic_duration["actual_notes"] = int(dur // 4) - symbolic_duration["normal_notes"] = int(diff[min(list(diff.keys()))]) // 4 - if dots: - symbolic_duration["dots"] = dots - self.note_duration_values[self.total_parsed_elements] = dot_function((float(dur) if isinstance(dur, str) else dur), dots) if not is_grace else inf - return symbolic_duration - - def process_symbol(self, note, symbols): - """ - Process the symbols of a note. - - Parameters - ---------- - note - symbol - - Returns - ------- - - """ - if "[" in symbols: - self.tie_prev[self.total_parsed_elements] = True - # pop symbol and call again - symbols.pop(symbols.index("[")) - self.process_symbol(note, symbols) - if "]" in symbols: - self.tie_next[self.total_parsed_elements] = True - symbols.pop(symbols.index("]")) - self.process_symbol(note, symbols) - if "_" in symbols: - # continuing tie - self.tie_prev[self.total_parsed_elements] = True - self.tie_next[self.total_parsed_elements] = True - symbols.pop(symbols.index("_")) - self.process_symbol(note, symbols) - return - - def meta_note_line(self, line, voice=None, add=True): - """ - Grammar Defining a note line. - - A note line is specified by the following grammar: - note_line = symbol | duration | pitch | symbol - - Parameters - ---------- - line - - Returns - ------- - - """ - self.total_parsed_elements += 1 if add else 0 - voice = self.voice if voice is None else voice - # extract first occurence of one of the following: a-g A-G r # - n - find_pitch = re.search(r"([a-gA-Gr\-n#]+)", line) - if find_pitch is None: - warnings.warn("No pitch found in line: {}, transforming to a rest".format(line)) - pitch = "r" - else: - pitch = find_pitch.group(0) - # extract duration can be any of the following: 0-9 . - dur_search = re.search(r"([0-9.%]+)", line) - # if no duration is found, then the duration is 8 by default (for grace notes with no duration) - duration = dur_search.group(0) if dur_search else "8" - # extract symbol can be any of the following: _()[]{}<>|: - symbols = re.findall(r"([_()\[\]{}<>|:])", line) - symbolic_duration = self._process_kern_duration(duration, is_grace="q" in line) - el_id = "{}-s{}-v{}-el{}".format(self.id, self.staff, voice, self.total_parsed_elements) - if pitch.startswith("r"): - return spt.Rest(symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) - step, octave, alter = self._process_kern_pitch(pitch) - # check if the note is a grace note - if "q" in line: - note = spt.GraceNote(grace_type="grace", step=step, octave=octave, alter=alter, symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) - else: - note = spt.Note(step, octave, alter, symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) - if symbols: - self.process_symbol(note, symbols) - return note - - def meta_barline_line(self, line): - """ - Grammar Defining a barline line. - - A barline line is specified by the following grammar: - barline_line = repeat | barline | number | repeat - - Parameters - ---------- - line - - Returns - ------- - - """ - # find number and keep its index. - self.total_parsed_elements += 1 - number = re.findall(r"([0-9]+)", line) - number_index = line.index(number[0]) if number else line.index("=") - closing_repeat = re.findall(r"[:|]", line[:number_index]) - opening_repeat = re.findall(r"[|:]", line[number_index:]) - return spt.Measure(number=int(number[0]) if number else None) - - def meta_chord_line(self, line): - """ - Grammar Defining a chord line. - - A chord line is specified by the following grammar: - chord_line = note | chord - - Parameters - ---------- - line - - Returns - ------- - - """ - self.total_parsed_elements += 1 - chord = ("c", [self.meta_note_line(n, add=False) for n in line.split(" ")]) - return chord diff --git a/tests/test_kern.py b/tests/test_kern.py index e6293930..10a5e530 100644 --- a/tests/test_kern.py +++ b/tests/test_kern.py @@ -11,7 +11,7 @@ from tempfile import TemporaryDirectory from partitura.score import merge_parts from partitura.utils import ensure_notearray -from partitura.io.importkern_v2 import load_kern +from partitura.io.importkern import load_kern from partitura.io.exportkern import save_kern from partitura import load_musicxml import numpy as np From 093758d9e3c4f904e50bbf34615ea46e747214ac Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sun, 12 May 2024 10:29:47 +0200 Subject: [PATCH 166/197] Fixed test file name typo. Addressing PR comment: https://github.com/CPJKU/partitura/pull/344#discussion_r1597126836 --- tests/__init__.py | 2 +- .../kern/{voice_dublifications.krn => voice_dublications.krn} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/data/kern/{voice_dublifications.krn => voice_dublications.krn} (100%) diff --git a/tests/__init__.py b/tests/__init__.py index 712e6a11..e7f176d8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -183,7 +183,7 @@ "double_repeat_example.krn", "fine_with_repeat.krn", "tuple_durations.krn", - "voice_dublifications.krn", + "voice_dublications.krn", "variable_length_pr_bug.krn", "chor228.krn", ] diff --git a/tests/data/kern/voice_dublifications.krn b/tests/data/kern/voice_dublications.krn similarity index 100% rename from tests/data/kern/voice_dublifications.krn rename to tests/data/kern/voice_dublications.krn From f0a8fa43f454361c62983b42fa6f9b99b28d254b Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sun, 12 May 2024 10:32:32 +0200 Subject: [PATCH 167/197] Removed example within file containing local path. Addressed PR comment: https://github.com/CPJKU/partitura/pull/344#discussion_r1597113561 --- partitura/io/exportkern.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/partitura/io/exportkern.py b/partitura/io/exportkern.py index 2a709141..ef511d58 100644 --- a/partitura/io/exportkern.py +++ b/partitura/io/exportkern.py @@ -281,9 +281,3 @@ def save_kern( comments="!!!", encoding="utf-8") else: return out_data - - -if __name__ == "__main__": - import partitura as pt - score = pt.load_score("/home/manos/Desktop/JKU/data/mozart_piano_sonatas/K279-1.musicxml") - save_kern(score, "/home/manos/Desktop/test.krn") \ No newline at end of file From e7b61cc36a4ed4cc1649e6c009da703050777e92 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sun, 12 May 2024 10:36:35 +0200 Subject: [PATCH 168/197] Added some documentation for clarity Addressed PR comment: https://github.com/CPJKU/partitura/pull/344#discussion_r1597100110 --- partitura/io/exportkern.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/partitura/io/exportkern.py b/partitura/io/exportkern.py index ef511d58..b5cced23 100644 --- a/partitura/io/exportkern.py +++ b/partitura/io/exportkern.py @@ -27,6 +27,7 @@ 2: "##", } +# Kern notes encoding has a dedicated octave for each note. KERN_NOTES = { ('C', 3): 'C', ('D', 3): 'D', @@ -181,9 +182,12 @@ def duration_to_kern(self, element: spt.GenericNote) -> str: return self.sym_dur_to_kern(element.symbolic_duration) def pitch_to_kern(self, element: spt.GenericNote) -> str: + # To encode pitch correctly in kern we need to take into account the octave + # duplication of the step in kern can either move the note up or down an octave if isinstance(element, spt.Rest): return "r" step, alter, octave = element.step, element.alter, element.octave + # Check if we need to have duplication of the step character if octave > 4: multiply_character = octave - 3 octave = 4 @@ -192,6 +196,7 @@ def pitch_to_kern(self, element: spt.GenericNote) -> str: octave = 3 else: multiply_character = 1 + # Fetch the correct string for the step and multiply it if needed kern_step = KERN_NOTES[(step, octave)] * multiply_character kern_alter = ACC_TO_SIGN[alter] if alter is not None else "" return kern_step + kern_alter From 432ed3b94e32e8072a3caab32b621f5b8d0717b6 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sun, 12 May 2024 10:50:25 +0200 Subject: [PATCH 169/197] Improved documentation and typing. --- partitura/io/importkern.py | 156 ++++++++++++++++++++++--------------- 1 file changed, 94 insertions(+), 62 deletions(-) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index 81397b02..b3e7ac2f 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -74,7 +74,10 @@ def add_durations(a, b): return a*b / (a + b) -def dot_function(duration, dots): +def dot_function( + duration: int, + dots: int + ): if dots == 0: return duration elif duration == 0: @@ -82,7 +85,10 @@ def dot_function(duration, dots): else: return add_durations((2**dots)*duration, dot_function(duration, dots - 1)) -def parse_by_voice(file, dtype=np.object_): +def parse_by_voice( + file: list, + dtype=np.object_ + ): indices_to_remove = [] voices = 1 for i, line in enumerate(file): @@ -113,7 +119,24 @@ def parse_by_voice(file, dtype=np.object_): return data, voice_indices, num_voices -def _handle_kern_with_spine_splitting(kern_path): +def _handle_kern_with_spine_splitting(kern_path: PathLike): + """ + Parse a kern file with spine splitting. + + A special case of kern files is when the file contains multiple spines that are split by voice. In this case, this + function will restructure the data in a way that it can be parsed by the kern parser. + + Parameters + ---------- + kern_path: str + + Returns + ------- + data: np.array + The data to be parsed. + parsing_idxs: np.array + The indices of the data that are being parsed indicating the assignment of voices. + """ # org_file = np.loadtxt(kern_path, dtype="U", delimiter="\n", comments="!!!", encoding="cp437") org_file = np.genfromtxt(kern_path, dtype="U", delimiter="\n", comments="!!!", encoding="cp437") # Get Main Number of parts and Spline Types @@ -139,36 +162,14 @@ def _handle_kern_with_spine_splitting(kern_path): data = np.vstack(data).T parsing_idxs = np.hstack(parsing_idxs).T return data, parsing_idxs - # - # - # # Find all expansions points - # expansion_indices = np.where(np.char.find(file, "*^") != -1)[0] - # # For all expansion points find which stream is being expanded - # expansion_streams_per_index = [np.argwhere(np.array(line.split("\t")) == "*^")[0] for line in - # file[expansion_indices]] - # - # # Find all Spline Reduction points - # reduction_indices = np.where(np.char.find(file, "*v\t*v") != -1)[0] - # # For all reduction points find which stream is being reduced - # reduction_streams_per_index = [ - # np.argwhere(np.char.add(np.array(line.split("\t")[:-1]), np.array(line.split("\t")[1:])) == "*v*v")[0] for line - # in file[reduction_indices]] - # - # # Find all pairs of expansion and reduction points - # expansion_reduction_pairs = [] - # last_exhaustive_reduction = 0 - # for expansion_index in expansion_indices: - # for expansion_stream in expansion_index: - # # Find the first reduction index that is after the expansion index and has the same index. - # for i, reduction_index in enumerate(reduction_indices[last_exhaustive_reduction:]): - # for reduction_stream in reduction_streams_per_index[i]: - # if expansion_stream == reduction_stream: - # expansion_reduction_pairs.append((expansion_index, reduction_index)) - # last_exhaustive_reduction = i if i == last_exhaustive_reduction + 1 else last_exhaustive_reduction - # break - - -def element_parsing(part, elements, total_duration_values, same_part): + + +def element_parsing( + part: spt.Part, + elements:np.array, + total_duration_values: np.array, + same_part: bool + ): divs_pq = part._quarter_durations[0] current_tl_pos = 0 measure_mapping = {m.number: m.start.t for m in part.iter_all(spt.Measure)} @@ -360,7 +361,20 @@ def __init__(self, id="P1", staff=1, voice=1, size=1, name=""): self.slurs_start = [] self.slurs_end = [] - def parse(self, spline): + def parse(self, spline: np.array): + """ + Parse a spline line and return the elements. + + Parameters + ---------- + spline: np.array + The spline line to parse. It is a numpy array of strings. + + Returns + ------- + elements: np.array + The parsed elements of the spline line. + """ # Remove "-" lines spline = spline[spline != '-'] # Remove "." lines @@ -427,7 +441,7 @@ def parse(self, spline): return elements - def meta_tandem_line(self, line): + def meta_tandem_line(self, line: str): """ Find all tandem lines """ @@ -469,43 +483,43 @@ def meta_tandem_line(self, line): elif line.startswith("*-"): return self.process_fine() - def process_tempo_line(self, line): + def process_tempo_line(self, line: str): return spt.Tempo(float(line)) def process_fine(self): return spt.Fine() - def process_istrument_line(self, line): + def process_istrument_line(self, line: str): #TODO: add support for instrument lines return - def process_istrument_class_line(self, line): + def process_istrument_class_line(self, line: str): # TODO: add support for instrument class lines return - def process_istrument_group_line(self, line): + def process_istrument_group_line(self, line: str): # TODO: add support for instrument group lines return - def process_timebase_line(self, line): + def process_timebase_line(self, line: str): # TODO: add support for timebase lines return - def process_istrument_transpose_line(self, line): + def process_istrument_transpose_line(self, line: str): # TODO: add support for instrument transpose lines return - def process_key_line(self, line): + def process_key_line(self, line: str): find = re.search(r"([a-gA-G])", line).group(0) # check if the key is major or minor by checking if the key is in lower or upper case. self.mode = "minor" if find.islower() else "major" return - def process_staff_line(self, line): + def process_staff_line(self, line: str): self.staff = int(line) return spt.Staff(self.staff) - def process_clef_line(self, line): + def process_clef_line(self, line: str): # if the cleff line does not contain any of the following characters, ["G", "F", "C"], raise a ValueError. if not any(c in line for c in ["G", "F", "C"]): raise ValueError("Unrecognized clef: {}".format(line)) @@ -535,7 +549,7 @@ def process_clef_line(self, line): return spt.Clef(sign=clef, staff=self.staff, line=int(clef_line), octave_change=octave) - def process_key_signature_line(self, line): + def process_key_signature_line(self, line: str): fifths = line.count("#") - line.count("-") alters = re.findall(r"([a-gA-G#\-]+)", line) alters = "".join(alters) @@ -545,7 +559,7 @@ def process_key_signature_line(self, line): mode = "major" return spt.KeySignature(fifths, mode) - def process_meter_line(self, line): + def process_meter_line(self, line: str): if " " in line: line = line.split(" ")[0] numerator, denominator = line.split("/") @@ -554,7 +568,7 @@ def process_meter_line(self, line): denominator = int(re.search(r"([0-9]+)", denominator).group(0)) return spt.TimeSignature(numerator, denominator) - def _process_kern_pitch(self, pitch): + def _process_kern_pitch(self, pitch: str): # find accidentals alter = re.search(r"([n#-]+)", pitch) # remove alter from pitch @@ -568,7 +582,21 @@ def _process_kern_pitch(self, pitch): alter = SIGN_TO_ACC[alter.group(0)] if alter is not None else None return step, octave, alter - def _process_kern_duration(self, duration, is_grace=False): + def _process_kern_duration(self, duration: str, is_grace=False): + """ + Process the duration of a note. + + Parameters + ---------- + duration: str + The duration of the note. + is_grace: bool(default=False) + If the note is a grace note. + Returns + ------- + symbolic_duration: dict + A dictionary containing the symbolic duration of the note. + """ dots = duration.count(".") dur = duration.replace(".", "") if dur in KERN_DURS.keys(): @@ -593,18 +621,16 @@ def _process_kern_duration(self, duration, is_grace=False): self.note_duration_values[self.total_parsed_elements] = dot_function((float(dur) if isinstance(dur, str) else dur), dots) if not is_grace else inf return symbolic_duration - def process_symbol(self, note, symbols): + def process_symbol(self, note: spt.Note, symbols: list): """ Process the symbols of a note. Parameters ---------- - note - symbol - - Returns - ------- - + note: spt.Note + The note to add the symbols to. + symbols: list + List of symbols to process. """ if "[" in symbols: self.tie_prev[self.total_parsed_elements] = True @@ -623,7 +649,7 @@ def process_symbol(self, note, symbols): self.process_symbol(note, symbols) return - def meta_note_line(self, line, voice=None, add=True): + def meta_note_line(self, line: str, voice=None, add=True): """ Grammar Defining a note line. @@ -632,11 +658,16 @@ def meta_note_line(self, line, voice=None, add=True): Parameters ---------- - line + line: str + The line to parse containing a note element. + voice: int + The voice of the note. + add: bool + If True, the element is added to the number of parsed elements. Returns ------- - + spt.Note object """ self.total_parsed_elements += 1 if add else 0 voice = self.voice if voice is None else voice @@ -667,7 +698,7 @@ def meta_note_line(self, line, voice=None, add=True): self.process_symbol(note, symbols) return note - def meta_barline_line(self, line): + def meta_barline_line(self, line: str): """ Grammar Defining a barline line. @@ -676,11 +707,12 @@ def meta_barline_line(self, line): Parameters ---------- - line + line: str + The line to parse containing a barline. Returns ------- - + spt.Measure object """ # find number and keep its index. self.total_parsed_elements += 1 @@ -690,7 +722,7 @@ def meta_barline_line(self, line): opening_repeat = re.findall(r"[|:]", line[number_index:]) return spt.Measure(number=int(number[0]) if number else None) - def meta_chord_line(self, line): + def meta_chord_line(self, line: str): """ Grammar Defining a chord line. From 3548d68502bc5b9c0a33d5d000bff49829918723 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 14 May 2024 12:31:00 +0200 Subject: [PATCH 170/197] cleanup of unused function. --- partitura/io/importkern.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index b3e7ac2f..551a3d27 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -339,13 +339,6 @@ def load_kern( return score -def rec_divisible_by_two(number): - if number % 2 == 0: - return rec_divisible_by_two(number // 2) - else: - return number - - class SplineParser(object): def __init__(self, id="P1", staff=1, voice=1, size=1, name=""): self.id = id From 8f8c7e2d6014a064a5b820ea5d7c68f2fef2fd56 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 14 May 2024 14:16:21 +0200 Subject: [PATCH 171/197] updated test to increase coverage. --- tests/data/kern/chor228.krn | 9 ++++++--- tests/test_kern.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/data/kern/chor228.krn b/tests/data/kern/chor228.krn index 31a5e2bb..5c237046 100644 --- a/tests/data/kern/chor228.krn +++ b/tests/data/kern/chor228.krn @@ -7,9 +7,12 @@ !!!AGN: chorale **kern **kern **kern **kern *ICvox *ICvox *ICvox *ICvox +*IGrand *IGrand *IGrand *IGrand *Ibass *Itenor *Ialto *Isoprn +*tb *tb *tb *tb +*ITr *ITr *ITr *ITr *I"Bass *I"Tenor *I"Alto *I"Soprano -*clefF4 *clefGv2 *clefG2 *clefG2 +*clefF *clefGv2 *clefG2 *clefG *k[] *k[] *k[] *k[] *a: *a: *a: *a: *M4/4 *M4/4 *M4/4 *M4/4 @@ -18,8 +21,8 @@ 4A 4c 4e 4a =1 =1 =1 =1 4.A 4e 4a 4cc -. 4e [4b 4b -8G# . . . +. 4e [8b 4b +8G# . 8b_ . 8AL 4e 8bL] 4cc 8AAJ . [8aJ . 4BB [4d 8aL] 4dd diff --git a/tests/test_kern.py b/tests/test_kern.py index 10a5e530..f29ed3b4 100644 --- a/tests/test_kern.py +++ b/tests/test_kern.py @@ -38,6 +38,16 @@ def test_examples(self): score = load_kern(fn) self.assertTrue(True) + def test_chorale_import(self): + file_path = os.path.join(KERN_PATH, "chor228.krn") + score = load_kern(file_path) + num_measures = 8 + num_parts = 4 + num_notes = 102 + self.assertTrue(len(score.parts) == num_parts) + self.assertTrue(all([len(part.measures) == num_measures for part in score.parts])) + self.assertTrue(len(score.note_array()) == num_notes) + def test_tie_mismatch(self): fn = KERN_TIES[0] From c1e27f7553d9c9a636f4b74c2e7445d40e38281e Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 21 May 2024 17:46:36 +0200 Subject: [PATCH 172/197] fixing test file typo. --- docs/source/Tutorial/notebook.ipynb | 413 ++++-------------- tests/__init__.py | 2 +- ...dublications.krn => voice_duplication.krn} | 0 3 files changed, 91 insertions(+), 324 deletions(-) rename tests/data/kern/{voice_dublications.krn => voice_duplication.krn} (100%) diff --git a/docs/source/Tutorial/notebook.ipynb b/docs/source/Tutorial/notebook.ipynb index fc75b60c..31d98713 100644 --- a/docs/source/Tutorial/notebook.ipynb +++ b/docs/source/Tutorial/notebook.ipynb @@ -52,7 +52,6 @@ "is_executing": true } }, - "outputs": [], "source": [ "# Install partitura\n", "! pip install partitura\n", @@ -65,20 +64,21 @@ "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,20 +103,6 @@ "execution_count": 3, "id": "photographic-profession", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": "Output()", - "application/vnd.jupyter.widget-view+json": { - "version_major": 2, - "version_minor": 0, - "model_id": "51b999065d4e4460b960ff64e7507006" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ "# setup the dataset\n", "from load_data import init_dataset\n", @@ -124,7 +110,8 @@ "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", @@ -233,53 +220,12 @@ "execution_count": 4, "id": "c9179e78", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Part id=\"P1\" name=\"Piano\"\n", - " │\n", - " ├─ TimePoint t=0 quarter=12\n", - " │ │\n", - " │ └─ starting objects\n", - " │ │\n", - " │ ├─ 0--48 Measure number=1\n", - " │ ├─ 0--48 Note id=n01 voice=1 staff=2 type=whole pitch=A4\n", - " │ ├─ 0--48 Page number=1\n", - " │ ├─ 0--24 Rest id=r01 voice=2 staff=1 type=half\n", - " │ ├─ 0--48 System number=1\n", - " │ └─ 0-- TimeSignature 4/4\n", - " │\n", - " ├─ TimePoint t=24 quarter=12\n", - " │ │\n", - " │ ├─ ending objects\n", - " │ │ │\n", - " │ │ └─ 0--24 Rest id=r01 voice=2 staff=1 type=half\n", - " │ │\n", - " │ └─ starting objects\n", - " │ │\n", - " │ ├─ 24--48 Note id=n02 voice=2 staff=1 type=half pitch=C5\n", - " │ └─ 24--48 Note id=n03 voice=2 staff=1 type=half pitch=E5\n", - " │\n", - " └─ TimePoint t=48 quarter=12\n", - " │\n", - " └─ ending objects\n", - " │\n", - " ├─ 0--48 Measure number=1\n", - " ├─ 0--48 Note id=n01 voice=1 staff=2 type=whole pitch=A4\n", - " ├─ 24--48 Note id=n02 voice=2 staff=1 type=half pitch=C5\n", - " ├─ 24--48 Note id=n03 voice=2 staff=1 type=half pitch=E5\n", - " ├─ 0--48 Page number=1\n", - " └─ 0--48 System number=1\n" - ] - } - ], "source": [ "path_to_musicxml = pt.EXAMPLE_MUSICXML\n", "part = pt.load_musicxml(path_to_musicxml)[0]\n", "print(part.pretty())" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -304,38 +250,20 @@ "execution_count": 5, "id": "423aac6a", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": "[,\n ,\n ]" - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "part.notes" - ] + ], + "outputs": [] }, { "cell_type": "code", "execution_count": 6, "id": "0a929369", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": "['__class__',\n '__delattr__',\n '__dict__',\n '__dir__',\n '__doc__',\n '__eq__',\n '__format__',\n '__ge__',\n '__getattribute__',\n '__gt__',\n '__hash__',\n '__init__',\n '__init_subclass__',\n '__le__',\n '__lt__',\n '__module__',\n '__ne__',\n '__new__',\n '__reduce__',\n '__reduce_ex__',\n '__repr__',\n '__setattr__',\n '__sizeof__',\n '__str__',\n '__subclasshook__',\n '__weakref__',\n '_ref_attrs',\n '_sym_dur',\n 'alter',\n 'alter_sign',\n 'articulations',\n 'beam',\n 'doc_order',\n 'duration',\n 'duration_from_symbolic',\n 'duration_tied',\n 'end',\n 'end_tied',\n 'fermata',\n 'id',\n 'iter_chord',\n 'midi_pitch',\n 'octave',\n 'ornaments',\n 'replace_refs',\n 'slur_starts',\n 'slur_stops',\n 'staff',\n 'start',\n 'step',\n 'symbolic_duration',\n 'tie_next',\n 'tie_next_notes',\n 'tie_prev',\n 'tie_prev_notes',\n 'tuplet_starts',\n 'tuplet_stops',\n 'voice']" - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "dir(part.notes[0])" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -350,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", @@ -385,19 +313,10 @@ "execution_count": 9, "id": "e95eb0f7", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": "array(4.)" - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "part.beat_map(part.notes[0].end.t)" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -412,19 +331,10 @@ "execution_count": 10, "id": "05346a03", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": "array([4., 4., 4.])" - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "part.time_signature_map(part.notes[0].end.t)" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -446,39 +356,22 @@ "execution_count": 11, "id": "74943a93", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0--48 Measure number=1\n" - ] - } - ], "source": [ "for measure in part.iter_all(pt.score.Measure):\n", " print(measure)" - ] + ], + "outputs": [] }, { "cell_type": "code", "execution_count": 12, "id": "6cbfd044", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0--48 Note id=n01 voice=1 staff=2 type=whole pitch=A4\n", - "0--24 Rest id=r01 voice=2 staff=1 type=half\n" - ] - } - ], "source": [ "for note in part.iter_all(pt.score.GenericNote, include_subclasses=True, start=0, end=24):\n", " print(note)" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -495,7 +388,6 @@ "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", @@ -513,17 +405,18 @@ "# 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", @@ -543,30 +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": [ - { - "data": { - "text/plain": "[{'midi_pitch': 69,\n 'note_on': 0.0,\n 'note_off': 2.0,\n 'track': 0,\n 'channel': 1,\n 'velocity': 64,\n 'id': 'n0',\n 'sound_off': 2.0},\n {'midi_pitch': 72,\n 'note_on': 1.0,\n 'note_off': 2.0,\n 'track': 0,\n 'channel': 2,\n 'velocity': 64,\n 'id': 'n1',\n 'sound_off': 2.0},\n {'midi_pitch': 76,\n 'note_on': 1.0,\n 'note_off': 2.0,\n 'track': 0,\n 'channel': 2,\n 'velocity': 64,\n 'id': 'n2',\n 'sound_off': 2.0}]" - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "performedpart.notes" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -581,7 +465,6 @@ "execution_count": 17, "id": "d6eb12f2", "metadata": {}, - "outputs": [], "source": [ "import numpy as np \n", "\n", @@ -608,14 +491,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", @@ -626,53 +509,54 @@ " 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", @@ -718,7 +602,6 @@ "execution_count": 23, "id": "first-basin", "metadata": {}, - "outputs": [], "source": [ "# Note array from a score\n", "\n", @@ -730,7 +613,8 @@ "\n", "# Get note array.\n", "score_note_array = score_part.note_array()" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -745,28 +629,11 @@ "execution_count": 24, "id": "alternate-coordinate", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[(-4., 1., -2. , 0.5, 0, 8, 60, 4, 'n2', 16)\n", - " (-4., 1., -2. , 0.5, 0, 8, 72, 1, 'n1', 16)\n", - " (-3., 2., -1.5, 1. , 8, 16, 60, 4, 'n4', 16)\n", - " (-3., 2., -1.5, 1. , 8, 16, 72, 1, 'n3', 16)\n", - " (-1., 1., -0.5, 0.5, 24, 8, 60, 4, 'n6', 16)\n", - " (-1., 1., -0.5, 0.5, 24, 8, 72, 1, 'n5', 16)\n", - " ( 0., 2., 0. , 1. , 32, 16, 60, 4, 'n8', 16)\n", - " ( 0., 2., 0. , 1. , 32, 16, 72, 1, 'n7', 16)\n", - " ( 2., 1., 1. , 0.5, 48, 8, 60, 4, 'n10', 16)\n", - " ( 2., 1., 1. , 0.5, 48, 8, 72, 1, 'n9', 16)]\n" - ] - } - ], "source": [ "# Lets see the first notes in this note array\n", "print(score_note_array[:10])" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -783,18 +650,10 @@ "execution_count": 25, "id": "subtle-millennium", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "('onset_beat', 'duration_beat', 'onset_quarter', 'duration_quarter', 'onset_div', 'duration_div', 'pitch', 'voice', 'id', 'divs_pq')\n" - ] - } - ], "source": [ "print(score_note_array.dtype.names)" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -827,7 +686,6 @@ "execution_count": 26, "id": "passing-lending", "metadata": {}, - "outputs": [], "source": [ "# Note array from a performance\n", "\n", @@ -839,7 +697,8 @@ "\n", "# Get note array!\n", "performance_note_array = performance_part.note_array()" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -854,18 +713,10 @@ "execution_count": 27, "id": "pointed-stupid", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "('onset_sec', 'duration_sec', 'pitch', 'velocity', 'track', 'channel', 'id')\n" - ] - } - ], "source": [ "print(performance_note_array.dtype.names)" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -886,22 +737,10 @@ "execution_count": 28, "id": "subject-reducing", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[(5.6075 , 5.5025 , 72, 37, 0, 0, 'n0')\n", - " (5.63375, 5.47625, 60, 27, 0, 0, 'n1')\n", - " (6.07 , 5.04 , 72, 45, 0, 0, 'n2')\n", - " (6.11125, 4.99875, 60, 26, 0, 0, 'n3')\n", - " (6.82625, 4.28375, 60, 39, 0, 0, 'n4')]\n" - ] - } - ], "source": [ "print(performance_note_array[:5])" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -916,7 +755,6 @@ "execution_count": 29, "id": "spread-performer", "metadata": {}, - "outputs": [], "source": [ "note_array = np.array(\n", " [(60, 0, 2, 40),\n", @@ -933,7 +771,8 @@ "\n", "# Note array to `PerformedPart`\n", "performed_part = pt.performance.PerformedPart.from_note_array(note_array)" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -948,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", @@ -969,7 +808,6 @@ "execution_count": 31, "id": "figured-coordinator", "metadata": {}, - "outputs": [], "source": [ "extended_score_note_array = pt.utils.music.ensure_notearray(\n", " score_part,\n", @@ -979,26 +817,18 @@ " # 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": [ - { - "data": { - "text/plain": "('onset_beat',\n 'duration_beat',\n 'onset_quarter',\n 'duration_quarter',\n 'onset_div',\n 'duration_div',\n 'pitch',\n 'voice',\n 'id',\n 'step',\n 'alter',\n 'octave',\n 'is_grace',\n 'grace_type',\n 'ks_fifths',\n 'ks_mode',\n 'ts_beats',\n 'ts_beat_type',\n 'ts_mus_beats',\n 'divs_pq')" - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "extended_score_note_array.dtype.names" - ] + ], + "outputs": [] }, { "cell_type": "code", @@ -1007,19 +837,6 @@ "metadata": { "scrolled": true }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[('n2', 'C', 0, 4, -1, 1) ('n1', 'C', 0, 5, -1, 1)\n", - " ('n4', 'C', 0, 4, -1, 1) ('n3', 'C', 0, 5, -1, 1)\n", - " ('n6', 'C', 0, 4, -1, 1) ('n5', 'C', 0, 5, -1, 1)\n", - " ('n8', 'C', 0, 4, -1, 1) ('n7', 'C', 0, 5, -1, 1)\n", - " ('n10', 'C', 0, 4, -1, 1) ('n9', 'C', 0, 5, -1, 1)]\n" - ] - } - ], "source": [ "print(extended_score_note_array[['id', \n", " 'step', \n", @@ -1028,7 +845,8 @@ " 'ks_fifths', \n", " 'ks_mode', #'is_downbeat'\n", " ]][:10])" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -1061,15 +879,6 @@ "metadata": { "scrolled": true }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[(0.25, 47, 1) (1.25, 47, 1) (2.25, 47, 1) (3. , 68, 1) (3.25, 47, 1)]\n" - ] - } - ], "source": [ "# Path to the MusicXML file\n", "score_fn = os.path.join(MUSICXML_DIR, 'Chopin_op10_no3.musicxml')\n", @@ -1105,7 +914,8 @@ "\n", "accented_note_idxs = np.where(accent_note_array['accent'])\n", "print(accent_note_array[accented_note_idxs][:5])" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -1134,7 +944,6 @@ "execution_count": 35, "id": "essential-academy", "metadata": {}, - "outputs": [], "source": [ "# TODO: change the example\n", "# Path to the MusicXML file\n", @@ -1144,7 +953,8 @@ "score_part = pt.load_musicxml(score_fn)\n", "# compute piano roll\n", "pianoroll = pt.utils.compute_pianoroll(score_part)" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -1159,7 +969,6 @@ "execution_count": 36, "id": "massive-monaco", "metadata": {}, - "outputs": [], "source": [ "piano_range = True\n", "time_unit = 'beat'\n", @@ -1170,7 +979,8 @@ " 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", @@ -1197,25 +1007,14 @@ "execution_count": 37, "id": "mature-dylan", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": "
", - "image/png": "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\n" - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], "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", @@ -1232,25 +1031,13 @@ "metadata": { "scrolled": true }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[59 0 4]\n", - " [40 4 12]\n", - " [40 4 6]\n", - " [56 4 6]\n", - " [64 4 8]]\n" - ] - } - ], "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", @@ -1268,7 +1055,6 @@ "execution_count": 39, "id": "parental-links", "metadata": {}, - "outputs": [], "source": [ "pianoroll = pt.utils.compute_pianoroll(score_part)\n", "\n", @@ -1278,7 +1064,8 @@ "ppart = pt.performance.PerformedPart.from_note_array(new_note_array)\n", "\n", "pt.save_performance_midi(ppart, \"newmidi.mid\")" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -1325,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", @@ -1353,7 +1140,6 @@ "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", @@ -1364,7 +1150,8 @@ "\n", "# loading a match file\n", "performed_part, alignment = pt.load_match(match_fn)" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -1388,19 +1175,10 @@ "execution_count": 42, "id": "radio-interim", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": "[{'label': 'match', 'score_id': 'n1', 'performance_id': 0},\n {'label': 'match', 'score_id': 'n2', 'performance_id': 2},\n {'label': 'match', 'score_id': 'n3', 'performance_id': 3},\n {'label': 'match', 'score_id': 'n4', 'performance_id': 1},\n {'label': 'match', 'score_id': 'n5', 'performance_id': 5},\n {'label': 'match', 'score_id': 'n6', 'performance_id': 4},\n {'label': 'match', 'score_id': 'n7', 'performance_id': 6},\n {'label': 'match', 'score_id': 'n8', 'performance_id': 7},\n {'label': 'match', 'score_id': 'n9', 'performance_id': 8},\n {'label': 'match', 'score_id': 'n10', 'performance_id': 9}]" - }, - "execution_count": 42, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "alignment[:10]" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -1419,7 +1197,6 @@ "execution_count": 43, "id": "published-understanding", "metadata": {}, - "outputs": [], "source": [ "# note array of the score\n", "snote_array = score_part.note_array()\n", @@ -1432,7 +1209,8 @@ "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", @@ -1449,7 +1227,6 @@ "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", @@ -1476,7 +1253,8 @@ " # 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", @@ -1485,18 +1263,6 @@ "metadata": { "scrolled": false }, - "outputs": [ - { - "data": { - "text/plain": "
", - "image/png": "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\n" - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], "source": [ "fig, ax = plt.subplots(1, figsize=(15, 8))\n", "color = plt.cm.rainbow(np.linspace(0, 1, len(tempo_curves)))\n", @@ -1518,7 +1284,8 @@ "plt.legend(frameon=False, bbox_to_anchor = (1.15, .9))\n", "plt.grid(axis='x')\n", "plt.show()" - ] + ], + "outputs": [] }, { "cell_type": "markdown", diff --git a/tests/__init__.py b/tests/__init__.py index e7f176d8..252f75bb 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -183,7 +183,7 @@ "double_repeat_example.krn", "fine_with_repeat.krn", "tuple_durations.krn", - "voice_dublications.krn", + "voice_duplication.krn", "variable_length_pr_bug.krn", "chor228.krn", ] diff --git a/tests/data/kern/voice_dublications.krn b/tests/data/kern/voice_duplication.krn similarity index 100% rename from tests/data/kern/voice_dublications.krn rename to tests/data/kern/voice_duplication.krn From 565ff521d0968334bd5a4194eb7d09ed9ee07904 Mon Sep 17 00:00:00 2001 From: manoskary Date: Tue, 21 May 2024 15:58:12 +0000 Subject: [PATCH 173/197] Format code with black (bot) --- partitura/utils/fluidsynth.py | 4 ++-- partitura/utils/synth.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/partitura/utils/fluidsynth.py b/partitura/utils/fluidsynth.py index 5452c817..7f8d9f2c 100644 --- a/partitura/utils/fluidsynth.py +++ b/partitura/utils/fluidsynth.py @@ -225,7 +225,7 @@ def synth_note_info( ) -> np.ndarray: """ Synthesize note information with Fluidsynth. - This method is designed to synthesize the notes in a + This method is designed to synthesize the notes in a single track and channel. Parameters @@ -242,7 +242,7 @@ def synth_note_info( A list of MIDI controls (e.g., pedals). (as the `controls` attribute in `PerformedPart` objects) program : Optional[int] - A list of MIDI programs as dictionaries + A list of MIDI programs as dictionaries (as the `program` attribute in `PerformedPart` objects). synthesizer : Synth An instance of a fluidsynth Synth object. diff --git a/partitura/utils/synth.py b/partitura/utils/synth.py index 6a433669..848be7af 100644 --- a/partitura/utils/synth.py +++ b/partitura/utils/synth.py @@ -71,8 +71,6 @@ } - - def midi_pitch_to_natural_frequency( midi_pitch: Union[int, float, np.ndarray], a4: Union[int, float] = A4, From 758b90f7362518a83df03e3d969bc522098c48f1 Mon Sep 17 00:00:00 2001 From: fosfrancesco Date: Wed, 22 May 2024 09:22:50 +0000 Subject: [PATCH 174/197] Format code with black (bot) --- partitura/io/exportkern.py | 135 ++++++++++++++++++++----------- partitura/io/importkern.py | 160 +++++++++++++++++++++++++------------ partitura/score.py | 73 +++++++++++++---- partitura/utils/music.py | 4 +- 4 files changed, 256 insertions(+), 116 deletions(-) diff --git a/partitura/io/exportkern.py b/partitura/io/exportkern.py index b5cced23..a269e9f5 100644 --- a/partitura/io/exportkern.py +++ b/partitura/io/exportkern.py @@ -29,41 +29,40 @@ # Kern notes encoding has a dedicated octave for each note. KERN_NOTES = { - ('C', 3): 'C', - ('D', 3): 'D', - ('E', 3): 'E', - ('F', 3): 'F', - ('G', 3): 'G', - ('A', 3): 'A', - ('B', 3): 'B', - ('C', 4): 'c', - ('D', 4): 'd', - ('E', 4): 'e', - ('F', 4): 'f', - ('G', 4): 'g', - ('A', 4): 'a', - ('B', 4): 'b' + ("C", 3): "C", + ("D", 3): "D", + ("E", 3): "E", + ("F", 3): "F", + ("G", 3): "G", + ("A", 3): "A", + ("B", 3): "B", + ("C", 4): "c", + ("D", 4): "d", + ("E", 4): "e", + ("F", 4): "f", + ("G", 4): "g", + ("A", 4): "a", + ("B", 4): "b", } KERN_DURS = { - 'maxima': '000', - 'long': '00', - 'breve': '0', - 'whole': '1', - 'half': '2', - 'quarter': '4', - 'eighth': '8', - '16th': '16', - '32nd': '32', - '64th': '64', - '128th': '128', - '256th': '256' - } + "maxima": "000", + "long": "00", + "breve": "0", + "whole": "1", + "half": "2", + "quarter": "4", + "eighth": "8", + "16th": "16", + "32nd": "32", + "64th": "64", + "128th": "128", + "256th": "256", +} KEYS = ["f", "c", "g", "d", "a", "e", "b"] - class KernExporter(object): """ Class for exporting a partitura score to Kern format. @@ -73,6 +72,7 @@ class KernExporter(object): part: spt.Part Part to export to Kern format. """ + def __init__(self, part): self.part = part note_array = part.note_array(include_staff=True) @@ -80,13 +80,17 @@ def __init__(self, part): num_notes = len(part.notes) num_rests = len(part.rests) self.unique_voc_staff = np.unique(note_array[["voice", "staff"]], axis=0) - self.vocstaff_map_dict = {f"{self.unique_voc_staff[i][0]}-{self.unique_voc_staff[i][1]}": i for i in - range(self.unique_voc_staff.shape[0])} + self.vocstaff_map_dict = { + f"{self.unique_voc_staff[i][0]}-{self.unique_voc_staff[i][1]}": i + for i in range(self.unique_voc_staff.shape[0]) + } # Part elements is really the maximum number of lines we could have in the kern file # we add some to account for the **kern and the *- encoding at beginning and end of file and also tandem elements # that might be added. We also add the number of measures to account for the measure encoding total_elements_ish = num_measures + num_notes + num_rests + 2 + 10 - self.out_data = np.empty((total_elements_ish, len(self.unique_voc_staff)), dtype=object) + self.out_data = np.empty( + (total_elements_ish, len(self.unique_voc_staff)), dtype=object + ) self.unique_times = np.array([p.t for p in part._points]) # Fill all values with the "." character to filter afterwards self.out_data.fill(".") @@ -104,18 +108,31 @@ def parse(self): for start_time in self.unique_times: end_time = start_time + 1 # Get all elements starting at this time - elements_starting = np.array(list(self.part.iter_all(start=start_time, end=end_time)), dtype=object) + elements_starting = np.array( + list(self.part.iter_all(start=start_time, end=end_time)), dtype=object + ) # Find notes - note_mask = np.array([isinstance(el, spt.GenericNote) for el in elements_starting]) + note_mask = np.array( + [isinstance(el, spt.GenericNote) for el in elements_starting] + ) if np.any(~note_mask): - bar_mask = np.array([isinstance(el, spt.Measure) for el in elements_starting[~note_mask]]) + bar_mask = np.array( + [ + isinstance(el, spt.Measure) + for el in elements_starting[~note_mask] + ] + ) tandem_mask = ~bar_mask structural_elements = elements_starting[~note_mask] - structural_elements = np.hstack((structural_elements[tandem_mask], structural_elements[bar_mask])) + structural_elements = np.hstack( + (structural_elements[tandem_mask], structural_elements[bar_mask]) + ) else: structural_elements = elements_starting[~note_mask] # Put structural elements first (start with tandem elements, then measure elements, then notes and rests) - elements_starting = np.hstack((structural_elements, elements_starting[note_mask])) + elements_starting = np.hstack( + (structural_elements, elements_starting[note_mask]) + ) for el in elements_starting: add_row = True if isinstance(el, spt.GenericNote): @@ -142,9 +159,9 @@ def parse(self): elif isinstance(el, spt.KeySignature): # Apply element to all splines if el.fifths < 0: - alters = "-".join(KEYS[:el.fifths]) + alters = "-".join(KEYS[: el.fifths]) elif el.fifths > 0: - alters = "#".join(KEYS[:el.fifths]) + alters = "#".join(KEYS[: el.fifths]) else: alters = "" kern_el = f"*k[{alters}]" @@ -163,9 +180,17 @@ def trim(self, data): def sym_dur_to_kern(self, symbolic_duration: dict) -> str: kern_base = KERN_DURS[symbolic_duration["type"]] - dots = "." * symbolic_duration["dots"] if "dots" in symbolic_duration.keys() else "" + dots = ( + "." * symbolic_duration["dots"] + if "dots" in symbolic_duration.keys() + else "" + ) if "actual_notes" in symbolic_duration.keys() and "normal_notes": - kern_base = int(kern_base) * symbolic_duration["actual_notes"] / symbolic_duration["normal_notes"] + kern_base = ( + int(kern_base) + * symbolic_duration["actual_notes"] + / symbolic_duration["normal_notes"] + ) kern_base = str(kern_base) return kern_base + dots @@ -216,7 +241,11 @@ def markings_to_kern(self, element: spt.GenericNote) -> str: symbols += ")" if isinstance(element, spt.Note): if element.beam is not None: - symbols += "L" if element.beam == "begin" else "J" if element.beam == "end" else "K" + symbols += ( + "L" + if element.beam == "begin" + else "J" if element.beam == "end" else "K" + ) return symbols def _handle_note(self, el: spt.GenericNote, row_idx) -> str: @@ -230,8 +259,11 @@ def _handle_note(self, el: spt.GenericNote, row_idx) -> str: if self.prev_note_time == el.start.t: if self.prev_note_col_idx == col_idx: # Chords in Kern - self.out_data[self.prev_note_row_idx, self.prev_note_col_idx] = self.out_data[ - self.prev_note_row_idx, self.prev_note_col_idx] + " " + kern_el + self.out_data[self.prev_note_row_idx, self.prev_note_col_idx] = ( + self.out_data[self.prev_note_row_idx, self.prev_note_col_idx] + + " " + + kern_el + ) else: # Same row (start.t) other spline self.out_data[self.prev_note_row_idx, col_idx] = kern_el @@ -246,7 +278,7 @@ def _handle_note(self, el: spt.GenericNote, row_idx) -> str: def save_kern( score_data: spt.ScoreLike, out: Optional[PathLike] = None, - ) -> Optional[np.ndarray]: +) -> Optional[np.ndarray]: """ Save a score in Kern format. @@ -280,9 +312,16 @@ def save_kern( # Use numpy savetxt to save the file footer = "Encoded using the Partitura Python package, version 1.5.0" if out is not None: - np.savetxt(fname=out, X=out_data, fmt="%1.26s", - delimiter="\t", newline="\n", - header=header, footer=footer, - comments="!!!", encoding="utf-8") + np.savetxt( + fname=out, + X=out_data, + fmt="%1.26s", + delimiter="\t", + newline="\n", + header=header, + footer=footer, + comments="!!!", + encoding="utf-8", + ) else: return out_data diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index 551a3d27..b7012569 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -71,24 +71,19 @@ def add_durations(a, b): - return a*b / (a + b) + return a * b / (a + b) -def dot_function( - duration: int, - dots: int - ): +def dot_function(duration: int, dots: int): if dots == 0: return duration elif duration == 0: return 0 else: - return add_durations((2**dots)*duration, dot_function(duration, dots - 1)) + return add_durations((2**dots) * duration, dot_function(duration, dots - 1)) -def parse_by_voice( - file: list, - dtype=np.object_ - ): + +def parse_by_voice(file: list, dtype=np.object_): indices_to_remove = [] voices = 1 for i, line in enumerate(file): @@ -100,7 +95,6 @@ def parse_by_voice( sum_vred = sum([line[v] == "*v" for v in range(voices)]) // 2 voices = voices - sum_vred - voice_indices = np.array(indices_to_remove) num_voices = voice_indices[:, 1].max() + 1 data = np.empty((len(file), num_voices), dtype=dtype) @@ -138,7 +132,9 @@ def _handle_kern_with_spine_splitting(kern_path: PathLike): The indices of the data that are being parsed indicating the assignment of voices. """ # org_file = np.loadtxt(kern_path, dtype="U", delimiter="\n", comments="!!!", encoding="cp437") - org_file = np.genfromtxt(kern_path, dtype="U", delimiter="\n", comments="!!!", encoding="cp437") + org_file = np.genfromtxt( + kern_path, dtype="U", delimiter="\n", comments="!!!", encoding="cp437" + ) # Get Main Number of parts and Spline Types spline_types = org_file[0].split("\t") parsing_idxs = [] @@ -153,23 +149,26 @@ def _handle_kern_with_spine_splitting(kern_path: PathLike): data.append(d) parsing_idxs.append([i for _ in range(num_voices)]) # Remove all parsed cells from the file - voice_indices = voice_indices[np.lexsort((voice_indices[:, 1]*-1, voice_indices[:, 0]))] + voice_indices = voice_indices[ + np.lexsort((voice_indices[:, 1] * -1, voice_indices[:, 0])) + ] for line, voice in voice_indices: if voice < len(file[line]): file[line].pop(voice) else: - print("Line {} does not have a voice {} from original line {}".format(line, voice, org_file[line])) + print( + "Line {} does not have a voice {} from original line {}".format( + line, voice, org_file[line] + ) + ) data = np.vstack(data).T parsing_idxs = np.hstack(parsing_idxs).T return data, parsing_idxs def element_parsing( - part: spt.Part, - elements:np.array, - total_duration_values: np.array, - same_part: bool - ): + part: spt.Part, elements: np.array, total_duration_values: np.array, same_part: bool +): divs_pq = part._quarter_durations[0] current_tl_pos = 0 measure_mapping = {m.number: m.start.t for m in part.iter_all(spt.Measure)} @@ -179,7 +178,9 @@ def element_parsing( continue if isinstance(element, spt.GenericNote): if total_duration_values[i] == 0: - duration_divs = symbolic_to_numeric_duration(element.symbolic_duration, divs_pq) + duration_divs = symbolic_to_numeric_duration( + element.symbolic_duration, divs_pq + ) else: quarter_duration = 4 / total_duration_values[i] duration_divs = ceil(quarter_duration * divs_pq) @@ -229,22 +230,24 @@ def load_kern( """ try: # This version of the parser is faster but does not support spine splitting. - file = np.loadtxt(filename, dtype="U", delimiter="\t", comments="!!", encoding="cp437") + file = np.loadtxt( + filename, dtype="U", delimiter="\t", comments="!!", encoding="cp437" + ) parsing_idxs = np.arange(file.shape[0]) # Decide Parts - except ValueError: # This version of the parser supports spine splitting but is slower. file, parsing_idxs = _handle_kern_with_spine_splitting(filename) - partlist = [] # Get Main Number of parts and Spline Types spline_types = file[0] # Find parsable parts if they start with "**kern" or "**notes" - note_parts = np.char.startswith(spline_types, "**kern") | np.char.startswith(spline_types, "**notes") + note_parts = np.char.startswith(spline_types, "**kern") | np.char.startswith( + spline_types, "**notes" + ) # Get Splines splines = file[1:].T[note_parts] # Inverse Order @@ -253,17 +256,27 @@ def load_kern( prev_staff = 1 has_instrument = np.char.startswith(splines, "*I") # if all parts have the same instrument, then they are the same part. - p_same_part = np.all(splines[has_instrument] == splines[has_instrument][0], axis=0) if np.any(has_instrument) else False + p_same_part = ( + np.all(splines[has_instrument] == splines[has_instrument][0], axis=0) + if np.any(has_instrument) + else False + ) total_durations_list = list() elements_list = list() part_assignments = list() copy_partlist = list() for j, spline in enumerate(splines): - parser = SplineParser(size=spline.shape[-1], id="P{}".format(parsing_idxs[j]) if not p_same_part else "P{}".format(j), staff=prev_staff) + parser = SplineParser( + size=spline.shape[-1], + id="P{}".format(parsing_idxs[j]) if not p_same_part else "P{}".format(j), + staff=prev_staff, + ) same_part = False if parser.id in [p.id for p in copy_partlist]: same_part = True - warnings.warn("Part {} already exists. Adding to previous Part.".format(parser.id)) + warnings.warn( + "Part {} already exists. Adding to previous Part.".format(parser.id) + ) part = [p for p in copy_partlist if p.id == parser.id][0] has_staff = np.char.startswith(spline, "*staff") staff = int(spline[has_staff][0][6:]) if np.count_nonzero(has_staff) else 1 @@ -299,7 +312,9 @@ def load_kern( divs_pq = np.lcm.reduce(unique_durs) divs_pq = divs_pq if divs_pq > 4 else 4 # Initialize Part - part = spt.Part(id=parser.id, quarter_duration=divs_pq, part_name=parser.name) + part = spt.Part( + id=parser.id, quarter_duration=divs_pq, part_name=parser.name + ) part_assignments.append(same_part) total_durations_list.append(parser.total_duration_values) @@ -311,7 +326,9 @@ 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): + for part, elements, total_duration_values, same_part in zip( + copy_partlist, elements_list, total_durations_list, part_assignments + ): element_parsing(part, elements, total_duration_values, same_part) for i, part in enumerate(copy_partlist): @@ -329,7 +346,6 @@ def load_kern( if parser.id not in [p.id for p in partlist]: partlist.append(part) - spt.assign_note_ids( partlist, keep=(force_note_ids is True or force_note_ids == "keep") ) @@ -369,11 +385,11 @@ def parse(self, spline: np.array): The parsed elements of the spline line. """ # Remove "-" lines - spline = spline[spline != '-'] + spline = spline[spline != "-"] # Remove "." lines - spline = spline[spline != '.'] + spline = spline[spline != "."] # Remove Empty lines - spline = spline[spline != ''] + spline = spline[spline != ""] # Remove None lines spline = spline[spline != None] # Remove lines that start with "!" @@ -383,10 +399,14 @@ def parse(self, spline: np.array): self.total_duration_values = np.ones(len(spline)) # Find Global indices, i.e. where spline cells start with "*" and process tandem_mask = np.char.find(spline, "*") != -1 - elements[tandem_mask] = np.vectorize(self.meta_tandem_line, otypes=[object])(spline[tandem_mask]) + elements[tandem_mask] = np.vectorize(self.meta_tandem_line, otypes=[object])( + spline[tandem_mask] + ) # Find Barline indices, i.e. where spline cells start with "=" bar_mask = np.char.find(spline, "=") != -1 - elements[bar_mask] = np.vectorize(self.meta_barline_line, otypes=[object])(spline[bar_mask]) + elements[bar_mask] = np.vectorize(self.meta_barline_line, otypes=[object])( + spline[bar_mask] + ) # Find Chord indices, i.e. where spline cells contain " " chord_mask = np.char.find(spline, " ") != -1 chord_mask = np.logical_and(chord_mask, np.logical_and(~tandem_mask, ~bar_mask)) @@ -395,7 +415,9 @@ def parse(self, spline: np.array): chord_num = np.count_nonzero(chord_mask) self.tie_next = np.zeros(chord_num, dtype=bool) self.tie_prev = np.zeros(chord_num, dtype=bool) - elements[chord_mask] = np.vectorize(self.meta_chord_line, otypes=[object])(spline[chord_mask]) + elements[chord_mask] = np.vectorize(self.meta_chord_line, otypes=[object])( + spline[chord_mask] + ) self.total_duration_values[chord_mask] = self.note_duration_values # TODO: figure out slurs for chords @@ -409,10 +431,14 @@ def parse(self, spline: np.array): 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 - for note, to_tie in np.c_[notes[self.tie_next], notes[np.roll(self.tie_next, -1)]]: + 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)]]: + 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 @@ -430,7 +456,11 @@ def parse(self, spline: np.array): # Add slurs to elements elements = np.append(elements, slurs) else: - warnings.warn("Slurs openings and closings do not match. Skipping parsing slurs for this part {}.".format(self.id)) + warnings.warn( + "Slurs openings and closings do not match. Skipping parsing slurs for this part {}.".format( + self.id + ) + ) return elements @@ -483,7 +513,7 @@ def process_fine(self): return spt.Fine() def process_istrument_line(self, line: str): - #TODO: add support for instrument lines + # TODO: add support for instrument lines return def process_istrument_class_line(self, line: str): @@ -540,14 +570,16 @@ def process_clef_line(self, line: str): else: octave = 0 - return spt.Clef(sign=clef, staff=self.staff, line=int(clef_line), octave_change=octave) + return spt.Clef( + sign=clef, staff=self.staff, line=int(clef_line), octave_change=octave + ) def process_key_signature_line(self, line: str): fifths = line.count("#") - line.count("-") alters = re.findall(r"([a-gA-G#\-]+)", line) alters = "".join(alters) # split alters by two characters - self.alters = [alters[i:i + 2] for i in range(0, len(alters), 2)] + self.alters = [alters[i : i + 2] for i in range(0, len(alters), 2)] # TODO retrieve the key mode mode = "major" return spt.KeySignature(fifths, mode) @@ -596,7 +628,7 @@ def _process_kern_duration(self, duration: str, is_grace=False): symbolic_duration = copy.deepcopy(KERN_DURS[dur]) else: dur = float(dur) - key_loolup = [2 ** i for i in range(0, 9)] + key_loolup = [2**i for i in range(0, 9)] diff = dict( ( map( @@ -611,7 +643,11 @@ def _process_kern_duration(self, duration: str, is_grace=False): symbolic_duration["normal_notes"] = int(diff[min(list(diff.keys()))]) // 4 if dots: symbolic_duration["dots"] = dots - self.note_duration_values[self.total_parsed_elements] = dot_function((float(dur) if isinstance(dur, str) else dur), dots) if not is_grace else inf + self.note_duration_values[self.total_parsed_elements] = ( + dot_function((float(dur) if isinstance(dur, str) else dur), dots) + if not is_grace + else inf + ) return symbolic_duration def process_symbol(self, note: spt.Note, symbols: list): @@ -667,7 +703,9 @@ def meta_note_line(self, line: str, voice=None, add=True): # extract first occurence of one of the following: a-g A-G r # - n find_pitch = re.search(r"([a-gA-Gr\-n#]+)", line) if find_pitch is None: - warnings.warn("No pitch found in line: {}, transforming to a rest".format(line)) + warnings.warn( + "No pitch found in line: {}, transforming to a rest".format(line) + ) pitch = "r" else: pitch = find_pitch.group(0) @@ -678,15 +716,39 @@ def meta_note_line(self, line: str, voice=None, add=True): # extract symbol can be any of the following: _()[]{}<>|: symbols = re.findall(r"([_()\[\]{}<>|:])", line) symbolic_duration = self._process_kern_duration(duration, is_grace="q" in line) - el_id = "{}-s{}-v{}-el{}".format(self.id, self.staff, voice, self.total_parsed_elements) + el_id = "{}-s{}-v{}-el{}".format( + self.id, self.staff, voice, self.total_parsed_elements + ) if pitch.startswith("r"): - return spt.Rest(symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) + return spt.Rest( + symbolic_duration=symbolic_duration, + staff=self.staff, + voice=voice, + id=el_id, + ) step, octave, alter = self._process_kern_pitch(pitch) # check if the note is a grace note if "q" in line: - note = spt.GraceNote(grace_type="grace", step=step, octave=octave, alter=alter, symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) + note = spt.GraceNote( + grace_type="grace", + step=step, + octave=octave, + alter=alter, + symbolic_duration=symbolic_duration, + staff=self.staff, + voice=voice, + id=el_id, + ) else: - note = spt.Note(step, octave, alter, symbolic_duration=symbolic_duration, staff=self.staff, voice=voice, id=el_id) + note = spt.Note( + step, + octave, + alter, + symbolic_duration=symbolic_duration, + staff=self.staff, + voice=voice, + id=el_id, + ) if symbols: self.process_symbol(note, symbols) return note diff --git a/partitura/score.py b/partitura/score.py index 83edb150..659594ad 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -4853,39 +4853,73 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: # get note with min start.t min_start_note = notes_per_vocstaff[np.argmin(notes_per_vocstaff.start.t)] if min_start_note.start.t > start_time: - sym_dur = estimate_symbolic_duration(min_start_note.start.t - start_time, part._quarter_durations[0]) - rest = Rest(symbolic_duration=sym_dur, staff=min_start_note.staff, voice=min_start_note.voice) + sym_dur = estimate_symbolic_duration( + min_start_note.start.t - start_time, part._quarter_durations[0] + ) + rest = Rest( + symbolic_duration=sym_dur, + staff=min_start_note.staff, + voice=min_start_note.voice, + ) part.add(rest, start_time, min_start_note.start.t) - min_end_note = notes_per_vocstaff[np.argmin(np.vectorize(lambda x: x.end.t)(notes_per_vocstaff))] + min_end_note = notes_per_vocstaff[ + np.argmin(np.vectorize(lambda x: x.end.t)(notes_per_vocstaff)) + ] if min_end_note.end.t < end_time: - sym_dur = estimate_symbolic_duration(end_time - min_end_note.end.t, part._quarter_durations[0]) - rest = Rest(symbolic_duration=sym_dur, staff=min_end_note.staff, voice=min_end_note.voice) + sym_dur = estimate_symbolic_duration( + end_time - min_end_note.end.t, part._quarter_durations[0] + ) + rest = Rest( + symbolic_duration=sym_dur, + staff=min_end_note.staff, + voice=min_end_note.voice, + ) part.add(rest, min_end_note.end.t, end_time) -def _fill_rests_global(measure: Measure, part: Part, unique_voc_staff: np.ndarray) -> None: +def _fill_rests_global( + measure: Measure, part: Part, unique_voc_staff: np.ndarray +) -> None: start_time = measure.start.t end_time = measure.end.t if end_time - start_time == 0: return - notes = np.array(list(part.iter_all(GenericNote, start_time, end_time, include_subclasses=True))) + notes = np.array( + list(part.iter_all(GenericNote, start_time, end_time, include_subclasses=True)) + ) voc_staff = np.array([[n.voice, n.staff] for n in notes]) un_voc_staff, inverse_map = np.unique(voc_staff, axis=0, return_inverse=True) for i in range(un_voc_staff.shape[0]): note_mask = inverse_map == i notes_per_vocstaff = notes[note_mask] # get note with min start.t - min_start_note = notes_per_vocstaff[np.argmin(np.vectorize(lambda x: x.start.t)(notes_per_vocstaff))] + min_start_note = notes_per_vocstaff[ + np.argmin(np.vectorize(lambda x: x.start.t)(notes_per_vocstaff)) + ] if min_start_note.start.t > start_time: - sym_dur = estimate_symbolic_duration(min_start_note.start.t - start_time, part._quarter_durations[0]) - rest = Rest(symbolic_duration=sym_dur, staff=min_start_note.staff, voice=min_start_note.voice) + sym_dur = estimate_symbolic_duration( + min_start_note.start.t - start_time, part._quarter_durations[0] + ) + rest = Rest( + symbolic_duration=sym_dur, + staff=min_start_note.staff, + voice=min_start_note.voice, + ) part.add(rest, start_time, min_start_note.start.t) - min_end_note = notes_per_vocstaff[np.argmax(np.vectorize(lambda x: x.end.t)(notes_per_vocstaff))] + min_end_note = notes_per_vocstaff[ + np.argmax(np.vectorize(lambda x: x.end.t)(notes_per_vocstaff)) + ] if min_end_note.end.t < end_time: - sym_dur = estimate_symbolic_duration(end_time - min_end_note.end.t, part._quarter_durations[0]) - rest = Rest(symbolic_duration=sym_dur, staff=min_end_note.staff, voice=min_end_note.voice) + sym_dur = estimate_symbolic_duration( + end_time - min_end_note.end.t, part._quarter_durations[0] + ) + rest = Rest( + symbolic_duration=sym_dur, + staff=min_end_note.staff, + voice=min_end_note.voice, + ) part.add(rest, min_end_note.end.t, end_time) if un_voc_staff.shape[0] != unique_voc_staff.shape[0]: @@ -4893,12 +4927,16 @@ def _fill_rests_global(measure: Measure, part: Part, unique_voc_staff: np.ndarra diff = unique_voc_staff else: # View `un_voc_staff` and `unique_voc_staff` as 1-D structured arrays - x_sa = un_voc_staff.view([('', un_voc_staff.dtype)] * un_voc_staff.shape[1]) - y_sa = unique_voc_staff.view([('', unique_voc_staff.dtype)] * unique_voc_staff.shape[1]) + x_sa = un_voc_staff.view([("", un_voc_staff.dtype)] * un_voc_staff.shape[1]) + y_sa = unique_voc_staff.view( + [("", unique_voc_staff.dtype)] * unique_voc_staff.shape[1] + ) # Find rows in `unique_voc_staff` that are not in `un_voc_staff` diff = np.setdiff1d(y_sa, x_sa) for voice, staff in diff: - sym_dur = estimate_symbolic_duration(end_time - start_time, part._quarter_durations[0]) + sym_dur = estimate_symbolic_duration( + end_time - start_time, part._quarter_durations[0] + ) rest = Rest(symbolic_duration=sym_dur, staff=staff, voice=voice) part.add(rest, start_time, end_time) @@ -4929,7 +4967,8 @@ def fill_rests(score_data: ScoreLike, measurewise=True) -> None: else: note_array = part.note_array(include_staff=True) unique_vocstaff = np.unique( - np.array([note_array["voice"], note_array["staff"]], dtype=np.int64), axis=1 + np.array([note_array["voice"], note_array["staff"]], dtype=np.int64), + axis=1, ) for measure in measures: _fill_rests_global(measure, part, unique_vocstaff.T) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 83d33343..456c7865 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -942,11 +942,11 @@ def estimate_symbolic_duration(dur, div, eps=10**-3): return SYM_DURS[i].copy() else: # NOTE: Guess tuplets (Naive) it doesn't cover composite durations from tied notes. - type = SYM_DURS[i+3]["type"] + type = SYM_DURS[i + 3]["type"] normal_notes = 2 return { "type": type, - "actual_notes": math.ceil(normal_notes/qdur), + "actual_notes": math.ceil(normal_notes / qdur), "normal_notes": normal_notes, } From 61881a482f6cc70fd2184f8a6546f22c0afd2172 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 27 May 2024 15:06:03 +0200 Subject: [PATCH 175/197] Correcting documentation for `estimate_symbolic_duration`. --- partitura/utils/music.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index aad5178c..46d123a0 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -322,15 +322,15 @@ class MIDITokenizer(object): A4 = 440.0 COMPOSITE_DURS = np.array( - [1 + 4/16, 1 + 4/32, 2+4/8, 2+4/16, 2+4/32] + [1 + 4/32, 1 + 4/16, 2+4/32, 2+4/16, 2+4/8] ) SYM_COMPOSITE_DURS = [ - ({"type": "quarter", "dots": 0}, {"type": "16nd", "dots": 0}), ({"type": "quarter", "dots": 0}, {"type": "32nd", "dots": 0}), - ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0}), + ({"type": "quarter", "dots": 0}, {"type": "16nd", "dots": 0}), + ({"type": "half", "dots": 0}, {"type": "32nd", "dots": 0}), ({"type": "half", "dots": 0}, {"type": "16th", "dots": 0}), - ({"type": "half", "dots": 0}, {"type": "32nd", "dots": 0}) + ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0}), ] @@ -908,15 +908,14 @@ def key_int_to_mode(mode): raise ValueError("Unknown mode {}".format(mode)) -def estimate_symbolic_duration(dur, div, eps=10**-3, return_com_durations=False) -> Union[Union[Dict[str, Any], Tuple[Dict[str, Any]]], None]: +def estimate_symbolic_duration(dur, div, eps=10**-3, return_com_durations=False) -> Union[Dict[str, Any], Tuple[Dict[str, Any]]]: """Given a numeric duration, a divisions value (specifiying the number of units per quarter note) and optionally a tolerance `eps` for numerical imprecisions, estimate corresponding the symbolic duration. If a matching symbolic duration is found, it is returned as a tuple (type, dots), where type is a string such as 'quarter', or '16th', and dots is an integer specifying the number of dots. - If no matching symbolic duration is found the function returns - None. + NOTE : this function does not estimate composite durations, nor time-modifications such as triplets. @@ -934,9 +933,8 @@ def estimate_symbolic_duration(dur, div, eps=10**-3, return_com_durations=False) Returns ------- - out: Union[Union[Dict[str, Any], Tuple[Dict[str, Any]]], None] - Symbolic duration as a dictionary, or None if no matching - duration is found. When a composite duration is found, then it returns a tuple of symbolic durations. + out: Union[Dict[str, Any], Tuple[Dict[str, Any]]] + Symbolic duration as a dictionary. When a composite duration is found, then it returns a tuple of symbolic durations. The returned tuple should be tied notes. Examples @@ -947,10 +945,15 @@ def estimate_symbolic_duration(dur, div, eps=10**-3, return_com_durations=False) >>> estimate_symbolic_duration(15, 10) {'type': 'quarter', 'dots': 1} - The following example returns None: + >>> estimate_symbolic_duration(15, 16) + {'type': 'eighth', 'dots': 3} - >>> estimate_symbolic_duration(23, 16) + >>> estimate_symbolic_duration(4, 6) + {'type': 'eighth', 'actual_notes': 3, 'normal_notes': 2} + It can also return composite durations: + >>> estimate_symbolic_duration(34, 16, return_com_durations=True) + ({'type': 'half', 'dots': 0}, {'type': '32nd', 'dots': 0}) """ global DURS, SYM_DURS qdur = dur / div From 1b10d9063642afe6a7b4aa3e26e1f29d6f39d1ab Mon Sep 17 00:00:00 2001 From: fosfrancesco Date: Mon, 27 May 2024 13:28:30 +0000 Subject: [PATCH 176/197] Format code with black (bot) --- partitura/io/importmusicxml.py | 4 ++- partitura/score.py | 48 +++++++++++++++++++++++++--------- partitura/utils/music.py | 11 ++++---- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index f19f0d8e..8f60b68d 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -1449,7 +1449,9 @@ 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, "Tuplet start time is after tuplet stop time" + assert ( + 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 diff --git a/partitura/score.py b/partitura/score.py index 76af2cef..7f027aac 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -3846,7 +3846,9 @@ def find_tuplets(part): start_note = note_tuplet[0] stop_note = note_tuplet[-1] tuplet = Tuplet(start_note, stop_note) - assert start_note.start.t <= stop_note.start.t, "The start note of a Tuplet should be before the stop note" + assert ( + start_note.start.t <= stop_note.start.t + ), "The start note of a Tuplet should be before the stop note" part.add(tuplet, start_note.start.t, stop_note.end.t) tup_start += actual_notes @@ -5145,12 +5147,16 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: if staff not in unique_staff: # solution when estimation returns composite durations. sym_dur = estimate_symbolic_duration( - end_time - start_time, part._quarter_durations[0], return_com_durations=True + end_time - start_time, + part._quarter_durations[0], + return_com_durations=True, ) if isinstance(sym_dur, tuple): st = start_time for i, sd in enumerate(sym_dur): - et = start_time + symbolic_to_numeric_duration(sd, part._quarter_durations[0]) + et = start_time + symbolic_to_numeric_duration( + sd, part._quarter_durations[0] + ) rest = Rest( symbolic_duration=sd, staff=staff, voice=un_voice.max() + 1 ) @@ -5173,15 +5179,21 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: min_start_note = notes_per_vocstaff[sort_note_start[0]] if min_start_note.start.t > start_time: sym_dur = estimate_symbolic_duration( - min_start_note.start.t - start_time, part._quarter_durations[0], return_com_durations=True + min_start_note.start.t - start_time, + part._quarter_durations[0], + return_com_durations=True, ) # solution when estimation returns composite durations. if isinstance(sym_dur, tuple): st = start_time for i, sd in enumerate(sym_dur): - et = st + symbolic_to_numeric_duration(sd, part._quarter_durations[0]) + et = st + symbolic_to_numeric_duration( + sd, part._quarter_durations[0] + ) rest = Rest( - symbolic_duration=sd, staff=min_start_note.staff, voice=min_start_note.voice + symbolic_duration=sd, + staff=min_start_note.staff, + voice=min_start_note.voice, ) part.add(rest, st, et) st = et @@ -5197,15 +5209,21 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: min_end_note = notes_per_vocstaff[sort_note_end[-1]] if min_end_note.end.t < end_time: sym_dur = estimate_symbolic_duration( - end_time - min_end_note.end.t, part._quarter_durations[0], return_com_durations=True + end_time - min_end_note.end.t, + part._quarter_durations[0], + return_com_durations=True, ) # solution when estimation returns composite durations. if isinstance(sym_dur, tuple): st = min_end_note.end.t for i, sd in enumerate(sym_dur): - et = st + symbolic_to_numeric_duration(sd, part._quarter_durations[0]) + et = st + symbolic_to_numeric_duration( + sd, part._quarter_durations[0] + ) rest = Rest( - symbolic_duration=sd, staff=min_end_note.staff, voice=min_end_note.voice + symbolic_duration=sd, + staff=min_end_note.staff, + voice=min_end_note.voice, ) part.add(rest, st, et) st = et @@ -5228,15 +5246,19 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None: sym_dur = estimate_symbolic_duration( notes_per_vocstaff[sort_note_start[i]].start.t - notes_per_vocstaff[sort_note_end[i - 1]].end.t, - part._quarter_durations[0], return_com_durations=True + part._quarter_durations[0], + return_com_durations=True, ) if isinstance(sym_dur, tuple): st = notes_per_vocstaff[sort_note_end[i - 1]].end.t for i, sd in enumerate(sym_dur): - et = st + symbolic_to_numeric_duration(sd, part._quarter_durations[0]) + et = st + symbolic_to_numeric_duration( + sd, part._quarter_durations[0] + ) rest = Rest( - symbolic_duration=sd, staff=notes_per_vocstaff[sort_note_end[i - 1]].staff, - voice=notes_per_vocstaff[sort_note_end[i - 1]].voice + symbolic_duration=sd, + staff=notes_per_vocstaff[sort_note_end[i - 1]].staff, + voice=notes_per_vocstaff[sort_note_end[i - 1]].voice, ) part.add(rest, st, et) st = et diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 46d123a0..c86066c9 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -172,7 +172,6 @@ class MIDITokenizer(object): ) - SYM_DURS = [ {"type": "256th", "dots": 0}, {"type": "256th", "dots": 1}, @@ -321,9 +320,7 @@ class MIDITokenizer(object): # 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 / 32, 1 + 4 / 16, 2 + 4 / 32, 2 + 4 / 16, 2 + 4 / 8]) SYM_COMPOSITE_DURS = [ ({"type": "quarter", "dots": 0}, {"type": "32nd", "dots": 0}), @@ -331,7 +328,7 @@ class MIDITokenizer(object): ({"type": "half", "dots": 0}, {"type": "32nd", "dots": 0}), ({"type": "half", "dots": 0}, {"type": "16th", "dots": 0}), ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0}), - ] +] def ensure_notearray(notearray_or_part, *args, **kwargs): @@ -908,7 +905,9 @@ def key_int_to_mode(mode): raise ValueError("Unknown mode {}".format(mode)) -def estimate_symbolic_duration(dur, div, eps=10**-3, return_com_durations=False) -> Union[Dict[str, Any], Tuple[Dict[str, Any]]]: +def estimate_symbolic_duration( + dur, div, eps=10**-3, return_com_durations=False +) -> Union[Dict[str, Any], Tuple[Dict[str, Any]]]: """Given a numeric duration, a divisions value (specifiying the number of units per quarter note) and optionally a tolerance `eps` for numerical imprecisions, estimate corresponding the symbolic From 1e282d9d5432340fbbd23a16ae717f3e199eaee9 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 27 May 2024 16:11:35 +0200 Subject: [PATCH 177/197] moving composite duration dictionaries to globals. --- partitura/utils/globals.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/partitura/utils/globals.py b/partitura/utils/globals.py index 7ce87baa..65935698 100644 --- a/partitura/utils/globals.py +++ b/partitura/utils/globals.py @@ -281,6 +281,16 @@ # 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]) + +SYM_COMPOSITE_DURS = [ + ({"type": "quarter", "dots": 0}, {"type": "32nd", "dots": 0}), + ({"type": "quarter", "dots": 0}, {"type": "16nd", "dots": 0}), + ({"type": "half", "dots": 0}, {"type": "32nd", "dots": 0}), + ({"type": "half", "dots": 0}, {"type": "16th", "dots": 0}), + ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0}), +] + UNABBREVS = [ (re.compile(r"(crescendo|cresc\.?)"), "crescendo"), From a4e899b9b893ac1cbedf4b26bb6955b364f71117 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 3 Jul 2024 14:43:29 +0200 Subject: [PATCH 178/197] Soling review comment on alters: https://github.com/CPJKU/partitura/pull/345#discussion_r1660595638 --- partitura/io/importdcml.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index b6db4a5e..274296ef 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -28,19 +28,19 @@ def read_note_tsv(note_tsv_path, metadata=None): quarter_durations = data["duration_qb"] duration_div = np.array([ceil(qd * qdivs) for qd in quarter_durations]) onset_div = np.array([ceil(qd * qdivs) for qd in data["quarterbeats"]]) - flats = data["name"].str.contains("b") - sharps = data["name"].str.contains("#") - double_sharps = data["name"].str.contains("##") - double_flats = data["name"].str.contains("bb") - alter = np.zeros(len(data), dtype=np.int32) - alter[flats] = -1 - alter[sharps] = 1 - alter[double_sharps] = 2 - alter[double_flats] = -2 + data["alter"] = data["name"].str.count("#") - data["name"].str.count("b") + # flats = data["name"].str.contains("b") + # sharps = data["name"].str.contains("#") + # double_sharps = data["name"].str.contains("##") + # double_flats = data["name"].str.contains("bb") + # alter = np.zeros(len(data), dtype=np.int32) + # alter[flats] = -1 + # alter[sharps] = 1 + # alter[double_sharps] = 2 + # alter[double_flats] = -2 data["step"] = data["name"].apply(lambda x: x[0]) data["onset_div"] = onset_div data["duration_div"] = duration_div - data["alter"] = alter data["pitch"] = data["midi"] grace_mask = ~data["gracenote"].isna().to_numpy() if "gracenote" in data.columns else np.zeros(len(data), dtype=bool) data["id"] = np.arange(len(data)) From 4e3f1ab544501e67187a4a49d2fe180742f4a314 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 3 Jul 2024 14:44:35 +0200 Subject: [PATCH 179/197] Soling review comment on voice assignment: https://github.com/CPJKU/partitura/pull/345#discussion_r1660654382 --- partitura/io/importdcml.py | 1 + 1 file changed, 1 insertion(+) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 274296ef..cf837eff 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -45,6 +45,7 @@ def read_note_tsv(note_tsv_path, metadata=None): grace_mask = ~data["gracenote"].isna().to_numpy() if "gracenote" in data.columns else np.zeros(len(data), dtype=bool) data["id"] = np.arange(len(data)) # Rewrite Voices for correct export + # taking the maximum voice number for the entire staff, and having the second staff starting from that number. staffs = data["staff"].unique() re_index_voice_value = 0 for staff in staffs: From 160707e832485f9028e9bb9adc50436c6f616935 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 3 Jul 2024 14:47:01 +0200 Subject: [PATCH 180/197] Soling review comment on grace notes: https://github.com/CPJKU/partitura/pull/345#discussion_r1660658511 --- partitura/io/importdcml.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index cf837eff..501c8fd1 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -29,15 +29,6 @@ def read_note_tsv(note_tsv_path, metadata=None): duration_div = np.array([ceil(qd * qdivs) for qd in quarter_durations]) onset_div = np.array([ceil(qd * qdivs) for qd in data["quarterbeats"]]) data["alter"] = data["name"].str.count("#") - data["name"].str.count("b") - # flats = data["name"].str.contains("b") - # sharps = data["name"].str.contains("#") - # double_sharps = data["name"].str.contains("##") - # double_flats = data["name"].str.contains("bb") - # alter = np.zeros(len(data), dtype=np.int32) - # alter[flats] = -1 - # alter[sharps] = 1 - # alter[double_sharps] = 2 - # alter[double_flats] = -2 data["step"] = data["name"].apply(lambda x: x[0]) data["onset_div"] = onset_div data["duration_div"] = duration_div @@ -104,6 +95,9 @@ def read_note_tsv(note_tsv_path, metadata=None): while note["staff"] != next_note["staff"] or note["voice"] != next_note["voice"]: i += 1 next_note = note_array[grace_idx+i] + if i > 10: + warnings.warn("Grace note ignored, no matching main note found within 10 notes.") + break assert note["staff"] == next_note["staff"], "Grace note and main note must be in the same staff" assert note["voice"] == next_note["voice"], "Grace note and main note must be in the same voice" assert note["onset_div"] == next_note[ From c1d1ce526bef8375a40cd50b6220f9e5bc596b67 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 3 Jul 2024 14:52:49 +0200 Subject: [PATCH 181/197] Added minor comment. --- partitura/io/importdcml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 501c8fd1..4dd58553 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -120,7 +120,7 @@ def read_note_tsv(note_tsv_path, metadata=None): part.add(spt.Clef(staff=1, sign="G", line=2, octave_change=0), start=0) part.add(spt.Clef(staff=2, sign="F", line=4, octave_change=0), start=0) - # TODO: Find Ties + # Add Ties tied_note_mask = data["tied"] == 1 for tied_note in note_array[tied_note_mask]: for note in part.iter_all(spt.Note, tied_note["onset_div"], tied_note["onset_div"]+1): From f93d11b991dd0a89d768570090f8fa227b0810ca Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 3 Jul 2024 14:54:25 +0200 Subject: [PATCH 182/197] Solved review comment on local key processing: https://github.com/CPJKU/partitura/pull/345#discussion_r1660683407 --- partitura/score.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index e165e4a2..c69f691a 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -5770,21 +5770,21 @@ def is_a_within_b(a, b, wholly=False): return contained -def process_local_key(loc_k, glob_k, return_step_alter=False): - local_key_sharps = loc_k.count("#") - local_key_flats = loc_k.count("b") - local_key = loc_k.replace("#", "").replace("b", "") +def process_local_key(loc_k_text, glob_k_text, return_step_alter=False): + local_key_sharps = loc_k_text.count("#") + local_key_flats = loc_k_text.count("b") + local_key = loc_k_text.replace("#", "").replace("b", "") local_key_is_minor = local_key.islower() local_key = local_key.lower() - global_key_is_minor = glob_k.islower() + global_key_is_minor = glob_k_text.islower() if local_key_is_minor == global_key_is_minor and local_key == "i" and local_key_sharps - local_key_flats == 0 and (not return_step_alter): - return glob_k - g_key = "minor" if glob_k.islower() else "major" + return glob_k_text + g_key = "minor" if glob_k_text.islower() else "major" num, qual = LOCAL_KEY_TRASPOSITIONS_DCML[g_key][local_key] transposition_interval = Interval(num, qual) transposition_interval = transposition_interval.change_quality(local_key_sharps - local_key_flats) - key_step = re.search(r"[a-gA-G]", glob_k).group(0) - key_alter = re.search(r"[#b]", glob_k).group(0) if re.search(r"[#b]", glob_k) else "" + key_step = re.search(r"[a-gA-G]", glob_k_text).group(0) + key_alter = re.search(r"[#b]", glob_k_text).group(0) if re.search(r"[#b]", glob_k_text) else "" key_alter = key_alter.replace("b", "-") key_alter = ALT_TO_INT[key_alter] key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) From 2083f1535c4b743b46e59d2316b8bd58b01a56d0 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 3 Jul 2024 15:01:31 +0200 Subject: [PATCH 183/197] Solved review comment on process inversion of RN: https://github.com/CPJKU/partitura/pull/345#discussion_r1660676860 --- partitura/score.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index c69f691a..4e4da6ca 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -2814,6 +2814,8 @@ def __init__(self, text, inversion=None, local_key=None, primary_degree=None, se super().__init__(text) self.text = text self.accepted_qualities = ('7', 'aug', 'aug6', 'aug7', 'dim', 'dim7', 'hdim7', 'maj', 'maj7', 'min', 'min7') + # The key of an inversion is text from RN string, and the value is a tuple (has_seven,inversion) + self.accepted_inversions = {"2": (3, True), "43": (2, True), "64": (2, False), "6": (1, False), "65": (1, True), "7": (0, True)} self.has_seven = "7" in text self.inversion = inversion if inversion is not None else self._process_inversion() self.local_key = local_key if local_key is not None else self._process_local_key() @@ -2831,22 +2833,9 @@ def _process_inversion(self): # If there is no inversion, return 0 numeric_indications_in_text = re.findall(r'\d+', self.text) if len(numeric_indications_in_text) > 0: - inversion_state = int(numeric_indications_in_text[0]) - if inversion_state == 2: - self.has_seven = True - return 3 - elif inversion_state == 43: - self.has_seven = True - return 2 - elif inversion_state == 64: - self.has_seven = False - return 2 - elif inversion_state == 6: - self.has_seven = False - return 1 - elif inversion_state == 65: - self.has_seven = True - return 1 + inversion, has_seven = self.accepted_inversions.get(numeric_indications_in_text[0], (0, False)) + self.has_seven = has_seven + return inversion return 0 def _process_local_key(self): From c867eb2e8ab030fd442818a6cf34009bee1eda29 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 3 Jul 2024 15:07:17 +0200 Subject: [PATCH 184/197] Added documentation on Interval class. --- partitura/score.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/partitura/score.py b/partitura/score.py index 4e4da6ca..4808442e 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -3065,6 +3065,26 @@ def semitones(self): return INTERVAL_TO_SEMITONES[self.quality + str(self.number)] def change_quality(self, num): + """ + Change the quality of the interval by a given number of semitones. + + The Interval Number is not changed, only the quality. + + Examples: + - M3 -> m3, num=-1 + - M3 -> A3, num=1 + - A4 -> d4, num=-2 + + Parameters + ---------- + num: int + The number of semitones to change the quality by. + + Returns + ------- + Interval + The interval with the new quality, but the same number and direction. + """ change_direction_c = ["dd", "d", "P", "A", "AA"] change_direction_d = ["dd", "d", "m", "M", "A", "AA"] From 60a0662a024da32e66808fe622a2f5266118b65e Mon Sep 17 00:00:00 2001 From: fosfrancesco Date: Tue, 16 Jul 2024 09:02:03 +0000 Subject: [PATCH 185/197] Format code with black (bot) --- partitura/directions.py | 3 - partitura/io/importdcml.py | 179 ++++++++++++----- partitura/musicanalysis/key_identification.py | 11 +- partitura/musicanalysis/meter.py | 16 +- partitura/score.py | 181 ++++++++++++++---- partitura/utils/globals.py | 49 ++++- partitura/utils/music.py | 14 +- partitura/utils/normalize.py | 1 - partitura/utils/synth.py | 9 +- 9 files changed, 355 insertions(+), 108 deletions(-) diff --git a/partitura/directions.py b/partitura/directions.py index 9c8081d4..d0428911 100644 --- a/partitura/directions.py +++ b/partitura/directions.py @@ -52,9 +52,6 @@ def join_items(items): ) - - - def unabbreviate(s): for p, v in UNABBREVS: if p.match(s): diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 4dd58553..78692131 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -5,6 +5,7 @@ import partitura.score as spt from partitura.score import process_local_key from partitura.utils.music import estimate_symbolic_duration + try: import pandas as pd except ImportError: @@ -19,8 +20,11 @@ def read_note_tsv(note_tsv_path, metadata=None): # (It happens with voltas when the second volta has a different number of measures) if not np.all(data["quarterbeats"].isna() == False): data = data[~data["quarterbeats"].isna()] - data["quarterbeats"] = data["quarterbeats"].apply(eval) if data.dtypes["quarterbeats"] == str or data.dtypes[ - "quarterbeats"] == object else data["quarterbeats"] + data["quarterbeats"] = ( + data["quarterbeats"].apply(eval) + if data.dtypes["quarterbeats"] == str or data.dtypes["quarterbeats"] == object + else data["quarterbeats"] + ) unique_durations = data["duration"].unique() denominators = [int(qb.split("/")[1]) for qb in unique_durations if "/" in qb] # transform quarter_beats to quarter_divs @@ -33,7 +37,11 @@ def read_note_tsv(note_tsv_path, metadata=None): data["onset_div"] = onset_div data["duration_div"] = duration_div data["pitch"] = data["midi"] - grace_mask = ~data["gracenote"].isna().to_numpy() if "gracenote" in data.columns else np.zeros(len(data), dtype=bool) + grace_mask = ( + ~data["gracenote"].isna().to_numpy() + if "gracenote" in data.columns + else np.zeros(len(data), dtype=bool) + ) data["id"] = np.arange(len(data)) # Rewrite Voices for correct export # taking the maximum voice number for the entire staff, and having the second staff starting from that number. @@ -46,7 +54,19 @@ def read_note_tsv(note_tsv_path, metadata=None): # update re_index_voice_value re_index_voice_value = data.loc[staff_mask, "voice"].max() - note_array = data[["onset_div", "duration_div", "pitch", "step", "alter", "octave", "id", "staff", "voice"]].to_records(index=False) + note_array = data[ + [ + "onset_div", + "duration_div", + "pitch", + "step", + "alter", + "octave", + "id", + "staff", + "voice", + ] + ].to_records(index=False) part = spt.Part("P0", "Metadata", quarter_duration=qdivs) # Add notes and grace notes @@ -66,7 +86,9 @@ def read_note_tsv(note_tsv_path, metadata=None): if grace_mask[n_idx - 1]: prev_note = note_array[n_idx - 1] - for note_prev in part.iter_all(spt.GraceNote, note["onset_div"], note["onset_div"] + 1): + for note_prev in part.iter_all( + spt.GraceNote, note["onset_div"], note["onset_div"] + 1 + ): if note_prev.id == "n-{}".format(prev_note["id"]): note_el.grace_prev = note_prev note_prev.grace_next = note_el @@ -74,14 +96,19 @@ def read_note_tsv(note_tsv_path, metadata=None): else: symbolic_duration = estimate_symbolic_duration(note["duration_div"], qdivs) note_el = spt.Note( - id="n-{}".format(note["id"]), - step=note["step"], - octave=note["octave"], - alter=note["alter"], - staff=note["staff"], - voice=note["voice"], - symbolic_duration=symbolic_duration) - part.add(note_el, start=note["onset_div"], end=(note["onset_div"]+note["duration_div"])) + id="n-{}".format(note["id"]), + step=note["step"], + octave=note["octave"], + alter=note["alter"], + staff=note["staff"], + voice=note["voice"], + symbolic_duration=symbolic_duration, + ) + part.add( + note_el, + start=note["onset_div"], + end=(note["onset_div"] + note["duration_div"]), + ) # Curate grace notes grace_note_idxs = np.where(grace_mask)[0] @@ -89,32 +116,54 @@ def read_note_tsv(note_tsv_path, metadata=None): grace_idx = grace_note_idxs[i] note = note_array[grace_idx] # Find the next note in the same staff and voice - if not grace_mask[grace_idx+1]: + if not grace_mask[grace_idx + 1]: i = 1 - next_note = note_array[grace_idx+i] - while note["staff"] != next_note["staff"] or note["voice"] != next_note["voice"]: + next_note = note_array[grace_idx + i] + while ( + note["staff"] != next_note["staff"] + or note["voice"] != next_note["voice"] + ): i += 1 - next_note = note_array[grace_idx+i] + next_note = note_array[grace_idx + i] if i > 10: - warnings.warn("Grace note ignored, no matching main note found within 10 notes.") + warnings.warn( + "Grace note ignored, no matching main note found within 10 notes." + ) break - assert note["staff"] == next_note["staff"], "Grace note and main note must be in the same staff" - assert note["voice"] == next_note["voice"], "Grace note and main note must be in the same voice" - assert note["onset_div"] == next_note[ - "onset_div"], "Grace note and main note must have the same onset" - for note in part.iter_all(spt.Note, note["onset_div"], note["onset_div"]+1): + assert ( + note["staff"] == next_note["staff"] + ), "Grace note and main note must be in the same staff" + assert ( + note["voice"] == next_note["voice"] + ), "Grace note and main note must be in the same voice" + assert ( + note["onset_div"] == next_note["onset_div"] + ), "Grace note and main note must have the same onset" + for note in part.iter_all( + spt.Note, note["onset_div"], note["onset_div"] + 1 + ): if note.id == "n-{}".format(next_note["id"]): grace_el.grace_next = note break # Find time signatures - time_signatures_changes = data["timesig"][data["timesig"].shift(1) != data["timesig"]].index + time_signatures_changes = data["timesig"][ + data["timesig"].shift(1) != data["timesig"] + ].index time_signatures = data["timesig"][time_signatures_changes] - start_divs = np.array([int(qd * qdivs) for qd in data["quarterbeats"][time_signatures_changes]]) - end_of_piece = (note_array["onset_div"]+note_array["duration_div"]).max() + start_divs = np.array( + [int(qd * qdivs) for qd in data["quarterbeats"][time_signatures_changes]] + ) + end_of_piece = (note_array["onset_div"] + note_array["duration_div"]).max() end_divs = np.r_[start_divs[1:], end_of_piece] for ts, start, end in zip(time_signatures, start_divs, end_divs): - part.add(spt.TimeSignature(beats=int(ts.split("/")[0]), beat_type=int(ts.split("/")[1])), start=start, end=end) + part.add( + spt.TimeSignature( + beats=int(ts.split("/")[0]), beat_type=int(ts.split("/")[1]) + ), + start=start, + end=end, + ) # Add default clefs for piano pieces (Naive) part.add(spt.Clef(staff=1, sign="G", line=2, octave_change=0), start=0) @@ -123,13 +172,21 @@ def read_note_tsv(note_tsv_path, metadata=None): # Add Ties tied_note_mask = data["tied"] == 1 for tied_note in note_array[tied_note_mask]: - for note in part.iter_all(spt.Note, tied_note["onset_div"], tied_note["onset_div"]+1): + for note in part.iter_all( + spt.Note, tied_note["onset_div"], tied_note["onset_div"] + 1 + ): if note.id == "n-{}".format(tied_note["id"]): found_next = False - for note_next in part.iter_all(spt.Note, note.end.t, note.end.t+1, mode="starting"): - condition = note_next.alter == note.alter and note_next.step == note.step and \ - note_next.octave == note.octave and note.voice == note_next.voice and \ - note.staff == note_next.staff + for note_next in part.iter_all( + spt.Note, note.end.t, note.end.t + 1, mode="starting" + ): + condition = ( + note_next.alter == note.alter + and note_next.step == note.step + and note_next.octave == note.octave + and note.voice == note_next.voice + and note.staff == note_next.staff + ) if condition: note.tie_next = note_next note_next.tie_prev = note @@ -148,14 +205,22 @@ def read_measure_tsv(measure_tsv_path, part): # (It happens with voltas when the second volta has a different number of measures) if not np.all(data["quarterbeats"].isna() == False): data = data[~data["quarterbeats"].isna()] - data["quarterbeats"] = data["quarterbeats"].apply(eval) if data.dtypes["quarterbeats"] == str or data.dtypes["quarterbeats"] == object else data["quarterbeats"] + data["quarterbeats"] = ( + data["quarterbeats"].apply(eval) + if data.dtypes["quarterbeats"] == str or data.dtypes["quarterbeats"] == object + else data["quarterbeats"] + ) data["onset_div"] = np.array([int(qd * qdivs) for qd in data["quarterbeats"]]) data["duration_div"] = np.array([int(qd * qdivs) for qd in data["duration_qb"]]) # Get first index repeat_index, _ = next(data.iterrows()) for idx, row in data.iterrows(): - part.add(spt.Measure(number=row["mc"], name=row["mn"]), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) + part.add( + spt.Measure(number=row["mc"], name=row["mn"]), + start=row["onset_div"], + end=row["onset_div"] + row["duration_div"], + ) if row["repeats"] == "start": repeat_index = idx @@ -175,8 +240,11 @@ def read_harmony_tsv(beat_tsv_path, part): # (It happens with voltas when the second volta has a different number of measures) if not np.all(data["quarterbeats"].isna() == False): data = data[~data["quarterbeats"].isna()] - data["quarterbeats"] = data["quarterbeats"].apply(eval) if data.dtypes["quarterbeats"] == str or data.dtypes[ - "quarterbeats"] == object else data["quarterbeats"] + data["quarterbeats"] = ( + data["quarterbeats"].apply(eval) + if data.dtypes["quarterbeats"] == str or data.dtypes["quarterbeats"] == object + else data["quarterbeats"] + ) data["onset_div"] = np.array([int(qd * qdivs) for qd in data["quarterbeats"]]) data["duration_div"] = np.array([int(qd * qdivs) for qd in data["duration_qb"]]) is_na_cad = data["cadence"].isna() @@ -189,21 +257,29 @@ def read_harmony_tsv(beat_tsv_path, part): # Local key is in relation to the global key. if "/" in row["localkey"]: # if the local key has a secondary degree (e.g. "V/IV") we need to process it differently - inter_key = process_local_key(row["localkey"].split("/")[-1], row["globalkey"]) + inter_key = process_local_key( + row["localkey"].split("/")[-1], row["globalkey"] + ) local_key = process_local_key(row["localkey"].split("/")[0], inter_key) else: local_key = process_local_key(row["localkey"], row["globalkey"]) part.add( - spt.RomanNumeral(text=row["chord"], - local_key=local_key, - # quality=row["chord_type"], - ), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) + spt.RomanNumeral( + text=row["chord"], + local_key=local_key, + # quality=row["chord_type"], + ), + start=row["onset_div"], + end=row["onset_div"] + row["duration_div"], + ) for idx, row in data[~is_na_cad].iterrows(): if "/" in row["localkey"]: # if the local key has a secondary degree (e.g. "V/IV") we need to process it differently - inter_key = process_local_key(row["localkey"].split("/")[-1], row["globalkey"]) + inter_key = process_local_key( + row["localkey"].split("/")[-1], row["globalkey"] + ) local_key = process_local_key(row["localkey"].split("/")[0], inter_key) else: local_key = process_local_key(row["localkey"], row["globalkey"]) @@ -214,9 +290,13 @@ def read_harmony_tsv(beat_tsv_path, part): # key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) # local_key = key_step + INT_TO_ALT[key_alter] part.add( - spt.Cadence(text=row["cadence"], - local_key=local_key, - ), start=row["onset_div"], end=row["onset_div"]+row["duration_div"]) + spt.Cadence( + text=row["cadence"], + local_key=local_key, + ), + start=row["onset_div"], + end=row["onset_div"] + row["duration_div"], + ) # Check if phrase information is available. if np.all(data["phraseend"].isna()): @@ -230,11 +310,15 @@ def read_harmony_tsv(beat_tsv_path, part): part.add(spt.Phrase(), start=start[1]["onset_div"], end=end[1]["onset_div"]) else: # TODO: account for unfoldings and repeats. - warnings.warn("Number of phrase starts and ends do not match, skipping parsing phrases") + warnings.warn( + "Number of phrase starts and ends do not match, skipping parsing phrases" + ) return -def load_dcml(note_tsv_path, measure_tsv_path=None, harmony_tsv_path=None, metadata=None): +def load_dcml( + note_tsv_path, measure_tsv_path=None, harmony_tsv_path=None, metadata=None +): """ Load a score from tsv files containing the notes, measures and harmony annotations. @@ -270,4 +354,3 @@ def load_dcml(note_tsv_path, measure_tsv_path=None, harmony_tsv_path=None, metad read_harmony_tsv(harmony_tsv_path, part) score = spt.Score([part]) return score - diff --git a/partitura/musicanalysis/key_identification.py b/partitura/musicanalysis/key_identification.py index 62bb1ea7..e66e4624 100644 --- a/partitura/musicanalysis/key_identification.py +++ b/partitura/musicanalysis/key_identification.py @@ -12,8 +12,14 @@ from scipy.linalg import circulant from partitura.utils.music import ensure_notearray from partitura.utils.globals import ( - KEYS, key_prof_maj_kk, key_prof_min_kk, key_prof_maj_cbms, key_prof_min_cbms, - key_prof_maj_kp, key_prof_min_kp, VALID_KEY_PROFILES + KEYS, + key_prof_maj_kk, + key_prof_min_kk, + key_prof_maj_cbms, + key_prof_min_cbms, + key_prof_maj_kp, + key_prof_min_kp, + VALID_KEY_PROFILES, ) __all__ = ["estimate_key"] @@ -24,7 +30,6 @@ # the circle of fifths. - def build_key_profile_matrix(key_prof_maj, key_prof_min): """ Generate Matrix of key profiles diff --git a/partitura/musicanalysis/meter.py b/partitura/musicanalysis/meter.py index 1c337d5c..7dfd2689 100644 --- a/partitura/musicanalysis/meter.py +++ b/partitura/musicanalysis/meter.py @@ -22,13 +22,21 @@ from partitura.utils import get_time_units_from_note_array, ensure_notearray, add_field from partitura.utils.globals import ( - CHORD_SPREAD_TIME, MIN_INTERVAL, MAX_INTERVAL, CLUSTER_WIDTH, N_CLUSTERS, - INIT_DURATION, TIMEOUT, TOLERANCE_PRE, TOLERANCE_POST, TOLERANCE_INNER, CORRECTION_FACTOR, MAX_AGENTS + CHORD_SPREAD_TIME, + MIN_INTERVAL, + MAX_INTERVAL, + CLUSTER_WIDTH, + N_CLUSTERS, + INIT_DURATION, + TIMEOUT, + TOLERANCE_PRE, + TOLERANCE_POST, + TOLERANCE_INNER, + CORRECTION_FACTOR, + MAX_AGENTS, ) - - class MultipleAgents: """ Class to compute inter onset interval clusters diff --git a/partitura/score.py b/partitura/score.py index 4808442e..302cd7d4 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -16,7 +16,11 @@ import re # import copy -from partitura.utils.globals import MUSICAL_BEATS, INTERVALCLASSES, INTERVAL_TO_SEMITONES +from partitura.utils.globals import ( + MUSICAL_BEATS, + INTERVALCLASSES, + INTERVAL_TO_SEMITONES, +) import warnings, sys import numpy as np import re @@ -48,7 +52,12 @@ ) from partitura.utils.generic import interp1d from partitura.utils.music import transpose_note, step2pc -from partitura.utils.globals import (INT_TO_ALT, ALT_TO_INT, ACCEPTED_ROMANS, LOCAL_KEY_TRASPOSITIONS_DCML) +from partitura.utils.globals import ( + INT_TO_ALT, + ALT_TO_INT, + ACCEPTED_ROMANS, + LOCAL_KEY_TRASPOSITIONS_DCML, +) class Part(object): @@ -2810,20 +2819,69 @@ class RomanNumeral(Harmony): See parameters """ - def __init__(self, text, inversion=None, local_key=None, primary_degree=None, secondary_degree=None, quality=None): + def __init__( + self, + text, + inversion=None, + local_key=None, + primary_degree=None, + secondary_degree=None, + quality=None, + ): super().__init__(text) self.text = text - self.accepted_qualities = ('7', 'aug', 'aug6', 'aug7', 'dim', 'dim7', 'hdim7', 'maj', 'maj7', 'min', 'min7') + self.accepted_qualities = ( + "7", + "aug", + "aug6", + "aug7", + "dim", + "dim7", + "hdim7", + "maj", + "maj7", + "min", + "min7", + ) # The key of an inversion is text from RN string, and the value is a tuple (has_seven,inversion) - self.accepted_inversions = {"2": (3, True), "43": (2, True), "64": (2, False), "6": (1, False), "65": (1, True), "7": (0, True)} + self.accepted_inversions = { + "2": (3, True), + "43": (2, True), + "64": (2, False), + "6": (1, False), + "65": (1, True), + "7": (0, True), + } self.has_seven = "7" in text - self.inversion = inversion if inversion is not None else self._process_inversion() - self.local_key = local_key if local_key is not None else self._process_local_key() - self.primary_degree = primary_degree if primary_degree is not None else self._process_primary_degree() - 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() + self.inversion = ( + inversion if inversion is not None else self._process_inversion() + ) + self.local_key = ( + local_key if local_key is not None else self._process_local_key() + ) + self.primary_degree = ( + primary_degree + if primary_degree is not None + else self._process_primary_degree() + ) + 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 + ): self.root = self.find_root_note() self.bass_note = self.find_bass_note() @@ -2831,9 +2889,11 @@ def _process_inversion(self): """Find the inversion of the roman numeral from the text""" # The inversion should be right after the roman numeral. # If there is no inversion, return 0 - numeric_indications_in_text = re.findall(r'\d+', self.text) + numeric_indications_in_text = re.findall(r"\d+", self.text) if len(numeric_indications_in_text) > 0: - inversion, has_seven = self.accepted_inversions.get(numeric_indications_in_text[0], (0, False)) + inversion, has_seven = self.accepted_inversions.get( + numeric_indications_in_text[0], (0, False) + ) self.has_seven = has_seven return inversion return 0 @@ -2857,14 +2917,16 @@ def _process_primary_degree(self): # Remove any key information roman_text = self.text.split(":")[-1] roman_text = roman_text.split(".")[-1] if "." in roman_text else roman_text - primary_degree = re.search(r'[a-zA-Z+]+', roman_text) + primary_degree = re.search(r"[a-zA-Z+]+", roman_text) if primary_degree: prim_d = primary_degree.group(0) # if the primary degree is not in accepted values, return the closest one if prim_d in ACCEPTED_ROMANS: return prim_d else: - matches = difflib.get_close_matches(prim_d, ACCEPTED_ROMANS, n=1, cutoff=0.5) + matches = difflib.get_close_matches( + prim_d, ACCEPTED_ROMANS, n=1, cutoff=0.5 + ) if matches: return matches[0] return None @@ -2880,7 +2942,7 @@ def _process_secondary_degree(self): roman_text = self.text.split(":")[-1] split_pr_sec = roman_text.split("/") if len(split_pr_sec) > 1: - secondary_degree = re.search(r'[a-zA-Z+]+', split_pr_sec[-1]) + secondary_degree = re.search(r"[a-zA-Z+]+", split_pr_sec[-1]) return secondary_degree.group(0) elif self.primary_degree is not None and self.local_key is not None: secondary_degree = "I" if self.local_key.isupper() else "i" @@ -2896,10 +2958,18 @@ def _process_quality(self): """ # The quality should be M, m, +, o, or None. aug_cond = "aug" in self.text.lower() or "+" in self.text.lower() - minor_cond = self.primary_degree.islower() if self.primary_degree is not None else False - major_cond = self.primary_degree.isupper() if self.primary_degree is not None else False + minor_cond = ( + self.primary_degree.islower() if self.primary_degree is not None else False + ) + major_cond = ( + self.primary_degree.isupper() if self.primary_degree is not None else False + ) dim_cond = "dim" in self.text or "o" in self.text - aug6_cond = "ger" in self.text.lower() or "it" in self.text.lower() or "fr" in self.text.lower() + aug6_cond = ( + "ger" in self.text.lower() + or "it" in self.text.lower() + or "fr" in self.text.lower() + ) hdim_cond = "0" in self.text or "%" in self.text or "ø" in self.text if aug6_cond: quality = "aug6" @@ -2924,7 +2994,9 @@ def _process_quality(self): elif major_cond: quality = "maj" else: - warnings.warn(f"Quality for {self.text} was not found, could be a special case. Setting to None.") + warnings.warn( + f"Quality for {self.text} was not found, could be a special case. Setting to None." + ) quality = None return quality @@ -2939,10 +3011,18 @@ def find_root_note(self): """ # Corrected step after degree2 key_step = re.search(r"[a-gA-G]", self.local_key).group(0) - key_alter = re.search(r"[#b]", self.local_key).group(0) if re.search(r"[#b]", self.local_key) else "" + key_alter = ( + re.search(r"[#b]", self.local_key).group(0) + if re.search(r"[#b]", self.local_key) + else "" + ) key_alter = ALT_TO_INT[key_alter] try: - interval = Roman2Interval_Min[self.secondary_degree] if self.local_key.islower() else Roman2Interval_Maj[self.secondary_degree] + interval = ( + Roman2Interval_Min[self.secondary_degree] + if self.local_key.islower() + else Roman2Interval_Maj[self.secondary_degree] + ) step, alter = transpose_note(key_step, key_alter, interval) except KeyError: loc_k = self.secondary_degree @@ -2951,7 +3031,11 @@ def find_root_note(self): # Corrected step after degree1 # TODO add support for diminished and augmented chords try: - interval = Roman2Interval_Min[self.primary_degree] if self.secondary_degree.islower() else Roman2Interval_Maj[self.primary_degree] + interval = ( + Roman2Interval_Min[self.primary_degree] + if self.secondary_degree.islower() + else Roman2Interval_Maj[self.primary_degree] + ) step, alter = transpose_note(step, alter, interval) root = step + INT_TO_ALT[alter] except KeyError: @@ -2987,6 +3071,7 @@ def __str__(self): class Cadence(TimedObject): """A cadence element in the score usually for Cadences.""" + def __init__(self, text, local_key=None): super().__init__() self.text = text @@ -2998,7 +3083,7 @@ def _filter_cadence_type(self): # capitalize text self.text = self.text.upper() # Filter alphabet characters only. - self.text = re.findall(r'[A-Z]+', self.text)[0] + self.text = re.findall(r"[A-Z]+", self.text)[0] self.text = "IAC" if "IAC" in self.text else self.text if self.text not in ["PAC", "IAC", "HC", "DC", "EC", "PC"]: warnings.warn(f"Cadence type {self.text} not found. Setting to None") @@ -3013,7 +3098,7 @@ def __init__(self): super().__init__() def __str__(self): - return f'{super().__str__()}' + return f"{super().__str__()}" class ChordSymbol(Harmony): @@ -3092,7 +3177,11 @@ def change_quality(self, num): if num == 0: pass else: - change_dir = change_direction_c if self.number in [1, 4, 5, 8] else change_direction_d + change_dir = ( + change_direction_c + if self.number in [1, 4, 5, 8] + else change_direction_d + ) cur_index = change_dir.index(prev_quality) new_index = cur_index + num if new_index >= len(change_dir) or new_index < 0: @@ -5786,20 +5875,33 @@ def process_local_key(loc_k_text, glob_k_text, return_step_alter=False): local_key_is_minor = local_key.islower() local_key = local_key.lower() global_key_is_minor = glob_k_text.islower() - if local_key_is_minor == global_key_is_minor and local_key == "i" and local_key_sharps - local_key_flats == 0 and (not return_step_alter): + if ( + local_key_is_minor == global_key_is_minor + and local_key == "i" + and local_key_sharps - local_key_flats == 0 + and (not return_step_alter) + ): return glob_k_text g_key = "minor" if glob_k_text.islower() else "major" num, qual = LOCAL_KEY_TRASPOSITIONS_DCML[g_key][local_key] transposition_interval = Interval(num, qual) - transposition_interval = transposition_interval.change_quality(local_key_sharps - local_key_flats) + transposition_interval = transposition_interval.change_quality( + local_key_sharps - local_key_flats + ) key_step = re.search(r"[a-gA-G]", glob_k_text).group(0) - key_alter = re.search(r"[#b]", glob_k_text).group(0) if re.search(r"[#b]", glob_k_text) else "" + key_alter = ( + re.search(r"[#b]", glob_k_text).group(0) + if re.search(r"[#b]", glob_k_text) + else "" + ) key_alter = key_alter.replace("b", "-") key_alter = ALT_TO_INT[key_alter] key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) if return_step_alter: return key_step, key_alter - local_key = (key_step.lower() if local_key_is_minor else key_step.upper()) + INT_TO_ALT[key_alter] + local_key = ( + key_step.lower() if local_key_is_minor else key_step.upper() + ) + INT_TO_ALT[key_alter] return local_key @@ -5859,22 +5961,33 @@ def process_local_key(loc_k, glob_k, return_step_alter=False): local_key_is_minor = local_key.islower() local_key = local_key.lower() global_key_is_minor = glob_k.islower() - if local_key_is_minor == global_key_is_minor and local_key == "i" and local_key_sharps - local_key_flats == 0 and (not return_step_alter): + if ( + local_key_is_minor == global_key_is_minor + and local_key == "i" + and local_key_sharps - local_key_flats == 0 + and (not return_step_alter) + ): return glob_k g_key = "minor" if glob_k.islower() else "major" # keep only letters in local_key local_key = re.sub(r"[^a-zA-Z]", "", local_key) num, qual = LOCAL_KEY_TRASPOSITIONS_DCML[g_key][local_key] transposition_interval = Interval(num, qual) - transposition_interval = transposition_interval.change_quality(local_key_sharps - local_key_flats) + transposition_interval = transposition_interval.change_quality( + local_key_sharps - local_key_flats + ) key_step = re.search(r"[a-gA-G]", glob_k).group(0) - key_alter = re.search(r"[#b]", glob_k).group(0) if re.search(r"[#b]", glob_k) else "" + key_alter = ( + re.search(r"[#b]", glob_k).group(0) if re.search(r"[#b]", glob_k) else "" + ) key_alter = key_alter.replace("b", "-") key_alter = ALT_TO_INT[key_alter] key_step, key_alter = transpose_note(key_step, key_alter, transposition_interval) if return_step_alter: return key_step, key_alter - local_key = (key_step.lower() if local_key_is_minor else key_step.upper()) + INT_TO_ALT[key_alter] + local_key = ( + key_step.lower() if local_key_is_minor else key_step.upper() + ) + INT_TO_ALT[key_alter] return local_key diff --git a/partitura/utils/globals.py b/partitura/utils/globals.py index 65935698..50a2653e 100644 --- a/partitura/utils/globals.py +++ b/partitura/utils/globals.py @@ -315,7 +315,6 @@ ] - TWO_PI = 2 * np.pi SAMPLE_RATE = 44100 DTYPE = float @@ -441,17 +440,35 @@ CHORD_SPREAD_TIME = 1 / 12 # for onset aggregation - -Voc_majmin = [ - "Cad64", "V", "viio", "V7", "N", "It", "Fr7", "Ger7", "v" -] +Voc_majmin = ["Cad64", "V", "viio", "V7", "N", "It", "Fr7", "Ger7", "v"] Voc_maj_only = [ - "I", "ii", "iii", "IV", "vi", "I7", "ii7", "iii7", "IV7", "vi7", "viio7", "V+" + "I", + "ii", + "iii", + "IV", + "vi", + "I7", + "ii7", + "iii7", + "IV7", + "vi7", + "viio7", + "V+", ] Voc_min_only = [ - "i", "iio", "III+", "iv", "VI", "i7", "iio7", "III+7", "iv7", "VI7", "viio7" + "i", + "iio", + "III+", + "iv", + "VI", + "i7", + "iio7", + "III+7", + "iv7", + "VI7", + "viio7", ] Voc_maj = Voc_majmin + Voc_maj_only @@ -460,8 +477,20 @@ ACCEPTED_ROMANS = list(set(Voc_maj + Voc_min)) Voc_T_degree = [ - "I", "II", "III", "IV", "V", "VI", "VII", - "i", "ii", "iii", "iv", "v", "vi", "vii", + "I", + "II", + "III", + "IV", + "V", + "VI", + "VII", + "i", + "ii", + "iii", + "iv", + "v", + "vi", + "vii", ] @@ -513,4 +542,4 @@ "vi": (6, "M"), "vii": (7, "M"), }, -} \ No newline at end of file +} diff --git a/partitura/utils/music.py b/partitura/utils/music.py index a247a41a..60654175 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -228,7 +228,7 @@ def transpose_note_old(step, alter, interval): else: diff_sm = prev_pc - tmp_pc if prev_pc >= tmp_pc else prev_pc + 12 - tmp_pc new_alter = ( - INTERVAL_TO_SEMITONES[interval.quality + str(interval.number)] - diff_sm + INTERVAL_TO_SEMITONES[interval.quality + str(interval.number)] - diff_sm ) return new_step, new_alter @@ -257,15 +257,21 @@ def transpose_note(step, alter, interval): prev_step = step.capitalize() assert interval.direction == "up", "Only interval direction 'up' is supported." assert -3 < alter < 3, f"Input Alteration {alter} is not in the range -2 to 2." - assert interval.number < 8, f"Input Interval {interval.number} is not in the range 1 to 7." - assert prev_step in BASE_PC.keys(), f"Input Step {prev_step} is must be one of: {BASE_PC.keys()}." + assert ( + interval.number < 8 + ), f"Input Interval {interval.number} is not in the range 1 to 7." + assert ( + prev_step in BASE_PC.keys() + ), f"Input Step {prev_step} is must be one of: {BASE_PC.keys()}." new_step = STEPS[(STEPS[prev_step] + interval.number - 1) % 7] prev_alter = alter if alter is not None else 0 pc_prev = step2pc(prev_step, prev_alter) pc_new = step2pc(new_step, prev_alter) new_alter = interval.semitones - (pc_new - pc_prev) % 12 + prev_alter # add test to check if the new alteration is correct (i.e. accept maximum of 2 flats or sharps) - assert -3 < new_alter < 3, f"New alteration {new_alter} is not in the range -2 to 2." + assert ( + -3 < new_alter < 3 + ), f"New alteration {new_alter} is not in the range -2 to 2." return new_step, new_alter diff --git a/partitura/utils/normalize.py b/partitura/utils/normalize.py index 72fb9d59..438b2219 100644 --- a/partitura/utils/normalize.py +++ b/partitura/utils/normalize.py @@ -7,7 +7,6 @@ from partitura.utils.globals import EPSILON - def range_normalize( array, min_value=None, diff --git a/partitura/utils/synth.py b/partitura/utils/synth.py index bcc8d3ce..7585f09d 100644 --- a/partitura/utils/synth.py +++ b/partitura/utils/synth.py @@ -23,7 +23,14 @@ midi_pitch_to_frequency, performance_notearray_from_score_notearray, ) -from partitura.utils.globals import DTYPE, SAMPLE_RATE, TWO_PI, FIVE_LIMIT_INTERVAL_RATIOS, A4, NATURAL_INTERVAL_RATIOS +from partitura.utils.globals import ( + DTYPE, + SAMPLE_RATE, + TWO_PI, + FIVE_LIMIT_INTERVAL_RATIOS, + A4, + NATURAL_INTERVAL_RATIOS, +) if TYPE_CHECKING: # Import typing info for typing annotations. From 63d91e3e564bc676a0f30dd23d784e83a5632be9 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jul 2024 16:53:48 +0200 Subject: [PATCH 186/197] Adding documentation to parse method of kern export. --- partitura/io/exportkern.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/partitura/io/exportkern.py b/partitura/io/exportkern.py index a269e9f5..656e6b22 100644 --- a/partitura/io/exportkern.py +++ b/partitura/io/exportkern.py @@ -104,6 +104,18 @@ def __init__(self, part): self.prev_note_row_idx = None def parse(self): + """ + Parse the partitura score to Kern format. + + This method iterates over all elements in the partitura score and converts them to Kern format. + To better process the elements, the method first groups them by start time and then processes them in order. + It first finds notes and then processes structural elements (clefs, time signatures, etc.) and finally measures. + + Returns + ------- + self.out_data: np.ndarray + Kern file as a numpy array of strings. + """ row_idx = 2 for start_time in self.unique_times: end_time = start_time + 1 From 78af2cc97e81f76be6bba67d74ff664efc2304d8 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jul 2024 16:56:27 +0200 Subject: [PATCH 187/197] Updated documentation string for step2pc. --- 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 60654175..6300613e 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -3281,7 +3281,7 @@ def tokenize( def step2pc(step, alter): """ - Convert a step to a pitch class. + Convert a tonal pitch class (i.e. step + alter) to a pitch class (i.e. integer in [0, 11]). Parameters ---------- From f4f274b4cacea2a4c9c7588740a160ac0139b082 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jul 2024 16:58:49 +0200 Subject: [PATCH 188/197] updated documentation on transpose_note function. --- partitura/utils/music.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 6300613e..f9678228 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -235,7 +235,9 @@ def transpose_note_old(step, alter, interval): def transpose_note(step, alter, interval): """ - Transpose a note by a given interval without changing the octave or creating a Note Object. + Transpose a note by a given interval without considering the octave. + + This function does not create a new Note object, but returns the new step and alteration of the note. Parameters From 2095af5547424b6a983609b8d50d664f771bdd25 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jul 2024 17:04:41 +0200 Subject: [PATCH 189/197] updated tests with an assertion --- tests/test_dcml_import.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_dcml_import.py b/tests/test_dcml_import.py index fe765a11..b3f358bd 100644 --- a/tests/test_dcml_import.py +++ b/tests/test_dcml_import.py @@ -2,6 +2,7 @@ from partitura import load_dcml from tests import TSV_PATH import os +import pandas as pd class ImportDCMLAnnotations(unittest.TestCase): @@ -10,7 +11,9 @@ def test_tsv_import_from_dcml(self): measure_path = os.path.join(TSV_PATH, "test_measures.tsv") harmony_path = os.path.join(TSV_PATH, "test_harmonies.tsv") score = load_dcml(note_path, measure_path, harmony_path) + note_lines = pd.read_csv(note_path, sep="\t", header=None) self.assertEqual(len(score.parts), 1) + self.assertEqual(len(score[0].notes), len(note_lines)-1, "Number of notes do not match") if __name__ == '__main__': From 612744c0b89febb70152b8332f3cce021f958cdc Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jul 2024 17:06:39 +0200 Subject: [PATCH 190/197] updated module header. --- partitura/io/exportaudio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/exportaudio.py b/partitura/io/exportaudio.py index 6b8cb335..cdf1a11c 100644 --- a/partitura/io/exportaudio.py +++ b/partitura/io/exportaudio.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- """ -This module contains methods to synthesize Partitura object to wav. +This module contains methods to synthesize a Partitura ScoreLike object to wav. """ from typing import Union, Optional, Callable, Dict, Any import numpy as np From ea00d8b265b034f450684ac0c40ca7144ffd540d Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jul 2024 17:09:08 +0200 Subject: [PATCH 191/197] updated documentation of kern_export method for pitch. --- partitura/io/exportkern.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/partitura/io/exportkern.py b/partitura/io/exportkern.py index 656e6b22..5c6500b2 100644 --- a/partitura/io/exportkern.py +++ b/partitura/io/exportkern.py @@ -219,8 +219,13 @@ def duration_to_kern(self, element: spt.GenericNote) -> str: return self.sym_dur_to_kern(element.symbolic_duration) def pitch_to_kern(self, element: spt.GenericNote) -> str: - # To encode pitch correctly in kern we need to take into account the octave - # duplication of the step in kern can either move the note up or down an octave + """ + Transform a Partitura Note object to a kern note string (only pitch). + + To encode pitch correctly in kern we need to take into account that the octave + duplication of the step in kern can either move the note up or down an octave + + """ if isinstance(element, spt.Rest): return "r" step, alter, octave = element.step, element.alter, element.octave From 8b0fe3a1a73960db2906c64573887a701613187d Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jul 2024 17:10:07 +0200 Subject: [PATCH 192/197] Curation of unused commented code. --- partitura/io/importdcml.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index 78692131..fc18d4dd 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -13,8 +13,6 @@ def read_note_tsv(note_tsv_path, metadata=None): - # data = np.genfromtxt(note_tsv_path, delimiter="\t", dtype=None, names=True, invalid_raise=False) - # unique_durations = np.unique(data["duration"]) data = pd.read_csv(note_tsv_path, sep="\t") # Hack for empty values in quarterbeats, to investigate. # (It happens with voltas when the second volta has a different number of measures) From 07d9afca31b69f42b1da53f09b347a7ac9b0ac27 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jul 2024 17:11:22 +0200 Subject: [PATCH 193/197] removed unused import. --- partitura/io/importdcml.py | 1 - 1 file changed, 1 deletion(-) diff --git a/partitura/io/importdcml.py b/partitura/io/importdcml.py index fc18d4dd..5feee486 100644 --- a/partitura/io/importdcml.py +++ b/partitura/io/importdcml.py @@ -1,5 +1,4 @@ import warnings -import re import numpy as np from math import ceil import partitura.score as spt From c28a79908684b4821f1c58ee2eceb914e63dfb37 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jul 2024 17:12:04 +0200 Subject: [PATCH 194/197] removed duplicated import. --- partitura/score.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index 302cd7d4..e882151a 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -13,9 +13,6 @@ from collections import defaultdict from collections.abc import Iterable from numbers import Number -import re - -# import copy from partitura.utils.globals import ( MUSICAL_BEATS, INTERVALCLASSES, From ac167672cbc35d0f7091ae154167799d9c98bd30 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jul 2024 17:32:56 +0200 Subject: [PATCH 195/197] update on the partitura version number. --- CHANGES.md | 25 +++++++++++++++++++++++++ docs/source/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d6fe20eb..451d6134 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,31 @@ Release Notes ============= +Version 1.5.0 (Released on 2024-07-17) +-------------------------------------- + +## New Features + +- Dcml annotation parser +- New kern import for faster and more robust +- Barebones Kern export +- MEI export +- Mei export Updates +- Estimate symbolic durations +- New harmony classes and checks for Roman numerals, Chord Symbols, Cadences and Phrases in +- Intervals as partitura classes +- transposition of parts +- Export wav with fluidsynth + +## Other Changes + +- improved documentation +- improved typing +- New tests +- optional dependency of pandas + + + Version 1.4.1 (Released on 2023-10-25) -------------------------------------- diff --git a/docs/source/conf.py b/docs/source/conf.py index 0b4820cc..9b1bd0b3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -29,9 +29,9 @@ # built documents. # # The short X.Y version. -version = "1.4.1" # pkg_resources.get_distribution("partitura").version +version = "1.5.0" # pkg_resources.get_distribution("partitura").version # The full version, including alpha/beta/rc tags. -release = "1.4.1" +release = "1.5.0" # # The full version, including alpha/beta/rc tags # release = pkg_resources.get_distribution("partitura").version diff --git a/setup.py b/setup.py index 4f9432f0..11f9a34f 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.4.1" +VERSION = "1.5.0" # What packages are required for this module to be executed? REQUIRED = ["numpy", "scipy", "lxml", "lark-parser", "xmlschema", "mido"] From ded232fe62612a5230e22c5ea030529a22254041 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jul 2024 18:07:30 +0200 Subject: [PATCH 196/197] minor typo on estimate symbolic durations globals. --- partitura/utils/globals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/utils/globals.py b/partitura/utils/globals.py index 50a2653e..3739e0d4 100644 --- a/partitura/utils/globals.py +++ b/partitura/utils/globals.py @@ -285,7 +285,7 @@ SYM_COMPOSITE_DURS = [ ({"type": "quarter", "dots": 0}, {"type": "32nd", "dots": 0}), - ({"type": "quarter", "dots": 0}, {"type": "16nd", "dots": 0}), + ({"type": "quarter", "dots": 0}, {"type": "16th", "dots": 0}), ({"type": "half", "dots": 0}, {"type": "32nd", "dots": 0}), ({"type": "half", "dots": 0}, {"type": "16th", "dots": 0}), ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0}), From 4a102fae6f2c583f36a8dcbf579d4f1a05df2955 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Wed, 17 Jul 2024 18:20:00 +0200 Subject: [PATCH 197/197] minor typing edits. --- tests/test_note_array.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_note_array.py b/tests/test_note_array.py index 721f3e60..bb439be9 100644 --- a/tests/test_note_array.py +++ b/tests/test_note_array.py @@ -170,9 +170,7 @@ def test_notearray_ts_beats(self): def test_ensure_na_different_divs(self): # check if divs are correctly rescaled when producing a note array from # parts with different divs values - # parts = list(score.iter_parts(load_kern(KERN_TESTFILES[7]))) parts = load_kern(KERN_TESTFILES[7]).parts - # note_arrays = [p.note_array(include_divs_per_quarter= True) for p in parts] merged_note_array = ensure_notearray(parts) for note in merged_note_array[-4:]: self.assertTrue(note["onset_div"] == 2208)