partitura 1.3.0__py3-none-any.whl → 1.4.0__py3-none-any.whl

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 (41) hide show
  1. partitura/directions.py +3 -0
  2. partitura/display.py +0 -1
  3. partitura/io/__init__.py +41 -35
  4. partitura/io/exportmatch.py +52 -10
  5. partitura/io/exportmidi.py +37 -19
  6. partitura/io/exportmusicxml.py +6 -92
  7. partitura/io/exportparangonada.py +18 -19
  8. partitura/io/importkern.py +2 -4
  9. partitura/io/importmatch.py +121 -39
  10. partitura/io/importmei.py +161 -34
  11. partitura/io/importmidi.py +23 -14
  12. partitura/io/importmusic21.py +0 -1
  13. partitura/io/importmusicxml.py +48 -63
  14. partitura/io/importparangonada.py +0 -1
  15. partitura/io/matchfile_base.py +0 -21
  16. partitura/io/matchfile_utils.py +29 -17
  17. partitura/io/matchlines_v0.py +0 -22
  18. partitura/io/matchlines_v1.py +8 -42
  19. partitura/io/musescore.py +68 -41
  20. partitura/musicanalysis/__init__.py +1 -1
  21. partitura/musicanalysis/note_array_to_score.py +147 -92
  22. partitura/musicanalysis/note_features.py +66 -51
  23. partitura/musicanalysis/performance_codec.py +140 -96
  24. partitura/musicanalysis/performance_features.py +190 -129
  25. partitura/musicanalysis/pitch_spelling.py +0 -2
  26. partitura/musicanalysis/tonal_tension.py +0 -6
  27. partitura/musicanalysis/voice_separation.py +1 -22
  28. partitura/performance.py +178 -5
  29. partitura/score.py +154 -74
  30. partitura/utils/__init__.py +1 -1
  31. partitura/utils/generic.py +3 -7
  32. partitura/utils/misc.py +0 -1
  33. partitura/utils/music.py +108 -66
  34. partitura/utils/normalize.py +75 -35
  35. partitura/utils/synth.py +1 -7
  36. {partitura-1.3.0.dist-info → partitura-1.4.0.dist-info}/METADATA +2 -2
  37. partitura-1.4.0.dist-info/RECORD +51 -0
  38. {partitura-1.3.0.dist-info → partitura-1.4.0.dist-info}/WHEEL +1 -1
  39. partitura-1.3.0.dist-info/RECORD +0 -51
  40. {partitura-1.3.0.dist-info → partitura-1.4.0.dist-info}/LICENSE +0 -0
  41. {partitura-1.3.0.dist-info → partitura-1.4.0.dist-info}/top_level.txt +0 -0
partitura/directions.py CHANGED
@@ -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\.?)/",
partitura/display.py CHANGED
@@ -120,7 +120,6 @@ def render_lilypond(
120
120
  with TemporaryFile() as xml_fh, NamedTemporaryFile(
121
121
  suffix=prvw_sfx, delete=False
122
122
  ) as img_fh:
123
-
124
123
  # save part to musicxml in file handle xml_fh
125
124
  save_musicxml(score_data, xml_fh)
126
125
  # rewind read pointer of file handle before we pass it to musicxml2ly
partitura/io/__init__.py CHANGED
@@ -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
 
@@ -53,9 +54,7 @@ from partitura.utils.misc import (
53
54
  deprecated_parameter,
54
55
  )
55
56
 
56
- from partitura.musicanalysis.performance_codec import (
57
- get_time_maps_from_alignment
58
- )
57
+ from partitura.musicanalysis.performance_codec import get_time_maps_from_alignment
59
58
 
60
59
  __all__ = ["save_match"]
61
60
 
@@ -73,6 +72,8 @@ def matchfile_from_alignment(
73
72
  score_filename: Optional[PathLike] = None,
74
73
  performance_filename: Optional[PathLike] = None,
75
74
  assume_part_unfolded: bool = False,
75
+ tempo_indication: Optional[str] = None,
76
+ diff_score_version_notes: Optional[list] = None,
76
77
  version: Version = LATEST_VERSION,
77
78
  debug: bool = False,
78
79
  ) -> MatchFile:
@@ -108,6 +109,10 @@ def matchfile_from_alignment(
108
109
  repetitions in the alignment. If False, the part will be automatically
109
110
  unfolded to have maximal coverage of the notes in the alignment.
110
111
  See `partitura.score.unfold_part_alignment`.
112
+ tempo_indication : str or None
113
+ The tempo direction indicated in the beginning of the score
114
+ diff_score_version_notes : list or None
115
+ A list of score notes that reflect a special score version (e.g., original edition/Erstdruck, Editors note etc.)
111
116
  version: Version
112
117
  Version of the match file. For now only 1.0.0 is supported.
113
118
  Returns
@@ -201,17 +206,14 @@ def matchfile_from_alignment(
201
206
 
202
207
  # Score prop header lines
203
208
  scoreprop_lines = defaultdict(list)
204
-
205
209
  # For score notes
206
210
  score_info = dict()
207
211
  # Info for sorting lines
208
212
  snote_sort_info = dict()
209
213
  for (mnum, msd, msb), m in zip(measure_starts, measures):
210
-
211
214
  time_signatures = spart.iter_all(score.TimeSignature, m.start, m.end)
212
215
 
213
216
  for tsig in time_signatures:
214
-
215
217
  time_divs = int(tsig.start.t)
216
218
  time_beats = float(beat_map(time_divs))
217
219
  dpq = int(spart.quarter_duration_map(time_divs))
@@ -280,9 +282,7 @@ def matchfile_from_alignment(
280
282
  # Get all notes in the measure
281
283
  snotes = spart.iter_all(score.Note, m.start, m.end, include_subclasses=True)
282
284
  # Beginning of each measure
283
-
284
285
  for snote in snotes:
285
-
286
286
  onset_divs, offset_divs = snote.start.t, snote.start.t + snote.duration_tied
287
287
  duration_divs = offset_divs - onset_divs
288
288
 
@@ -329,6 +329,15 @@ def matchfile_from_alignment(
329
329
  if fermata is not None:
330
330
  score_attributes_list.append("fermata")
331
331
 
332
+ if isinstance(snote, score.GraceNote):
333
+ score_attributes_list.append("grace")
334
+
335
+ if (
336
+ diff_score_version_notes is not None
337
+ and snote.id in diff_score_version_notes
338
+ ):
339
+ score_attributes_list.append("diff_score_version")
340
+
332
341
  score_info[snote.id] = MatchSnote(
333
342
  version=version,
334
343
  anchor=str(snote.id),
@@ -351,6 +360,22 @@ def matchfile_from_alignment(
351
360
  )
352
361
  snote_sort_info[snote.id] = (onset_beats, snote.doc_order)
353
362
 
363
+ # # NOTE time position is hardcoded, not pretty... Assumes there is only one tempo indication at the beginning of the score
364
+ if tempo_indication is not None:
365
+ score_tempo_direction_header = make_scoreprop(
366
+ version=version,
367
+ attribute="tempoIndication",
368
+ value=MatchTempoIndication(
369
+ tempo_indication,
370
+ is_list=False,
371
+ ),
372
+ measure=measure_starts[0][0],
373
+ beat=1,
374
+ offset=0,
375
+ time_in_beats=measure_starts[0][2],
376
+ )
377
+ scoreprop_lines["tempo_indication"].append(score_tempo_direction_header)
378
+
354
379
  perf_info = dict()
355
380
  pnote_sort_info = dict()
356
381
  for pnote in ppart.notes:
@@ -377,8 +402,22 @@ def matchfile_from_alignment(
377
402
 
378
403
  sort_stime = []
379
404
  note_lines = []
380
- for al_note in alignment:
381
405
 
406
+ # Get ids of notes which voice overlap
407
+ sna = spart.note_array()
408
+ onset_pitch_slice = sna[["onset_div", "pitch"]]
409
+ uniques, counts = np.unique(onset_pitch_slice, return_counts=True)
410
+ duplicate_values = uniques[counts > 1]
411
+ duplicates = dict()
412
+ for v in duplicate_values:
413
+ idx = np.where(onset_pitch_slice == v)[0]
414
+ duplicates[tuple(v)] = idx
415
+ voice_overlap_note_ids = []
416
+ if len(duplicates) > 0:
417
+ duplicate_idx = np.concatenate(np.array(list(duplicates.values()))).flatten()
418
+ voice_overlap_note_ids = list(sna[duplicate_idx]["id"])
419
+
420
+ for al_note in alignment:
382
421
  label = al_note["label"]
383
422
 
384
423
  if label == "match":
@@ -390,6 +429,8 @@ def matchfile_from_alignment(
390
429
 
391
430
  elif label == "deletion":
392
431
  snote = score_info[al_note["score_id"]]
432
+ if al_note["score_id"] in voice_overlap_note_ids:
433
+ snote.ScoreAttributesList.append("voice_overlap")
393
434
  deletion_line = MatchSnoteDeletion(version=version, snote=snote)
394
435
  note_lines.append(deletion_line)
395
436
  sort_stime.append(snote_sort_info[al_note["score_id"]])
@@ -447,6 +488,7 @@ def matchfile_from_alignment(
447
488
  "clock_rate",
448
489
  "key_signatures",
449
490
  "time_signatures",
491
+ "tempo_indication",
450
492
  ]
451
493
  all_match_lines = []
452
494
  for h in header_order:
@@ -543,7 +585,7 @@ def save_match(
543
585
  else:
544
586
  raise ValueError(
545
587
  "`performance_data` should be a `Performance`, a `PerformedPart`, or a "
546
- f"list of `PerformedPart` objects, but is {type(score_data)}"
588
+ f"list of `PerformedPart` objects, but is {type(performance_data)}"
547
589
  )
548
590
 
549
591
  # Get matchfile
@@ -8,7 +8,7 @@ import numpy as np
8
8
  from collections import defaultdict, OrderedDict
9
9
  from typing import Optional, Iterable
10
10
 
11
- from mido import MidiFile, MidiTrack, Message, MetaMessage
11
+ from mido import MidiFile, MidiTrack, Message, MetaMessage, merge_tracks
12
12
 
13
13
  import partitura.score as score
14
14
  from partitura.score import Score, Part, PartGroup, ScoreLike
@@ -32,7 +32,7 @@ def map_to_track_channel(note_keys, mode):
32
32
  tr_helper = {}
33
33
  track = {}
34
34
  channel = {}
35
- for (pg, p, v) in note_keys:
35
+ for pg, p, v in note_keys:
36
36
  if mode == 0:
37
37
  trk = tr_helper.setdefault(p, len(tr_helper))
38
38
  ch1 = ch_helper.setdefault(p, {})
@@ -87,6 +87,7 @@ def save_performance_midi(
87
87
  mpq: int = 500000,
88
88
  ppq: int = 480,
89
89
  default_velocity: int = 64,
90
+ merge_tracks_save: Optional[bool] = False,
90
91
  ) -> Optional[MidiFile]:
91
92
  """Save a :class:`~partitura.performance.PerformedPart` or
92
93
  a :class:`~partitura.performance.Performance` as a MIDI file
@@ -107,6 +108,8 @@ def save_performance_midi(
107
108
  default_velocity : int, optional
108
109
  A default velocity value (between 0 and 127) to be used for
109
110
  notes without a specified velocity. Defaults to 64.
111
+ merge_tracks_save : bool, optional
112
+ Determines whether midi tracks are merged when exporting to a midi file. Defaults to False.
110
113
 
111
114
  Returns
112
115
  -------
@@ -134,7 +137,6 @@ def save_performance_midi(
134
137
  )
135
138
 
136
139
  track_events = defaultdict(lambda: defaultdict(list))
137
-
138
140
  for performed_part in performed_parts:
139
141
  for c in performed_part.controls:
140
142
  track = c.get("track", 0)
@@ -217,6 +219,10 @@ def save_performance_midi(
217
219
  track.append(msg.copy(time=t_delta))
218
220
  t_delta = 0
219
221
  t = t_msg
222
+
223
+ if merge_tracks_save and len(mf.tracks) > 1:
224
+ mf.tracks = [merge_tracks(mf.tracks)]
225
+
220
226
  if out is not None:
221
227
  if hasattr(out, "write"):
222
228
  mf.save(file=out)
@@ -287,9 +293,9 @@ def save_score_midi(
287
293
  downbeats position are coherent in case of incomplete measures
288
294
  later in the score.
289
295
  minimum_ppq : int, optional
290
- Minimum ppq to use for the MIDI file. If the ppq of the score is less,
296
+ Minimum ppq to use for the MIDI file. If the ppq of the score is less,
291
297
  it will be doubled until it is above the threshold. This is useful
292
- because some libraries like miditok require a certain minimum ppq to
298
+ because some libraries like miditok require a certain minimum ppq to
293
299
  work properly.
294
300
 
295
301
  Returns
@@ -315,7 +321,7 @@ def save_score_midi(
315
321
  # double it until it is above the minimum level.
316
322
  # Doubling instead of setting it ensure that the common divisors stay the same.
317
323
  while ppq < minimum_ppq:
318
- ppq = ppq*2
324
+ ppq = ppq * 2
319
325
 
320
326
  events = defaultdict(lambda: defaultdict(list))
321
327
  meta_events = defaultdict(lambda: defaultdict(list))
@@ -346,7 +352,6 @@ def save_score_midi(
346
352
  )
347
353
 
348
354
  for qm, part in zip(quarter_maps, score.iter_parts(parts)):
349
-
350
355
  pg = get_partgroup(part)
351
356
 
352
357
  notes = part.notes_tied
@@ -367,22 +372,30 @@ def save_score_midi(
367
372
  all_ts = list(part.iter_all(score.TimeSignature))
368
373
  ts_changing_time = [ts.start.t for ts in all_ts]
369
374
  for measure in part.iter_all(score.Measure):
370
- m_duration_beat = part.beat_map(measure.end.t) - part.beat_map(measure.start.t)
375
+ m_duration_beat = part.beat_map(measure.end.t) - part.beat_map(
376
+ measure.start.t
377
+ )
371
378
  m_ts = part.time_signature_map(measure.start.t)
372
379
  if m_duration_beat != m_ts[0]:
373
380
  # add ts change
374
381
  # TODO: add support for changing the beat type if number of beats is not integer
375
382
  meta_events[part][to_ppq(measure.start.t)].append(
376
383
  MetaMessage(
377
- "time_signature", numerator=int(m_duration_beat), denominator=int(m_ts[1])
384
+ "time_signature",
385
+ numerator=int(m_duration_beat),
386
+ denominator=int(m_ts[1]),
378
387
  )
379
388
  )
380
- ts_changing_time.append(measure.start.t) # keep track of changing the ts
389
+ ts_changing_time.append(
390
+ measure.start.t
391
+ ) # keep track of changing the ts
381
392
  # now go back to original ts if there is no ts change after this measure
382
393
  if not any([ts_t > measure.start.t for ts_t in ts_changing_time]):
383
394
  meta_events[part][to_ppq(measure.end.t)].append(
384
395
  MetaMessage(
385
- "time_signature", numerator=int(m_ts[0]), denominator=int(m_ts[1])
396
+ "time_signature",
397
+ numerator=int(m_ts[0]),
398
+ denominator=int(m_ts[1]),
386
399
  )
387
400
  )
388
401
  # filter out the multiple ts changes at the same time
@@ -394,27 +407,33 @@ def save_score_midi(
394
407
  # now add the normal time signature change
395
408
  for ts in part.iter_all(score.TimeSignature):
396
409
  if ts.start.t in ts_changing_time:
397
- #don't add if something is already added at this time to cover the case of a ts change when the first measure is shorter/longer
410
+ # don't add if something is already added at this time to cover the case of a ts change when the first measure is shorter/longer
398
411
  pass
399
412
  else:
400
413
  meta_events[part][to_ppq(ts.start.t)].append(
401
414
  MetaMessage(
402
- "time_signature", numerator=ts.beats, denominator=ts.beat_type
415
+ "time_signature",
416
+ numerator=ts.beats,
417
+ denominator=ts.beat_type,
403
418
  )
404
419
  )
405
- else: # just add the time signature that are explicit in partitura
420
+ else: # just add the time signature that are explicit in partitura
406
421
  for i, ts in enumerate(part.iter_all(score.TimeSignature)):
407
422
  if anacrusis_behavior == "pad_bar" and i == 0:
408
423
  # shift the first time signature to 0 so MIDI players can pick up the correct measure position
409
424
  meta_events[part][0].append(
410
425
  MetaMessage(
411
- "time_signature", numerator=ts.beats, denominator=ts.beat_type
426
+ "time_signature",
427
+ numerator=ts.beats,
428
+ denominator=ts.beat_type,
412
429
  )
413
- )
414
- else: #follow the position in the partitura part
430
+ )
431
+ else: # follow the position in the partitura part
415
432
  meta_events[part][to_ppq(ts.start.t)].append(
416
433
  MetaMessage(
417
- "time_signature", numerator=ts.beats, denominator=ts.beat_type
434
+ "time_signature",
435
+ numerator=ts.beats,
436
+ denominator=ts.beat_type,
418
437
  )
419
438
  )
420
439
 
@@ -424,7 +443,6 @@ def save_score_midi(
424
443
  )
425
444
 
426
445
  for note in notes:
427
-
428
446
  # key is a tuple (part_group, part, voice) that will be
429
447
  # converted into a (track, channel) pair.
430
448
  key = (pg, part, note.voice)