partitura 1.4.0__tar.gz → 1.5.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 (105) hide show
  1. {partitura-1.4.0 → partitura-1.5.0}/PKG-INFO +1 -1
  2. {partitura-1.4.0 → partitura-1.5.0}/partitura/__init__.py +6 -1
  3. {partitura-1.4.0 → partitura-1.5.0}/partitura/directions.py +1 -23
  4. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/__init__.py +3 -1
  5. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/exportaudio.py +79 -4
  6. partitura-1.5.0/partitura/io/exportkern.py +344 -0
  7. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/exportmatch.py +5 -2
  8. partitura-1.5.0/partitura/io/exportmei.py +618 -0
  9. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/exportmidi.py +41 -1
  10. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/exportmusicxml.py +11 -0
  11. partitura-1.5.0/partitura/io/importdcml.py +353 -0
  12. partitura-1.5.0/partitura/io/importkern.py +797 -0
  13. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/importmatch.py +21 -7
  14. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/importmidi.py +120 -17
  15. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/importmusic21.py +15 -9
  16. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/importmusicxml.py +54 -4
  17. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/importnakamura.py +1 -3
  18. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/matchfile_base.py +14 -1
  19. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/matchfile_utils.py +1 -0
  20. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/matchlines_v0.py +5 -1
  21. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/matchlines_v1.py +8 -6
  22. {partitura-1.4.0 → partitura-1.5.0}/partitura/musicanalysis/key_identification.py +10 -67
  23. {partitura-1.4.0 → partitura-1.5.0}/partitura/musicanalysis/meter.py +14 -16
  24. {partitura-1.4.0 → partitura-1.5.0}/partitura/musicanalysis/performance_codec.py +3 -2
  25. {partitura-1.4.0 → partitura-1.5.0}/partitura/performance.py +6 -0
  26. {partitura-1.4.0 → partitura-1.5.0}/partitura/score.py +1025 -20
  27. partitura-1.5.0/partitura/utils/fluidsynth.py +326 -0
  28. partitura-1.5.0/partitura/utils/globals.py +545 -0
  29. {partitura-1.4.0 → partitura-1.5.0}/partitura/utils/misc.py +26 -0
  30. {partitura-1.4.0 → partitura-1.5.0}/partitura/utils/music.py +222 -312
  31. {partitura-1.4.0 → partitura-1.5.0}/partitura/utils/normalize.py +1 -3
  32. {partitura-1.4.0 → partitura-1.5.0}/partitura/utils/synth.py +18 -38
  33. {partitura-1.4.0 → partitura-1.5.0}/partitura.egg-info/PKG-INFO +1 -1
  34. {partitura-1.4.0 → partitura-1.5.0}/partitura.egg-info/SOURCES.txt +6 -0
  35. {partitura-1.4.0 → partitura-1.5.0}/setup.py +1 -1
  36. partitura-1.5.0/tests/test_dcml_import.py +20 -0
  37. partitura-1.5.0/tests/test_fluidsynth.py +97 -0
  38. partitura-1.5.0/tests/test_kern.py +84 -0
  39. {partitura-1.4.0 → partitura-1.5.0}/tests/test_match_import.py +5 -1
  40. {partitura-1.4.0 → partitura-1.5.0}/tests/test_mei.py +38 -4
  41. {partitura-1.4.0 → partitura-1.5.0}/tests/test_note_array.py +3 -5
  42. {partitura-1.4.0 → partitura-1.5.0}/tests/test_synth.py +1 -1
  43. {partitura-1.4.0 → partitura-1.5.0}/tests/test_utils.py +45 -45
  44. partitura-1.4.0/partitura/io/exportmei.py +0 -2096
  45. partitura-1.4.0/partitura/io/importkern.py +0 -626
  46. partitura-1.4.0/tests/test_kern.py +0 -48
  47. {partitura-1.4.0 → partitura-1.5.0}/LICENSE +0 -0
  48. {partitura-1.4.0 → partitura-1.5.0}/README.md +0 -0
  49. {partitura-1.4.0 → partitura-1.5.0}/partitura/assets/musicxml.xsd +0 -0
  50. {partitura-1.4.0 → partitura-1.5.0}/partitura/assets/score_example.krn +0 -0
  51. {partitura-1.4.0 → partitura-1.5.0}/partitura/assets/score_example.mei +0 -0
  52. {partitura-1.4.0 → partitura-1.5.0}/partitura/assets/score_example.mid +0 -0
  53. {partitura-1.4.0 → partitura-1.5.0}/partitura/assets/score_example.musicxml +0 -0
  54. {partitura-1.4.0 → partitura-1.5.0}/partitura/display.py +0 -0
  55. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/exportparangonada.py +0 -0
  56. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/importmei.py +0 -0
  57. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/importparangonada.py +0 -0
  58. {partitura-1.4.0 → partitura-1.5.0}/partitura/io/musescore.py +0 -0
  59. {partitura-1.4.0 → partitura-1.5.0}/partitura/musicanalysis/__init__.py +0 -0
  60. {partitura-1.4.0 → partitura-1.5.0}/partitura/musicanalysis/note_array_to_score.py +0 -0
  61. {partitura-1.4.0 → partitura-1.5.0}/partitura/musicanalysis/note_features.py +0 -0
  62. {partitura-1.4.0 → partitura-1.5.0}/partitura/musicanalysis/performance_features.py +0 -0
  63. {partitura-1.4.0 → partitura-1.5.0}/partitura/musicanalysis/pitch_spelling.py +0 -0
  64. {partitura-1.4.0 → partitura-1.5.0}/partitura/musicanalysis/tonal_tension.py +0 -0
  65. {partitura-1.4.0 → partitura-1.5.0}/partitura/musicanalysis/voice_separation.py +0 -0
  66. {partitura-1.4.0 → partitura-1.5.0}/partitura/utils/__init__.py +0 -0
  67. {partitura-1.4.0 → partitura-1.5.0}/partitura/utils/generic.py +0 -0
  68. {partitura-1.4.0 → partitura-1.5.0}/partitura.egg-info/dependency_links.txt +0 -0
  69. {partitura-1.4.0 → partitura-1.5.0}/partitura.egg-info/requires.txt +0 -0
  70. {partitura-1.4.0 → partitura-1.5.0}/partitura.egg-info/top_level.txt +0 -0
  71. {partitura-1.4.0 → partitura-1.5.0}/setup.cfg +0 -0
  72. {partitura-1.4.0 → partitura-1.5.0}/tests/test_cross_staff_beaming.py +0 -0
  73. {partitura-1.4.0 → partitura-1.5.0}/tests/test_deprecations.py +0 -0
  74. {partitura-1.4.0 → partitura-1.5.0}/tests/test_display.py +0 -0
  75. {partitura-1.4.0 → partitura-1.5.0}/tests/test_harmony.py +0 -0
  76. {partitura-1.4.0 → partitura-1.5.0}/tests/test_key_estimation.py +0 -0
  77. {partitura-1.4.0 → partitura-1.5.0}/tests/test_load_performance.py +0 -0
  78. {partitura-1.4.0 → partitura-1.5.0}/tests/test_load_score.py +0 -0
  79. {partitura-1.4.0 → partitura-1.5.0}/tests/test_m21_import.py +0 -0
  80. {partitura-1.4.0 → partitura-1.5.0}/tests/test_match_export.py +0 -0
  81. {partitura-1.4.0 → partitura-1.5.0}/tests/test_merge_parts.py +0 -0
  82. {partitura-1.4.0 → partitura-1.5.0}/tests/test_metrical_position.py +0 -0
  83. {partitura-1.4.0 → partitura-1.5.0}/tests/test_midi_export.py +0 -0
  84. {partitura-1.4.0 → partitura-1.5.0}/tests/test_midi_import.py +0 -0
  85. {partitura-1.4.0 → partitura-1.5.0}/tests/test_musescore.py +0 -0
  86. {partitura-1.4.0 → partitura-1.5.0}/tests/test_nakamura.py +0 -0
  87. {partitura-1.4.0 → partitura-1.5.0}/tests/test_new_divs.py +0 -0
  88. {partitura-1.4.0 → partitura-1.5.0}/tests/test_note_features.py +0 -0
  89. {partitura-1.4.0 → partitura-1.5.0}/tests/test_octave_shift.py +0 -0
  90. {partitura-1.4.0 → partitura-1.5.0}/tests/test_parangonada.py +0 -0
  91. {partitura-1.4.0 → partitura-1.5.0}/tests/test_part_properties.py +0 -0
  92. {partitura-1.4.0 → partitura-1.5.0}/tests/test_partial_measures.py +0 -0
  93. {partitura-1.4.0 → partitura-1.5.0}/tests/test_performance.py +0 -0
  94. {partitura-1.4.0 → partitura-1.5.0}/tests/test_performance_codec.py +0 -0
  95. {partitura-1.4.0 → partitura-1.5.0}/tests/test_performance_features.py +0 -0
  96. {partitura-1.4.0 → partitura-1.5.0}/tests/test_pianoroll.py +0 -0
  97. {partitura-1.4.0 → partitura-1.5.0}/tests/test_pitch_spelling.py +0 -0
  98. {partitura-1.4.0 → partitura-1.5.0}/tests/test_quarter_adjust.py +0 -0
  99. {partitura-1.4.0 → partitura-1.5.0}/tests/test_rest_array.py +0 -0
  100. {partitura-1.4.0 → partitura-1.5.0}/tests/test_time_estimation.py +0 -0
  101. {partitura-1.4.0 → partitura-1.5.0}/tests/test_times.py +0 -0
  102. {partitura-1.4.0 → partitura-1.5.0}/tests/test_tonal_tension.py +0 -0
  103. {partitura-1.4.0 → partitura-1.5.0}/tests/test_transpose.py +0 -0
  104. {partitura-1.4.0 → partitura-1.5.0}/tests/test_voice_estimation.py +0 -0
  105. {partitura-1.4.0 → partitura-1.5.0}/tests/test_xml.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: partitura
3
- Version: 1.4.0
3
+ Version: 1.5.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
@@ -15,6 +15,7 @@ from .io.exportmusicxml import save_musicxml
15
15
  from .io.importmei import load_mei
16
16
  from .io.importkern import load_kern
17
17
  from .io.importmusic21 import load_music21
18
+ from .io.importdcml import load_dcml
18
19
  from .io.importmidi import load_score_midi, load_performance_midi, midi_to_notearray
19
20
  from .io.exportmidi import save_score_midi, save_performance_midi
20
21
  from .io.importmatch import load_match
@@ -22,7 +23,8 @@ from .io.exportmatch import save_match
22
23
  from .io.importnakamura import load_nakamuramatch, load_nakamuracorresp
23
24
  from .io.importparangonada import load_parangonada_csv
24
25
  from .io.exportparangonada import save_parangonada_csv, save_csv_for_parangonada
25
- from .io.exportaudio import save_wav
26
+ from .io.exportaudio import save_wav, save_wav_fluidsynth
27
+ from .io.exportmei import save_mei
26
28
  from .display import render
27
29
  from . import musicanalysis
28
30
  from .musicanalysis import make_note_features, compute_note_array, full_note_array
@@ -56,9 +58,12 @@ __all__ = [
56
58
  "save_performance_midi",
57
59
  "load_match",
58
60
  "save_match",
61
+ "load_dcml",
59
62
  "load_nakamuramatch",
60
63
  "load_nakamuracorresp",
61
64
  "load_parangonada_csv",
62
65
  "save_parangonada_csv",
66
+ "save_wav",
67
+ "save_wav_fluidsynth",
63
68
  "render",
64
69
  ]
@@ -13,6 +13,7 @@ The functionality is provided by the function `parse_words`
13
13
 
14
14
  import re
15
15
  import warnings
16
+ from partitura.utils.globals import UNABBREVS
16
17
 
17
18
  try:
18
19
  from lark import Lark
@@ -51,29 +52,6 @@ def join_items(items):
51
52
  )
52
53
 
53
54
 
54
- UNABBREVS = [
55
- (re.compile(r"(crescendo|cresc\.?)"), "crescendo"),
56
- (re.compile(r"(smorzando|smorz\.?)"), "smorzando"),
57
- (re.compile(r"(decrescendo|(decresc|decr|dimin|dim)\.?)"), "diminuendo"),
58
- (re.compile(r"((acceler|accel|acc)\.?)"), "accelerando"),
59
- (re.compile(r"(ritenente|riten\.?)"), "ritenuto"),
60
- (re.compile(r"((ritard|rit)\.?)"), "ritardando"),
61
- (re.compile(r"((rallent|rall)\.?)"), "rallentando"),
62
- (re.compile(r"(dolciss\.?)"), "dolcissimo"),
63
- (re.compile(r"((sosten|sost)\.?)"), "sostenuto"),
64
- (re.compile(r"(delicatiss\.?)"), "delicatissimo"),
65
- (re.compile(r"(leggieramente|leggiermente|leggiero|legg\.?)"), "leggiero"),
66
- (re.compile(r"(leggierissimo|(leggieriss\.?))"), "leggierissimo"),
67
- (re.compile(r"(scherz\.?)"), "scherzando"),
68
- (re.compile(r"(tenute|ten\.?)"), "tenuto"),
69
- (re.compile(r"(allegretto)"), "allegro"),
70
- (re.compile(r"(espress\.?)"), "espressivo"),
71
- (re.compile(r"(ligato)"), "legato"),
72
- (re.compile(r"(ligatissimo)"), "legatissimo"),
73
- (re.compile(r"((rinforz|rinf|rfz|rf)\.?)"), "rinforzando"),
74
- ]
75
-
76
-
77
55
  def unabbreviate(s):
78
56
  for p, v in UNABBREVS:
79
57
  if p.match(s):
@@ -12,10 +12,12 @@ from .musescore import load_via_musescore
12
12
  from .importmatch import load_match
13
13
  from .importmei import load_mei
14
14
  from .importkern import load_kern
15
+ from .exportkern import save_kern
15
16
  from .importparangonada import load_parangonada_csv
16
17
  from .exportparangonada import save_parangonada_csv
17
18
  from .importmusic21 import load_music21
18
-
19
+ from .exportmei import save_mei
20
+ from .importdcml import load_dcml
19
21
  from partitura.utils.misc import (
20
22
  deprecated_alias,
21
23
  deprecated_parameter,
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/python
2
2
  # -*- coding: utf-8 -*-
3
3
  """
4
- This module contains methods to synthesize Partitura object to wav using
5
- additive synthesis
4
+ This module contains methods to synthesize a Partitura ScoreLike object to wav.
6
5
  """
7
6
  from typing import Union, Optional, Callable, Dict, Any
8
7
  import numpy as np
@@ -13,10 +12,18 @@ from partitura.score import ScoreLike
13
12
  from partitura.performance import PerformanceLike
14
13
 
15
14
  from partitura.utils.synth import synthesize, SAMPLE_RATE, A4
15
+ from partitura.utils.fluidsynth import (
16
+ synthesize_fluidsynth,
17
+ DEFAULT_SOUNDFONT,
18
+ HAS_FLUIDSYNTH,
19
+ )
16
20
 
17
21
  from partitura.utils.misc import PathLike
18
22
 
19
- __all__ = ["save_wav"]
23
+ __all__ = [
24
+ "save_wav",
25
+ "save_wav_fluidsynth",
26
+ ]
20
27
 
21
28
 
22
29
  def save_wav(
@@ -89,8 +96,76 @@ def save_wav(
89
96
  bpm=bpm,
90
97
  )
91
98
 
99
+ if out is not None:
100
+ # convert to 16bit integers (save as PCM 16 bit)
101
+ # (some DAWs cannot load audio files that are float64,
102
+ # e.g., Logic)
103
+ amplitude = np.iinfo(np.int16).max
104
+ if abs(audio_signal).max() <= 1:
105
+ # convert to 16bit integers (save as PCM 16 bit)
106
+ amplitude = np.iinfo(np.int16).max
107
+ audio_signal *= amplitude
108
+ wavfile.write(out, samplerate, audio_signal.astype(np.int16))
109
+
110
+ else:
111
+ return audio_signal
112
+
113
+
114
+ def save_wav_fluidsynth(
115
+ input_data: Union[ScoreLike, PerformanceLike, np.ndarray],
116
+ out: Optional[PathLike] = None,
117
+ samplerate: int = SAMPLE_RATE,
118
+ soundfont: PathLike = DEFAULT_SOUNDFONT,
119
+ bpm: Union[float, np.ndarray, Callable] = 60,
120
+ ) -> Optional[np.ndarray]:
121
+ """
122
+ Export a score (a `Score`, `Part`, `PartGroup` or list of `Part` instances),
123
+ a performance (`Performance`, `PerformedPart` or list of `PerformedPart` instances)
124
+ as a WAV file using fluidsynth
125
+
126
+ Parameters
127
+ ----------
128
+ input_data : ScoreLike, PerformanceLike or np.ndarray
129
+ A partitura object with note information.
130
+ out : PathLike or None
131
+ Path of the output Wave file. If None, the method outputs
132
+ the audio signal as an array (see `audio_signal` below).
133
+ samplerate: int
134
+ The sample rate of the audio file in Hz. The default is 44100Hz.
135
+ soundfont : PathLike
136
+ Path to the soundfont in SF2/SF3 format for fluidsynth.
137
+ bpm : float, np.ndarray, callable
138
+ The bpm to render the output (if the input is a score-like object).
139
+ See `partitura.utils.music.performance_notearray_from_score_notearray`
140
+ for more information on this parameter.
141
+
142
+ Returns
143
+ -------
144
+ audio_signal : np.ndarray
145
+ Audio signal as a 1D array. Only returned if `out` is None.
146
+ """
147
+
148
+ if not HAS_FLUIDSYNTH:
149
+ raise ImportError("Fluidsynth is not installed!")
150
+
151
+ audio_signal = synthesize_fluidsynth(
152
+ note_info=input_data,
153
+ samplerate=samplerate,
154
+ soundfont=soundfont,
155
+ bpm=bpm,
156
+ )
157
+
92
158
  if out is not None:
93
159
  # Write audio signal
94
- wavfile.write(out, samplerate, audio_signal)
160
+
161
+ # convert to 16bit integers (save as PCM 16 bit)
162
+ # (some DAWs cannot load audio files that are float64,
163
+ # e.g., Logic)
164
+ amplitude = np.iinfo(np.int16).max
165
+ if abs(audio_signal).max() <= 1:
166
+ # convert to 16bit integers (save as PCM 16 bit)
167
+ amplitude = np.iinfo(np.int16).max
168
+ audio_signal *= amplitude
169
+ wavfile.write(out, samplerate, audio_signal.astype(np.int16))
95
170
  else:
96
171
  return audio_signal
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ This module contains methods for exporting Kern files.
5
+ """
6
+ import math
7
+ from collections import defaultdict
8
+
9
+ import numpy
10
+
11
+ import partitura.score as spt
12
+ from operator import itemgetter
13
+ from typing import Optional
14
+ import numpy as np
15
+ import warnings
16
+ from partitura.utils import partition, iter_current_next, to_quarter_tempo
17
+ from partitura.utils.misc import deprecated_alias, PathLike
18
+
19
+ __all__ = ["save_kern"]
20
+
21
+
22
+ ACC_TO_SIGN = {
23
+ 0: "n",
24
+ -1: "-",
25
+ 1: "#",
26
+ -2: "--",
27
+ 2: "##",
28
+ }
29
+
30
+ # Kern notes encoding has a dedicated octave for each note.
31
+ KERN_NOTES = {
32
+ ("C", 3): "C",
33
+ ("D", 3): "D",
34
+ ("E", 3): "E",
35
+ ("F", 3): "F",
36
+ ("G", 3): "G",
37
+ ("A", 3): "A",
38
+ ("B", 3): "B",
39
+ ("C", 4): "c",
40
+ ("D", 4): "d",
41
+ ("E", 4): "e",
42
+ ("F", 4): "f",
43
+ ("G", 4): "g",
44
+ ("A", 4): "a",
45
+ ("B", 4): "b",
46
+ }
47
+
48
+ KERN_DURS = {
49
+ "maxima": "000",
50
+ "long": "00",
51
+ "breve": "0",
52
+ "whole": "1",
53
+ "half": "2",
54
+ "quarter": "4",
55
+ "eighth": "8",
56
+ "16th": "16",
57
+ "32nd": "32",
58
+ "64th": "64",
59
+ "128th": "128",
60
+ "256th": "256",
61
+ }
62
+
63
+ KEYS = ["f", "c", "g", "d", "a", "e", "b"]
64
+
65
+
66
+ class KernExporter(object):
67
+ """
68
+ Class for exporting a partitura score to Kern format.
69
+
70
+ Parameters
71
+ ----------
72
+ part: spt.Part
73
+ Part to export to Kern format.
74
+ """
75
+
76
+ def __init__(self, part):
77
+ self.part = part
78
+ note_array = part.note_array(include_staff=True)
79
+ num_measures = len(part.measures)
80
+ num_notes = len(part.notes)
81
+ num_rests = len(part.rests)
82
+ self.unique_voc_staff = np.unique(note_array[["voice", "staff"]], axis=0)
83
+ self.vocstaff_map_dict = {
84
+ f"{self.unique_voc_staff[i][0]}-{self.unique_voc_staff[i][1]}": i
85
+ for i in range(self.unique_voc_staff.shape[0])
86
+ }
87
+ # Part elements is really the maximum number of lines we could have in the kern file
88
+ # we add some to account for the **kern and the *- encoding at beginning and end of file and also tandem elements
89
+ # that might be added. We also add the number of measures to account for the measure encoding
90
+ total_elements_ish = num_measures + num_notes + num_rests + 2 + 10
91
+ self.out_data = np.empty(
92
+ (total_elements_ish, len(self.unique_voc_staff)), dtype=object
93
+ )
94
+ self.unique_times = np.array([p.t for p in part._points])
95
+ # Fill all values with the "." character to filter afterwards
96
+ self.out_data.fill(".")
97
+ self.out_data[0] = "**kern"
98
+ self.out_data[-1] = "*-"
99
+ # Add the staff element to the second line
100
+ for i in range(self.unique_voc_staff.shape[0]):
101
+ self.out_data[1, i] = f"*staff{self.unique_voc_staff[i][1]}"
102
+ self.prev_note_time = None
103
+ self.prev_note_col_idx = None
104
+ self.prev_note_row_idx = None
105
+
106
+ def parse(self):
107
+ """
108
+ Parse the partitura score to Kern format.
109
+
110
+ This method iterates over all elements in the partitura score and converts them to Kern format.
111
+ To better process the elements, the method first groups them by start time and then processes them in order.
112
+ It first finds notes and then processes structural elements (clefs, time signatures, etc.) and finally measures.
113
+
114
+ Returns
115
+ -------
116
+ self.out_data: np.ndarray
117
+ Kern file as a numpy array of strings.
118
+ """
119
+ row_idx = 2
120
+ for start_time in self.unique_times:
121
+ end_time = start_time + 1
122
+ # Get all elements starting at this time
123
+ elements_starting = np.array(
124
+ list(self.part.iter_all(start=start_time, end=end_time)), dtype=object
125
+ )
126
+ # Find notes
127
+ note_mask = np.array(
128
+ [isinstance(el, spt.GenericNote) for el in elements_starting]
129
+ )
130
+ if np.any(~note_mask):
131
+ bar_mask = np.array(
132
+ [
133
+ isinstance(el, spt.Measure)
134
+ for el in elements_starting[~note_mask]
135
+ ]
136
+ )
137
+ tandem_mask = ~bar_mask
138
+ structural_elements = elements_starting[~note_mask]
139
+ structural_elements = np.hstack(
140
+ (structural_elements[tandem_mask], structural_elements[bar_mask])
141
+ )
142
+ else:
143
+ structural_elements = elements_starting[~note_mask]
144
+ # Put structural elements first (start with tandem elements, then measure elements, then notes and rests)
145
+ elements_starting = np.hstack(
146
+ (structural_elements, elements_starting[note_mask])
147
+ )
148
+ for el in elements_starting:
149
+ add_row = True
150
+ if isinstance(el, spt.GenericNote):
151
+ self._handle_note(el, row_idx)
152
+ elif isinstance(el, spt.Clef):
153
+ # Apply clef to all voices of the same staff
154
+ currect_staff = el.staff
155
+ for staff_idx in range(self.unique_voc_staff.shape[0]):
156
+ if self.unique_voc_staff[staff_idx][1] == currect_staff:
157
+ kern_el = f"*clef{el.sign.upper()}{el.line}"
158
+ self.out_data[row_idx, staff_idx] = kern_el
159
+ elif isinstance(el, spt.Tempo):
160
+ # Apply tempo to all splines
161
+ kern_el = f"*MM{to_quarter_tempo(el.qpm)}"
162
+ self.out_data[row_idx] = kern_el
163
+ elif isinstance(el, spt.Measure):
164
+ # Apply measure to all splines
165
+ kern_el = f"={el.number}"
166
+ self.out_data[row_idx] = kern_el
167
+ elif isinstance(el, spt.TimeSignature):
168
+ # Apply element to all splines
169
+ kern_el = f"*M{el.beats}/{el.beat_type}"
170
+ self.out_data[row_idx] = kern_el
171
+ elif isinstance(el, spt.KeySignature):
172
+ # Apply element to all splines
173
+ if el.fifths < 0:
174
+ alters = "-".join(KEYS[: el.fifths])
175
+ elif el.fifths > 0:
176
+ alters = "#".join(KEYS[: el.fifths])
177
+ else:
178
+ alters = ""
179
+ kern_el = f"*k[{alters}]"
180
+ self.out_data[row_idx] = kern_el
181
+ else:
182
+ add_row = False
183
+ warnings.warn(f"Element {el} is not supported for kern export yet.")
184
+ if add_row:
185
+ row_idx += 1
186
+ return self.out_data
187
+
188
+ def trim(self, data):
189
+ # if an entire row is filled with "." elements remove it.
190
+ out_data = data[~np.all(data == ".", axis=1)]
191
+ return out_data
192
+
193
+ def sym_dur_to_kern(self, symbolic_duration: dict) -> str:
194
+ kern_base = KERN_DURS[symbolic_duration["type"]]
195
+ dots = (
196
+ "." * symbolic_duration["dots"]
197
+ if "dots" in symbolic_duration.keys()
198
+ else ""
199
+ )
200
+ if "actual_notes" in symbolic_duration.keys() and "normal_notes":
201
+ kern_base = (
202
+ int(kern_base)
203
+ * symbolic_duration["actual_notes"]
204
+ / symbolic_duration["normal_notes"]
205
+ )
206
+ kern_base = str(kern_base)
207
+ return kern_base + dots
208
+
209
+ def duration_to_kern(self, element: spt.GenericNote) -> str:
210
+ if isinstance(element, spt.GraceNote):
211
+ if element.grace_type == "acciaccatura":
212
+ return "p"
213
+ else:
214
+ return "q"
215
+ else:
216
+ if "type" not in element.symbolic_duration.keys():
217
+ warnings.warn(f"Element {element} has no symbolic duration type")
218
+ return "4"
219
+ return self.sym_dur_to_kern(element.symbolic_duration)
220
+
221
+ def pitch_to_kern(self, element: spt.GenericNote) -> str:
222
+ """
223
+ Transform a Partitura Note object to a kern note string (only pitch).
224
+
225
+ To encode pitch correctly in kern we need to take into account that the octave
226
+ duplication of the step in kern can either move the note up or down an octave
227
+
228
+ """
229
+ if isinstance(element, spt.Rest):
230
+ return "r"
231
+ step, alter, octave = element.step, element.alter, element.octave
232
+ # Check if we need to have duplication of the step character
233
+ if octave > 4:
234
+ multiply_character = octave - 3
235
+ octave = 4
236
+ elif octave < 3:
237
+ multiply_character = 4 - octave
238
+ octave = 3
239
+ else:
240
+ multiply_character = 1
241
+ # Fetch the correct string for the step and multiply it if needed
242
+ kern_step = KERN_NOTES[(step, octave)] * multiply_character
243
+ kern_alter = ACC_TO_SIGN[alter] if alter is not None else ""
244
+ return kern_step + kern_alter
245
+
246
+ def markings_to_kern(self, element: spt.GenericNote) -> str:
247
+ symbols = ""
248
+ if not isinstance(element, spt.Rest):
249
+ if element.tie_next and element.tie_prev:
250
+ symbols += "-"
251
+ elif element.tie_next:
252
+ symbols += "["
253
+ elif element.tie_prev:
254
+ symbols += "]"
255
+ if element.slur_starts:
256
+ symbols += "("
257
+ if element.slur_stops:
258
+ symbols += ")"
259
+ if isinstance(element, spt.Note):
260
+ if element.beam is not None:
261
+ symbols += (
262
+ "L"
263
+ if element.beam == "begin"
264
+ else "J" if element.beam == "end" else "K"
265
+ )
266
+ return symbols
267
+
268
+ def _handle_note(self, el: spt.GenericNote, row_idx) -> str:
269
+ voice = el.voice
270
+ staff = el.staff
271
+ duration = self.duration_to_kern(el)
272
+ pitch = self.pitch_to_kern(el)
273
+ col_idx = self.vocstaff_map_dict[f"{voice}-{staff}"]
274
+ markings = self.markings_to_kern(el)
275
+ kern_el = duration + pitch + markings
276
+ if self.prev_note_time == el.start.t:
277
+ if self.prev_note_col_idx == col_idx:
278
+ # Chords in Kern
279
+ self.out_data[self.prev_note_row_idx, self.prev_note_col_idx] = (
280
+ self.out_data[self.prev_note_row_idx, self.prev_note_col_idx]
281
+ + " "
282
+ + kern_el
283
+ )
284
+ else:
285
+ # Same row (start.t) other spline
286
+ self.out_data[self.prev_note_row_idx, col_idx] = kern_el
287
+ else:
288
+ # New line
289
+ self.out_data[row_idx, col_idx] = kern_el
290
+ self.prev_note_row_idx = row_idx
291
+ self.prev_note_col_idx = col_idx
292
+ self.prev_note_time = el.start.t
293
+
294
+
295
+ def save_kern(
296
+ score_data: spt.ScoreLike,
297
+ out: Optional[PathLike] = None,
298
+ ) -> Optional[np.ndarray]:
299
+ """
300
+ Save a score in Kern format.
301
+
302
+ Parameters
303
+ ----------
304
+ score_data: spt.ScoreLike
305
+ Score to save in Kern format
306
+
307
+ out: Optional[PathLike]
308
+ Path to save the Kern file. If None, the function returns the Kern file as a numpy array.
309
+
310
+ Returns
311
+ -------
312
+ Optional[np.ndarray]
313
+ If out is None, the Kern file is returned as a numpy array.
314
+ """
315
+ # Header extracts meta information about the score
316
+ header = "Here is some random piece"
317
+ # Kern can output only from part so first let's merge parts (we need a timewise representation)
318
+ if isinstance(score_data, spt.Score):
319
+ # TODO check that divisions are the same
320
+ part = spt.merge_parts(score_data.parts)
321
+ else:
322
+ part = score_data
323
+ if not part.measures:
324
+ spt.add_measures(part)
325
+ spt.fill_rests(part, measurewise=False)
326
+ exporter = KernExporter(part)
327
+ out_data = exporter.parse()
328
+ out_data = exporter.trim(out_data)
329
+ # Use numpy savetxt to save the file
330
+ footer = "Encoded using the Partitura Python package, version 1.5.0"
331
+ if out is not None:
332
+ np.savetxt(
333
+ fname=out,
334
+ X=out_data,
335
+ fmt="%1.26s",
336
+ delimiter="\t",
337
+ newline="\n",
338
+ header=header,
339
+ footer=footer,
340
+ comments="!!!",
341
+ encoding="utf-8",
342
+ )
343
+ else:
344
+ return out_data
@@ -358,7 +358,10 @@ def matchfile_from_alignment(
358
358
  offset_in_beats=offset_beats,
359
359
  score_attributes_list=score_attributes_list,
360
360
  )
361
- snote_sort_info[snote.id] = (onset_beats, snote.doc_order)
361
+ snote_sort_info[snote.id] = (
362
+ onset_beats,
363
+ snote.doc_order if snote.doc_order is not None else 0,
364
+ )
362
365
 
363
366
  # # NOTE time position is hardcoded, not pretty... Assumes there is only one tempo indication at the beginning of the score
364
367
  if tempo_indication is not None:
@@ -392,7 +395,7 @@ def matchfile_from_alignment(
392
395
  onset=onset,
393
396
  offset=offset,
394
397
  velocity=pnote["velocity"],
395
- channel=pnote.get("channel", 1),
398
+ channel=pnote.get("channel", 0),
396
399
  track=pnote.get("track", 0),
397
400
  )
398
401
  pnote_sort_info[pnote["id"]] = (