musicdiff.detaillevel
1# ------------------------------------------------------------------------------ 2# Purpose: detaillevel defines the levels of detail that can be requested 3# of musicdiff. 4# musicdiff is a package for comparing music scores using music21. 5# 6# Authors: Greg Chapman <gregc@mac.com> 7# musicdiff is derived from: 8# https://github.com/fosfrancesco/music-score-diff.git 9# by Francesco Foscarin <foscarin.francesco@gmail.com> 10# 11# Copyright: (c) 2022-2025 Francesco Foscarin, Greg Chapman 12# License: MIT, see LICENSE 13# ------------------------------------------------------------------------------ 14 15from enum import IntEnum 16import typing as t 17 18import music21 as m21 19 20from converter21 import M21Utilities 21 22_typesCache: dict[int, tuple[t.Type, ...]] = {} 23 24 25class DetailLevel(IntEnum): 26 # Bit definitions (can be |'ed together, as well as &~'ed to turn off options): 27 28 # notes, rests (without any decorations: beams/ties/ornaments/articulations/etc) 29 NotesAndRests = 1 30 31 # beams (if not requested, beams are treated exactly like flags) 32 # Note that if NotesAndRests are not also requested, no beams differences will be found. 33 Beams = 1 << 1 34 35 # tremolos (fingered and bowed) 36 # Note that if NotesAndRests are not also requested, no tremolo differences will be found. 37 Tremolos = 1 << 2 38 39 # trills, turns, mordents, fermatas, etc 40 # Note that if NotesAndRests are not also requested, no expression differences will be found. 41 Ornaments = 1 << 3 42 43 # staccato, tenuto, spiccato, fingering etc 44 # Note that if NotesAndRests are not also requested, no articulation differences will be found. 45 Articulations = 1 << 4 46 47 # ties 48 # Note that if NotesAndRests are not also requested, no tie differences will be found. 49 Ties = 1 << 5 50 51 # slurs 52 Slurs = 1 << 6 53 54 # decorated notes and rests (a combination of all of the above) 55 DecoratedNotesAndRests = ( 56 NotesAndRests | Beams | Tremolos | Ornaments | Articulations | Ties | Slurs 57 ) 58 59 # clefs, key signatures, time signatures 60 Signatures = 1 << 7 61 62 # tempos, metronome marks, dynamics, alternate endings, and other directions 63 Directions = 1 << 8 64 65 # bar lines (includes repeat-style bar lines) 66 Barlines = 1 << 9 67 68 # staff details (lines in staff, staff groups) 69 StaffDetails = 1 << 10 70 71 # chord symbols (jazz chords, roman-style chords, etc) 72 ChordSymbols = 1 << 11 73 74 # 8va, 8vb, etc 75 Ottavas = 1 << 12 76 77 # arpeggios 78 Arpeggios = 1 << 13 79 80 # Lyrics 81 Lyrics = 1 << 14 82 83 # other objects (everything above that isn't in DecoratedNotesAndRests) 84 OtherObjects = ( 85 Signatures | Directions | Barlines | StaffDetails 86 | ChordSymbols | Ottavas | Arpeggios | Lyrics 87 ) 88 89 # all objects = decorated notes and other musical objects 90 AllObjects = DecoratedNotesAndRests | OtherObjects 91 92 # A few extra details that are not a part of any combination. These must be added 93 # by hand if you want them. 94 95 # Typographical stuff: stem direction, note shape, color, italic/bold, etc 96 Style = 1 << 15 97 98 # Metadata: title, composer, etc 99 Metadata = 1 << 16 100 101 # By default, we ignore which voice and chord each note is in, and just compare the 102 # individual notes (and rests) themselves. If Voicing is turned on, we compare which 103 # voice and which chord each note is in. 104 # Note that comparison of voices is done with no consideration of voice ordering or 105 # voice ids; we compare the best matching pairs of voices. 106 Voicing = 1 << 17 107 108 # default detail level is all objects: 109 Default = AllObjects 110 111 # checkers for each individual bit 112 @classmethod 113 def includesNotesAndRests(cls, val: int) -> bool: 114 return val & cls.NotesAndRests != 0 115 116 @classmethod 117 def includesBeams(cls, val: int) -> bool: 118 return val & cls.Beams != 0 119 120 @classmethod 121 def includesTremolos(cls, val: int) -> bool: 122 return val & cls.Tremolos != 0 123 124 @classmethod 125 def includesOrnaments(cls, val: int) -> bool: 126 return val & cls.Ornaments != 0 127 128 @classmethod 129 def includesArticulations(cls, val: int) -> bool: 130 return val & cls.Articulations != 0 131 132 @classmethod 133 def includesTies(cls, val: int) -> bool: 134 return val & cls.Ties != 0 135 136 @classmethod 137 def includesSlurs(cls, val: int) -> bool: 138 return val & cls.Slurs != 0 139 140 @classmethod 141 def includesSignatures(cls, val: int) -> bool: 142 return val & cls.Signatures != 0 143 144 @classmethod 145 def includesDirections(cls, val: int) -> bool: 146 return val & cls.Directions != 0 147 148 @classmethod 149 def includesBarlines(cls, val: int) -> bool: 150 return val & cls.Barlines != 0 151 152 @classmethod 153 def includesStaffDetails(cls, val: int) -> bool: 154 return val & cls.StaffDetails != 0 155 156 @classmethod 157 def includesChordSymbols(cls, val: int) -> bool: 158 return val & cls.ChordSymbols != 0 159 160 @classmethod 161 def includesOttavas(cls, val: int) -> bool: 162 return val & cls.Ottavas != 0 163 164 @classmethod 165 def includesArpeggios(cls, val: int) -> bool: 166 return val & cls.Arpeggios != 0 167 168 @classmethod 169 def includesLyrics(cls, val: int) -> bool: 170 return val & cls.Lyrics != 0 171 172 @classmethod 173 def includesStyle(cls, val: int) -> bool: 174 return val & cls.Style != 0 175 176 @classmethod 177 def includesMetadata(cls, val: int) -> bool: 178 return val & cls.Metadata != 0 179 180 @classmethod 181 def includesVoicing(cls, val: int) -> bool: 182 return val & cls.Voicing != 0 183 184 @classmethod 185 def _included_m21_types(cls, val: int) -> tuple[t.Type, ...]: 186 # Not all types go in here, just the ones where we will have pulled 187 # a bunch of objects, and then have to filter them. So far, that's 188 # just the non-GeneralNote objects that we pull from a measure 189 # for the extras list (including all spanners). We don't have 190 # to put GeneralNotes here, or anything that we would only find in 191 # gn.expressions (a.k.a. DetailLevel.Ornaments) or gn.articulations 192 # (a.k.a. DetailLevel.Articulations). 193 if val not in _typesCache: 194 typesList: list[t.Type] = [] 195 if cls.includesTremolos(val): 196 typesList.extend([ 197 m21.expressions.Tremolo, 198 m21.expressions.TremoloSpanner 199 ]) 200 201 if cls.includesSlurs(val): 202 typesList.append(m21.spanner.Slur) 203 204 if cls.includesSignatures(val): 205 typesList.extend([ 206 m21.clef.Clef, 207 m21.key.KeySignature, 208 m21.meter.TimeSignature, 209 ]) 210 211 if cls.includesStaffDetails(val): 212 typesList.extend([ 213 m21.layout.StaffLayout, 214 m21.layout.StaffGroup 215 ]) 216 217 if cls.includesDirections(val): 218 typesList.extend([ 219 m21.expressions.TextExpression, 220 m21.tempo.TempoIndication, 221 m21.dynamics.Dynamic, 222 m21.dynamics.DynamicWedge, 223 m21.spanner.RepeatBracket, # e.g. first and second endings 224 m21.expressions.RehearsalMark, 225 m21.repeat.RepeatExpressionMarker, 226 m21.repeat.RepeatExpressionCommand, 227 ]) 228 if M21Utilities.m21PedalMarksSupported(): 229 typesList.extend([ 230 m21.expressions.PedalMark, # type: ignore 231 m21.expressions.PedalBounce, # type: ignore 232 m21.expressions.PedalGapStart, # type: ignore 233 m21.expressions.PedalGapEnd, # type: ignore 234 ]) 235 236 if cls.includesBarlines(val): 237 typesList.extend([ 238 m21.bar.Barline, 239 m21.bar.Repeat 240 ]) 241 242 if cls.includesOttavas(val): 243 typesList.append(m21.spanner.Ottava) 244 245 if cls.includesArpeggios(val): 246 typesList.extend([ 247 m21.expressions.ArpeggioMark, 248 m21.expressions.ArpeggioMarkSpanner 249 ]) 250 251 if cls.includesChordSymbols(val): 252 typesList.append(m21.harmony.ChordSymbol) 253 254 if cls.includesStyle(val): 255 # we have to add these here, because they are style-only (no substance) 256 typesList.extend([ 257 m21.layout.SystemLayout, 258 m21.layout.PageLayout 259 ]) 260 261 _typesCache[val] = tuple(typesList) 262 263 return _typesCache[val] 264 265 @classmethod 266 def objIsIncluded(cls, obj: m21.base.Music21Object, val: int) -> bool: 267 types: tuple[t.Type, ...] = cls._included_m21_types(val) 268 269 # We have to check ChordSymbol by hand, since ChordSymbol is derived 270 # from GeneralNote, and should ONLY be included if ChordSymbol is in 271 # the list, NOT just because GeneralNote is in the list. 272 # I would note that GeneralNote is currently _never_ in the list, 273 # but I leave this code in place so that we don't break something 274 # unexpectedly if we put GeneralNote in the list in the future. 275 if isinstance(obj, m21.harmony.ChordSymbol): 276 return m21.harmony.ChordSymbol in types 277 278 return isinstance(obj, types)
class
DetailLevel(enum.IntEnum):
26class DetailLevel(IntEnum): 27 # Bit definitions (can be |'ed together, as well as &~'ed to turn off options): 28 29 # notes, rests (without any decorations: beams/ties/ornaments/articulations/etc) 30 NotesAndRests = 1 31 32 # beams (if not requested, beams are treated exactly like flags) 33 # Note that if NotesAndRests are not also requested, no beams differences will be found. 34 Beams = 1 << 1 35 36 # tremolos (fingered and bowed) 37 # Note that if NotesAndRests are not also requested, no tremolo differences will be found. 38 Tremolos = 1 << 2 39 40 # trills, turns, mordents, fermatas, etc 41 # Note that if NotesAndRests are not also requested, no expression differences will be found. 42 Ornaments = 1 << 3 43 44 # staccato, tenuto, spiccato, fingering etc 45 # Note that if NotesAndRests are not also requested, no articulation differences will be found. 46 Articulations = 1 << 4 47 48 # ties 49 # Note that if NotesAndRests are not also requested, no tie differences will be found. 50 Ties = 1 << 5 51 52 # slurs 53 Slurs = 1 << 6 54 55 # decorated notes and rests (a combination of all of the above) 56 DecoratedNotesAndRests = ( 57 NotesAndRests | Beams | Tremolos | Ornaments | Articulations | Ties | Slurs 58 ) 59 60 # clefs, key signatures, time signatures 61 Signatures = 1 << 7 62 63 # tempos, metronome marks, dynamics, alternate endings, and other directions 64 Directions = 1 << 8 65 66 # bar lines (includes repeat-style bar lines) 67 Barlines = 1 << 9 68 69 # staff details (lines in staff, staff groups) 70 StaffDetails = 1 << 10 71 72 # chord symbols (jazz chords, roman-style chords, etc) 73 ChordSymbols = 1 << 11 74 75 # 8va, 8vb, etc 76 Ottavas = 1 << 12 77 78 # arpeggios 79 Arpeggios = 1 << 13 80 81 # Lyrics 82 Lyrics = 1 << 14 83 84 # other objects (everything above that isn't in DecoratedNotesAndRests) 85 OtherObjects = ( 86 Signatures | Directions | Barlines | StaffDetails 87 | ChordSymbols | Ottavas | Arpeggios | Lyrics 88 ) 89 90 # all objects = decorated notes and other musical objects 91 AllObjects = DecoratedNotesAndRests | OtherObjects 92 93 # A few extra details that are not a part of any combination. These must be added 94 # by hand if you want them. 95 96 # Typographical stuff: stem direction, note shape, color, italic/bold, etc 97 Style = 1 << 15 98 99 # Metadata: title, composer, etc 100 Metadata = 1 << 16 101 102 # By default, we ignore which voice and chord each note is in, and just compare the 103 # individual notes (and rests) themselves. If Voicing is turned on, we compare which 104 # voice and which chord each note is in. 105 # Note that comparison of voices is done with no consideration of voice ordering or 106 # voice ids; we compare the best matching pairs of voices. 107 Voicing = 1 << 17 108 109 # default detail level is all objects: 110 Default = AllObjects 111 112 # checkers for each individual bit 113 @classmethod 114 def includesNotesAndRests(cls, val: int) -> bool: 115 return val & cls.NotesAndRests != 0 116 117 @classmethod 118 def includesBeams(cls, val: int) -> bool: 119 return val & cls.Beams != 0 120 121 @classmethod 122 def includesTremolos(cls, val: int) -> bool: 123 return val & cls.Tremolos != 0 124 125 @classmethod 126 def includesOrnaments(cls, val: int) -> bool: 127 return val & cls.Ornaments != 0 128 129 @classmethod 130 def includesArticulations(cls, val: int) -> bool: 131 return val & cls.Articulations != 0 132 133 @classmethod 134 def includesTies(cls, val: int) -> bool: 135 return val & cls.Ties != 0 136 137 @classmethod 138 def includesSlurs(cls, val: int) -> bool: 139 return val & cls.Slurs != 0 140 141 @classmethod 142 def includesSignatures(cls, val: int) -> bool: 143 return val & cls.Signatures != 0 144 145 @classmethod 146 def includesDirections(cls, val: int) -> bool: 147 return val & cls.Directions != 0 148 149 @classmethod 150 def includesBarlines(cls, val: int) -> bool: 151 return val & cls.Barlines != 0 152 153 @classmethod 154 def includesStaffDetails(cls, val: int) -> bool: 155 return val & cls.StaffDetails != 0 156 157 @classmethod 158 def includesChordSymbols(cls, val: int) -> bool: 159 return val & cls.ChordSymbols != 0 160 161 @classmethod 162 def includesOttavas(cls, val: int) -> bool: 163 return val & cls.Ottavas != 0 164 165 @classmethod 166 def includesArpeggios(cls, val: int) -> bool: 167 return val & cls.Arpeggios != 0 168 169 @classmethod 170 def includesLyrics(cls, val: int) -> bool: 171 return val & cls.Lyrics != 0 172 173 @classmethod 174 def includesStyle(cls, val: int) -> bool: 175 return val & cls.Style != 0 176 177 @classmethod 178 def includesMetadata(cls, val: int) -> bool: 179 return val & cls.Metadata != 0 180 181 @classmethod 182 def includesVoicing(cls, val: int) -> bool: 183 return val & cls.Voicing != 0 184 185 @classmethod 186 def _included_m21_types(cls, val: int) -> tuple[t.Type, ...]: 187 # Not all types go in here, just the ones where we will have pulled 188 # a bunch of objects, and then have to filter them. So far, that's 189 # just the non-GeneralNote objects that we pull from a measure 190 # for the extras list (including all spanners). We don't have 191 # to put GeneralNotes here, or anything that we would only find in 192 # gn.expressions (a.k.a. DetailLevel.Ornaments) or gn.articulations 193 # (a.k.a. DetailLevel.Articulations). 194 if val not in _typesCache: 195 typesList: list[t.Type] = [] 196 if cls.includesTremolos(val): 197 typesList.extend([ 198 m21.expressions.Tremolo, 199 m21.expressions.TremoloSpanner 200 ]) 201 202 if cls.includesSlurs(val): 203 typesList.append(m21.spanner.Slur) 204 205 if cls.includesSignatures(val): 206 typesList.extend([ 207 m21.clef.Clef, 208 m21.key.KeySignature, 209 m21.meter.TimeSignature, 210 ]) 211 212 if cls.includesStaffDetails(val): 213 typesList.extend([ 214 m21.layout.StaffLayout, 215 m21.layout.StaffGroup 216 ]) 217 218 if cls.includesDirections(val): 219 typesList.extend([ 220 m21.expressions.TextExpression, 221 m21.tempo.TempoIndication, 222 m21.dynamics.Dynamic, 223 m21.dynamics.DynamicWedge, 224 m21.spanner.RepeatBracket, # e.g. first and second endings 225 m21.expressions.RehearsalMark, 226 m21.repeat.RepeatExpressionMarker, 227 m21.repeat.RepeatExpressionCommand, 228 ]) 229 if M21Utilities.m21PedalMarksSupported(): 230 typesList.extend([ 231 m21.expressions.PedalMark, # type: ignore 232 m21.expressions.PedalBounce, # type: ignore 233 m21.expressions.PedalGapStart, # type: ignore 234 m21.expressions.PedalGapEnd, # type: ignore 235 ]) 236 237 if cls.includesBarlines(val): 238 typesList.extend([ 239 m21.bar.Barline, 240 m21.bar.Repeat 241 ]) 242 243 if cls.includesOttavas(val): 244 typesList.append(m21.spanner.Ottava) 245 246 if cls.includesArpeggios(val): 247 typesList.extend([ 248 m21.expressions.ArpeggioMark, 249 m21.expressions.ArpeggioMarkSpanner 250 ]) 251 252 if cls.includesChordSymbols(val): 253 typesList.append(m21.harmony.ChordSymbol) 254 255 if cls.includesStyle(val): 256 # we have to add these here, because they are style-only (no substance) 257 typesList.extend([ 258 m21.layout.SystemLayout, 259 m21.layout.PageLayout 260 ]) 261 262 _typesCache[val] = tuple(typesList) 263 264 return _typesCache[val] 265 266 @classmethod 267 def objIsIncluded(cls, obj: m21.base.Music21Object, val: int) -> bool: 268 types: tuple[t.Type, ...] = cls._included_m21_types(val) 269 270 # We have to check ChordSymbol by hand, since ChordSymbol is derived 271 # from GeneralNote, and should ONLY be included if ChordSymbol is in 272 # the list, NOT just because GeneralNote is in the list. 273 # I would note that GeneralNote is currently _never_ in the list, 274 # but I leave this code in place so that we don't break something 275 # unexpectedly if we put GeneralNote in the list in the future. 276 if isinstance(obj, m21.harmony.ChordSymbol): 277 return m21.harmony.ChordSymbol in types 278 279 return isinstance(obj, types)
An enumeration.
NotesAndRests =
<DetailLevel.NotesAndRests: 1>
Beams =
<DetailLevel.Beams: 2>
Tremolos =
<DetailLevel.Tremolos: 4>
Ornaments =
<DetailLevel.Ornaments: 8>
Articulations =
<DetailLevel.Articulations: 16>
Ties =
<DetailLevel.Ties: 32>
Slurs =
<DetailLevel.Slurs: 64>
DecoratedNotesAndRests =
<DetailLevel.DecoratedNotesAndRests: 127>
Signatures =
<DetailLevel.Signatures: 128>
Directions =
<DetailLevel.Directions: 256>
Barlines =
<DetailLevel.Barlines: 512>
StaffDetails =
<DetailLevel.StaffDetails: 1024>
ChordSymbols =
<DetailLevel.ChordSymbols: 2048>
Ottavas =
<DetailLevel.Ottavas: 4096>
Arpeggios =
<DetailLevel.Arpeggios: 8192>
Lyrics =
<DetailLevel.Lyrics: 16384>
OtherObjects =
<DetailLevel.OtherObjects: 32640>
AllObjects =
<DetailLevel.AllObjects: 32767>
Style =
<DetailLevel.Style: 32768>
Metadata =
<DetailLevel.Metadata: 65536>
Voicing =
<DetailLevel.Voicing: 131072>
Default =
<DetailLevel.AllObjects: 32767>
@classmethod
def
objIsIncluded(cls, obj: music21.base.Music21Object, val: int) -> bool:
266 @classmethod 267 def objIsIncluded(cls, obj: m21.base.Music21Object, val: int) -> bool: 268 types: tuple[t.Type, ...] = cls._included_m21_types(val) 269 270 # We have to check ChordSymbol by hand, since ChordSymbol is derived 271 # from GeneralNote, and should ONLY be included if ChordSymbol is in 272 # the list, NOT just because GeneralNote is in the list. 273 # I would note that GeneralNote is currently _never_ in the list, 274 # but I leave this code in place so that we don't break something 275 # unexpectedly if we put GeneralNote in the list in the future. 276 if isinstance(obj, m21.harmony.ChordSymbol): 277 return m21.harmony.ChordSymbol in types 278 279 return isinstance(obj, types)