partitura 1.5.0__tar.gz → 1.7.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 (111) hide show
  1. {partitura-1.5.0 → partitura-1.7.0}/PKG-INFO +14 -2
  2. partitura-1.7.0/partitura/assets/score_example.mei +69 -0
  3. {partitura-1.5.0 → partitura-1.7.0}/partitura/assets/score_example.musicxml +3 -0
  4. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/__init__.py +36 -8
  5. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/exportkern.py +1 -1
  6. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/exportmatch.py +42 -28
  7. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/exportmei.py +35 -8
  8. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/exportmidi.py +13 -13
  9. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/exportmusicxml.py +88 -10
  10. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/importdcml.py +1 -1
  11. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/importkern.py +195 -79
  12. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/importmatch.py +128 -104
  13. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/importmei.py +48 -12
  14. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/importmidi.py +83 -31
  15. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/importmusicxml.py +220 -14
  16. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/importnakamura.py +1 -1
  17. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/matchfile_utils.py +2 -1
  18. {partitura-1.5.0 → partitura-1.7.0}/partitura/musicanalysis/note_array_to_score.py +1 -1
  19. {partitura-1.5.0 → partitura-1.7.0}/partitura/musicanalysis/note_features.py +138 -8
  20. {partitura-1.5.0 → partitura-1.7.0}/partitura/musicanalysis/performance_codec.py +53 -27
  21. {partitura-1.5.0 → partitura-1.7.0}/partitura/musicanalysis/performance_features.py +24 -3
  22. {partitura-1.5.0 → partitura-1.7.0}/partitura/performance.py +68 -22
  23. {partitura-1.5.0 → partitura-1.7.0}/partitura/score.py +701 -380
  24. {partitura-1.5.0 → partitura-1.7.0}/partitura/utils/__init__.py +4 -1
  25. {partitura-1.5.0 → partitura-1.7.0}/partitura/utils/generic.py +78 -41
  26. {partitura-1.5.0 → partitura-1.7.0}/partitura/utils/globals.py +200 -2
  27. {partitura-1.5.0 → partitura-1.7.0}/partitura/utils/misc.py +24 -3
  28. {partitura-1.5.0 → partitura-1.7.0}/partitura/utils/music.py +355 -94
  29. {partitura-1.5.0 → partitura-1.7.0}/partitura.egg-info/PKG-INFO +14 -2
  30. {partitura-1.5.0 → partitura-1.7.0}/partitura.egg-info/SOURCES.txt +4 -1
  31. {partitura-1.5.0 → partitura-1.7.0}/setup.py +1 -1
  32. partitura-1.7.0/tests/test_clef.py +105 -0
  33. partitura-1.7.0/tests/test_cross_staff.py +34 -0
  34. partitura-1.7.0/tests/test_iter.py +164 -0
  35. {partitura-1.5.0 → partitura-1.7.0}/tests/test_mei.py +8 -7
  36. {partitura-1.5.0 → partitura-1.7.0}/tests/test_merge_parts.py +10 -1
  37. {partitura-1.5.0 → partitura-1.7.0}/tests/test_metrical_position.py +2 -0
  38. {partitura-1.5.0 → partitura-1.7.0}/tests/test_midi_import.py +196 -3
  39. {partitura-1.5.0 → partitura-1.7.0}/tests/test_note_features.py +17 -1
  40. partitura-1.7.0/tests/test_part_properties.py +103 -0
  41. partitura-1.7.0/tests/test_performance.py +332 -0
  42. partitura-1.7.0/tests/test_performance_features.py +46 -0
  43. partitura-1.7.0/tests/test_transpose.py +43 -0
  44. partitura-1.7.0/tests/test_urlload.py +26 -0
  45. {partitura-1.5.0 → partitura-1.7.0}/tests/test_utils.py +130 -0
  46. {partitura-1.5.0 → partitura-1.7.0}/tests/test_xml.py +77 -0
  47. partitura-1.5.0/partitura/assets/score_example.mei +0 -56
  48. partitura-1.5.0/tests/test_cross_staff_beaming.py +0 -20
  49. partitura-1.5.0/tests/test_part_properties.py +0 -38
  50. partitura-1.5.0/tests/test_performance.py +0 -140
  51. partitura-1.5.0/tests/test_performance_features.py +0 -42
  52. partitura-1.5.0/tests/test_transpose.py +0 -26
  53. {partitura-1.5.0 → partitura-1.7.0}/LICENSE +0 -0
  54. {partitura-1.5.0 → partitura-1.7.0}/README.md +0 -0
  55. {partitura-1.5.0 → partitura-1.7.0}/partitura/__init__.py +0 -0
  56. {partitura-1.5.0 → partitura-1.7.0}/partitura/assets/musicxml.xsd +0 -0
  57. {partitura-1.5.0 → partitura-1.7.0}/partitura/assets/score_example.krn +0 -0
  58. {partitura-1.5.0 → partitura-1.7.0}/partitura/assets/score_example.mid +0 -0
  59. {partitura-1.5.0 → partitura-1.7.0}/partitura/directions.py +0 -0
  60. {partitura-1.5.0 → partitura-1.7.0}/partitura/display.py +0 -0
  61. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/exportaudio.py +0 -0
  62. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/exportparangonada.py +0 -0
  63. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/importmusic21.py +0 -0
  64. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/importparangonada.py +0 -0
  65. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/matchfile_base.py +0 -0
  66. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/matchlines_v0.py +0 -0
  67. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/matchlines_v1.py +0 -0
  68. {partitura-1.5.0 → partitura-1.7.0}/partitura/io/musescore.py +0 -0
  69. {partitura-1.5.0 → partitura-1.7.0}/partitura/musicanalysis/__init__.py +0 -0
  70. {partitura-1.5.0 → partitura-1.7.0}/partitura/musicanalysis/key_identification.py +0 -0
  71. {partitura-1.5.0 → partitura-1.7.0}/partitura/musicanalysis/meter.py +0 -0
  72. {partitura-1.5.0 → partitura-1.7.0}/partitura/musicanalysis/pitch_spelling.py +0 -0
  73. {partitura-1.5.0 → partitura-1.7.0}/partitura/musicanalysis/tonal_tension.py +0 -0
  74. {partitura-1.5.0 → partitura-1.7.0}/partitura/musicanalysis/voice_separation.py +0 -0
  75. {partitura-1.5.0 → partitura-1.7.0}/partitura/utils/fluidsynth.py +0 -0
  76. {partitura-1.5.0 → partitura-1.7.0}/partitura/utils/normalize.py +0 -0
  77. {partitura-1.5.0 → partitura-1.7.0}/partitura/utils/synth.py +0 -0
  78. {partitura-1.5.0 → partitura-1.7.0}/partitura.egg-info/dependency_links.txt +0 -0
  79. {partitura-1.5.0 → partitura-1.7.0}/partitura.egg-info/requires.txt +0 -0
  80. {partitura-1.5.0 → partitura-1.7.0}/partitura.egg-info/top_level.txt +0 -0
  81. {partitura-1.5.0 → partitura-1.7.0}/setup.cfg +0 -0
  82. {partitura-1.5.0 → partitura-1.7.0}/tests/test_dcml_import.py +0 -0
  83. {partitura-1.5.0 → partitura-1.7.0}/tests/test_deprecations.py +0 -0
  84. {partitura-1.5.0 → partitura-1.7.0}/tests/test_display.py +0 -0
  85. {partitura-1.5.0 → partitura-1.7.0}/tests/test_fluidsynth.py +0 -0
  86. {partitura-1.5.0 → partitura-1.7.0}/tests/test_harmony.py +0 -0
  87. {partitura-1.5.0 → partitura-1.7.0}/tests/test_kern.py +0 -0
  88. {partitura-1.5.0 → partitura-1.7.0}/tests/test_key_estimation.py +0 -0
  89. {partitura-1.5.0 → partitura-1.7.0}/tests/test_load_performance.py +0 -0
  90. {partitura-1.5.0 → partitura-1.7.0}/tests/test_load_score.py +0 -0
  91. {partitura-1.5.0 → partitura-1.7.0}/tests/test_m21_import.py +0 -0
  92. {partitura-1.5.0 → partitura-1.7.0}/tests/test_match_export.py +0 -0
  93. {partitura-1.5.0 → partitura-1.7.0}/tests/test_match_import.py +0 -0
  94. {partitura-1.5.0 → partitura-1.7.0}/tests/test_midi_export.py +0 -0
  95. {partitura-1.5.0 → partitura-1.7.0}/tests/test_musescore.py +0 -0
  96. {partitura-1.5.0 → partitura-1.7.0}/tests/test_nakamura.py +0 -0
  97. {partitura-1.5.0 → partitura-1.7.0}/tests/test_new_divs.py +0 -0
  98. {partitura-1.5.0 → partitura-1.7.0}/tests/test_note_array.py +0 -0
  99. {partitura-1.5.0 → partitura-1.7.0}/tests/test_octave_shift.py +0 -0
  100. {partitura-1.5.0 → partitura-1.7.0}/tests/test_parangonada.py +0 -0
  101. {partitura-1.5.0 → partitura-1.7.0}/tests/test_partial_measures.py +0 -0
  102. {partitura-1.5.0 → partitura-1.7.0}/tests/test_performance_codec.py +0 -0
  103. {partitura-1.5.0 → partitura-1.7.0}/tests/test_pianoroll.py +0 -0
  104. {partitura-1.5.0 → partitura-1.7.0}/tests/test_pitch_spelling.py +0 -0
  105. {partitura-1.5.0 → partitura-1.7.0}/tests/test_quarter_adjust.py +0 -0
  106. {partitura-1.5.0 → partitura-1.7.0}/tests/test_rest_array.py +0 -0
  107. {partitura-1.5.0 → partitura-1.7.0}/tests/test_synth.py +0 -0
  108. {partitura-1.5.0 → partitura-1.7.0}/tests/test_time_estimation.py +0 -0
  109. {partitura-1.5.0 → partitura-1.7.0}/tests/test_times.py +0 -0
  110. {partitura-1.5.0 → partitura-1.7.0}/tests/test_tonal_tension.py +0 -0
  111. {partitura-1.5.0 → partitura-1.7.0}/tests/test_voice_estimation.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: partitura
3
- Version: 1.5.0
3
+ Version: 1.7.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,18 @@ 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: license-file
33
+ Dynamic: requires-dist
34
+ Dynamic: requires-python
35
+ Dynamic: summary
24
36
 
25
37
 
26
38
  [//]: # (<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>
@@ -13,6 +13,9 @@
13
13
  <measure number="1">
14
14
  <attributes>
15
15
  <divisions>12</divisions>
16
+ <key>
17
+ <fifths>0</fifths>
18
+ </key>
16
19
  <time>
17
20
  <beats>4</beats>
18
21
  <beat-type>4</beat-type>
@@ -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,
@@ -71,7 +70,7 @@ def matchfile_from_alignment(
71
70
  piece: Optional[str] = None,
72
71
  score_filename: Optional[PathLike] = None,
73
72
  performance_filename: Optional[PathLike] = None,
74
- assume_part_unfolded: bool = False,
73
+ assume_part_unfolded: bool = True,
75
74
  tempo_indication: Optional[str] = None,
76
75
  diff_score_version_notes: Optional[list] = None,
77
76
  version: Version = LATEST_VERSION,
@@ -108,7 +107,7 @@ def matchfile_from_alignment(
108
107
  Whether to assume that the part has been unfolded according to the
109
108
  repetitions in the alignment. If False, the part will be automatically
110
109
  unfolded to have maximal coverage of the notes in the alignment.
111
- See `partitura.score.unfold_part_alignment`.
110
+ See `partitura.score.unfold_part_alignment`, defaults to True.
112
111
  tempo_indication : str or None
113
112
  The tempo direction indicated in the beginning of the score
114
113
  diff_score_version_notes : list or None
@@ -211,18 +210,28 @@ def matchfile_from_alignment(
211
210
  # Info for sorting lines
212
211
  snote_sort_info = dict()
213
212
  for (mnum, msd, msb), m in zip(measure_starts, measures):
213
+ if mnum == 0:
214
+ # handle offsets in anacrusis measure
215
+ ts_num, ts_den, _ = spart.time_signature_map(0)
216
+ dpq = int(spart.quarter_duration_map(0))
217
+ measure_dur_in_divs = m.end.t - m.start.t
218
+ expected_measure_dur = ts_num * 4 / ts_den * dpq
219
+ if measure_dur_in_divs < expected_measure_dur:
220
+ msd -= expected_measure_dur - measure_dur_in_divs
221
+ msb -= (expected_measure_dur - measure_dur_in_divs) / dpq * ts_den / 4
222
+
214
223
  time_signatures = spart.iter_all(score.TimeSignature, m.start, m.end)
215
224
 
216
225
  for tsig in time_signatures:
217
226
  time_divs = int(tsig.start.t)
218
227
  time_beats = float(beat_map(time_divs))
228
+ ts_num, ts_den, _ = spart.time_signature_map(tsig.start.t)
219
229
  dpq = int(spart.quarter_duration_map(time_divs))
230
+ divs_per_beat = 4 / ts_den * dpq
220
231
  beat = int((time_beats - msb) // 1)
221
232
 
222
- ts_num, ts_den, _ = spart.time_signature_map(tsig.start.t)
223
-
224
233
  moffset_divs = Fraction(
225
- int(time_divs - msd - beat * dpq), (int(ts_den) * dpq)
234
+ int(time_divs - msd - beat * divs_per_beat), int(ts_den * divs_per_beat)
226
235
  )
227
236
 
228
237
  scoreprop_lines["time_signatures"].append(
@@ -248,15 +257,15 @@ def matchfile_from_alignment(
248
257
  key_signatures = spart.iter_all(score.KeySignature, m.start, m.end)
249
258
 
250
259
  for ksig in key_signatures:
251
- time_divs = int(tsig.start.t)
260
+ time_divs = int(ksig.start.t)
252
261
  time_beats = float(beat_map(time_divs))
262
+ ts_num, ts_den, _ = spart.time_signature_map(ksig.start.t)
253
263
  dpq = int(spart.quarter_duration_map(time_divs))
264
+ divs_per_beat = 4 / ts_den * dpq
254
265
  beat = int((time_beats - msb) // 1)
255
266
 
256
- ts_num, ts_den, _ = spart.time_signature_map(tsig.start.t)
257
-
258
267
  moffset_divs = Fraction(
259
- int(time_divs - msd - beat * dpq), (int(ts_den) * dpq)
268
+ int(time_divs - msd - beat * divs_per_beat), int(ts_den * divs_per_beat)
260
269
  )
261
270
 
262
271
  scoreprop_lines["key_signatures"].append(
@@ -283,27 +292,28 @@ def matchfile_from_alignment(
283
292
  snotes = spart.iter_all(score.Note, m.start, m.end, include_subclasses=True)
284
293
  # Beginning of each measure
285
294
  for snote in snotes:
286
- onset_divs, offset_divs = snote.start.t, snote.start.t + snote.duration_tied
295
+ onset_divs = snote.start.t
296
+ offset_divs = snote.start.t + snote.duration_tied
287
297
  duration_divs = offset_divs - onset_divs
288
-
298
+ # beat computations
289
299
  onset_beats, offset_beats = beat_map([onset_divs, offset_divs])
290
-
291
- dpq = int(spart.quarter_duration_map(onset_divs))
292
-
293
- beat = int((onset_beats - msb) // 1)
294
-
300
+ duration_beats = offset_beats - onset_beats
301
+ beat = int((onset_beats - msb) // 1) # beat field of the snote
302
+ # quarter, div, symbolic computation
295
303
  ts_num, ts_den, _ = spart.time_signature_map(snote.start.t)
296
-
297
- duration_symb = Fraction(duration_divs, dpq * 4)
298
-
299
- beat = int((onset_divs - msd) // dpq)
300
-
301
- moffset_divs = Fraction(int(onset_divs - msd - beat * dpq), (dpq * 4))
304
+ dpq = int(spart.quarter_duration_map(onset_divs))
305
+ duration_symb = Fraction(
306
+ duration_divs, dpq * 4
307
+ ) # compute duration from quarters/divs
308
+ divs_per_beat = 4 / ts_den * dpq
309
+ moffset_divs = Fraction(
310
+ int(onset_divs - msd - beat * divs_per_beat),
311
+ int(ts_den * divs_per_beat),
312
+ )
302
313
 
303
314
  if debug:
304
- duration_beats = offset_beats - onset_beats
305
315
  moffset_beat = (onset_beats - msb - beat) / ts_den
306
- assert np.isclose(float(duration_symb), duration_beats)
316
+ assert np.isclose(float(duration_symb), duration_beats / ts_den)
307
317
  assert np.isclose(moffset_beat, float(moffset_divs))
308
318
 
309
319
  score_attributes_list = []
@@ -313,6 +323,7 @@ def matchfile_from_alignment(
313
323
  staff = getattr(snote, "staff", None)
314
324
  ornaments = getattr(snote, "ornaments", None)
315
325
  fermata = getattr(snote, "fermata", None)
326
+ technical = getattr(snote, "technical", None)
316
327
 
317
328
  if voice is not None:
318
329
  score_attributes_list.append(f"v{voice}")
@@ -329,6 +340,11 @@ def matchfile_from_alignment(
329
340
  if fermata is not None:
330
341
  score_attributes_list.append("fermata")
331
342
 
343
+ if technical is not None:
344
+ for tech_el in technical:
345
+ if isinstance(tech_el, score.Fingering):
346
+ score_attributes_list.append(f"fingering{tech_el.fingering}")
347
+
332
348
  if isinstance(snote, score.GraceNote):
333
349
  score_attributes_list.append("grace")
334
350
 
@@ -417,7 +433,7 @@ def matchfile_from_alignment(
417
433
  duplicates[tuple(v)] = idx
418
434
  voice_overlap_note_ids = []
419
435
  if len(duplicates) > 0:
420
- duplicate_idx = np.concatenate(np.array(list(duplicates.values()))).flatten()
436
+ duplicate_idx = np.hstack(list(duplicates.values()))
421
437
  voice_overlap_note_ids = list(sna[duplicate_idx]["id"])
422
438
 
423
439
  for al_note in alignment:
@@ -503,9 +519,7 @@ def matchfile_from_alignment(
503
519
 
504
520
  # Concatenate all lines
505
521
  all_match_lines += list(note_lines) + pedal_lines
506
-
507
522
  matchfile = MatchFile(lines=all_match_lines)
508
-
509
523
  return matchfile
510
524
 
511
525
 
@@ -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(
@@ -36,34 +36,34 @@ def map_to_track_channel(note_keys, mode):
36
36
  if mode == 0:
37
37
  trk = tr_helper.setdefault(p, len(tr_helper))
38
38
  ch1 = ch_helper.setdefault(p, {})
39
- ch2 = ch1.setdefault(v, len(ch1) + 1)
39
+ ch2 = ch1.setdefault(v, len(ch1))
40
40
  track[(pg, p, v)] = trk
41
41
  channel[(pg, p, v)] = ch2
42
42
  elif mode == 1:
43
43
  trk = tr_helper.setdefault(pg, len(tr_helper))
44
44
  ch1 = ch_helper.setdefault(pg, {})
45
- ch2 = ch1.setdefault(p, len(ch1) + 1)
45
+ ch2 = ch1.setdefault(p, len(ch1))
46
46
  track[(pg, p, v)] = trk
47
47
  channel[(pg, p, v)] = ch2
48
48
  elif mode == 2:
49
49
  track[(pg, p, v)] = 0
50
- ch = ch_helper.setdefault(p, len(ch_helper) + 1)
50
+ ch = ch_helper.setdefault(p, len(ch_helper))
51
51
  channel[(pg, p, v)] = ch
52
52
  elif mode == 3:
53
53
  trk = tr_helper.setdefault(p, len(tr_helper))
54
54
  track[(pg, p, v)] = trk
55
- channel[(pg, p, v)] = 1
55
+ channel[(pg, p, v)] = 0
56
56
  elif mode == 4:
57
57
  track[(pg, p, v)] = 0
58
- channel[(pg, p, v)] = 1
58
+ channel[(pg, p, v)] = 0
59
59
  elif mode == 5:
60
60
  trk = tr_helper.setdefault((p, v), len(tr_helper))
61
61
  track[(pg, p, v)] = trk
62
- channel[(pg, p, v)] = 1
62
+ channel[(pg, p, v)] = 0
63
63
  else:
64
64
  raise Exception("unsupported part/voice assign mode {}".format(mode))
65
65
 
66
- result = dict((k, (track.get(k, 0), channel.get(k, 1))) for k in note_keys)
66
+ result = dict((k, (track.get(k, 0), channel.get(k, 0))) for k in note_keys)
67
67
  # for (pg, p, voice), v in result.items():
68
68
  # pgn = pg.group_name if hasattr(pg, 'group_name') else pg.id
69
69
  # print(pgn, p.id, voice)
@@ -177,7 +177,7 @@ def save_performance_midi(
177
177
 
178
178
  for c in performed_part.controls:
179
179
  track = c.get("track", 0)
180
- ch = c.get("channel", 1)
180
+ ch = c.get("channel", 0)
181
181
  t = int(np.round(10**6 * ppq * c["time"] / mpq))
182
182
  track_events[track][t].append(
183
183
  Message(
@@ -190,7 +190,7 @@ def save_performance_midi(
190
190
 
191
191
  for n in performed_part.notes:
192
192
  track = n.get("track", 0)
193
- ch = n.get("channel", 1)
193
+ ch = n.get("channel", 0)
194
194
  t_on = int(np.round(10**6 * ppq * n["note_on"] / mpq))
195
195
  t_off = int(np.round(10**6 * ppq * n["note_off"] / mpq))
196
196
  vel = n.get("velocity", default_velocity)
@@ -203,7 +203,7 @@ def save_performance_midi(
203
203
 
204
204
  for p in performed_part.programs:
205
205
  track = p.get("track", 0)
206
- ch = p.get("channel", 1)
206
+ ch = p.get("channel", 0)
207
207
  t = int(np.round(10**6 * ppq * p["time"] / mpq))
208
208
  track_events[track][t].append(
209
209
  Message("program_change", program=int(p["program"]), channel=ch)
@@ -215,11 +215,11 @@ def save_performance_midi(
215
215
  list(
216
216
  set(
217
217
  [
218
- (c.get("channel", 1), c.get("track", 0))
218
+ (c.get("channel", 0), c.get("track", 0))
219
219
  for c in performed_part.controls
220
220
  ]
221
221
  + [
222
- (n.get("channel", 1), n.get("track", 0))
222
+ (n.get("channel", 0), n.get("track", 0))
223
223
  for n in performed_part.notes
224
224
  ]
225
225
  )
@@ -395,7 +395,7 @@ def save_score_midi(
395
395
 
396
396
  def to_ppq(t):
397
397
  # convert div times to new ppq
398
- return int(ppq * (qm(t) - ftp))
398
+ return round(ppq * (qm(t) - ftp))
399
399
 
400
400
  for tp in part.iter_all(score.Tempo):
401
401
  tempos[to_ppq(tp.start.t)] = MetaMessage(