partitura 1.3.1__tar.gz → 1.4.0__tar.gz

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