-
Notifications
You must be signed in to change notification settings - Fork 433
Add utility for removing duplicated objects (e.g. keys, clefs) #1454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 14 commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
0896e40
First implementation of #1392
MarkGotham afacdb0
explicitly limit to key.KeySignature and clef.Clef
MarkGotham 6b138cc
unused
MarkGotham e33e182
Check number of objects earlier
MarkGotham cd58782
Merge branch 'master' into duplicates
MarkGotham c96a580
Reinstate `meter.TimeSignature` checks (cf #1457 )
MarkGotham bf7db7a
lint - typing unused (not types beyond music21)
MarkGotham 270b5a3
Lint and docs
MarkGotham baa6d98
typo
MarkGotham a9cbd08
allow subclassing
MarkGotham c39bcfd
use inlace
MarkGotham 1a682f9
default `classesToRemove`
MarkGotham 60110ff
inPlace
MarkGotham 7c85ad3
dict by activeSite
MarkGotham 8df394c
score case
MarkGotham f85db3e
deep copy > coreCopy...
MarkGotham c0de8d2
remove each active site list at once
MarkGotham d1b91e2
is instance
MarkGotham 76617aa
pass `classesToRemove` on
MarkGotham 9f099cd
tuple (not mutable)
MarkGotham c6465d6
mypy?
MarkGotham e78ffb4
now unused import
MarkGotham 7601c8b
correct argument for values
MarkGotham 5965d6e
mypy - score may not have parts
MarkGotham a002505
fix indent
MarkGotham 8e1b668
tuple in name, not just in nature
MarkGotham 59cf4ae
didn't help mypy; did break lint
MarkGotham 9220d93
When typos come they do so not as single spies
MarkGotham fc0348a
mypy dict typing
MarkGotham File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| # -*- coding: utf-8 -*- | ||
| # ------------------------------------------------------------------------------ | ||
| # Name: stream/tools.py | ||
| # Purpose: Additional tools for working with streams | ||
| # | ||
| # Authors: Mark Gotham | ||
| # | ||
| # Copyright: Copyright © 2022 Michael Scott Asato Cuthbert | ||
| # License: BSD, see license.txt | ||
| # ------------------------------------------------------------------------------ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from music21 import clef | ||
| from music21 import environment | ||
| from music21 import key | ||
| from music21 import meter | ||
| from music21 import stream | ||
|
|
||
| from copy import deepcopy | ||
|
|
||
| environLocal = environment.Environment('stream.tools') | ||
|
|
||
|
|
||
| # ------------------------------------------------------------------------------ | ||
|
|
||
| def removeDuplicates(thisStream: stream.Stream, | ||
| classesToRemove: list = [meter.TimeSignature, key.KeySignature, clef.Clef], | ||
| inPlace: bool = True | ||
| ) -> stream.Stream: | ||
| ''' | ||
| The repetition of some classes like notes is common. | ||
| By contrast, the explicit repetition of certain other objects like clefs | ||
| usually indicates an error e.g., resulting from a copy'n'paste. | ||
| This function serves to remove those that are likely in error and make no change. | ||
|
|
||
| Use the `classesToRemove` argument to specify which music21 classes to check and remove. | ||
| The classes currently supported are: time signatures, key signatures, and clefs. | ||
| The `classesToRemove` arument defaults all three. | ||
MarkGotham marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
MarkGotham marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Sometimes there are legitimate reasons to duplicate even these classes. | ||
| In that case, override the default by specifying the list of which of the three of classes. | ||
| More classes may be added, but for now they will simply raise a ValueError. | ||
|
|
||
| So let's create an example part with an initial set of | ||
| time signature, key signature, and clef. | ||
|
|
||
| >>> s = stream.Part() | ||
| >>> s.append(meter.TimeSignature('3/4')) # first TS | ||
| >>> s.append(key.KeySignature(6)) # first KS | ||
| >>> s.append(clef.TrebleClef()) # first Clef | ||
|
|
||
| Then a few notes, followed by a duplicates of the | ||
| key signature, and clef. | ||
|
|
||
| >>> s.append(note.Note('C')) | ||
| >>> s.append(note.Note('C')) | ||
| >>> s.append(note.Note('D')) | ||
|
|
||
| >>> s.append(meter.TimeSignature('3/4')) # duplicate | ||
| >>> s.append(key.KeySignature(6)) # duplicate | ||
| >>> s.append(clef.TrebleClef()) # duplicate | ||
|
|
||
| Finally, a few more notes, followed by a | ||
| change of time signature, key signature, and clef. | ||
|
|
||
| >>> s.append(note.Note('E')) | ||
| >>> s.append(note.Note('F')) | ||
| >>> s.append(note.Note('G')) | ||
|
|
||
| >>> s.append(meter.TimeSignature('2/4')) | ||
| >>> s.append(key.KeySignature(-5)) | ||
| >>> s.append(clef.BassClef()) | ||
|
|
||
| >>> s.append(note.Note('A')) | ||
| >>> s.append(note.Note('B')) | ||
| >>> s.append(note.Note('C5')) | ||
|
|
||
| Now we'll make it into a proper part with measures and see | ||
| how it looks in its original, unaltered form: | ||
|
|
||
| >>> s.makeMeasures(inPlace=True) | ||
| >>> s.show('t') | ||
| {0.0} <music21.stream.Measure 1 offset=0.0> | ||
| {0.0} <music21.clef.TrebleClef> | ||
| {0.0} <music21.key.KeySignature of 6 sharps> | ||
| {0.0} <music21.meter.TimeSignature 3/4> | ||
| {0.0} <music21.note.Note C> | ||
| {1.0} <music21.note.Note C> | ||
| {2.0} <music21.note.Note D> | ||
| {3.0} <music21.stream.Measure 2 offset=3.0> | ||
| {0.0} <music21.clef.TrebleClef> | ||
| {0.0} <music21.key.KeySignature of 6 sharps> | ||
| {0.0} <music21.meter.TimeSignature 3/4> | ||
| {0.0} <music21.note.Note E> | ||
| {1.0} <music21.note.Note F> | ||
| {2.0} <music21.note.Note G> | ||
| {6.0} <music21.stream.Measure 3 offset=6.0> | ||
| {0.0} <music21.clef.BassClef> | ||
| {0.0} <music21.key.KeySignature of 5 flats> | ||
| {0.0} <music21.meter.TimeSignature 2/4> | ||
| {0.0} <music21.note.Note A> | ||
| {1.0} <music21.note.Note B> | ||
| {8.0} <music21.stream.Measure 4 offset=8.0> | ||
| {0.0} <music21.note.Note C> | ||
| {1.0} <music21.bar.Barline type=final> | ||
|
|
||
| Calling removeDuplicates should remove the duplicates | ||
| even with those changes now stored within measures, | ||
| not directly on the part. | ||
| Specifically, in our example, | ||
| the duplicates entries are removed from measure 2 | ||
| and the actual changes in measure 3 remain. | ||
|
|
||
| >>> testInPlace = stream.tools.removeDuplicates(s) | ||
| >>> testInPlace.show('t') | ||
| {0.0} <music21.stream.Measure 1 offset=0.0> | ||
| {0.0} <music21.clef.TrebleClef> | ||
| {0.0} <music21.key.KeySignature of 6 sharps> | ||
| {0.0} <music21.meter.TimeSignature 3/4> | ||
| {0.0} <music21.note.Note C> | ||
| {1.0} <music21.note.Note C> | ||
| {2.0} <music21.note.Note D> | ||
| {3.0} <music21.stream.Measure 2 offset=3.0> | ||
| {0.0} <music21.note.Note E> | ||
| {1.0} <music21.note.Note F> | ||
| {2.0} <music21.note.Note G> | ||
| {6.0} <music21.stream.Measure 3 offset=6.0> | ||
| {0.0} <music21.clef.BassClef> | ||
| {0.0} <music21.key.KeySignature of 5 flats> | ||
| {0.0} <music21.meter.TimeSignature 2/4> | ||
| {0.0} <music21.note.Note A> | ||
| {1.0} <music21.note.Note B> | ||
| {8.0} <music21.stream.Measure 4 offset=8.0> | ||
| {0.0} <music21.note.Note C> | ||
| {1.0} <music21.bar.Barline type=final> | ||
|
|
||
| As the example shows, this function defaults to working on a stream inPlace. | ||
|
|
||
| >>> testInPlace == s | ||
| True | ||
|
|
||
| To avoid this, set inPlace to False. | ||
|
|
||
| >>> testNotInPlace = stream.tools.removeDuplicates(s, inPlace=False) | ||
| >>> testNotInPlace == s | ||
| False | ||
|
|
||
| ''' | ||
|
|
||
| supportedClasses = [meter.TimeSignature, key.KeySignature, clef.Clef] | ||
|
|
||
| removalDict = {} | ||
|
|
||
| if not inPlace: | ||
| thisStream = deepcopy(thisStream) | ||
MarkGotham marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| for thisClass in classesToRemove: | ||
|
|
||
| if not any(issubclass(thisClass, supportedClass) for supportedClass in supportedClasses): | ||
| raise ValueError(f'Invalid class. Only {supportedClasses} are supported.') | ||
|
|
||
| allStates = thisStream.recurse().getElementsByClass(thisClass) | ||
|
|
||
| if len(allStates) < 2: # Not used, or doesn't change | ||
| continue | ||
|
|
||
| currentState = allStates[0] # First to initialize: can't be a duplicate | ||
| for thisState in allStates[1:]: | ||
| if thisState == currentState: | ||
| if thisState.activeSite in removalDict: # May be several in same (e.g., measure) | ||
| removalDict[thisState.activeSite].append(thisState) | ||
| else: | ||
| removalDict[thisState.activeSite] = [thisState] | ||
| else: | ||
| currentState = thisState | ||
|
|
||
| for activeSiteKey in removalDict: | ||
| for x in removalDict[activeSiteKey]: | ||
| activeSiteKey.remove(x, recurse=True) | ||
MarkGotham marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return thisStream | ||
|
|
||
|
|
||
| # ----------------------------------------------------------------------------- | ||
|
|
||
| if __name__ == '__main__': | ||
| import music21 | ||
| music21.mainTest() | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.