partitura 1.5.0__tar.gz → 1.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {partitura-1.5.0 → partitura-1.6.0}/PKG-INFO +13 -2
- partitura-1.6.0/partitura/assets/score_example.mei +69 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/__init__.py +36 -8
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/exportkern.py +1 -1
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/exportmatch.py +7 -2
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/exportmei.py +35 -8
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/exportmusicxml.py +55 -10
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importkern.py +195 -79
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importmatch.py +89 -87
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importmei.py +48 -12
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importmidi.py +71 -20
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importmusicxml.py +173 -10
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importnakamura.py +1 -1
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/matchfile_utils.py +2 -1
- {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/note_array_to_score.py +1 -1
- {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/note_features.py +138 -8
- {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/performance_codec.py +33 -22
- {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/performance_features.py +1 -1
- {partitura-1.5.0 → partitura-1.6.0}/partitura/performance.py +31 -7
- {partitura-1.5.0 → partitura-1.6.0}/partitura/score.py +495 -301
- {partitura-1.5.0 → partitura-1.6.0}/partitura/utils/__init__.py +4 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/utils/generic.py +34 -2
- {partitura-1.5.0 → partitura-1.6.0}/partitura/utils/globals.py +200 -2
- {partitura-1.5.0 → partitura-1.6.0}/partitura/utils/misc.py +22 -1
- {partitura-1.5.0 → partitura-1.6.0}/partitura/utils/music.py +33 -5
- {partitura-1.5.0 → partitura-1.6.0}/partitura.egg-info/PKG-INFO +13 -2
- {partitura-1.5.0 → partitura-1.6.0}/partitura.egg-info/SOURCES.txt +3 -1
- {partitura-1.5.0 → partitura-1.6.0}/setup.py +1 -1
- partitura-1.6.0/tests/test_clef.py +105 -0
- partitura-1.6.0/tests/test_cross_staff.py +34 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_mei.py +8 -7
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_merge_parts.py +10 -1
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_metrical_position.py +2 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_midi_import.py +60 -2
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_note_features.py +17 -1
- partitura-1.6.0/tests/test_performance.py +332 -0
- partitura-1.6.0/tests/test_performance_features.py +46 -0
- partitura-1.6.0/tests/test_urlload.py +26 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_utils.py +66 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_xml.py +64 -0
- partitura-1.5.0/partitura/assets/score_example.mei +0 -56
- partitura-1.5.0/tests/test_cross_staff_beaming.py +0 -20
- partitura-1.5.0/tests/test_performance.py +0 -140
- partitura-1.5.0/tests/test_performance_features.py +0 -42
- {partitura-1.5.0 → partitura-1.6.0}/LICENSE +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/README.md +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/__init__.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/assets/musicxml.xsd +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/assets/score_example.krn +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/assets/score_example.mid +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/assets/score_example.musicxml +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/directions.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/display.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/exportaudio.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/exportmidi.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/exportparangonada.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importdcml.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importmusic21.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importparangonada.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/matchfile_base.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/matchlines_v0.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/matchlines_v1.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/io/musescore.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/__init__.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/key_identification.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/meter.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/pitch_spelling.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/tonal_tension.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/voice_separation.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/utils/fluidsynth.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/utils/normalize.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura/utils/synth.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura.egg-info/dependency_links.txt +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura.egg-info/requires.txt +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/partitura.egg-info/top_level.txt +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/setup.cfg +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_dcml_import.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_deprecations.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_display.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_fluidsynth.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_harmony.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_kern.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_key_estimation.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_load_performance.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_load_score.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_m21_import.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_match_export.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_match_import.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_midi_export.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_musescore.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_nakamura.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_new_divs.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_note_array.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_octave_shift.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_parangonada.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_part_properties.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_partial_measures.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_performance_codec.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_pianoroll.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_pitch_spelling.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_quarter_adjust.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_rest_array.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_synth.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_time_estimation.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_times.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_tonal_tension.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_transpose.py +0 -0
- {partitura-1.5.0 → partitura-1.6.0}/tests/test_voice_estimation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: partitura
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: A package for handling symbolic musical information
|
|
5
5
|
Home-page: https://github.com/CPJKU/partitura
|
|
6
6
|
Author: Maarten Grachten, Carlos Cancino-Chacón, Silvan Peter, Emmanouil Karystinaios, Francesco Foscarin, Thassilo Gadermaier, Patricia Hu
|
|
@@ -21,6 +21,17 @@ Requires-Dist: lxml
|
|
|
21
21
|
Requires-Dist: lark-parser
|
|
22
22
|
Requires-Dist: xmlschema
|
|
23
23
|
Requires-Dist: mido
|
|
24
|
+
Dynamic: author
|
|
25
|
+
Dynamic: author-email
|
|
26
|
+
Dynamic: classifier
|
|
27
|
+
Dynamic: description
|
|
28
|
+
Dynamic: description-content-type
|
|
29
|
+
Dynamic: home-page
|
|
30
|
+
Dynamic: keywords
|
|
31
|
+
Dynamic: license
|
|
32
|
+
Dynamic: requires-dist
|
|
33
|
+
Dynamic: requires-python
|
|
34
|
+
Dynamic: summary
|
|
24
35
|
|
|
25
36
|
|
|
26
37
|
[//]: # (<p align="center"> )
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<?xml-model href="https://music-encoding.org/schema/5.0/mei-all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?>
|
|
3
|
+
<?xml-model href="https://music-encoding.org/schema/5.0/mei-all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?>
|
|
4
|
+
<mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="5.0">
|
|
5
|
+
<meiHead xml:id="m21agkk">
|
|
6
|
+
<fileDesc xml:id="f1x9ud6p">
|
|
7
|
+
<titleStmt xml:id="t1pe4slb">
|
|
8
|
+
<title />
|
|
9
|
+
<respStmt />
|
|
10
|
+
</titleStmt>
|
|
11
|
+
<pubStmt xml:id="p1kxcrok">
|
|
12
|
+
<date isodate="2024-10-30" type="encoding-date">2024-10-30</date>
|
|
13
|
+
</pubStmt>
|
|
14
|
+
</fileDesc>
|
|
15
|
+
<encodingDesc xml:id="e1xr2zpp">
|
|
16
|
+
<appInfo xml:id="a1wyxc5n">
|
|
17
|
+
<application xml:id="a1t43ge2" isodate="2024-10-30T10:56:32" version="4.3.1-3b8cc17">
|
|
18
|
+
<name xml:id="n1cp60l4">Verovio</name>
|
|
19
|
+
<p xml:id="p13nndma">Transcoded from MusicXML</p>
|
|
20
|
+
</application>
|
|
21
|
+
</appInfo>
|
|
22
|
+
</encodingDesc>
|
|
23
|
+
</meiHead>
|
|
24
|
+
<music>
|
|
25
|
+
<body>
|
|
26
|
+
<mdiv xml:id="muo97v6">
|
|
27
|
+
<score xml:id="suneqlv">
|
|
28
|
+
<scoreDef xml:id="sz00r05">
|
|
29
|
+
<staffGrp xml:id="s1h35kps">
|
|
30
|
+
<staffGrp xml:id="P1" bar.thru="true">
|
|
31
|
+
<grpSym xml:id="g1eji31e" symbol="brace" />
|
|
32
|
+
<label xml:id="l8lirjj">Piano</label>
|
|
33
|
+
<instrDef xml:id="iggd40h" midi.channel="0" midi.instrnum="0" midi.volume="78.00%" />
|
|
34
|
+
<staffDef xml:id="sbpks8p" n="1" lines="5" ppq="1">
|
|
35
|
+
<clef xml:id="c1p7nrnw" shape="G" line="2" />
|
|
36
|
+
<keySig xml:id="ki5anqv" sig="0" />
|
|
37
|
+
<meterSig xml:id="m1vpw2w2" count="4" unit="4" />
|
|
38
|
+
</staffDef>
|
|
39
|
+
<staffDef xml:id="s1g416nz" n="2" lines="5" ppq="1">
|
|
40
|
+
<clef xml:id="cezkswz" shape="G" line="2" />
|
|
41
|
+
<keySig xml:id="kk41xfz" sig="0" />
|
|
42
|
+
<meterSig xml:id="m7alo7s" count="4" unit="4" />
|
|
43
|
+
</staffDef>
|
|
44
|
+
</staffGrp>
|
|
45
|
+
</staffGrp>
|
|
46
|
+
</scoreDef>
|
|
47
|
+
<section xml:id="suu4o7p">
|
|
48
|
+
<measure xml:id="m1e358qx" n="1">
|
|
49
|
+
<staff xml:id="s1ufvigy" n="1">
|
|
50
|
+
<layer xml:id="l1r7cvga" n="1">
|
|
51
|
+
<rest xml:id="ro1o7cb" dur.ppq="2" dur="2" />
|
|
52
|
+
<chord xml:id="c1c1r6b7" dur.ppq="2" dur="2" stem.dir="down">
|
|
53
|
+
<note xml:id="n6dpu2p" oct="5" pname="c" />
|
|
54
|
+
<note xml:id="njfgcwp" oct="5" pname="e" />
|
|
55
|
+
</chord>
|
|
56
|
+
</layer>
|
|
57
|
+
</staff>
|
|
58
|
+
<staff xml:id="s710zw2" n="2">
|
|
59
|
+
<layer xml:id="leffrs2" n="5">
|
|
60
|
+
<note xml:id="n1txt37q" dur.ppq="4" dur="1" oct="4" pname="a" />
|
|
61
|
+
</layer>
|
|
62
|
+
</staff>
|
|
63
|
+
</measure>
|
|
64
|
+
</section>
|
|
65
|
+
</score>
|
|
66
|
+
</mdiv>
|
|
67
|
+
</body>
|
|
68
|
+
</music>
|
|
69
|
+
</mei>
|
|
@@ -5,7 +5,9 @@ This module contains methods for importing and exporting symbolic music formats.
|
|
|
5
5
|
"""
|
|
6
6
|
from typing import Union
|
|
7
7
|
import os
|
|
8
|
-
|
|
8
|
+
import urllib.request
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
import tempfile
|
|
9
11
|
from .importmusicxml import load_musicxml
|
|
10
12
|
from .importmidi import load_score_midi, load_performance_midi
|
|
11
13
|
from .musescore import load_via_musescore
|
|
@@ -32,6 +34,14 @@ class NotSupportedFormatError(Exception):
|
|
|
32
34
|
pass
|
|
33
35
|
|
|
34
36
|
|
|
37
|
+
def is_url(input):
|
|
38
|
+
try:
|
|
39
|
+
result = urlparse(input)
|
|
40
|
+
return all([result.scheme, result.netloc])
|
|
41
|
+
except ValueError:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
35
45
|
@deprecated_alias(score_fn="filename")
|
|
36
46
|
@deprecated_parameter("ensure_list")
|
|
37
47
|
def load_score(filename: PathLike, force_note_ids="keep") -> Score:
|
|
@@ -57,11 +67,29 @@ def load_score(filename: PathLike, force_note_ids="keep") -> Score:
|
|
|
57
67
|
scr: :class:`partitura.score.Score`
|
|
58
68
|
A score instance.
|
|
59
69
|
"""
|
|
70
|
+
if is_url(filename):
|
|
71
|
+
url = filename
|
|
72
|
+
# Send a GET request to the URL
|
|
73
|
+
with urllib.request.urlopen(url) as response:
|
|
74
|
+
data = response.read()
|
|
75
|
+
|
|
76
|
+
# Extract the file extension from the URL
|
|
77
|
+
extension = os.path.splitext(url)[-1]
|
|
78
|
+
|
|
79
|
+
# Create a temporary file
|
|
80
|
+
temp_file = tempfile.NamedTemporaryFile(suffix=extension, delete=True)
|
|
81
|
+
|
|
82
|
+
# Write the content to the temporary file
|
|
83
|
+
with open(temp_file.name, "wb") as f:
|
|
84
|
+
f.write(data)
|
|
85
|
+
|
|
86
|
+
filename = temp_file.name
|
|
87
|
+
else:
|
|
88
|
+
extension = os.path.splitext(filename)[-1].lower()
|
|
60
89
|
|
|
61
|
-
extension = os.path.splitext(filename)[-1].lower()
|
|
62
90
|
if extension in (".mxl", ".xml", ".musicxml"):
|
|
63
91
|
# Load MusicXML
|
|
64
|
-
|
|
92
|
+
score = load_musicxml(
|
|
65
93
|
filename=filename,
|
|
66
94
|
force_note_ids=force_note_ids,
|
|
67
95
|
)
|
|
@@ -71,15 +99,15 @@ def load_score(filename: PathLike, force_note_ids="keep") -> Score:
|
|
|
71
99
|
assign_note_ids = False
|
|
72
100
|
else:
|
|
73
101
|
assign_note_ids = True
|
|
74
|
-
|
|
102
|
+
score = load_score_midi(
|
|
75
103
|
filename=filename,
|
|
76
104
|
assign_note_ids=assign_note_ids,
|
|
77
105
|
)
|
|
78
106
|
elif extension in [".mei"]:
|
|
79
107
|
# Load MEI
|
|
80
|
-
|
|
108
|
+
score = load_mei(filename=filename)
|
|
81
109
|
elif extension in [".kern", ".krn"]:
|
|
82
|
-
|
|
110
|
+
score = load_kern(
|
|
83
111
|
filename=filename,
|
|
84
112
|
force_note_ids=force_note_ids,
|
|
85
113
|
)
|
|
@@ -107,7 +135,7 @@ def load_score(filename: PathLike, force_note_ids="keep") -> Score:
|
|
|
107
135
|
".gp",
|
|
108
136
|
]:
|
|
109
137
|
# Load MuseScore
|
|
110
|
-
|
|
138
|
+
score = load_via_musescore(
|
|
111
139
|
filename=filename,
|
|
112
140
|
force_note_ids=force_note_ids,
|
|
113
141
|
)
|
|
@@ -117,11 +145,11 @@ def load_score(filename: PathLike, force_note_ids="keep") -> Score:
|
|
|
117
145
|
filename=filename,
|
|
118
146
|
create_score=True,
|
|
119
147
|
)
|
|
120
|
-
return score
|
|
121
148
|
else:
|
|
122
149
|
raise NotSupportedFormatError(
|
|
123
150
|
f"{extension} file extension is not supported. If this should be supported, consider editing partitura/io/__init__.py file"
|
|
124
151
|
)
|
|
152
|
+
return score
|
|
125
153
|
|
|
126
154
|
|
|
127
155
|
def load_score_as_part(filename: PathLike) -> Part:
|
|
@@ -327,7 +327,7 @@ def save_kern(
|
|
|
327
327
|
out_data = exporter.parse()
|
|
328
328
|
out_data = exporter.trim(out_data)
|
|
329
329
|
# Use numpy savetxt to save the file
|
|
330
|
-
footer = "Encoded using the Partitura Python package, version 1.
|
|
330
|
+
footer = "Encoded using the Partitura Python package, version 1.6.0"
|
|
331
331
|
if out is not None:
|
|
332
332
|
np.savetxt(
|
|
333
333
|
fname=out,
|
|
@@ -21,7 +21,6 @@ from partitura.performance import Performance, PerformedPart, PerformanceLike
|
|
|
21
21
|
from partitura.io.matchlines_v1 import (
|
|
22
22
|
make_info,
|
|
23
23
|
make_scoreprop,
|
|
24
|
-
make_section,
|
|
25
24
|
MatchSnote,
|
|
26
25
|
MatchNote,
|
|
27
26
|
MatchSnoteNote,
|
|
@@ -313,6 +312,7 @@ def matchfile_from_alignment(
|
|
|
313
312
|
staff = getattr(snote, "staff", None)
|
|
314
313
|
ornaments = getattr(snote, "ornaments", None)
|
|
315
314
|
fermata = getattr(snote, "fermata", None)
|
|
315
|
+
technical = getattr(snote, "technical", None)
|
|
316
316
|
|
|
317
317
|
if voice is not None:
|
|
318
318
|
score_attributes_list.append(f"v{voice}")
|
|
@@ -329,6 +329,11 @@ def matchfile_from_alignment(
|
|
|
329
329
|
if fermata is not None:
|
|
330
330
|
score_attributes_list.append("fermata")
|
|
331
331
|
|
|
332
|
+
if technical is not None:
|
|
333
|
+
for tech_el in technical:
|
|
334
|
+
if isinstance(tech_el, score.Fingering):
|
|
335
|
+
score_attributes_list.append(f"fingering{tech_el.fingering}")
|
|
336
|
+
|
|
332
337
|
if isinstance(snote, score.GraceNote):
|
|
333
338
|
score_attributes_list.append("grace")
|
|
334
339
|
|
|
@@ -417,7 +422,7 @@ def matchfile_from_alignment(
|
|
|
417
422
|
duplicates[tuple(v)] = idx
|
|
418
423
|
voice_overlap_note_ids = []
|
|
419
424
|
if len(duplicates) > 0:
|
|
420
|
-
duplicate_idx = np.
|
|
425
|
+
duplicate_idx = np.hstack(list(duplicates.values()))
|
|
421
426
|
voice_overlap_note_ids = list(sna[duplicate_idx]["id"])
|
|
422
427
|
|
|
423
428
|
for al_note in alignment:
|
|
@@ -6,6 +6,7 @@ This module contains methods for exporting MEI files.
|
|
|
6
6
|
import math
|
|
7
7
|
from collections import defaultdict
|
|
8
8
|
from lxml import etree
|
|
9
|
+
import lxml
|
|
9
10
|
import partitura.score as spt
|
|
10
11
|
from operator import itemgetter
|
|
11
12
|
from itertools import groupby
|
|
@@ -225,6 +226,7 @@ class MEIExporter:
|
|
|
225
226
|
self._handle_harmony(measure_el, start=measure.start.t, end=measure.end.t)
|
|
226
227
|
self._handle_fermata(measure_el, start=measure.start.t, end=measure.end.t)
|
|
227
228
|
self._handle_barline(measure_el, start=measure.start.t, end=measure.end.t)
|
|
229
|
+
self._handle_fingering(measure_el, start=measure.start.t, end=measure.end.t)
|
|
228
230
|
return measure_el
|
|
229
231
|
|
|
230
232
|
def _handle_chord(self, chord, xml_voice_el):
|
|
@@ -277,6 +279,9 @@ class MEIExporter:
|
|
|
277
279
|
elif note.tie_prev is not None:
|
|
278
280
|
note_el.set("tie", "t")
|
|
279
281
|
|
|
282
|
+
if note.stem_direction in ["up", "down"]:
|
|
283
|
+
note_el.set("stem.dir", note.stem_direction)
|
|
284
|
+
|
|
280
285
|
if note.alter is not None:
|
|
281
286
|
if (
|
|
282
287
|
note.step.lower() + ALTER_TO_MEI[note.alter]
|
|
@@ -292,7 +297,7 @@ class MEIExporter:
|
|
|
292
297
|
note_el.set("grace", "acc")
|
|
293
298
|
return duration
|
|
294
299
|
|
|
295
|
-
def _handle_tuplets(self, measure_el, start, end):
|
|
300
|
+
def _handle_tuplets(self, measure_el: lxml.etree._Element, start: int, end: int):
|
|
296
301
|
for tuplet in self.part.iter_all(spt.Tuplet, start=start, end=end):
|
|
297
302
|
start_note = tuplet.start_note
|
|
298
303
|
end_note = tuplet.end_note
|
|
@@ -349,7 +354,7 @@ class MEIExporter:
|
|
|
349
354
|
for el in xml_el_within_tuplet:
|
|
350
355
|
tuplet_el.append(el)
|
|
351
356
|
|
|
352
|
-
def _handle_beams(self, measure_el, start, end):
|
|
357
|
+
def _handle_beams(self, measure_el: lxml.etree._Element, start: int, end: int):
|
|
353
358
|
for beam in self.part.iter_all(spt.Beam, start=start, end=end):
|
|
354
359
|
# If the beam has only one note, skip it
|
|
355
360
|
if len(beam.notes) < 2:
|
|
@@ -397,7 +402,9 @@ class MEIExporter:
|
|
|
397
402
|
if note_el.getparent() != beam_el:
|
|
398
403
|
beam_el.append(note_el)
|
|
399
404
|
|
|
400
|
-
def _handle_clef_changes(
|
|
405
|
+
def _handle_clef_changes(
|
|
406
|
+
self, measure_el: lxml.etree._Element, start: int, end: int
|
|
407
|
+
):
|
|
401
408
|
for clef in self.part.iter_all(spt.Clef, start=start, end=end):
|
|
402
409
|
# Clef element is parent of the note element
|
|
403
410
|
if clef.start.t == 0:
|
|
@@ -418,7 +425,7 @@ class MEIExporter:
|
|
|
418
425
|
clef_el.set("shape", str(clef.sign))
|
|
419
426
|
clef_el.set("line", str(clef.line))
|
|
420
427
|
|
|
421
|
-
def _handle_ks_changes(self, measure_el, start, end):
|
|
428
|
+
def _handle_ks_changes(self, measure_el: lxml.etree._Element, start: int, end: int):
|
|
422
429
|
# For key signature changes, we add a new scoreDef element at the beginning of the measure
|
|
423
430
|
# and add the key signature element as attributes of the scoreDef element
|
|
424
431
|
for key_sig in self.part.iter_all(spt.KeySignature, start=start, end=end):
|
|
@@ -448,7 +455,7 @@ class MEIExporter:
|
|
|
448
455
|
parent = measure_el.getparent()
|
|
449
456
|
parent.insert(parent.index(measure_el), score_def_el)
|
|
450
457
|
|
|
451
|
-
def _handle_ts_changes(self, measure_el, start, end):
|
|
458
|
+
def _handle_ts_changes(self, measure_el: lxml.etree._Element, start: int, end: int):
|
|
452
459
|
# For key signature changes, we add a new scoreDef element at the beginning of the measure
|
|
453
460
|
# and add the key signature element as attributes of the scoreDef element
|
|
454
461
|
for time_sig in self.part.iter_all(spt.TimeSignature, start=start, end=end):
|
|
@@ -464,7 +471,7 @@ class MEIExporter:
|
|
|
464
471
|
score_def_el.set("count", str(time_sig.beats))
|
|
465
472
|
score_def_el.set("unit", str(time_sig.beat_type))
|
|
466
473
|
|
|
467
|
-
def _handle_harmony(self, measure_el, start, end):
|
|
474
|
+
def _handle_harmony(self, measure_el: lxml.etree._Element, start: int, end: int):
|
|
468
475
|
"""
|
|
469
476
|
For harmonies we add a new harm element at the beginning of the measure.
|
|
470
477
|
The position doesn't really matter since the tstamp attribute will place it correctly
|
|
@@ -505,7 +512,7 @@ class MEIExporter:
|
|
|
505
512
|
# text is a child element of harmony but not a xml element
|
|
506
513
|
harm_el.text = "|" + harmony.text
|
|
507
514
|
|
|
508
|
-
def _handle_fermata(self, measure_el, start, end):
|
|
515
|
+
def _handle_fermata(self, measure_el: lxml.etree._Element, start: int, end: int):
|
|
509
516
|
for fermata in self.part.iter_all(spt.Fermata, start=start, end=end):
|
|
510
517
|
if fermata.ref is not None:
|
|
511
518
|
note = fermata.ref
|
|
@@ -524,7 +531,7 @@ class MEIExporter:
|
|
|
524
531
|
# Set the fermata to be above the staff (the highest staff)
|
|
525
532
|
fermata_el.set("staff", "1")
|
|
526
533
|
|
|
527
|
-
def _handle_barline(self, measure_el, start, end):
|
|
534
|
+
def _handle_barline(self, measure_el: lxml.etree._Element, start: int, end: int):
|
|
528
535
|
for end_barline in self.part.iter_all(
|
|
529
536
|
spt.Ending, start=end, end=end + 1, mode="ending"
|
|
530
537
|
):
|
|
@@ -543,6 +550,26 @@ class MEIExporter:
|
|
|
543
550
|
):
|
|
544
551
|
measure_el.set("left", "rptstart")
|
|
545
552
|
|
|
553
|
+
def _handle_fingering(self, measure_el: lxml.etree._Element, start: int, end: int):
|
|
554
|
+
"""
|
|
555
|
+
For fingering we add a new fing element at the end of the measure.
|
|
556
|
+
The position doesn't really matter since the startid attribute will place it correctly
|
|
557
|
+
"""
|
|
558
|
+
for note in self.part.iter_all(spt.Note, start=start, end=end):
|
|
559
|
+
if note.technical is not None:
|
|
560
|
+
for technical_notation in note.technical:
|
|
561
|
+
if (
|
|
562
|
+
isinstance(technical_notation, score.Fingering)
|
|
563
|
+
and note.id is not None
|
|
564
|
+
):
|
|
565
|
+
fing_el = etree.SubElement(measure_el, "fing")
|
|
566
|
+
fing_el.set(XMLNS_ID, "fing-" + self.elc_id())
|
|
567
|
+
fing_el.set("startid", note.id)
|
|
568
|
+
# Naive way to place the fingering notation
|
|
569
|
+
fing_el.set("place", ("above" if note.staff == 1 else "below"))
|
|
570
|
+
# text is a child element of fingering but not a xml element
|
|
571
|
+
fing_el.text = technical_notation.fingering
|
|
572
|
+
|
|
546
573
|
|
|
547
574
|
@deprecated_alias(parts="score_data")
|
|
548
575
|
def save_mei(
|
|
@@ -137,6 +137,10 @@ def make_note_el(note, dur, voice, counter, n_of_staves):
|
|
|
137
137
|
if voice not in (None, 0):
|
|
138
138
|
etree.SubElement(note_e, "voice").text = "{}".format(voice)
|
|
139
139
|
|
|
140
|
+
if note.stem_direction is not None:
|
|
141
|
+
stem_e = etree.SubElement(note_e, "stem")
|
|
142
|
+
stem_e.text = note.stem_direction
|
|
143
|
+
|
|
140
144
|
if note.fermata is not None:
|
|
141
145
|
notations.append(etree.Element("fermata"))
|
|
142
146
|
|
|
@@ -150,6 +154,19 @@ def make_note_el(note, dur, voice, counter, n_of_staves):
|
|
|
150
154
|
articulations_e.extend(articulations)
|
|
151
155
|
notations.append(articulations_e)
|
|
152
156
|
|
|
157
|
+
if note.technical:
|
|
158
|
+
technical = []
|
|
159
|
+
for technical_notation in note.technical:
|
|
160
|
+
if isinstance(technical_notation, score.Fingering):
|
|
161
|
+
tech_el = etree.Element("fingering")
|
|
162
|
+
tech_el.text = str(technical_notation.fingering)
|
|
163
|
+
technical.append(tech_el)
|
|
164
|
+
|
|
165
|
+
if technical:
|
|
166
|
+
technical_e = etree.Element("technical")
|
|
167
|
+
technical_e.extend(technical)
|
|
168
|
+
notations.append(technical_e)
|
|
169
|
+
|
|
153
170
|
sym_dur = note.symbolic_duration or {}
|
|
154
171
|
|
|
155
172
|
if sym_dur.get("type") is not None:
|
|
@@ -210,9 +227,26 @@ def make_note_el(note, dur, voice, counter, n_of_staves):
|
|
|
210
227
|
else:
|
|
211
228
|
del counter[tuplet_key]
|
|
212
229
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
230
|
+
tuplet_e = etree.Element("tuplet", number="{}".format(number), type="start")
|
|
231
|
+
if (
|
|
232
|
+
tuplet.actual_notes is not None
|
|
233
|
+
and tuplet.normal_notes is not None
|
|
234
|
+
and tuplet.actual_type is not None
|
|
235
|
+
and tuplet.normal_type is not None
|
|
236
|
+
):
|
|
237
|
+
# tuplet-actual tag
|
|
238
|
+
tuplet_actual_e = etree.SubElement(tuplet_e, "tuplet-actual")
|
|
239
|
+
tuplet_actual_notes_e = etree.SubElement(tuplet_actual_e, "tuplet-number")
|
|
240
|
+
tuplet_actual_notes_e.text = str(tuplet.actual_notes)
|
|
241
|
+
tuplet_actual_type_e = etree.SubElement(tuplet_actual_e, "tuplet-type")
|
|
242
|
+
tuplet_actual_type_e.text = str(tuplet.actual_type)
|
|
243
|
+
# tuplet-normal tag
|
|
244
|
+
tuplet_normal_e = etree.SubElement(tuplet_e, "tuplet-normal")
|
|
245
|
+
tuplet_normal_notes_e = etree.SubElement(tuplet_normal_e, "tuplet-number")
|
|
246
|
+
tuplet_normal_notes_e.text = str(tuplet.normal_notes)
|
|
247
|
+
tuplet_normal_type_e = etree.SubElement(tuplet_normal_e, "tuplet-type")
|
|
248
|
+
tuplet_normal_type_e.text = str(tuplet.normal_type)
|
|
249
|
+
notations.append(tuplet_e)
|
|
216
250
|
|
|
217
251
|
if notations:
|
|
218
252
|
notations_e = etree.SubElement(note_e, "notations")
|
|
@@ -413,9 +447,12 @@ def linearize_segment_contents(part, start, end, state):
|
|
|
413
447
|
|
|
414
448
|
for voice in sorted(notes_by_voice.keys()):
|
|
415
449
|
voice_notes = notes_by_voice[voice]
|
|
416
|
-
# sort by pitch
|
|
450
|
+
# sort by pitch (then step in case of enharmonic notes)
|
|
417
451
|
voice_notes.sort(
|
|
418
|
-
key=lambda n:
|
|
452
|
+
key=lambda n: (
|
|
453
|
+
(n.midi_pitch, n.step) if hasattr(n, "midi_pitch") else (-1, "")
|
|
454
|
+
),
|
|
455
|
+
reverse=True,
|
|
419
456
|
)
|
|
420
457
|
# grace notes should precede other notes at the same onset
|
|
421
458
|
voice_notes.sort(key=lambda n: not isinstance(n, score.GraceNote))
|
|
@@ -634,12 +671,20 @@ def merge_measure_contents(notes, other, measure_start):
|
|
|
634
671
|
merged[0] = []
|
|
635
672
|
cost[0] = 0
|
|
636
673
|
|
|
674
|
+
# CHANGE: disabled cost-based merging of non-note elements into stream
|
|
675
|
+
# because this led to attributes not being in the beginning of the measure,
|
|
676
|
+
# which in turn led to problems with musescore
|
|
677
|
+
# fix: add atributes first, then the notes.
|
|
678
|
+
# problem: unclear whether this cost-based merging will break anything or
|
|
679
|
+
# was just cosmetic to avoid too many forwards and backwards.
|
|
680
|
+
# related issue: https://github.com/CPJKU/partitura/issues/390
|
|
681
|
+
|
|
637
682
|
# get the voice for which merging notes and other has lowest cost
|
|
638
|
-
merge_voice = sorted(cost.items(), key=itemgetter(1))[0][0]
|
|
683
|
+
# merge_voice = sorted(cost.items(), key=itemgetter(1))[0][0]
|
|
639
684
|
result = []
|
|
640
685
|
pos = measure_start
|
|
641
686
|
for i, voice in enumerate(sorted(notes.keys())):
|
|
642
|
-
if voice == merge_voice:
|
|
687
|
+
if i == 0: # voice == merge_voice:
|
|
643
688
|
elements = merged[voice]
|
|
644
689
|
|
|
645
690
|
else:
|
|
@@ -658,7 +703,7 @@ def merge_measure_contents(notes, other, measure_start):
|
|
|
658
703
|
elif gap > 0:
|
|
659
704
|
e = etree.Element("forward")
|
|
660
705
|
ee = etree.SubElement(e, "duration")
|
|
661
|
-
ee.text = "{:d}".format(gap)
|
|
706
|
+
ee.text = "{:d}".format(int(gap))
|
|
662
707
|
result.append(e)
|
|
663
708
|
|
|
664
709
|
result.extend([e for _, _, e in elements])
|
|
@@ -1053,8 +1098,8 @@ def save_musicxml(
|
|
|
1053
1098
|
part_e.append(etree.Comment(MEASURE_SEP_COMMENT))
|
|
1054
1099
|
attrib = {}
|
|
1055
1100
|
|
|
1056
|
-
if measure.
|
|
1057
|
-
attrib["number"] = str(measure.
|
|
1101
|
+
if measure.name is not None:
|
|
1102
|
+
attrib["number"] = str(measure.name)
|
|
1058
1103
|
|
|
1059
1104
|
measure_e = etree.SubElement(part_e, "measure", **attrib)
|
|
1060
1105
|
contents = linearize_measure_contents(
|