Skip to content
Merged
Show file tree
Hide file tree
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 Oct 7, 2022
afacdb0
explicitly limit to key.KeySignature and clef.Clef
MarkGotham Oct 11, 2022
6b138cc
unused
MarkGotham Oct 11, 2022
e33e182
Check number of objects earlier
MarkGotham Oct 11, 2022
cd58782
Merge branch 'master' into duplicates
MarkGotham Feb 1, 2023
c96a580
Reinstate `meter.TimeSignature` checks (cf #1457 )
MarkGotham Feb 1, 2023
bf7db7a
lint - typing unused (not types beyond music21)
MarkGotham Feb 1, 2023
270b5a3
Lint and docs
MarkGotham Feb 1, 2023
baa6d98
typo
MarkGotham Feb 12, 2023
a9cbd08
allow subclassing
MarkGotham Feb 12, 2023
c39bcfd
use inlace
MarkGotham Feb 12, 2023
1a682f9
default `classesToRemove`
MarkGotham Feb 12, 2023
60110ff
inPlace
MarkGotham Feb 12, 2023
7c85ad3
dict by activeSite
MarkGotham Feb 12, 2023
8df394c
score case
MarkGotham Feb 12, 2023
f85db3e
deep copy > coreCopy...
MarkGotham Feb 12, 2023
c0de8d2
remove each active site list at once
MarkGotham Feb 12, 2023
d1b91e2
is instance
MarkGotham Feb 12, 2023
76617aa
pass `classesToRemove` on
MarkGotham Feb 12, 2023
9f099cd
tuple (not mutable)
MarkGotham Feb 12, 2023
c6465d6
mypy?
MarkGotham Feb 12, 2023
e78ffb4
now unused import
MarkGotham Feb 12, 2023
7601c8b
correct argument for values
MarkGotham Feb 12, 2023
5965d6e
mypy - score may not have parts
MarkGotham Feb 12, 2023
a002505
fix indent
MarkGotham Feb 12, 2023
8e1b668
tuple in name, not just in nature
MarkGotham Feb 12, 2023
59cf4ae
didn't help mypy; did break lint
MarkGotham Feb 12, 2023
9220d93
When typos come they do so not as single spies
MarkGotham Feb 13, 2023
fc0348a
mypy dict typing
MarkGotham Feb 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions music21/stream/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@
from music21.stream import iterator
from music21.stream import makeNotation
from music21.stream import streamStatus
from music21.stream import tools
188 changes: 188 additions & 0 deletions music21/stream/tools.py
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.
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)

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)

return thisStream


# -----------------------------------------------------------------------------

if __name__ == '__main__':
import music21
music21.mainTest()