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.
Files changed (108) hide show
  1. {partitura-1.5.0 → partitura-1.6.0}/PKG-INFO +13 -2
  2. partitura-1.6.0/partitura/assets/score_example.mei +69 -0
  3. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/__init__.py +36 -8
  4. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/exportkern.py +1 -1
  5. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/exportmatch.py +7 -2
  6. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/exportmei.py +35 -8
  7. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/exportmusicxml.py +55 -10
  8. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importkern.py +195 -79
  9. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importmatch.py +89 -87
  10. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importmei.py +48 -12
  11. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importmidi.py +71 -20
  12. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importmusicxml.py +173 -10
  13. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importnakamura.py +1 -1
  14. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/matchfile_utils.py +2 -1
  15. {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/note_array_to_score.py +1 -1
  16. {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/note_features.py +138 -8
  17. {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/performance_codec.py +33 -22
  18. {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/performance_features.py +1 -1
  19. {partitura-1.5.0 → partitura-1.6.0}/partitura/performance.py +31 -7
  20. {partitura-1.5.0 → partitura-1.6.0}/partitura/score.py +495 -301
  21. {partitura-1.5.0 → partitura-1.6.0}/partitura/utils/__init__.py +4 -0
  22. {partitura-1.5.0 → partitura-1.6.0}/partitura/utils/generic.py +34 -2
  23. {partitura-1.5.0 → partitura-1.6.0}/partitura/utils/globals.py +200 -2
  24. {partitura-1.5.0 → partitura-1.6.0}/partitura/utils/misc.py +22 -1
  25. {partitura-1.5.0 → partitura-1.6.0}/partitura/utils/music.py +33 -5
  26. {partitura-1.5.0 → partitura-1.6.0}/partitura.egg-info/PKG-INFO +13 -2
  27. {partitura-1.5.0 → partitura-1.6.0}/partitura.egg-info/SOURCES.txt +3 -1
  28. {partitura-1.5.0 → partitura-1.6.0}/setup.py +1 -1
  29. partitura-1.6.0/tests/test_clef.py +105 -0
  30. partitura-1.6.0/tests/test_cross_staff.py +34 -0
  31. {partitura-1.5.0 → partitura-1.6.0}/tests/test_mei.py +8 -7
  32. {partitura-1.5.0 → partitura-1.6.0}/tests/test_merge_parts.py +10 -1
  33. {partitura-1.5.0 → partitura-1.6.0}/tests/test_metrical_position.py +2 -0
  34. {partitura-1.5.0 → partitura-1.6.0}/tests/test_midi_import.py +60 -2
  35. {partitura-1.5.0 → partitura-1.6.0}/tests/test_note_features.py +17 -1
  36. partitura-1.6.0/tests/test_performance.py +332 -0
  37. partitura-1.6.0/tests/test_performance_features.py +46 -0
  38. partitura-1.6.0/tests/test_urlload.py +26 -0
  39. {partitura-1.5.0 → partitura-1.6.0}/tests/test_utils.py +66 -0
  40. {partitura-1.5.0 → partitura-1.6.0}/tests/test_xml.py +64 -0
  41. partitura-1.5.0/partitura/assets/score_example.mei +0 -56
  42. partitura-1.5.0/tests/test_cross_staff_beaming.py +0 -20
  43. partitura-1.5.0/tests/test_performance.py +0 -140
  44. partitura-1.5.0/tests/test_performance_features.py +0 -42
  45. {partitura-1.5.0 → partitura-1.6.0}/LICENSE +0 -0
  46. {partitura-1.5.0 → partitura-1.6.0}/README.md +0 -0
  47. {partitura-1.5.0 → partitura-1.6.0}/partitura/__init__.py +0 -0
  48. {partitura-1.5.0 → partitura-1.6.0}/partitura/assets/musicxml.xsd +0 -0
  49. {partitura-1.5.0 → partitura-1.6.0}/partitura/assets/score_example.krn +0 -0
  50. {partitura-1.5.0 → partitura-1.6.0}/partitura/assets/score_example.mid +0 -0
  51. {partitura-1.5.0 → partitura-1.6.0}/partitura/assets/score_example.musicxml +0 -0
  52. {partitura-1.5.0 → partitura-1.6.0}/partitura/directions.py +0 -0
  53. {partitura-1.5.0 → partitura-1.6.0}/partitura/display.py +0 -0
  54. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/exportaudio.py +0 -0
  55. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/exportmidi.py +0 -0
  56. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/exportparangonada.py +0 -0
  57. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importdcml.py +0 -0
  58. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importmusic21.py +0 -0
  59. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/importparangonada.py +0 -0
  60. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/matchfile_base.py +0 -0
  61. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/matchlines_v0.py +0 -0
  62. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/matchlines_v1.py +0 -0
  63. {partitura-1.5.0 → partitura-1.6.0}/partitura/io/musescore.py +0 -0
  64. {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/__init__.py +0 -0
  65. {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/key_identification.py +0 -0
  66. {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/meter.py +0 -0
  67. {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/pitch_spelling.py +0 -0
  68. {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/tonal_tension.py +0 -0
  69. {partitura-1.5.0 → partitura-1.6.0}/partitura/musicanalysis/voice_separation.py +0 -0
  70. {partitura-1.5.0 → partitura-1.6.0}/partitura/utils/fluidsynth.py +0 -0
  71. {partitura-1.5.0 → partitura-1.6.0}/partitura/utils/normalize.py +0 -0
  72. {partitura-1.5.0 → partitura-1.6.0}/partitura/utils/synth.py +0 -0
  73. {partitura-1.5.0 → partitura-1.6.0}/partitura.egg-info/dependency_links.txt +0 -0
  74. {partitura-1.5.0 → partitura-1.6.0}/partitura.egg-info/requires.txt +0 -0
  75. {partitura-1.5.0 → partitura-1.6.0}/partitura.egg-info/top_level.txt +0 -0
  76. {partitura-1.5.0 → partitura-1.6.0}/setup.cfg +0 -0
  77. {partitura-1.5.0 → partitura-1.6.0}/tests/test_dcml_import.py +0 -0
  78. {partitura-1.5.0 → partitura-1.6.0}/tests/test_deprecations.py +0 -0
  79. {partitura-1.5.0 → partitura-1.6.0}/tests/test_display.py +0 -0
  80. {partitura-1.5.0 → partitura-1.6.0}/tests/test_fluidsynth.py +0 -0
  81. {partitura-1.5.0 → partitura-1.6.0}/tests/test_harmony.py +0 -0
  82. {partitura-1.5.0 → partitura-1.6.0}/tests/test_kern.py +0 -0
  83. {partitura-1.5.0 → partitura-1.6.0}/tests/test_key_estimation.py +0 -0
  84. {partitura-1.5.0 → partitura-1.6.0}/tests/test_load_performance.py +0 -0
  85. {partitura-1.5.0 → partitura-1.6.0}/tests/test_load_score.py +0 -0
  86. {partitura-1.5.0 → partitura-1.6.0}/tests/test_m21_import.py +0 -0
  87. {partitura-1.5.0 → partitura-1.6.0}/tests/test_match_export.py +0 -0
  88. {partitura-1.5.0 → partitura-1.6.0}/tests/test_match_import.py +0 -0
  89. {partitura-1.5.0 → partitura-1.6.0}/tests/test_midi_export.py +0 -0
  90. {partitura-1.5.0 → partitura-1.6.0}/tests/test_musescore.py +0 -0
  91. {partitura-1.5.0 → partitura-1.6.0}/tests/test_nakamura.py +0 -0
  92. {partitura-1.5.0 → partitura-1.6.0}/tests/test_new_divs.py +0 -0
  93. {partitura-1.5.0 → partitura-1.6.0}/tests/test_note_array.py +0 -0
  94. {partitura-1.5.0 → partitura-1.6.0}/tests/test_octave_shift.py +0 -0
  95. {partitura-1.5.0 → partitura-1.6.0}/tests/test_parangonada.py +0 -0
  96. {partitura-1.5.0 → partitura-1.6.0}/tests/test_part_properties.py +0 -0
  97. {partitura-1.5.0 → partitura-1.6.0}/tests/test_partial_measures.py +0 -0
  98. {partitura-1.5.0 → partitura-1.6.0}/tests/test_performance_codec.py +0 -0
  99. {partitura-1.5.0 → partitura-1.6.0}/tests/test_pianoroll.py +0 -0
  100. {partitura-1.5.0 → partitura-1.6.0}/tests/test_pitch_spelling.py +0 -0
  101. {partitura-1.5.0 → partitura-1.6.0}/tests/test_quarter_adjust.py +0 -0
  102. {partitura-1.5.0 → partitura-1.6.0}/tests/test_rest_array.py +0 -0
  103. {partitura-1.5.0 → partitura-1.6.0}/tests/test_synth.py +0 -0
  104. {partitura-1.5.0 → partitura-1.6.0}/tests/test_time_estimation.py +0 -0
  105. {partitura-1.5.0 → partitura-1.6.0}/tests/test_times.py +0 -0
  106. {partitura-1.5.0 → partitura-1.6.0}/tests/test_tonal_tension.py +0 -0
  107. {partitura-1.5.0 → partitura-1.6.0}/tests/test_transpose.py +0 -0
  108. {partitura-1.5.0 → partitura-1.6.0}/tests/test_voice_estimation.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: partitura
3
- Version: 1.5.0
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
- return load_musicxml(
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
- return load_score_midi(
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
- return load_mei(filename=filename)
108
+ score = load_mei(filename=filename)
81
109
  elif extension in [".kern", ".krn"]:
82
- return load_kern(
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
- return load_via_musescore(
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.5.0"
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.concatenate(np.array(list(duplicates.values()))).flatten()
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(self, measure_el, start, end):
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
- notations.append(
214
- etree.Element("tuplet", number="{}".format(number), type="start")
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: n.midi_pitch if hasattr(n, "midi_pitch") else -1, reverse=True
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.number is not None:
1057
- attrib["number"] = str(measure.number)
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(