partitura 1.3.1__tar.gz → 1.4.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.3.1 → partitura-1.4.0}/PKG-INFO +8 -2
- {partitura-1.3.1 → partitura-1.4.0}/partitura/directions.py +3 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/__init__.py +41 -35
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/exportmatch.py +51 -3
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/exportmidi.py +8 -2
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/importmatch.py +46 -35
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/importmei.py +159 -33
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/importmidi.py +23 -2
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/importmusicxml.py +40 -5
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/matchfile_utils.py +29 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/matchlines_v1.py +8 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/musescore.py +68 -31
- {partitura-1.3.1 → partitura-1.4.0}/partitura/performance.py +178 -2
- {partitura-1.3.1 → partitura-1.4.0}/partitura/score.py +107 -13
- {partitura-1.3.1 → partitura-1.4.0}/partitura/utils/music.py +16 -7
- {partitura-1.3.1 → partitura-1.4.0}/partitura.egg-info/PKG-INFO +8 -2
- {partitura-1.3.1 → partitura-1.4.0}/partitura.egg-info/SOURCES.txt +2 -0
- {partitura-1.3.1 → partitura-1.4.0}/setup.py +2 -2
- partitura-1.4.0/tests/test_cross_staff_beaming.py +20 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_load_score.py +9 -13
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_match_import.py +2 -1
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_mei.py +45 -26
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_midi_import.py +9 -0
- partitura-1.4.0/tests/test_musescore.py +33 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_pianoroll.py +4 -3
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_xml.py +9 -0
- {partitura-1.3.1 → partitura-1.4.0}/LICENSE +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/README.md +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/__init__.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/assets/musicxml.xsd +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/assets/score_example.krn +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/assets/score_example.mei +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/assets/score_example.mid +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/assets/score_example.musicxml +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/display.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/exportaudio.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/exportmei.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/exportmusicxml.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/exportparangonada.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/importkern.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/importmusic21.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/importnakamura.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/importparangonada.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/matchfile_base.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/io/matchlines_v0.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/musicanalysis/__init__.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/musicanalysis/key_identification.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/musicanalysis/meter.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/musicanalysis/note_array_to_score.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/musicanalysis/note_features.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/musicanalysis/performance_codec.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/musicanalysis/performance_features.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/musicanalysis/pitch_spelling.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/musicanalysis/tonal_tension.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/musicanalysis/voice_separation.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/utils/__init__.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/utils/generic.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/utils/misc.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/utils/normalize.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura/utils/synth.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura.egg-info/dependency_links.txt +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura.egg-info/requires.txt +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/partitura.egg-info/top_level.txt +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/setup.cfg +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_deprecations.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_display.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_harmony.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_kern.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_key_estimation.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_load_performance.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_m21_import.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_match_export.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_merge_parts.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_metrical_position.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_midi_export.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_nakamura.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_new_divs.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_note_array.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_note_features.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_octave_shift.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_parangonada.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_part_properties.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_partial_measures.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_performance.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_performance_codec.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_performance_features.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_pitch_spelling.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_quarter_adjust.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_rest_array.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_synth.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_time_estimation.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_times.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_tonal_tension.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_transpose.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_utils.py +0 -0
- {partitura-1.3.1 → partitura-1.4.0}/tests/test_voice_estimation.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: partitura
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: A package for handling symbolic musical information
|
|
5
5
|
Home-page: https://github.com/CPJKU/partitura
|
|
6
|
-
Author: Maarten Grachten, Carlos Cancino-Chacón, Silvan Peter, Emmanouil Karystinaios, Francesco Foscarin, Thassilo Gadermaier
|
|
6
|
+
Author: Maarten Grachten, Carlos Cancino-Chacón, Silvan Peter, Emmanouil Karystinaios, Francesco Foscarin, Thassilo Gadermaier, Patricia Hu
|
|
7
7
|
Author-email: partitura-users@googlegroups.com
|
|
8
8
|
License: Apache 2.0
|
|
9
9
|
Keywords: music notation musicxml midi
|
|
@@ -15,6 +15,12 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
|
15
15
|
Requires-Python: >=3.7
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
|
+
Requires-Dist: numpy
|
|
19
|
+
Requires-Dist: scipy
|
|
20
|
+
Requires-Dist: lxml
|
|
21
|
+
Requires-Dist: lark-parser
|
|
22
|
+
Requires-Dist: xmlschema
|
|
23
|
+
Requires-Dist: mido
|
|
18
24
|
|
|
19
25
|
|
|
20
26
|
[//]: # (<p align="center"> )
|
|
@@ -151,6 +151,8 @@ CONSTANT_TEMPO_ADJ = [
|
|
|
151
151
|
"adagio",
|
|
152
152
|
"agitato",
|
|
153
153
|
"andante",
|
|
154
|
+
"andante cantabile",
|
|
155
|
+
"andante amoroso",
|
|
154
156
|
"andantino",
|
|
155
157
|
"animato",
|
|
156
158
|
"appassionato",
|
|
@@ -193,6 +195,7 @@ CONSTANT_TEMPO_ADJ = [
|
|
|
193
195
|
"tranquilamente",
|
|
194
196
|
"tranquilo",
|
|
195
197
|
"recitativo",
|
|
198
|
+
"allegro moderato",
|
|
196
199
|
r"/(vivo|vivacissimamente|vivace)/",
|
|
197
200
|
r"/(allegro|allegretto)/",
|
|
198
201
|
r"/(espressivo|espress\.?)/",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
This module contains methods for importing and exporting symbolic music formats.
|
|
5
5
|
"""
|
|
6
6
|
from typing import Union
|
|
7
|
+
import os
|
|
7
8
|
|
|
8
9
|
from .importmusicxml import load_musicxml
|
|
9
10
|
from .importmidi import load_score_midi, load_performance_midi
|
|
@@ -35,7 +36,7 @@ def load_score(filename: PathLike, force_note_ids="keep") -> Score:
|
|
|
35
36
|
"""
|
|
36
37
|
Load a score format supported by partitura. Currently the accepted formats
|
|
37
38
|
are MusicXML, MIDI, Kern and MEI, plus all formats for which
|
|
38
|
-
MuseScore has support import-support (requires MuseScore 3).
|
|
39
|
+
MuseScore has support import-support (requires MuseScore 4 or 3).
|
|
39
40
|
|
|
40
41
|
Parameters
|
|
41
42
|
----------
|
|
@@ -54,20 +55,16 @@ def load_score(filename: PathLike, force_note_ids="keep") -> Score:
|
|
|
54
55
|
scr: :class:`partitura.score.Score`
|
|
55
56
|
A score instance.
|
|
56
57
|
"""
|
|
57
|
-
part = None
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
try:
|
|
59
|
+
extension = os.path.splitext(filename)[-1].lower()
|
|
60
|
+
if extension in (".mxl", ".xml", ".musicxml"):
|
|
61
|
+
# Load MusicXML
|
|
63
62
|
return load_musicxml(
|
|
64
63
|
filename=filename,
|
|
65
64
|
force_note_ids=force_note_ids,
|
|
66
65
|
)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# Load MIDI
|
|
70
|
-
try:
|
|
66
|
+
elif extension in [".midi", ".mid"]:
|
|
67
|
+
# Load MIDI
|
|
71
68
|
if (force_note_ids is None) or (not force_note_ids):
|
|
72
69
|
assign_note_ids = False
|
|
73
70
|
else:
|
|
@@ -76,44 +73,53 @@ def load_score(filename: PathLike, force_note_ids="keep") -> Score:
|
|
|
76
73
|
filename=filename,
|
|
77
74
|
assign_note_ids=assign_note_ids,
|
|
78
75
|
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# Load MEI
|
|
82
|
-
try:
|
|
76
|
+
elif extension in [".mei"]:
|
|
77
|
+
# Load MEI
|
|
83
78
|
return load_mei(filename=filename)
|
|
84
|
-
|
|
85
|
-
exception_dictionary["MEI"] = e
|
|
86
|
-
# Load Kern
|
|
87
|
-
try:
|
|
79
|
+
elif extension in [".kern", ".krn"]:
|
|
88
80
|
return load_kern(
|
|
89
81
|
filename=filename,
|
|
90
82
|
force_note_ids=force_note_ids,
|
|
91
83
|
)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
84
|
+
elif extension in [
|
|
85
|
+
".mscz",
|
|
86
|
+
".mscx",
|
|
87
|
+
".musescore",
|
|
88
|
+
".mscore",
|
|
89
|
+
".ms",
|
|
90
|
+
".kar",
|
|
91
|
+
".md",
|
|
92
|
+
".cap",
|
|
93
|
+
".capx",
|
|
94
|
+
".bww",
|
|
95
|
+
".mgu",
|
|
96
|
+
".sgu",
|
|
97
|
+
".ove",
|
|
98
|
+
".scw",
|
|
99
|
+
".ptb",
|
|
100
|
+
".gtp",
|
|
101
|
+
".gp3",
|
|
102
|
+
".gp4",
|
|
103
|
+
".gp5",
|
|
104
|
+
".gpx",
|
|
105
|
+
".gp",
|
|
106
|
+
]:
|
|
107
|
+
# Load MuseScore
|
|
96
108
|
return load_via_musescore(
|
|
97
109
|
filename=filename,
|
|
98
110
|
force_note_ids=force_note_ids,
|
|
99
111
|
)
|
|
100
|
-
|
|
101
|
-
exception_dictionary["MuseScore"] = e
|
|
102
|
-
try:
|
|
112
|
+
elif extension in [".match"]:
|
|
103
113
|
# Load the score information from a Matchfile
|
|
104
|
-
_, _,
|
|
114
|
+
_, _, score = load_match(
|
|
105
115
|
filename=filename,
|
|
106
116
|
create_score=True,
|
|
107
117
|
)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
print(f"Error loading score as {score_format}:")
|
|
114
|
-
print(exception)
|
|
115
|
-
|
|
116
|
-
raise NotSupportedFormatError
|
|
118
|
+
return score
|
|
119
|
+
else:
|
|
120
|
+
raise NotSupportedFormatError(
|
|
121
|
+
f"{extension} file extension is not supported. If this should be supported, consider editing partitura/io/__init__.py file"
|
|
122
|
+
)
|
|
117
123
|
|
|
118
124
|
|
|
119
125
|
def load_score_as_part(filename: PathLike) -> Part:
|
|
@@ -37,6 +37,7 @@ from partitura.io.matchfile_utils import (
|
|
|
37
37
|
FractionalSymbolicDuration,
|
|
38
38
|
MatchKeySignature,
|
|
39
39
|
MatchTimeSignature,
|
|
40
|
+
MatchTempoIndication,
|
|
40
41
|
Version,
|
|
41
42
|
)
|
|
42
43
|
|
|
@@ -71,6 +72,8 @@ def matchfile_from_alignment(
|
|
|
71
72
|
score_filename: Optional[PathLike] = None,
|
|
72
73
|
performance_filename: Optional[PathLike] = None,
|
|
73
74
|
assume_part_unfolded: bool = False,
|
|
75
|
+
tempo_indication: Optional[str] = None,
|
|
76
|
+
diff_score_version_notes: Optional[list] = None,
|
|
74
77
|
version: Version = LATEST_VERSION,
|
|
75
78
|
debug: bool = False,
|
|
76
79
|
) -> MatchFile:
|
|
@@ -106,6 +109,10 @@ def matchfile_from_alignment(
|
|
|
106
109
|
repetitions in the alignment. If False, the part will be automatically
|
|
107
110
|
unfolded to have maximal coverage of the notes in the alignment.
|
|
108
111
|
See `partitura.score.unfold_part_alignment`.
|
|
112
|
+
tempo_indication : str or None
|
|
113
|
+
The tempo direction indicated in the beginning of the score
|
|
114
|
+
diff_score_version_notes : list or None
|
|
115
|
+
A list of score notes that reflect a special score version (e.g., original edition/Erstdruck, Editors note etc.)
|
|
109
116
|
version: Version
|
|
110
117
|
Version of the match file. For now only 1.0.0 is supported.
|
|
111
118
|
Returns
|
|
@@ -199,7 +206,6 @@ def matchfile_from_alignment(
|
|
|
199
206
|
|
|
200
207
|
# Score prop header lines
|
|
201
208
|
scoreprop_lines = defaultdict(list)
|
|
202
|
-
|
|
203
209
|
# For score notes
|
|
204
210
|
score_info = dict()
|
|
205
211
|
# Info for sorting lines
|
|
@@ -276,7 +282,6 @@ def matchfile_from_alignment(
|
|
|
276
282
|
# Get all notes in the measure
|
|
277
283
|
snotes = spart.iter_all(score.Note, m.start, m.end, include_subclasses=True)
|
|
278
284
|
# Beginning of each measure
|
|
279
|
-
|
|
280
285
|
for snote in snotes:
|
|
281
286
|
onset_divs, offset_divs = snote.start.t, snote.start.t + snote.duration_tied
|
|
282
287
|
duration_divs = offset_divs - onset_divs
|
|
@@ -324,6 +329,15 @@ def matchfile_from_alignment(
|
|
|
324
329
|
if fermata is not None:
|
|
325
330
|
score_attributes_list.append("fermata")
|
|
326
331
|
|
|
332
|
+
if isinstance(snote, score.GraceNote):
|
|
333
|
+
score_attributes_list.append("grace")
|
|
334
|
+
|
|
335
|
+
if (
|
|
336
|
+
diff_score_version_notes is not None
|
|
337
|
+
and snote.id in diff_score_version_notes
|
|
338
|
+
):
|
|
339
|
+
score_attributes_list.append("diff_score_version")
|
|
340
|
+
|
|
327
341
|
score_info[snote.id] = MatchSnote(
|
|
328
342
|
version=version,
|
|
329
343
|
anchor=str(snote.id),
|
|
@@ -346,6 +360,22 @@ def matchfile_from_alignment(
|
|
|
346
360
|
)
|
|
347
361
|
snote_sort_info[snote.id] = (onset_beats, snote.doc_order)
|
|
348
362
|
|
|
363
|
+
# # NOTE time position is hardcoded, not pretty... Assumes there is only one tempo indication at the beginning of the score
|
|
364
|
+
if tempo_indication is not None:
|
|
365
|
+
score_tempo_direction_header = make_scoreprop(
|
|
366
|
+
version=version,
|
|
367
|
+
attribute="tempoIndication",
|
|
368
|
+
value=MatchTempoIndication(
|
|
369
|
+
tempo_indication,
|
|
370
|
+
is_list=False,
|
|
371
|
+
),
|
|
372
|
+
measure=measure_starts[0][0],
|
|
373
|
+
beat=1,
|
|
374
|
+
offset=0,
|
|
375
|
+
time_in_beats=measure_starts[0][2],
|
|
376
|
+
)
|
|
377
|
+
scoreprop_lines["tempo_indication"].append(score_tempo_direction_header)
|
|
378
|
+
|
|
349
379
|
perf_info = dict()
|
|
350
380
|
pnote_sort_info = dict()
|
|
351
381
|
for pnote in ppart.notes:
|
|
@@ -372,6 +402,21 @@ def matchfile_from_alignment(
|
|
|
372
402
|
|
|
373
403
|
sort_stime = []
|
|
374
404
|
note_lines = []
|
|
405
|
+
|
|
406
|
+
# Get ids of notes which voice overlap
|
|
407
|
+
sna = spart.note_array()
|
|
408
|
+
onset_pitch_slice = sna[["onset_div", "pitch"]]
|
|
409
|
+
uniques, counts = np.unique(onset_pitch_slice, return_counts=True)
|
|
410
|
+
duplicate_values = uniques[counts > 1]
|
|
411
|
+
duplicates = dict()
|
|
412
|
+
for v in duplicate_values:
|
|
413
|
+
idx = np.where(onset_pitch_slice == v)[0]
|
|
414
|
+
duplicates[tuple(v)] = idx
|
|
415
|
+
voice_overlap_note_ids = []
|
|
416
|
+
if len(duplicates) > 0:
|
|
417
|
+
duplicate_idx = np.concatenate(np.array(list(duplicates.values()))).flatten()
|
|
418
|
+
voice_overlap_note_ids = list(sna[duplicate_idx]["id"])
|
|
419
|
+
|
|
375
420
|
for al_note in alignment:
|
|
376
421
|
label = al_note["label"]
|
|
377
422
|
|
|
@@ -384,6 +429,8 @@ def matchfile_from_alignment(
|
|
|
384
429
|
|
|
385
430
|
elif label == "deletion":
|
|
386
431
|
snote = score_info[al_note["score_id"]]
|
|
432
|
+
if al_note["score_id"] in voice_overlap_note_ids:
|
|
433
|
+
snote.ScoreAttributesList.append("voice_overlap")
|
|
387
434
|
deletion_line = MatchSnoteDeletion(version=version, snote=snote)
|
|
388
435
|
note_lines.append(deletion_line)
|
|
389
436
|
sort_stime.append(snote_sort_info[al_note["score_id"]])
|
|
@@ -441,6 +488,7 @@ def matchfile_from_alignment(
|
|
|
441
488
|
"clock_rate",
|
|
442
489
|
"key_signatures",
|
|
443
490
|
"time_signatures",
|
|
491
|
+
"tempo_indication",
|
|
444
492
|
]
|
|
445
493
|
all_match_lines = []
|
|
446
494
|
for h in header_order:
|
|
@@ -537,7 +585,7 @@ def save_match(
|
|
|
537
585
|
else:
|
|
538
586
|
raise ValueError(
|
|
539
587
|
"`performance_data` should be a `Performance`, a `PerformedPart`, or a "
|
|
540
|
-
f"list of `PerformedPart` objects, but is {type(
|
|
588
|
+
f"list of `PerformedPart` objects, but is {type(performance_data)}"
|
|
541
589
|
)
|
|
542
590
|
|
|
543
591
|
# Get matchfile
|
|
@@ -8,7 +8,7 @@ import numpy as np
|
|
|
8
8
|
from collections import defaultdict, OrderedDict
|
|
9
9
|
from typing import Optional, Iterable
|
|
10
10
|
|
|
11
|
-
from mido import MidiFile, MidiTrack, Message, MetaMessage
|
|
11
|
+
from mido import MidiFile, MidiTrack, Message, MetaMessage, merge_tracks
|
|
12
12
|
|
|
13
13
|
import partitura.score as score
|
|
14
14
|
from partitura.score import Score, Part, PartGroup, ScoreLike
|
|
@@ -87,6 +87,7 @@ def save_performance_midi(
|
|
|
87
87
|
mpq: int = 500000,
|
|
88
88
|
ppq: int = 480,
|
|
89
89
|
default_velocity: int = 64,
|
|
90
|
+
merge_tracks_save: Optional[bool] = False,
|
|
90
91
|
) -> Optional[MidiFile]:
|
|
91
92
|
"""Save a :class:`~partitura.performance.PerformedPart` or
|
|
92
93
|
a :class:`~partitura.performance.Performance` as a MIDI file
|
|
@@ -107,6 +108,8 @@ def save_performance_midi(
|
|
|
107
108
|
default_velocity : int, optional
|
|
108
109
|
A default velocity value (between 0 and 127) to be used for
|
|
109
110
|
notes without a specified velocity. Defaults to 64.
|
|
111
|
+
merge_tracks_save : bool, optional
|
|
112
|
+
Determines whether midi tracks are merged when exporting to a midi file. Defaults to False.
|
|
110
113
|
|
|
111
114
|
Returns
|
|
112
115
|
-------
|
|
@@ -134,7 +137,6 @@ def save_performance_midi(
|
|
|
134
137
|
)
|
|
135
138
|
|
|
136
139
|
track_events = defaultdict(lambda: defaultdict(list))
|
|
137
|
-
|
|
138
140
|
for performed_part in performed_parts:
|
|
139
141
|
for c in performed_part.controls:
|
|
140
142
|
track = c.get("track", 0)
|
|
@@ -217,6 +219,10 @@ def save_performance_midi(
|
|
|
217
219
|
track.append(msg.copy(time=t_delta))
|
|
218
220
|
t_delta = 0
|
|
219
221
|
t = t_msg
|
|
222
|
+
|
|
223
|
+
if merge_tracks_save and len(mf.tracks) > 1:
|
|
224
|
+
mf.tracks = [merge_tracks(mf.tracks)]
|
|
225
|
+
|
|
220
226
|
if out is not None:
|
|
221
227
|
if hasattr(out, "write"):
|
|
222
228
|
mf.save(file=out)
|
|
@@ -6,7 +6,7 @@ This module contains methods for parsing matchfiles
|
|
|
6
6
|
import os
|
|
7
7
|
from typing import Union, Tuple, Optional, Callable, List
|
|
8
8
|
import warnings
|
|
9
|
-
|
|
9
|
+
from functools import partial
|
|
10
10
|
import numpy as np
|
|
11
11
|
|
|
12
12
|
from partitura import score
|
|
@@ -197,21 +197,23 @@ def load_matchfile(
|
|
|
197
197
|
|
|
198
198
|
parsed_lines = list()
|
|
199
199
|
# Functionality to remove duplicate lines
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
200
|
+
len_raw_lines = len(raw_lines)
|
|
201
|
+
np_lines = np.array(raw_lines, dtype=str)
|
|
202
|
+
# Remove empty lines
|
|
203
|
+
np_lines = np_lines[np_lines != ""]
|
|
204
|
+
# Remove duplicate lines
|
|
205
|
+
_, idx = np.unique(np_lines, return_index=True)
|
|
206
|
+
np_lines = np_lines[np.sort(idx)]
|
|
207
|
+
# Parse lines
|
|
208
|
+
f = partial(
|
|
209
|
+
parse_matchline, version=version, from_matchline_methods=from_matchline_methods
|
|
210
|
+
)
|
|
211
|
+
f_vec = np.vectorize(f)
|
|
212
|
+
parsed_lines = f_vec(np_lines).tolist()
|
|
213
|
+
# Create MatchFile instance
|
|
210
214
|
mf = MatchFile(lines=parsed_lines)
|
|
211
|
-
|
|
212
215
|
# Validate match for duplicate snote_ids or pnote_ids
|
|
213
216
|
validate_match_ids(mf)
|
|
214
|
-
|
|
215
217
|
return mf
|
|
216
218
|
|
|
217
219
|
|
|
@@ -371,29 +373,38 @@ def performed_part_from_match(
|
|
|
371
373
|
# PerformedNote instances for all MatchNotes
|
|
372
374
|
notes = []
|
|
373
375
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
376
|
+
notes = list()
|
|
377
|
+
note_onsets_in_secs = np.array(np.zeros(len(mf.notes)), dtype=float)
|
|
378
|
+
note_onsets_in_tick = np.array(np.zeros(len(mf.notes)), dtype=int)
|
|
379
|
+
for i, note in enumerate(mf.notes):
|
|
380
|
+
n_onset_sec = midi_ticks_to_seconds(note.Onset, mpq, ppq)
|
|
381
|
+
note_onsets_in_secs[i] = n_onset_sec
|
|
382
|
+
note_onsets_in_tick[i] = note.Onset
|
|
383
|
+
notes.append(
|
|
384
|
+
dict(
|
|
385
|
+
id=format_pnote_id(note.Id),
|
|
386
|
+
midi_pitch=note.MidiPitch,
|
|
387
|
+
note_on=n_onset_sec,
|
|
388
|
+
note_off=midi_ticks_to_seconds(note.Offset, mpq, ppq),
|
|
389
|
+
note_on_tick=note.Onset,
|
|
390
|
+
note_off_tick=note.Offset,
|
|
391
|
+
sound_off=midi_ticks_to_seconds(note.Offset, mpq, ppq),
|
|
392
|
+
velocity=note.Velocity,
|
|
393
|
+
track=getattr(note, "Track", 0),
|
|
394
|
+
channel=getattr(note, "Channel", 1),
|
|
395
|
+
)
|
|
394
396
|
)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
+
# Set first note_on to zero in ticks and seconds if first_note_at_zero
|
|
398
|
+
if first_note_at_zero and len(note_onsets_in_secs) > 0:
|
|
399
|
+
offset = note_onsets_in_secs.min()
|
|
400
|
+
offset_tick = note_onsets_in_tick.min()
|
|
401
|
+
if offset > 0 and offset_tick > 0:
|
|
402
|
+
for note in notes:
|
|
403
|
+
note["note_on"] -= offset
|
|
404
|
+
note["note_off"] -= offset
|
|
405
|
+
note["sound_off"] -= offset
|
|
406
|
+
note["note_on_tick"] -= offset_tick
|
|
407
|
+
note["note_off_tick"] -= offset_tick
|
|
397
408
|
|
|
398
409
|
# SustainPedal instances for sustain pedal lines
|
|
399
410
|
sustain_pedal = [
|