1515__docformat__ = "google"
1616
1717from fractions import Fraction
18+ from typing import Optional
1819
1920import music21 as m21
2021
2122from musicdiff import M21Utils
22-
23+ from musicdiff import DetailLevel
2324
2425class AnnNote :
25- def __init__ (self , general_note , enhanced_beam_list , tuplet_list ):
26+ def __init__ (self , general_note : m21 . note . GeneralNote , enhanced_beam_list , tuplet_list , detail : DetailLevel = DetailLevel . Default ):
2627 """
2728 Extend music21 GeneralNote with some precomputed, easily compared information about it.
2829
2930 Args:
3031 general_note (music21.note.GeneralNote): The music21 note/chord/rest to extend.
3132 enhanced_beam_list (list): A list of beaming information about this GeneralNote.
3233 tuplet_list (list): A list of tuplet info about this GeneralNote.
34+ detail (DetailLevel): What level of detail to use during the diff. Can be
35+ GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
36+ currently equivalent to AllObjects).
37+
3338 """
3439 self .general_note = general_note .id
3540 self .beamings = enhanced_beam_list
3641 self .tuplets = tuplet_list
42+
43+ self .stylestr : str = ''
44+ self .styledict : dict = {}
45+ if M21Utils .has_style (general_note ):
46+ self .styledict = M21Utils .obj_to_styledict (general_note , detail )
47+ self .noteshape : str = 'normal'
48+ self .noteheadFill : Optional [bool ] = None
49+ self .noteheadParenthesis : bool = False
50+ self .stemDirection : str = 'unspecified'
51+ if detail >= DetailLevel .AllObjectsWithStyle and isinstance (general_note , m21 .note .NotRest ):
52+ self .noteshape = general_note .notehead
53+ self .noteheadFill = general_note .noteheadFill
54+ self .noteheadParenthesis = general_note .noteheadParenthesis
55+ self .stemDirection = general_note .stemDirection
56+
3757 # compute the representation of NoteNode as in the paper
3858 # pitches is a list of elements, each one is (pitchposition, accidental, tie)
3959 if general_note .isRest :
@@ -101,7 +121,8 @@ def notation_size(self):
101121 def __repr__ (self ):
102122 # does consider the MEI id!
103123 return (f"{ self .pitches } ,{ self .note_head } ,{ self .dots } ,{ self .beamings } ," +
104- f"{ self .tuplets } ,{ self .general_note } ,{ self .articulations } ,{ self .expressions } " )
124+ f"{ self .tuplets } ,{ self .general_note } ,{ self .articulations } ,{ self .expressions } " +
125+ f"{ self .styledict } " )
105126
106127 def __str__ (self ):
107128 """
@@ -151,6 +172,22 @@ def __str__(self):
151172 if len (self .expressions ) > 0 : # add for articulations
152173 for e in self .expressions :
153174 string += e
175+
176+ if self .noteshape != 'normal' :
177+ string += f"noteshape={ self .noteshape } "
178+ if self .noteheadFill is not None :
179+ string += f"noteheadFill={ self .noteheadFill } "
180+ if self .noteheadParenthesis :
181+ string += f"noteheadParenthesis={ self .noteheadParenthesis } "
182+ if self .stemDirection != 'unspecified' :
183+ string += f"stemDirection={ self .stemDirection } "
184+
185+ # and then the style fields
186+ for i , (k , v ) in enumerate (self .styledict .items ()):
187+ if i > 0 :
188+ string += ","
189+ string += f"{ k } ={ v } "
190+
154191 return string
155192
156193 def get_note_ids (self ):
@@ -188,7 +225,7 @@ def __eq__(self, other):
188225
189226
190227class AnnExtra :
191- def __init__ (self , extra : m21 .base .Music21Object , measure : m21 .stream .Measure , score : m21 .stream .Score ):
228+ def __init__ (self , extra : m21 .base .Music21Object , measure : m21 .stream .Measure , score : m21 .stream .Score , detail : DetailLevel = DetailLevel . Default ):
192229 """
193230 Extend music21 non-GeneralNote and non-Stream objects with some precomputed, easily compared information about it.
194231 Examples: TextExpression, Dynamic, Clef, Key, TimeSignature, MetronomeMark, etc.
@@ -197,6 +234,9 @@ def __init__(self, extra: m21.base.Music21Object, measure: m21.stream.Measure, s
197234 extra (music21.base.Music21Object): The music21 non-GeneralNote/non-Stream object to extend.
198235 measure (music21.stream.Measure): The music21 Measure the extra was found in. If the extra
199236 was found in a Voice, this is the Measure that the Voice was found in.
237+ detail (DetailLevel): What level of detail to use during the diff. Can be
238+ GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
239+ currently equivalent to AllObjects).
200240 """
201241 self .extra = extra .id
202242 self .offset : float
@@ -213,6 +253,9 @@ def __init__(self, extra: m21.base.Music21Object, measure: m21.stream.Measure, s
213253 self .offset = float (extra .getOffsetInHierarchy (measure ))
214254 self .duration = float (extra .duration .quarterLength )
215255 self .content : str = M21Utils .extra_to_string (extra )
256+ self .styledict : str = {}
257+ if M21Utils .has_style (extra ):
258+ self .styledict = M21Utils .obj_to_styledict (extra , detail ) # includes extra.placement if present
216259 self ._notation_size : int = 1 # so far, always 1, but maybe some extra will be bigger someday
217260
218261 # precomputed representations for faster comparison
@@ -235,20 +278,27 @@ def __str__(self):
235278 Returns:
236279 str: the compared representation of the AnnExtra. Does not consider music21 id.
237280 """
238- return f'[{ self .content } ,off={ self .offset } ,dur={ self .duration } ]'
281+ string = f'{ self .content } ,off={ self .offset } ,dur={ self .duration } '
282+ # and then any style fields
283+ for k , v in self .styledict .items ():
284+ string += f",{ k } ={ v } "
285+ return string
239286
240287 def __eq__ (self , other ):
241288 # equality does not consider the MEI id!
242289 return self .precomputed_str == other .precomputed_str
243290
244291
245292class AnnVoice :
246- def __init__ (self , voice ):
293+ def __init__ (self , voice : m21 . stream . Voice , detail : DetailLevel = DetailLevel . Default ):
247294 """
248295 Extend music21 Voice with some precomputed, easily compared information about it.
249296
250297 Args:
251298 voice (music21.stream.Voice): The music21 voice to extend.
299+ detail (DetailLevel): What level of detail to use during the diff. Can be
300+ GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
301+ currently equivalent to AllObjects).
252302 """
253303 self .voice = voice .id
254304 note_list = M21Utils .get_notes (voice )
@@ -269,7 +319,7 @@ def __init__(self, voice):
269319 self .annot_notes = []
270320 for i , n in enumerate (note_list ):
271321 self .annot_notes .append (
272- AnnNote (n , self .en_beam_list [i ], self .tuplet_list [i ])
322+ AnnNote (n , self .en_beam_list [i ], self .tuplet_list [i ], detail )
273323 )
274324
275325 self .n_of_notes = len (self .annot_notes )
@@ -323,35 +373,44 @@ def get_note_ids(self):
323373
324374
325375class AnnMeasure :
326- def __init__ (self , measure , score , spannerBundle ):
376+ def __init__ (self , measure : m21 .stream .Measure ,
377+ score : m21 .stream .Score ,
378+ spannerBundle : m21 .spanner .SpannerBundle ,
379+ detail : DetailLevel = DetailLevel .Default ):
327380 """
328381 Extend music21 Measure with some precomputed, easily compared information about it.
329382
330383 Args:
331384 measure (music21.stream.Measure): The music21 measure to extend.
385+ score (music21.stream.Score): the enclosing music21 Score.
386+ spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners in the score.
387+ detail (DetailLevel): What level of detail to use during the diff. Can be
388+ GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
389+ currently equivalent to AllObjects).
332390 """
333391 self .measure = measure .id
334392 self .voices_list = []
335393 if (
336394 len (measure .voices ) == 0
337395 ): # there is a single AnnVoice ( == for the library there are no voices)
338- ann_voice = AnnVoice (measure )
396+ ann_voice = AnnVoice (measure , detail )
339397 if ann_voice .n_of_notes > 0 :
340398 self .voices_list .append (ann_voice )
341399 else : # there are multiple voices (or an array with just one voice)
342400 for voice in measure .voices :
343- ann_voice = AnnVoice (voice )
401+ ann_voice = AnnVoice (voice , detail )
344402 if ann_voice .n_of_notes > 0 :
345403 self .voices_list .append (ann_voice )
346404 self .n_of_voices = len (self .voices_list )
347405
348406 self .extras_list = []
349- for extra in M21Utils .get_extras (measure , spannerBundle ):
350- self .extras_list .append (AnnExtra (extra , measure , score ))
407+ if detail >= DetailLevel .AllObjects :
408+ for extra in M21Utils .get_extras (measure , spannerBundle ):
409+ self .extras_list .append (AnnExtra (extra , measure , score , detail ))
351410
352- # For correct comparison, sort the extras_list, so that any list slices
353- # that all have the same offset are sorted alphabetically.
354- self .extras_list .sort (key = lambda e : ( e .offset , str (e ) ))
411+ # For correct comparison, sort the extras_list, so that any list slices
412+ # that all have the same offset are sorted alphabetically.
413+ self .extras_list .sort (key = lambda e : ( e .offset , str (e ) ))
355414
356415 # precomputed values to speed up the computation. As they start to be long, they are hashed
357416 self .precomputed_str = hash (self .__str__ ())
@@ -400,17 +459,25 @@ def get_note_ids(self):
400459
401460
402461class AnnPart :
403- def __init__ (self , part , score , spannerBundle ):
462+ def __init__ (self , part : m21 .stream .Part ,
463+ score : m21 .stream .Score ,
464+ spannerBundle : m21 .spanner .SpannerBundle ,
465+ detail : DetailLevel = DetailLevel .Default ):
404466 """
405467 Extend music21 Part/PartStaff with some precomputed, easily compared information about it.
406468
407469 Args:
408470 part (music21.stream.Part, music21.stream.PartStaff): The music21 Part/PartStaff to extend.
471+ score (music21.stream.Score): the enclosing music21 Score.
472+ spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners in the score.
473+ detail (DetailLevel): What level of detail to use during the diff. Can be
474+ GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
475+ currently equivalent to AllObjects).
409476 """
410477 self .part = part .id
411478 self .bar_list = []
412479 for measure in part .getElementsByClass ("Measure" ):
413- ann_bar = AnnMeasure (measure , score , spannerBundle ) # create the bar objects
480+ ann_bar = AnnMeasure (measure , score , spannerBundle , detail ) # create the bar objects
414481 if ann_bar .n_of_voices > 0 :
415482 self .bar_list .append (ann_bar )
416483 self .n_of_bars = len (self .bar_list )
@@ -456,19 +523,22 @@ def get_note_ids(self):
456523
457524
458525class AnnScore :
459- def __init__ (self , score ):
526+ def __init__ (self , score : m21 . stream . Score , detail : DetailLevel = DetailLevel . Default ):
460527 """
461528 Take a music21 score and store it as a sequence of Full Trees.
462529 The hierarchy is "score -> parts -> measures -> voices -> notes"
463530 Args:
464531 score (music21.stream.Score): The music21 score
532+ detail (DetailLevel): What level of detail to use during the diff. Can be
533+ GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
534+ currently equivalent to AllObjects).
465535 """
466536 self .score = score .id
467537 self .part_list = []
468- spannerBundle = score .spannerBundle
538+ spannerBundle : m21 . spanner . SpannerBundle = score .spannerBundle
469539 for part in score .parts .stream ():
470540 # create and add the AnnPart object to part_list
471- ann_part = AnnPart (part , score , spannerBundle )
541+ ann_part = AnnPart (part , score , spannerBundle , detail )
472542 if ann_part .n_of_bars > 0 :
473543 self .part_list .append (ann_part )
474544 self .n_of_parts = len (self .part_list )
0 commit comments