partitura 1.3.0__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.0 → partitura-1.4.0}/PKG-INFO +8 -2
- {partitura-1.3.0 → partitura-1.4.0}/partitura/directions.py +3 -0
- {partitura-1.3.0 → partitura-1.4.0}/partitura/display.py +0 -1
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/__init__.py +41 -35
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/exportmatch.py +52 -10
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/exportmidi.py +37 -19
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/exportmusicxml.py +6 -92
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/exportparangonada.py +18 -19
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/importkern.py +2 -4
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/importmatch.py +121 -39
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/importmei.py +161 -34
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/importmidi.py +23 -14
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/importmusic21.py +0 -1
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/importmusicxml.py +48 -63
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/importparangonada.py +0 -1
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/matchfile_base.py +0 -21
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/matchfile_utils.py +29 -17
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/matchlines_v0.py +0 -22
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/matchlines_v1.py +8 -42
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/musescore.py +68 -41
- {partitura-1.3.0 → partitura-1.4.0}/partitura/musicanalysis/__init__.py +1 -1
- {partitura-1.3.0 → partitura-1.4.0}/partitura/musicanalysis/note_array_to_score.py +147 -92
- {partitura-1.3.0 → partitura-1.4.0}/partitura/musicanalysis/note_features.py +66 -51
- {partitura-1.3.0 → partitura-1.4.0}/partitura/musicanalysis/performance_codec.py +140 -96
- {partitura-1.3.0 → partitura-1.4.0}/partitura/musicanalysis/performance_features.py +190 -129
- {partitura-1.3.0 → partitura-1.4.0}/partitura/musicanalysis/pitch_spelling.py +0 -2
- {partitura-1.3.0 → partitura-1.4.0}/partitura/musicanalysis/tonal_tension.py +0 -6
- {partitura-1.3.0 → partitura-1.4.0}/partitura/musicanalysis/voice_separation.py +1 -22
- {partitura-1.3.0 → partitura-1.4.0}/partitura/performance.py +178 -5
- {partitura-1.3.0 → partitura-1.4.0}/partitura/score.py +154 -74
- {partitura-1.3.0 → partitura-1.4.0}/partitura/utils/__init__.py +1 -1
- {partitura-1.3.0 → partitura-1.4.0}/partitura/utils/generic.py +3 -7
- {partitura-1.3.0 → partitura-1.4.0}/partitura/utils/misc.py +0 -1
- {partitura-1.3.0 → partitura-1.4.0}/partitura/utils/music.py +108 -66
- {partitura-1.3.0 → partitura-1.4.0}/partitura/utils/normalize.py +75 -35
- {partitura-1.3.0 → partitura-1.4.0}/partitura/utils/synth.py +1 -7
- {partitura-1.3.0 → partitura-1.4.0}/partitura.egg-info/PKG-INFO +8 -2
- {partitura-1.3.0 → partitura-1.4.0}/partitura.egg-info/SOURCES.txt +2 -0
- {partitura-1.3.0 → partitura-1.4.0}/setup.py +2 -2
- partitura-1.4.0/tests/test_cross_staff_beaming.py +20 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_load_score.py +9 -13
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_match_import.py +2 -1
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_mei.py +45 -26
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_midi_import.py +9 -0
- partitura-1.4.0/tests/test_musescore.py +33 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_pianoroll.py +4 -3
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_xml.py +9 -0
- {partitura-1.3.0 → partitura-1.4.0}/LICENSE +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/README.md +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/partitura/__init__.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/partitura/assets/musicxml.xsd +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/partitura/assets/score_example.krn +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/partitura/assets/score_example.mei +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/partitura/assets/score_example.mid +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/partitura/assets/score_example.musicxml +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/exportaudio.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/exportmei.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/partitura/io/importnakamura.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/partitura/musicanalysis/key_identification.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/partitura/musicanalysis/meter.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/partitura.egg-info/dependency_links.txt +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/partitura.egg-info/requires.txt +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/partitura.egg-info/top_level.txt +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/setup.cfg +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_deprecations.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_display.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_harmony.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_kern.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_key_estimation.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_load_performance.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_m21_import.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_match_export.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_merge_parts.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_metrical_position.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_midi_export.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_nakamura.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_new_divs.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_note_array.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_note_features.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_octave_shift.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_parangonada.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_part_properties.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_partial_measures.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_performance.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_performance_codec.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_performance_features.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_pitch_spelling.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_quarter_adjust.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_rest_array.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_synth.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_time_estimation.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_times.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_tonal_tension.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_transpose.py +0 -0
- {partitura-1.3.0 → partitura-1.4.0}/tests/test_utils.py +0 -0
- {partitura-1.3.0 → 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\.?)/",
|
|
@@ -120,7 +120,6 @@ def render_lilypond(
|
|
|
120
120
|
with TemporaryFile() as xml_fh, NamedTemporaryFile(
|
|
121
121
|
suffix=prvw_sfx, delete=False
|
|
122
122
|
) as img_fh:
|
|
123
|
-
|
|
124
123
|
# save part to musicxml in file handle xml_fh
|
|
125
124
|
save_musicxml(score_data, xml_fh)
|
|
126
125
|
# rewind read pointer of file handle before we pass it to musicxml2ly
|
|
@@ -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
|
|
|
@@ -53,9 +54,7 @@ from partitura.utils.misc import (
|
|
|
53
54
|
deprecated_parameter,
|
|
54
55
|
)
|
|
55
56
|
|
|
56
|
-
from partitura.musicanalysis.performance_codec import
|
|
57
|
-
get_time_maps_from_alignment
|
|
58
|
-
)
|
|
57
|
+
from partitura.musicanalysis.performance_codec import get_time_maps_from_alignment
|
|
59
58
|
|
|
60
59
|
__all__ = ["save_match"]
|
|
61
60
|
|
|
@@ -73,6 +72,8 @@ def matchfile_from_alignment(
|
|
|
73
72
|
score_filename: Optional[PathLike] = None,
|
|
74
73
|
performance_filename: Optional[PathLike] = None,
|
|
75
74
|
assume_part_unfolded: bool = False,
|
|
75
|
+
tempo_indication: Optional[str] = None,
|
|
76
|
+
diff_score_version_notes: Optional[list] = None,
|
|
76
77
|
version: Version = LATEST_VERSION,
|
|
77
78
|
debug: bool = False,
|
|
78
79
|
) -> MatchFile:
|
|
@@ -108,6 +109,10 @@ def matchfile_from_alignment(
|
|
|
108
109
|
repetitions in the alignment. If False, the part will be automatically
|
|
109
110
|
unfolded to have maximal coverage of the notes in the alignment.
|
|
110
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.)
|
|
111
116
|
version: Version
|
|
112
117
|
Version of the match file. For now only 1.0.0 is supported.
|
|
113
118
|
Returns
|
|
@@ -201,17 +206,14 @@ def matchfile_from_alignment(
|
|
|
201
206
|
|
|
202
207
|
# Score prop header lines
|
|
203
208
|
scoreprop_lines = defaultdict(list)
|
|
204
|
-
|
|
205
209
|
# For score notes
|
|
206
210
|
score_info = dict()
|
|
207
211
|
# Info for sorting lines
|
|
208
212
|
snote_sort_info = dict()
|
|
209
213
|
for (mnum, msd, msb), m in zip(measure_starts, measures):
|
|
210
|
-
|
|
211
214
|
time_signatures = spart.iter_all(score.TimeSignature, m.start, m.end)
|
|
212
215
|
|
|
213
216
|
for tsig in time_signatures:
|
|
214
|
-
|
|
215
217
|
time_divs = int(tsig.start.t)
|
|
216
218
|
time_beats = float(beat_map(time_divs))
|
|
217
219
|
dpq = int(spart.quarter_duration_map(time_divs))
|
|
@@ -280,9 +282,7 @@ def matchfile_from_alignment(
|
|
|
280
282
|
# Get all notes in the measure
|
|
281
283
|
snotes = spart.iter_all(score.Note, m.start, m.end, include_subclasses=True)
|
|
282
284
|
# Beginning of each measure
|
|
283
|
-
|
|
284
285
|
for snote in snotes:
|
|
285
|
-
|
|
286
286
|
onset_divs, offset_divs = snote.start.t, snote.start.t + snote.duration_tied
|
|
287
287
|
duration_divs = offset_divs - onset_divs
|
|
288
288
|
|
|
@@ -329,6 +329,15 @@ def matchfile_from_alignment(
|
|
|
329
329
|
if fermata is not None:
|
|
330
330
|
score_attributes_list.append("fermata")
|
|
331
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
|
+
|
|
332
341
|
score_info[snote.id] = MatchSnote(
|
|
333
342
|
version=version,
|
|
334
343
|
anchor=str(snote.id),
|
|
@@ -351,6 +360,22 @@ def matchfile_from_alignment(
|
|
|
351
360
|
)
|
|
352
361
|
snote_sort_info[snote.id] = (onset_beats, snote.doc_order)
|
|
353
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
|
+
|
|
354
379
|
perf_info = dict()
|
|
355
380
|
pnote_sort_info = dict()
|
|
356
381
|
for pnote in ppart.notes:
|
|
@@ -377,8 +402,22 @@ def matchfile_from_alignment(
|
|
|
377
402
|
|
|
378
403
|
sort_stime = []
|
|
379
404
|
note_lines = []
|
|
380
|
-
for al_note in alignment:
|
|
381
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
|
+
|
|
420
|
+
for al_note in alignment:
|
|
382
421
|
label = al_note["label"]
|
|
383
422
|
|
|
384
423
|
if label == "match":
|
|
@@ -390,6 +429,8 @@ def matchfile_from_alignment(
|
|
|
390
429
|
|
|
391
430
|
elif label == "deletion":
|
|
392
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")
|
|
393
434
|
deletion_line = MatchSnoteDeletion(version=version, snote=snote)
|
|
394
435
|
note_lines.append(deletion_line)
|
|
395
436
|
sort_stime.append(snote_sort_info[al_note["score_id"]])
|
|
@@ -447,6 +488,7 @@ def matchfile_from_alignment(
|
|
|
447
488
|
"clock_rate",
|
|
448
489
|
"key_signatures",
|
|
449
490
|
"time_signatures",
|
|
491
|
+
"tempo_indication",
|
|
450
492
|
]
|
|
451
493
|
all_match_lines = []
|
|
452
494
|
for h in header_order:
|
|
@@ -543,7 +585,7 @@ def save_match(
|
|
|
543
585
|
else:
|
|
544
586
|
raise ValueError(
|
|
545
587
|
"`performance_data` should be a `Performance`, a `PerformedPart`, or a "
|
|
546
|
-
f"list of `PerformedPart` objects, but is {type(
|
|
588
|
+
f"list of `PerformedPart` objects, but is {type(performance_data)}"
|
|
547
589
|
)
|
|
548
590
|
|
|
549
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
|
|
@@ -32,7 +32,7 @@ def map_to_track_channel(note_keys, mode):
|
|
|
32
32
|
tr_helper = {}
|
|
33
33
|
track = {}
|
|
34
34
|
channel = {}
|
|
35
|
-
for
|
|
35
|
+
for pg, p, v in note_keys:
|
|
36
36
|
if mode == 0:
|
|
37
37
|
trk = tr_helper.setdefault(p, len(tr_helper))
|
|
38
38
|
ch1 = ch_helper.setdefault(p, {})
|
|
@@ -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)
|
|
@@ -287,9 +293,9 @@ def save_score_midi(
|
|
|
287
293
|
downbeats position are coherent in case of incomplete measures
|
|
288
294
|
later in the score.
|
|
289
295
|
minimum_ppq : int, optional
|
|
290
|
-
Minimum ppq to use for the MIDI file. If the ppq of the score is less,
|
|
296
|
+
Minimum ppq to use for the MIDI file. If the ppq of the score is less,
|
|
291
297
|
it will be doubled until it is above the threshold. This is useful
|
|
292
|
-
because some libraries like miditok require a certain minimum ppq to
|
|
298
|
+
because some libraries like miditok require a certain minimum ppq to
|
|
293
299
|
work properly.
|
|
294
300
|
|
|
295
301
|
Returns
|
|
@@ -315,7 +321,7 @@ def save_score_midi(
|
|
|
315
321
|
# double it until it is above the minimum level.
|
|
316
322
|
# Doubling instead of setting it ensure that the common divisors stay the same.
|
|
317
323
|
while ppq < minimum_ppq:
|
|
318
|
-
ppq = ppq*2
|
|
324
|
+
ppq = ppq * 2
|
|
319
325
|
|
|
320
326
|
events = defaultdict(lambda: defaultdict(list))
|
|
321
327
|
meta_events = defaultdict(lambda: defaultdict(list))
|
|
@@ -346,7 +352,6 @@ def save_score_midi(
|
|
|
346
352
|
)
|
|
347
353
|
|
|
348
354
|
for qm, part in zip(quarter_maps, score.iter_parts(parts)):
|
|
349
|
-
|
|
350
355
|
pg = get_partgroup(part)
|
|
351
356
|
|
|
352
357
|
notes = part.notes_tied
|
|
@@ -367,22 +372,30 @@ def save_score_midi(
|
|
|
367
372
|
all_ts = list(part.iter_all(score.TimeSignature))
|
|
368
373
|
ts_changing_time = [ts.start.t for ts in all_ts]
|
|
369
374
|
for measure in part.iter_all(score.Measure):
|
|
370
|
-
m_duration_beat = part.beat_map(measure.end.t) - part.beat_map(
|
|
375
|
+
m_duration_beat = part.beat_map(measure.end.t) - part.beat_map(
|
|
376
|
+
measure.start.t
|
|
377
|
+
)
|
|
371
378
|
m_ts = part.time_signature_map(measure.start.t)
|
|
372
379
|
if m_duration_beat != m_ts[0]:
|
|
373
380
|
# add ts change
|
|
374
381
|
# TODO: add support for changing the beat type if number of beats is not integer
|
|
375
382
|
meta_events[part][to_ppq(measure.start.t)].append(
|
|
376
383
|
MetaMessage(
|
|
377
|
-
"time_signature",
|
|
384
|
+
"time_signature",
|
|
385
|
+
numerator=int(m_duration_beat),
|
|
386
|
+
denominator=int(m_ts[1]),
|
|
378
387
|
)
|
|
379
388
|
)
|
|
380
|
-
ts_changing_time.append(
|
|
389
|
+
ts_changing_time.append(
|
|
390
|
+
measure.start.t
|
|
391
|
+
) # keep track of changing the ts
|
|
381
392
|
# now go back to original ts if there is no ts change after this measure
|
|
382
393
|
if not any([ts_t > measure.start.t for ts_t in ts_changing_time]):
|
|
383
394
|
meta_events[part][to_ppq(measure.end.t)].append(
|
|
384
395
|
MetaMessage(
|
|
385
|
-
"time_signature",
|
|
396
|
+
"time_signature",
|
|
397
|
+
numerator=int(m_ts[0]),
|
|
398
|
+
denominator=int(m_ts[1]),
|
|
386
399
|
)
|
|
387
400
|
)
|
|
388
401
|
# filter out the multiple ts changes at the same time
|
|
@@ -394,27 +407,33 @@ def save_score_midi(
|
|
|
394
407
|
# now add the normal time signature change
|
|
395
408
|
for ts in part.iter_all(score.TimeSignature):
|
|
396
409
|
if ts.start.t in ts_changing_time:
|
|
397
|
-
|
|
410
|
+
# don't add if something is already added at this time to cover the case of a ts change when the first measure is shorter/longer
|
|
398
411
|
pass
|
|
399
412
|
else:
|
|
400
413
|
meta_events[part][to_ppq(ts.start.t)].append(
|
|
401
414
|
MetaMessage(
|
|
402
|
-
"time_signature",
|
|
415
|
+
"time_signature",
|
|
416
|
+
numerator=ts.beats,
|
|
417
|
+
denominator=ts.beat_type,
|
|
403
418
|
)
|
|
404
419
|
)
|
|
405
|
-
else:
|
|
420
|
+
else: # just add the time signature that are explicit in partitura
|
|
406
421
|
for i, ts in enumerate(part.iter_all(score.TimeSignature)):
|
|
407
422
|
if anacrusis_behavior == "pad_bar" and i == 0:
|
|
408
423
|
# shift the first time signature to 0 so MIDI players can pick up the correct measure position
|
|
409
424
|
meta_events[part][0].append(
|
|
410
425
|
MetaMessage(
|
|
411
|
-
|
|
426
|
+
"time_signature",
|
|
427
|
+
numerator=ts.beats,
|
|
428
|
+
denominator=ts.beat_type,
|
|
412
429
|
)
|
|
413
|
-
)
|
|
414
|
-
else:
|
|
430
|
+
)
|
|
431
|
+
else: # follow the position in the partitura part
|
|
415
432
|
meta_events[part][to_ppq(ts.start.t)].append(
|
|
416
433
|
MetaMessage(
|
|
417
|
-
"time_signature",
|
|
434
|
+
"time_signature",
|
|
435
|
+
numerator=ts.beats,
|
|
436
|
+
denominator=ts.beat_type,
|
|
418
437
|
)
|
|
419
438
|
)
|
|
420
439
|
|
|
@@ -424,7 +443,6 @@ def save_score_midi(
|
|
|
424
443
|
)
|
|
425
444
|
|
|
426
445
|
for note in notes:
|
|
427
|
-
|
|
428
446
|
# key is a tuple (part_group, part, voice) that will be
|
|
429
447
|
# converted into a (track, channel) pair.
|
|
430
448
|
key = (pg, part, note.voice)
|