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 includesNotesAndRests(cls, val: int) -> bool:
113    @classmethod
114    def includesNotesAndRests(cls, val: int) -> bool:
115        return val & cls.NotesAndRests != 0
@classmethod
def includesBeams(cls, val: int) -> bool:
117    @classmethod
118    def includesBeams(cls, val: int) -> bool:
119        return val & cls.Beams != 0
@classmethod
def includesTremolos(cls, val: int) -> bool:
121    @classmethod
122    def includesTremolos(cls, val: int) -> bool:
123        return val & cls.Tremolos != 0
@classmethod
def includesOrnaments(cls, val: int) -> bool:
125    @classmethod
126    def includesOrnaments(cls, val: int) -> bool:
127        return val & cls.Ornaments != 0
@classmethod
def includesArticulations(cls, val: int) -> bool:
129    @classmethod
130    def includesArticulations(cls, val: int) -> bool:
131        return val & cls.Articulations != 0
@classmethod
def includesTies(cls, val: int) -> bool:
133    @classmethod
134    def includesTies(cls, val: int) -> bool:
135        return val & cls.Ties != 0
@classmethod
def includesSlurs(cls, val: int) -> bool:
137    @classmethod
138    def includesSlurs(cls, val: int) -> bool:
139        return val & cls.Slurs != 0
@classmethod
def includesSignatures(cls, val: int) -> bool:
141    @classmethod
142    def includesSignatures(cls, val: int) -> bool:
143        return val & cls.Signatures != 0
@classmethod
def includesDirections(cls, val: int) -> bool:
145    @classmethod
146    def includesDirections(cls, val: int) -> bool:
147        return val & cls.Directions != 0
@classmethod
def includesBarlines(cls, val: int) -> bool:
149    @classmethod
150    def includesBarlines(cls, val: int) -> bool:
151        return val & cls.Barlines != 0
@classmethod
def includesStaffDetails(cls, val: int) -> bool:
153    @classmethod
154    def includesStaffDetails(cls, val: int) -> bool:
155        return val & cls.StaffDetails != 0
@classmethod
def includesChordSymbols(cls, val: int) -> bool:
157    @classmethod
158    def includesChordSymbols(cls, val: int) -> bool:
159        return val & cls.ChordSymbols != 0
@classmethod
def includesOttavas(cls, val: int) -> bool:
161    @classmethod
162    def includesOttavas(cls, val: int) -> bool:
163        return val & cls.Ottavas != 0
@classmethod
def includesArpeggios(cls, val: int) -> bool:
165    @classmethod
166    def includesArpeggios(cls, val: int) -> bool:
167        return val & cls.Arpeggios != 0
@classmethod
def includesLyrics(cls, val: int) -> bool:
169    @classmethod
170    def includesLyrics(cls, val: int) -> bool:
171        return val & cls.Lyrics != 0
@classmethod
def includesStyle(cls, val: int) -> bool:
173    @classmethod
174    def includesStyle(cls, val: int) -> bool:
175        return val & cls.Style != 0
@classmethod
def includesMetadata(cls, val: int) -> bool:
177    @classmethod
178    def includesMetadata(cls, val: int) -> bool:
179        return val & cls.Metadata != 0
@classmethod
def includesVoicing(cls, val: int) -> bool:
181    @classmethod
182    def includesVoicing(cls, val: int) -> bool:
183        return val & cls.Voicing != 0
@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)