diff --git a/docs/source/Tutorial/notebook.ipynb b/docs/source/Tutorial/notebook.ipynb index 31d98713..b90d44e4 100644 --- a/docs/source/Tutorial/notebook.ipynb +++ b/docs/source/Tutorial/notebook.ipynb @@ -47,11 +47,12 @@ }, "id": "PeabdL1k7YC4", "outputId": "fcb7d1be-27a1-4c79-c5d3-8cbfa54cae44", - "scrolled": true, "pycharm": { "is_executing": true - } + }, + "scrolled": true }, + "outputs": [], "source": [ "# Install partitura\n", "! pip install partitura\n", @@ -64,21 +65,20 @@ "import sys, os\n", "sys.path.insert(0, os.path.join(os.getcwd(), \"partitura_tutorial\", \"content\"))\n", "sys.path.insert(0,'/content/partitura_tutorial/content')\n" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 2, "id": "impressed-principle", "metadata": {}, + "outputs": [], "source": [ "import glob\n", "import partitura as pt\n", "import numpy as np\n", "import matplotlib.pyplot as plt" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -103,6 +103,7 @@ "execution_count": 3, "id": "photographic-profession", "metadata": {}, + "outputs": [], "source": [ "# setup the dataset\n", "from load_data import init_dataset\n", @@ -110,8 +111,7 @@ "MUSICXML_DIR = os.path.join(DATASET_DIR, 'musicxml')\n", "MIDI_DIR = os.path.join(DATASET_DIR, 'midi')\n", "MATCH_DIR = os.path.join(DATASET_DIR, 'match')" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -220,12 +220,12 @@ "execution_count": 4, "id": "c9179e78", "metadata": {}, + "outputs": [], "source": [ "path_to_musicxml = pt.EXAMPLE_MUSICXML\n", "part = pt.load_musicxml(path_to_musicxml)[0]\n", "print(part.pretty())" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -250,20 +250,20 @@ "execution_count": 5, "id": "423aac6a", "metadata": {}, + "outputs": [], "source": [ "part.notes" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 6, "id": "0a929369", "metadata": {}, + "outputs": [], "source": [ "dir(part.notes[0])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -278,23 +278,23 @@ "execution_count": 7, "id": "2a8293c9", "metadata": {}, + "outputs": [], "source": [ "a_new_note = pt.score.Note(id='n04', step='A', octave=4, voice=1)\n", "part.add(a_new_note, start=3, end=15)\n", "# print(part.pretty())" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 8, "id": "eba2fa93", "metadata": {}, + "outputs": [], "source": [ "part.remove(a_new_note)\n", "# print(part.pretty())" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -313,10 +313,10 @@ "execution_count": 9, "id": "e95eb0f7", "metadata": {}, + "outputs": [], "source": [ "part.beat_map(part.notes[0].end.t)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -331,10 +331,10 @@ "execution_count": 10, "id": "05346a03", "metadata": {}, + "outputs": [], "source": [ "part.time_signature_map(part.notes[0].end.t)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -356,22 +356,22 @@ "execution_count": 11, "id": "74943a93", "metadata": {}, + "outputs": [], "source": [ "for measure in part.iter_all(pt.score.Measure):\n", " print(measure)" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 12, "id": "6cbfd044", "metadata": {}, + "outputs": [], "source": [ "for note in part.iter_all(pt.score.GenericNote, include_subclasses=True, start=0, end=24):\n", " print(note)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -388,6 +388,7 @@ "execution_count": 13, "id": "fe430921", "metadata": {}, + "outputs": [], "source": [ "# figure out the last measure position, time signature and beat length in divs\n", "measures = [m for m in part.iter_all(pt.score.Measure)]\n", @@ -405,18 +406,17 @@ "# add a note\n", "a_new_note = pt.score.Note(id='n04', step='A', octave=4, voice=1)\n", "part.add(a_new_note, start=append_measure_start, end=append_measure_start+one_beat_in_divs_at_the_end)" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 14, "id": "f9d738a5", "metadata": {}, + "outputs": [], "source": [ "# print(part.pretty())" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -436,21 +436,21 @@ "execution_count": 15, "id": "5d82a340", "metadata": {}, + "outputs": [], "source": [ "path_to_midifile = pt.EXAMPLE_MIDI\n", "performedpart = pt.load_performance_midi(path_to_midifile)[0]" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 16, "id": "4e3090d9", "metadata": {}, + "outputs": [], "source": [ "performedpart.notes" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -465,6 +465,7 @@ "execution_count": 17, "id": "d6eb12f2", "metadata": {}, + "outputs": [], "source": [ "import numpy as np \n", "\n", @@ -491,14 +492,14 @@ " part.add(pt.score.Note(id='n{}'.format(idx), step=step, \n", " octave=int(octave), alter=alter, voice=voice, staff=str((voice-1)%2+1)), \n", " start=start, end=end)" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 18, "id": "572e856c", "metadata": {}, + "outputs": [], "source": [ "l = 200\n", "p = pt.score.Part('CoK', 'Cat on Keyboard', quarter_duration=8)\n", @@ -509,54 +510,53 @@ " np.random.randint(40,60, size=(1,l+1)),\n", " np.random.randint(40,60, size=(1,l+1))\n", " ))" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 19, "id": "f9f03a50", "metadata": {}, + "outputs": [], "source": [ "for k in range(l):\n", " for j in range(4):\n", " addnote(pitch[j,k], p, j+1, ons[j,k], ons[j,k]+dur[j,k+1], \"v\"+str(j)+\"n\"+str(k))" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 20, "id": "09fb6b45", "metadata": {}, + "outputs": [], "source": [ "p.add(pt.score.TimeSignature(4, 4), start=0)\n", "p.add(pt.score.Clef(1, \"G\", line = 3, octave_change=0),start=0)\n", "p.add(pt.score.Clef(2, \"G\", line = 3, octave_change=0),start=0)\n", "pt.score.add_measures(p)\n", "pt.score.tie_notes(p)" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 21, "id": "834582d5", "metadata": {}, + "outputs": [], "source": [ "# pt.save_score_midi(p, \"CatPerformance.mid\", part_voice_assign_mode=2)" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 22, "id": "006f02ed", "metadata": {}, + "outputs": [], "source": [ "# pt.save_musicxml(p, \"CatScore.xml\")" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -602,6 +602,7 @@ "execution_count": 23, "id": "first-basin", "metadata": {}, + "outputs": [], "source": [ "# Note array from a score\n", "\n", @@ -613,8 +614,7 @@ "\n", "# Get note array.\n", "score_note_array = score_part.note_array()" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -629,11 +629,11 @@ "execution_count": 24, "id": "alternate-coordinate", "metadata": {}, + "outputs": [], "source": [ "# Lets see the first notes in this note array\n", "print(score_note_array[:10])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -650,10 +650,10 @@ "execution_count": 25, "id": "subtle-millennium", "metadata": {}, + "outputs": [], "source": [ "print(score_note_array.dtype.names)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -686,6 +686,7 @@ "execution_count": 26, "id": "passing-lending", "metadata": {}, + "outputs": [], "source": [ "# Note array from a performance\n", "\n", @@ -697,8 +698,7 @@ "\n", "# Get note array!\n", "performance_note_array = performance_part.note_array()" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -713,10 +713,10 @@ "execution_count": 27, "id": "pointed-stupid", "metadata": {}, + "outputs": [], "source": [ "print(performance_note_array.dtype.names)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -737,10 +737,10 @@ "execution_count": 28, "id": "subject-reducing", "metadata": {}, + "outputs": [], "source": [ "print(performance_note_array[:5])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -755,6 +755,7 @@ "execution_count": 29, "id": "spread-performer", "metadata": {}, + "outputs": [], "source": [ "note_array = np.array(\n", " [(60, 0, 2, 40),\n", @@ -771,8 +772,7 @@ "\n", "# Note array to `PerformedPart`\n", "performed_part = pt.performance.PerformedPart.from_note_array(note_array)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -787,11 +787,11 @@ "execution_count": 30, "id": "changed-check", "metadata": {}, + "outputs": [], "source": [ "# export as MIDI file\n", "pt.save_performance_midi(performed_part, \"example.mid\")" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -808,6 +808,7 @@ "execution_count": 31, "id": "figured-coordinator", "metadata": {}, + "outputs": [], "source": [ "extended_score_note_array = pt.utils.music.ensure_notearray(\n", " score_part,\n", @@ -817,18 +818,17 @@ " # include_metrical_position=True, # adds 3 fields: is_downbeat, rel_onset_div, tot_measure_div\n", " include_grace_notes=True # adds 2 fields: is_grace, grace_type\n", ")" - ], - "outputs": [] + ] }, { "cell_type": "code", "execution_count": 32, "id": "vietnamese-pathology", "metadata": {}, + "outputs": [], "source": [ "extended_score_note_array.dtype.names" - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -837,6 +837,7 @@ "metadata": { "scrolled": true }, + "outputs": [], "source": [ "print(extended_score_note_array[['id', \n", " 'step', \n", @@ -845,8 +846,7 @@ " 'ks_fifths', \n", " 'ks_mode', #'is_downbeat'\n", " ]][:10])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -879,6 +879,7 @@ "metadata": { "scrolled": true }, + "outputs": [], "source": [ "# Path to the MusicXML file\n", "score_fn = os.path.join(MUSICXML_DIR, 'Chopin_op10_no3.musicxml')\n", @@ -914,8 +915,7 @@ "\n", "accented_note_idxs = np.where(accent_note_array['accent'])\n", "print(accent_note_array[accented_note_idxs][:5])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -944,6 +944,7 @@ "execution_count": 35, "id": "essential-academy", "metadata": {}, + "outputs": [], "source": [ "# TODO: change the example\n", "# Path to the MusicXML file\n", @@ -953,8 +954,7 @@ "score_part = pt.load_musicxml(score_fn)\n", "# compute piano roll\n", "pianoroll = pt.utils.compute_pianoroll(score_part)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -969,6 +969,7 @@ "execution_count": 36, "id": "massive-monaco", "metadata": {}, + "outputs": [], "source": [ "piano_range = True\n", "time_unit = 'beat'\n", @@ -979,8 +980,7 @@ " time_div=time_div, # Number of cells per time unit\n", " piano_range=piano_range # Use range of the piano (88 keys)\n", ")" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1007,14 +1007,14 @@ "execution_count": 37, "id": "mature-dylan", "metadata": {}, + "outputs": [], "source": [ "fig, ax = plt.subplots(1, figsize=(20, 10))\n", "ax.imshow(pianoroll.toarray(), origin=\"lower\", cmap='gray', interpolation='nearest', aspect='auto')\n", "ax.set_xlabel(f'Time ({time_unit}s/{time_div})')\n", "ax.set_ylabel('Piano key' if piano_range else 'MIDI pitch')\n", "plt.show()" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1031,13 +1031,13 @@ "metadata": { "scrolled": true }, + "outputs": [], "source": [ "pianoroll, note_indices = pt.utils.compute_pianoroll(score_part, return_idxs=True)\n", "\n", "# MIDI pitch, start, end\n", "print(note_indices[:5])" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1055,6 +1055,7 @@ "execution_count": 39, "id": "parental-links", "metadata": {}, + "outputs": [], "source": [ "pianoroll = pt.utils.compute_pianoroll(score_part)\n", "\n", @@ -1064,8 +1065,7 @@ "ppart = pt.performance.PerformedPart.from_note_array(new_note_array)\n", "\n", "pt.save_performance_midi(ppart, \"newmidi.mid\")" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1112,13 +1112,13 @@ "execution_count": 40, "id": "rolled-cloud", "metadata": {}, + "outputs": [], "source": [ "# path to the match\n", "match_fn = os.path.join(MATCH_DIR, 'Chopin_op10_no3_p01.match')\n", "# loading a match file\n", "performed_part, alignment, score_part = pt.load_match(match_fn, create_part=True)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1140,6 +1140,7 @@ "execution_count": 41, "id": "latest-smell", "metadata": {}, + "outputs": [], "source": [ "# path to the match\n", "match_fn = os.path.join(MATCH_DIR, 'Chopin_op10_no3_p01.match')\n", @@ -1150,8 +1151,7 @@ "\n", "# loading a match file\n", "performed_part, alignment = pt.load_match(match_fn)" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1175,10 +1175,10 @@ "execution_count": 42, "id": "radio-interim", "metadata": {}, + "outputs": [], "source": [ "alignment[:10]" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1197,6 +1197,7 @@ "execution_count": 43, "id": "published-understanding", "metadata": {}, + "outputs": [], "source": [ "# note array of the score\n", "snote_array = score_part.note_array()\n", @@ -1209,8 +1210,7 @@ "matched_snote_array = snote_array[matched_note_idxs[:, 0]]\n", "# note array of the matched performed notes\n", "matched_pnote_array = pnote_array[matched_note_idxs[:, 1]]" - ], - "outputs": [] + ] }, { "cell_type": "markdown", @@ -1227,6 +1227,7 @@ "execution_count": 44, "id": "offshore-bridal", "metadata": {}, + "outputs": [], "source": [ "# get all match files\n", "matchfiles = glob.glob(os.path.join(MATCH_DIR, 'Chopin_op10_no3_p*.match'))\n", @@ -1248,13 +1249,12 @@ " performance, alignment = pt.load_match(matchfile)\n", " ppart = performance[0]\n", " # Get score time to performance time map\n", - " _, stime_to_ptime_map = pt.utils.music.get_time_maps_from_alignment(\n", + " _, stime_to_ptime_map = pt.musicanalysis.performance_codec.get_time_maps_from_alignment(\n", " ppart, score_part, alignment)\n", " # Compute naïve tempo curve\n", " performance_time = stime_to_ptime_map(score_time_ending)\n", " tempo_curves[i,:] = 60 * np.diff(score_time_ending) / np.diff(performance_time)" - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -1263,6 +1263,7 @@ "metadata": { "scrolled": false }, + "outputs": [], "source": [ "fig, ax = plt.subplots(1, figsize=(15, 8))\n", "color = plt.cm.rainbow(np.linspace(0, 1, len(tempo_curves)))\n", @@ -1284,8 +1285,7 @@ "plt.legend(frameon=False, bbox_to_anchor = (1.15, .9))\n", "plt.grid(axis='x')\n", "plt.show()" - ], - "outputs": [] + ] }, { "cell_type": "markdown", diff --git a/partitura/assets/score_example.mei b/partitura/assets/score_example.mei index d8267f75..fc384420 100644 --- a/partitura/assets/score_example.mei +++ b/partitura/assets/score_example.mei @@ -1,52 +1,65 @@ - - - - - - - + + + + + + + <respStmt /> </titleStmt> - <pubStmt></pubStmt> + <pubStmt xml:id="p1kxcrok"> + <date isodate="2024-10-30" type="encoding-date">2024-10-30</date> + </pubStmt> </fileDesc> - <encodingDesc xml:id="encodingdesc-2o9bqo"> - <appInfo xml:id="appinfo-j7rtco"> - <application xml:id="application-hc9py4" isodate="2021-12-09T11:27:15" version="3.8.0-dev-45c3f2c"> - <name xml:id="name-symba1">Verovio</name> - <p xml:id="p-roup2t">Transcoded from MusicXML</p> + <encodingDesc xml:id="e1xr2zpp"> + <appInfo xml:id="a1wyxc5n"> + <application xml:id="a1t43ge2" isodate="2024-10-30T10:56:32" version="4.3.1-3b8cc17"> + <name xml:id="n1cp60l4">Verovio</name> + <p xml:id="p13nndma">Transcoded from MusicXML</p> </application> </appInfo> </encodingDesc> </meiHead> <music> <body> - <mdiv xml:id="mhblkrl"> - <score xml:id="ssc72wy"> - <scoreDef xml:id="s3uaoz5"> - <staffGrp xml:id="sjczhy0"> - <staffDef xml:id="P1" n="1" lines="5" ppq="12"> - <label xml:id="lezfcog">Piano</label> - <meterSig xml:id="mhw0sp2" count="4" unit="4" /> - </staffDef> + <mdiv xml:id="muo97v6"> + <score xml:id="suneqlv"> + <scoreDef xml:id="sz00r05"> + <staffGrp xml:id="s1h35kps"> + <staffGrp xml:id="P1" bar.thru="true"> + <grpSym xml:id="g1eji31e" symbol="brace" /> + <label xml:id="l8lirjj">Piano</label> + <instrDef xml:id="iggd40h" midi.channel="0" midi.instrnum="0" midi.volume="78.00%" /> + <staffDef xml:id="sbpks8p" n="1" lines="5" ppq="1"> + <clef xml:id="c1p7nrnw" shape="G" line="2" /> + <keySig xml:id="ki5anqv" sig="0" /> + <meterSig xml:id="m1vpw2w2" count="4" unit="4" /> + </staffDef> + <staffDef xml:id="s1g416nz" n="2" lines="5" ppq="1"> + <clef xml:id="cezkswz" shape="G" line="2" /> + <keySig xml:id="kk41xfz" sig="0" /> + <meterSig xml:id="m7alo7s" count="4" unit="4" /> + </staffDef> + </staffGrp> </staffGrp> </scoreDef> - <sb xml:id="st1gphw" /> - <section xml:id="swgpvx8"> - <pb xml:id="paw6v6b" /> - <measure xml:id="mz87quy" n="1"> - <staff xml:id="sxxu2aq" n="1"> - <layer xml:id="llktcv2" n="1"> - <note xml:id="n01" dur.ppq="48" dur="1" staff="2" oct="4" pname="a" /> - </layer> - <layer xml:id="lgap59p" n="2"> - <rest xml:id="r01" dur.ppq="24" dur="2" /> - <chord xml:id="carc8ao" dur.ppq="24" dur="2"> - <note xml:id="n02" oct="5" pname="c" /> - <note xml:id="n03" oct="5" pname="e" /> + <section xml:id="suu4o7p"> + <measure xml:id="m1e358qx" n="1"> + <staff xml:id="s1ufvigy" n="1"> + <layer xml:id="l1r7cvga" n="1"> + <rest xml:id="ro1o7cb" dur.ppq="2" dur="2" /> + <chord xml:id="c1c1r6b7" dur.ppq="2" dur="2" stem.dir="down"> + <note xml:id="n6dpu2p" oct="5" pname="c" /> + <note xml:id="njfgcwp" oct="5" pname="e" /> </chord> </layer> </staff> + <staff xml:id="s710zw2" n="2"> + <layer xml:id="leffrs2" n="5"> + <note xml:id="n1txt37q" dur.ppq="4" dur="1" oct="4" pname="a" /> + </layer> + </staff> </measure> </section> </score> diff --git a/partitura/io/exportmatch.py b/partitura/io/exportmatch.py index 72435956..8afa8d64 100644 --- a/partitura/io/exportmatch.py +++ b/partitura/io/exportmatch.py @@ -21,7 +21,6 @@ from partitura.io.matchlines_v1 import ( make_info, make_scoreprop, - make_section, MatchSnote, MatchNote, MatchSnoteNote, @@ -313,6 +312,7 @@ def matchfile_from_alignment( staff = getattr(snote, "staff", None) ornaments = getattr(snote, "ornaments", None) fermata = getattr(snote, "fermata", None) + technical = getattr(snote, "technical", None) if voice is not None: score_attributes_list.append(f"v{voice}") @@ -329,6 +329,11 @@ def matchfile_from_alignment( if fermata is not None: score_attributes_list.append("fermata") + if technical is not None: + for tech_el in technical: + if isinstance(tech_el, score.Fingering): + score_attributes_list.append(f"fingering{tech_el.fingering}") + if isinstance(snote, score.GraceNote): score_attributes_list.append("grace") diff --git a/partitura/io/exportmei.py b/partitura/io/exportmei.py index 3a918858..3479b36f 100644 --- a/partitura/io/exportmei.py +++ b/partitura/io/exportmei.py @@ -6,6 +6,7 @@ import math from collections import defaultdict from lxml import etree +import lxml import partitura.score as spt from operator import itemgetter from itertools import groupby @@ -225,6 +226,7 @@ def _handle_measure(self, measure, measure_el): self._handle_harmony(measure_el, start=measure.start.t, end=measure.end.t) self._handle_fermata(measure_el, start=measure.start.t, end=measure.end.t) self._handle_barline(measure_el, start=measure.start.t, end=measure.end.t) + self._handle_fingering(measure_el, start=measure.start.t, end=measure.end.t) return measure_el def _handle_chord(self, chord, xml_voice_el): @@ -277,6 +279,9 @@ def _handle_note(self, note, xml_voice_el): elif note.tie_prev is not None: note_el.set("tie", "t") + if note.stem_direction in ["up", "down"]: + note_el.set("stem.dir", note.stem_direction) + if note.alter is not None: if ( note.step.lower() + ALTER_TO_MEI[note.alter] @@ -292,7 +297,7 @@ def _handle_note(self, note, xml_voice_el): note_el.set("grace", "acc") return duration - def _handle_tuplets(self, measure_el, start, end): + def _handle_tuplets(self, measure_el: lxml.etree._Element, start: int, end: int): for tuplet in self.part.iter_all(spt.Tuplet, start=start, end=end): start_note = tuplet.start_note end_note = tuplet.end_note @@ -349,7 +354,7 @@ def _handle_tuplets(self, measure_el, start, end): for el in xml_el_within_tuplet: tuplet_el.append(el) - def _handle_beams(self, measure_el, start, end): + def _handle_beams(self, measure_el: lxml.etree._Element, start: int, end: int): for beam in self.part.iter_all(spt.Beam, start=start, end=end): # If the beam has only one note, skip it if len(beam.notes) < 2: @@ -397,7 +402,9 @@ def _handle_beams(self, measure_el, start, end): if note_el.getparent() != beam_el: beam_el.append(note_el) - def _handle_clef_changes(self, measure_el, start, end): + def _handle_clef_changes( + self, measure_el: lxml.etree._Element, start: int, end: int + ): for clef in self.part.iter_all(spt.Clef, start=start, end=end): # Clef element is parent of the note element if clef.start.t == 0: @@ -418,7 +425,7 @@ def _handle_clef_changes(self, measure_el, start, end): clef_el.set("shape", str(clef.sign)) clef_el.set("line", str(clef.line)) - def _handle_ks_changes(self, measure_el, start, end): + def _handle_ks_changes(self, measure_el: lxml.etree._Element, start: int, end: int): # For key signature changes, we add a new scoreDef element at the beginning of the measure # and add the key signature element as attributes of the scoreDef element for key_sig in self.part.iter_all(spt.KeySignature, start=start, end=end): @@ -448,7 +455,7 @@ def _handle_ks_changes(self, measure_el, start, end): parent = measure_el.getparent() parent.insert(parent.index(measure_el), score_def_el) - def _handle_ts_changes(self, measure_el, start, end): + def _handle_ts_changes(self, measure_el: lxml.etree._Element, start: int, end: int): # For key signature changes, we add a new scoreDef element at the beginning of the measure # and add the key signature element as attributes of the scoreDef element for time_sig in self.part.iter_all(spt.TimeSignature, start=start, end=end): @@ -464,7 +471,7 @@ def _handle_ts_changes(self, measure_el, start, end): score_def_el.set("count", str(time_sig.beats)) score_def_el.set("unit", str(time_sig.beat_type)) - def _handle_harmony(self, measure_el, start, end): + def _handle_harmony(self, measure_el: lxml.etree._Element, start: int, end: int): """ For harmonies we add a new harm element at the beginning of the measure. The position doesn't really matter since the tstamp attribute will place it correctly @@ -505,7 +512,7 @@ def _handle_harmony(self, measure_el, start, end): # text is a child element of harmony but not a xml element harm_el.text = "|" + harmony.text - def _handle_fermata(self, measure_el, start, end): + def _handle_fermata(self, measure_el: lxml.etree._Element, start: int, end: int): for fermata in self.part.iter_all(spt.Fermata, start=start, end=end): if fermata.ref is not None: note = fermata.ref @@ -524,7 +531,7 @@ def _handle_fermata(self, measure_el, start, end): # Set the fermata to be above the staff (the highest staff) fermata_el.set("staff", "1") - def _handle_barline(self, measure_el, start, end): + def _handle_barline(self, measure_el: lxml.etree._Element, start: int, end: int): for end_barline in self.part.iter_all( spt.Ending, start=end, end=end + 1, mode="ending" ): @@ -543,6 +550,26 @@ def _handle_barline(self, measure_el, start, end): ): measure_el.set("left", "rptstart") + def _handle_fingering(self, measure_el: lxml.etree._Element, start: int, end: int): + """ + For fingering we add a new fing element at the end of the measure. + The position doesn't really matter since the startid attribute will place it correctly + """ + for note in self.part.iter_all(spt.Note, start=start, end=end): + if note.technical is not None: + for technical_notation in note.technical: + if ( + isinstance(technical_notation, score.Fingering) + and note.id is not None + ): + fing_el = etree.SubElement(measure_el, "fing") + fing_el.set(XMLNS_ID, "fing-" + self.elc_id()) + fing_el.set("startid", note.id) + # Naive way to place the fingering notation + fing_el.set("place", ("above" if note.staff == 1 else "below")) + # text is a child element of fingering but not a xml element + fing_el.text = technical_notation.fingering + @deprecated_alias(parts="score_data") def save_mei( diff --git a/partitura/io/exportmusicxml.py b/partitura/io/exportmusicxml.py index 168fd430..0f065fcd 100644 --- a/partitura/io/exportmusicxml.py +++ b/partitura/io/exportmusicxml.py @@ -137,6 +137,10 @@ def make_note_el(note, dur, voice, counter, n_of_staves): if voice not in (None, 0): etree.SubElement(note_e, "voice").text = "{}".format(voice) + if note.stem_direction is not None: + stem_e = etree.SubElement(note_e, "stem") + stem_e.text = note.stem_direction + if note.fermata is not None: notations.append(etree.Element("fermata")) @@ -150,6 +154,19 @@ def make_note_el(note, dur, voice, counter, n_of_staves): articulations_e.extend(articulations) notations.append(articulations_e) + if note.technical: + technical = [] + for technical_notation in note.technical: + if isinstance(technical_notation, score.Fingering): + tech_el = etree.Element("fingering") + tech_el.text = str(technical_notation.fingering) + technical.append(tech_el) + + if technical: + technical_e = etree.Element("technical") + technical_e.extend(technical) + notations.append(technical_e) + sym_dur = note.symbolic_duration or {} if sym_dur.get("type") is not None: @@ -413,9 +430,12 @@ def linearize_segment_contents(part, start, end, state): for voice in sorted(notes_by_voice.keys()): voice_notes = notes_by_voice[voice] - # sort by pitch + # sort by pitch (then step in case of enharmonic notes) voice_notes.sort( - key=lambda n: n.midi_pitch if hasattr(n, "midi_pitch") else -1, reverse=True + key=lambda n: ( + (n.midi_pitch, n.step) if hasattr(n, "midi_pitch") else (-1, "") + ), + reverse=True, ) # grace notes should precede other notes at the same onset voice_notes.sort(key=lambda n: not isinstance(n, score.GraceNote)) @@ -634,12 +654,20 @@ def merge_measure_contents(notes, other, measure_start): merged[0] = [] cost[0] = 0 + # CHANGE: disabled cost-based merging of non-note elements into stream + # because this led to attributes not being in the beginning of the measure, + # which in turn led to problems with musescore + # fix: add atributes first, then the notes. + # problem: unclear whether this cost-based merging will break anything or + # was just cosmetic to avoid too many forwards and backwards. + # related issue: https://github.com/CPJKU/partitura/issues/390 + # get the voice for which merging notes and other has lowest cost - merge_voice = sorted(cost.items(), key=itemgetter(1))[0][0] + # merge_voice = sorted(cost.items(), key=itemgetter(1))[0][0] result = [] pos = measure_start for i, voice in enumerate(sorted(notes.keys())): - if voice == merge_voice: + if i == 0: # voice == merge_voice: elements = merged[voice] else: @@ -658,7 +686,7 @@ def merge_measure_contents(notes, other, measure_start): elif gap > 0: e = etree.Element("forward") ee = etree.SubElement(e, "duration") - ee.text = "{:d}".format(gap) + ee.text = "{:d}".format(int(gap)) result.append(e) result.extend([e for _, _, e in elements]) @@ -1053,8 +1081,8 @@ def handle_parents(part): part_e.append(etree.Comment(MEASURE_SEP_COMMENT)) attrib = {} - if measure.number is not None: - attrib["number"] = str(measure.number) + if measure.name is not None: + attrib["number"] = str(measure.name) measure_e = etree.SubElement(part_e, "measure", **attrib) contents = linearize_measure_contents( diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index b7012569..e853840f 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -70,6 +70,15 @@ } +class KernElement(object): + def __init__(self, element): + self.editorial_start = True if "ossia" in element else False + self.editorial_end = True if "Xstrophe" in element else False + self.voice_end = True if "*v" in element else False + self.voice_start = True if "*^" in element else False + self.element = element.replace("*", "") + + def add_durations(a, b): return a * b / (a + b) @@ -102,8 +111,10 @@ def parse_by_voice(file: list, dtype=np.object_): data[line, voice] = file[line][voice] data = data.T if num_voices > 1: - # Copy global lines from the first voice to all other voices + # Copy global lines from the first voice to all other voices unless they are the string "*S/ossia" cp_idx = np.char.startswith(data[0], "*") + un_idx = np.char.startswith(data[0], "*S/ossia") + cp_idx = np.logical_and(cp_idx, ~un_idx) for i in range(1, num_voices): data[i][cp_idx] = data[0][cp_idx] # Copy Measure Lines from the first voice to all other voices @@ -171,10 +182,16 @@ def element_parsing( ): divs_pq = part._quarter_durations[0] current_tl_pos = 0 + editorial = False measure_mapping = {m.number: m.start.t for m in part.iter_all(spt.Measure)} for i in range(elements.shape[0]): element = elements[i] - if element is None: + if isinstance(element, KernElement): + if element.editorial_start: + editorial = True + if element.editorial_end: + editorial = False + if element is None or editorial: continue if isinstance(element, spt.GenericNote): if total_duration_values[i] == 0: @@ -250,9 +267,6 @@ def load_kern( ) # Get Splines splines = file[1:].T[note_parts] - # Inverse Order - splines = splines[::-1] - parsing_idxs = parsing_idxs[::-1] prev_staff = 1 has_instrument = np.char.startswith(splines, "*I") # if all parts have the same instrument, then they are the same part. @@ -351,7 +365,8 @@ def load_kern( ) doc_name = get_document_name(filename) - score = spt.Score(partlist=partlist, id=doc_name) + # inversing the partlist results to correct part order and visualization for exporting musicxml files + score = spt.Score(partlist=partlist[::-1], id=doc_name) return score @@ -430,17 +445,15 @@ def parse(self, spline: np.array): self.tie_prev = np.zeros(note_num, dtype=bool) notes = np.vectorize(self.meta_note_line, otypes=[object])(spline[note_mask]) self.total_duration_values[note_mask] = self.note_duration_values - # shift tie_next by one to the right + # Notes should appear in order within stream so shift tie_next by one to the right + # and tie next and inversingly tie_prev also + # Case of note to chord tie or chord to note tie is not handled yet for note, to_tie in np.c_[ notes[self.tie_next], notes[np.roll(self.tie_next, -1)] ]: to_tie.tie_next = note - # note.tie_prev = to_tie - for note, to_tie in np.c_[ - notes[self.tie_prev], notes[np.roll(self.tie_prev, 1)] - ]: note.tie_prev = to_tie - # to_tie.tie_next = note + elements[note_mask] = notes # Find Slur indices, i.e. where spline cells contain "(" or ")" @@ -505,6 +518,8 @@ def meta_tandem_line(self, line: str): return self.process_key_line(rest) elif line.startswith("*-"): return self.process_fine() + else: + return KernElement(element=line) def process_tempo_line(self, line: str): return spt.Tempo(float(line)) @@ -561,7 +576,7 @@ def process_clef_line(self, line: str): else: raise ValueError("Unrecognized clef line: {}".format(line)) else: - clef_line = has_line.group(0) + clef_line = int(has_line.group(0)) if octave_change and clef_line == 2 and clef == "G": octave = -1 elif octave_change: diff --git a/partitura/io/importmatch.py b/partitura/io/importmatch.py index bb1f061a..6f79f5c9 100644 --- a/partitura/io/importmatch.py +++ b/partitura/io/importmatch.py @@ -12,41 +12,16 @@ from partitura import score from partitura.score import Part, Score from partitura.performance import PerformedPart, Performance -from partitura.musicanalysis import estimate_voices, estimate_key from partitura.io.matchlines_v0 import ( FROM_MATCHLINE_METHODS as FROM_MATCHLINE_METHODSV0, - parse_matchline as parse_matchlinev0, MatchInfo as MatchInfoV0, - MatchMeta as MatchMetaV0, - MatchSnote as MatchSnoteV0, - MatchNote as MatchNoteV0, - MatchSnoteNote as MatchSnoteNoteV0, - MatchSnoteDeletion as MatchSnoteDeletionV0, - MatchSnoteTrailingScore as MatchSnoteTrailingScoreV0, - MatchInsertionNote as MatchInsertionNoteV0, - MatchHammerBounceNote as MatchHammerBounceNoteV0, - MatchTrailingPlayedNote as MatchTrailingPlayedNoteV0, - MatchSustainPedal as MatchSustainPedalV0, - MatchSoftPedal as MatchSoftPedalV0, MatchTrillNote as MatchTrillNoteV0, ) from partitura.io.matchlines_v1 import ( FROM_MATCHLINE_METHODS as FROM_MATCHLINE_METHODSV1, MatchInfo as MatchInfoV1, - MatchScoreProp as MatchScorePropV1, - MatchSection as MatchSectionV1, - MatchStime as MatchStimeV1, - MatchPtime as MatchPtimeV1, - MatchStimePtime as MatchStimePtimeV1, - MatchSnote as MatchSnoteV1, - MatchNote as MatchNoteV1, - MatchSnoteNote as MatchSnoteNoteV1, - MatchSnoteDeletion as MatchSnoteDeletionV1, - MatchInsertionNote as MatchInsertionNoteV1, - MatchSustainPedal as MatchSustainPedalV1, - MatchSoftPedal as MatchSoftPedalV1, MatchOrnamentNote as MatchOrnamentNoteV1, ) @@ -56,41 +31,26 @@ MatchLine, BaseSnoteLine, BaseSnoteNoteLine, - BaseStimePtimeLine, BaseDeletionLine, BaseInsertionLine, BaseOrnamentLine, - BaseSustainPedalLine, - BaseSoftPedalLine, ) from partitura.io.matchfile_utils import ( Version, number_pattern, vnumber_pattern, + fingering_pattern, MatchTimeSignature, - MatchKeySignature, format_pnote_id, ) -from partitura.utils.music import ( - midi_ticks_to_seconds, - pitch_spelling_to_midi_pitch, - ensure_pitch_spelling_format, - key_name_to_fifths_mode, - estimate_clef_properties, - note_array_from_note_list, -) +from partitura.utils.music import midi_ticks_to_seconds -from partitura.utils.misc import ( - deprecated_alias, - deprecated_parameter, - PathLike, - get_document_name, -) +from partitura.utils.misc import deprecated_alias, PathLike, get_document_name -from partitura.utils.generic import interp1d, partition, iter_current_next +from partitura.utils.generic import interp1d, iter_current_next __all__ = ["load_match"] @@ -244,6 +204,12 @@ def load_match( first_note_at_zero : bool, optional When True the note_on and note_off times in the performance are shifted to make the first note_on time equal zero. + Defaults to False. + offset_duration_whole: Boolean, optional + A flag for the type of offset and duration given in the matchfile. + When true, the function expects the values to be given in whole + notes (e.g. 1/4 for a quarter note) independet of time signature. + Defaults to True. Returns ------- @@ -490,9 +456,14 @@ def part_from_matchfile( ts = mf.time_signatures min_time = snotes[0].OnsetInBeats # sorted by OnsetInBeats max_time = max(n.OffsetInBeats for n in snotes) - _, beats_map, _, beat_type_map, min_time_q, max_time_q = make_timesig_maps( - ts, max_time - ) + ( + beats_map_from_beats, + beats_map, + beat_type_map_from_beats, + beat_type_map, + min_time_q, + max_time_q, + ) = make_timesig_maps(ts, max_time) # compute necessary divs based on the types of notes in the # match snotes (only integers) @@ -511,7 +482,6 @@ def part_from_matchfile( onset_in_beats = np.array([note.OnsetInBeats for note in snotes]) unique_onsets, inv_idxs = np.unique(onset_in_beats, return_inverse=True) - # unique_onset_idxs = [np.where(onset_in_beats == u) for u in unique_onsets] iois_in_beats = np.diff(unique_onsets) beat_to_quarter = 4 / beat_type_map(onset_in_beats) @@ -528,10 +498,6 @@ def part_from_matchfile( onset_in_divs = np.r_[0, np.cumsum(divs * iois_in_quarters)][inv_idxs] onset_in_quarters = onset_in_quarters[inv_idxs] - # duration_in_beats = np.array([note.DurationInBeats for note in snotes]) - # duration_in_quarters = duration_in_beats * beat_to_quarter - # duration_in_divs = duration_in_quarters * divs - part.set_quarter_duration(0, divs) bars = np.unique([n.Measure for n in snotes]) t = min_time @@ -542,8 +508,6 @@ def part_from_matchfile( if t > 0: # if we have an incomplete first measure that isn't an anacrusis # measure, add a rest (dummy) - # t = t-t%beats_map(min_time) - # if starting beat is above zero, add padding rest = score.Rest() part.add(rest, start=0, end=t * divs) @@ -551,16 +515,34 @@ def part_from_matchfile( offset = 0 t = t - t % beats_map(min_time) - for b0, b1 in iter_current_next(bars, end=bars[-1] + 1): - bar_times.setdefault(b0, t) - if t < 0: - t = 0 + for b_name in bars: + notes_in_this_bar = [ + (ni, n) for ni, n in enumerate(snotes) if n.Measure == b_name + ] + a_note_in_this_bar = notes_in_this_bar[0][1] + a_note_id_in_this_bar = notes_in_this_bar[0][0] + bar_offset = ( + (a_note_in_this_bar.Beat - 1) + * 4 + / beat_type_map_from_beats(a_note_in_this_bar.OnsetInBeats) + ) + on_off_scale = 1 + if not match_offset_duration_in_whole: + on_off_scale = beat_type_map_from_beats(a_note_in_this_bar.OnsetInBeats) + beat_offset = ( + 4 + / on_off_scale + * a_note_in_this_bar.Offset.numerator + / ( + a_note_in_this_bar.Offset.denominator + * (a_note_in_this_bar.Offset.tuple_div or 1) + ) + ) - else: - # multiply by diff between consecutive bar numbers - n_bars = b1 - b0 - if t <= max_time_q: - t += (n_bars * 4 * beats_map(t)) / beat_type_map(t) + barline_in_quarters = ( + onset_in_quarters[a_note_id_in_this_bar] - bar_offset - beat_offset + ) + bar_times[b_name] = barline_in_quarters for ni, note in enumerate(snotes): # start of bar in quarter units @@ -585,15 +567,6 @@ def part_from_matchfile( / (note.Offset.denominator * (note.Offset.tuple_div or 1)) ) - # check anacrusis measure beat counting type for the first note - if bar_start < 0 and (bar_offset != 0 or beat_offset != 0) and ni == 0: - # in case of fully counted anacrusis we set the bar_start - # to -bar_duration (in quarters) so that the below calculation is correct - # not active for shortened anacrusis measures - bar_start = -beats_map(bar_start) * 4 / beat_type_map(bar_start) - # reset the bar_start for other notes in the anacrusis measure - bar_times[note.Bar] = bar_start - # convert the onset time in quarters (0 at first barline) to onset # time in divs (0 at first note) onset_divs = int(round(divs * (bar_start + bar_offset + beat_offset - offset))) @@ -622,6 +595,7 @@ def part_from_matchfile( alter=note.Modifier, id=note.Anchor, articulations=articulations, + technical=[], ) staff_nr = next( @@ -650,6 +624,18 @@ def part_from_matchfile( None, ) + if any(a.startswith("fingering") for a in note.ScoreAttributesList): + note_attributes["technical"].append( + next( + ( + score.Fingering(int(a[9:])) + for a in note.ScoreAttributesList + if fingering_pattern.match(a) + ), + None, + ) + ) + # get rid of this if as soon as we have a way to iterate over the # duration components. For now we have to treat the cases simple # and compound durations separately. @@ -754,9 +740,25 @@ def part_from_matchfile( add_staffs(part) # add_clefs(part) - # add incomplete measure if necessary - if offset < 0: - part.add(score.Measure(number=0), 0, int(-offset * divs)) + prev_measure = None + for measure_counter, measure_name in enumerate(bar_times.keys()): + barline_in_quarters = bar_times[measure_name] + barline_in_divs = int(round(divs * (barline_in_quarters - offset))) + if barline_in_divs < 0: + barline_in_divs = 0 + if prev_measure is not None: + part.add(prev_measure, None, barline_in_divs) + prev_measure = score.Measure(number=measure_counter + 1, name=str(measure_name)) + part.add(prev_measure, barline_in_divs) + last_closing_barline = barline_in_divs + int( + round( + divs + * beats_map(barline_in_quarters) + * 4 + / beat_type_map(barline_in_quarters) + ) + ) + part.add(prev_measure, None, last_closing_barline) # add the rest of the measures automatically score.add_measures(part) @@ -780,7 +782,7 @@ def part_from_matchfile( def make_timesig_maps( ts_orig: List[Tuple[float, int, MatchTimeSignature]], max_time: float, -) -> (Callable, Callable, Callable, Callable, float, float): +) -> Tuple[Callable, Callable, Callable, Callable, float, float]: """ Create time signature (interpolation) maps @@ -850,17 +852,17 @@ def make_timesig_maps( def add_staffs(part: Part, split: int = 55, only_missing: bool = True) -> None: """ - Method to add staff information to a part - - Parameters - ---------- - part: Part - Part to add staff information to. - split: int - MIDI pitch to split staff into upper and lower. Default is 55 - only_missing: bool - If True, only add staff to those notes that do not have staff info already. - x""" + Method to add staff information to a part + + Parameters + ---------- + part: Part + Part to add staff information to. + split: int + MIDI pitch to split staff into upper and lower. Default is 55 + only_missing: bool + If True, only add staff to those notes that do not have staff info already. + """ # assign staffs using a hard limit notes = part.notes_tied for n in notes: diff --git a/partitura/io/importmei.py b/partitura/io/importmei.py index 2f4ba704..95606d8b 100644 --- a/partitura/io/importmei.py +++ b/partitura/io/importmei.py @@ -3,6 +3,7 @@ """ This module contains methods for importing MEI files. """ +import os from collections import OrderedDict from lxml import etree from fractions import Fraction @@ -64,6 +65,9 @@ def load_mei(filename: PathLike) -> score.Score: class MeiParser(object): def __init__(self, mei_path: PathLike) -> None: + # check if the file exists. Verovio won't complain if it doesn't + if not os.path.exists(mei_path): + raise FileNotFoundError(f"File {mei_path} not found.") document, ns = self._parse_mei(mei_path, use_verovio=VEROVIO_AVAILABLE) self.document = document self.ns = ns # the namespace in the MEI file @@ -322,11 +326,9 @@ def _handle_clef(self, element, position, part): # find the staff number parent = element.getparent() if parent.tag == self._ns_name("staffDef"): - # number = parent.attrib["n"] - number = 1 + number = parent.attrib.get("n", 1) else: # go back another level to staff element - # number = parent.getparent().attrib["n"] - number = 1 + number = parent.getparent().attrib.get("n", 1) sign = element.attrib["shape"] line = element.attrib["line"] octave = self._compute_clef_octave( @@ -641,6 +643,10 @@ def _handle_note(self, note_el, position, voice, staff, part) -> int: note_id, duration, symbolic_duration = self._duration_info(note_el, part) # find if it's grace grace_attr = note_el.get("grace") + # find if it has a different staff specification (for staff crossings) + different_staff = note_el.get("staff") + if different_staff is not None: + staff = int(different_staff) if grace_attr is None: # create normal note note = score.Note( @@ -649,7 +655,7 @@ def _handle_note(self, note_el, position, voice, staff, part) -> int: alter=alter, id=note_id, voice=voice, - staff=1, + staff=staff, symbolic_duration=symbolic_duration, articulations=None, # TODO : add articulation ) @@ -668,7 +674,7 @@ def _handle_note(self, note_el, position, voice, staff, part) -> int: alter=alter, id=note_id, voice=voice, - staff=1, + staff=staff, symbolic_duration=symbolic_duration, articulations=None, # TODO : add articulation ) @@ -702,11 +708,15 @@ def _handle_rest(self, rest_el, position, voice, staff, part): """ # find duration info rest_id, duration, symbolic_duration = self._duration_info(rest_el, part) + # find if it has a different staff specification (for staff crossings) + different_staff = rest_el.get("staff") + if different_staff is not None: + staff = int(different_staff) # create rest rest = score.Rest( id=rest_id, voice=voice, - staff=1, + staff=staff, symbolic_duration=symbolic_duration, articulations=None, ) @@ -744,12 +754,16 @@ def _handle_mrest(self, mrest_el, position, voice, staff, part): # find divs per measure ppq = part.quarter_duration_map(position) parts_per_measure = int(ppq * 4 * last_ts.beats / last_ts.beat_type) + # find if it has a different staff specification (for staff crossings) + different_staff = mrest_el.get("staff") + if different_staff is not None: + staff = int(different_staff) # create dummy rest to insert in the timeline rest = score.Rest( id=mrest_id, voice=voice, - staff=1, + staff=staff, symbolic_duration=estimate_symbolic_duration(parts_per_measure, ppq), articulations=None, ) @@ -793,12 +807,16 @@ def _handle_multirest(self, multirest_el, position, voice, staff, part): # find divs per measure ppq = part.quarter_duration_map(position) parts_per_measure = int(ppq * 4 * last_ts.beats / last_ts.beat_type) + # find if it has a different staff specification (for staff crossings) + different_staff = multirest_el.get("staff") + if different_staff is not None: + staff = int(different_staff) # create dummy rest to insert in the timeline rest = score.Rest( id=multirest_id, voice=voice, - staff=1, + staff=staff, symbolic_duration=estimate_symbolic_duration(parts_per_measure, ppq), articulations=None, ) @@ -832,12 +850,22 @@ def _handle_chord(self, chord_el, position, voice, staff, part): """ # find duration info chord_id, duration, symbolic_duration = self._duration_info(chord_el, part) + # find if the entire chord has a different staff specification (for staff crossings) + different_staff = chord_el.get("staff") + if different_staff is not None: + staff = int(different_staff) # find notes info notes_el = chord_el.findall(self._ns_name("note")) for note_el in notes_el: note_id = note_el.attrib[self._ns_name("id", XML_NAMESPACE)] # find pitch info step, octave, alter = self._pitch_info(note_el) + # find if single notes have a different staff specification + different_staff = note_el.get("staff") + if different_staff is not None: + note_staff = int(different_staff) + else: + note_staff = staff # create note note = score.Note( step=step, @@ -845,7 +873,7 @@ def _handle_chord(self, chord_el, position, voice, staff, part): alter=alter, id=note_id, voice=voice, - staff=1, + staff=note_staff, symbolic_duration=symbolic_duration, articulations=None, # TODO : add articulation ) @@ -982,7 +1010,11 @@ def _handle_staff_in_measure( for i_layer, layer_el in enumerate(layers_el): end_positions.append( self._handle_layer_in_staff_in_measure( - layer_el, i_layer + 1, staff_ind, position, part + layer_el, + int(layer_el.attrib.get("n", i_layer + 1)), + staff_ind, + position, + part, ) ) # check if layers have equal duration (bad encoding, but it often happens) @@ -1063,7 +1095,11 @@ def _handle_section(self, section_el, parts, position: int, measure_number: int) for i_s, (part, staff_el) in enumerate(zip(parts, staves_el)): end_positions.append( self._handle_staff_in_measure( - staff_el, i_s + 1, position, part, measure_number + staff_el, + int(staff_el.attrib.get("n", i_s + 1)), + position, + part, + measure_number, ) ) # handle directives (dir elements) diff --git a/partitura/io/importmidi.py b/partitura/io/importmidi.py index 96ee2756..50f8ad0f 100644 --- a/partitura/io/importmidi.py +++ b/partitura/io/importmidi.py @@ -306,7 +306,7 @@ def load_score_midi( filename: Union[PathLike, mido.MidiFile], part_voice_assign_mode: Optional[int] = 0, quantization_unit: Optional[int] = None, - estimate_voice_info: bool = True, + estimate_voice_info: bool = False, estimate_key: bool = False, assign_note_ids: bool = True, ) -> score.Score: @@ -363,10 +363,8 @@ def load_score_midi( the MIDI file. estimate_voice_info : bool, optional When True use Chew and Wu's voice separation algorithm [2]_ to - estimate voice information. This option is ignored for - part/voice assignment modes that infer voice information from - the track/channel info (i.e. `part_voice_assign_mode` equals - 1, 3, 4, or 5). Defaults to True. + estimate voice information. If the voice information was imported + from the file, it will be overridden. Defaults to False. Returns ------- diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index a359fbe3..69b7a04a 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -8,7 +8,7 @@ import warnings import zipfile -from typing import Union, Optional +from typing import Union, Optional, List import numpy as np from lxml import etree @@ -1198,6 +1198,9 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non # initialize beam to None beam = None + # get the stem direction of the note if any + stem_dir = get_value_from_tag(e, "stem", str) or None + # add support of uppercase "ID" tags note_id = ( get_value_from_attribute(e, "id", str) @@ -1241,6 +1244,12 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non else: ornaments = {} + technical_e = e.find("notations/technical") + if technical_e is not None: + technical_notations = get_technical_notations(technical_e) + else: + technical_notations = {} + pitch = e.find("pitch") unpitch = e.find("unpitched") if pitch is not None: @@ -1268,8 +1277,10 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non symbolic_duration=symbolic_duration, articulations=articulations, ornaments=ornaments, + technical=technical_notations, steal_proportion=steal_proportion, doc_order=doc_order, + stem_direction=stem_dir, ) if isinstance(prev_note, score.GraceNote) and prev_note.voice == voice: note.grace_prev = prev_note @@ -1305,7 +1316,9 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non symbolic_duration=symbolic_duration, articulations=articulations, ornaments=ornaments, + technical=technical_notations, doc_order=doc_order, + stem_direction=stem_dir, ) if isinstance(prev_note, score.GraceNote) and prev_note.voice == voice: @@ -1333,8 +1346,10 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non notehead=notehead, noteheadstyle=noteheadstylebool, articulations=articulations, + technical=technical_notations, symbolic_duration=symbolic_duration, doc_order=doc_order, + stem_direction=stem_dir, ) else: @@ -1345,6 +1360,7 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non staff=staff, symbolic_duration=symbolic_duration, articulations=articulations, + technical=technical_notations, doc_order=doc_order, ) @@ -1453,7 +1469,7 @@ def handle_tuplets(notations, ongoing, note): # assert that starting tuplet times are before stopping tuplet times for start_tuplet, stop_tuplet in zip(starting_tuplets, stopping_tuplets): assert ( - start_tuplet.start_note.start.t < stop_tuplet.end_note.start.t + start_tuplet.start_note.start.t <= stop_tuplet.end_note.start.t ), "Tuplet start time is after tuplet stop time" return starting_tuplets, stopping_tuplets @@ -1642,6 +1658,30 @@ def get_ornaments(e): return [a for a in ornaments if e.find(a) is not None] +def get_technical_notations(e: etree._Element) -> List[score.NoteTechnicalNotation]: + # For a full list of technical notations + # https://usermanuals.musicxml.com/MusicXML/Content/EL-MusicXML-technical.htm + # for now we only support fingering + technical_notation_parsers = { + "fingering": parse_fingering, + } + + technical_notations = [ + parser(e.find(a)) + for a, parser in technical_notation_parsers.items() + if e.find(a) is not None + ] + + return technical_notations + + +def parse_fingering(e: etree._Element) -> score.Fingering: + + fingering = score.Fingering(fingering=int(e.text)) + + return fingering + + @deprecated_alias(fn="filename") def musicxml_to_notearray( filename, diff --git a/partitura/io/importnakamura.py b/partitura/io/importnakamura.py index f2f32672..b77523eb 100644 --- a/partitura/io/importnakamura.py +++ b/partitura/io/importnakamura.py @@ -35,7 +35,7 @@ def load_nakamuracorresp(filename: PathLike) -> Tuple[Union[np.ndarray, list]]: Parameters ---------- filename : str - The nakamura match.txt-file + The nakamura corresp.txt-file Returns ------- diff --git a/partitura/io/matchfile_utils.py b/partitura/io/matchfile_utils.py index 9fe76745..2f7340f7 100644 --- a/partitura/io/matchfile_utils.py +++ b/partitura/io/matchfile_utils.py @@ -47,6 +47,7 @@ number_pattern = re.compile(r"\d+") vnumber_pattern = re.compile(r"v\d+") +fingering_pattern = re.compile(r"fingering\d+") # For matchfiles before 1.0.0. old_version_pattern = re.compile(r"^(?P<minor>[0-9]+)\.(?P<patch>[0-9]+)") diff --git a/partitura/musicanalysis/note_array_to_score.py b/partitura/musicanalysis/note_array_to_score.py index a1dc19a3..08d35218 100644 --- a/partitura/musicanalysis/note_array_to_score.py +++ b/partitura/musicanalysis/note_array_to_score.py @@ -190,7 +190,7 @@ def create_part( warnings.warn("add measures", stacklevel=2) if not barebones and anacrusis_divs > 0: - part.add(score.Measure(0), 0, anacrusis_divs) + part.add(score.Measure(number=1, name=str(0)), 0, anacrusis_divs) if not barebones and sanitize: warnings.warn("Inferring measures", stacklevel=2) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index 57b9f9b5..121c2c83 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -11,8 +11,9 @@ import types from typing import List, Union, Tuple -from partitura.utils import ensure_notearray, ensure_rest_array +from partitura.utils import ensure_notearray, ensure_rest_array, clef_sign_to_int from partitura.score import ScoreLike +from collections import defaultdict __all__ = [ "list_note_feats_functions", @@ -131,6 +132,16 @@ def make_note_features( include_grace_notes=True, include_time_signature=True, ) + + if len(set(na["id"])) != len(na): + warnings.warn( + "Length of note array {0} " + "does not correspond to number of unique IDs {1}. " + "Some feature functions may return spurious values.".format( + len(na), len(set(na["id"])) + ) + ) + acc = [] if isinstance(feature_functions, str) and feature_functions == "all": feature_functions = list_note_feats_functions() @@ -256,6 +267,15 @@ def make_rest_features( if na.size == 0: return np.array([]) + if len(set(na["id"])) != len(na): + warnings.warn( + "Length of rest array {0} " + "does not correspond to number of unique IDs {1}. " + "Some feature functions may return spurious values.".format( + len(na), len(set(na["id"])) + ) + ) + acc = [] if isinstance(feature_functions, str) and feature_functions == "all": feature_functions = list_note_feats_functions() @@ -526,6 +546,65 @@ def grace_feature(na, part, **kwargs): return W, feature_names +def clef_feature(na, part, **kwargs): + """Clef feature + + This feature encodes the current clef of the staff of each note. + Note that this feature does not return the staff number per note, + see staff_feature for this information. + """ + notes = {n.id: n for n in part.notes_tied} + names = ["clef_sign", "clef_line", "clef_octave_change"] + clef_dict = defaultdict(list) + staff_numbers = set() + clef_list = [clef for clef in part.iter_all(score.Clef)] + if len(clef_list) > 0: + for clef in clef_list: + staff = clef.staff or 1 + staff_numbers.add(staff) + time_key = "time_" + str(staff) + clef_key = "clef_" + str(staff) + clef_dict[time_key].append(clef.start.t) + clef_dict[clef_key].append(clef) + + for staff in staff_numbers: + time_key = "time_" + str(staff) + interpolator_key = "interp_" + str(staff) + start_times = np.array(clef_dict[time_key]) + clef_indices = np.arange(len(start_times)) + interpolator = interp1d( + start_times, + clef_indices, + kind="previous", + bounds_error=False, + fill_value="extrapolate", + ) + clef_dict[interpolator_key].append(interpolator) + + W = np.zeros((len(notes), 3)) + for i, na_n in enumerate(na): + n = notes[na_n["id"]] + staff = n.staff or 1 + time = n.start.t + clef_key = "clef_" + str(staff) + interpolator_key = "interp_" + str(staff) + clef_idx = clef_dict[interpolator_key][0](time) + clef = clef_dict[clef_key][int(clef_idx)] + sign = clef.sign or "none" + W[i, 0] = clef_sign_to_int(sign) + W[i, 1] = clef.line or 0 + W[i, 2] = clef.octave_change or 0 + + else: + # add dummy clef + W = np.zeros((len(notes), 3)) + W[:, 0] = 6 # "none" + W[:, 1] = 0 + W[:, 2] = 0 + + return W, names + + def loudness_direction_feature(na, part, **kwargs): """The loudness directions in part. @@ -872,9 +951,10 @@ def articulation_feature(na, part, **kwargs): force_size = False feature_by_name = {} - notes = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests + notes = {n.id: n for n in part.notes_tied} N = len(notes) - for i, n in enumerate(notes): + for i, na_n in enumerate(na): + n = notes[na_n["id"]] if n.articulations: for art in n.articulations: if art in names: @@ -929,9 +1009,10 @@ def ornament_feature(na, part, **kwargs): "other-ornament", ] feature_by_name = {} - notes = part.notes_tied + notes = {n.id: n for n in part.notes_tied} N = len(notes) - for i, n in enumerate(notes): + for i, na_n in enumerate(na): + n = notes[na_n["id"]] if n.ornaments: for art in n.ornaments: if art in names: @@ -1008,13 +1089,15 @@ def metrical_feature(na, part, **kwargs): non-zero value in the 'metrical_4_4_weak' descriptor. """ - notes = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests + notes_list = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests + notes = {n.id: n for n in notes_list} ts_map = part.time_signature_map bm = part.beat_map feature_by_name = {} eps = 10**-6 - for i, n in enumerate(notes): + for i, na_n in enumerate(na): + n = notes[na_n["id"]] beats, beat_type, mus_beats = ts_map(n.start.t).astype(int) measure = next(n.start.iter_prev(score.Measure, eq=True), None) @@ -1049,6 +1132,12 @@ def metrical_strength_feature(na, part, **kwargs): This feature encodes the beat phase (relative position of a note within the measure), as well as metrical strength of common time signatures. + + 'beat_phase' encodes the position in the measure as value between 0.0 and 1.0 + 'metrical_strength_downbeat' is 1.0 on downbeats, 0.0 elsewhere + 'metrical_strength_secondary' is 1.0 on measure midpoint, 0.0 elsewhere, + not valid for triple meters + 'metrical_strength_weak' is 1.0 where both others are 0.0, 0.0 elsewhere """ names = [ "beat_phase", @@ -1063,7 +1152,48 @@ def metrical_strength_feature(na, part, **kwargs): W[:, 0] = np.divide(relod, totmd) # Onset Phase W[:, 1] = na["is_downbeat"].astype(float) W[:, 2][W[:, 0] == 0.5] = 1.00 - W[:, 3][np.nonzero(np.add(W[:, 1], W[:, 0]) == 1.00)] = 1.00 + W[:, 3][W[:, 1] == W[:, 2]] = 1.00 + + return W, names + + +def measure_feature(na, part, **kwargs): + """Measure feature + + This feature encodes the measure each note is in. + + """ + notes_list = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests + notes = {n.id: n for n in notes_list} + bm = part.beat_map + + global_start = bm(part.first_point.t) + global_end = bm(part.last_point.t) + global_number = 0 # default global measure number + + names = [ + "measure_number", + "measure_start_beat", + "measure_end_beat", + ] + W = np.zeros((len(notes), 3)) + + for i, na_n in enumerate(na): + n = notes[na_n["id"]] + measure = next(n.start.iter_prev(score.Measure, eq=True), None) + + if measure: + start = bm(measure.start.t) + end = bm(measure.end.t) + number = measure.number + else: + start = global_start + end = global_end + number = global_number + + W[i, 0] = number + W[i, 1] = start + W[i, 2] = end return W, names diff --git a/partitura/musicanalysis/performance_codec.py b/partitura/musicanalysis/performance_codec.py index 2eac40c2..d6c2d737 100644 --- a/partitura/musicanalysis/performance_codec.py +++ b/partitura/musicanalysis/performance_codec.py @@ -608,8 +608,8 @@ def tempo_by_derivative( @deprecated_alias(part="score", ppart="performance") def to_matched_score( - score: ScoreLike, - performance: PerformanceLike, + score: Union[ScoreLike, np.ndarray], + performance: Union[PerformanceLike, np.ndarray], alignment: list, include_score_markings=False, ): @@ -635,7 +635,7 @@ def to_matched_score( a["score_id"] = str(a["score_id"]) feature_functions = None - if include_score_markings: + if include_score_markings and not isinstance(score, np.ndarray): feature_functions = [ "loudness_direction_feature", "articulation_feature", @@ -643,8 +643,18 @@ def to_matched_score( "slur_feature", ] - na = note_features.compute_note_array(score, feature_functions=feature_functions) - p_na = performance.note_array() + if isinstance(score, np.ndarray): + na = score + else: + na = note_features.compute_note_array( + score, + feature_functions=feature_functions, + ) + + if isinstance(performance, np.ndarray): + p_na = performance + else: + p_na = performance.note_array() part_by_id = dict((n["id"], na[na["id"] == n["id"]]) for n in na) ppart_by_id = dict((n["id"], p_na[p_na["id"] == n["id"]]) for n in p_na) @@ -682,7 +692,7 @@ def to_matched_score( ("p_duration", "f4"), ("velocity", "i4"), ] - if include_score_markings: + if include_score_markings and not isinstance(score, np.ndarray): fields += [("voice", "i4")] fields += [ (field, sn.dtype.fields[field][0]) @@ -747,27 +757,23 @@ def get_time_maps_from_alignment( # Remove grace notes if remove_ornaments: - # TODO: check that all onsets have a duration? + # check that all onsets have a duration # ornaments (grace notes) do not have a duration - score_unique_onset_idxs = np.array( - [ - np.where(np.logical_and(score_onsets == u, score_durations > 0))[0] - for u in score_unique_onsets - ], - dtype=object, - ) + score_unique_onset_idxs = [ + np.where(np.logical_and(score_onsets == u, score_durations > 0))[0] + for u in score_unique_onsets + ] else: - score_unique_onset_idxs = np.array( - [np.where(score_onsets == u)[0] for u in score_unique_onsets], - dtype=object, - ) + score_unique_onset_idxs = [ + np.where(score_onsets == u)[0] for u in score_unique_onsets + ] # For chords, we use the average performed onset as a proxy for # representing the "performeance time" of the position of the score # onsets eq_perf_onsets = np.array( - [np.mean(perf_onsets[u]) for u in score_unique_onset_idxs] + [np.mean(perf_onsets[u.astype(int)]) for u in score_unique_onset_idxs] ) # Get maps @@ -828,6 +834,14 @@ def get_matched_notes(spart_note_array, ppart_note_array, alignment): p_idx = int(p_idx) matched_idxs.append((s_idx, p_idx)) + if len(matched_idxs) == 0: + warnings.warn( + "No matched note IDs found. " + "Either the alignment contains no matches " + "or the IDs in score of performance do not correspond to the alignment " + "(maybe due to repeat unfolding)." + ) + return np.array(matched_idxs) diff --git a/partitura/performance.py b/partitura/performance.py index 8957491b..a04e726b 100644 --- a/partitura/performance.py +++ b/partitura/performance.py @@ -152,8 +152,8 @@ def num_tracks(self) -> int: def note_array(self, *args, **kwargs) -> np.ndarray: """Structured array containing performance information. - The fields are 'id', 'pitch', 'onset_div', 'duration_div', - 'onset_sec', 'duration_sec' and 'velocity'. + The fields are 'id', 'pitch', 'onset_tick', 'duration_tick', + 'onset_sec', 'duration_sec', 'track', 'channel', and 'velocity'. """ fields = [ @@ -178,7 +178,7 @@ def note_array(self, *args, **kwargs) -> np.ndarray: duration_sec = offset - note_on_sec duration_tick = ( n.get( - "note_off_tick", + seconds_to_midi_ticks(n["sound_off"], mpq=self.mpq, ppq=self.ppq), seconds_to_midi_ticks(n["note_off"], mpq=self.mpq, ppq=self.ppq), ) - note_on_tick @@ -206,9 +206,19 @@ def from_note_array( id: str = None, part_name: str = None, ): - """Create an instance of PerformedPart from a note_array. + """ + Create an instance of PerformedPart from a note_array. Note that this property does not include non-note information (i.e. - controls such as sustain pedal). + controls such as sustain pedal, program changes, tempo changes, etc.). + + The following fields are mandatory: + 'pitch', 'onset_sec', 'duration_sec', and 'velocity'. + + The following fields are used if available: + 'id', 'track', 'channel'. + + The following fields are ignored: + 'onset_tick', 'duration_tick', all others """ if "id" not in note_array.dtype.names: n_ids = ["n{0}".format(i) for i in range(len(note_array))] @@ -271,7 +281,6 @@ def adjust_offsets_w_sustain( pedal = pedal[np.argsort(pedal[:, 0]), :] # reduce the pedal info to just the times where there is a change in pedal state - pedal = np.vstack( ( (min(pedal[0, 0] - 1, first_off - 1), 0), @@ -290,6 +299,21 @@ def adjust_offsets_w_sustain( offs[pedal_down_at_off] = next_pedal_time[pedal_down_at_off] + # adjust offset times of notes that have a reonset while the sustain pedal is on + pitches = np.array([n["midi_pitch"] for n in notes]) + note_ons = np.array([n["note_on"] for n in notes]) + + for pitch in np.unique(pitches): + pitch_indices = np.where(pitches == pitch)[0] + + sorted_indices = pitch_indices[np.argsort(note_ons[pitch_indices])] + sorted_note_ons = note_ons[sorted_indices] + sorted_sound_offs = offs[sorted_indices] + + adjusted_sound_offs = np.minimum(sorted_sound_offs[:-1], sorted_note_ons[1:]) + + offs[sorted_indices[:-1]] = adjusted_sound_offs + for offset, note in zip(offs, notes): note["sound_off"] = offset diff --git a/partitura/score.py b/partitura/score.py index e882151a..acbd1507 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -8,7 +8,6 @@ object). This object serves as a timeline at which musical elements are registered in terms of their start and end times. """ - from copy import copy, deepcopy from collections import defaultdict from collections.abc import Iterable @@ -22,7 +21,7 @@ import numpy as np import re from scipy.interpolate import PPoly -from typing import Union, List, Optional, Iterator, Iterable as Itertype +from typing import Union, List, Optional, Iterator, Any, Iterable as Itertype import difflib from partitura.utils import ( ComparableMixin, @@ -46,6 +45,7 @@ key_mode_to_int, _OrderedSet, update_note_ids_after_unfolding, + clef_sign_to_int, ) from partitura.utils.generic import interp1d from partitura.utils.music import transpose_note, step2pc @@ -229,6 +229,78 @@ def key_signature_map(self): fill_value="extrapolate", ) + @property + def clef_map(self): + """A function mapping timeline times to the clef in each + staff at that time. The function can take scalar + values or lists/arrays of values + + Returns + ------- + function + The mapping function + """ + clefs = np.array( + [ + ( + c.start.t, + c.staff, + clef_sign_to_int(c.sign), + c.line, + c.octave_change if c.octave_change is not None else 0, + ) + for c in self.iter_all(Clef) + ] + ) + + interpolators = [] + for s in range(1, self.number_of_staves + 1): + staff_clefs = clefs[clefs[:, 1] == s] + if len(staff_clefs) == 0: + # default "none" clef + staff, clef, line, octave_change = s, clef_sign_to_int("none"), 0, 0 + + warnings.warn( + "No clefs found on staff {}, assuming {} clef.".format(s, clef) + ) + if self.first_point is None: + t0, tN = 0, 0 + else: + t0 = self.first_point.t + tN = self.last_point.t + staff_clefs = np.array( + [ + (t0, staff, clef, line, octave_change), + (tN, staff, clef, line, octave_change), + ] + ) + elif len(staff_clefs) == 1: + # If there is only a single clef + staff_clefs = np.array([staff_clefs[0, :], staff_clefs[0, :]]) + + if staff_clefs[0, 0] > self.first_point.t: + staff_clefs = np.vstack( + ((self.first_point.t, *staff_clefs[0, 1:]), staff_clefs) + ) + + interpolators.append( + interp1d( + staff_clefs[:, 0], + staff_clefs[:, 1:], + axis=0, + kind="previous", + bounds_error=False, + fill_value="extrapolate", + ) + ) + + def collator(time: Union[int, np.ndarray]) -> np.ndarray: + return np.array( + [interpolator(time) for interpolator in interpolators], dtype=int + ) + + return collator + @property def measure_map(self): """A function mapping timeline times to the start and end of @@ -1626,6 +1698,11 @@ class GenericNote(TimedObject): appearance of this note (with respect to other notes) in the document in case the Note belongs to a part that was imported from MusicXML. Defaults to None. + stem_direction : str, optional + The stem direction of the note. Can be 'up', 'down', or None. + Defaults to None. + technical: list, optional + Technical notation elements. """ @@ -1638,6 +1715,8 @@ def __init__( articulations=None, ornaments=None, doc_order=None, + stem_direction=None, + technical=None, **kwargs, ): self._sym_dur = None @@ -1648,8 +1727,11 @@ def __init__( self.symbolic_duration = symbolic_duration self.articulations = articulations self.ornaments = ornaments + self.technical = technical self.doc_order = doc_order - + self.stem_direction = ( + stem_direction if stem_direction in ("up", "down") else None + ) # these attributes are set after the instance is constructed self.fermata = None self.tie_prev = None @@ -3102,7 +3184,9 @@ class ChordSymbol(Harmony): """A harmony element in the score usually for Chord Symbols.""" def __init__(self, root, kind, bass=None): - super().__init__(text=root + kind + (f"/{bass}" if bass else "")) + super().__init__( + text=root + (f"/{kind}" if kind else "") + (f"/{bass}" if bass else "") + ) self.kind = kind self.root = root self.bass = bass @@ -3299,6 +3383,27 @@ def reference_tempo(self): return direction +class NoteTechnicalNotation(object): + """ + This object represents technical notations that + are part of a GenericNote object (e.g., fingering,) + These elements depend on a note, but can have their own properties + """ + + def __init__(self, type: str, info: Optional[Any] = None) -> None: + self.type = type + self.info = info + + +class Fingering(NoteTechnicalNotation): + def __init__(self, fingering: int) -> None: + super().__init__( + type="fingering", + info=fingering, + ) + self.fingering = fingering + + class PartGroup(object): """Represents a grouping of several instruments, usually named, and expressed in the score with a group symbol such as a brace or @@ -3769,11 +3874,8 @@ def add_measures(part): if existing_measure.start.t == measure_start: assert existing_measure.end.t > pos pos = existing_measure.end.t - if existing_measure.number != 0: - # if existing_measure is a match anacrusis measure, - # keep number 0 - existing_measure.number = mcounter - mcounter += 1 + existing_measure.number = mcounter + mcounter += 1 continue else: @@ -5359,6 +5461,10 @@ def merge_parts(parts, reassign="voice"): If "voice", the new part have only one staff, and as manually voices as the sum of the voices in parts; the voice number get reassigned. + If "both", we reassign all the staves and voices to have unique staff + and voice numbers. According to musicxml standards, we consider 4 voices + per staff. So for example staff 1 will have voices 1,2,3,4, staff 2 will + have voices 5,6,7,8, and so on. Returns ------- @@ -5367,7 +5473,7 @@ def merge_parts(parts, reassign="voice"): """ # check if reassign has valid values - if reassign not in ["staff", "voice"]: + if reassign not in ["staff", "voice", "auto"]: raise ValueError( "Only 'staff' and 'voice' are supported ressign values. Found", reassign ) @@ -5405,26 +5511,16 @@ def merge_parts(parts, reassign="voice"): new_part._quarter_durations = [lcm] note_arrays = [part.note_array(include_staff=True) for part in parts] - # find the maximum number of voices for each part (voice number start from 1) - maximum_voices = [ - ( - max(note_array["voice"], default=0) - if max(note_array["voice"], default=0) != 0 - else 1 - ) - for note_array in note_arrays - ] - # find the maximum number of staves for each part (staff number start from 0 but we force them to 1) - maximum_staves = [ - ( - max(note_array["staff"], default=0) - if max(note_array["staff"], default=0) != 0 - else 1 - ) - for note_array in note_arrays - ] - - if reassign == "staff": + # find the unique number of voices for each part (voice numbers start from 1) + unique_voices = [np.unique(note_array["voice"]) for note_array in note_arrays] + # find the unique number of staves for each part + unique_staves = [np.unique(note_array["staff"]) for note_array in note_arrays] + # find the maximum number of voices for each part (voice numbers start from 1) + maximum_voices = [max(unique_voice, default=1) for unique_voice in unique_voices] + # find the maximum number of staves for each part + maximum_staves = [max(unique_staff, default=1) for unique_staff in unique_staves] + + if reassign in ["staff", "auto"]: el_to_discard = ( Barline, Page, @@ -5455,6 +5551,31 @@ def merge_parts(parts, reassign="voice"): ) for p_ind, p in enumerate(parts): + if reassign == "auto": + # find how many staves this part has + n_staves = len(unique_staves[p_ind]) + # find total number of staves in previous parts + if p_ind == 0: + n_previous_staves = 0 + else: + n_previous_staves = sum( + [len(unique_staff) for unique_staff in unique_staves[:p_ind]] + ) + # build a mapping between the old staff numbers and the new staff numbers + staff_mapping = dict( + zip( + unique_staves[p_ind], n_previous_staves + np.arange(1, n_staves + 1) + ) + ) + # find how many voices this part has + n_voices = len(unique_voices[p_ind]) + # build a mapping between the old and new voices + voice_mapping = dict( + zip( + unique_voices[p_ind], + n_previous_staves * 4 + np.arange(1, n_voices + 1), + ) + ) for e in p.iter_all(): # full copy the first part and partially copy the others # we don't copy elements like duplicate barlines, clefs or @@ -5474,16 +5595,17 @@ def merge_parts(parts, reassign="voice"): if isinstance(e, GenericNote): e.voice = e.voice + sum(maximum_voices[:p_ind]) elif reassign == "staff": - if isinstance(e, (GenericNote, Words, Direction)): - e.staff = (e.staff if e.staff is not None else 1) + sum( - maximum_staves[:p_ind] - ) - elif isinstance( - e, Clef - ): # TODO: to update if "number" get changed in "staff" + if isinstance(e, (GenericNote, Words, Direction, Clef)): e.staff = (e.staff if e.staff is not None else 1) + sum( maximum_staves[:p_ind] ) + elif reassign == "auto": + # assign based on the voice and staff mappings + if isinstance(e, GenericNote): + # new voice is computed as the sum of voices in staves in previous parts, plus the current + e.voice = voice_mapping[e.voice] + if isinstance(e, (GenericNote, Words, Direction, Clef)): + e.staff = staff_mapping[e.staff] new_part.add(e, start=new_start, end=new_end) # new_part.add(copy.deepcopy(e), start=new_start, end=new_end) diff --git a/partitura/utils/__init__.py b/partitura/utils/__init__.py index 97f07142..b9605316 100644 --- a/partitura/utils/__init__.py +++ b/partitura/utils/__init__.py @@ -37,6 +37,8 @@ pianoroll_to_notearray, match_note_arrays, key_mode_to_int, + clef_sign_to_int, + clef_int_to_sign, remove_silence_from_performed_part, note_array_from_part_list, slice_notearray_by_time, @@ -74,6 +76,8 @@ "key_name_to_fifths_mode", "fifths_mode_to_key_name", "key_mode_to_int", + "clef_sign_to_int", + "clef_int_to_sign", "pitch_spelling_to_midi_pitch", "pitch_spelling_to_note_name", "show_diff", diff --git a/partitura/utils/globals.py b/partitura/utils/globals.py index 3739e0d4..b9e9bbbc 100644 --- a/partitura/utils/globals.py +++ b/partitura/utils/globals.py @@ -192,6 +192,37 @@ {"type": "long", "dots": 3}, ] +# Straight durs do not include copies for naming or dots, when searching they work better for base triplet types in `estimate_symbolic_duration`. +STRAIGHT_DURS = np.array( + [ + 4 / 256, + 4 / 128, + 4 / 64, + 4 / 32, + 4 / 16, + 4 / 8, + 4 / 4, + 4 / 2, + 4 / 1, + 4 / 0.5, + 4 / 0.25, + ] +) + +SYM_STRAIGHT_DURS = [ + {"type": "256th", "dots": 0}, + {"type": "128th", "dots": 0}, + {"type": "64th", "dots": 0}, + {"type": "32nd", "dots": 0}, + {"type": "16th", "dots": 0}, + {"type": "eighth", "dots": 0}, + {"type": "quarter", "dots": 0}, + {"type": "half", "dots": 0}, + {"type": "whole", "dots": 0}, + {"type": "breve", "dots": 0}, + {"type": "long", "dots": 0}, +] + MAJOR_KEYS = [ "Cb", "Gb", @@ -281,14 +312,169 @@ # Standard tuning frequency of A4 in Hz A4 = 440.0 -COMPOSITE_DURS = np.array([1 + 4 / 32, 1 + 4 / 16, 2 + 4 / 32, 2 + 4 / 16, 2 + 4 / 8]) +COMPOSITE_DURS = np.array( + [ + 1 / 4 + 1 / 6, + 1 / 2 + 1 / 12, + 1 / 2 + 1 / 3, + 1 / 2 + 1 / 4 + 1 / 6, + 1 + 1 / 12, + 1 + 1 / 8, + 1 + 1 / 6, + 1 + 1 / 4, + 1 + 1 / 4 + 1 / 6, + 1 + 1 / 2 + 1 / 12, + 1 + 1 / 2 + 1 / 6, + 1 + 1 / 2 + 1 / 3, + 1 + 1 / 2 + 1 / 4 + 1 / 6, + 2 + 1 / 12, + 2 + 1 / 8, + 2 + 1 / 6, + 2 + 1 / 4, + 2 + 1 / 3, + 2 + 1 / 4 + 1 / 6, + 2 + 1 / 2, + 2 + 1 / 2 + 1 / 12, + 2 + 2 / 3, + 2 + 1 / 2 + 1 / 4, + 2 + 1 / 2 + 1 / 3, + 2 + 1 / 2 + 1 / 4 + 1 / 6, + 3 + 1 / 12, + 3 + 1 / 8, + 3 + 1 / 6, + 3 + 1 / 4, + 3 + 1 / 3, + 3 + 1 / 4 + 1 / 6, + 3 + 1 / 2 + 1 / 12, + 3 + 2 / 3, + 3 + 1 / 2 + 1 / 3, + 3 + 1 / 2 + 1 / 4 + 1 / 6, + ] +) SYM_COMPOSITE_DURS = [ + ( + {"type": "16th", "dots": 0}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "eighth", "dots": 0}, + {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "eighth", "dots": 0}, + {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "eighth", "dots": 1}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "quarter", "dots": 0}, + {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), ({"type": "quarter", "dots": 0}, {"type": "32nd", "dots": 0}), + ( + {"type": "quarter", "dots": 0}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), ({"type": "quarter", "dots": 0}, {"type": "16th", "dots": 0}), + ( + {"type": "quarter", "dots": 0}, + {"type": "16th", "dots": 0}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "quarter", "dots": 1}, + {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "quarter", "dots": 1}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "quarter", "dots": 1}, + {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "quarter", "dots": 2}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 0}, + {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), ({"type": "half", "dots": 0}, {"type": "32nd", "dots": 0}), + ( + {"type": "half", "dots": 0}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), ({"type": "half", "dots": 0}, {"type": "16th", "dots": 0}), + ( + {"type": "half", "dots": 0}, + {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 0}, + {"type": "16th", "dots": 0}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0}), + ( + {"type": "half", "dots": 0}, + {"type": "eighth", "dots": 0}, + {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 0}, + {"type": "quarter", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ({"type": "half", "dots": 0}, {"type": "eighth", "dots": 1}), + ( + {"type": "half", "dots": 0}, + {"type": "eighth", "dots": 0}, + {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 0}, + {"type": "eighth", "dots": 1}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 1}, + {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ({"type": "half", "dots": 1}, {"type": "32nd", "dots": 0}), + ( + {"type": "half", "dots": 1}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ({"type": "half", "dots": 1}, {"type": "16th", "dots": 0}), + ( + {"type": "half", "dots": 1}, + {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 1}, + {"type": "16th", "dots": 0}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 2}, + {"type": "32nd", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 1}, + {"type": "quarter", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 2}, + {"type": "eighth", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), + ( + {"type": "half", "dots": 3}, + {"type": "16th", "dots": 0, "actual_notes": 3, "normal_notes": 2}, + ), ] @@ -543,3 +729,15 @@ "vii": (7, "M"), }, } + +# ["G", "F", "C", "percussion", "TAB", "jianpu", "none"] +CLEF_TO_INT = { + "G": 0, + "F": 1, + "C": 2, + "percussion": 3, + "TAB": 4, + "jianpu": 5, + "none": 6, +} +INT_TO_CLEF = {v: k for k, v in CLEF_TO_INT.items()} diff --git a/partitura/utils/music.py b/partitura/utils/music.py index f9678228..777189ec 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -700,6 +700,14 @@ def key_int_to_mode(mode): raise ValueError("Unknown mode {}".format(mode)) +def clef_sign_to_int(clef_sign: str) -> int: + return CLEF_TO_INT[clef_sign] + + +def clef_int_to_sign(clef_int: int) -> str: + return INT_TO_CLEF[clef_int] + + def estimate_symbolic_duration( dur, div, eps=10**-3, return_com_durations=False ) -> Union[Dict[str, Any], Tuple[Dict[str, Any]]]: @@ -762,15 +770,32 @@ def estimate_symbolic_duration( # 2. The duration is a composite duration # For composite duration. We can use the following approach: j = find_nearest(COMPOSITE_DURS, qdur) - if np.abs(qdur - COMPOSITE_DURS[j]) < eps and return_com_durations: - return copy.copy(SYM_COMPOSITE_DURS[j]) + if np.abs(qdur - COMPOSITE_DURS[j]) < eps: + if return_com_durations: + return copy.copy(SYM_COMPOSITE_DURS[j]) + else: + warnings.warn( + f"Quarter duration {qdur} from {dur}/{div} is a composite" + f"duration but composite durations are not allowed. Returning empty symbolic duration." + ) + return {} + # Naive condition to only apply tuplet estimation if the quarter duration is less than a bar (4) + elif qdur > 4: + warnings.warn( + f"Quarter duration {qdur} from {dur}/{div} is not a tuplet or composite duration." + f"Returning empty symbolic duration." + ) + return {} else: + i = np.searchsorted(STRAIGHT_DURS, qdur, side="left") - 1 # NOTE: Guess tuplets (Naive) it doesn't cover composite durations from tied notes. - type = SYM_DURS[i + 3]["type"] + type = SYM_STRAIGHT_DURS[i + 1]["type"] normal_notes = 2 + while (normal_notes * STRAIGHT_DURS[i + 1] / qdur) % 1 > eps: + normal_notes += 1 return { "type": type, - "actual_notes": math.ceil(normal_notes / qdur), + "actual_notes": math.ceil(normal_notes * STRAIGHT_DURS[i + 1] / qdur), "normal_notes": normal_notes, } @@ -1182,6 +1207,9 @@ def _make_pianoroll( onset = note_info[:, 1] duration = note_info[:, 2] + if len(note_info) == 0: + raise ValueError("Note array is empty") + if np.any(duration < 0): raise ValueError("Note durations should be >= 0!") diff --git a/setup.py b/setup.py index 11f9a34f..f1374914 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ EMAIL = "partitura-users@googlegroups.com" AUTHOR = "Maarten Grachten, Carlos Cancino-Chacón, Silvan Peter, Emmanouil Karystinaios, Francesco Foscarin, Thassilo Gadermaier, Patricia Hu" REQUIRES_PYTHON = ">=3.7" -VERSION = "1.5.0" +VERSION = "1.6.0" # What packages are required for this module to be executed? REQUIRED = ["numpy", "scipy", "lxml", "lark-parser", "xmlschema", "mido"] diff --git a/tests/__init__.py b/tests/__init__.py index 32824bcc..8ce30bb0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -112,6 +112,11 @@ for fn in ["test_ts_map_ts_starts_not_at_zero.xml"] ] +CLEF_MAP_TESTFILES = [ + os.path.join(MUSICXML_PATH, fn) + for fn in ["test_clef_map.musicxml"] +] + REST_ARRAY_TESTFILES = [ os.path.join(MUSICXML_PATH, fn) for fn in ["test_unfold_complex.xml", "test_rest.musicxml"] @@ -132,8 +137,11 @@ "test_single_part_change_divs.xml", "test_merge_voices1.xml", "test_merge_voices2.xml", - ] -] + ]] + [ + os.path.join(MEI_PATH, fn) + for fn in [ + "test_merge_voices2.mei", + ]] PIANOROLL_TESTFILES = [ os.path.join(MUSICXML_PATH, fn) @@ -196,6 +204,7 @@ ] KERN_TIES = [os.path.join(KERN_PATH, fn) for fn in ["tie_mismatch.krn"]] + M21_TESTFILES = [ os.path.join(DATA_PATH, "musicxml", fn) for fn in [ @@ -205,6 +214,7 @@ "test_note_ties.xml", ] ] + HARMONY_TESTFILES = [os.path.join(MUSICXML_PATH, fn) for fn in ["test_harmony.musicxml"]] MOZART_VARIATION_FILES = dict( @@ -220,7 +230,6 @@ parangonada_zalign=os.path.join(PARANGONADA_PATH, "mozart_k265_var1", "zalign.csv"), ) - WAV_TESTFILES = [ os.path.join(WAV_PATH, fn) for fn in [ @@ -245,4 +254,14 @@ MIDIINPORT_TESTFILES = [ os.path.join(DATA_PATH, "midi", "bach_midi_score.mid") -] \ No newline at end of file +] + +CLEF_TESTFILES = [ + os.path.join(DATA_PATH, "musicxml", "test_clef.musicxml") +] + +CROSS_STAFF_TESTFILES = [ + os.path.join(DATA_PATH, "musicxml", "test_cross_staff_beaming.musicxml"), + os.path.join(DATA_PATH, MUSICXML_PATH, "test_cross_staff_voices.musicxml"), + os.path.join(DATA_PATH, MEI_PATH, "test_cross_staff_voices.mei"), +] diff --git a/tests/data/match/test_fuer_elise.match b/tests/data/match/test_fuer_elise.match index 4a1aea34..511b2338 100644 --- a/tests/data/match/test_fuer_elise.match +++ b/tests/data/match/test_fuer_elise.match @@ -844,70 +844,70 @@ snote(828,[B,n],4,99:1,0,1/8,291.0,292.0,[voice1,staff1,s])-note(829,[B,n],4,117 snote(829,[E,n],4,99:1,0,1/8,291.0,292.0,[voice2,staff1])-note(831,[E,n],4,117548,117658,118466,24). snote(830,[E,n],3,99:1,0,1/8,291.0,292.0,[voice5,staff2])-note(828,[E,n],3,117496,117777,118466,51). snote(831,[G,#],3,99:1,0,1/8,291.0,292.0,[voice5,staff2])-note(830,[G,#],3,117546,117668,118466,48). -snote(832,[A,n],3,100:1,0,11184811/268435456,294.0,294.3333,[voice1,staff1,s])-note(832,[A,n],3,118744,118860,119820,54). +snote(832,[A,n],3,100:1,0,1/24,294.0,294.3333,[voice1,staff1,s])-note(832,[A,n],3,118744,118860,119820,54). snote(833,[A,n],1,100:1,0,1/8,294.0,295.0,[voice5,staff2])-note(833,[A,n],1,118776,118820,119820,60). -snote(834,[C,n],4,100:1,11184811/268435456,11184811/268435456,294.3333,294.6667,[voice1,staff1,s])-note(834,[C,n],4,118876,118951,119820,59). -snote(835,[E,n],4,100:1,11184811/134217728,11184811/268435456,294.6667,295.0,[voice1,staff1,s])-note(835,[E,n],4,118974,119018,119820,54). -snote(836,[A,n],4,100:2,0,11184811/268435456,295.0,295.3333,[voice1,staff1,s])-note(836,[A,n],4,119152,119227,119820,37). -snote(837,[C,n],5,100:2,11184811/268435456,11184811/268435456,295.3333,295.6667,[voice1,staff1,s])-note(837,[C,n],5,119245,119367,119820,59). -snote(838,[E,n],5,100:2,11184811/134217728,11184811/268435456,295.6667,296.0,[voice1,staff1,s])-note(838,[E,n],5,119371,119536,119820,67). -snote(839,[D,n],5,100:3,0,11184811/268435456,296.0,296.3333,[voice1,staff1,s])-note(839,[D,n],5,119511,119643,119820,67). +snote(834,[C,n],4,100:1,1/24,1/24,294.3333,294.6667,[voice1,staff1,s])-note(834,[C,n],4,118876,118951,119820,59). +snote(835,[E,n],4,100:1,1/12,1/24,294.6667,295.0,[voice1,staff1,s])-note(835,[E,n],4,118974,119018,119820,54). +snote(836,[A,n],4,100:2,0,1/24,295.0,295.3333,[voice1,staff1,s])-note(836,[A,n],4,119152,119227,119820,37). +snote(837,[C,n],5,100:2,1/24,1/24,295.3333,295.6667,[voice1,staff1,s])-note(837,[C,n],5,119245,119367,119820,59). +snote(838,[E,n],5,100:2,1/12,1/24,295.6667,296.0,[voice1,staff1,s])-note(838,[E,n],5,119371,119536,119820,67). +snote(839,[D,n],5,100:3,0,1/24,296.0,296.3333,[voice1,staff1,s])-note(839,[D,n],5,119511,119643,119820,67). snote(840,[A,n],3,100:3,0,1/8,296.0,297.0,[voice5,staff2])-note(841,[A,n],3,119541,119596,119820,63). snote(841,[C,n],4,100:3,0,1/8,296.0,297.0,[voice5,staff2])-note(840,[C,n],4,119534,119609,119820,59). snote(842,[E,n],4,100:3,0,1/8,296.0,297.0,[voice5,staff2])-note(842,[E,n],4,119601,119642,119820,26). -snote(843,[C,n],5,100:3,11184811/268435456,11184811/268435456,296.3333,296.6667,[voice1,staff1,s])-note(843,[C,n],5,119606,119732,119820,64). -snote(844,[B,n],4,100:3,11184811/134217728,11184811/268435456,296.6667,297.0,[voice1,staff1,s])-note(844,[B,n],4,119713,119752,119820,64). -snote(845,[A,n],4,101:1,0,11184811/268435456,297.0,297.3333,[voice1,staff1,s])-note(845,[A,n],4,119862,119975,120886,63). +snote(843,[C,n],5,100:3,1/24,1/24,296.3333,296.6667,[voice1,staff1,s])-note(843,[C,n],5,119606,119732,119820,64). +snote(844,[B,n],4,100:3,1/12,1/24,296.6667,297.0,[voice1,staff1,s])-note(844,[B,n],4,119713,119752,119820,64). +snote(845,[A,n],4,101:1,0,1/24,297.0,297.3333,[voice1,staff1,s])-note(845,[A,n],4,119862,119975,120886,63). snote(846,[A,n],3,101:1,0,1/8,297.0,298.0,[voice5,staff2])-note(846,[A,n],3,119899,120012,120886,59). snote(847,[C,n],4,101:1,0,1/8,297.0,298.0,[voice5,staff2])-note(847,[C,n],4,119909,120022,120886,59). snote(848,[E,n],4,101:1,0,1/8,297.0,298.0,[voice5,staff2])-note(848,[E,n],4,119914,119999,120886,55). -snote(849,[C,n],5,101:1,11184811/268435456,11184811/268435456,297.3333,297.6667,[voice1,staff1,s])-note(849,[C,n],5,119971,120089,120886,69). -snote(850,[E,n],5,101:1,11184811/134217728,11184811/268435456,297.6667,298.0,[voice1,staff1,s])-note(850,[E,n],5,120103,120147,120886,54). -snote(851,[A,n],5,101:2,0,11184811/268435456,298.0,298.3333,[voice1,staff1,s])-note(851,[A,n],5,120259,120320,120886,52). -snote(852,[C,n],6,101:2,11184811/268435456,11184811/268435456,298.3333,298.6667,[voice1,staff1,s])-note(852,[C,n],6,120341,120450,120886,61). -snote(853,[E,n],6,101:2,11184811/134217728,11184811/268435456,298.6667,299.0,[voice1,staff1,s])-note(853,[E,n],6,120460,120629,120886,66). -snote(854,[D,n],6,101:3,0,11184811/268435456,299.0,299.3333,[voice1,staff1,s])-note(854,[D,n],6,120585,120721,120886,63). +snote(849,[C,n],5,101:1,1/24,1/24,297.3333,297.6667,[voice1,staff1,s])-note(849,[C,n],5,119971,120089,120886,69). +snote(850,[E,n],5,101:1,1/12,1/24,297.6667,298.0,[voice1,staff1,s])-note(850,[E,n],5,120103,120147,120886,54). +snote(851,[A,n],5,101:2,0,1/24,298.0,298.3333,[voice1,staff1,s])-note(851,[A,n],5,120259,120320,120886,52). +snote(852,[C,n],6,101:2,1/24,1/24,298.3333,298.6667,[voice1,staff1,s])-note(852,[C,n],6,120341,120450,120886,61). +snote(853,[E,n],6,101:2,1/12,1/24,298.6667,299.0,[voice1,staff1,s])-note(853,[E,n],6,120460,120629,120886,66). +snote(854,[D,n],6,101:3,0,1/24,299.0,299.3333,[voice1,staff1,s])-note(854,[D,n],6,120585,120721,120886,63). snote(855,[A,n],3,101:3,0,1/8,299.0,300.0,[voice5,staff2])-note(855,[A,n],3,120636,120695,120886,62). snote(856,[C,n],4,101:3,0,1/8,299.0,300.0,[voice5,staff2])-note(856,[C,n],4,120639,120692,120886,60). snote(857,[E,n],4,101:3,0,1/8,299.0,300.0,[voice5,staff2])-note(857,[E,n],4,120649,120696,120886,56). -snote(858,[C,n],6,101:3,11184811/268435456,11184811/268435456,299.3333,299.6667,[voice1,staff1,s])-note(858,[C,n],6,120665,120815,120886,59). -snote(859,[B,n],5,101:3,11184811/134217728,11184811/268435456,299.6667,300.0,[voice1,staff1,s])-note(859,[B,n],5,120763,120817,120886,67). -snote(860,[A,n],5,102:1,0,11184811/268435456,300.0,300.3333,[voice1,staff1,s])-note(860,[A,n],5,120919,121042,122009,73). +snote(858,[C,n],6,101:3,1/24,1/24,299.3333,299.6667,[voice1,staff1,s])-note(858,[C,n],6,120665,120815,120886,59). +snote(859,[B,n],5,101:3,1/12,1/24,299.6667,300.0,[voice1,staff1,s])-note(859,[B,n],5,120763,120817,120886,67). +snote(860,[A,n],5,102:1,0,1/24,300.0,300.3333,[voice1,staff1,s])-note(860,[A,n],5,120919,121042,122009,73). snote(861,[A,n],3,102:1,0,1/8,300.0,301.0,[voice5,staff2])-note(861,[A,n],3,120955,121092,122009,69). snote(862,[C,n],4,102:1,0,1/8,300.0,301.0,[voice5,staff2])-note(862,[C,n],4,120958,121087,122009,63). snote(863,[E,n],4,102:1,0,1/8,300.0,301.0,[voice5,staff2])-note(863,[E,n],4,120967,121042,122009,54). -snote(864,[C,n],6,102:1,11184811/268435456,11184811/268435456,300.3333,300.6667,[voice1,staff1,s])-note(864,[C,n],6,121030,121136,122009,69). -snote(865,[E,n],6,102:1,11184811/134217728,11184811/268435456,300.6667,301.0,[voice1,staff1,s])-note(865,[E,n],6,121128,121163,122009,62). -snote(866,[A,n],6,102:2,0,11184811/268435456,301.0,301.3333,[voice1,staff1,s])-note(866,[A,n],6,121285,121390,122009,68). -snote(867,[C,n],7,102:2,11184811/268435456,11184811/268435456,301.3333,301.6667,[voice1,staff1,s])-note(867,[C,n],7,121407,121515,122009,59). -snote(868,[E,n],7,102:2,11184811/134217728,11184811/268435456,301.6667,302.0,[voice1,staff1,s])-note(868,[E,n],7,121521,121731,122009,71). -snote(869,[D,n],7,102:3,0,11184811/268435456,302.0,302.3333,[voice1,staff1,s])-note(870,[D,n],7,121659,121790,122009,62). +snote(864,[C,n],6,102:1,1/24,1/24,300.3333,300.6667,[voice1,staff1,s])-note(864,[C,n],6,121030,121136,122009,69). +snote(865,[E,n],6,102:1,1/12,1/24,300.6667,301.0,[voice1,staff1,s])-note(865,[E,n],6,121128,121163,122009,62). +snote(866,[A,n],6,102:2,0,1/24,301.0,301.3333,[voice1,staff1,s])-note(866,[A,n],6,121285,121390,122009,68). +snote(867,[C,n],7,102:2,1/24,1/24,301.3333,301.6667,[voice1,staff1,s])-note(867,[C,n],7,121407,121515,122009,59). +snote(868,[E,n],7,102:2,1/12,1/24,301.6667,302.0,[voice1,staff1,s])-note(868,[E,n],7,121521,121731,122009,71). +snote(869,[D,n],7,102:3,0,1/24,302.0,302.3333,[voice1,staff1,s])-note(870,[D,n],7,121659,121790,122009,62). snote(870,[A,n],3,102:3,0,1/8,302.0,303.0,[voice5,staff2])-note(869,[A,n],3,121656,121762,122009,60). snote(871,[C,n],4,102:3,0,1/8,302.0,303.0,[voice5,staff2])-note(871,[C,n],4,121665,121771,122009,62). snote(872,[E,n],4,102:3,0,1/8,302.0,303.0,[voice5,staff2])-note(872,[E,n],4,121694,121740,122009,54). -snote(873,[C,n],7,102:3,11184811/268435456,11184811/268435456,302.3333,302.6667,[voice1,staff1,s])-note(873,[C,n],7,121750,121845,122009,62). -snote(874,[B,n],6,102:3,11184811/134217728,11184811/268435456,302.6667,303.0,[voice1,staff1,s])-note(874,[B,n],6,121839,121930,122009,71). -snote(875,[B,b],6,103:1,0,11184811/268435456,303.0,303.3333,[voice1,staff1,s])-note(875,[B,b],6,121990,122083,122083,67). +snote(873,[C,n],7,102:3,1/24,1/24,302.3333,302.6667,[voice1,staff1,s])-note(873,[C,n],7,121750,121845,122009,62). +snote(874,[B,n],6,102:3,1/12,1/24,302.6667,303.0,[voice1,staff1,s])-note(874,[B,n],6,121839,121930,122009,71). +snote(875,[B,b],6,103:1,0,1/24,303.0,303.3333,[voice1,staff1,s])-note(875,[B,b],6,121990,122083,122083,67). snote(876,[A,n],3,103:1,0,1/8,303.0,304.0,[voice5,staff2])-note(877,[A,n],3,122034,122201,124486,65). snote(877,[C,n],4,103:1,0,1/8,303.0,304.0,[voice5,staff2])-note(876,[C,n],4,122020,122200,124486,65). snote(878,[E,n],4,103:1,0,1/8,303.0,304.0,[voice5,staff2])-note(878,[E,n],4,122041,122224,124486,63). -snote(879,[A,n],6,103:1,11184811/268435456,11184811/268435456,303.3333,303.6667,[voice1,staff1,s])-note(879,[A,n],6,122105,122192,124486,68). -snote(880,[G,#],6,103:1,11184811/134217728,11184811/268435456,303.6667,304.0,[voice1,staff1,s])-note(880,[G,#],6,122238,122328,124486,61). -snote(881,[G,n],6,103:2,0,11184811/268435456,304.0,304.3333,[voice1,staff1,s])-note(881,[G,n],6,122334,122435,124486,71). -snote(882,[F,#],6,103:2,11184811/268435456,11184811/268435456,304.3333,304.6667,[voice1,staff1,s])-note(882,[F,#],6,122454,122559,124486,66). -snote(883,[F,n],6,103:2,11184811/134217728,11184811/268435456,304.6667,305.0,[voice1,staff1,s])-note(883,[F,n],6,122557,122599,124486,66). -snote(884,[E,n],6,103:3,0,11184811/268435456,305.0,305.3333,[voice1,staff1,s])-note(884,[E,n],6,122674,122806,124486,64). -snote(885,[D,#],6,103:3,11184811/268435456,11184811/268435456,305.3333,305.6667,[voice1,staff1,s])-note(885,[D,#],6,122799,122888,124486,68). -snote(886,[D,n],6,103:3,11184811/134217728,11184811/268435456,305.6667,306.0,[voice1,staff1,s])-note(886,[D,n],6,122891,122969,124486,63). -snote(887,[C,#],6,104:1,0,11184811/268435456,306.0,306.3333,[voice1,staff1,s])-note(887,[C,#],6,123010,123116,124486,64). -snote(888,[C,n],6,104:1,11184811/268435456,11184811/268435456,306.3333,306.6667,[voice1,staff1,s])-note(888,[C,n],6,123123,123168,124486,69). -snote(889,[B,n],5,104:1,11184811/134217728,11184811/268435456,306.6667,307.0,[voice1,staff1,s])-note(889,[B,n],5,123249,123354,124486,69). -snote(890,[B,b],5,104:2,0,11184811/268435456,307.0,307.3333,[voice1,staff1,s])-note(890,[B,b],5,123350,123444,124486,62). -snote(891,[A,n],5,104:2,11184811/268435456,11184811/268435456,307.3333,307.6667,[voice1,staff1,s])-note(891,[A,n],5,123452,123537,124486,65). -snote(892,[G,#],5,104:2,11184811/134217728,11184811/268435456,307.6667,308.0,[voice1,staff1,s])-note(892,[G,#],5,123577,123667,124486,66). -snote(893,[G,n],5,104:3,0,11184811/268435456,308.0,308.3333,[voice1,staff1,s])-note(893,[G,n],5,123701,123761,124486,62). -snote(894,[F,#],5,104:3,11184811/268435456,11184811/268435456,308.3333,308.6667,[voice1,staff1,s])-note(894,[F,#],5,123781,123906,124486,73). -snote(895,[F,n],5,104:3,11184811/134217728,11184811/268435456,308.6667,309.0,[voice1,staff1,s])-note(895,[F,n],5,123915,123969,124486,67). +snote(879,[A,n],6,103:1,1/24,1/24,303.3333,303.6667,[voice1,staff1,s])-note(879,[A,n],6,122105,122192,124486,68). +snote(880,[G,#],6,103:1,1/12,1/24,303.6667,304.0,[voice1,staff1,s])-note(880,[G,#],6,122238,122328,124486,61). +snote(881,[G,n],6,103:2,0,1/24,304.0,304.3333,[voice1,staff1,s])-note(881,[G,n],6,122334,122435,124486,71). +snote(882,[F,#],6,103:2,1/24,1/24,304.3333,304.6667,[voice1,staff1,s])-note(882,[F,#],6,122454,122559,124486,66). +snote(883,[F,n],6,103:2,1/12,1/24,304.6667,305.0,[voice1,staff1,s])-note(883,[F,n],6,122557,122599,124486,66). +snote(884,[E,n],6,103:3,0,1/24,305.0,305.3333,[voice1,staff1,s])-note(884,[E,n],6,122674,122806,124486,64). +snote(885,[D,#],6,103:3,1/24,1/24,305.3333,305.6667,[voice1,staff1,s])-note(885,[D,#],6,122799,122888,124486,68). +snote(886,[D,n],6,103:3,1/12,1/24,305.6667,306.0,[voice1,staff1,s])-note(886,[D,n],6,122891,122969,124486,63). +snote(887,[C,#],6,104:1,0,1/24,306.0,306.3333,[voice1,staff1,s])-note(887,[C,#],6,123010,123116,124486,64). +snote(888,[C,n],6,104:1,1/24,1/24,306.3333,306.6667,[voice1,staff1,s])-note(888,[C,n],6,123123,123168,124486,69). +snote(889,[B,n],5,104:1,1/12,1/24,306.6667,307.0,[voice1,staff1,s])-note(889,[B,n],5,123249,123354,124486,69). +snote(890,[B,b],5,104:2,0,1/24,307.0,307.3333,[voice1,staff1,s])-note(890,[B,b],5,123350,123444,124486,62). +snote(891,[A,n],5,104:2,1/24,1/24,307.3333,307.6667,[voice1,staff1,s])-note(891,[A,n],5,123452,123537,124486,65). +snote(892,[G,#],5,104:2,1/12,1/24,307.6667,308.0,[voice1,staff1,s])-note(892,[G,#],5,123577,123667,124486,66). +snote(893,[G,n],5,104:3,0,1/24,308.0,308.3333,[voice1,staff1,s])-note(893,[G,n],5,123701,123761,124486,62). +snote(894,[F,#],5,104:3,1/24,1/24,308.3333,308.6667,[voice1,staff1,s])-note(894,[F,#],5,123781,123906,124486,73). +snote(895,[F,n],5,104:3,1/12,1/24,308.6667,309.0,[voice1,staff1,s])-note(895,[F,n],5,123915,123969,124486,67). snote(896,[E,n],5,105:1,0,1/16,309.0,309.5,[voice1,staff1,s])-note(896,[E,n],5,124064,124272,124486,69). snote(897,[D,#],5,105:1,1/16,1/16,309.5,310.0,[voice1,staff1,s])-note(897,[D,#],5,124242,124436,124486,61). snote(898,[E,n],5,105:2,0,1/16,310.0,310.5,[voice1,staff1,s])-note(898,[E,n],5,124440,124614,124614,67). diff --git a/tests/data/mei/test_cross_staff_voices.mei b/tests/data/mei/test_cross_staff_voices.mei new file mode 100644 index 00000000..cc3247cb --- /dev/null +++ b/tests/data/mei/test_cross_staff_voices.mei @@ -0,0 +1,463 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-model href="https://music-encoding.org/schema/5.0/mei-all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?> +<?xml-model href="https://music-encoding.org/schema/5.0/mei-all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?> +<mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="5.0"> + <meiHead xml:id="m7kmp7y"> + <fileDesc xml:id="fj27vth"> + <titleStmt xml:id="trvhoky"> + <title>Untitled score + + Composer / arranger + + + + 2024-10-29 + + + + + + Verovio +

Transcoded from MusicXML

+
+
+
+
+ + + + + + + Untitled score + Subtitle + Composer / arranger + + + + + + Pno. + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
diff --git a/tests/data_examples/test_basic_midi.mid b/tests/data/midi/test_basic_midi.mid similarity index 100% rename from tests/data_examples/test_basic_midi.mid rename to tests/data/midi/test_basic_midi.mid diff --git a/tests/data_examples/Three-Part_Invention_No_13_(fragment).xml b/tests/data/musicxml/Three-Part_Invention_No_13_(fragment).xml similarity index 100% rename from tests/data_examples/Three-Part_Invention_No_13_(fragment).xml rename to tests/data/musicxml/Three-Part_Invention_No_13_(fragment).xml diff --git a/tests/data_examples/test_basic_midi.musicxml b/tests/data/musicxml/test_basic_midi.musicxml similarity index 100% rename from tests/data_examples/test_basic_midi.musicxml rename to tests/data/musicxml/test_basic_midi.musicxml diff --git a/tests/data/musicxml/test_clef.musicxml b/tests/data/musicxml/test_clef.musicxml new file mode 100644 index 00000000..f17890c1 --- /dev/null +++ b/tests/data/musicxml/test_clef.musicxml @@ -0,0 +1,335 @@ + + + + + Clef Test + + + Composer / arranger + + MuseScore 4.4.2 + 2024-10-04 + + + + + + + + + + Piano 1 + Pno. 1 + + Piano + keyboard.piano + + + + 1 + 1 + 78.7402 + 0 + + + + Piano 2 + Pno. 2 + + Piano + keyboard.piano + + + + 2 + 1 + 78.7402 + 0 + + + + + + + 1 + + 0 + + + 2 + + G + 2 + + + F + 4 + + + + + G + 4 + + 4 + 1 + whole + 1 + + + 4 + + + + B + 2 + + 4 + 5 + whole + 2 + + + + + + G + 2 + 1 + + + C + 3 + + + + + G + 5 + + 2 + 1 + half + up + 1 + + + + G + 5 + + 2 + 1 + half + up + 1 + + + 4 + + + + A + 3 + + 4 + 5 + whole + 2 + + + + + + G + 2 + -1 + + + C + 4 + + + + + G + 3 + + 2 + 1 + half + up + 1 + + + + G + 2 + + + + + G + 4 + + 2 + 1 + half + up + 1 + + + 4 + + + + F + 3 + + 4 + 5 + whole + 2 + + + + + + F + 4 + + + + + G + 4 + + 4 + 1 + whole + 1 + + + 4 + + + + B + 2 + + 4 + 5 + whole + 2 + + + light-heavy + + + + + + + 1 + + 0 + + + 2 + + + + 4 + 1 + 1 + + + 4 + + + + 4 + 5 + 2 + + + + + + 4 + 1 + 1 + + + 4 + + + + 4 + 5 + 2 + + + + + + F + 4 + + + G + 2 + + + + + B + 2 + + 4 + 1 + whole + 1 + + + 4 + + + + G + 4 + + 4 + 5 + whole + 2 + + + + + + 4 + 1 + 1 + + + 4 + + + + 4 + 5 + 2 + + + light-heavy + + + + diff --git a/tests/data/musicxml/test_clef_map.musicxml b/tests/data/musicxml/test_clef_map.musicxml new file mode 100644 index 00000000..dd785afc --- /dev/null +++ b/tests/data/musicxml/test_clef_map.musicxml @@ -0,0 +1,301 @@ + + + + + Partition sans titre + + + Compositeur / Arrangeur + + MuseScore 4.4.2 + 2024-10-03 + + + + + + + + + + Piano + Pno. + + Piano + keyboard.piano + + + + 1 + 1 + 78.7402 + 0 + + + + + + + 2 + + 0 + + + 2 + + G + 2 + + + F + 4 + + + + + C + 4 + + 2 + 1 + quarter + up + 1 + + + + G + 2 + -2 + + + + + C + 2 + + 2 + 1 + quarter + up + 1 + + + + C + 2 + + 2 + 1 + quarter + up + 1 + + + + C + 2 + + 2 + 1 + quarter + up + 1 + + + 8 + + + + C + 3 + + 2 + 5 + quarter + up + 2 + + + + C + 3 + + 2 + 5 + quarter + up + 2 + + + + G + 2 + + + + + A + 4 + + 2 + 5 + quarter + up + 2 + + + + A + 4 + + 2 + 5 + quarter + up + 2 + + + + + + G + 2 + 1 + + + F + 4 + + + + + C + 5 + + 2 + 1 + quarter + up + 1 + + + + C + 3 + + + + + D + 3 + + 2 + 1 + quarter + up + 1 + + + + D + 3 + + 1 + 1 + eighth + down + 1 + begin + + + + C + 4 + + + + + B + 2 + + 1 + 1 + eighth + down + 1 + end + + + + B + 2 + + 2 + 1 + quarter + up + 1 + + + 8 + + + + C + 3 + + 2 + 5 + quarter + up + 2 + + + + C + 3 + + 2 + 5 + quarter + up + 2 + + + + F + 3 + + + + + E + 3 + + 2 + 5 + quarter + up + 2 + + + + E + 3 + + 2 + 5 + quarter + up + 2 + + + light-heavy + + + + diff --git a/tests/data/musicxml/test_cross_staff_voices.musicxml b/tests/data/musicxml/test_cross_staff_voices.musicxml new file mode 100644 index 00000000..abab754c --- /dev/null +++ b/tests/data/musicxml/test_cross_staff_voices.musicxml @@ -0,0 +1,806 @@ + + + + + Untitled score + + + Composer / arranger + + MuseScore 4.3.2 + 2024-10-29 + + + + + + + + + + 6.99911 + 40 + + + 1696.94 + 1200.48 + + 85.7252 + 85.7252 + 85.7252 + 85.7252 + + + 85.7252 + 85.7252 + 85.7252 + 85.7252 + + + + 1.8 + 5.5 + 5 + 4.5 + 1 + 1 + 1.1 + 1 + 1.6 + 1.1 + 1.1 + 2.1 + 0.5 + 1.1 + 1 + 2.1 + 0.5 + 1 + 1.2 + 70 + 70 + 49 + + + + + + + title + Untitled score + + + subtitle + Subtitle + + + composer + Composer / arranger + + + + Piano + Pno. + + Piano + keyboard.piano + + + + 1 + 1 + 78.7402 + 0 + + + + + + + + + 50.00 + 0.00 + + 170.00 + + + 65.00 + + + + 2 + + 0 + + + 2 + + G + 2 + + + F + 4 + + + + + B + 4 + + 8 + 1 + whole + 1 + + + 8 + + + + G + 4 + + 2 + 2 + quarter + down + 1 + + + + 2 + 2 + quarter + 1 + + + + 4 + 2 + half + 1 + + + 8 + + + + C + 3 + + 1 + 5 + eighth + up + 2 + begin + + + + G + 3 + + 1 + 5 + eighth + up + 2 + continue + + + + D + 4 + + 1 + 5 + eighth + down + 1 + continue + + + + B + 3 + + 1 + 5 + eighth + up + 2 + end + + + + D + 4 + + 1 + 5 + eighth + down + 1 + begin + + + + E + 4 + + 1 + 5 + eighth + down + 1 + end + + + + 2 + 5 + quarter + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + + -0.00 + 0.00 + + 245.01 + + + 65.00 + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + + -0.00 + 0.00 + + 245.01 + + + 65.00 + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + + + + 8 + 1 + 1 + + + 8 + + + + 8 + 5 + 2 + + + light-heavy + + + + diff --git a/tests/data/musicxml/test_note_features.xml b/tests/data/musicxml/test_note_features.xml index 371ed54d..a206da7e 100644 --- a/tests/data/musicxml/test_note_features.xml +++ b/tests/data/musicxml/test_note_features.xml @@ -60,7 +60,7 @@ - + @@ -138,7 +138,7 @@ - + diff --git a/tests/data/musicxml/test_note_ties.xml b/tests/data/musicxml/test_note_ties.xml index b34cb50c..0803eb96 100644 --- a/tests/data/musicxml/test_note_ties.xml +++ b/tests/data/musicxml/test_note_ties.xml @@ -27,6 +27,7 @@ 15 1 + up quarter diff --git a/tests/data/musicxml/test_unfold_complex_result.xml b/tests/data/musicxml/test_unfold_complex_result.xml index d17c2190..4aa27621 100644 --- a/tests/data/musicxml/test_unfold_complex_result.xml +++ b/tests/data/musicxml/test_unfold_complex_result.xml @@ -57,6 +57,7 @@ 1 1 + up quarter @@ -66,6 +67,7 @@ 1 1 + up quarter @@ -78,6 +80,7 @@ 2 1 + up half @@ -102,6 +105,7 @@ 2 1 + up half @@ -114,6 +118,7 @@ 2 1 + up half @@ -144,6 +149,7 @@ 2 1 + up half @@ -164,6 +170,7 @@ 2 1 + up half @@ -224,6 +231,7 @@ 1 1 + up quarter @@ -233,6 +241,7 @@ 1 1 + up quarter @@ -245,6 +254,7 @@ 2 1 + up half diff --git a/tests/data/musicxml/test_unfold_dacapo_result.xml b/tests/data/musicxml/test_unfold_dacapo_result.xml index 834ed671..343b405f 100644 --- a/tests/data/musicxml/test_unfold_dacapo_result.xml +++ b/tests/data/musicxml/test_unfold_dacapo_result.xml @@ -81,6 +81,7 @@ 2 1 + up half @@ -105,6 +106,7 @@ 2 1 + up half @@ -129,6 +131,7 @@ 2 1 + up half @@ -147,6 +150,7 @@ 2 1 + up half @@ -159,6 +163,7 @@ 2 1 + up half @@ -177,6 +182,7 @@ 2 1 + up half @@ -285,6 +291,7 @@ 2 1 + up half @@ -309,6 +316,7 @@ 2 1 + up half diff --git a/tests/data/musicxml/test_unfold_volta_numbers_result.xml b/tests/data/musicxml/test_unfold_volta_numbers_result.xml index 20fe39e2..72913027 100644 --- a/tests/data/musicxml/test_unfold_volta_numbers_result.xml +++ b/tests/data/musicxml/test_unfold_volta_numbers_result.xml @@ -57,6 +57,7 @@ 1 1 + up quarter @@ -66,6 +67,7 @@ 1 1 + up quarter @@ -78,6 +80,7 @@ 2 1 + up half @@ -102,6 +105,7 @@ 2 1 + up half @@ -114,6 +118,7 @@ 2 1 + up half @@ -144,6 +149,7 @@ 2 1 + up half @@ -164,6 +170,7 @@ 2 1 + up half @@ -200,6 +207,7 @@ 2 1 + up half @@ -224,6 +232,7 @@ 2 1 + up half @@ -236,6 +245,7 @@ 2 1 + up half @@ -260,6 +270,7 @@ 2 1 + up half diff --git a/tests/test_clef.py b/tests/test_clef.py new file mode 100644 index 00000000..10032bea --- /dev/null +++ b/tests/test_clef.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains tests for clef related methods. +""" +import unittest +from tests import ( + CLEF_TESTFILES, + CLEF_MAP_TESTFILES +) +import numpy as np +from partitura import load_musicxml +from partitura.musicanalysis import compute_note_array +from partitura.musicanalysis.note_features import clef_feature +from partitura.score import merge_parts +import partitura + +class TestingClefFeatureExtraction(unittest.TestCase): + def test_clef_feature_exctraction(self): + for fn in CLEF_TESTFILES: + score = load_musicxml(fn, force_note_ids = "keep") + sna1 = compute_note_array(score.parts[0], + feature_functions=["clef_feature"]) + sna2 = compute_note_array(score.parts[1], + feature_functions=["clef_feature"]) + mpart = merge_parts(score.parts, reassign="staff") + sna3 = compute_note_array(mpart, + feature_functions=["clef_feature"]) + + sna1test1 = sna1["clef_feature.clef_sign"] == np.array([1., 0., 2., 0., 0., 2., 0., 0., 1., 0.]) + sna1test2 = sna1["clef_feature.clef_line"] == np.array([4., 2., 3., 2., 2., 4., 2., 2., 4., 2.]) + sna1test3 = sna1["clef_feature.clef_octave_change"] == np.array([0., 0., 0., 1., 1., 0., -1., 0., 0., 0.]) + self.assertTrue(np.all(sna1test1), "clef sign does not match") + self.assertTrue(np.all(sna1test2), "clef line does not match") + self.assertTrue(np.all(sna1test3), "clef octave does not match") + + + sna2test1 = sna2["clef_feature.clef_sign"] == np.array([1., 0.]) + sna2test2 = sna2["clef_feature.clef_line"] == np.array([4., 2.]) + sna2test3 = sna2["clef_feature.clef_octave_change"] == np.array([0., 0.]) + self.assertTrue(np.all(sna2test1), "clef sign does not match") + self.assertTrue(np.all(sna2test2), "clef line does not match") + self.assertTrue(np.all(sna2test3), "clef octave does not match") + + sna3test1 = sna3["clef_feature.clef_sign"] == np.array([1., 0., 2., 0., 0., 1., 2., 0., 0., 0., 1., 0.]) + sna3test2 = sna3["clef_feature.clef_line"] == np.array([4., 2., 3., 2., 2., 4., 4., 2., 2., 2., 4., 2.]) + sna3test3 = sna3["clef_feature.clef_octave_change"] == np.array([0., 0., 0., 1., 1., 0., 0., -1., 0., 0., 0., 0.]) + self.assertTrue(np.all(sna3test1), "clef sign does not match") + self.assertTrue(np.all(sna3test2), "clef line does not match") + self.assertTrue(np.all(sna3test3), "clef octave does not match") + + + + +class TestClefMap(unittest.TestCase): + def test_clef_map(self): + score = load_musicxml(CLEF_MAP_TESTFILES[0]) + for part in score: + # clef = (staff_no, sign_shape, line, octave_shift) + map_fn = part.clef_map + self.assertTrue( + np.all(map_fn(part.first_point.t) == np.array([[1, 0, 2, 0], [2, 1, 4, 0]])) # treble / bass + ) + self.assertTrue( + np.all(map_fn(7) == np.array([[1, 0, 2, -2], [2, 0, 2, 0]])) # treble15vb / treble + ) + self.assertTrue( + np.all(map_fn(8) == np.array([[1, 0, 2, 1], [2, 1, 4, 0]])) # treble8va / bass + ) + self.assertTrue( + np.all(map_fn(11) == np.array([[1, 2, 3, 0], [2, 1, 4, 0]])) # ut3 / bass + ) + self.assertTrue( + np.all(map_fn(12) == np.array([[1, 2, 3, 0], [2, 1, 3, 0]])) # ut3 / bass3 + ) + self.assertTrue( + np.all(map_fn(13) == np.array([[1, 2, 4, 0], [2, 1, 3, 0]])) # ut4 / bass3 + ) + self.assertTrue( + np.all(map_fn(part.last_point.t) == np.array([[1, 2, 4, 0], [2, 1, 3, 0]])) # ut4 / bass3 + ) + + + def test_clef_map_multipart(self): + score = load_musicxml(CLEF_TESTFILES[0]) + p1 = score.parts[0] + p2 = score.parts[1] + + t = np.arange(16) + target_p1_octave_change = np.array([ 0, 0, 0, 0, 1, 1, 1, 1, -1, -1, 0, 0, 0, 0, 0, 0]) + target_p1_line = np.array([4, 4, 4, 4, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4]) + map_fn = p1.clef_map + self.assertTrue(np.all(map_fn(t)[0,:,3] == target_p1_octave_change)) + self.assertTrue(np.all(map_fn(t)[1,:,2] == target_p1_line)) + + target_p2_sign = np.zeros(16) # 16 stepgs G clef, imputed missing clef in the beginning + map_fn = p2.clef_map + self.assertTrue(np.all(map_fn(t)[1,:,1] == target_p2_sign)) + + + p3 = merge_parts(score.parts, reassign="staff") + map_fn = p3.clef_map + self.assertTrue(np.all(map_fn(t)[0,:,3] == target_p1_octave_change)) + self.assertTrue(np.all(map_fn(t)[1,:,2] == target_p1_line)) + self.assertTrue(np.all(map_fn(t)[3,:,1] == target_p2_sign)) diff --git a/tests/test_cross_staff.py b/tests/test_cross_staff.py new file mode 100644 index 00000000..7467255f --- /dev/null +++ b/tests/test_cross_staff.py @@ -0,0 +1,34 @@ +import unittest +from partitura import load_musicxml, load_mei +import numpy as np +from tests import CROSS_STAFF_TESTFILES + + +class CrossStaffBeaming(unittest.TestCase): + def test_cross_staff_single_part_musicxml(self): + score = load_musicxml(CROSS_STAFF_TESTFILES[0]) + note_array = score.note_array(include_staff=True) + expected_staff = np.array([1, 1, 1, 1, 1, 2, 2, 2, 1, 2, 1, 2, 2, 1, 1]) + cross_staff_mask = (note_array["pitch"] > 52) & (note_array["pitch"] < 72) + note_array_staff = note_array[cross_staff_mask]["staff"] + expected_voice = np.ones(len(note_array_staff), dtype=int) + note_array_voice = note_array[cross_staff_mask]["voice"] + self.assertTrue(np.all(note_array_staff == expected_staff)) + self.assertTrue(np.all(note_array_voice == expected_voice)) + +class CrossStaffVoices(unittest.TestCase): + def test_music_xml(self): + score = load_musicxml(CROSS_STAFF_TESTFILES[1]) + note_array = score.note_array(include_staff=True) + expected_staff = [2,1,1,2,1,2,1,1] + expected_voice = [5,2,1,5,5,5,5,5] + self.assertEqual(note_array["staff"].tolist(), expected_staff) + self.assertEqual(note_array["voice"].tolist(), expected_voice) + + def test_mei(self): + score = load_mei(CROSS_STAFF_TESTFILES[2]) + note_array = score.note_array(include_staff=True) + expected_staff = [2,1,1,2,1,2,1,1] + expected_voice = [5,2,1,5,5,5,5,5] + self.assertEqual(note_array["staff"].tolist(), expected_staff) + self.assertEqual(note_array["voice"].tolist(), expected_voice) \ No newline at end of file diff --git a/tests/test_cross_staff_beaming.py b/tests/test_cross_staff_beaming.py deleted file mode 100644 index 1b8ecd8f..00000000 --- a/tests/test_cross_staff_beaming.py +++ /dev/null @@ -1,20 +0,0 @@ -import unittest -import os -from tests import MUSICXML_PATH -from partitura import load_musicxml -import numpy as np - -EXAMPLE_FILE = os.path.join(MUSICXML_PATH, "test_cross_staff_beaming.musicxml") - -class CrossStaffBeaming(unittest.TestCase): - def test_cross_staff_single_part_musicxml(self): - score = load_musicxml(EXAMPLE_FILE) - note_array = score.note_array(include_staff=True) - expected_staff = np.array([1, 1, 1, 1, 1, 2, 2, 2, 1, 2, 1, 2, 2, 1, 1]) - cross_staff_mask = (note_array["pitch"] > 52) & (note_array["pitch"] < 72) - note_array_staff = note_array[cross_staff_mask]["staff"] - expected_voice = np.ones(len(note_array_staff), dtype=int) - note_array_voice = note_array[cross_staff_mask]["voice"] - self.assertTrue(np.all(note_array_staff == expected_staff)) - self.assertTrue(np.all(note_array_voice == expected_voice)) - diff --git a/tests/test_mei.py b/tests/test_mei.py index e5f26b4c..32b03287 100644 --- a/tests/test_mei.py +++ b/tests/test_mei.py @@ -16,6 +16,7 @@ from xmlschema.names import XML_NAMESPACE import os import numpy as np +from partitura.score import merge_parts # class TestSaveMEI(unittest.TestCase): @@ -36,7 +37,8 @@ def test_export_mei_simple(self): ina = import_score.note_array() with TemporaryDirectory() as tmpdir: tmp_mei = os.path.join(tmpdir, "test.mei") - save_mei(import_score, tmp_mei) + merged_mei = merge_parts(import_score.parts, reassign="auto") + save_mei(merged_mei, tmp_mei) export_score = load_mei(tmp_mei) ena = export_score.note_array() self.assertTrue(np.all(ina["onset_beat"] == ena["onset_beat"])) @@ -163,12 +165,12 @@ def test_clef(self): self.assertTrue(clefs2[0].start.t == 0) self.assertTrue(clefs2[0].sign == "C") self.assertTrue(clefs2[0].line == 3) - self.assertTrue(clefs2[0].staff == 1) + self.assertTrue(clefs2[0].staff == 3) self.assertTrue(clefs2[0].octave_change == 0) self.assertTrue(clefs2[1].start.t == 8) self.assertTrue(clefs2[1].sign == "F") self.assertTrue(clefs2[1].line == 4) - self.assertTrue(clefs2[1].staff == 1) + self.assertTrue(clefs2[1].staff == 3) self.assertTrue(clefs2[1].octave_change == 0) # test on part 3 part3 = list(score.iter_parts(part_list))[3] @@ -178,7 +180,7 @@ def test_clef(self): self.assertTrue(clefs3[1].start.t == 4) self.assertTrue(clefs3[1].sign == "G") self.assertTrue(clefs3[1].line == 2) - self.assertTrue(clefs3[1].staff == 1) + self.assertTrue(clefs3[1].staff == 4) self.assertTrue(clefs3[1].octave_change == -1) def test_key_signature1(self): @@ -300,9 +302,8 @@ def test_voice(self): self.assertTrue(np.array_equal(voices, expected_voices)) def test_staff(self): - parts = load_mei(MEI_TESTFILES[15]) - merged_part = score.merge_parts(parts, reassign="staff") - staves = merged_part.note_array(include_staff=True)["staff"] + score = load_mei(MEI_TESTFILES[15]) + staves = score.note_array(include_staff=True)["staff"] expected_staves = [4, 3, 2, 1, 1, 1] self.assertTrue(np.array_equal(staves, expected_staves)) diff --git a/tests/test_merge_parts.py b/tests/test_merge_parts.py index 5496e697..7c9da1c4 100644 --- a/tests/test_merge_parts.py +++ b/tests/test_merge_parts.py @@ -8,7 +8,7 @@ import unittest from pathlib import Path -from partitura import load_musicxml +from partitura import load_musicxml, load_mei from partitura.score import merge_parts, Part, iter_parts from partitura.utils.music import ensure_notearray @@ -100,3 +100,12 @@ def test_reassign_staves2(self): expected_staves = [4, 3, 2, 1, 1, 1] self.assertTrue(note_array["voice"].tolist() == expected_voices) self.assertTrue(note_array["staff"].tolist() == expected_staves) + + def test_reassign_auto(self): + score = load_mei(MERGE_PARTS_TESTFILES[8]) + merged_part = merge_parts(score.parts, reassign="auto") + note_array = merged_part.note_array(include_staff=True) + expected_voices = [13, 9, 5, 2, 1, 1] + expected_staves = [4, 3, 2, 1, 1, 1 ] + self.assertEqual(note_array["voice"].tolist(),expected_voices) + self.assertEqual(note_array["staff"].tolist(),expected_staves) diff --git a/tests/test_metrical_position.py b/tests/test_metrical_position.py index 9d2612fd..742e7cc8 100644 --- a/tests/test_metrical_position.py +++ b/tests/test_metrical_position.py @@ -133,5 +133,7 @@ def test_time_signature_map(self): ) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_midi_import.py b/tests/test_midi_import.py index 0255d38e..9389a34b 100644 --- a/tests/test_midi_import.py +++ b/tests/test_midi_import.py @@ -136,7 +136,7 @@ def make_triplets_example_2(): fill_track(track, notes, divs) # target: actual_notes = [5] * 5 + [3] * 3 - normal_notes = [2] * 5 + [2] * 3 + normal_notes = [4] * 5 + [2] * 3 return mid, actual_notes, normal_notes diff --git a/tests/test_note_features.py b/tests/test_note_features.py index 891a254b..660823bc 100644 --- a/tests/test_note_features.py +++ b/tests/test_note_features.py @@ -18,7 +18,7 @@ class TestingNoteFeatureExtraction(unittest.TestCase): def test_metrical_basis(self): for fn in METRICAL_POSITION_TESTFILES: - score = load_musicxml(fn) + score = load_musicxml(fn, force_note_ids = "keep") make_note_feats(score[0], ["metrical_feature"]) def test_grace_basis(self): @@ -59,6 +59,22 @@ def test_slur_grace_art_dyn_orn(self): self.assertTrue(np.all(dyntest), "forte feature does not match") self.assertTrue(np.all(slurtest), "slur feature does not match") + def test_measure_feature(self): + for fn in MUSICXML_NOTE_FEATURES: + score = load_musicxml(fn, force_note_ids=True) + feats = [ + "measure_feature" + ] + na = compute_note_array(score[0], feature_functions=feats) + + numtest = na["measure_feature.measure_number"] == np.array([1, 1, 1, 2, 2, 2]) + starttest = na["measure_feature.measure_start_beat"] == np.array([0, 0, 0, 4, 4, 4]) + endtest = na["measure_feature.measure_end_beat"] == np.array([4, 4, 4, 8, 8, 8]) + self.assertTrue(np.all(numtest), "measure number feature does not match") + self.assertTrue(np.all(starttest), "measure start feature does not match") + self.assertTrue(np.all(endtest), "measure end feature does not match") + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_performance_features.py b/tests/test_performance_features.py index e0c4723a..566ba105 100644 --- a/tests/test_performance_features.py +++ b/tests/test_performance_features.py @@ -11,32 +11,36 @@ import os - class TestPerformanceFeatures(unittest.TestCase): def test_performance_features(self): - fields = ['id','pedal_feature.onset_value','pedal_feature.offset_value','pedal_feature.to_prev_release', - 'pedal_feature.to_next_release','onset','duration', 'pitch', 'p_onset', 'p_duration','velocity', 'beat_period'] - True_array = np.array([('n1', 0.23374297, 89.74999 , 62.000057, 0., 0.16015087, -0.5, 0.5 , 59, 4.9925 , 0.8775 , 44, 1.4700003), - ('n4', 0.03011051, 114.25004 , 61.000244, 0., 0.4027142 , 0. , 1. , 40, 5.7025 , 2.4375 , 22, 2.8474998), - ('n3', 2.527984 , 87.500046, 61.000244, 0., 0.4027142 , 0. , 0.25, 56, 5.77625, 2.36375, 26, 2.8474998)], - dtype=[('id', '