From eede2a810b53b75ae0dd5ac0112c5c95610e319e Mon Sep 17 00:00:00 2001 From: Myke Cuthbert Date: Wed, 27 Apr 2022 00:36:07 -1000 Subject: [PATCH 1/6] more class substitution, editing, typing. --- music21/abcFormat/translate.py | 22 +- music21/analysis/discrete.py | 2 +- music21/analysis/elements.py | 2 +- music21/analysis/metrical.py | 2 +- music21/analysis/reduceChords.py | 2 +- music21/analysis/reduceChordsOld.py | 2 +- music21/analysis/reduction.py | 2 +- music21/base.py | 14 +- music21/braille/test.py | 2 +- music21/derivation.py | 8 +- music21/features/base.py | 2 +- music21/figuredBass/checker.py | 4 +- music21/humdrum/spineParser.py | 2 +- music21/layout.py | 35 +- music21/midi/translate.py | 9 +- music21/musicxml/test_m21ToXml.py | 2 +- music21/musicxml/test_xmlToM21.py | 52 +- music21/musicxml/xmlToM21.py | 2 +- music21/omr/correctors.py | 2 +- music21/repeat.py | 84 +- music21/romanText/writeRoman.py | 2 +- music21/search/lyrics.py | 2 +- music21/search/serial.py | 2 +- music21/serial.py | 2 +- music21/spanner.py | 2 +- music21/stream/base.py | 19 +- music21/stream/iterator.py | 10 +- music21/stream/makeNotation.py | 2 +- music21/stream/tests.py | 8 +- music21/tinyNotation.py | 4 +- music21/variant.py | 4193 ++++++++++++++------------- 31 files changed, 2254 insertions(+), 2244 deletions(-) diff --git a/music21/abcFormat/translate.py b/music21/abcFormat/translate.py index 167cac1b26..759891d468 100644 --- a/music21/abcFormat/translate.py +++ b/music21/abcFormat/translate.py @@ -205,7 +205,7 @@ def abcToStreamPart(abcHandler, inputM21=None, spannerBundle=None): if postTransposition != 0: p.transpose(postTransposition, inPlace=True) - if useMeasures and p.recurse().getElementsByClass('TimeSignature'): + if useMeasures and p[meter.TimeSignature]: # call make beams for now; later, import beams # environLocal.printDebug(['abcToStreamPart: calling makeBeams']) try: @@ -850,16 +850,16 @@ def testChordSymbols(self): p1 = o.getScoreByNumber(81).parts[0] self.assertEqual(p1.offset, 0.0) self.assertEqual(len(p1.flatten().notesAndRests), 77) - self.assertEqual(len(list(p1.flatten().getElementsByClass('ChordSymbol'))), 25) + self.assertEqual(len(list(p1.flatten().getElementsByClass(harmony.ChordSymbol))), 25) # Am/C - self.assertEqual(list(p1.flatten().getElementsByClass('ChordSymbol'))[7].root(), + self.assertEqual(list(p1.flatten().getElementsByClass(harmony.ChordSymbol))[7].root(), pitch.Pitch('A3')) - self.assertEqual(list(p1.flatten().getElementsByClass('ChordSymbol'))[7].bass(), + self.assertEqual(list(p1.flatten().getElementsByClass(harmony.ChordSymbol))[7].bass(), pitch.Pitch('C3')) # G7/B - self.assertEqual(list(p1.flatten().getElementsByClass('ChordSymbol'))[14].root(), + self.assertEqual(list(p1.flatten().getElementsByClass(harmony.ChordSymbol))[14].root(), pitch.Pitch('G3')) - self.assertEqual(list(p1.flatten().getElementsByClass('ChordSymbol'))[14].bass(), + self.assertEqual(list(p1.flatten().getElementsByClass(harmony.ChordSymbol))[14].bass(), pitch.Pitch('B2')) def testNoChord(self): @@ -884,9 +884,9 @@ def testNoChord(self): score = harmony.realizeChordSymbolDurations(score) - self.assertEqual(8, score.getElementsByClass('ChordSymbol')[ + self.assertEqual(8, score.getElementsByClass(harmony.ChordSymbol)[ -1].quarterLength) - self.assertEqual(4, score.getElementsByClass('ChordSymbol')[ + self.assertEqual(4, score.getElementsByClass(harmony.ChordSymbol)[ 0].quarterLength) def testAbcKeyImport(self): @@ -965,14 +965,14 @@ def testRepeatBracketsB(self): from music21 import corpus s = converter.parse(testFiles.morrisonsJig) # TODO: get - self.assertEqual(len(s.flatten().getElementsByClass('RepeatBracket')), 2) + self.assertEqual(len(s.flatten().getElementsByClass(spanner.RepeatBracket)), 2) # s.show() # four repeat brackets here; 2 at beginning, 2 at end s = converter.parse(testFiles.hectorTheHero) - self.assertEqual(len(s.flatten().getElementsByClass('RepeatBracket')), 4) + self.assertEqual(len(s.flatten().getElementsByClass(spanner.RepeatBracket)), 4) s = corpus.parse('JollyTinkersReel') - self.assertEqual(len(s.flatten().getElementsByClass('RepeatBracket')), 4) + self.assertEqual(len(s.flatten().getElementsByClass(spanner.RepeatBracket)), 4) def testMetronomeMarkA(self): from music21.abcFormat import testFiles diff --git a/music21/analysis/discrete.py b/music21/analysis/discrete.py index e5e804e0df..7f1779f590 100644 --- a/music21/analysis/discrete.py +++ b/music21/analysis/discrete.py @@ -1240,7 +1240,7 @@ def countMelodicIntervals(self, sStream, found=None, ignoreDirection=True, ignor for p in procList: # get only Notes for now, skipping rests and chords # flatten to reach notes contained in measures - noteStream = p.flatten().stripTies(inPlace=False).getElementsByClass('Note').stream() + noteStream = p.flatten().stripTies(inPlace=False).getElementsByClass(note.Note).stream() # noteStream.show() for i, n in enumerate(noteStream): if i <= len(noteStream) - 2: diff --git a/music21/analysis/elements.py b/music21/analysis/elements.py index 1d0c89d8f1..070477374c 100644 --- a/music21/analysis/elements.py +++ b/music21/analysis/elements.py @@ -19,7 +19,7 @@ def attributeCount(streamOrStreamIter, attrName='quarterLength') -> collections. >>> from music21 import corpus >>> bach = corpus.parse('bach/bwv324.xml') - >>> bachIter = bach.parts[0].recurse().getElementsByClass('Note') + >>> bachIter = bach.parts[0].recurse().getElementsByClass(note.Note) >>> qlCount = analysis.elements.attributeCount(bachIter, 'quarterLength') >>> qlCount.most_common(3) [(1.0, 12), (2.0, 11), (4.0, 2)] diff --git a/music21/analysis/metrical.py b/music21/analysis/metrical.py index 14bfc089ae..b512042e4d 100644 --- a/music21/analysis/metrical.py +++ b/music21/analysis/metrical.py @@ -86,7 +86,7 @@ def thomassenMelodicAccent(streamIn): .. _melac: https://www.humdrum.org/Humdrum/commands/melac.html Takes in a Stream of :class:`~music21.note.Note` objects (use `.flatten().notes` to get it, or - better `.flatten().getElementsByClass('Note')` to filter out chords) and adds the attribute to + better `.flatten().getElementsByClass(note.Note)` to filter out chords) and adds the attribute to each. Note that Huron and Royal's work suggests that melodic accent has a correlation with metrical accent only for solo works/passages; even treble passages do not have a strong correlation. (Gregorian chants were found to have a strong ''negative'' correlation diff --git a/music21/analysis/reduceChords.py b/music21/analysis/reduceChords.py index 3a69721e1c..b555e513dd 100644 --- a/music21/analysis/reduceChords.py +++ b/music21/analysis/reduceChords.py @@ -142,7 +142,7 @@ def run(self, reduction.append(chordifiedPart) if closedPosition: - for x in reduction.recurse().getElementsByClass('Chord'): + for x in reduction[chord.Chord]: x.closedPosition(forceOctave=4, inPlace=True) return reduction diff --git a/music21/analysis/reduceChordsOld.py b/music21/analysis/reduceChordsOld.py index c645cf4ddb..a6be45d543 100644 --- a/music21/analysis/reduceChordsOld.py +++ b/music21/analysis/reduceChordsOld.py @@ -369,7 +369,7 @@ def testTrecentoMadrigal(self): from music21 import key from music21 import roman cm = key.Key('G') - for thisChord in p.recurse().getElementsByClass('Chord'): + for thisChord in p[chord.Chord]: thisChord.lyric = roman.romanNumeralFromChord(thisChord, cm, preferSecondaryDominants=True).figure diff --git a/music21/analysis/reduction.py b/music21/analysis/reduction.py index 1997e66205..5b8e8323a1 100644 --- a/music21/analysis/reduction.py +++ b/music21/analysis/reduction.py @@ -399,7 +399,7 @@ def _createReduction(self): v.makeRests(fillGaps=True, inPlace=True) m.flattenUnnecessaryVoices(inPlace=True) # hide all rests in all containers - for r in m.recurse().getElementsByClass('Rest'): + for r in m[note.Rest]: r.style.hideObjectOnPrint = True # m.show('t') # add to score diff --git a/music21/base.py b/music21/base.py index 4ddc0edcb9..d147937edc 100644 --- a/music21/base.py +++ b/music21/base.py @@ -3877,14 +3877,14 @@ class ElementWrapper(Music21Object): ... el = music21.ElementWrapper(soundFile) ... s.insert(i, el) - >>> for j in s.getElementsByClass('ElementWrapper'): + >>> for j in s.getElementsByClass(base.ElementWrapper): ... if j.beatStrength > 0.4: ... (j.offset, j.beatStrength, j.getnchannels(), j.fileName) (0.0, 1.0, 2, 'thisSound_1.wav') (3.0, 1.0, 2, 'thisSound_16.wav') (6.0, 1.0, 2, 'thisSound_12.wav') (9.0, 1.0, 2, 'thisSound_8.wav') - >>> for j in s.getElementsByClass('ElementWrapper'): + >>> for j in s.getElementsByClass(base.ElementWrapper): ... if j.beatStrength > 0.4: ... (j.offset, j.beatStrength, j.getnchannels() + 1, j.fileName) (0.0, 1.0, 3, 'thisSound_1.wav') @@ -3894,7 +3894,7 @@ class ElementWrapper(Music21Object): Test representation of an ElementWrapper - >>> for i, j in enumerate(s.getElementsByClass('ElementWrapper')): + >>> for i, j in enumerate(s.getElementsByClass(base.ElementWrapper)): ... if i == 2: ... j.id = None ... else: @@ -4575,15 +4575,15 @@ def testRecurseByClass(self): s3.append(n3) # only get n1 here, as that is only level available - self.assertEqual(s1.recurse().getElementsByClass('Note').first(), n1) - self.assertEqual(s2.recurse().getElementsByClass('Note').first(), n2) + self.assertEqual(s1.recurse().getElementsByClass(note.Note).first(), n1) + self.assertEqual(s2.recurse().getElementsByClass(note.Note).first(), n2) self.assertEqual(s1.recurse().getElementsByClass('Clef').first(), c1) self.assertEqual(s2.recurse().getElementsByClass('Clef').first(), c2) # attach s2 to s1 s2.append(s1) # stream 1 gets both notes - self.assertEqual(list(s2.recurse().getElementsByClass('Note')), [n2, n1]) + self.assertEqual(list(s2.recurse().getElementsByClass(note.Note)), [n2, n1]) def testSetEditorial(self): b2 = Music21Object() @@ -4697,7 +4697,7 @@ def getnchannels(self): matchBeatStrength = [] matchAudioChannels = [] - for j in s.getElementsByClass('ElementWrapper'): + for j in s.getElementsByClass(ElementWrapper): matchOffset.append(j.offset) matchBeatStrength.append(j.beatStrength) matchAudioChannels.append(j.getnchannels()) diff --git a/music21/braille/test.py b/music21/braille/test.py index abe734cb68..bb755d480f 100644 --- a/music21/braille/test.py +++ b/music21/braille/test.py @@ -2124,7 +2124,7 @@ def test_drill10_4(self): bm.insert(0, tempo.TempoText('Con brio')) bm.insert(25.0, clef.TrebleClef()) bm.insert(32.0, clef.BassClef()) - bm.recurse().getElementsByClass('TimeSignature').first().symbol = 'common' + bm[meter.TimeSignature].first().symbol = 'common' bm.makeNotation(inPlace=True, cautionaryNotImmediateRepeat=False) m = bm.getElementsByClass(stream.Measure) m[0].padAsAnacrusis(useInitialRests=True) diff --git a/music21/derivation.py b/music21/derivation.py index 763c79221e..12566c4191 100644 --- a/music21/derivation.py +++ b/music21/derivation.py @@ -222,9 +222,9 @@ def chain(self) -> Generator['music21.base.Music21Object', None, None]: >>> s1.id = 's1' >>> s1.repeatAppend(note.Note(), 10) >>> s1.repeatAppend(note.Rest(), 10) - >>> s2 = s1.getElementsByClass('GeneralNote').stream() + >>> s2 = s1.notesAndRests.stream() >>> s2.id = 's2' - >>> s3 = s2.getElementsByClass('Note').stream() + >>> s3 = s2.getElementsByClass(note.Note).stream() >>> s3.id = 's3' >>> for y in s3.derivation.chain(): ... print(y) @@ -308,8 +308,8 @@ def rootDerivation(self) -> Optional['music21.base.Music21Object']: >>> s1 = stream.Stream() >>> s1.repeatAppend(note.Note(), 10) >>> s1.repeatAppend(note.Rest(), 10) - >>> s2 = s1.getElementsByClass('GeneralNote').stream() - >>> s3 = s2.getElementsByClass('Note').stream() + >>> s2 = s1.notesAndRests.stream() + >>> s3 = s2.getElementsByClass(note.Note).stream() >>> s3.derivation.rootDerivation is s1 True ''' diff --git a/music21/features/base.py b/music21/features/base.py index 9f793342b1..2e881f036c 100644 --- a/music21/features/base.py +++ b/music21/features/base.py @@ -608,7 +608,7 @@ def setupPostStreamParse(self): else: self.partsCount = 0 - for v in self.stream.recurse().getElementsByClass('Voice'): + for v in self.stream[stream.Voice]: self.formsByPart.append(StreamForms(v)) def setClassLabel(self, classLabel, classValue=None): diff --git a/music21/figuredBass/checker.py b/music21/figuredBass/checker.py index f2f4bd50cb..7efd342fc1 100644 --- a/music21/figuredBass/checker.py +++ b/music21/figuredBass/checker.py @@ -141,7 +141,7 @@ def createOffsetMapping(music21Part): (11.0, 12.0) [ ] ''' currentMapping = collections.defaultdict(list) - for music21GeneralNote in music21Part.flatten().getElementsByClass('GeneralNote'): + for music21GeneralNote in music21Part.flatten().notesAndRests: initOffset = music21GeneralNote.offset endTime = initOffset + music21GeneralNote.quarterLength currentMapping[(initOffset, endTime)].append(music21GeneralNote) @@ -183,7 +183,7 @@ def correlateHarmonies(currentMapping, music21Part): for offsets in sorted(currentMapping.keys()): (initOffset, endTime) = offsets - notesInRange = music21Part.flatten().getElementsByClass('GeneralNote').getElementsByOffset( + notesInRange = music21Part.flatten().notesAndRests.getElementsByOffset( initOffset, offsetEnd=endTime, includeEndBoundary=False, mustFinishInSpan=False, mustBeginInSpan=False, includeElementsThatEndAtStart=False) diff --git a/music21/humdrum/spineParser.py b/music21/humdrum/spineParser.py index 660e36ff15..2290329f95 100644 --- a/music21/humdrum/spineParser.py +++ b/music21/humdrum/spineParser.py @@ -773,7 +773,7 @@ def parseMetadata(self, s=None): s.metadata = md grToRemove = [] - for gr in s.recurse().getElementsByClass('GlobalReference'): + for gr in s[GlobalReference]: wasParsed = gr.updateMetadata(md) if wasParsed: grToRemove.append(gr) diff --git a/music21/layout.py b/music21/layout.py index 620562fb1f..1e3bf97ce4 100644 --- a/music21/layout.py +++ b/music21/layout.py @@ -26,14 +26,14 @@ e.g., the left hand of the Piano is a PartStaff paired with the right hand). PageLayout and SystemLayout objects also have a property, 'isNew', -which if set to `True` signifies that a new page +which, if set to `True`, signifies that a new page or system should begin here. In theory, one could define new dimensions for a page or system in the middle of the system or page without setting isNew to True, in which case these measurements would start applying on the next page. In practice, there's really one good place to use these Layout objects and that's in the first part in a score at offset 0 of the first measure on a page or system (or for ScoreLayout, at the beginning -of a piece outside of any parts). But it's not an +of a piece outside any parts). But it's not an error to put them in other places, such as at offset 0 of the first measure of a page or system in all the other parts. In fact, MusicXML tends to do this, and it ends up not being a waste if a program extracts a single part from the middle of a score. @@ -86,11 +86,12 @@ SmartScore Pro tends to produce very good MusicXML layout data. ''' -# may need to have object to convert between size units +# may need to have an object to convert between size units import copy import unittest from collections import namedtuple +from typing import Tuple, Optional from music21 import base from music21 import exceptions21 @@ -103,8 +104,8 @@ environLocal = environment.Environment(_MOD) -SystemSize = namedtuple('SystemSize', 'top left right bottom') -PageSize = namedtuple('PageSize', 'top left right bottom width height') +SystemSize = namedtuple('SystemSize', ['top', 'left', 'right', 'bottom']) +PageSize = namedtuple('PageSize', ['top', 'left', 'right', 'bottom', 'width', 'height']) class LayoutBase(base.Music21Object): @@ -461,7 +462,7 @@ def __init__(self, *arguments, **keywords): self.name = None # if this group has a name self.abbreviation = None - self._symbol = None # can be bracket, line, brace, square + self._symbol = None # Choices: bracket, line, brace, square # determines if barlines are grouped through; this is group barline # in musicxml self._barTogether = True @@ -702,7 +703,7 @@ def getRichSystemLayout(inner_allSystemLayouts): staffObject.elements = p thisSystem.replace(p, staffObject) - allStaffLayouts = p.recurse().getElementsByClass('StaffLayout') + allStaffLayouts = p[StaffLayout] if not allStaffLayouts: continue # else: @@ -710,9 +711,9 @@ def getRichSystemLayout(inner_allSystemLayouts): # if len(allStaffLayouts) > 1: # print('Got many staffLayouts') - allSystemLayouts = thisSystem.recurse().getElementsByClass('SystemLayout') + allSystemLayouts = thisSystem[SystemLayout] if len(allSystemLayouts) >= 2: - thisSystem.systemLayout = getRichSystemLayout(allSystemLayouts) + thisSystem.systemLayout = getRichSystemLayout(list(allSystemLayouts)) elif len(allSystemLayouts) == 1: thisSystem.systemLayout = allSystemLayouts[0] else: @@ -1021,7 +1022,7 @@ def getPositionForStaff(self, pageId, systemId, staffId): This distance is specified with respect to the top of the system. Staff scaling ( in musicxml inside an object) is - taken into account, but not non 5-line staves. Thus a normally sized staff + taken into account, but not non-five-line staves. Thus a normally sized staff is always of height 40 (4 spaces of 10-tenths each) >>> lt = corpus.parse('demos/layoutTest.xml') @@ -1209,12 +1210,12 @@ def getStaffDistanceFromPrevious(self, pageId, systemId, staffId): positionForStaffCache[cacheKey] = staffDistanceFromPrevious return staffDistanceFromPrevious - def getStaffSizeFromLayout(self, pageId, systemId, staffId): + def getStaffSizeFromLayout(self, pageId: int, systemId: int, staffId: int) -> float: ''' Get the currently active staff-size for a given pageId, systemId, and staffId. - Note that this does not take into account the hidden state of the staff, which - if True makes the effective size 0.0 -- see getStaffHiddenAttribute + Note that this does not take into account the hidden state of the staff, which, + if True, makes the effective size 0.0 -- see getStaffHiddenAttribute >>> lt = corpus.parse('demos/layoutTest.xml') >>> ls = layout.divideByPages(lt, fastMeasures=True) @@ -1273,7 +1274,7 @@ def getStaffSizeFromLayout(self, pageId, systemId, staffId): staffSizeCache[cacheKey] = staffSize return staffSize - def getStaffHiddenAttribute(self, pageId, systemId, staffId): + def getStaffHiddenAttribute(self, pageId: int, systemId: int, staffId: int) -> bool: ''' returns the staffLayout.hidden attribute for a staffId, or if it is not defined, recursively search through previous staves until one is found. @@ -1318,7 +1319,11 @@ def getStaffHiddenAttribute(self, pageId, systemId, staffId): staffHiddenCache[cacheKey] = hiddenTag return hiddenTag - def getSystemBeforeThis(self, pageId, systemId): + def getSystemBeforeThis( + self, + pageId: int, + systemId: int + ) -> Tuple[Optional[int], Optional[int]]: # noinspection PyShadowingNames ''' given a pageId and systemId, get the (pageId, systemId) for the previous system. diff --git a/music21/midi/translate.py b/music21/midi/translate.py index 9eb09449e8..5f907b62fa 100644 --- a/music21/midi/translate.py +++ b/music21/midi/translate.py @@ -2323,7 +2323,7 @@ def channelInstrumentData( # Music tracks for subs in substreamList: # get a first instrument; iterate over rest - instrumentStream = subs.recurse().getElementsByClass('Instrument') + instrumentStream = subs[instrument.Instrument] setAnInstrument = False for inst in instrumentStream: if inst.midiChannel is not None and inst.midiProgram not in channelByInstrument: @@ -3587,6 +3587,7 @@ def testMidiTempoImportA(self): def testMidiTempoImportB(self): from music21 import converter + from music21 import tempo dirLib = common.getSourceFilePath() / 'midi' / 'testPrimitive' # a file with three tracks and one conductor track with four tempo marks @@ -3595,15 +3596,15 @@ def testMidiTempoImportB(self): self.assertEqual(len(s.parts), 3) # metronome marks propagate to every staff, but are hidden on subsequent staffs self.assertEqual( - [mm.numberImplicit for mm in s.parts[0].recurse().getElementsByClass('MetronomeMark')], + [mm.numberImplicit for mm in s.parts[0][tempo.MetronomeMark]], [False, False, False, False] ) self.assertEqual( - [mm.numberImplicit for mm in s.parts[1].recurse().getElementsByClass('MetronomeMark')], + [mm.numberImplicit for mm in s.parts[1][tempo.MetronomeMark]], [True, True, True, True] ) self.assertEqual( - [mm.numberImplicit for mm in s.parts[2].recurse().getElementsByClass('MetronomeMark')], + [mm.numberImplicit for mm in s.parts[2][tempo.MetronomeMark]], [True, True, True, True] ) diff --git a/music21/musicxml/test_m21ToXml.py b/music21/musicxml/test_m21ToXml.py index 233e5117fc..1a62b7be83 100644 --- a/music21/musicxml/test_m21ToXml.py +++ b/music21/musicxml/test_m21ToXml.py @@ -362,7 +362,7 @@ def testMultiDigitEndingsWrite(self): self.assertEqual([e.get('number') for e in endings], ['1,2', '1,2', '3', '3']) # m21 represents lack of bracket numbers as 0; musicxml uses '' - s.parts[0].getElementsByClass('RepeatBracket').first().number = 0 + s.parts[0].getElementsByClass(spanner.RepeatBracket).first().number = 0 x = self.getET(s) endings = x.findall('.//ending') self.assertEqual([e.get('number') for e in endings], ['', '', '3', '3']) diff --git a/music21/musicxml/test_xmlToM21.py b/music21/musicxml/test_xmlToM21.py index 37e6c0bc22..ac353b525e 100644 --- a/music21/musicxml/test_xmlToM21.py +++ b/music21/musicxml/test_xmlToM21.py @@ -277,7 +277,7 @@ def testImportRepeatBracketA(self): # has repeats in it; start with single measure s = corpus.parse('opus74no1', 3) # there are 2 for each part, totaling 8 - self.assertEqual(len(s.flatten().getElementsByClass('RepeatBracket')), 8) + self.assertEqual(len(s.flatten().getElementsByClass(spanner.RepeatBracket)), 8) # can get for each part as spanners are stored in Part now # TODO: need to test getting repeat brackets after measure extraction @@ -285,7 +285,7 @@ def testImportRepeatBracketA(self): sSub = s.parts[0].measures(72, 77) # 2 repeat brackets are gathered b/c they are stored at the Part by # default - rbSpanners = sSub.getElementsByClass('RepeatBracket') + rbSpanners = sSub.getElementsByClass(spanner.RepeatBracket) self.assertEqual(len(rbSpanners), 2) def testImportVoicesA(self): @@ -513,21 +513,21 @@ def testHarmonyA(self): from music21 import corpus s = corpus.parse('leadSheet/berlinAlexandersRagtime.xml') - self.assertEqual(len(s.flatten().getElementsByClass('ChordSymbol')), 19) + self.assertEqual(len(s[harmony.ChordSymbol]), 19) - match = [h.chordKind for h in s.recurse().getElementsByClass('ChordSymbol')] + match = [h.chordKind for h in s[harmony.ChordSymbol]] self.assertEqual(match, ['major', 'dominant-seventh', 'major', 'major', 'major', 'major', 'dominant-seventh', 'major', 'dominant-seventh', 'major', 'dominant-seventh', 'major', 'dominant-seventh', 'major', 'dominant-seventh', 'major', 'dominant-seventh', 'major', 'major']) - match = [str(h.root()) for h in s.recurse().getElementsByClass('ChordSymbol')] + match = [str(h.root()) for h in s[harmony.ChordSymbol]] self.assertEqual(match, ['F3', 'C3', 'F3', 'B-2', 'F3', 'C3', 'G2', 'C3', 'C3', 'F3', 'C3', 'F3', 'F2', 'B-2', 'F2', 'F3', 'C3', 'F3', 'C3']) - match = {str(h.figure) for h in s.recurse().getElementsByClass('ChordSymbol')} + match = {str(h.figure) for h in s[harmony.ChordSymbol]} self.assertEqual(match, {'F', 'F7', 'B-', 'C7', 'G7', 'C'}) @@ -644,8 +644,8 @@ def testImportWedgeA(self): from music21.musicxml import testPrimitive s = converter.parse(testPrimitive.spanners33a) - self.assertEqual(len(s.recurse().getElementsByClass('Crescendo')), 1) - self.assertEqual(len(s.recurse().getElementsByClass('Diminuendo')), 1) + self.assertEqual(len(s[dynamics.Crescendo]), 1) + self.assertEqual(len(s[dynamics.Diminuendo]), 1) def testImportWedgeB(self): from music21 import converter @@ -653,7 +653,7 @@ def testImportWedgeB(self): # this produces a single component cresc s = converter.parse(testPrimitive.directions31a) - self.assertEqual(len(s.recurse().getElementsByClass('Crescendo')), 2) + self.assertEqual(len(s[dynamics.Crescendo]), 2) def testBracketImportB(self): from music21 import converter @@ -661,21 +661,21 @@ def testBracketImportB(self): s = converter.parse(testPrimitive.spanners33a) # s.show() - self.assertEqual(len(s.recurse().getElementsByClass('Line')), 6) + self.assertEqual(len(s[spanner.Line]), 6) def testTrillExtensionImportA(self): from music21 import converter from music21.musicxml import testPrimitive s = converter.parse(testPrimitive.notations32a) # s.show() - self.assertEqual(len(s.recurse().getElementsByClass('TrillExtension')), 2) + self.assertEqual(len(s[expressions.TrillExtension]), 2) def testGlissandoImportA(self): from music21 import converter from music21.musicxml import testPrimitive s = converter.parse(testPrimitive.spanners33a) # s.show() - glisses = list(s.recurse().getElementsByClass('Glissando')) + glisses = list(s[spanner.Glissando]) self.assertEqual(len(glisses), 2) self.assertEqual(glisses[0].slideType, 'chromatic') self.assertEqual(glisses[1].slideType, 'continuous') @@ -686,7 +686,7 @@ def testImportDashes(self): from music21.musicxml import testPrimitive s = converter.parse(testPrimitive.spanners33a, format='musicxml') - self.assertEqual(len(s.recurse().getElementsByClass('Line')), 6) + self.assertEqual(len(s[spanner.Line]), 6) def testImportGraceA(self): from music21 import converter @@ -1017,7 +1017,7 @@ def testRehearsalMarks(self): from music21.musicxml import testPrimitive s = converter.parse(testPrimitive.directions31a) - rmIterator = s.recurse().getElementsByClass('RehearsalMark') + rmIterator = s[expressions.RehearsalMark] self.assertEqual(len(rmIterator), 4) self.assertEqual(rmIterator[0].content, 'A') self.assertEqual(rmIterator[1].content, 'B') @@ -1032,17 +1032,17 @@ def testNoChordImport(self): testFp = thisDir / 'testNC.xml' s = converter.parse(testFp) - self.assertEqual(5, len(s.recurse().getElementsByClass('ChordSymbol'))) - self.assertEqual(2, len(s.recurse().getElementsByClass('NoChord'))) + self.assertEqual(5, len(s[harmony.ChordSymbol])) + self.assertEqual(2, len(s[harmony.NoChord])) self.assertEqual('augmented-seventh', - s.recurse().getElementsByClass('ChordSymbol')[0].chordKind) + s[harmony.ChordSymbol][0].chordKind) self.assertEqual('none', - s.recurse().getElementsByClass('ChordSymbol')[1].chordKind) + s[harmony.ChordSymbol][1].chordKind) - self.assertEqual('random', str(s.recurse().getElementsByClass('NoChord')[ + self.assertEqual('random', str(s[harmony.NoChord][ 0].chordKindStr)) - self.assertEqual('N.C.', str(s.recurse().getElementsByClass('NoChord')[ + self.assertEqual('N.C.', str(s[harmony.NoChord][ 1].chordKindStr)) def testChordAlias(self): @@ -1067,7 +1067,7 @@ def testChordOffset(self): s = converter.parse(testFp) offsets = [0.0, 2.0, 0.0, 2.0, 0.0, 2.0] - for ch, offset in zip(s.recurse().getElementsByClass('ChordSymbol'), + for ch, offset in zip(s[harmony.ChordSymbol], offsets): self.assertEqual(ch.offset, offset) @@ -1194,13 +1194,13 @@ def testMultiDigitEnding(self): # Measure 3, left barline: # Measure 3, right barline: score = converter.parse(testPrimitive.multiDigitEnding) - repeatBrackets = score.recurse().getElementsByClass('RepeatBracket') + repeatBrackets = score.recurse().getElementsByClass(spanner.RepeatBracket) self.assertListEqual(repeatBrackets[0].getNumberList(), [1, 2]) self.assertListEqual(repeatBrackets[1].getNumberList(), [3]) nonconformingInput = testPrimitive.multiDigitEnding.replace("1,2", "ad lib.") score2 = converter.parse(nonconformingInput) - repeatBracket = score2.recurse().getElementsByClass('RepeatBracket').first() + repeatBracket = score2.recurse().getElementsByClass(spanner.RepeatBracket).first() self.assertListEqual(repeatBracket.getNumberList(), [1]) def testChordAlteration(self): @@ -1269,12 +1269,12 @@ def testDirectionPosition(self): # Dynamic s = converter.parse(testFiles.mozartTrioK581Excerpt) - dyn = s.recurse().getElementsByClass('Dynamic').first() + dyn = s[dynamics.Dynamic].first() self.assertEqual(dyn.style.relativeY, 6) # Coda/Segno s = converter.parse(testPrimitive.repeatExpressionsA) - seg = s.recurse().getElementsByClass('Segno').first() + seg = s[repeat.Segno].first() self.assertEqual(seg.style.relativeX, 10) # TextExpression @@ -1301,7 +1301,7 @@ def testDirectionPosition(self): # Metronome s = converter.parse(testFiles.tabTest) - metro = s.recurse().getElementsByClass('MetronomeMark').first() + metro = s[tempo.MetronomeMark].first() self.assertEqual(metro.style.absoluteY, 40) self.assertEqual(metro.placement, 'above') diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index d3355ba919..e19f6a824e 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -1928,7 +1928,7 @@ def xmlMeasureToMeasure(self, mxMeasure: ET.Element) -> stream.Measure: # because it should happen on a voice level. if measureParser.fullMeasureRest is True: # recurse is necessary because it could be in voices... - r1 = m.recurse().getElementsByClass('Rest').first() + r1 = m[note.Rest].first() lastTSQl = self.lastTimeSignature.barDuration.quarterLength if (r1.fullMeasure is True # set by xml measure='yes' or (r1.duration.quarterLength != lastTSQl diff --git a/music21/omr/correctors.py b/music21/omr/correctors.py index a8a79644a1..3f840d9ea0 100644 --- a/music21/omr/correctors.py +++ b/music21/omr/correctors.py @@ -314,7 +314,7 @@ def substituteOneMeasureContentsForAnother( self.singleParts[destinationVerticalIndex].measureStream[destinationHorizontalIndex]) # Measure object correctMeasure = self.singleParts[sourceVerticalIndex].measureStream[sourceHorizontalIndex] - oldNotePitches = [n.pitch for n in incorrectMeasure.getElementsByClass('Note')] + oldNotePitches = [n.pitch for n in incorrectMeasure.getElementsByClass(note.Note)] for el in incorrectMeasure.elements: incorrectMeasure.remove(el) diff --git a/music21/repeat.py b/music21/repeat.py index ab7a0a38dd..e196822104 100644 --- a/music21/repeat.py +++ b/music21/repeat.py @@ -23,6 +23,7 @@ from music21 import exceptions21 from music21 import expressions +from music21 import prebase from music21 import spanner from music21 import style @@ -32,7 +33,7 @@ # ------------------------------------------------------------------------------ -class RepeatMark: +class RepeatMark(prebase.ProtoM21Object): ''' Base class of all repeat objects, including RepeatExpression objects and Repeat (Barline) objects. @@ -53,7 +54,7 @@ class RepeatMark: >>> s = stream.Stream() >>> s.append(note.Note()) >>> s.append(PartialRepeat()) - >>> repeats = s.getElementsByClass(repeat.RepeatMark) + >>> repeats = s.getElementsByClass('RepeatMark') # not a Music21Object, so use quotes >>> if repeats: ... print('Stream has %s repeat(s) in it' % (len(repeats))) Stream has 1 repeat(s) in it @@ -124,7 +125,7 @@ def setText(self, value): def applyTextFormatting(self, te=None): ''' - Apply the default text formatting to the text expression version of of this repeat + Apply the default text formatting to the text expression version of this repeat ''' if te is None: # use the stored version if possible te = self._textExpression @@ -406,7 +407,7 @@ def insertRepeatEnding(s, start, end, endingNumber=1, *, inPlace=False): the method adds a second ending from measures 6 to 7. - Does not (yet) add a :class:`~music21.bar.RepeatMark` to the end of the first ending. + Does not (yet) add a :class:`~music21.repeat.RepeatMark` to the end of the first ending. Example: create first and second endings over measures 4-6 and measures 11-13 of a chorale, respectively. @@ -764,7 +765,7 @@ def process(self, deepcopy=True): happens for Measures contained in the given Stream. Other objects in that Stream are neither processed nor copied. - if deepcopy is False then it will leave the stream in a unusual state, but acceptable if + if deepcopy is False then it will leave the stream in an unusual state, but acceptable if the source stream has already been deep-copied and will be discarded later ''' canExpand = self.isExpandable() @@ -783,20 +784,20 @@ def process(self, deepcopy=True): if canExpand is None: return srcStream - # these must by copied, otherwise we have the original still + # these must be copied, otherwise we have the original still self._repeatBrackets = copy.deepcopy(self._repeatBrackets) # srcStream = self._srcMeasureStream # after copying, update repeat brackets (as spanners) for m in srcStream: - # processes uses the spanner bundle stored on this Stream + # uses the spanner bundle stored on this Stream self._repeatBrackets.spannerBundle.replaceSpannedElement( m.derivation.origin, m) # srcStream = self._srcMeasureStream # post = copy.deepcopy(self._srcMeasureStream) hasDaCapoOrSegno = self._daCapoOrSegno() - # will deep copy + # will deepcopy if hasDaCapoOrSegno is None: post = self._processRecursiveRepeatBars(srcStream) else: # we have a segno or capo @@ -911,7 +912,7 @@ def repeatBarsAreCoherent(self): if lb.direction == 'start': startCount += 1 countBalance += 1 - # ends may be encountered on left of bar + # ends of repeats may be encountered on left of bar elif lb.direction == 'end': if countBalance == 0: # the first repeat found startCount += 1 # simulate first @@ -920,7 +921,7 @@ def repeatBarsAreCoherent(self): countBalance -= 1 if rb is not None and 'music21.bar.Repeat' in rb.classSet: if rb.direction == 'end': - # if this is the first of all repeats found, then we + # if this is the first repeat found, then we # have an acceptable case where the first repeat is omitted if countBalance == 0: # the first repeat found startCount += 1 # simulate first @@ -966,7 +967,7 @@ def _daCapoOrSegno(self): def _getRepeatExpressionCommandType(self): ''' Return the class of the repeat expression command. This should - only be called if it has been determine that there is one + only be called if it has been determined that there is one repeat command in this Stream. ''' if self._dcCount == 1: @@ -1133,8 +1134,7 @@ def _groupRepeatBracketIndices(self, streamObj): # shiftedIndex = True # match = True break -# if match: -# break + # if not shiftedIndex: i += 1 if groupIndices: @@ -1175,7 +1175,7 @@ def _repeatBracketsAreCoherent(self): environLocal.printDebug([ f'repeat brackets are not numbered consecutively: {match}, {target}']) return False - # there needs to be repeat after each bracket except the last + # there needs to be a repeat mark after each bracket except the last spannedMeasureIds = [] for rbCount, rb in enumerate(rBrackets): # environLocal.printDebug(['rbCount', rbCount, rb]) @@ -1337,7 +1337,7 @@ def processInnermostRepeatBars(self, Process and return a new Stream of Measures, likely a Part. If `repeatIndices` are given, only these indices will be copied. - All inclusive indices must be listed, not just the start and end. + All the included indices must be listed, not just the start and end. If `returnExpansionOnly` is True, only the expanded portion is returned, the rest of the Stream is not retained. @@ -1495,8 +1495,8 @@ def processInnermostRepeatBars(self, # always copying from the same source for j in repeatIndices: mSub = copy.deepcopy(streamObj[j]) - # must do for each pass, b/c not changing source - # stream + # We must do this for each pass, b/c we are not changing + # the source stream # environLocal.printDebug(['got j, repeatIndices', j, repeatIndices]) if j in [repeatIndices[0], repeatIndices[-1]]: self._stripRepeatBarlines(mSub) @@ -1512,7 +1512,7 @@ def processInnermostRepeatBars(self, new.append(mSub) # renumber at end # number += 1 - # check if need to clear repeats from next bar + # check if we need to clear repeats from next bar if mLast is not mEndBarline: stripFirstNextMeasure = True # set i to next measure after mLast @@ -1566,13 +1566,13 @@ def _processInnermostRepeatsAndBrackets(self, # need to remove groups that are already used groups = self._groupRepeatBracketIndices(streamObj) - # if we do not groups when expected it is probably b/c spanners have - # been orphaned + # if we do not group when we are expected to do so, it is probably b/c spanners have + # been orphaned. # environLocal.printDebug(['got groups:', groups]) if not groups: # none found: return self.processInnermostRepeatBars(streamObj) - # need to find innermost repeat, and then see it it has any + # need to find innermost repeat, and then see if it has any # repeat brackets that are relevant for the span # this group may ultimately extend beyond the innermost, as this may # be the first of a few brackets @@ -1638,7 +1638,7 @@ def _processInnermostRepeatsAndBrackets(self, raise ExpanderException( 'failed to find start or end index of bracket expansion') - # if mLast does not have a repeat bar, its probably not a repeat + # if mLast does not have a repeat bar, it is probably not a repeat mLastRightBar = mLast.rightBarline if (mLastRightBar is not None and 'music21.bar.Repeat' in mLastRightBar.classSet): @@ -1694,7 +1694,7 @@ def _processInnermostRepeatsAndBrackets(self, for m in sub: self._stripRepeatBarlines(m) new.append(m) - # need 1 more than highest measure counted + # need 1 more than the highest measure counted streamObjPost = streamObj[highestIndexRepeated + 1:] for m in streamObjPost: new.append(m) @@ -1740,7 +1740,7 @@ def isExpandable(self) -> Union[bool, None]: Return None if there's nothing to expand (a third case...) ''' match = self._daCapoOrSegno() - # if neither repeats nor segno/capo, than not expandable + # if neither repeats nor segno/capo, then not expandable if match is None and not self._hasRepeat(self._srcMeasureStream): environLocal.printDebug( 'no dc/segno, no repeats; is expandable but will not do anything' @@ -1774,8 +1774,8 @@ def _processRecursiveRepeatBars(self, streamObj, makeDeepCopy=False): if makeDeepCopy is True, then it will make a deepcopy of the stream. Otherwise assumes it has already been done. ''' - # this assumes just a stream of measures - # assume already copied + # This assumes just a stream of measures. + # Also assumes the stream has already been copied unless makeDeepCopy is True. if makeDeepCopy is True: streamObj = copy.deepcopy(streamObj) repeatBracketsMemo = {} # store completed brackets @@ -1805,8 +1805,8 @@ def _processRepeatExpressionAndRepeats(self, streamObj): Process and return a new Stream of Measures, likely a Part. Expand any repeat expressions found within. ''' - # should already be a stream of measures - # assume already copied + # Should already be a stream of measures. + # Assumes that the stream has already been copied. capoOrSegno = self._daCapoOrSegno() recType = self._getRepeatExpressionCommandType() # a string form recObj = self._getRepeatExpressionCommand(streamObj) @@ -2163,7 +2163,7 @@ def getMeasureSimilarityList(self): res.append([]) for i in range(len(mLists) - 1, -1, -1): - # mHash is a the concatenation of the measure i for each part. + # mHash is the concatenation of the measure i for each part. mHash = ''.join(mLists[i]) if mHash in tempDict: @@ -2627,7 +2627,7 @@ def testFilterByRepeatMark(self): # s.show() # now have 4 - self.assertEqual(len(s.recurse().getElementsByClass('RepeatMark')), 4) + self.assertEqual(len(s['RepeatMark']), 4) # check coherence ex = repeat.Expander(s) @@ -2711,7 +2711,7 @@ def testRepeatCoherenceB2(self): 28.0, 32.0, 36.0, 40.0, 44.0]) self.assertEqual([n.nameWithOctave - for n in post.flatten().getElementsByClass('Note')], + for n in post.flatten().getElementsByClass(note.Note)], ['G3', 'G3', 'G3', 'G3', 'B3', 'B3', 'B3', 'B3', 'D4', 'D4', 'D4', 'D4', 'B3', 'B3', 'B3', 'B3', 'D4', 'D4', 'D4', 'D4', @@ -2797,7 +2797,7 @@ def testRepeatCoherenceC(self): ex = repeat.Expander(s) self.assertTrue(ex.isExpandable()) - # ds al fine missing fine + # "ds al fine" missing "fine" s = stream.Part() m1 = stream.Measure() m1.append(Segno()) @@ -2875,7 +2875,7 @@ def testExpandRepeatA(self): [0.0, 4.0, 8.0, 12.0]) self.assertEqual([n.nameWithOctave - for n in post.flatten().getElementsByClass('Note')], + for n in post.flatten().getElementsByClass(note.Note)], ['G3', 'G3', 'G3', 'G3', 'G3', 'G3', 'G3', 'G3', 'D4', 'D4', 'D4', 'D4', 'D4', 'D4', 'D4', 'D4']) measureNumbersPost = [m.measureNumberWithSuffix() @@ -2910,7 +2910,7 @@ def testExpandRepeatA(self): self.assertEqual([m.offset for m in post.getElementsByClass(stream.Measure)], [0.0, 4.0, 8.0, 12.0, 16.0]) self.assertEqual([n.nameWithOctave - for n in post.flatten().getElementsByClass('Note')], + for n in post.flatten().getElementsByClass(note.Note)], ['G3', 'G3', 'G3', 'G3', 'G3', 'G3', 'G3', 'G3', 'F3', 'F3', 'F3', 'F3', 'D4', 'D4', 'D4', 'D4', 'D4', 'D4', 'D4', 'D4']) @@ -4039,7 +4039,7 @@ def testRepeatEndingsE(self): m3.rightBarline = bar.Repeat() p.append(rb1) rb2 = spanner.RepeatBracket(m4, number=3) - # second ending may not have repeat + # The second ending might not have a repeat sign. p.append(rb2) # p.show() @@ -4094,7 +4094,7 @@ def testRepeatEndingsF(self): p.append(rb4) rb5 = spanner.RepeatBracket(m8, number=5) p.append(rb5) - # second ending may not have repeat + # The second ending might not have a repeat sign. # p.show() ex = Expander(p) @@ -4242,7 +4242,7 @@ def testRepeatEndingsI(self): p.append(rb4) rb5 = spanner.RepeatBracket(m8, number=5) p.append(rb5) - # second ending may not have repeat + # The second ending might not have a repeat sign. # p.show() # p.show() @@ -4380,7 +4380,7 @@ def testRepeatEndingsImportedC(self): # post = s.expandRepeats() def test_expand_repeats_preserves_name(self): - # Test case provided by jacobtylerwalls on issue #1165 + # Test case provided by Jacob Tyler Walls on issue #1165 # https://github.com/cuthbertLab/music21/issues/1165#issuecomment-967293691 from music21 import converter @@ -4388,11 +4388,11 @@ def test_expand_repeats_preserves_name(self): p = converter.parse('tinyNotation: c1 d e f') p.measure(2).storeAtEnd(bar.Repeat(direction='end', times=1)) - p.partName = 'mypartname' - p.partAbbreviation = 'mypartabbreviation' + p.partName = 'my_part_name' + p.partAbbreviation = 'my_part_abbreviation' exp = p.expandRepeats() - self.assertEqual(exp.partName, 'mypartname') - self.assertEqual(exp.partAbbreviation, 'mypartabbreviation') + self.assertEqual(exp.partName, 'my_part_name') + self.assertEqual(exp.partAbbreviation, 'my_part_abbreviation') # ------------------------------------------------------------------------------ diff --git a/music21/romanText/writeRoman.py b/music21/romanText/writeRoman.py index 28c83f6fc8..5658e1b0a5 100644 --- a/music21/romanText/writeRoman.py +++ b/music21/romanText/writeRoman.py @@ -167,7 +167,7 @@ def __init__(self, ''] # One blank line between metadata and analysis # Note: blank analyst and proofreader entries until supported within music21 metadata - if not self.container.recurse().getElementsByClass('TimeSignature'): + if not self.container[meter.TimeSignature]: self.container.insert(0, meter.TimeSignature('4/4')) # Placeholder self.currentKeyString: str = '' diff --git a/music21/search/lyrics.py b/music21/search/lyrics.py index 46a8b01f4f..d3f0adf649 100644 --- a/music21/search/lyrics.py +++ b/music21/search/lyrics.py @@ -187,7 +187,7 @@ def index(self, s=None) -> List[IndexedLyric]: iTextByIdentifier = OrderedDict() lastSyllabicByIdentifier = OrderedDict() - for n in s.recurse().getElementsByClass('NotRest'): + for n in s.recurse().notes: ls: List[note.Lyric] = n.lyrics if not ls: continue diff --git a/music21/search/serial.py b/music21/search/serial.py index 4223fa6f08..688ff0c8d9 100644 --- a/music21/search/serial.py +++ b/music21/search/serial.py @@ -596,7 +596,7 @@ def byLength(self, length): self.searchLength = length self.listOfContiguousSegments = [] hasParts = True - partList = self.stream.recurse().getElementsByClass('Part') + partList = self.stream[stream.Part] if not partList: partList = [self.stream] hasParts = False diff --git a/music21/serial.py b/music21/serial.py index 65577c3aaa..ad4c81662f 100644 --- a/music21/serial.py +++ b/music21/serial.py @@ -710,7 +710,7 @@ def matrix(self): ''' # note: do not want to return a TwelveToneRow() type, as this will # add again the same pitches to the elements list twice. - noteList = self.getElementsByClass('Note') + noteList = self.getElementsByClass(note.Note) i = [(12 - x.pitch.pitchClass) % 12 for x in noteList] matrix = [[(x.pitch.pitchClass + t) % 12 for x in noteList] for t in i] diff --git a/music21/spanner.py b/music21/spanner.py index cb8e205f74..7016742fd9 100644 --- a/music21/spanner.py +++ b/music21/spanner.py @@ -100,7 +100,7 @@ class Spanner(base.Music21Object): - (2) we can get a stream of spanners (equiv. to getElementsByClass('Spanner')) + (2) we can get a stream of spanners (equiv. to getElementsByClass(spanner.Spanner)) by calling the .spanner property on the stream. >>> spannerCollection = s.spanners # a stream object diff --git a/music21/stream/base.py b/music21/stream/base.py index ec78b61e62..25d8f6d858 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -1157,10 +1157,11 @@ def staffLines(self) -> int: >>> m.staffLines = 2 >>> staffLayout.staffLines 2 - ''' - staffLayouts = self.recurse().getElementsByClass('StaffLayout') - sl: 'music21.layout.StaffLayout' + from music21 import layout + + staffLayouts = self[layout.StaffLayout] + sl: layout.StaffLayout for sl in staffLayouts: if sl.getOffsetInHierarchy(self) > 0: break @@ -5964,7 +5965,7 @@ def chordifyOneMeasure(templateInner, streamToChordify): def consolidateRests(templateInner): consecutiveRests = [] - for el in list(templateInner.getElementsByClass('GeneralNote')): + for el in list(templateInner.notesAndRests): if not isinstance(el, note.Rest): removeConsecutiveRests(templateInner, consecutiveRests) consecutiveRests = [] @@ -9444,7 +9445,7 @@ def allSubstreamsHaveMeasures(testStream): if 'Measure' in self.classes or 'Voice' in self.classes: return True # all other Stream classes are not well-formed if they have "loose" notes - elif self.getElementsByClass('GeneralNote'): + elif self.notesAndRests: return False elif 'Part' in self.classes: if self.hasMeasures(): @@ -12751,7 +12752,7 @@ def padAsAnacrusis(self, useGaps=True, useInitialRests=False): ''' if useInitialRests: removeList = [] - for gn in self.getElementsByClass('GeneralNote'): + for gn in self.notesAndRests: if not isinstance(gn, note.Rest): break removeList.append(gn) @@ -13039,7 +13040,7 @@ def _getPartName(self): return self._cache['_partName'] else: pn = None - for e in self.recurse().getElementsByClass('Instrument'): + for e in self[instrument.Instrument]: pn = e.partName if pn is None: pn = e.instrumentName @@ -13093,7 +13094,7 @@ def _getPartAbbreviation(self): return self._cache['_partAbbreviation'] else: pn = None - for e in self.recurse().getElementsByClass('Instrument'): + for e in self[instrument.Instrument]: pn = e.partAbbreviation if pn is None: pn = e.instrumentAbbreviation @@ -13388,7 +13389,7 @@ def measure(self, {0.0} {4.0} - >>> lastChord = excerptChords.recurse().getElementsByClass('Chord').last() + >>> lastChord = excerptChords[chord.Chord].last() >>> lastChord diff --git a/music21/stream/iterator.py b/music21/stream/iterator.py index 0684f0aa1a..224a694dff 100644 --- a/music21/stream/iterator.py +++ b/music21/stream/iterator.py @@ -514,12 +514,12 @@ def first(self) -> Optional[M21ObjType]: >>> s = converter.parse('tinyNotation: 3/4 D4 E2 F4 r2 G2 r4') >>> s.recurse().notes.first() - >>> s.recurse().getElementsByClass('Rest').first() + >>> s[note.Rest].first() If no elements match, returns None: - >>> print(s.recurse().getElementsByClass('Chord').first()) + >>> print(s[chord.Chord].first()) None New in v7. @@ -565,7 +565,7 @@ def last(self) -> Optional[M21ObjType]: >>> s = converter.parse('tinyNotation: 3/4 D4 E2 F4 r2 G2 r4') >>> s.recurse().notes.last() - >>> s.recurse().getElementsByClass('Rest').last() + >>> s[note.Rest].last() New in v7. @@ -790,7 +790,7 @@ def stream(self, returnStreamSubClass=True) -> Union['music21.stream.Stream', St >>> b = bar.Barline() >>> s.storeAtEnd(b) - >>> s2 = s.iter().getElementsByClass('Note').stream() + >>> s2 = s.iter().getElementsByClass(note.Note).stream() >>> s2.show('t') {0.0} {2.0} @@ -2000,7 +2000,7 @@ def testAddingFiltersMidIteration(self): self.assertIs(r0, r) # adding a filter gives a new StreamIterator that restarts at 0 - sIter2 = sIter.getElementsByClass('GeneralNote') # this filter does nothing here. + sIter2 = sIter.notesAndRests # this filter does nothing here. obj0 = next(sIter2) self.assertIs(obj0, r) diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py index 3260abfbad..71208c7383 100644 --- a/music21/stream/makeNotation.py +++ b/music21/stream/makeNotation.py @@ -1762,7 +1762,7 @@ def iterateBeamGroups( iterator: 'music21.stream.iterator.StreamIterator' = s.recurse() if recurse else s.iter() current_beam_group: List[note.NotRest] = [] in_beam_group: bool = False - for el in iterator.getElementsByClass('NotRest'): + for el in iterator.notes: first_el_type: Optional[str] = None if el.beams and el.beams.getByNumber(1): first_el_type = el.beams.getTypeByNumber(1) diff --git a/music21/stream/tests.py b/music21/stream/tests.py index 7fd9218296..490772027d 100644 --- a/music21/stream/tests.py +++ b/music21/stream/tests.py @@ -710,7 +710,7 @@ def testExtractedNoteAssignLyric(self): a = converter.parse(corpus.getWork('corelli/opus3no1/1grave')) b = a.parts[1] c = b.flatten() - for thisNote in c.getElementsByClass('Note'): + for thisNote in c.getElementsByClass(note.Note): thisNote.lyric = thisNote.name textStr = text.assembleLyrics(b) self.assertEqual(textStr.startswith('A A G F E'), @@ -6169,7 +6169,7 @@ def testDerivationA(self): # for testing against s2 = Stream() - s3 = s1.getElementsByClass('GeneralNote').stream() + s3 = s1.notesAndRests.stream() self.assertEqual(len(s3), 20) # environLocal.printDebug(['s3.derivation.origin', s3.derivation.origin]) self.assertIs(s3.derivation.origin, s1) @@ -6260,7 +6260,7 @@ def testDerivationMethodA(self): self.assertIs(s1Flat.derivation.origin, s1) self.assertEqual(s1Flat.derivation.method, 'flat') - s1Elements = s1Flat.getElementsByClass('Note').stream() + s1Elements = s1Flat.getElementsByClass(note.Note).stream() self.assertEqual(s1Elements.derivation.method, 'getElementsByClass') s1 = converter.parse('tinyNotation: 4/4 C2 D2') @@ -7365,7 +7365,7 @@ def testExtendTiesA(self): s.append(chord.Chord(['c4', 'a5'])) s.extendTies() post = [] - for n in s.flatten().getElementsByClass('GeneralNote'): + for n in s.flatten().notesAndRests: if isinstance(n, chord.Chord): post.append([repr(q.tie) for q in n]) else: diff --git a/music21/tinyNotation.py b/music21/tinyNotation.py index 001b6b915b..d81a30fe46 100644 --- a/music21/tinyNotation.py +++ b/music21/tinyNotation.py @@ -123,7 +123,7 @@ >>> tnc = tinyNotation.Converter('3/4 C4*pink* D4*green* E4*blue*') >>> tnc.modifierStar = ColorModifier >>> s = tnc.parse().stream ->>> for n in s.recurse().getElementsByClass('Note'): +>>> for n in s.recurse().getElementsByClass(note.Note): ... print(n.step, n.style.color) C pink D green @@ -147,7 +147,7 @@ {2.0} {3.0} {4.0} ->>> for cs in s.recurse().getElementsByClass('ChordSymbol'): +>>> for cs in s.recurse().getElementsByClass(harmony.ChordSymbol): ... print([p.name for p in cs.pitches]) ['C', 'E', 'G', 'B'] ['D', 'F', 'A'] diff --git a/music21/variant.py b/music21/variant.py index 55f927d97f..e783ed7ca0 100644 --- a/music21/variant.py +++ b/music21/variant.py @@ -40,2473 +40,2476 @@ environLocal = environment.Environment(_MOD) -# ------Public Merge Functions -def mergeVariants(streamX, streamY, variantName='variant', *, inPlace=False): - # noinspection PyShadowingNames - ''' - Takes two streams objects or their derivatives (Score, Part, Measure, etc.) which - should be variant versions of the same stream, - and merges them (determines differences and stores those differences as variant objects - in streamX) via the appropriate merge - function for their type. This will not know how to deal with scores meant for - mergePartAsOssia(). If this is the intention, use - that function instead. - - >>> streamX = converter.parse('tinynotation: 4/4 a4 b c d', makeNotation=False) - >>> streamY = converter.parse('tinynotation: 4/4 a4 b- c e', makeNotation=False) +# ------------------------------------------------------------------------------ +# classes - >>> mergedStream = variant.mergeVariants(streamX, streamY, - ... variantName='docVariant', inPlace=False) - >>> mergedStream.show('text') - {0.0} - {0.0} - {1.0} - {1.0} - {2.0} - {3.0} - {3.0} - >>> v0 = mergedStream.getElementsByClass('Variant').first() - >>> v0 - - >>> v0.first() - +class VariantException(exceptions21.Music21Exception): + pass - >>> streamZ = converter.parse('tinynotation: 4/4 a4 b c d e f g a', makeNotation=False) - >>> variant.mergeVariants(streamX, streamZ, variantName='docVariant', inPlace=False) - Traceback (most recent call last): - music21.variant.VariantException: Could not determine what merging method to use. - Try using a more specific merging function. +class Variant(base.Music21Object): + ''' + A Music21Object that stores elements like a Stream, but does not + represent itself externally to a Stream; i.e., the contents of a Variant are not flattened. - Example: Create a main score (aScore) and a variant score (vScore), each with - two parts (ap1/vp1 - and ap2/vp2) and some small variants between ap1/vp1 and ap2/vp2, marked with * below. + This is accomplished not by subclassing, but by object composition: similar to the Spanner, + the Variant contains a Stream as a private attribute. Calls to this Stream, for the Variant, + are automatically delegated by use of the __getattr__ method. Special cases are overridden + or managed as necessary: e.g., the Duration of a Variant is generally always zero. - >>> aScore = stream.Score() - >>> vScore = stream.Score() + To use Variants from a Stream, see the :func:`~music21.stream.Stream.activateVariants` method. - >>> # * - >>> ap1 = converter.parse('tinynotation: 4/4 a4 b c d e2 f g2 f4 g ') - >>> vp1 = converter.parse('tinynotation: 4/4 a4 b c e e2 f g2 f4 a ') - >>> # * * * - >>> ap2 = converter.parse('tinynotation: 4/4 a4 g f e f2 e d2 g4 f ') - >>> vp2 = converter.parse('tinynotation: 4/4 a4 g f e f2 g f2 g4 d ') + >>> v = variant.Variant() + >>> v.repeatAppend(note.Note(), 8) + >>> len(v.notes) + 8 + >>> v.highestTime + 0.0 + >>> v.containedHighestTime + 8.0 - >>> ap1.id = 'aPart1' - >>> ap2.id = 'aPart2' + >>> v.duration # handled by Music21Object + + >>> v.isStream + False - >>> aScore.insert(0.0, ap1) - >>> aScore.insert(0.0, ap2) - >>> vScore.insert(0.0, vp1) - >>> vScore.insert(0.0, vp2) + >>> s = stream.Stream() + >>> s.append(v) + >>> s.append(note.Note()) + >>> s.highestTime + 1.0 + >>> s.show('t') + {0.0} + {0.0} + >>> s.flatten().show('t') + {0.0} + {0.0} + ''' - Create one merged score where everything different in vScore from aScore is called a variant. + classSortOrder = stream.Stream.classSortOrder - 2 # variants should always come first? - >>> mergedScore = variant.mergeVariants(aScore, vScore, variantName='docVariant', inPlace=False) - >>> mergedScore.show('text') - {0.0} - {0.0} - {0.0} - {0.0} - {0.0} - {0.0} - {1.0} - {2.0} - {3.0} - {4.0} - {0.0} - {2.0} - {8.0} - {8.0} - {0.0} - {2.0} - {3.0} - {4.0} - {0.0} - {0.0} - {0.0} - {0.0} - {0.0} - {1.0} - {2.0} - {3.0} - {4.0} - {4.0} - {0.0} - {2.0} - {8.0} - {0.0} - {2.0} - {3.0} - {4.0} + # this copies the init of Streams + def __init__(self, givenElements=None, *args, **keywords): + super().__init__() + self.exposeTime = False + self._stream = stream.VariantStorage(givenElements=givenElements, + *args, **keywords) + self._replacementDuration = None - >>> mergedPart = variant.mergeVariants(ap2, vp2, variantName='docVariant', inPlace=False) - >>> mergedPart.show('text') - {0.0} - ... - {4.0} - {4.0} - ... - {4.0} - ''' - classesX = streamX.classes - if 'Score' in classesX: - return mergeVariantScores(streamX, streamY, variantName, inPlace=inPlace) - elif streamX.getElementsByClass(stream.Measure): - return mergeVariantMeasureStreams(streamX, streamY, variantName, inPlace=inPlace) - elif (streamX.iter().notesAndRests - and streamX.duration.quarterLength == streamY.duration.quarterLength): - return mergeVariantsEqualDuration([streamX, streamY], [variantName], inPlace=inPlace) - else: - raise VariantException( - 'Could not determine what merging method to use. ' - + 'Try using a more specific merging function.') + if 'name' in keywords: + self.groups.append(keywords['name']) -def mergeVariantScores(aScore, vScore, variantName='variant', *, inPlace=False): - # noinspection PyShadowingNames - ''' - Takes two scores and merges them with mergeVariantMeasureStreams, part-by-part. + def _deepcopySubclassable(self, memo=None, ignoreAttributes=None, removeFromIgnore=None): + ''' + see __deepcopy__ on Spanner for tests and docs + ''' + # NOTE: this is a performance critical operation + defaultIgnoreSet = {'_cache'} + if ignoreAttributes is None: + ignoreAttributes = defaultIgnoreSet + else: + ignoreAttributes = ignoreAttributes | defaultIgnoreSet - >>> aScore, vScore = stream.Score(), stream.Score() + new = super()._deepcopySubclassable(memo, ignoreAttributes, removeFromIgnore) - >>> ap1 = converter.parse('tinynotation: 4/4 a4 b c d e2 f2 g2 f4 g4 ') - >>> vp1 = converter.parse('tinynotation: 4/4 a4 b c e e2 f2 g2 f4 a4 ') + return new - >>> ap2 = converter.parse('tinynotation: 4/4 a4 g f e f2 e2 d2 g4 f4 ') - >>> vp2 = converter.parse('tinynotation: 4/4 a4 g f e f2 g2 f2 g4 d4 ') + def __deepcopy__(self, memo=None): + return self._deepcopySubclassable(memo) - >>> aScore.insert(0.0, ap1) - >>> aScore.insert(0.0, ap2) - >>> vScore.insert(0.0, vp1) - >>> vScore.insert(0.0, vp2) + # -------------------------------------------------------------------------- + # as _stream is a private Stream, unwrap/wrap methods need to override + # Music21Object to get at these objects + # this is the same as with Spanners - >>> mergedScores = variant.mergeVariantScores(aScore, vScore, - ... variantName='docVariant', inPlace=False) - >>> mergedScores.show('text') - {0.0} - {0.0} - {0.0} - {0.0} - {0.0} - {0.0} - {1.0} - {2.0} - {3.0} - {4.0} - {0.0} - {2.0} - {8.0} - {8.0} - {0.0} - {2.0} - {3.0} - {4.0} - {0.0} - {0.0} - {0.0} - {0.0} - {0.0} - {1.0} - {2.0} - {3.0} - {4.0} - {4.0} - {0.0} - {2.0} - {8.0} - {0.0} - {2.0} - {3.0} - {4.0} - ''' - if len(aScore.iter().parts) != len(vScore.iter().parts): - raise VariantException( - 'These scores do not have the same number of parts and cannot be merged.') + def purgeOrphans(self, excludeStorageStreams=True): + self._stream.purgeOrphans(excludeStorageStreams) + base.Music21Object.purgeOrphans(self, excludeStorageStreams) - if inPlace is True: - returnObj = aScore - else: - returnObj = aScore.coreCopyAsDerivation('mergeVariantScores') + def purgeLocations(self, rescanIsDead=False): + # must override Music21Object to purge locations from the contained + self._stream.purgeLocations(rescanIsDead=rescanIsDead) + base.Music21Object.purgeLocations(self, rescanIsDead=rescanIsDead) - for returnPart, vPart in zip(returnObj.parts, vScore.parts): - mergeVariantMeasureStreams(returnPart, vPart, variantName, inPlace=True) + def _reprInternal(self): + return 'object of length ' + str(self.containedHighestTime) - if inPlace is False: - return returnObj + def __getattr__(self, attr): + ''' + This defers all calls not defined in this Class to calls on the privately contained Stream. + ''' + # environLocal.printDebug(['relaying unmatched attribute request ' + # + attr + ' to private Stream']) + # must mask pitches so as not to recurse + # TODO: check tt recurse does not go into this + if attr in ['flat', 'pitches']: + raise AttributeError -def mergeVariantMeasureStreams(streamX, streamY, variantName='variant', *, inPlace=False): - ''' - Takes two streams of measures and returns a stream (new if inPlace is False) with the second - merged with the first as variants. This function differs from mergeVariantsEqualDuration by - dealing with streams that are of different length. This function matches measures that are - exactly equal and creates variant objects for regions of measures that differ at all. If more - refined variants are sought (with variation within the bar considered and related but different - bars associated with each other), use variant.refineVariant(). - - In this example, the second bar has been deleted in the second version, - a new bar has been inserted between the - original third and fourth bars, and two bars have been added at the end. - + # needed for unpickling where ._stream doesn't exist until later... + if attr != '_stream' and hasattr(self, '_stream'): + return getattr(self._stream, attr) + else: + raise AttributeError - >>> data1M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), - ... ('a', 'quarter'), ('a', 'quarter')] - >>> data1M2 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), - ... ('a', 'quarter'),('b', 'quarter')] - >>> data1M3 = [('c', 'quarter'), ('d', 'quarter'), - ... ('e', 'quarter'), ('e', 'quarter')] - >>> data1M4 = [('d', 'quarter'), ('g', 'eighth'), ('g', 'eighth'), - ... ('a', 'quarter'), ('b', 'quarter')] + def __getitem__(self, key): + return self._stream.__getitem__(key) - >>> data2M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), - ... ('a', 'quarter'), ('a', 'quarter')] - >>> data2M2 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')] - >>> data2M3 = [('e', 'quarter'), ('g', 'eighth'), ('g', 'eighth'), - ... ('a', 'quarter'), ('b', 'quarter')] - >>> data2M4 = [('d', 'quarter'), ('g', 'eighth'), ('g', 'eighth'), - ... ('a', 'quarter'), ('b', 'quarter')] - >>> data2M5 = [('f', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), - ... ('a', 'quarter'), ('b', 'quarter')] - >>> data2M6 = [('g', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')] - >>> data1 = [data1M1, data1M2, data1M3, data1M4] - >>> data2 = [data2M1, data2M2, data2M3, data2M4, data2M5, data2M6] - >>> stream1 = stream.Stream() - >>> stream2 = stream.Stream() - >>> mNumber = 1 - >>> for d in data1: - ... m = stream.Measure() - ... m.number = mNumber - ... mNumber += 1 - ... for pitchName, durType in d: - ... n = note.Note(pitchName) - ... n.duration.type = durType - ... m.append(n) - ... stream1.append(m) - >>> mNumber = 1 - >>> for d in data2: - ... m = stream.Measure() - ... m.number = mNumber - ... mNumber += 1 - ... for pitchName, durType in d: - ... n = note.Note(pitchName) - ... n.duration.type = durType - ... m.append(n) - ... stream2.append(m) - >>> #_DOCS_SHOW stream1.show() + def __len__(self): + return len(self._stream) + def __iter__(self): + return self._stream.__iter__() - .. image:: images/variant_measuresStreamMergeStream1.* - :width: 600 + def getElementIds(self): + if 'elementIds' not in self._cache or self._cache['elementIds'] is None: + self._cache['elementIds'] = [id(c) for c in self._stream._elements] + return self._cache['elementIds'] - >>> #_DOCS_SHOW stream2.show() + def replaceElement(self, old, new): + ''' + When copying a Variant, we need to update the Variant with new + references for copied elements. Given the old element, + this method will replace the old with the new. - .. image:: images/variant_measuresStreamMergeStream2.* - :width: 600 + The `old` parameter can be either an object or object id. - >>> mergedStream = variant.mergeVariantMeasureStreams(stream1, stream2, 'paris', inPlace=False) - >>> mergedStream.show('text') - {0.0} - {0.0} - {1.0} - {1.5} - {2.0} - {3.0} - {4.0} - {4.0} - {0.0} - {0.5} - {1.0} - {2.0} - {3.0} - {8.0} - {0.0} - {1.0} - {2.0} - {3.0} - {12.0} - {12.0} - {0.0} - {1.0} - {1.5} - {2.0} - {3.0} - {16.0} + This method is very similar to the replaceSpannedElement method on Spanner. + ''' + if old is None: + return None # do nothing + if common.isNum(old): + # this must be id(obj), not obj.id + e = self._stream.coreGetElementByMemoryLocation(old) + if e is not None: + self._stream.replace(e, new, allDerived=False) + else: + # do not do all Sites: only care about this one + self._stream.replace(old, new, allDerived=False) - >>> mergedStream[variant.Variant][0].replacementDuration - 4.0 - >>> mergedStream[variant.Variant][1].replacementDuration - 0.0 + # -------------------------------------------------------------------------- + # Stream simulation/overrides + @property + def highestTime(self): + ''' + This property masks calls to Stream.highestTime. Assuming `exposeTime` + is False, this always returns zero, making the Variant always take zero time. - >>> parisStream = mergedStream.activateVariants('paris', inPlace=False) - >>> parisStream.show('text') - {0.0} - {0.0} - {1.0} - {1.5} - {2.0} - {3.0} - {4.0} - {4.0} - {0.0} - {1.0} - {2.0} - {3.0} - {8.0} - {8.0} - {0.0} - {1.0} - {1.5} - {2.0} - {3.0} - {12.0} - {0.0} - {1.0} - {1.5} - {2.0} - {3.0} - {16.0} - {16.0} - {0.0} - {0.5} - {1.5} - {2.0} - {3.0} - {20.0} - {0.0} - {1.0} - {2.0} - {3.0} + >>> v = variant.Variant() + >>> v.append(note.Note(quarterLength=4)) + >>> v.highestTime + 0.0 + ''' + if self.exposeTime: + return self._stream.highestTime + else: + return 0.0 - >>> parisStream[variant.Variant][0].replacementDuration - 0.0 - >>> parisStream[variant.Variant][1].replacementDuration - 4.0 - >>> parisStream[variant.Variant][2].replacementDuration - 8.0 - ''' - if inPlace is True: - returnObj = streamX - else: - returnObj = streamX.coreCopyAsDerivation('mergeVariantMeasureStreams') + @property + def highestOffset(self): + ''' + This property masks calls to Stream.highestOffset. Assuming `exposeTime` + is False, this always returns zero, making the Variant always take zero time. - regions = _getRegionsFromStreams(returnObj, streamY) - for (regionType, xRegionStartMeasure, xRegionEndMeasure, - yRegionStartMeasure, yRegionEndMeasure) in regions: - # Note that the 'end' measure indices are 1 greater - # than the 0-indexed number of the measure. - if xRegionStartMeasure >= len(returnObj.getElementsByClass(stream.Measure)): - startOffset = returnObj.duration.quarterLength - # This deals with insertion at the end case where - # returnObj.measure(xRegionStartMeasure + 1) does not exist. + >>> v = variant.Variant() + >>> v.append(note.Note(quarterLength=4)) + >>> v.highestOffset + 0.0 + ''' + if self.exposeTime: + return self._stream.highestOffset else: - startOffset = returnObj.measure(xRegionStartMeasure + 1).getOffsetBySite(returnObj) + return 0.0 - yRegion = None - replacementDuration = 0.0 + def show(self, fmt=None, app=None): + ''' + Call show() on the Stream contained by this Variant. - if regionType == 'equal': - # yRegion = streamY.measures(yRegionStartMeasure + 1, yRegionEndMeasure) - continue # Do nothing - elif regionType == 'replace': - xRegion = returnObj.measures(xRegionStartMeasure + 1, xRegionEndMeasure) - replacementDuration = xRegion.duration.quarterLength - yRegion = streamY.measures(yRegionStartMeasure + 1, yRegionEndMeasure) - elif regionType == 'delete': - xRegion = returnObj.measures(xRegionStartMeasure + 1, xRegionEndMeasure) - replacementDuration = xRegion.duration.quarterLength - yRegion = None - elif regionType == 'insert': - yRegion = streamY.measures(yRegionStartMeasure + 1, yRegionEndMeasure) - replacementDuration = 0.0 - else: - raise VariantException(f'Unknown regionType {regionType!r}') - addVariant(returnObj, startOffset, yRegion, - variantName=variantName, replacementDuration=replacementDuration) + This method must be overridden, otherwise Music21Object.show() is called. - if inPlace is True: - return - else: - return returnObj + >>> v = variant.Variant() + >>> v.repeatAppend(note.Note(quarterLength=0.25), 8) + >>> v.show('t') + {0.0} + {0.25} + {0.5} + {0.75} + {1.0} + {1.25} + {1.5} + {1.75} + ''' + self._stream.show(fmt=fmt, app=app) -def mergeVariantsEqualDuration(streams, variantNames, *, inPlace=False): - ''' - Pass this function a list of streams (they must be of the same - length or a VariantException will be raised). - It will return a stream which merges the differences between the - streams into variant objects keeping the - first stream in the list as the default. If inPlace is True, the - first stream in the list will be modified, - otherwise a new stream will be returned. Pass a list of names to - associate variants with their sources, if this list - does not contain an entry for each non-default variant, - naming may not behave properly. Variants that have the - same differences from the default will be saved as separate - variant objects (i.e. more than once under different names). - Also, note that a streams with bars of differing lengths will not behave properly. + # -------------------------------------------------------------------------- + # properties particular to this class + @property + def containedHighestTime(self): + ''' + This property calls the contained Stream.highestTime. - >>> stream1 = stream.Stream() - >>> stream2paris = stream.Stream() - >>> stream3london = stream.Stream() - >>> data1 = [('a', 'quarter'), ('b', 'eighth'), - ... ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'), - ... ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'), - ... ('b', 'quarter'), ('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter')] - >>> data2 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter'), - ... ('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), ('a', 'quarter'), - ... ('b', 'quarter'), ('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter')] - >>> data3 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), - ... ('a', 'quarter'), ('a', 'quarter'), - ... ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'), - ... ('c', 'quarter'), ('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter')] - >>> for pitchName, durType in data1: - ... n = note.Note(pitchName) - ... n.duration.type = durType - ... stream1.append(n) - >>> for pitchName, durType in data2: - ... n = note.Note(pitchName) - ... n.duration.type = durType - ... stream2paris.append(n) - >>> for pitchName, durType in data3: - ... n = note.Note(pitchName) - ... n.duration.type = durType - ... stream3london.append(n) - >>> mergedStreams = variant.mergeVariantsEqualDuration( - ... [stream1, stream2paris, stream3london], ['paris', 'london']) - >>> mergedStreams.show('t') - {0.0} - {1.0} - {1.0} - {1.5} - {2.0} - {3.0} - {3.0} - {4.0} - {4.5} - {4.5} - {5.0} - {6.0} - {7.0} - {7.0} - {8.0} - {9.0} - {9.0} - {10.0} + >>> v = variant.Variant() + >>> v.append(note.Note(quarterLength=4)) + >>> v.containedHighestTime + 4.0 + ''' + return self._stream.highestTime - >>> mergedStreams.activateVariants('london').show('t') - {0.0} - {1.0} - {1.0} - {1.5} - {2.0} - {3.0} - {3.0} - {4.0} - {4.5} - {4.5} - {5.0} - {6.0} - {7.0} - {7.0} - {8.0} - {9.0} - {9.0} - {10.0} + @property + def containedHighestOffset(self): + ''' + This property calls the contained Stream.highestOffset. - If the streams contain parts and measures, the merge function will iterate - through them and determine - and store variant differences within each measure/part. + >>> v = variant.Variant() + >>> v.append(note.Note(quarterLength=4)) + >>> v.append(note.Note()) + >>> v.containedHighestOffset + 4.0 + ''' + return self._stream.highestOffset - >>> stream1 = stream.Stream() - >>> stream2 = stream.Stream() - >>> data1M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), - ... ('a', 'quarter'), ('a', 'quarter')] - >>> data1M2 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), - ... ('a', 'quarter'),('b', 'quarter')] - >>> data1M3 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')] - >>> data2M1 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter')] - >>> data2M2 = [('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), - ... ('a', 'quarter'), ('b', 'quarter')] - >>> data2M3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] - >>> data1 = [data1M1, data1M2, data1M3] - >>> data2 = [data2M1, data2M2, data2M3] - >>> tempPart = stream.Part() - >>> for d in data1: - ... m = stream.Measure() - ... for pitchName, durType in d: - ... n = note.Note(pitchName) - ... n.duration.type = durType - ... m.append(n) - ... tempPart.append(m) - >>> stream1.append(tempPart) - >>> tempPart = stream.Part() - >>> for d in data2: - ... m = stream.Measure() - ... for pitchName, durType in d: - ... n = note.Note(pitchName) - ... n.duration.type = durType - ... m.append(n) - ... tempPart.append(m) - >>> stream2.append(tempPart) - >>> mergedStreams = variant.mergeVariantsEqualDuration([stream1, stream2], ['paris']) - >>> mergedStreams.show('t') - {0.0} - {0.0} + @property + def containedSite(self): + ''' + Return the Stream contained in this Variant. + ''' + return self._stream + + def _getReplacementDuration(self): + if self._replacementDuration is None: + return self._stream.duration.quarterLength + else: + return self._replacementDuration + + def _setReplacementDuration(self, value): + self._replacementDuration = value + + replacementDuration = property(_getReplacementDuration, _setReplacementDuration, doc=''' + Set or Return the quarterLength duration in the main stream which this variant + object replaces in the variant version of the stream. If replacementDuration is + not set, it is assumed to be the same length as the variant. If, it is set to 0, + the variant should be interpreted as an insertion. Setting replacementDuration + to None will return the value to the default which is the duration of the variant + itself. + ''') + + @property + def lengthType(self): + ''' + Returns 'deletion' if variant is shorter than the region it replaces, 'elongation' + if the variant is longer than the region it replaces, and 'replacement' if it is + the same length. + ''' + lengthDifference = self.replacementDuration - self.containedHighestTime + if lengthDifference > 0.0: + return 'deletion' + elif lengthDifference < 0.0: + return 'elongation' + else: + return 'replacement' + + def replacedElements(self, contextStream=None, classList=None, + keepOriginalOffsets=False, includeSpacers=False): + # noinspection PyShadowingNames + ''' + Returns a Stream containing the elements which this variant replaces in a + given context stream. + This Stream will have length self.replacementDuration. + + In regions that are strictly replaced, only elements that share a class with + an element in the variant + are captured. Elsewhere, all elements are captured. + + >>> s = converter.parse("tinynotation: 4/4 d4 e4 f4 g4 a2 b-4 a4 g4 a8 g8 f4 e4 d2 a2 d4 e4 f4 g4 a2 b-4 a4 g4 a8 b-8 c'4 c4 f1", makeNotation=False) + >>> s.makeMeasures(inPlace=True) + >>> v1stream = converter.parse("tinynotation: 4/4 a2. b-8 a8", makeNotation=False) + >>> v2stream1 = converter.parse("tinynotation: 4/4 d4 f4 a2", makeNotation=False) + >>> v2stream2 = converter.parse("tinynotation: 4/4 d4 f4 AA2", makeNotation=False) + + >>> v1 = variant.Variant() + >>> v1measure = stream.Measure() + >>> v1.insert(0.0, v1measure) + >>> for e in v1stream.notesAndRests: + ... v1measure.insert(e.offset, e) + + >>> v2 = variant.Variant() + >>> v2measure1 = stream.Measure() + >>> v2measure2 = stream.Measure() + >>> v2.insert(0.0, v2measure1) + >>> v2.insert(4.0, v2measure2) + >>> for e in v2stream1.notesAndRests: + ... v2measure1.insert(e.offset, e) + >>> for e in v2stream2.notesAndRests: + ... v2measure2.insert(e.offset, e) + + >>> v3 = variant.Variant() + >>> v2.replacementDuration = 4.0 + >>> v3.replacementDuration = 4.0 + + >>> s.insert(4.0, v1) # replacement variant + >>> s.insert(12.0, v2) # insertion variant (2 bars replace 1 bar) + >>> s.insert(20.0, v3) # deletion variant (0 bars replace 1 bar) + + >>> v1.replacedElements(s).show('text') + {0.0} {0.0} - {1.0} - {1.0} - {1.5} + {2.0} + {3.0} + + >>> v2.replacedElements(s).show('text') + {0.0} + {0.0} {2.0} - {3.0} + + >>> v3.replacedElements(s).show('text') + {0.0} + {0.0} + {2.0} {3.0} - {4.0} + + >>> v3.replacedElements(s, keepOriginalOffsets=True).show('text') + {20.0} + {0.0} + {2.0} + {3.0} + + + A second example: + + + >>> v = variant.Variant() + >>> variantDataM1 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), + ... ('a', 'quarter'),('b', 'quarter')] + >>> variantDataM2 = [('c', 'quarter'), ('d', 'quarter'), + ... ('e', 'quarter'), ('e', 'quarter')] + >>> variantData = [variantDataM1, variantDataM2] + >>> for d in variantData: + ... m = stream.Measure() + ... for pitchName, durType in d: + ... n = note.Note(pitchName) + ... n.duration.type = durType + ... m.append(n) + ... v.append(m) + >>> v.groups = ['paris'] + >>> v.replacementDuration = 4.0 + + >>> s = stream.Stream() + >>> streamDataM1 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter')] + >>> streamDataM2 = [('b', 'eighth'), ('c', 'quarter'), + ... ('a', 'eighth'), ('a', 'quarter'), ('b', 'quarter')] + >>> streamDataM3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] + >>> streamDataM4 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] + >>> streamData = [streamDataM1, streamDataM2, streamDataM3, streamDataM4] + >>> for d in streamData: + ... m = stream.Measure() + ... for pitchName, durType in d: + ... n = note.Note(pitchName) + ... n.duration.type = durType + ... m.append(n) + ... s.append(m) + >>> s.insert(4.0, v) + + >>> v.replacedElements(s).show('t') + {0.0} {0.0} - {0.5} {0.5} - {1.0} + {1.5} {2.0} {3.0} - {8.0} - {0.0} - {1.0} - {1.0} - {2.0} - {3.0} - >>> #_DOCS_SHOW mergedStreams.show() + ''' + spacerFilter = lambda r: r.hasStyleInformation and r.style.hideObjectOnPrint + + if contextStream is None: + contextStream = self.activeSite + if contextStream is None: + environLocal.printDebug( + 'No contextStream or activeSite, finding most recently added site (dangerous)') + contextStream = self.getContextByClass('Stream') + if contextStream is None: + raise VariantException('Cannot find a Stream context for this object...') + + if self not in contextStream.getElementsByClass(self.__class__): + raise VariantException(f'Variant not found in stream {contextStream}') + + vStart = self.getOffsetBySite(contextStream) + + if includeSpacers is True: + spacerDuration = (self + .getElementsByClass('Rest') + .addFilter(spacerFilter) + .first().duration.quarterLength) + else: + spacerDuration = 0.0 + + + if self.lengthType in ('replacement', 'elongation'): + vEnd = vStart + self.replacementDuration + spacerDuration + classes = [] + for e in self.elements: + classes.append(e.classes[0]) + if classList is not None: + classes.extend(classList) + returnStream = contextStream.getElementsByOffset(vStart, vEnd, + includeEndBoundary=False, + mustFinishInSpan=False, + mustBeginInSpan=True, + classList=classes).stream() + elif self.lengthType == 'deletion': + vMiddle = vStart + self.containedHighestTime + vEnd = vStart + self.replacementDuration + classes = [] # collect all classes found in this variant + for e in self.elements: + classes.append(e.classes[0]) + if classList is not None: + classes.extend(classList) + returnPart1 = contextStream.getElementsByOffset(vStart, vMiddle, + includeEndBoundary=False, + mustFinishInSpan=False, + mustBeginInSpan=True, + classList=classes).stream() + returnPart2 = contextStream.getElementsByOffset(vMiddle, vEnd, + includeEndBoundary=False, + mustFinishInSpan=False, + mustBeginInSpan=True).stream() - .. image:: images/variant_measuresAndParts.* - :width: 600 + returnStream = returnPart1 + for e in returnPart2.elements: + oInPart = e.getOffsetBySite(returnPart2) + returnStream.insert(vMiddle - vStart + oInPart, e) + else: + raise VariantException('lengthType must be replacement, elongation, or deletion') + if self in returnStream: + returnStream.remove(self) - >>> for p in mergedStreams.getElementsByClass('Part'): - ... for m in p.getElementsByClass(stream.Measure): - ... m.activateVariants('paris', inPlace=True) - >>> mergedStreams.show('t') - {0.0} + # This probably makes sense to do, but activateVariants + # for example only uses the offset in the original. + # Also, we are not changing measure numbers and should + # not as that will cause activateVariants to fail. + if keepOriginalOffsets is False: + for e in returnStream: + e.setOffsetBySite(returnStream, e.getOffsetBySite(returnStream) - vStart) + + return returnStream + + def removeReplacedElementsFromStream(self, referenceStream=None, classList=None): + ''' + remove replaced elements from a referenceStream or activeSite + + + >>> v = variant.Variant() + >>> variantDataM1 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), + ... ('a', 'quarter'),('b', 'quarter')] + >>> variantDataM2 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')] + >>> variantData = [variantDataM1, variantDataM2] + >>> for d in variantData: + ... m = stream.Measure() + ... for pitchName, durType in d: + ... n = note.Note(pitchName) + ... n.duration.type = durType + ... m.append(n) + ... v.append(m) + >>> v.groups = ['paris'] + >>> v.replacementDuration = 4.0 + + >>> s = stream.Stream() + >>> streamDataM1 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter')] + >>> streamDataM2 = [('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), + ... ('a', 'quarter'), ('b', 'quarter')] + >>> streamDataM3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] + >>> streamDataM4 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] + >>> streamData = [streamDataM1, streamDataM2, streamDataM3, streamDataM4] + >>> for d in streamData: + ... m = stream.Measure() + ... for pitchName, durType in d: + ... n = note.Note(pitchName) + ... n.duration.type = durType + ... m.append(n) + ... s.append(m) + >>> s.insert(4.0, v) + + >>> v.removeReplacedElementsFromStream(s) + >>> s.show('t') {0.0} {0.0} - {1.0} {1.0} {2.0} - {3.0} {3.0} - {4.0} - {0.0} - {0.5} - {0.5} - {1.5} - {2.0} - {3.0} + {4.0} {8.0} {0.0} - {1.0} {1.0} {2.0} {3.0} - >>> #_DOCS_SHOW mergedStreams.show() + {12.0} + {0.0} + {1.0} + {2.0} + {3.0} + ''' + if referenceStream is None: + referenceStream = self.activeSite + if referenceStream is None: + environLocal.printDebug('No referenceStream or activeSite, ' + + 'finding most recently added site (dangerous)') + referenceStream = self.getContextByClass('Stream') + if referenceStream is None: + raise VariantException('Cannot find a Stream context for this object...') + if self not in referenceStream.getElementsByClass(self.__class__): + raise VariantException(f'Variant not found in stream {referenceStream}') + replacedElements = self.replacedElements(referenceStream, classList) + for el in replacedElements: + referenceStream.remove(el) - .. image:: images/variant_measuresAndParts2.* - :width: 600 - If barlines do not match up, an exception will be thrown. Here two streams that are identical - are merged, except one is in 3/4, the other in 4/4. This throws an exception. - >>> streamDifferentMeasures = stream.Stream() - >>> dataDiffM1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter')] - >>> dataDiffM2 = [ ('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter')] - >>> dataDiffM3 = [('a', 'quarter'), ('b', 'quarter'), ('c', 'quarter')] - >>> dataDiffM4 = [('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')] - >>> dataDiff = [dataDiffM1, dataDiffM2, dataDiffM3, dataDiffM4] - >>> streamDifferentMeasures.insert(0.0, meter.TimeSignature('3/4')) - >>> tempPart = stream.Part() - >>> for d in dataDiff: - ... m = stream.Measure() - ... for pitchName, durType in d: - ... n = note.Note(pitchName) - ... n.duration.type = durType - ... m.append(n) - ... tempPart.append(m) - >>> streamDifferentMeasures.append(tempPart) - >>> mergedStreams = variant.mergeVariantsEqualDuration( - ... [stream1, streamDifferentMeasures], ['paris']) - Traceback (most recent call last): - music21.variant.VariantException: _mergeVariants cannot merge streams - which are of different lengths +# ------Public Merge Functions +def mergeVariants(streamX, streamY, variantName='variant', *, inPlace=False): + # noinspection PyShadowingNames ''' + Takes two streams objects or their derivatives (Score, Part, Measure, etc.) which + should be variant versions of the same stream, + and merges them (determines differences and stores those differences as variant objects + in streamX) via the appropriate merge + function for their type. This will not know how to deal with scores meant for + mergePartAsOssia(). If this is the intention, use + that function instead. - if inPlace is True: - returnObj = streams[0] - else: - returnObj = streams[0].coreCopyAsDerivation('mergeVariantsEqualDuration') - - # Adds a None element at beginning (corresponding to default variant streams[0]) - variantNames.insert(0, None) - while len(streams) > len(variantNames): # Adds Blank names if too few - variantNames.append(None) - while len(streams) < len(variantNames): # Removes extra names - variantNames.pop() - - zipped = list(zip(streams, variantNames)) - - for s, variantName in zipped[1:]: - if returnObj.highestTime != s.highestTime: - raise VariantException('cannot merge streams of different lengths') - - returnObjParts = returnObj.getElementsByClass('Part') - if returnObjParts: # If parts exist, iterate through them. - sParts = s.getElementsByClass('Part') - for i, returnObjPart in enumerate(returnObjParts): - sPart = sParts[i] - - returnObjMeasures = returnObjPart.getElementsByClass(stream.Measure) - if returnObjMeasures: - # If measures exist and parts exist, iterate through them both. - for j, returnObjMeasure in enumerate(returnObjMeasures): - sMeasure = sPart.getElementsByClass(stream.Measure)[j] - _mergeVariants( - returnObjMeasure, sMeasure, variantName=variantName, inPlace=True) - - else: # If parts exist but no measures. - _mergeVariants(returnObjPart, sPart, variantName=variantName, inPlace=True) - else: - returnObjMeasures = returnObj.getElementsByClass(stream.Measure) - if returnObjMeasures: # If no parts, but still measures, iterate through them. - for j, returnObjMeasure in enumerate(returnObjMeasures): - sMeasure = s.getElementsByClass(stream.Measure)[j] - _mergeVariants(returnObjMeasure, sMeasure, - variantName=variantName, inPlace=True) - else: # If no parts and no measures. - _mergeVariants(returnObj, s, variantName=variantName, inPlace=True) - - return returnObj + >>> streamX = converter.parse('tinynotation: 4/4 a4 b c d', makeNotation=False) + >>> streamY = converter.parse('tinynotation: 4/4 a4 b- c e', makeNotation=False) + >>> mergedStream = variant.mergeVariants(streamX, streamY, + ... variantName='docVariant', inPlace=False) + >>> mergedStream.show('text') + {0.0} + {0.0} + {1.0} + {1.0} + {2.0} + {3.0} + {3.0} -def mergePartAsOssia(mainPart, ossiaPart, ossiaName, - inPlace=False, compareByMeasureNumber=False, recurseInMeasures=False): - # noinspection PyShadowingNames - ''' - Some MusicXML files are generated with full parts that have only a few non-rest measures - instead of ossia parts, such as those - created by Sibelius 7. This function - takes two streams (mainPart and ossiaPart), the second interpreted as an ossia. - It outputs a stream with the ossia part merged into the stream as a - group of variants. + >>> v0 = mergedStream.getElementsByClass(variant.Variant).first() + >>> v0 + + >>> v0.first() + - If compareByMeasureNumber is True, then the ossia measures will be paired with the - measures in the mainPart that have the - same measure.number. Otherwise, they will be paired by offset. In most cases - these should have the same result. + >>> streamZ = converter.parse('tinynotation: 4/4 a4 b c d e f g a', makeNotation=False) + >>> variant.mergeVariants(streamX, streamZ, variantName='docVariant', inPlace=False) + Traceback (most recent call last): + music21.variant.VariantException: Could not determine what merging method to use. + Try using a more specific merging function. - Note that this method has no way of knowing if a variant is supposed to be a - different duration than the segment of stream which it replaces - because that information is not contained in the format of score this method is - designed to deal with. + Example: Create a main score (aScore) and a variant score (vScore), each with + two parts (ap1/vp1 + and ap2/vp2) and some small variants between ap1/vp1 and ap2/vp2, marked with * below. - >>> mainStream = converter.parse('tinynotation: 4/4 A4 B4 C4 D4 E1 F2 E2 E8 F8 F4 G2 G2 G4 F4 F4 F4 F4 F4 G1 ') - >>> ossiaStream = converter.parse('tinynotation: 4/4 r1 r1 r1 E4 E4 F4 G4 r1 F2 F2 r1 ') - >>> mainStream.makeMeasures(inPlace=True) - >>> ossiaStream.makeMeasures(inPlace=True) + >>> aScore = stream.Score() + >>> vScore = stream.Score() - >>> mainPart = stream.Part() - >>> for m in mainStream: - ... mainPart.insert(m.offset, m) - >>> ossiaPart = stream.Part() - >>> for m in ossiaStream: - ... ossiaPart.insert(m.offset, m) + >>> # * + >>> ap1 = converter.parse('tinynotation: 4/4 a4 b c d e2 f g2 f4 g ') + >>> vp1 = converter.parse('tinynotation: 4/4 a4 b c e e2 f g2 f4 a ') - >>> s = stream.Stream() - >>> s.insert(0.0, ossiaPart) - >>> s.insert(0.0, mainPart) - >>> #_DOCS_SHOW s.show() + >>> # * * * + >>> ap2 = converter.parse('tinynotation: 4/4 a4 g f e f2 e d2 g4 f ') + >>> vp2 = converter.parse('tinynotation: 4/4 a4 g f e f2 g f2 g4 d ') - >>> mainPartWithOssiaVariantsFT = variant.mergePartAsOssia(mainPart, ossiaPart, - ... ossiaName='Parisian_Variant', - ... inPlace=False, - ... compareByMeasureNumber=False, - ... recurseInMeasures=True) - >>> mainPartWithOssiaVariantsTT = variant.mergePartAsOssia(mainPart, ossiaPart, - ... ossiaName='Parisian_Variant', - ... inPlace=False, - ... compareByMeasureNumber=True, - ... recurseInMeasures=True) - >>> mainPartWithOssiaVariantsFF = variant.mergePartAsOssia(mainPart, ossiaPart, - ... ossiaName='Parisian_Variant', - ... inPlace=False, - ... compareByMeasureNumber=False, - ... recurseInMeasures=False) - >>> mainPartWithOssiaVariantsTF = variant.mergePartAsOssia(mainPart, ossiaPart, - ... ossiaName='Parisian_Variant', - ... inPlace=False, - ... compareByMeasureNumber=True, - ... recurseInMeasures=False) + >>> ap1.id = 'aPart1' + >>> ap2.id = 'aPart2' - >>> mainPartWithOssiaVariantsFT.show('text') == mainPartWithOssiaVariantsTT.show('text') - {0.0} >> aScore.insert(0.0, ap1) + >>> aScore.insert(0.0, ap2) + >>> vScore.insert(0.0, vp1) + >>> vScore.insert(0.0, vp2) - >>> mainPartWithOssiaVariantsFF.show('text') == mainPartWithOssiaVariantsFT.show('text') - {0.0} >> mainPartWithOssiaVariantsFT.show('text') - {0.0} - ... - {12.0} - {0.0} - {0.0} - {0.5} - {1.0} - {2.0} - {16.0} - ... - {20.0} + >>> mergedScore = variant.mergeVariants(aScore, vScore, variantName='docVariant', inPlace=False) + >>> mergedScore.show('text') + {0.0} {0.0} - {0.0} - {1.0} - {2.0} - {3.0} - ... + {0.0} + {0.0} + {0.0} + {0.0} + {1.0} + {2.0} + {3.0} + {4.0} + {0.0} + {2.0} + {8.0} + {8.0} + {0.0} + {2.0} + {3.0} + {4.0} + {0.0} + {0.0} + {0.0} + {0.0} + {0.0} + {1.0} + {2.0} + {3.0} + {4.0} + {4.0} + {0.0} + {2.0} + {8.0} + {0.0} + {2.0} + {3.0} + {4.0} - >>> mainPartWithOssiaVariantsFF.activateVariants('Parisian_Variant').show('text') + + >>> mergedPart = variant.mergeVariants(ap2, vp2, variantName='docVariant', inPlace=False) + >>> mergedPart.show('text') {0.0} ... - {12.0} - {12.0} - {0.0} - {1.0} - {2.0} - {3.0} - {16.0} - ... - {20.0} - {20.0} - {0.0} - {2.0} + {4.0} + {4.0} ... - + {4.0} ''' - if inPlace is True: - returnObj = mainPart - else: - returnObj = mainPart.coreCopyAsDerivation('mergePartAsOssia') - - if compareByMeasureNumber is True: - for ossiaMeasure in ossiaPart.getElementsByClass(stream.Measure): - if ossiaMeasure.notes: # If the measure is not just rests - ossiaNumber = ossiaMeasure.number - returnMeasure = returnObj.measure(ossiaNumber) - if recurseInMeasures is True: - mergeVariantsEqualDuration( - [returnMeasure, ossiaMeasure], - [ossiaName], - inPlace=True - ) - else: - ossiaOffset = returnMeasure.getOffsetBySite(returnObj) - addVariant(returnObj, - ossiaOffset, - ossiaMeasure, - variantName=ossiaName, - variantGroups=None, - replacementDuration=None - ) - else: - for ossiaMeasure in ossiaPart.getElementsByClass(stream.Measure): - if ossiaMeasure.notes: # If the measure is not just rests - ossiaOffset = ossiaMeasure.getOffsetBySite(ossiaPart) - if recurseInMeasures is True: - returnMeasure = returnObj.getElementsByOffset( - ossiaOffset - ).getElementsByClass(stream.Measure).first() - mergeVariantsEqualDuration( - [returnMeasure, ossiaMeasure], - [ossiaName], - inPlace=True - ) - else: - addVariant(returnObj, ossiaOffset, ossiaMeasure, - variantName=ossiaName, variantGroups=None, replacementDuration=None) - - if inPlace is True: - return + classesX = streamX.classes + if 'Score' in classesX: + return mergeVariantScores(streamX, streamY, variantName, inPlace=inPlace) + elif streamX.getElementsByClass(stream.Measure): + return mergeVariantMeasureStreams(streamX, streamY, variantName, inPlace=inPlace) + elif (streamX.iter().notesAndRests + and streamX.duration.quarterLength == streamY.duration.quarterLength): + return mergeVariantsEqualDuration([streamX, streamY], [variantName], inPlace=inPlace) else: - return returnObj - + raise VariantException( + 'Could not determine what merging method to use. ' + + 'Try using a more specific merging function.') -# ------ Public Helper Functions -def addVariant( - s: stream.Stream, - startOffset: Union[int, float], - sVariant: Union[stream.Stream, 'Variant'], - variantName=None, - variantGroups=None, - replacementDuration=None -): +def mergeVariantScores(aScore, vScore, variantName='variant', *, inPlace=False): # noinspection PyShadowingNames ''' - Takes a stream, the location of the variant to be added to - that stream (startOffset), the content of the - variant to be added (sVariant), and the duration of the section of the stream which the variant - replaces (replacementDuration). - - If replacementDuration is 0, - this is an insertion. If sVariant is - None, this is a deletion. - + Takes two scores and merges them with mergeVariantMeasureStreams, part-by-part. - >>> data1M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), - ... ('a', 'quarter'), ('a', 'quarter')] - >>> data1M3 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')] - >>> data1M2 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), - ... ('a', 'quarter'),('b', 'quarter')] - >>> data1 = [data1M1, data1M2, data1M3] - >>> tempPart = stream.Part() - >>> stream1 = stream.Stream() - >>> for d in data1: - ... m = stream.Measure() - ... for pitchName, durType in d: - ... n = note.Note(pitchName) - ... n.duration.type = durType - ... m.append(n) - ... stream1.append(m) + >>> aScore, vScore = stream.Score(), stream.Score() - >>> data2M2 = [('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), - ... ('a', 'quarter'), ('b', 'quarter')] - >>> stream2 = stream.Stream() - >>> m = stream.Measure() - >>> for pitchName, durType in data2M2: - ... n = note.Note(pitchName) - ... n.duration.type = durType - ... m.append(n) - >>> stream2.append(m) - >>> variant.addVariant(stream1, 4.0, stream2, - ... variantName='rhythmic_switch', replacementDuration=4.0) - >>> stream1.show('text') - {0.0} - {0.0} - {1.0} - {1.5} - {2.0} - {3.0} - {4.0} - {4.0} - {0.0} - {0.5} - {1.0} - {2.0} - {3.0} - {8.0} - {0.0} - {1.0} - {2.0} - {3.0} + >>> ap1 = converter.parse('tinynotation: 4/4 a4 b c d e2 f2 g2 f4 g4 ') + >>> vp1 = converter.parse('tinynotation: 4/4 a4 b c e e2 f2 g2 f4 a4 ') - >>> stream1 = stream.Stream() - >>> stream1.repeatAppend(note.Note('e'), 6) - >>> variant1 = variant.Variant() - >>> variant1.repeatAppend(note.Note('f'), 3) - >>> startOffset = 3.0 - >>> variant.addVariant(stream1, startOffset, variant1, - ... variantName='paris', replacementDuration=3.0) - >>> stream1.show('text') - {0.0} - {1.0} - {2.0} - {3.0} - {3.0} - {4.0} - {5.0} - ''' - tempVariant = Variant() + >>> ap2 = converter.parse('tinynotation: 4/4 a4 g f e f2 e2 d2 g4 f4 ') + >>> vp2 = converter.parse('tinynotation: 4/4 a4 g f e f2 g2 f2 g4 d4 ') - if variantGroups is not None: - tempVariant.groups = variantGroups - if variantName is not None: - tempVariant.groups.append(variantName) + >>> aScore.insert(0.0, ap1) + >>> aScore.insert(0.0, ap2) + >>> vScore.insert(0.0, vp1) + >>> vScore.insert(0.0, vp2) - tempVariant.replacementDuration = replacementDuration + >>> mergedScores = variant.mergeVariantScores(aScore, vScore, + ... variantName='docVariant', inPlace=False) + >>> mergedScores.show('text') + {0.0} + {0.0} + {0.0} + {0.0} + {0.0} + {0.0} + {1.0} + {2.0} + {3.0} + {4.0} + {0.0} + {2.0} + {8.0} + {8.0} + {0.0} + {2.0} + {3.0} + {4.0} + {0.0} + {0.0} + {0.0} + {0.0} + {0.0} + {1.0} + {2.0} + {3.0} + {4.0} + {4.0} + {0.0} + {2.0} + {8.0} + {0.0} + {2.0} + {3.0} + {4.0} + ''' + if len(aScore.iter().parts) != len(vScore.iter().parts): + raise VariantException( + 'These scores do not have the same number of parts and cannot be merged.') - if sVariant is None: # deletion - pass - else: # replacement or insertion - if isinstance(sVariant, stream.Measure): # sVariant is a measure put it in a variant and insert. - tempVariant.append(sVariant) - else: # sVariant is not a measure - sVariantMeasures = sVariant.getElementsByClass(stream.Measure) - if not sVariantMeasures: # If there are no measures, work element-wise - for e in sVariant: - offset = e.getOffsetBySite(sVariant) + startOffset - tempVariant.insert(offset, e) - else: # if there are measures work measure-wise - for m in sVariantMeasures: - tempVariant.append(m) + if inPlace is True: + returnObj = aScore + else: + returnObj = aScore.coreCopyAsDerivation('mergeVariantScores') - s.insert(startOffset, tempVariant) + for returnPart, vPart in zip(returnObj.parts, vScore.parts): + mergeVariantMeasureStreams(returnPart, vPart, variantName, inPlace=True) + if inPlace is False: + return returnObj -def refineVariant(s, sVariant, *, inPlace=False): - # noinspection PyShadowingNames +def mergeVariantMeasureStreams(streamX, streamY, variantName='variant', *, inPlace=False): ''' - Given a stream and variant contained in that stream, returns a - stream with that variant 'refined.' + Takes two streams of measures and returns a stream (new if inPlace is False) with the second + merged with the first as variants. This function differs from mergeVariantsEqualDuration by + dealing with streams that are of different length. This function matches measures that are + exactly equal and creates variant objects for regions of measures that differ at all. If more + refined variants are sought (with variation within the bar considered and related but different + bars associated with each other), use variant.refineVariant(). - It is refined in the sense that, (with the best estimates) measures which have been determined - to be related are merged within the measure. + In this example, the second bar has been deleted in the second version, + a new bar has been inserted between the + original third and fourth bars, and two bars have been added at the end. - Suppose a four-bar phrase in a piece is a slightly - different five-bar phrase in a variant. In the variant, every F# has been replaced by an F, - and the last bar is repeated. Given these streams, mergeVariantMeasureStreams would return - the first stream with a single variant object containing the entire 5 bars of the variant. - Calling refineVariant on this stream and that variant object would result in a variant object - in the measures for each F#/F pair, and a variant object containing the added bar at the end. - For a more detailed explanation of how similar measures are properly associated with each other - look at the documentation for _getBestListAndScore - Note that this code does not work properly yet. + >>> data1M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), + ... ('a', 'quarter'), ('a', 'quarter')] + >>> data1M2 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), + ... ('a', 'quarter'),('b', 'quarter')] + >>> data1M3 = [('c', 'quarter'), ('d', 'quarter'), + ... ('e', 'quarter'), ('e', 'quarter')] + >>> data1M4 = [('d', 'quarter'), ('g', 'eighth'), ('g', 'eighth'), + ... ('a', 'quarter'), ('b', 'quarter')] + >>> data2M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), + ... ('a', 'quarter'), ('a', 'quarter')] + >>> data2M2 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')] + >>> data2M3 = [('e', 'quarter'), ('g', 'eighth'), ('g', 'eighth'), + ... ('a', 'quarter'), ('b', 'quarter')] + >>> data2M4 = [('d', 'quarter'), ('g', 'eighth'), ('g', 'eighth'), + ... ('a', 'quarter'), ('b', 'quarter')] + >>> data2M5 = [('f', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), + ... ('a', 'quarter'), ('b', 'quarter')] + >>> data2M6 = [('g', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')] - >>> v = variant.Variant() - >>> variantDataM1 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), - ... ('a', 'quarter'),('b', 'quarter')] - >>> variantDataM2 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')] - >>> variantData = [variantDataM1, variantDataM2] - >>> for d in variantData: + >>> data1 = [data1M1, data1M2, data1M3, data1M4] + >>> data2 = [data2M1, data2M2, data2M3, data2M4, data2M5, data2M6] + >>> stream1 = stream.Stream() + >>> stream2 = stream.Stream() + >>> mNumber = 1 + >>> for d in data1: ... m = stream.Measure() + ... m.number = mNumber + ... mNumber += 1 ... for pitchName, durType in d: ... n = note.Note(pitchName) ... n.duration.type = durType ... m.append(n) - ... v.append(m) - >>> v.groups = ['paris'] - >>> v.replacementDuration = 8.0 - - >>> s = stream.Stream() - >>> streamDataM1 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter')] - >>> streamDataM2 = [('b', 'eighth'), ('c', 'quarter'), - ... ('a', 'eighth'), ('a', 'quarter'), ('b', 'quarter')] - >>> streamDataM3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] - >>> streamDataM4 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] - >>> streamData = [streamDataM1, streamDataM2, streamDataM3, streamDataM4] - >>> for d in streamData: + ... stream1.append(m) + >>> mNumber = 1 + >>> for d in data2: ... m = stream.Measure() + ... m.number = mNumber + ... mNumber += 1 ... for pitchName, durType in d: ... n = note.Note(pitchName) ... n.duration.type = durType ... m.append(n) - ... s.append(m) - >>> s.insert(4.0, v) - - >>> variant.refineVariant(s, v, inPlace=True) - >>> s.show('text') - {0.0} - {0.0} - {1.0} - {2.0} - {3.0} - {4.0} - {0.0} - {0.5} - {0.5} - {1.5} - {2.0} - {3.0} - {8.0} - {0.0} - {1.0} - {1.0} - {2.0} - {3.0} - {12.0} - {0.0} - {1.0} - {2.0} - {3.0} - - ''' - # stream that will be returned - if sVariant not in s.getElementsByClass('Variant'): - raise VariantException(f'{sVariant} not found in stream {s}.') - - if inPlace is True: - returnObject = s - variantRegion = sVariant - else: - sVariantIndex = s.getElementsByClass('Variant').index(sVariant) - - returnObject = s.coreCopyAsDerivation('refineVariant') - variantRegion = returnObject.getElementsByClass('Variant')(sVariantIndex) - - - # useful parameters from variant and its location - variantGroups = sVariant.groups - replacementDuration = sVariant.replacementDuration - startOffset = sVariant.getOffsetBySite(s) - # endOffset = replacementDuration + startOffset - - # region associated with the given variant in the stream - returnRegion = variantRegion.replacedElements(returnObject) - - # associating measures in variantRegion to those in returnRegion -> - # This is done via 0 indexed lists corresponding to measures - returnRegionMeasureList = list(range(len(returnRegion))) - badnessDict = {} - listDict = {} - variantMeasureList, unused_badness = _getBestListAndScore(returnRegion, - variantRegion, - badnessDict, - listDict) + ... stream2.append(m) + >>> #_DOCS_SHOW stream1.show() - # badness is a measure of how different the streams are. - # The list returned, variantMeasureList, minimizes that quantity. - # mentioned lists are compared via difflib for optimal edit regions - # (equal, delete, insert, replace) - sm = difflib.SequenceMatcher() - sm.set_seqs(returnRegionMeasureList, variantMeasureList) - regions = sm.get_opcodes() + .. image:: images/variant_measuresStreamMergeStream1.* + :width: 600 - # each region is processed for variants. - for regionType, returnStart, returnEnd, variantStart, variantEnd in regions: - startOffset = returnRegion[returnStart].getOffsetBySite(returnRegion) - # endOffset = (returnRegion[returnEnd-1].getOffsetBySite(returnRegion) + - # returnRegion[returnEnd-1].duration.quarterLength) - variantSubRegion = None - if regionType == 'equal': - returnSubRegion = returnRegion.measures(returnStart + 1, returnEnd) - variantSubRegion = variantRegion.measures(variantStart + 1, variantEnd) - mergeVariantsEqualDuration( - [returnSubRegion, variantSubRegion], - variantGroups, - inPlace=True - ) - continue - elif regionType == 'replace': - returnSubRegion = returnRegion.measures(returnStart + 1, returnEnd) - replacementDuration = returnSubRegion.duration.quarterLength - variantSubRegion = variantRegion.measures(variantStart + 1, variantEnd) - elif regionType == 'delete': - returnSubRegion = returnRegion.measures(returnStart + 1, returnEnd) - replacementDuration = returnSubRegion.duration.quarterLength - variantSubRegion = None - elif regionType == 'insert': - variantSubRegion = variantRegion.measures(variantStart + 1, variantEnd) - replacementDuration = 0.0 - else: - raise VariantException(f'Unknown regionType {regionType!r}') + >>> #_DOCS_SHOW stream2.show() - addVariant(returnRegion, - startOffset, - variantSubRegion, - variantGroups=variantGroups, - replacementDuration=replacementDuration - ) - # The original variant object has been replaced by more refined - # variant objects and so should be deleted. - returnObject.remove(variantRegion) + .. image:: images/variant_measuresStreamMergeStream2.* + :width: 600 - if inPlace: - return None - else: - return returnObject + >>> mergedStream = variant.mergeVariantMeasureStreams(stream1, stream2, 'paris', inPlace=False) + >>> mergedStream.show('text') + {0.0} + {0.0} + {1.0} + {1.5} + {2.0} + {3.0} + {4.0} + {4.0} + {0.0} + {0.5} + {1.0} + {2.0} + {3.0} + {8.0} + {0.0} + {1.0} + {2.0} + {3.0} + {12.0} + {12.0} + {0.0} + {1.0} + {1.5} + {2.0} + {3.0} + {16.0} + >>> mergedStream[variant.Variant][0].replacementDuration + 4.0 + >>> mergedStream[variant.Variant][1].replacementDuration + 0.0 -def _mergeVariantMeasureStreamsCarefully(streamX, streamY, variantName, *, inPlace=False): - ''' - There seem to be some problems with this function, and it isn't well tested. - It is not recommended to use it at this time. + >>> parisStream = mergedStream.activateVariants('paris', inPlace=False) + >>> parisStream.show('text') + {0.0} + {0.0} + {1.0} + {1.5} + {2.0} + {3.0} + {4.0} + {4.0} + {0.0} + {1.0} + {2.0} + {3.0} + {8.0} + {8.0} + {0.0} + {1.0} + {1.5} + {2.0} + {3.0} + {12.0} + {0.0} + {1.0} + {1.5} + {2.0} + {3.0} + {16.0} + {16.0} + {0.0} + {0.5} + {1.5} + {2.0} + {3.0} + {20.0} + {0.0} + {1.0} + {2.0} + {3.0} + >>> parisStream[variant.Variant][0].replacementDuration + 0.0 + >>> parisStream[variant.Variant][1].replacementDuration + 4.0 + >>> parisStream[variant.Variant][2].replacementDuration + 8.0 ''' - # stream that will be returned if inPlace is True: - returnObject = streamX - variantObject = streamY + returnObj = streamX else: - returnObject = copy.deepcopy(streamX) - variantObject = copy.deepcopy(streamY) - - # associating measures in variantRegion to those in returnRegion -> - # This is done via 0 indexed lists corresponding to measures - returnObjectMeasureList = list(range(len(returnObject.getElementsByClass(stream.Measure)))) - badnessDict = {} - listDict = {} - variantObjectMeasureList, unused_badness = _getBestListAndScore( - returnObject.getElementsByClass(stream.Measure), - variantObject.getElementsByClass(stream.Measure), - badnessDict, - listDict - ) + returnObj = streamX.coreCopyAsDerivation('mergeVariantMeasureStreams') - # badness is a measure of how different the streams are. - # The list returned, variantMeasureList, minimizes that quantity. + regions = _getRegionsFromStreams(returnObj, streamY) + for (regionType, xRegionStartMeasure, xRegionEndMeasure, + yRegionStartMeasure, yRegionEndMeasure) in regions: + # Note that the 'end' measure indices are 1 greater + # than the 0-indexed number of the measure. + if xRegionStartMeasure >= len(returnObj.getElementsByClass(stream.Measure)): + startOffset = returnObj.duration.quarterLength + # This deals with insertion at the end case where + # returnObj.measure(xRegionStartMeasure + 1) does not exist. + else: + startOffset = returnObj.measure(xRegionStartMeasure + 1).getOffsetBySite(returnObj) - # mentioned lists are compared via difflib for optimal edit regions - # (equal, delete, insert, replace) - sm = difflib.SequenceMatcher() - sm.set_seqs(returnObjectMeasureList, variantObjectMeasureList) - regions = sm.get_opcodes() + yRegion = None + replacementDuration = 0.0 - # each region is processed for variants. - for regionType, returnStart, returnEnd, variantStart, variantEnd in regions: - startOffset = returnObject.measure(returnStart + 1).getOffsetBySite(returnObject) if regionType == 'equal': - returnSubRegion = returnObject.measures(returnStart + 1, returnEnd) - variantSubRegion = variantObject.measures(variantStart + 1, variantEnd) - mergeVariantMeasureStreams( - returnSubRegion, - variantSubRegion, - variantName, - inPlace=True - ) - continue + # yRegion = streamY.measures(yRegionStartMeasure + 1, yRegionEndMeasure) + continue # Do nothing elif regionType == 'replace': - returnSubRegion = returnObject.measures(returnStart + 1, returnEnd) - replacementDuration = returnSubRegion.duration.quarterLength - variantSubRegion = variantObject.measures(variantStart + 1, variantEnd) + xRegion = returnObj.measures(xRegionStartMeasure + 1, xRegionEndMeasure) + replacementDuration = xRegion.duration.quarterLength + yRegion = streamY.measures(yRegionStartMeasure + 1, yRegionEndMeasure) elif regionType == 'delete': - returnSubRegion = returnObject.measures(returnStart + 1, returnEnd) - replacementDuration = returnSubRegion.duration.quarterLength - variantSubRegion = None + xRegion = returnObj.measures(xRegionStartMeasure + 1, xRegionEndMeasure) + replacementDuration = xRegion.duration.quarterLength + yRegion = None elif regionType == 'insert': - variantSubRegion = variantObject.measures(variantStart + 1, variantEnd) + yRegion = streamY.measures(yRegionStartMeasure + 1, yRegionEndMeasure) replacementDuration = 0.0 - else: # pragma: no cover - raise VariantException(f'Unknown regionType: {regionType}') - - - addVariant( - returnObject, - startOffset, - variantSubRegion, - variantGroups=[variantName], - replacementDuration=replacementDuration - ) - - if not inPlace: - return returnObject + else: + raise VariantException(f'Unknown regionType {regionType!r}') + addVariant(returnObj, startOffset, yRegion, + variantName=variantName, replacementDuration=replacementDuration) + if inPlace is True: + return + else: + return returnObj -def getMeasureHashes(s): - # noinspection PyShadowingNames - ''' - Takes in a stream containing measures and returns a list of hashes, - one for each measure. Currently - implemented with search.translateStreamToString() - >>> s = converter.parse("tinynotation: 2/4 c4 d8. e16 FF4 a'4 b-2") - >>> sm = s.makeMeasures() - >>> hashes = variant.getMeasureHashes(sm) - >>> hashes - ['

K@<', ')PQP', 'FZ'] +def mergeVariantsEqualDuration(streams, variantNames, *, inPlace=False): ''' - hashes = [] - if isinstance(s, list): - for m in s: - hashes.append(search.translateStreamToString(m.notesAndRests)) - return hashes - else: - for m in s.getElementsByClass(stream.Measure): - hashes.append(search.translateStreamToString(m.notesAndRests)) - return hashes + Pass this function a list of streams (they must be of the same + length or a VariantException will be raised). + It will return a stream which merges the differences between the + streams into variant objects keeping the + first stream in the list as the default. If inPlace is True, the + first stream in the list will be modified, + otherwise a new stream will be returned. Pass a list of names to + associate variants with their sources, if this list + does not contain an entry for each non-default variant, + naming may not behave properly. Variants that have the + same differences from the default will be saved as separate + variant objects (i.e. more than once under different names). + Also, note that a streams with bars of differing lengths will not behave properly. -# ----- Private Helper Functions -def _getBestListAndScore(streamX, streamY, badnessDict, listDict, - isNone=False, streamXIndex=-1, streamYIndex=-1): - # noinspection PyShadowingNames - ''' - This is a recursive function which makes a map between two related streams of measures. - It is designed for streams of measures that contain few if any measures that are actually - identical and that have a different number of measures (within reason). For example, - if one stream has 10 bars of eighth notes and the second stream has the same ten bars - of eighth notes except with some dotted rhythms mixed in and the fifth bar is repeated. - The first, streamX, is the reference stream. This function returns a list of - integers with length len(streamY) which maps each measure of StreamY to the measure - in streamX it is most likely associated with. For example, if the returned list is - [0, 2, 3, 'addedBar', 4]. This indicates that streamY is most similar to streamX - after the second bar of streamX has been removed and a new bar inserted between - bars 4 and 5. Note that this list has measures 0-indexed. This function generates this map by - minimizing the difference or 'badness' for the sequence of measures on the whole as determined - by the helper function _simScore which compares measures for similarity. 'addedBar' appears - in the list where this function has determined that the bar appearing - in streamY does not have a counterpart in streamX anywhere and is an insertion. + >>> stream1 = stream.Stream() + >>> stream2paris = stream.Stream() + >>> stream3london = stream.Stream() + >>> data1 = [('a', 'quarter'), ('b', 'eighth'), + ... ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'), + ... ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'), + ... ('b', 'quarter'), ('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter')] + >>> data2 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter'), + ... ('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), ('a', 'quarter'), + ... ('b', 'quarter'), ('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter')] + >>> data3 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), + ... ('a', 'quarter'), ('a', 'quarter'), + ... ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'), + ... ('c', 'quarter'), ('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter')] + >>> for pitchName, durType in data1: + ... n = note.Note(pitchName) + ... n.duration.type = durType + ... stream1.append(n) + >>> for pitchName, durType in data2: + ... n = note.Note(pitchName) + ... n.duration.type = durType + ... stream2paris.append(n) + >>> for pitchName, durType in data3: + ... n = note.Note(pitchName) + ... n.duration.type = durType + ... stream3london.append(n) + >>> mergedStreams = variant.mergeVariantsEqualDuration( + ... [stream1, stream2paris, stream3london], ['paris', 'london']) + >>> mergedStreams.show('t') + {0.0} + {1.0} + {1.0} + {1.5} + {2.0} + {3.0} + {3.0} + {4.0} + {4.5} + {4.5} + {5.0} + {6.0} + {7.0} + {7.0} + {8.0} + {9.0} + {9.0} + {10.0} + + >>> mergedStreams.activateVariants('london').show('t') + {0.0} + {1.0} + {1.0} + {1.5} + {2.0} + {3.0} + {3.0} + {4.0} + {4.5} + {4.5} + {5.0} + {6.0} + {7.0} + {7.0} + {8.0} + {9.0} + {9.0} + {10.0} + If the streams contain parts and measures, the merge function will iterate + through them and determine + and store variant differences within each measure/part. - >>> badnessDict = {} - >>> listDict = {} >>> stream1 = stream.Stream() >>> stream2 = stream.Stream() - >>> data1M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), ... ('a', 'quarter'), ('a', 'quarter')] >>> data1M2 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ... ('a', 'quarter'),('b', 'quarter')] >>> data1M3 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')] - - >>> data2M1 = [('a', 'quarter'), ('b', 'quarter'), ('c', 'quarter'), ('g#', 'quarter')] + >>> data2M1 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter')] >>> data2M2 = [('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), ... ('a', 'quarter'), ('b', 'quarter')] >>> data2M3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] - >>> data2M4 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] >>> data1 = [data1M1, data1M2, data1M3] - >>> data2 = [data2M1, data2M2, data2M3, data2M4] + >>> data2 = [data2M1, data2M2, data2M3] + >>> tempPart = stream.Part() >>> for d in data1: ... m = stream.Measure() ... for pitchName, durType in d: ... n = note.Note(pitchName) ... n.duration.type = durType ... m.append(n) - ... stream1.append(m) + ... tempPart.append(m) + >>> stream1.append(tempPart) + >>> tempPart = stream.Part() >>> for d in data2: ... m = stream.Measure() ... for pitchName, durType in d: ... n = note.Note(pitchName) ... n.duration.type = durType ... m.append(n) - ... stream2.append(m) - >>> kList, kBadness = variant._getBestListAndScore(stream1, stream2, - ... badnessDict, listDict, isNone=False) - >>> kList - [0, 1, 2, 'addedBar'] - ''' - # Initialize 'Best' Values for maximizing algorithm - bestScore = 1 - bestNormalizedScore = 1 - bestList = [] + ... tempPart.append(m) + >>> stream2.append(tempPart) + >>> mergedStreams = variant.mergeVariantsEqualDuration([stream1, stream2], ['paris']) + >>> mergedStreams.show('t') + {0.0} + {0.0} + {0.0} + {1.0} + {1.0} + {1.5} + {2.0} + {3.0} + {3.0} + {4.0} + {0.0} + {0.5} + {0.5} + {1.0} + {2.0} + {3.0} + {8.0} + {0.0} + {1.0} + {1.0} + {2.0} + {3.0} + >>> #_DOCS_SHOW mergedStreams.show() - # Base Cases: - if streamYIndex >= len(streamY): - listDict[(streamXIndex, streamYIndex, isNone)] = [] - badnessDict[(streamXIndex, streamYIndex, isNone)] = 0.0 - return [], 0 - # Query Dict for existing results - if (streamXIndex, streamYIndex, isNone) in badnessDict: - badness = badnessDict[(streamXIndex, streamYIndex, isNone)] - bestList = listDict[(streamXIndex, streamYIndex, isNone)] - return bestList, badness + .. image:: images/variant_measuresAndParts.* + :width: 600 - # Get salient similarity score - if streamXIndex == -1 and streamYIndex == -1: - simScore = 0 - elif isNone: - simScore = 0.5 - else: - simScore = _diffScore(streamX[streamXIndex], streamY[streamYIndex]) + >>> for p in mergedStreams.getElementsByClass('Part'): + ... for m in p.getElementsByClass(stream.Measure): + ... m.activateVariants('paris', inPlace=True) + >>> mergedStreams.show('t') + {0.0} + {0.0} + {0.0} + {1.0} + {1.0} + {2.0} + {3.0} + {3.0} + {4.0} + {0.0} + {0.5} + {0.5} + {1.5} + {2.0} + {3.0} + {8.0} + {0.0} + {1.0} + {1.0} + {2.0} + {3.0} + >>> #_DOCS_SHOW mergedStreams.show() - # Check the added bar case: - kList, kBadness = _getBestListAndScore(streamX, streamY, badnessDict, listDict, - isNone=True, streamXIndex=streamXIndex, streamYIndex=streamYIndex + 1) - if kList is None: - kList = [] - if kList: - normalizedBadness = kBadness / len(kList) - else: - normalizedBadness = 0 - if normalizedBadness <= bestNormalizedScore: - bestScore = kBadness - bestNormalizedScore = normalizedBadness - bestList = kList + .. image:: images/variant_measuresAndParts2.* + :width: 600 - # Check the other cases - for k in range(streamXIndex + 1, len(streamX)): - kList, kBadness = _getBestListAndScore(streamX, streamY, badnessDict, - listDict, isNone=False, - streamXIndex=k, streamYIndex=streamYIndex + 1) - if kList is None: - kList = [] - if kList: - normalizedBadness = kBadness / len(kList) - else: - normalizedBadness = 0 + If barlines do not match up, an exception will be thrown. Here two streams that are identical + are merged, except one is in 3/4, the other in 4/4. This throws an exception. - if normalizedBadness <= bestNormalizedScore: - bestScore = kBadness - bestNormalizedScore = normalizedBadness - bestList = kList + >>> streamDifferentMeasures = stream.Stream() + >>> dataDiffM1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter')] + >>> dataDiffM2 = [ ('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter')] + >>> dataDiffM3 = [('a', 'quarter'), ('b', 'quarter'), ('c', 'quarter')] + >>> dataDiffM4 = [('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')] + >>> dataDiff = [dataDiffM1, dataDiffM2, dataDiffM3, dataDiffM4] + >>> streamDifferentMeasures.insert(0.0, meter.TimeSignature('3/4')) + >>> tempPart = stream.Part() + >>> for d in dataDiff: + ... m = stream.Measure() + ... for pitchName, durType in d: + ... n = note.Note(pitchName) + ... n.duration.type = durType + ... m.append(n) + ... tempPart.append(m) + >>> streamDifferentMeasures.append(tempPart) + >>> mergedStreams = variant.mergeVariantsEqualDuration( + ... [stream1, streamDifferentMeasures], ['paris']) + Traceback (most recent call last): + music21.variant.VariantException: _mergeVariants cannot merge streams + which are of different lengths + ''' - # Prepare and Return Results - returnList = copy.deepcopy(bestList) - if isNone: - returnList.insert(0, 'addedBar') - elif streamXIndex == -1: - pass + if inPlace is True: + returnObj = streams[0] else: - returnList.insert(0, streamXIndex) - badness = bestScore + simScore - - badnessDict[(streamXIndex, streamYIndex, isNone)] = badness - listDict[(streamXIndex, streamYIndex, isNone)] = returnList - return returnList, badness - - -def _diffScore(measureX, measureY): - ''' - Helper function for _getBestListAndScore which compares two measures and returns a value - associated with their similarity. The higher the normalized (0, 1) value the poorer the match. - This should be calibrated such that the value that appears in _getBestListAndScore for - isNone is true (i.e. testing when a bar does not associate with any existing bars the reference - stream), is well-matched with the similarity scores generated by this function. + returnObj = streams[0].coreCopyAsDerivation('mergeVariantsEqualDuration') + # Adds a None element at beginning (corresponding to default variant streams[0]) + variantNames.insert(0, None) + while len(streams) > len(variantNames): # Adds Blank names if too few + variantNames.append(None) + while len(streams) < len(variantNames): # Removes extra names + variantNames.pop() - >>> m1 = stream.Measure() - >>> m2 = stream.Measure() - >>> m1.append([note.Note('e'), note.Note('f'), note.Note('g'), note.Note('a')]) - >>> m2.append([note.Note('e'), note.Note('f'), note.Note('g#'), note.Note('a')]) - >>> variant._diffScore(m1, m2) - 0.4... + zipped = list(zip(streams, variantNames)) - ''' - hashes = getMeasureHashes([measureX, measureY]) - if hashes[0] == hashes[1]: - baseValue = 0.0 - else: - baseValue = 0.4 + for s, variantName in zipped[1:]: + if returnObj.highestTime != s.highestTime: + raise VariantException('cannot merge streams of different lengths') - numberDelta = measureX.number - measureY.number + returnObjParts = returnObj.getElementsByClass('Part') + if returnObjParts: # If parts exist, iterate through them. + sParts = s.getElementsByClass('Part') + for i, returnObjPart in enumerate(returnObjParts): + sPart = sParts[i] - distanceModifier = float(numberDelta) * 0.001 + returnObjMeasures = returnObjPart.getElementsByClass(stream.Measure) + if returnObjMeasures: + # If measures exist and parts exist, iterate through them both. + for j, returnObjMeasure in enumerate(returnObjMeasures): + sMeasure = sPart.getElementsByClass(stream.Measure)[j] + _mergeVariants( + returnObjMeasure, sMeasure, variantName=variantName, inPlace=True) + else: # If parts exist but no measures. + _mergeVariants(returnObjPart, sPart, variantName=variantName, inPlace=True) + else: + returnObjMeasures = returnObj.getElementsByClass(stream.Measure) + if returnObjMeasures: # If no parts, but still measures, iterate through them. + for j, returnObjMeasure in enumerate(returnObjMeasures): + sMeasure = s.getElementsByClass(stream.Measure)[j] + _mergeVariants(returnObjMeasure, sMeasure, + variantName=variantName, inPlace=True) + else: # If no parts and no measures. + _mergeVariants(returnObj, s, variantName=variantName, inPlace=True) - return baseValue + distanceModifier + return returnObj -def _getRegionsFromStreams(streamX, streamY): +def mergePartAsOssia(mainPart, ossiaPart, ossiaName, + inPlace=False, compareByMeasureNumber=False, recurseInMeasures=False): # noinspection PyShadowingNames ''' - Takes in two streams, returns a list of 5-tuples via difflib.get_opcodes() - working on measure differences. - + Some MusicXML files are generated with full parts that have only a few non-rest measures + instead of ossia parts, such as those + created by Sibelius 7. This function + takes two streams (mainPart and ossiaPart), the second interpreted as an ossia. + It outputs a stream with the ossia part merged into the stream as a + group of variants. - >>> s1 = converter.parse("tinynotation: 2/4 d4 e8. f16 GG4 b'4 b-2 c4 d8. e16 FF4 a'4 b-2") + If compareByMeasureNumber is True, then the ossia measures will be paired with the + measures in the mainPart that have the + same measure.number. Otherwise, they will be paired by offset. In most cases + these should have the same result. - *0:Eq *1:Rep * *3:Eq *6:In + Note that this method has no way of knowing if a variant is supposed to be a + different duration than the segment of stream which it replaces + because that information is not contained in the format of score this method is + designed to deal with. - >>> s2 = converter.parse("tinynotation: 2/4 d4 e8. f16 FF4 b'4 c4 d8. e16 FF4 a'4 b-2 b-2") - >>> s1m = s1.makeMeasures() - >>> s2m = s2.makeMeasures() - >>> regions = variant._getRegionsFromStreams(s1m, s2m) - >>> regions - [('equal', 0, 1, 0, 1), - ('replace', 1, 3, 1, 2), - ('equal', 3, 6, 2, 5), - ('insert', 6, 6, 5, 6)] - ''' - hashesX = getMeasureHashes(streamX) - hashesY = getMeasureHashes(streamY) - sm = difflib.SequenceMatcher() - sm.set_seqs(hashesX, hashesY) - regions = sm.get_opcodes() - return regions + >>> mainStream = converter.parse('tinynotation: 4/4 A4 B4 C4 D4 E1 F2 E2 E8 F8 F4 G2 G2 G4 F4 F4 F4 F4 F4 G1 ') + >>> ossiaStream = converter.parse('tinynotation: 4/4 r1 r1 r1 E4 E4 F4 G4 r1 F2 F2 r1 ') + >>> mainStream.makeMeasures(inPlace=True) + >>> ossiaStream.makeMeasures(inPlace=True) + >>> mainPart = stream.Part() + >>> for m in mainStream: + ... mainPart.insert(m.offset, m) + >>> ossiaPart = stream.Part() + >>> for m in ossiaStream: + ... ossiaPart.insert(m.offset, m) -def _mergeVariants(streamA, streamB, *, variantName=None, inPlace=False): - ''' - This is a helper function for mergeVariantsEqualDuration which takes two streams - (which cannot contain container - streams like measures and parts) and merges the second into the first via variant objects. - If the first already contains variant objects, containsVariants should be set to true and the - function will compare streamB to the streamA as well as the - variant streams contained in streamA. - Note that variant streams in streamB will be ignored and lost. + >>> s = stream.Stream() + >>> s.insert(0.0, ossiaPart) + >>> s.insert(0.0, mainPart) + >>> #_DOCS_SHOW s.show() + >>> mainPartWithOssiaVariantsFT = variant.mergePartAsOssia(mainPart, ossiaPart, + ... ossiaName='Parisian_Variant', + ... inPlace=False, + ... compareByMeasureNumber=False, + ... recurseInMeasures=True) + >>> mainPartWithOssiaVariantsTT = variant.mergePartAsOssia(mainPart, ossiaPart, + ... ossiaName='Parisian_Variant', + ... inPlace=False, + ... compareByMeasureNumber=True, + ... recurseInMeasures=True) + >>> mainPartWithOssiaVariantsFF = variant.mergePartAsOssia(mainPart, ossiaPart, + ... ossiaName='Parisian_Variant', + ... inPlace=False, + ... compareByMeasureNumber=False, + ... recurseInMeasures=False) + >>> mainPartWithOssiaVariantsTF = variant.mergePartAsOssia(mainPart, ossiaPart, + ... ossiaName='Parisian_Variant', + ... inPlace=False, + ... compareByMeasureNumber=True, + ... recurseInMeasures=False) - >>> stream1 = stream.Stream() - >>> stream2 = stream.Stream() - >>> data1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), - ... ('a', 'quarter'), ('a', 'quarter'), - ... ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'), - ... ('b', 'quarter'), ('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter')] - >>> data2 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter'), - ... ('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), ('a', 'quarter'), - ... ('b', 'quarter'), ('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter')] - >>> for pitchName, durType in data1: - ... n = note.Note(pitchName) - ... n.duration.type = durType - ... stream1.append(n) - >>> for pitchName, durType in data2: - ... n = note.Note(pitchName) - ... n.duration.type = durType - ... stream2.append(n) - >>> mergedStreams = variant._mergeVariants(stream1, stream2, variantName='paris') - >>> mergedStreams.show('t') - {0.0} - {1.0} - {1.0} - {1.5} - {2.0} - {3.0} - {3.0} - {4.0} - {4.5} - {4.5} - {5.0} - {6.0} - {7.0} - {8.0} - {9.0} - {9.0} - {10.0} + >>> mainPartWithOssiaVariantsFT.show('text') == mainPartWithOssiaVariantsTT.show('text') + {0.0} >> mergedStreams.activateVariants('paris').show('t') - {0.0} - {1.0} - {1.0} - {2.0} - {3.0} - {3.0} - {4.0} - {4.5} - {4.5} - {5.5} - {6.0} - {7.0} - {8.0} - {9.0} - {9.0} - {10.0} + >>> mainPartWithOssiaVariantsFF.show('text') == mainPartWithOssiaVariantsFT.show('text') + {0.0} >> stream1.append(note.Note('e')) - >>> mergedStreams = variant._mergeVariants(stream1, stream2, variantName=['paris']) - Traceback (most recent call last): - music21.variant.VariantException: _mergeVariants cannot merge streams - which are of different lengths - ''' - # TODO: Add the feature for merging a stream to a stream with existing variants - # (it has to compare against both the stream and the contained variant) - if (streamA.getElementsByClass(stream.Measure) - or streamA.getElementsByClass('Part') - or streamB.getElementsByClass(stream.Measure) - or streamB.getElementsByClass('Part')): - raise VariantException( - '_mergeVariants cannot merge streams which contain measures or parts.' - ) + >>> mainPartWithOssiaVariantsFT.show('text') + {0.0} + ... + {12.0} + {0.0} + {0.0} + {0.5} + {1.0} + {2.0} + {16.0} + ... + {20.0} + {0.0} + {0.0} + {1.0} + {2.0} + {3.0} + ... - if streamA.highestTime != streamB.highestTime: - raise VariantException( - '_mergeVariants cannot merge streams which are of different lengths' - ) + >>> mainPartWithOssiaVariantsFF.activateVariants('Parisian_Variant').show('text') + {0.0} + ... + {12.0} + {12.0} + {0.0} + {1.0} + {2.0} + {3.0} + {16.0} + ... + {20.0} + {20.0} + {0.0} + {2.0} + ... + ''' if inPlace is True: - returnObj = streamA + returnObj = mainPart else: - returnObj = copy.deepcopy(streamA) - - i = 0 - j = 0 - inVariant = False - streamANotes = streamA.flatten().notesAndRests - streamBNotes = streamB.flatten().notesAndRests - - noteBuffer = [] - variantStart = 0.0 + returnObj = mainPart.coreCopyAsDerivation('mergePartAsOssia') - while i < len(streamANotes) and j < len(streamBNotes): - if i == len(streamANotes): - i = len(streamANotes) - 1 - if j == len(streamBNotes): - break - if (streamANotes[i].getOffsetBySite(streamA.flatten()) - == streamBNotes[j].getOffsetBySite(streamB.flatten())): - # Comparing Notes at same offset - # TODO: Will not work until __eq__ overwritten for Generalized Notes - if streamANotes[i] != streamBNotes[j]: - # If notes are different, start variant if not started and append note. - if inVariant is False: - variantStart = streamBNotes[j].getOffsetBySite(streamB.flatten()) - inVariant = True - noteBuffer = [] - noteBuffer.append(streamBNotes[j]) + if compareByMeasureNumber is True: + for ossiaMeasure in ossiaPart.getElementsByClass(stream.Measure): + if ossiaMeasure.notes: # If the measure is not just rests + ossiaNumber = ossiaMeasure.number + returnMeasure = returnObj.measure(ossiaNumber) + if recurseInMeasures is True: + mergeVariantsEqualDuration( + [returnMeasure, ossiaMeasure], + [ossiaName], + inPlace=True + ) else: - noteBuffer.append(streamBNotes[j]) - else: # If notes are the same, end and insert variant if in variant. - if inVariant is True: - returnObj.insert( - variantStart, - _generateVariant( - noteBuffer, - streamB, - variantStart, - variantName - ) + ossiaOffset = returnMeasure.getOffsetBySite(returnObj) + addVariant(returnObj, + ossiaOffset, + ossiaMeasure, + variantName=ossiaName, + variantGroups=None, + replacementDuration=None + ) + else: + for ossiaMeasure in ossiaPart.getElementsByClass(stream.Measure): + if ossiaMeasure.notes: # If the measure is not just rests + ossiaOffset = ossiaMeasure.getOffsetBySite(ossiaPart) + if recurseInMeasures is True: + returnMeasure = returnObj.getElementsByOffset( + ossiaOffset + ).getElementsByClass(stream.Measure).first() + mergeVariantsEqualDuration( + [returnMeasure, ossiaMeasure], + [ossiaName], + inPlace=True ) - inVariant = False - noteBuffer = [] else: - inVariant = False - - i += 1 - j += 1 - continue - - elif (streamANotes[i].getOffsetBySite(streamA.flatten()) - > streamBNotes[j].getOffsetBySite(streamB.flatten())): - if inVariant is False: - variantStart = streamBNotes[j].getOffsetBySite(streamB.flatten()) - noteBuffer = [] - noteBuffer.append(streamBNotes[j]) - inVariant = True - else: - noteBuffer.append(streamBNotes[j]) - j += 1 - continue - - else: # Less-than - i += 1 - continue - - if inVariant is True: # insert final variant if exists - returnObj.insert( - variantStart, - _generateVariant( - noteBuffer, - streamB, - variantStart, - variantName - ) - ) - inVariant = False - noteBuffer = [] + addVariant(returnObj, ossiaOffset, ossiaMeasure, + variantName=ossiaName, variantGroups=None, replacementDuration=None) if inPlace is True: - return None + return else: return returnObj -def _generateVariant(noteList, originStream, start, variantName=None): +# ------ Public Helper Functions + +def addVariant( + s: stream.Stream, + startOffset: Union[int, float], + sVariant: Union[stream.Stream, Variant], + variantName=None, + variantGroups=None, + replacementDuration=None +): # noinspection PyShadowingNames ''' - Helper function for mergeVariantsEqualDuration which takes a list of - consecutive notes from a stream and returns - a variant object containing the notes from the list at the offsets - derived from their original context. + Takes a stream, the location of the variant to be added to + that stream (startOffset), the content of the + variant to be added (sVariant), and the duration of the section of the stream which the variant + replaces (replacementDuration). - >>> originStream = stream.Stream() - >>> data = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), - ... ('a', 'quarter'), ('a', 'quarter'), - ... ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'), - ... ('b', 'quarter'), ('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter')] - >>> for pitchName, durType in data: + If replacementDuration is 0, + this is an insertion. If sVariant is + None, this is a deletion. + + + >>> data1M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), + ... ('a', 'quarter'), ('a', 'quarter')] + >>> data1M3 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')] + >>> data1M2 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), + ... ('a', 'quarter'),('b', 'quarter')] + >>> data1 = [data1M1, data1M2, data1M3] + >>> tempPart = stream.Part() + >>> stream1 = stream.Stream() + >>> for d in data1: + ... m = stream.Measure() + ... for pitchName, durType in d: + ... n = note.Note(pitchName) + ... n.duration.type = durType + ... m.append(n) + ... stream1.append(m) + + >>> data2M2 = [('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), + ... ('a', 'quarter'), ('b', 'quarter')] + >>> stream2 = stream.Stream() + >>> m = stream.Measure() + >>> for pitchName, durType in data2M2: ... n = note.Note(pitchName) ... n.duration.type = durType - ... originStream.append(n) - >>> noteList = [] - >>> for n in originStream.notes[2:5]: - ... noteList.append(n) - >>> start = originStream.notes[2].offset - >>> variantName='paris' - >>> v = variant._generateVariant(noteList, originStream, start, variantName) - >>> v.show('text') - {0.0} - {0.5} - {1.5} - - >>> v.groups - ['paris'] + ... m.append(n) + >>> stream2.append(m) + >>> variant.addVariant(stream1, 4.0, stream2, + ... variantName='rhythmic_switch', replacementDuration=4.0) + >>> stream1.show('text') + {0.0} + {0.0} + {1.0} + {1.5} + {2.0} + {3.0} + {4.0} + {4.0} + {0.0} + {0.5} + {1.0} + {2.0} + {3.0} + {8.0} + {0.0} + {1.0} + {2.0} + {3.0} + >>> stream1 = stream.Stream() + >>> stream1.repeatAppend(note.Note('e'), 6) + >>> variant1 = variant.Variant() + >>> variant1.repeatAppend(note.Note('f'), 3) + >>> startOffset = 3.0 + >>> variant.addVariant(stream1, startOffset, variant1, + ... variantName='paris', replacementDuration=3.0) + >>> stream1.show('text') + {0.0} + {1.0} + {2.0} + {3.0} + {3.0} + {4.0} + {5.0} ''' - returnVariant = Variant() - for n in noteList: - returnVariant.insert(n.getOffsetBySite(originStream.flatten()) - start, n) + tempVariant = Variant() + + if variantGroups is not None: + tempVariant.groups = variantGroups if variantName is not None: - returnVariant.groups.append(variantName) - return returnVariant + tempVariant.groups.append(variantName) + tempVariant.replacementDuration = replacementDuration -# ------- Variant Manipulation Methods -def makeAllVariantsReplacements(streamWithVariants, - variantNames=None, - inPlace=False, - recurse=False): - # noinspection PyShadowingNames,GrazieInspection + if sVariant is None: # deletion + pass + else: # replacement or insertion + if isinstance(sVariant, stream.Measure): # sVariant is a measure put it in a variant and insert. + tempVariant.append(sVariant) + else: # sVariant is not a measure + sVariantMeasures = sVariant.getElementsByClass(stream.Measure) + if not sVariantMeasures: # If there are no measures, work element-wise + for e in sVariant: + offset = e.getOffsetBySite(sVariant) + startOffset + tempVariant.insert(offset, e) + else: # if there are measures work measure-wise + for m in sVariantMeasures: + tempVariant.append(m) + + s.insert(startOffset, tempVariant) + + + +def refineVariant(s, sVariant, *, inPlace=False): + # noinspection PyShadowingNames ''' - This function takes a stream and a list of variantNames - (default works on all variants), and changes all insertion - (elongations with replacementDuration 0) - and deletion variants (with containedHighestTime 0) into variants with non-zero - replacementDuration and non-null elements - by adding measures on the front of insertions and measures on the end - of deletions. This is designed to make it possible to format all variants in a - readable way as a graphical ossia (via lilypond). If inPlace is True - it will perform this action on the stream itself; otherwise it will return a - modified copy. If recurse is True, this - method will work on variants within container objects within the stream (like parts). + Given a stream and variant contained in that stream, returns a + stream with that variant 'refined.' - >>> # * * * - >>> s = converter.parse("tinynotation: 4/4 d4 e4 f4 g4 a2 b-4 a4 g4 a8 g8 f4 e4 d2 a2 d4 e4 f4 g4 a2 b-4 a4 g4 a8 b-8 c'4 c4 f1") - >>> s2 = converter.parse("tinynotation: 4/4 d4 e4 f4 g4 a2. b-8 a8 g4 a8 g8 f4 e4 d2 a2 d4 f4 a2 d4 f4 AA2 d4 e4 f4 g4 g4 a8 b-8 c'4 c4 f1") - >>> # replacement insertion deletion - >>> s.makeMeasures(inPlace=True) - >>> s2.makeMeasures(inPlace=True) - >>> variant.mergeVariants(s, s2, variantName='london', inPlace=True) + It is refined in the sense that, (with the best estimates) measures which have been determined + to be related are merged within the measure. - >>> newStream = stream.Score(s) + Suppose a four-bar phrase in a piece is a slightly + different five-bar phrase in a variant. In the variant, every F# has been replaced by an F, + and the last bar is repeated. Given these streams, mergeVariantMeasureStreams would return + the first stream with a single variant object containing the entire 5 bars of the variant. + Calling refineVariant on this stream and that variant object would result in a variant object + in the measures for each F#/F pair, and a variant object containing the added bar at the end. + For a more detailed explanation of how similar measures are properly associated with each other + look at the documentation for _getBestListAndScore - >>> returnStream = variant.makeAllVariantsReplacements(newStream, recurse=False) - >>> for v in returnStream.parts[0][variant.Variant]: - ... (v.offset, v.lengthType, v.replacementDuration) - (4.0, 'replacement', 4.0) - (16.0, 'elongation', 0.0) - (20.0, 'deletion', 4.0) + Note that this code does not work properly yet. - >>> returnStream = variant.makeAllVariantsReplacements( - ... newStream, variantNames=['france'], recurse=True) - >>> for v in returnStream.parts[0][variant.Variant]: - ... (v.offset, v.lengthType, v.replacementDuration) - (4.0, 'replacement', 4.0) - (16.0, 'elongation', 0.0) - (20.0, 'deletion', 4.0) - >>> variant.makeAllVariantsReplacements(newStream, recurse=True, inPlace=True) - >>> for v in newStream.parts[0][variant.Variant]: - ... (v.offset, v.lengthType, v.replacementDuration, v.containedHighestTime) - (4.0, 'replacement', 4.0, 4.0) - (12.0, 'elongation', 4.0, 12.0) - (20.0, 'deletion', 8.0, 4.0) + >>> v = variant.Variant() + >>> variantDataM1 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), + ... ('a', 'quarter'),('b', 'quarter')] + >>> variantDataM2 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')] + >>> variantData = [variantDataM1, variantDataM2] + >>> for d in variantData: + ... m = stream.Measure() + ... for pitchName, durType in d: + ... n = note.Note(pitchName) + ... n.duration.type = durType + ... m.append(n) + ... v.append(m) + >>> v.groups = ['paris'] + >>> v.replacementDuration = 8.0 + + >>> s = stream.Stream() + >>> streamDataM1 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter')] + >>> streamDataM2 = [('b', 'eighth'), ('c', 'quarter'), + ... ('a', 'eighth'), ('a', 'quarter'), ('b', 'quarter')] + >>> streamDataM3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] + >>> streamDataM4 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] + >>> streamData = [streamDataM1, streamDataM2, streamDataM3, streamDataM4] + >>> for d in streamData: + ... m = stream.Measure() + ... for pitchName, durType in d: + ... n = note.Note(pitchName) + ... n.duration.type = durType + ... m.append(n) + ... s.append(m) + >>> s.insert(4.0, v) + + >>> variant.refineVariant(s, v, inPlace=True) + >>> s.show('text') + {0.0} + {0.0} + {1.0} + {2.0} + {3.0} + {4.0} + {0.0} + {0.5} + {0.5} + {1.5} + {2.0} + {3.0} + {8.0} + {0.0} + {1.0} + {1.0} + {2.0} + {3.0} + {12.0} + {0.0} + {1.0} + {2.0} + {3.0} ''' + # stream that will be returned + if sVariant not in s.getElementsByClass(Variant): + raise VariantException(f'{sVariant} not found in stream {s}.') if inPlace is True: - returnStream = streamWithVariants + returnObject = s + variantRegion = sVariant else: - returnStream = copy.deepcopy(streamWithVariants) + sVariantIndex = s.getElementsByClass(Variant).index(sVariant) - if recurse is True: - for s in returnStream.recurse(streamsOnly=True): - _doVariantFixingOnStream(s, variantNames=variantNames) - else: - _doVariantFixingOnStream(returnStream, variantNames=variantNames) + returnObject = s.coreCopyAsDerivation('refineVariant') + variantRegion = returnObject.getElementsByClass(Variant)(sVariantIndex) - if inPlace is True: - return - else: - return returnStream + # useful parameters from variant and its location + variantGroups = sVariant.groups + replacementDuration = sVariant.replacementDuration + startOffset = sVariant.getOffsetBySite(s) + # endOffset = replacementDuration + startOffset + # region associated with the given variant in the stream + returnRegion = variantRegion.replacedElements(returnObject) -def _doVariantFixingOnStream(s, variantNames=None): - # noinspection PyShadowingNames,GrazieInspection - ''' - This is a helper function for makeAllVariantsReplacements. - It iterates through the appropriate variants - and performs the variant changing operation to eliminate strict deletion and insertion variants. + # associating measures in variantRegion to those in returnRegion -> + # This is done via 0 indexed lists corresponding to measures + returnRegionMeasureList = list(range(len(returnRegion))) + badnessDict = {} + listDict = {} + variantMeasureList, unused_badness = _getBestListAndScore(returnRegion, + variantRegion, + badnessDict, + listDict) - >>> # * * * * * - >>> s = converter.parse("tinynotation: 4/4 d4 e4 f4 g4 a2 b-4 a4 g4 a8 g8 f4 e4 d2 a2 d4 e4 f4 g4 a2 b-4 a4 g4 a8 b-8 c'4 c4 f1 ", makeNotation=False) - >>> s2 = converter.parse("tinynotation: 4/4 a4 b c d d4 e4 f4 g4 a2. b-8 a8 g4 a8 g8 f4 e4 d2 a2 d4 f4 a2 d4 f4 AA2 d4 e4 f4 g4 g4 a8 b-8 c'4 c4 ", makeNotation=False) - >>> # initial insertion replacement insertion deletion final deletion - >>> s.makeMeasures(inPlace=True) - >>> s2.makeMeasures(inPlace=True) - >>> variant.mergeVariants(s, s2, variantName='london', inPlace=True) + # badness is a measure of how different the streams are. + # The list returned, variantMeasureList, minimizes that quantity. - >>> variant._doVariantFixingOnStream(s, 'london') - >>> s.show('text') - {0.0} - {0.0} - ... - {4.0} - {4.0} - ... - {12.0} - {12.0} - ... - {20.0} - {20.0} - ... - {24.0} - {24.0} - ... + # mentioned lists are compared via difflib for optimal edit regions + # (equal, delete, insert, replace) + sm = difflib.SequenceMatcher() + sm.set_seqs(returnRegionMeasureList, variantMeasureList) + regions = sm.get_opcodes() - >>> for v in s[variant.Variant]: - ... (v.offset, v.lengthType, v.replacementDuration) - (0.0, 'elongation', 4.0) - (4.0, 'replacement', 4.0) - (12.0, 'elongation', 4.0) - (20.0, 'deletion', 8.0) - (24.0, 'deletion', 8.0) + # each region is processed for variants. + for regionType, returnStart, returnEnd, variantStart, variantEnd in regions: + startOffset = returnRegion[returnStart].getOffsetBySite(returnRegion) + # endOffset = (returnRegion[returnEnd-1].getOffsetBySite(returnRegion) + + # returnRegion[returnEnd-1].duration.quarterLength) + variantSubRegion = None + if regionType == 'equal': + returnSubRegion = returnRegion.measures(returnStart + 1, returnEnd) + variantSubRegion = variantRegion.measures(variantStart + 1, variantEnd) + mergeVariantsEqualDuration( + [returnSubRegion, variantSubRegion], + variantGroups, + inPlace=True + ) + continue + elif regionType == 'replace': + returnSubRegion = returnRegion.measures(returnStart + 1, returnEnd) + replacementDuration = returnSubRegion.duration.quarterLength + variantSubRegion = variantRegion.measures(variantStart + 1, variantEnd) + elif regionType == 'delete': + returnSubRegion = returnRegion.measures(returnStart + 1, returnEnd) + replacementDuration = returnSubRegion.duration.quarterLength + variantSubRegion = None + elif regionType == 'insert': + variantSubRegion = variantRegion.measures(variantStart + 1, variantEnd) + replacementDuration = 0.0 + else: + raise VariantException(f'Unknown regionType {regionType!r}') + addVariant(returnRegion, + startOffset, + variantSubRegion, + variantGroups=variantGroups, + replacementDuration=replacementDuration + ) - This also works on streams with variants that contain notes and rests rather than measures. + # The original variant object has been replaced by more refined + # variant objects and so should be deleted. + returnObject.remove(variantRegion) - >>> s = converter.parse('tinyNotation: 4/4 e4 b b b f4 f f f g4 a a a ', makeNotation=False) - >>> v1Stream = converter.parse('tinyNotation: 4/4 a4 a a a ', makeNotation=False) - >>> # initial insertion deletion - >>> v1 = variant.Variant(v1Stream.notes) - >>> v1.replacementDuration = 0.0 - >>> v1.groups = ['london'] - >>> s.insert(0.0, v1) + if inPlace: + return None + else: + return returnObject - >>> v2 = variant.Variant() - >>> v2.replacementDuration = 4.0 - >>> v2.groups = ['london'] - >>> s.insert(4.0, v2) - >>> variant._doVariantFixingOnStream(s, 'london') - >>> for v in s[variant.Variant]: - ... (v.offset, v.lengthType, v.replacementDuration, v.containedHighestTime) - (0.0, 'elongation', 1.0, 5.0) - (4.0, 'deletion', 5.0, 1.0) +def _mergeVariantMeasureStreamsCarefully(streamX, streamY, variantName, *, inPlace=False): ''' + There seem to be some problems with this function, and it isn't well tested. + It is not recommended to use it at this time. - for v in s.getElementsByClass('Variant'): - if isinstance(variantNames, list): # If variantNames are controlled - if set(v.groups) and not set(variantNames): - # and if this variant is not in the controlled list - continue # then skip it - else: - continue # huh???? - lengthType = v.lengthType - replacementDuration = v.replacementDuration - highestTime = v.containedHighestTime - - if lengthType == 'elongation' and replacementDuration == 0.0: - variantType = 'insertion' - elif lengthType == 'deletion' and highestTime == 0.0: - variantType = 'deletion' - else: - continue - - if v.getOffsetBySite(s) == 0.0: - isInitial = True - isFinal = False - elif v.getOffsetBySite(s) + v.replacementDuration == s.duration.quarterLength: - isInitial = False - isFinal = True - else: - isInitial = False - isFinal = False - - # If a non-final deletion or an INITIAL insertion, - # add the next element after the variant. - if ((variantType == 'insertion' and (isInitial is True)) - or (variantType == 'deletion' and (isFinal is False))): - targetElement = _getNextElements(s, v) - - # Delete initial clefs, etc. from initial insertion targetElement if it exists - if isinstance(targetElement, stream.Stream): - # Must use .elements, because of removal of elements - for e in targetElement.elements: - if isinstance(e, (clef.Clef, meter.TimeSignature)): - targetElement.remove(e) - - v.append(copy.deepcopy(targetElement)) # Appends a copy + ''' + # stream that will be returned + if inPlace is True: + returnObject = streamX + variantObject = streamY + else: + returnObject = copy.deepcopy(streamX) + variantObject = copy.deepcopy(streamY) - # If a non-initial insertion or a FINAL deletion, - # add the previous element after the variant. - # #elif ((variantType == 'deletion' and (isFinal is True)) or - # (type == 'insertion' and (isInitial is False))): - else: - targetElement = _getPreviousElement(s, v) - newVariantOffset = targetElement.getOffsetBySite(s) - # Need to shift elements to make way for new element at front - offsetShift = targetElement.duration.quarterLength - for e in v.containedSite: - oldOffset = e.getOffsetBySite(v.containedSite) - e.setOffsetBySite(v.containedSite, oldOffset + offsetShift) - v.insert(0.0, copy.deepcopy(targetElement)) - s.remove(v) - s.insert(newVariantOffset, v) + # associating measures in variantRegion to those in returnRegion -> + # This is done via 0 indexed lists corresponding to measures + returnObjectMeasureList = list(range(len(returnObject.getElementsByClass(stream.Measure)))) + badnessDict = {} + listDict = {} + variantObjectMeasureList, unused_badness = _getBestListAndScore( + returnObject.getElementsByClass(stream.Measure), + variantObject.getElementsByClass(stream.Measure), + badnessDict, + listDict + ) - # Give it a new replacementDuration including the added element - oldReplacementDuration = v.replacementDuration - v.replacementDuration = oldReplacementDuration + targetElement.duration.quarterLength + # badness is a measure of how different the streams are. + # The list returned, variantMeasureList, minimizes that quantity. + # mentioned lists are compared via difflib for optimal edit regions + # (equal, delete, insert, replace) + sm = difflib.SequenceMatcher() + sm.set_seqs(returnObjectMeasureList, variantObjectMeasureList) + regions = sm.get_opcodes() -def _getNextElements(s, v, numberOfElements=1): - # noinspection PyShadowingNames, GrazieInspection - ''' - This is a helper function for makeAllVariantsReplacements() which returns the next element in s - of the type of elements found in the variant v so that it can be added to v. + # each region is processed for variants. + for regionType, returnStart, returnEnd, variantStart, variantEnd in regions: + startOffset = returnObject.measure(returnStart + 1).getOffsetBySite(returnObject) + if regionType == 'equal': + returnSubRegion = returnObject.measures(returnStart + 1, returnEnd) + variantSubRegion = variantObject.measures(variantStart + 1, variantEnd) + mergeVariantMeasureStreams( + returnSubRegion, + variantSubRegion, + variantName, + inPlace=True + ) + continue + elif regionType == 'replace': + returnSubRegion = returnObject.measures(returnStart + 1, returnEnd) + replacementDuration = returnSubRegion.duration.quarterLength + variantSubRegion = variantObject.measures(variantStart + 1, variantEnd) + elif regionType == 'delete': + returnSubRegion = returnObject.measures(returnStart + 1, returnEnd) + replacementDuration = returnSubRegion.duration.quarterLength + variantSubRegion = None + elif regionType == 'insert': + variantSubRegion = variantObject.measures(variantStart + 1, variantEnd) + replacementDuration = 0.0 + else: # pragma: no cover + raise VariantException(f'Unknown regionType: {regionType}') - >>> # * * - >>> s1 = converter.parse('tinyNotation: 4/4 b4 c d e f4 g a b d4 e f g ', makeNotation=False) - >>> s2 = converter.parse('tinyNotation: 4/4 e4 f g a b4 c d e d4 e f g ', makeNotation=False) - >>> # insertion deletion - >>> s1.makeMeasures(inPlace=True) - >>> s2.makeMeasures(inPlace=True) - >>> mergedStream = variant.mergeVariants(s1, s2, 'london') - >>> for v in mergedStream.getElementsByClass(variant.Variant): - ... returnElement = variant._getNextElements(mergedStream, v) - ... print(returnElement) - - + addVariant( + returnObject, + startOffset, + variantSubRegion, + variantGroups=[variantName], + replacementDuration=replacementDuration + ) - This also works on streams with variants that contain notes and rests rather than measures. + if not inPlace: + return returnObject - >>> s = converter.parse('tinyNotation: 4/4 e4 b b b f4 f f f g4 a a a ', makeNotation=False) - >>> v1Stream = converter.parse('tinyNotation: 4/4 a4 a a a ', makeNotation=False) - >>> # initial insertion - >>> v1 = variant.Variant(v1Stream.notes) - >>> v1.replacementDuration = 0.0 - >>> v1.groups = ['london'] - >>> s.insert(0.0, v1) - >>> v2 = variant.Variant() - >>> v2.replacementDuration = 4.0 - >>> v2.groups = ['london'] - >>> s.insert(4.0, v2) - >>> for v in s[variant.Variant]: - ... returnElement = variant._getNextElements(s, v) - ... print(returnElement) - - +def getMeasureHashes(s): + # noinspection PyShadowingNames ''' - replacedElements = v.replacedElements(s) - lengthType = v.lengthType - # Get class of elements in variant or replaced Region - if lengthType == 'elongation': - vClass = type(v.getElementsByClass(['Measure', 'Note', 'Rest']).first()) - if isinstance(vClass, note.GeneralNote): - vClass = note.GeneralNote + Takes in a stream containing measures and returns a list of hashes, + one for each measure. Currently + implemented with search.translateStreamToString() + + >>> s = converter.parse("tinynotation: 2/4 c4 d8. e16 FF4 a'4 b-2") + >>> sm = s.makeMeasures() + >>> hashes = variant.getMeasureHashes(sm) + >>> hashes + ['

K@<', ')PQP', 'FZ'] + ''' + hashes = [] + if isinstance(s, list): + for m in s: + hashes.append(search.translateStreamToString(m.notesAndRests)) + return hashes else: - vClass = type(replacedElements.getElementsByClass(['Measure', 'Note', 'Rest']).first()) - if isinstance(vClass, note.GeneralNote): - vClass = note.GeneralNote + for m in s.getElementsByClass(stream.Measure): + hashes.append(search.translateStreamToString(m.notesAndRests)) + return hashes - # Get next element in s after v which is of type vClass - if lengthType == 'elongation': - variantOffset = v.getOffsetBySite(s) - potentialTargets = s.getElementsByOffset(variantOffset, - offsetEnd=s.highestTime, - includeEndBoundary=True, - mustFinishInSpan=False, - mustBeginInSpan=True, - classList=[vClass]) - returnElement = potentialTargets.first() - else: - replacementDuration = v.replacementDuration - variantOffset = v.getOffsetBySite(s) - potentialTargets = s.getElementsByOffset(variantOffset + replacementDuration, - offsetEnd=s.highestTime, - includeEndBoundary=True, - mustFinishInSpan=False, - mustBeginInSpan=True, - classList=[vClass]) - returnElement = potentialTargets.first() +# ----- Private Helper Functions +def _getBestListAndScore(streamX, streamY, badnessDict, listDict, + isNone=False, streamXIndex=-1, streamYIndex=-1): + # noinspection PyShadowingNames + ''' + This is a recursive function which makes a map between two related streams of measures. + It is designed for streams of measures that contain few if any measures that are actually + identical and that have a different number of measures (within reason). For example, + if one stream has 10 bars of eighth notes and the second stream has the same ten bars + of eighth notes except with some dotted rhythms mixed in and the fifth bar is repeated. + The first, streamX, is the reference stream. This function returns a list of + integers with length len(streamY) which maps each measure of StreamY to the measure + in streamX it is most likely associated with. For example, if the returned list is + [0, 2, 3, 'addedBar', 4]. This indicates that streamY is most similar to streamX + after the second bar of streamX has been removed and a new bar inserted between + bars 4 and 5. Note that this list has measures 0-indexed. This function generates this map by + minimizing the difference or 'badness' for the sequence of measures on the whole as determined + by the helper function _simScore which compares measures for similarity. 'addedBar' appears + in the list where this function has determined that the bar appearing + in streamY does not have a counterpart in streamX anywhere and is an insertion. - return returnElement + >>> badnessDict = {} + >>> listDict = {} + >>> stream1 = stream.Stream() + >>> stream2 = stream.Stream() + >>> data1M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), + ... ('a', 'quarter'), ('a', 'quarter')] + >>> data1M2 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), + ... ('a', 'quarter'),('b', 'quarter')] + >>> data1M3 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')] -def _getPreviousElement(s, v): - # noinspection PyShadowingNames,GrazieInspection + >>> data2M1 = [('a', 'quarter'), ('b', 'quarter'), ('c', 'quarter'), ('g#', 'quarter')] + >>> data2M2 = [('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), + ... ('a', 'quarter'), ('b', 'quarter')] + >>> data2M3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] + >>> data2M4 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] + >>> data1 = [data1M1, data1M2, data1M3] + >>> data2 = [data2M1, data2M2, data2M3, data2M4] + >>> for d in data1: + ... m = stream.Measure() + ... for pitchName, durType in d: + ... n = note.Note(pitchName) + ... n.duration.type = durType + ... m.append(n) + ... stream1.append(m) + >>> for d in data2: + ... m = stream.Measure() + ... for pitchName, durType in d: + ... n = note.Note(pitchName) + ... n.duration.type = durType + ... m.append(n) + ... stream2.append(m) + >>> kList, kBadness = variant._getBestListAndScore(stream1, stream2, + ... badnessDict, listDict, isNone=False) + >>> kList + [0, 1, 2, 'addedBar'] ''' - This is a helper function for makeAllVariantsReplacements() which returns - the previous element in s - of the type of elements found in the variant v so that it can be added to v. - + # Initialize 'Best' Values for maximizing algorithm + bestScore = 1 + bestNormalizedScore = 1 + bestList = [] - >>> # * * - >>> s1 = converter.parse('tinyNotation: 4/4 a4 b c d b4 c d e f4 g a b ') - >>> s2 = converter.parse('tinyNotation: 4/4 a4 b c d e4 f g a b4 c d e ') - >>> # insertion deletion - >>> s1.makeMeasures(inPlace=True) - >>> s2.makeMeasures(inPlace=True) - >>> mergedStream = variant.mergeVariants(s1, s2, 'london') - >>> for v in mergedStream[variant.Variant]: - ... returnElement = variant._getPreviousElement(mergedStream, v) - ... print(returnElement) - - + # Base Cases: + if streamYIndex >= len(streamY): + listDict[(streamXIndex, streamYIndex, isNone)] = [] + badnessDict[(streamXIndex, streamYIndex, isNone)] = 0.0 + return [], 0 - This also works on streams with variants that contain notes and rests rather than measures. + # Query Dict for existing results + if (streamXIndex, streamYIndex, isNone) in badnessDict: + badness = badnessDict[(streamXIndex, streamYIndex, isNone)] + bestList = listDict[(streamXIndex, streamYIndex, isNone)] + return bestList, badness - >>> s = converter.parse('tinyNotation: 4/4 b4 b b a e4 b b b g4 e e e ', makeNotation=False) - >>> v1Stream = converter.parse('tinyNotation: 4/4 f4 f f f ', makeNotation=False) - >>> # insertion final deletion - >>> v1 = variant.Variant(v1Stream.notes) - >>> v1.replacementDuration = 0.0 - >>> v1.groups = ['london'] - >>> s.insert(4.0, v1) + # Get salient similarity score + if streamXIndex == -1 and streamYIndex == -1: + simScore = 0 + elif isNone: + simScore = 0.5 + else: + simScore = _diffScore(streamX[streamXIndex], streamY[streamYIndex]) - >>> v2 = variant.Variant() - >>> v2.replacementDuration = 4.0 - >>> v2.groups = ['london'] - >>> s.insert(8.0, v2) - >>> for v in s[variant.Variant]: - ... returnElement = variant._getPreviousElement(s, v) - ... print(returnElement) - - - ''' - replacedElements = v.replacedElements(s) - lengthType = v.lengthType - # Get class of elements in variant or replaced Region - foundStream = None - if lengthType == 'elongation': - foundStream = v.getElementsByClass(['Measure', 'Note', 'Rest']) + # Check the added bar case: + kList, kBadness = _getBestListAndScore(streamX, streamY, badnessDict, listDict, + isNone=True, streamXIndex=streamXIndex, streamYIndex=streamYIndex + 1) + if kList is None: + kList = [] + if kList: + normalizedBadness = kBadness / len(kList) else: - foundStream = replacedElements.getElementsByClass(['Measure', 'Note', 'Rest']) + normalizedBadness = 0 - if not foundStream: - raise VariantException('Cannot find any Measures, Notes, or Rests in variant') - vClass = type(foundStream[0]) - if isinstance(vClass, note.GeneralNote): - vClass = note.GeneralNote + if normalizedBadness <= bestNormalizedScore: + bestScore = kBadness + bestNormalizedScore = normalizedBadness + bestList = kList - # Get next element in s after v which is of type vClass - variantOffset = v.getOffsetBySite(s) - potentialTargets = s.getElementsByOffset( - 0.0, - offsetEnd=variantOffset, - includeEndBoundary=False, - mustFinishInSpan=False, - mustBeginInSpan=True, - ).getElementsByClass(vClass) - returnElement = potentialTargets.last() + # Check the other cases + for k in range(streamXIndex + 1, len(streamX)): + kList, kBadness = _getBestListAndScore(streamX, streamY, badnessDict, + listDict, isNone=False, + streamXIndex=k, streamYIndex=streamYIndex + 1) + if kList is None: + kList = [] + if kList: + normalizedBadness = kBadness / len(kList) + else: + normalizedBadness = 0 - return returnElement + if normalizedBadness <= bestNormalizedScore: + bestScore = kBadness + bestNormalizedScore = normalizedBadness + bestList = kList + # Prepare and Return Results + returnList = copy.deepcopy(bestList) + if isNone: + returnList.insert(0, 'addedBar') + elif streamXIndex == -1: + pass + else: + returnList.insert(0, streamXIndex) + badness = bestScore + simScore -# ------------------------------------------------------------------------------ -# classes + badnessDict[(streamXIndex, streamYIndex, isNone)] = badness + listDict[(streamXIndex, streamYIndex, isNone)] = returnList + return returnList, badness -class VariantException(exceptions21.Music21Exception): - pass +def _diffScore(measureX, measureY): + ''' + Helper function for _getBestListAndScore which compares two measures and returns a value + associated with their similarity. The higher the normalized (0, 1) value the poorer the match. + This should be calibrated such that the value that appears in _getBestListAndScore for + isNone is true (i.e. testing when a bar does not associate with any existing bars the reference + stream), is well-matched with the similarity scores generated by this function. -class Variant(base.Music21Object): + >>> m1 = stream.Measure() + >>> m2 = stream.Measure() + >>> m1.append([note.Note('e'), note.Note('f'), note.Note('g'), note.Note('a')]) + >>> m2.append([note.Note('e'), note.Note('f'), note.Note('g#'), note.Note('a')]) + >>> variant._diffScore(m1, m2) + 0.4... + ''' - A Music21Object that stores elements like a Stream, but does not - represent itself externally to a Stream; i.e., the contents of a Variant are not flattened. + hashes = getMeasureHashes([measureX, measureY]) + if hashes[0] == hashes[1]: + baseValue = 0.0 + else: + baseValue = 0.4 - This is accomplished not by subclassing, but by object composition: similar to the Spanner, - the Variant contains a Stream as a private attribute. Calls to this Stream, for the Variant, - are automatically delegated by use of the __getattr__ method. Special cases are overridden - or managed as necessary: e.g., the Duration of a Variant is generally always zero. + numberDelta = measureX.number - measureY.number - To use Variants from a Stream, see the :func:`~music21.stream.Stream.activateVariants` method. + distanceModifier = float(numberDelta) * 0.001 - >>> v = variant.Variant() - >>> v.repeatAppend(note.Note(), 8) - >>> len(v.notes) - 8 - >>> v.highestTime - 0.0 - >>> v.containedHighestTime - 8.0 + return baseValue + distanceModifier - >>> v.duration # handled by Music21Object - - >>> v.isStream - False - >>> s = stream.Stream() - >>> s.append(v) - >>> s.append(note.Note()) - >>> s.highestTime - 1.0 - >>> s.show('t') - {0.0} - {0.0} - >>> s.flatten().show('t') - {0.0} - {0.0} +def _getRegionsFromStreams(streamX, streamY): + # noinspection PyShadowingNames ''' + Takes in two streams, returns a list of 5-tuples via difflib.get_opcodes() + working on measure differences. - classSortOrder = stream.Stream.classSortOrder - 2 # variants should always come first? - - # this copies the init of Streams - def __init__(self, givenElements=None, *args, **keywords): - super().__init__() - self.exposeTime = False - self._stream = stream.VariantStorage(givenElements=givenElements, - *args, **keywords) - self._replacementDuration = None + >>> s1 = converter.parse("tinynotation: 2/4 d4 e8. f16 GG4 b'4 b-2 c4 d8. e16 FF4 a'4 b-2") - if 'name' in keywords: - self.groups.append(keywords['name']) + *0:Eq *1:Rep * *3:Eq *6:In + >>> s2 = converter.parse("tinynotation: 2/4 d4 e8. f16 FF4 b'4 c4 d8. e16 FF4 a'4 b-2 b-2") + >>> s1m = s1.makeMeasures() + >>> s2m = s2.makeMeasures() + >>> regions = variant._getRegionsFromStreams(s1m, s2m) + >>> regions + [('equal', 0, 1, 0, 1), + ('replace', 1, 3, 1, 2), + ('equal', 3, 6, 2, 5), + ('insert', 6, 6, 5, 6)] - def _deepcopySubclassable(self, memo=None, ignoreAttributes=None, removeFromIgnore=None): - ''' - see __deepcopy__ on Spanner for tests and docs - ''' - # NOTE: this is a performance critical operation - defaultIgnoreSet = {'_cache'} - if ignoreAttributes is None: - ignoreAttributes = defaultIgnoreSet - else: - ignoreAttributes = ignoreAttributes | defaultIgnoreSet + ''' + hashesX = getMeasureHashes(streamX) + hashesY = getMeasureHashes(streamY) + sm = difflib.SequenceMatcher() + sm.set_seqs(hashesX, hashesY) + regions = sm.get_opcodes() + return regions - new = super()._deepcopySubclassable(memo, ignoreAttributes, removeFromIgnore) - return new +def _mergeVariants(streamA, streamB, *, variantName=None, inPlace=False): + ''' + This is a helper function for mergeVariantsEqualDuration which takes two streams + (which cannot contain container + streams like measures and parts) and merges the second into the first via variant objects. + If the first already contains variant objects, containsVariants should be set to true and the + function will compare streamB to the streamA as well as the + variant streams contained in streamA. + Note that variant streams in streamB will be ignored and lost. - def __deepcopy__(self, memo=None): - return self._deepcopySubclassable(memo) - # -------------------------------------------------------------------------- - # as _stream is a private Stream, unwrap/wrap methods need to override - # Music21Object to get at these objects - # this is the same as with Spanners + >>> stream1 = stream.Stream() + >>> stream2 = stream.Stream() + >>> data1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), + ... ('a', 'quarter'), ('a', 'quarter'), + ... ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'), + ... ('b', 'quarter'), ('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter')] + >>> data2 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter'), + ... ('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), ('a', 'quarter'), + ... ('b', 'quarter'), ('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter')] + >>> for pitchName, durType in data1: + ... n = note.Note(pitchName) + ... n.duration.type = durType + ... stream1.append(n) + >>> for pitchName, durType in data2: + ... n = note.Note(pitchName) + ... n.duration.type = durType + ... stream2.append(n) + >>> mergedStreams = variant._mergeVariants(stream1, stream2, variantName='paris') + >>> mergedStreams.show('t') + {0.0} + {1.0} + {1.0} + {1.5} + {2.0} + {3.0} + {3.0} + {4.0} + {4.5} + {4.5} + {5.0} + {6.0} + {7.0} + {8.0} + {9.0} + {9.0} + {10.0} - def purgeOrphans(self, excludeStorageStreams=True): - self._stream.purgeOrphans(excludeStorageStreams) - base.Music21Object.purgeOrphans(self, excludeStorageStreams) + >>> mergedStreams.activateVariants('paris').show('t') + {0.0} + {1.0} + {1.0} + {2.0} + {3.0} + {3.0} + {4.0} + {4.5} + {4.5} + {5.5} + {6.0} + {7.0} + {8.0} + {9.0} + {9.0} + {10.0} - def purgeLocations(self, rescanIsDead=False): - # must override Music21Object to purge locations from the contained - self._stream.purgeLocations(rescanIsDead=rescanIsDead) - base.Music21Object.purgeLocations(self, rescanIsDead=rescanIsDead) + >>> stream1.append(note.Note('e')) + >>> mergedStreams = variant._mergeVariants(stream1, stream2, variantName=['paris']) + Traceback (most recent call last): + music21.variant.VariantException: _mergeVariants cannot merge streams + which are of different lengths + ''' + # TODO: Add the feature for merging a stream to a stream with existing variants + # (it has to compare against both the stream and the contained variant) + if (streamA.getElementsByClass(stream.Measure) + or streamA.getElementsByClass('Part') + or streamB.getElementsByClass(stream.Measure) + or streamB.getElementsByClass('Part')): + raise VariantException( + '_mergeVariants cannot merge streams which contain measures or parts.' + ) - def _reprInternal(self): - return 'object of length ' + str(self.containedHighestTime) + if streamA.highestTime != streamB.highestTime: + raise VariantException( + '_mergeVariants cannot merge streams which are of different lengths' + ) - def __getattr__(self, attr): - ''' - This defers all calls not defined in this Class to calls on the privately contained Stream. - ''' - # environLocal.printDebug(['relaying unmatched attribute request ' - # + attr + ' to private Stream']) + if inPlace is True: + returnObj = streamA + else: + returnObj = copy.deepcopy(streamA) - # must mask pitches so as not to recurse - # TODO: check tt recurse does not go into this - if attr in ['flat', 'pitches']: - raise AttributeError + i = 0 + j = 0 + inVariant = False + streamANotes = streamA.flatten().notesAndRests + streamBNotes = streamB.flatten().notesAndRests - # needed for unpickling where ._stream doesn't exist until later... - if attr != '_stream' and hasattr(self, '_stream'): - return getattr(self._stream, attr) - else: - raise AttributeError + noteBuffer = [] + variantStart = 0.0 - def __getitem__(self, key): - return self._stream.__getitem__(key) + while i < len(streamANotes) and j < len(streamBNotes): + if i == len(streamANotes): + i = len(streamANotes) - 1 + if j == len(streamBNotes): + break + if (streamANotes[i].getOffsetBySite(streamA.flatten()) + == streamBNotes[j].getOffsetBySite(streamB.flatten())): + # Comparing Notes at same offset + # TODO: Will not work until __eq__ overwritten for Generalized Notes + if streamANotes[i] != streamBNotes[j]: + # If notes are different, start variant if not started and append note. + if inVariant is False: + variantStart = streamBNotes[j].getOffsetBySite(streamB.flatten()) + inVariant = True + noteBuffer = [] + noteBuffer.append(streamBNotes[j]) + else: + noteBuffer.append(streamBNotes[j]) + else: # If notes are the same, end and insert variant if in variant. + if inVariant is True: + returnObj.insert( + variantStart, + _generateVariant( + noteBuffer, + streamB, + variantStart, + variantName + ) + ) + inVariant = False + noteBuffer = [] + else: + inVariant = False + i += 1 + j += 1 + continue - def __len__(self): - return len(self._stream) + elif (streamANotes[i].getOffsetBySite(streamA.flatten()) + > streamBNotes[j].getOffsetBySite(streamB.flatten())): + if inVariant is False: + variantStart = streamBNotes[j].getOffsetBySite(streamB.flatten()) + noteBuffer = [] + noteBuffer.append(streamBNotes[j]) + inVariant = True + else: + noteBuffer.append(streamBNotes[j]) + j += 1 + continue - def __iter__(self): - return self._stream.__iter__() + else: # Less-than + i += 1 + continue - def getElementIds(self): - if 'elementIds' not in self._cache or self._cache['elementIds'] is None: - self._cache['elementIds'] = [id(c) for c in self._stream._elements] - return self._cache['elementIds'] + if inVariant is True: # insert final variant if exists + returnObj.insert( + variantStart, + _generateVariant( + noteBuffer, + streamB, + variantStart, + variantName + ) + ) + inVariant = False + noteBuffer = [] + if inPlace is True: + return None + else: + return returnObj - def replaceElement(self, old, new): - ''' - When copying a Variant, we need to update the Variant with new - references for copied elements. Given the old element, - this method will replace the old with the new. - The `old` parameter can be either an object or object id. +def _generateVariant(noteList, originStream, start, variantName=None): + # noinspection PyShadowingNames + ''' + Helper function for mergeVariantsEqualDuration which takes a list of + consecutive notes from a stream and returns + a variant object containing the notes from the list at the offsets + derived from their original context. - This method is very similar to the replaceSpannedElement method on Spanner. - ''' - if old is None: - return None # do nothing - if common.isNum(old): - # this must be id(obj), not obj.id - e = self._stream.coreGetElementByMemoryLocation(old) - if e is not None: - self._stream.replace(e, new, allDerived=False) - else: - # do not do all Sites: only care about this one - self._stream.replace(old, new, allDerived=False) + >>> originStream = stream.Stream() + >>> data = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), + ... ('a', 'quarter'), ('a', 'quarter'), + ... ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'), + ... ('b', 'quarter'), ('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter')] + >>> for pitchName, durType in data: + ... n = note.Note(pitchName) + ... n.duration.type = durType + ... originStream.append(n) + >>> noteList = [] + >>> for n in originStream.notes[2:5]: + ... noteList.append(n) + >>> start = originStream.notes[2].offset + >>> variantName='paris' + >>> v = variant._generateVariant(noteList, originStream, start, variantName) + >>> v.show('text') + {0.0} + {0.5} + {1.5} - # -------------------------------------------------------------------------- - # Stream simulation/overrides - @property - def highestTime(self): - ''' - This property masks calls to Stream.highestTime. Assuming `exposeTime` - is False, this always returns zero, making the Variant always take zero time. + >>> v.groups + ['paris'] - >>> v = variant.Variant() - >>> v.append(note.Note(quarterLength=4)) - >>> v.highestTime - 0.0 - ''' - if self.exposeTime: - return self._stream.highestTime - else: - return 0.0 + ''' + returnVariant = Variant() + for n in noteList: + returnVariant.insert(n.getOffsetBySite(originStream.flatten()) - start, n) + if variantName is not None: + returnVariant.groups.append(variantName) + return returnVariant - @property - def highestOffset(self): - ''' - This property masks calls to Stream.highestOffset. Assuming `exposeTime` - is False, this always returns zero, making the Variant always take zero time. - >>> v = variant.Variant() - >>> v.append(note.Note(quarterLength=4)) - >>> v.highestOffset - 0.0 - ''' - if self.exposeTime: - return self._stream.highestOffset - else: - return 0.0 +# ------- Variant Manipulation Methods +def makeAllVariantsReplacements(streamWithVariants, + variantNames=None, + inPlace=False, + recurse=False): + # noinspection PyShadowingNames,GrazieInspection + ''' + This function takes a stream and a list of variantNames + (default works on all variants), and changes all insertion + (elongations with replacementDuration 0) + and deletion variants (with containedHighestTime 0) into variants with non-zero + replacementDuration and non-null elements + by adding measures on the front of insertions and measures on the end + of deletions. This is designed to make it possible to format all variants in a + readable way as a graphical ossia (via lilypond). If inPlace is True + it will perform this action on the stream itself; otherwise it will return a + modified copy. If recurse is True, this + method will work on variants within container objects within the stream (like parts). - def show(self, fmt=None, app=None): - ''' - Call show() on the Stream contained by this Variant. + >>> # * * * + >>> s = converter.parse("tinynotation: 4/4 d4 e4 f4 g4 a2 b-4 a4 g4 a8 g8 f4 e4 d2 a2 d4 e4 f4 g4 a2 b-4 a4 g4 a8 b-8 c'4 c4 f1") + >>> s2 = converter.parse("tinynotation: 4/4 d4 e4 f4 g4 a2. b-8 a8 g4 a8 g8 f4 e4 d2 a2 d4 f4 a2 d4 f4 AA2 d4 e4 f4 g4 g4 a8 b-8 c'4 c4 f1") + >>> # replacement insertion deletion + >>> s.makeMeasures(inPlace=True) + >>> s2.makeMeasures(inPlace=True) + >>> variant.mergeVariants(s, s2, variantName='london', inPlace=True) - This method must be overridden, otherwise Music21Object.show() is called. + >>> newStream = stream.Score(s) + >>> returnStream = variant.makeAllVariantsReplacements(newStream, recurse=False) + >>> for v in returnStream.parts[0][variant.Variant]: + ... (v.offset, v.lengthType, v.replacementDuration) + (4.0, 'replacement', 4.0) + (16.0, 'elongation', 0.0) + (20.0, 'deletion', 4.0) - >>> v = variant.Variant() - >>> v.repeatAppend(note.Note(quarterLength=0.25), 8) - >>> v.show('t') - {0.0} - {0.25} - {0.5} - {0.75} - {1.0} - {1.25} - {1.5} - {1.75} - ''' - self._stream.show(fmt=fmt, app=app) + >>> returnStream = variant.makeAllVariantsReplacements( + ... newStream, variantNames=['france'], recurse=True) + >>> for v in returnStream.parts[0][variant.Variant]: + ... (v.offset, v.lengthType, v.replacementDuration) + (4.0, 'replacement', 4.0) + (16.0, 'elongation', 0.0) + (20.0, 'deletion', 4.0) - # -------------------------------------------------------------------------- - # properties particular to this class + >>> variant.makeAllVariantsReplacements(newStream, recurse=True, inPlace=True) + >>> for v in newStream.parts[0][variant.Variant]: + ... (v.offset, v.lengthType, v.replacementDuration, v.containedHighestTime) + (4.0, 'replacement', 4.0, 4.0) + (12.0, 'elongation', 4.0, 12.0) + (20.0, 'deletion', 8.0, 4.0) - @property - def containedHighestTime(self): - ''' - This property calls the contained Stream.highestTime. + ''' - >>> v = variant.Variant() - >>> v.append(note.Note(quarterLength=4)) - >>> v.containedHighestTime - 4.0 - ''' - return self._stream.highestTime + if inPlace is True: + returnStream = streamWithVariants + else: + returnStream = copy.deepcopy(streamWithVariants) + + if recurse is True: + for s in returnStream.recurse(streamsOnly=True): + _doVariantFixingOnStream(s, variantNames=variantNames) + else: + _doVariantFixingOnStream(returnStream, variantNames=variantNames) - @property - def containedHighestOffset(self): - ''' - This property calls the contained Stream.highestOffset. - >>> v = variant.Variant() - >>> v.append(note.Note(quarterLength=4)) - >>> v.append(note.Note()) - >>> v.containedHighestOffset - 4.0 - ''' - return self._stream.highestOffset + if inPlace is True: + return + else: + return returnStream - @property - def containedSite(self): - ''' - Return the Stream contained in this Variant. - ''' - return self._stream - def _getReplacementDuration(self): - if self._replacementDuration is None: - return self._stream.duration.quarterLength - else: - return self._replacementDuration +def _doVariantFixingOnStream(s, variantNames=None): + # noinspection PyShadowingNames,GrazieInspection + ''' + This is a helper function for makeAllVariantsReplacements. + It iterates through the appropriate variants + and performs the variant changing operation to eliminate strict deletion and insertion variants. - def _setReplacementDuration(self, value): - self._replacementDuration = value + >>> # * * * * * + >>> s = converter.parse("tinynotation: 4/4 d4 e4 f4 g4 a2 b-4 a4 g4 a8 g8 f4 e4 d2 a2 d4 e4 f4 g4 a2 b-4 a4 g4 a8 b-8 c'4 c4 f1 ", makeNotation=False) + >>> s2 = converter.parse("tinynotation: 4/4 a4 b c d d4 e4 f4 g4 a2. b-8 a8 g4 a8 g8 f4 e4 d2 a2 d4 f4 a2 d4 f4 AA2 d4 e4 f4 g4 g4 a8 b-8 c'4 c4 ", makeNotation=False) + >>> # initial insertion replacement insertion deletion final deletion + >>> s.makeMeasures(inPlace=True) + >>> s2.makeMeasures(inPlace=True) + >>> variant.mergeVariants(s, s2, variantName='london', inPlace=True) - replacementDuration = property(_getReplacementDuration, _setReplacementDuration, doc=''' - Set or Return the quarterLength duration in the main stream which this variant - object replaces in the variant version of the stream. If replacementDuration is - not set, it is assumed to be the same length as the variant. If, it is set to 0, - the variant should be interpreted as an insertion. Setting replacementDuration - to None will return the value to the default which is the duration of the variant - itself. - ''') + >>> variant._doVariantFixingOnStream(s, 'london') + >>> s.show('text') + {0.0} + {0.0} + ... + {4.0} + {4.0} + ... + {12.0} + {12.0} + ... + {20.0} + {20.0} + ... + {24.0} + {24.0} + ... - @property - def lengthType(self): - ''' - Returns 'deletion' if variant is shorter than the region it replaces, 'elongation' - if the variant is longer than the region it replaces, and 'replacement' if it is - the same length. - ''' - lengthDifference = self.replacementDuration - self.containedHighestTime - if lengthDifference > 0.0: - return 'deletion' - elif lengthDifference < 0.0: - return 'elongation' - else: - return 'replacement' + >>> for v in s[variant.Variant]: + ... (v.offset, v.lengthType, v.replacementDuration) + (0.0, 'elongation', 4.0) + (4.0, 'replacement', 4.0) + (12.0, 'elongation', 4.0) + (20.0, 'deletion', 8.0) + (24.0, 'deletion', 8.0) - def replacedElements(self, contextStream=None, classList=None, - keepOriginalOffsets=False, includeSpacers=False): - # noinspection PyShadowingNames - ''' - Returns a Stream containing the elements which this variant replaces in a - given context stream. - This Stream will have length self.replacementDuration. - In regions that are strictly replaced, only elements that share a class with - an element in the variant - are captured. Elsewhere, all elements are captured. + This also works on streams with variants that contain notes and rests rather than measures. - >>> s = converter.parse("tinynotation: 4/4 d4 e4 f4 g4 a2 b-4 a4 g4 a8 g8 f4 e4 d2 a2 d4 e4 f4 g4 a2 b-4 a4 g4 a8 b-8 c'4 c4 f1", makeNotation=False) - >>> s.makeMeasures(inPlace=True) - >>> v1stream = converter.parse("tinynotation: 4/4 a2. b-8 a8", makeNotation=False) - >>> v2stream1 = converter.parse("tinynotation: 4/4 d4 f4 a2", makeNotation=False) - >>> v2stream2 = converter.parse("tinynotation: 4/4 d4 f4 AA2", makeNotation=False) + >>> s = converter.parse('tinyNotation: 4/4 e4 b b b f4 f f f g4 a a a ', makeNotation=False) + >>> v1Stream = converter.parse('tinyNotation: 4/4 a4 a a a ', makeNotation=False) + >>> # initial insertion deletion + >>> v1 = variant.Variant(v1Stream.notes) + >>> v1.replacementDuration = 0.0 + >>> v1.groups = ['london'] + >>> s.insert(0.0, v1) - >>> v1 = variant.Variant() - >>> v1measure = stream.Measure() - >>> v1.insert(0.0, v1measure) - >>> for e in v1stream.notesAndRests: - ... v1measure.insert(e.offset, e) + >>> v2 = variant.Variant() + >>> v2.replacementDuration = 4.0 + >>> v2.groups = ['london'] + >>> s.insert(4.0, v2) - >>> v2 = variant.Variant() - >>> v2measure1 = stream.Measure() - >>> v2measure2 = stream.Measure() - >>> v2.insert(0.0, v2measure1) - >>> v2.insert(4.0, v2measure2) - >>> for e in v2stream1.notesAndRests: - ... v2measure1.insert(e.offset, e) - >>> for e in v2stream2.notesAndRests: - ... v2measure2.insert(e.offset, e) + >>> variant._doVariantFixingOnStream(s, 'london') + >>> for v in s[variant.Variant]: + ... (v.offset, v.lengthType, v.replacementDuration, v.containedHighestTime) + (0.0, 'elongation', 1.0, 5.0) + (4.0, 'deletion', 5.0, 1.0) + ''' - >>> v3 = variant.Variant() - >>> v2.replacementDuration = 4.0 - >>> v3.replacementDuration = 4.0 + for v in s.getElementsByClass(Variant): + if isinstance(variantNames, list): # If variantNames are controlled + if set(v.groups) and not set(variantNames): + # and if this variant is not in the controlled list + continue # then skip it + else: + continue # huh???? + lengthType = v.lengthType + replacementDuration = v.replacementDuration + highestTime = v.containedHighestTime - >>> s.insert(4.0, v1) # replacement variant - >>> s.insert(12.0, v2) # insertion variant (2 bars replace 1 bar) - >>> s.insert(20.0, v3) # deletion variant (0 bars replace 1 bar) + if lengthType == 'elongation' and replacementDuration == 0.0: + variantType = 'insertion' + elif lengthType == 'deletion' and highestTime == 0.0: + variantType = 'deletion' + else: + continue - >>> v1.replacedElements(s).show('text') - {0.0} - {0.0} - {2.0} - {3.0} + if v.getOffsetBySite(s) == 0.0: + isInitial = True + isFinal = False + elif v.getOffsetBySite(s) + v.replacementDuration == s.duration.quarterLength: + isInitial = False + isFinal = True + else: + isInitial = False + isFinal = False - >>> v2.replacedElements(s).show('text') - {0.0} - {0.0} - {2.0} + # If a non-final deletion or an INITIAL insertion, + # add the next element after the variant. + if ((variantType == 'insertion' and (isInitial is True)) + or (variantType == 'deletion' and (isFinal is False))): + targetElement = _getNextElements(s, v) - >>> v3.replacedElements(s).show('text') - {0.0} - {0.0} - {2.0} - {3.0} + # Delete initial clefs, etc. from initial insertion targetElement if it exists + if isinstance(targetElement, stream.Stream): + # Must use .elements, because of removal of elements + for e in targetElement.elements: + if isinstance(e, (clef.Clef, meter.TimeSignature)): + targetElement.remove(e) - >>> v3.replacedElements(s, keepOriginalOffsets=True).show('text') - {20.0} - {0.0} - {2.0} - {3.0} + v.append(copy.deepcopy(targetElement)) # Appends a copy + # If a non-initial insertion or a FINAL deletion, + # add the previous element after the variant. + # #elif ((variantType == 'deletion' and (isFinal is True)) or + # (type == 'insertion' and (isInitial is False))): + else: + targetElement = _getPreviousElement(s, v) + newVariantOffset = targetElement.getOffsetBySite(s) + # Need to shift elements to make way for new element at front + offsetShift = targetElement.duration.quarterLength + for e in v.containedSite: + oldOffset = e.getOffsetBySite(v.containedSite) + e.setOffsetBySite(v.containedSite, oldOffset + offsetShift) + v.insert(0.0, copy.deepcopy(targetElement)) + s.remove(v) + s.insert(newVariantOffset, v) - A second example: + # Give it a new replacementDuration including the added element + oldReplacementDuration = v.replacementDuration + v.replacementDuration = oldReplacementDuration + targetElement.duration.quarterLength - >>> v = variant.Variant() - >>> variantDataM1 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), - ... ('a', 'quarter'),('b', 'quarter')] - >>> variantDataM2 = [('c', 'quarter'), ('d', 'quarter'), - ... ('e', 'quarter'), ('e', 'quarter')] - >>> variantData = [variantDataM1, variantDataM2] - >>> for d in variantData: - ... m = stream.Measure() - ... for pitchName, durType in d: - ... n = note.Note(pitchName) - ... n.duration.type = durType - ... m.append(n) - ... v.append(m) - >>> v.groups = ['paris'] - >>> v.replacementDuration = 4.0 +def _getNextElements(s, v, numberOfElements=1): + # noinspection PyShadowingNames, GrazieInspection + ''' + This is a helper function for makeAllVariantsReplacements() which returns the next element in s + of the type of elements found in the variant v so that it can be added to v. - >>> s = stream.Stream() - >>> streamDataM1 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter')] - >>> streamDataM2 = [('b', 'eighth'), ('c', 'quarter'), - ... ('a', 'eighth'), ('a', 'quarter'), ('b', 'quarter')] - >>> streamDataM3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] - >>> streamDataM4 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] - >>> streamData = [streamDataM1, streamDataM2, streamDataM3, streamDataM4] - >>> for d in streamData: - ... m = stream.Measure() - ... for pitchName, durType in d: - ... n = note.Note(pitchName) - ... n.duration.type = durType - ... m.append(n) - ... s.append(m) - >>> s.insert(4.0, v) - >>> v.replacedElements(s).show('t') - {0.0} - {0.0} - {0.5} - {1.5} - {2.0} - {3.0} - ''' - spacerFilter = lambda r: r.hasStyleInformation and r.style.hideObjectOnPrint + >>> # * * + >>> s1 = converter.parse('tinyNotation: 4/4 b4 c d e f4 g a b d4 e f g ', makeNotation=False) + >>> s2 = converter.parse('tinyNotation: 4/4 e4 f g a b4 c d e d4 e f g ', makeNotation=False) + >>> # insertion deletion + >>> s1.makeMeasures(inPlace=True) + >>> s2.makeMeasures(inPlace=True) + >>> mergedStream = variant.mergeVariants(s1, s2, 'london') + >>> for v in mergedStream.getElementsByClass(variant.Variant): + ... returnElement = variant._getNextElements(mergedStream, v) + ... print(returnElement) + + + + This also works on streams with variants that contain notes and rests rather than measures. - if contextStream is None: - contextStream = self.activeSite - if contextStream is None: - environLocal.printDebug( - 'No contextStream or activeSite, finding most recently added site (dangerous)') - contextStream = self.getContextByClass('Stream') - if contextStream is None: - raise VariantException('Cannot find a Stream context for this object...') + >>> s = converter.parse('tinyNotation: 4/4 e4 b b b f4 f f f g4 a a a ', makeNotation=False) + >>> v1Stream = converter.parse('tinyNotation: 4/4 a4 a a a ', makeNotation=False) + >>> # initial insertion + >>> v1 = variant.Variant(v1Stream.notes) + >>> v1.replacementDuration = 0.0 + >>> v1.groups = ['london'] + >>> s.insert(0.0, v1) - if self not in contextStream.getElementsByClass('Variant'): - raise VariantException(f'Variant not found in stream {contextStream}') + >>> v2 = variant.Variant() + >>> v2.replacementDuration = 4.0 + >>> v2.groups = ['london'] + >>> s.insert(4.0, v2) + >>> for v in s[variant.Variant]: + ... returnElement = variant._getNextElements(s, v) + ... print(returnElement) + + + ''' + replacedElements = v.replacedElements(s) + lengthType = v.lengthType + # Get class of elements in variant or replaced Region + if lengthType == 'elongation': + vClass = type(v.getElementsByClass(['Measure', 'Note', 'Rest']).first()) + if isinstance(vClass, note.GeneralNote): + vClass = note.GeneralNote + else: + vClass = type(replacedElements.getElementsByClass(['Measure', 'Note', 'Rest']).first()) + if isinstance(vClass, note.GeneralNote): + vClass = note.GeneralNote - vStart = self.getOffsetBySite(contextStream) + # Get next element in s after v which is of type vClass + if lengthType == 'elongation': + variantOffset = v.getOffsetBySite(s) + potentialTargets = s.getElementsByOffset(variantOffset, + offsetEnd=s.highestTime, + includeEndBoundary=True, + mustFinishInSpan=False, + mustBeginInSpan=True, + classList=[vClass]) + returnElement = potentialTargets.first() - if includeSpacers is True: - spacerDuration = (self - .getElementsByClass('Rest') - .addFilter(spacerFilter) - .first().duration.quarterLength) - else: - spacerDuration = 0.0 + else: + replacementDuration = v.replacementDuration + variantOffset = v.getOffsetBySite(s) + potentialTargets = s.getElementsByOffset(variantOffset + replacementDuration, + offsetEnd=s.highestTime, + includeEndBoundary=True, + mustFinishInSpan=False, + mustBeginInSpan=True, + classList=[vClass]) + returnElement = potentialTargets.first() - if self.lengthType in ('replacement', 'elongation'): - vEnd = vStart + self.replacementDuration + spacerDuration - classes = [] - for e in self.elements: - classes.append(e.classes[0]) - if classList is not None: - classes.extend(classList) - returnStream = contextStream.getElementsByOffset(vStart, vEnd, - includeEndBoundary=False, - mustFinishInSpan=False, - mustBeginInSpan=True, - classList=classes).stream() + return returnElement - elif self.lengthType == 'deletion': - vMiddle = vStart + self.containedHighestTime - vEnd = vStart + self.replacementDuration - classes = [] # collect all classes found in this variant - for e in self.elements: - classes.append(e.classes[0]) - if classList is not None: - classes.extend(classList) - returnPart1 = contextStream.getElementsByOffset(vStart, vMiddle, - includeEndBoundary=False, - mustFinishInSpan=False, - mustBeginInSpan=True, - classList=classes).stream() - returnPart2 = contextStream.getElementsByOffset(vMiddle, vEnd, - includeEndBoundary=False, - mustFinishInSpan=False, - mustBeginInSpan=True).stream() - returnStream = returnPart1 - for e in returnPart2.elements: - oInPart = e.getOffsetBySite(returnPart2) - returnStream.insert(vMiddle - vStart + oInPart, e) - else: - raise VariantException('lengthType must be replacement, elongation, or deletion') +def _getPreviousElement(s, v): + # noinspection PyShadowingNames,GrazieInspection + ''' + This is a helper function for makeAllVariantsReplacements() which returns + the previous element in s + of the type of elements found in the variant v so that it can be added to v. - if self in returnStream: - returnStream.remove(self) - # This probably makes sense to do, but activateVariants - # for example only uses the offset in the original. - # Also, we are not changing measure numbers and should - # not as that will cause activateVariants to fail. - if keepOriginalOffsets is False: - for e in returnStream: - e.setOffsetBySite(returnStream, e.getOffsetBySite(returnStream) - vStart) + >>> # * * + >>> s1 = converter.parse('tinyNotation: 4/4 a4 b c d b4 c d e f4 g a b ') + >>> s2 = converter.parse('tinyNotation: 4/4 a4 b c d e4 f g a b4 c d e ') + >>> # insertion deletion + >>> s1.makeMeasures(inPlace=True) + >>> s2.makeMeasures(inPlace=True) + >>> mergedStream = variant.mergeVariants(s1, s2, 'london') + >>> for v in mergedStream[variant.Variant]: + ... returnElement = variant._getPreviousElement(mergedStream, v) + ... print(returnElement) + + - return returnStream + This also works on streams with variants that contain notes and rests rather than measures. - def removeReplacedElementsFromStream(self, referenceStream=None, classList=None): - ''' - remove replaced elements from a referenceStream or activeSite + >>> s = converter.parse('tinyNotation: 4/4 b4 b b a e4 b b b g4 e e e ', makeNotation=False) + >>> v1Stream = converter.parse('tinyNotation: 4/4 f4 f f f ', makeNotation=False) + >>> # insertion final deletion + >>> v1 = variant.Variant(v1Stream.notes) + >>> v1.replacementDuration = 0.0 + >>> v1.groups = ['london'] + >>> s.insert(4.0, v1) + >>> v2 = variant.Variant() + >>> v2.replacementDuration = 4.0 + >>> v2.groups = ['london'] + >>> s.insert(8.0, v2) + >>> for v in s[variant.Variant]: + ... returnElement = variant._getPreviousElement(s, v) + ... print(returnElement) + + + ''' - >>> v = variant.Variant() - >>> variantDataM1 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), - ... ('a', 'quarter'),('b', 'quarter')] - >>> variantDataM2 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')] - >>> variantData = [variantDataM1, variantDataM2] - >>> for d in variantData: - ... m = stream.Measure() - ... for pitchName, durType in d: - ... n = note.Note(pitchName) - ... n.duration.type = durType - ... m.append(n) - ... v.append(m) - >>> v.groups = ['paris'] - >>> v.replacementDuration = 4.0 + replacedElements = v.replacedElements(s) + lengthType = v.lengthType + # Get class of elements in variant or replaced Region + foundStream = None + if lengthType == 'elongation': + foundStream = v.getElementsByClass(['Measure', 'Note', 'Rest']) + else: + foundStream = replacedElements.getElementsByClass(['Measure', 'Note', 'Rest']) - >>> s = stream.Stream() - >>> streamDataM1 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter')] - >>> streamDataM2 = [('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), - ... ('a', 'quarter'), ('b', 'quarter')] - >>> streamDataM3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] - >>> streamDataM4 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')] - >>> streamData = [streamDataM1, streamDataM2, streamDataM3, streamDataM4] - >>> for d in streamData: - ... m = stream.Measure() - ... for pitchName, durType in d: - ... n = note.Note(pitchName) - ... n.duration.type = durType - ... m.append(n) - ... s.append(m) - >>> s.insert(4.0, v) + if not foundStream: + raise VariantException('Cannot find any Measures, Notes, or Rests in variant') + vClass = type(foundStream[0]) + if isinstance(vClass, note.GeneralNote): + vClass = note.GeneralNote + + # Get next element in s after v which is of type vClass + variantOffset = v.getOffsetBySite(s) + potentialTargets = s.getElementsByOffset( + 0.0, + offsetEnd=variantOffset, + includeEndBoundary=False, + mustFinishInSpan=False, + mustBeginInSpan=True, + ).getElementsByClass(vClass) + returnElement = potentialTargets.last() + + return returnElement - >>> v.removeReplacedElementsFromStream(s) - >>> s.show('t') - {0.0} - {0.0} - {1.0} - {2.0} - {3.0} - {4.0} - {8.0} - {0.0} - {1.0} - {2.0} - {3.0} - {12.0} - {0.0} - {1.0} - {2.0} - {3.0} - ''' - if referenceStream is None: - referenceStream = self.activeSite - if referenceStream is None: - environLocal.printDebug('No referenceStream or activeSite, ' - + 'finding most recently added site (dangerous)') - referenceStream = self.getContextByClass('Stream') - if referenceStream is None: - raise VariantException('Cannot find a Stream context for this object...') - if self not in referenceStream.getElementsByClass('Variant'): - raise VariantException(f'Variant not found in stream {referenceStream}') - replacedElements = self.replacedElements(referenceStream, classList) - for el in replacedElements: - referenceStream.remove(el) # ------------------------------------------------------------------------------ @@ -2572,8 +2575,8 @@ def testVariantClassA(self): self.assertIn('Variant', v1.classes) - self.assertFalse(v1.hasElementOfClass('Variant')) - self.assertTrue(v1.hasElementOfClass('Measure')) + self.assertFalse(v1.hasElementOfClass(Variant)) + self.assertTrue(v1.hasElementOfClass(stream.Measure)) def testDeepCopyVariantA(self): s = stream.Stream() @@ -2601,7 +2604,7 @@ def testDeepCopyVariantA(self): # test functionality on a deepcopy sCopy = copy.deepcopy(s) - self.assertEqual(len(sCopy.getElementsByClass('Variant')), 1) + self.assertEqual(len(sCopy.getElementsByClass(Variant)), 1) self.assertEqual(self.pitchOut(sCopy.pitches), '[G4, G4, G4, G4, G4, G4, G4, G4]') sCopy.activateVariants(inPlace=True) From 6cf1e5860648e44371a580a2972de910c0e99bdd Mon Sep 17 00:00:00 2001 From: Myke Cuthbert Date: Wed, 27 Apr 2022 00:51:33 -1000 Subject: [PATCH 2/6] Fix bugs. Probably add more... --- music21/abcFormat/translate.py | 2 +- music21/analysis/metrical.py | 3 ++- music21/base.py | 6 +++--- music21/common/numberTools.py | 13 ++++++------- music21/converter/__init__.py | 4 +++- music21/humdrum/spineParser.py | 2 +- music21/layout.py | 10 +++++----- music21/romanText/translate.py | 2 +- music21/romanText/tsvConverter.py | 2 +- music21/stream/base.py | 4 +++- music21/stream/iterator.py | 2 +- 11 files changed, 27 insertions(+), 23 deletions(-) diff --git a/music21/abcFormat/translate.py b/music21/abcFormat/translate.py index 759891d468..9adf92fc33 100644 --- a/music21/abcFormat/translate.py +++ b/music21/abcFormat/translate.py @@ -196,7 +196,7 @@ def abcToStreamPart(abcHandler, inputM21=None, spannerBundle=None): pass # clefs are not typically defined, but if so, are set to the first measure # following the meta data, or in the open stream - if not clefSet and not p.recurse().getElementsByClass('Clef'): + if not clefSet and not p[clef.Clef]: if useMeasures: # assume at start of measures p.getElementsByClass(stream.Measure).first().clef = clef.bestClef(p, recurse=True) else: diff --git a/music21/analysis/metrical.py b/music21/analysis/metrical.py index b512042e4d..d49d9fbc53 100644 --- a/music21/analysis/metrical.py +++ b/music21/analysis/metrical.py @@ -86,7 +86,8 @@ def thomassenMelodicAccent(streamIn): .. _melac: https://www.humdrum.org/Humdrum/commands/melac.html Takes in a Stream of :class:`~music21.note.Note` objects (use `.flatten().notes` to get it, or - better `.flatten().getElementsByClass(note.Note)` to filter out chords) and adds the attribute to + better `.flatten().getElementsByClass(note.Note)` to filter out chords) + and adds the attribute to each. Note that Huron and Royal's work suggests that melodic accent has a correlation with metrical accent only for solo works/passages; even treble passages do not have a strong correlation. (Gregorian chants were found to have a strong ''negative'' correlation diff --git a/music21/base.py b/music21/base.py index d147937edc..cfc79d6f96 100644 --- a/music21/base.py +++ b/music21/base.py @@ -4577,8 +4577,8 @@ def testRecurseByClass(self): # only get n1 here, as that is only level available self.assertEqual(s1.recurse().getElementsByClass(note.Note).first(), n1) self.assertEqual(s2.recurse().getElementsByClass(note.Note).first(), n2) - self.assertEqual(s1.recurse().getElementsByClass('Clef').first(), c1) - self.assertEqual(s2.recurse().getElementsByClass('Clef').first(), c2) + self.assertEqual(s1[clef.Clef].first(), c1) + self.assertEqual(s2[clef.Clef].first(), c2) # attach s2 to s1 s2.append(s1) @@ -4697,7 +4697,7 @@ def getnchannels(self): matchBeatStrength = [] matchAudioChannels = [] - for j in s.getElementsByClass(ElementWrapper): + for j in s.getElementsByClass(base.ElementWrapper): matchOffset.append(j.offset) matchBeatStrength.append(j.beatStrength) matchAudioChannels.append(j.getnchannels()) diff --git a/music21/common/numberTools.py b/music21/common/numberTools.py index 483355d394..c0198cfa15 100644 --- a/music21/common/numberTools.py +++ b/music21/common/numberTools.py @@ -64,27 +64,26 @@ @deprecated('v7.3', 'v9', 'Use common.opFrac(num) instead') -def cleanupFloat(floatNum, maxDenominator=defaults.limitOffsetDenominator): +def cleanupFloat(floatNum, maxDenominator=defaults.limitOffsetDenominator): # pragma: no cover ''' Cleans up a floating point number by converting it to a fractions.Fraction object limited to a denominator of maxDenominator - >>> common.cleanupFloat(0.33333327824) + common.cleanupFloat(0.33333327824) 0.333333333333... - >>> common.cleanupFloat(0.142857) + common.cleanupFloat(0.142857) 0.1428571428571... - >>> common.cleanupFloat(1.5) + common.cleanupFloat(1.5) 1.5 Fractions are passed through silently... - >>> import fractions - >>> common.cleanupFloat(fractions.Fraction(4, 3)) + import fractions + common.cleanupFloat(fractions.Fraction(4, 3)) Fraction(4, 3) - ''' if isinstance(floatNum, Fraction): return floatNum # do nothing to fractions diff --git a/music21/converter/__init__.py b/music21/converter/__init__.py index f93c0fefc9..1b330a129b 100644 --- a/music21/converter/__init__.py +++ b/music21/converter/__init__.py @@ -1947,6 +1947,8 @@ def testIncorrectNotCached(self): Here is a filename with an incorrect extension (.txt for .rnText). Make sure that it is not cached the second time... ''' + from music21 import harmony + fp = common.getSourceFilePath() / 'converter' / 'incorrectExtension.txt' pf = PickleFilter(fp) pf.removePickle() @@ -1955,7 +1957,7 @@ def testIncorrectNotCached(self): parse(fp) c = parse(fp, format='romantext') - self.assertEqual(len(c.recurse().getElementsByClass('Harmony')), 1) + self.assertEqual(len(c[harmony.Harmony]), 1) def testConverterFromPath(self): fp = common.getSourceFilePath() / 'corpus' / 'bach' / 'bwv66.6.mxl' diff --git a/music21/humdrum/spineParser.py b/music21/humdrum/spineParser.py index 2290329f95..af57bb2d9c 100644 --- a/music21/humdrum/spineParser.py +++ b/music21/humdrum/spineParser.py @@ -1931,7 +1931,7 @@ def moveDynamicsAndLyricsToStreams(self): stavesAppliedTo = [] prioritiesToSearch = {} - for tandem in thisSpine.stream.recurse().getElementsByClass('MiscTandem'): + for tandem in thisSpine.stream[MiscTandem]: if tandem.tandem.startswith('*staff'): staffInfo = tandem.tandem[6:] # could be multiple staves stavesAppliedTo = [int(x) for x in staffInfo.split('/')] diff --git a/music21/layout.py b/music21/layout.py index 1e3bf97ce4..52aa32ee43 100644 --- a/music21/layout.py +++ b/music21/layout.py @@ -703,7 +703,7 @@ def getRichSystemLayout(inner_allSystemLayouts): staffObject.elements = p thisSystem.replace(p, staffObject) - allStaffLayouts = p[StaffLayout] + allStaffLayouts: List[StaffLayout] = p.recurse().getElementsByClass('StaffLayout') if not allStaffLayouts: continue # else: @@ -1323,7 +1323,7 @@ def getSystemBeforeThis( self, pageId: int, systemId: int - ) -> Tuple[Optional[int], Optional[int]]: + ) -> Tuple[Optional[int], int]: # noinspection PyShadowingNames ''' given a pageId and systemId, get the (pageId, systemId) for the previous system. @@ -1337,16 +1337,16 @@ def getSystemBeforeThis( >>> ls = layout.divideByPages(lt, fastMeasures = True) >>> systemId = 1 >>> pageId = 2 # last system, last page - >>> while systemId is not None: + >>> while pageId is not None: ... pageId, systemId = ls.getSystemBeforeThis(pageId, systemId) ... (pageId, systemId) - (2, 0) (1, 2) (1, 1) (1, 0) (0, 4) (0, 3) (0, 2) (0, 1) (0, 0) (None, None) + (2, 0) (1, 2) (1, 1) (1, 0) (0, 4) (0, 3) (0, 2) (0, 1) (0, 0) (None, -1) ''' if systemId > 0: return pageId, systemId - 1 else: if pageId == 0: - return (None, None) + return (None, -1) previousPageId = pageId - 1 numSystems = len(self.pages[previousPageId].systems) return previousPageId, numSystems - 1 diff --git a/music21/romanText/translate.py b/music21/romanText/translate.py index bf16e1d5b9..51ce3af30c 100644 --- a/music21/romanText/translate.py +++ b/music21/romanText/translate.py @@ -1287,7 +1287,7 @@ class Test(unittest.TestCase): def testMinor67set(self): from music21.romanText import testFiles s = romanTextToStreamScore(testFiles.testSetMinorRootParse) - chords = list(s.recurse().getElementsByClass('RomanNumeral')) + chords = list(s[roman.RomanNumeral]) def pitchEqual(index, pitchStr): ch = chords[index] diff --git a/music21/romanText/tsvConverter.py b/music21/romanText/tsvConverter.py index 836ef457df..42e3d69811 100644 --- a/music21/romanText/tsvConverter.py +++ b/music21/romanText/tsvConverter.py @@ -443,7 +443,7 @@ def m21ToTsv(self): tsvData = [] - for thisRN in self.m21Stream.recurse().getElementsByClass('RomanNumeral'): + for thisRN in self.m21Stream[roman.RomanNumeral]: relativeroot = None if thisRN.secondaryRomanNumeral: diff --git a/music21/stream/base.py b/music21/stream/base.py index 25d8f6d858..748594e47f 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -5122,6 +5122,8 @@ def toSoundingPitch(self, *, inPlace=False): >>> sp.derivation.origin is p True ''' + from music21 import spanner + if not inPlace: # make a copy returnObj = self.coreCopyAsDerivation('toSoundingPitch') else: @@ -5142,7 +5144,7 @@ def toSoundingPitch(self, *, inPlace=False): for container in returnObj.recurse(streamsOnly=True, includeSelf=True): container.atSoundingPitch = True - for ottava in returnObj.recurse().getElementsByClass('Ottava'): + for ottava in returnObj[spanner.Ottava]: ottava.performTransposition() if not inPlace: diff --git a/music21/stream/iterator.py b/music21/stream/iterator.py index 224a694dff..08fdd461ae 100644 --- a/music21/stream/iterator.py +++ b/music21/stream/iterator.py @@ -1581,7 +1581,7 @@ def reset(self): # ----------------------------------------------------------------------------- -class RecursiveIterator(StreamIterator[M21ObjType]): +class RecursiveIterator(StreamIterator[M21ObjType], collections.abc.Sequence): ''' One of the most powerful iterators in music21. Generally not called directly, but created by being invoked on a stream with `Stream.recurse()` From a15bf6496061fba9b661fd3cf93eaf4461407690 Mon Sep 17 00:00:00 2001 From: Myke Cuthbert Date: Wed, 27 Apr 2022 01:10:02 -1000 Subject: [PATCH 3/6] Fix last time's bugs, add even more --- music21/abcFormat/translate.py | 12 +-- music21/analysis/reduceChordsOld.py | 2 +- music21/base.py | 10 ++- music21/capella/fromCapellaXML.py | 4 +- music21/chord/__init__.py | 5 +- music21/dynamics.py | 6 +- music21/figuredBass/realizer.py | 2 +- music21/freezeThaw.py | 6 +- music21/graph/plot.py | 4 +- music21/layout.py | 2 +- music21/lily/translate.py | 2 +- music21/midi/translate.py | 6 +- music21/musicxml/m21ToXml.py | 10 +-- music21/musicxml/testPrimitive.py | 8 +- music21/musicxml/test_xmlToM21.py | 24 +++--- music21/repeat.py | 17 +++-- music21/roman.py | 2 +- music21/romanText/clercqTemperley.py | 2 +- music21/stream/base.py | 8 +- music21/stream/iterator.py | 14 ++-- music21/stream/makeNotation.py | 2 +- music21/stream/tests.py | 107 +++++++++++++++------------ music21/variant.py | 2 +- 23 files changed, 139 insertions(+), 118 deletions(-) diff --git a/music21/abcFormat/translate.py b/music21/abcFormat/translate.py index 9adf92fc33..0ef1ecf1a2 100644 --- a/music21/abcFormat/translate.py +++ b/music21/abcFormat/translate.py @@ -660,7 +660,7 @@ def testChords(self): self.assertEqual(len(s.parts[1].flatten().notesAndRests), 127) # chords are defined in second part here - self.assertEqual(len(s.parts[1].flatten().getElementsByClass('Chord')), 32) + self.assertEqual(len(s.parts[1][chord.Chord]), 32) # check pitches in chords; sharps are applied due to key signature match = [p.nameWithOctave for p in s.parts[1].flatten().getElementsByClass( @@ -944,13 +944,13 @@ def testRepeatBracketsA(self): # s.show() # one start, one end # s.parts[0].show('t') - self.assertEqual(len(s.flatten().getElementsByClass('Repeat')), 2) + self.assertEqual(len(s['Repeat']), 2) # s.show() # this has a 1 note pickup # has three repeat bars; first one is implied s = converter.parse(testFiles.draughtOfAle) - self.assertEqual(len(s.flatten().getElementsByClass('Repeat')), 3) + self.assertEqual(len(s['Repeat']), 3) self.assertEqual(s.parts[0].getElementsByClass( 'Measure')[0].notes[0].pitch.nameWithOctave, 'D4') @@ -965,14 +965,14 @@ def testRepeatBracketsB(self): from music21 import corpus s = converter.parse(testFiles.morrisonsJig) # TODO: get - self.assertEqual(len(s.flatten().getElementsByClass(spanner.RepeatBracket)), 2) + self.assertEqual(len(s[spanner.RepeatBracket]), 2) # s.show() # four repeat brackets here; 2 at beginning, 2 at end s = converter.parse(testFiles.hectorTheHero) - self.assertEqual(len(s.flatten().getElementsByClass(spanner.RepeatBracket)), 4) + self.assertEqual(len(s[spanner.RepeatBracket]), 4) s = corpus.parse('JollyTinkersReel') - self.assertEqual(len(s.flatten().getElementsByClass(spanner.RepeatBracket)), 4) + self.assertEqual(len(s[spanner.RepeatBracket]), 4) def testMetronomeMarkA(self): from music21.abcFormat import testFiles diff --git a/music21/analysis/reduceChordsOld.py b/music21/analysis/reduceChordsOld.py index a6be45d543..0c53814f1f 100644 --- a/music21/analysis/reduceChordsOld.py +++ b/music21/analysis/reduceChordsOld.py @@ -355,7 +355,7 @@ def testTrecentoMadrigal(self): fixClef = True if fixClef: startClefs = c.parts[1].getElementsByClass(stream.Measure - ).first().getElementsByClass('Clef') + ).first().getElementsByClass(clef.Clef) if startClefs: clef1 = startClefs[0] c.parts[1].getElementsByClass(stream.Measure).first().remove(clef1) diff --git a/music21/base.py b/music21/base.py index cfc79d6f96..f58b9ea7a1 100644 --- a/music21/base.py +++ b/music21/base.py @@ -4301,8 +4301,12 @@ def testSitesClef(self): def testBeatAccess(self): '''Test getting beat data from various Music21Objects. ''' + from music21 import clef from music21 import corpus + from music21 import key + from music21 import meter from music21 import stream + s = corpus.parse('bach/bwv66.6.xml') p1 = s.parts['Soprano'] @@ -4311,18 +4315,18 @@ def testBeatAccess(self): # clef/ks can get its beat; these objects are in a pickup, # and this give their bar offset relative to the bar - eClef = p1.flatten().getElementsByClass('Clef').first() + eClef = p1.flatten().getElementsByClass(clef.Clef).first() self.assertEqual(eClef.beat, 4.0) self.assertEqual(eClef.beatDuration.quarterLength, 1.0) self.assertEqual(eClef.beatStrength, 0.25) - eKS = p1.flatten().getElementsByClass('KeySignature').first() + eKS = p1.flatten().getElementsByClass(key.KeySignature).first() self.assertEqual(eKS.beat, 4.0) self.assertEqual(eKS.beatDuration.quarterLength, 1.0) self.assertEqual(eKS.beatStrength, 0.25) # ts can get beatStrength, beatDuration - eTS = p1.flatten().getElementsByClass('TimeSignature').first() + eTS = p1.flatten().getElementsByClass(meter.TimeSignature).first() self.assertEqual(eTS.beatDuration.quarterLength, 1.0) self.assertEqual(eTS.beatStrength, 0.25) diff --git a/music21/capella/fromCapellaXML.py b/music21/capella/fromCapellaXML.py index 8924f2c733..4552ab1aca 100644 --- a/music21/capella/fromCapellaXML.py +++ b/music21/capella/fromCapellaXML.py @@ -203,7 +203,7 @@ def partScoreFromSystemScore(self, systemScore): if p is None: print('part entries do not match partDict!') continue - clefs = p.getElementsByClass('Clef') + clefs = p.getElementsByClass(clef.Clef) keySignatures = p.getElementsByClass('KeySignature') lastClef = None lastKeySignature = None @@ -219,7 +219,7 @@ def partScoreFromSystemScore(self, systemScore): lastKeySignature = ks p.makeMeasures(inPlace=True) # for m in p.getElementsByClass(stream.Measure): - # barLines = m.getElementsByClass('Barline') + # barLines = m.getElementsByClass(bar.Barline) # for bl in barLines: # blOffset = bl.offset # if blOffset == 0.0: diff --git a/music21/chord/__init__.py b/music21/chord/__init__.py index fce741c7f2..02ef08f690 100644 --- a/music21/chord/__init__.py +++ b/music21/chord/__init__.py @@ -6343,9 +6343,10 @@ def testScaleDegreesB(self): def testTiesA(self): # test creating independent ties for each Pitch + from music21 import chord from music21.musicxml import m21ToXml - c1 = Chord(['c', 'd', 'b']) + c1 = chord.Chord(['c', 'd', 'b']) # as this is a subclass of Note, we have a .tie attribute already # here, it is managed by a property self.assertEqual(c1.tie, None) @@ -6379,7 +6380,7 @@ def testTiesA(self): from music21.musicxml import testPrimitive from music21 import converter s = converter.parse(testPrimitive.chordIndependentTies) - chords = s.flatten().getElementsByClass('Chord') + chords = s.flatten().getElementsByClass(chord.Chord) # the middle pitch should have a tie self.assertEqual(chords[0].getTie(pitch.Pitch('a4')).type, 'start') self.assertEqual(chords[0].getTie(pitch.Pitch('c5')), None) diff --git a/music21/dynamics.py b/music21/dynamics.py index 942a3c126d..d6d100ca73 100644 --- a/music21/dynamics.py +++ b/music21/dynamics.py @@ -451,11 +451,13 @@ def testBasic(self): def testCorpusDynamicsWedge(self): from music21 import corpus + from music21 import dynamics + a = corpus.parse('opus41no1/movement2') # has dynamics! - b = a.parts[0].flatten().getElementsByClass('Dynamic') + b = a.parts[0].flatten().getElementsByClass(dynamics.Dynamic) self.assertEqual(len(b), 35) - b = a.parts[0].flatten().getElementsByClass('DynamicWedge') + b = a.parts[0].flatten().getElementsByClass(dynamics.DynamicWedge) self.assertEqual(len(b), 2) def testMusicxmlOutput(self): diff --git a/music21/figuredBass/realizer.py b/music21/figuredBass/realizer.py index 71e3f0a61c..c677d840ac 100644 --- a/music21/figuredBass/realizer.py +++ b/music21/figuredBass/realizer.py @@ -333,7 +333,7 @@ def generateBassLine(self): bl2 = bassLine.makeNotation(inPlace=False, cautionaryNotImmediateRepeat=False) if r is not None: m0 = bl2.getElementsByClass(stream.Measure).first() - m0.remove(m0.getElementsByClass('Rest').first()) + m0.remove(m0.getElementsByClass(note.Rest).first()) m0.padAsAnacrusis() return bl2 diff --git a/music21/freezeThaw.py b/music21/freezeThaw.py index 1a16fbeca8..e6ef627724 100644 --- a/music21/freezeThaw.py +++ b/music21/freezeThaw.py @@ -592,7 +592,7 @@ def findActiveStreamIdsInHierarchy( streamIds += spannerBundle.getSpannerStorageIds() if getVariants is True: - for el in streamObj.recurse(includeSelf=True).getElementsByClass('Variant'): + for el in streamObj.recurse(includeSelf=True).getElementsByClass(variant.Variant): streamIds += self.findActiveStreamIdsInHierarchy(el._stream) # should not happen that there are duplicates, but possible with spanners... @@ -1162,7 +1162,7 @@ def testFreezeThawVariant(self): variantName='rhythmic_switch', replacementDuration=3.0) # test Variant is in stream - unused_v1 = c.parts.first().getElementsByClass('Variant').first() + unused_v1 = c.parts.first().getElementsByClass(variant.Variant).first() sf = freezeThaw.StreamFreezer(c, fastButUnsafe=True) # sf.v = v @@ -1176,7 +1176,7 @@ def testFreezeThawVariant(self): s = st.stream # s.show('lily.pdf') p0 = s.parts[0] - variants = p0.getElementsByClass('Variant') + variants = p0.getElementsByClass(variant.Variant) v2 = variants[0] self.assertEqual(v2._stream[0][1].offset, 0.5) # v2.show('t') diff --git a/music21/graph/plot.py b/music21/graph/plot.py index a334358f81..44454bf259 100644 --- a/music21/graph/plot.py +++ b/music21/graph/plot.py @@ -1198,11 +1198,13 @@ def _getPartGroups(self): Examine the instruments in the Score and determine if there is a good match for a default configuration of parts. ''' + from music21 import instrument + if self.partGroups: return # keep what the user set if self.streamObj: return None - instStream = self.streamObj.flatten().getElementsByClass('Instrument') + instStream = self.streamObj.flatten().getElementsByClass(instrument.Instrument) if not instStream: return # do not set anything diff --git a/music21/layout.py b/music21/layout.py index 52aa32ee43..8a831e80fc 100644 --- a/music21/layout.py +++ b/music21/layout.py @@ -91,7 +91,7 @@ import unittest from collections import namedtuple -from typing import Tuple, Optional +from typing import Tuple, Optional, List from music21 import base from music21 import exceptions21 diff --git a/music21/lily/translate.py b/music21/lily/translate.py index be7f443dab..994f832d13 100644 --- a/music21/lily/translate.py +++ b/music21/lily/translate.py @@ -2148,7 +2148,7 @@ def lyPrefixCompositeMusicFromVariant(self, musicList = [] - varFilter = [r for r in variantObject.getElementsByClass('Rest') + varFilter = [r for r in variantObject.getElementsByClass(note.Rest) if r.style.hideObjectOnPrint] if varFilter: diff --git a/music21/midi/translate.py b/music21/midi/translate.py index 5f907b62fa..0cb18e47f2 100644 --- a/music21/midi/translate.py +++ b/music21/midi/translate.py @@ -3849,7 +3849,7 @@ def testImportChordVoiceA(self): fp = dirLib / 'test14.mid' s = converter.parse(fp) # three chords will be created, as well as two voices - self.assertEqual(len(s.flatten().getElementsByClass('Chord')), 3) + self.assertEqual(len(s[chord.Chord]), 3) self.assertEqual(len(s.parts.first().measure(3).voices), 2) def testImportChordsA(self): @@ -3861,7 +3861,7 @@ def testImportChordsA(self): # a simple file created in athenacl s = converter.parse(fp) # s.show('t') - self.assertEqual(len(s.flatten().getElementsByClass('Chord')), 5) + self.assertEqual(len(s[chord.Chord]), 5) def testMidiEventsImported(self): self.maxDiff = None @@ -4017,7 +4017,7 @@ def testRestsMadeInVoice(self): inn = converter.parse(fp) self.assertEqual( - len(inn.parts[1].measure(3).voices.last().getElementsByClass('Rest')), 1) + len(inn.parts[1].measure(3).voices.last().getElementsByClass(note.Rest)), 1) def testRestsMadeInMeasures(self): from music21 import converter diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 759aacfa02..f432f61ddc 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -531,7 +531,7 @@ def fromStream(self, st): st2 = stream.Part() st2.mergeAttributes(st) st2.elements = copy.deepcopy(st) - if not st.getElementsByClass('Clef').getElementsByOffset(0.0): + if not st.getElementsByClass(clef.Clef).getElementsByOffset(0.0): st2.clef = clef.bestClef(st2) st2.makeNotation(inPlace=True) st2.metadata = copy.deepcopy(getMetadataFromContext(st)) @@ -549,7 +549,7 @@ def fromStream(self, st): st2 = stream.Part() st2.mergeAttributes(st) st2.elements = copy.deepcopy(st) - if not st.getElementsByClass('Clef').getElementsByOffset(0.0): + if not st.getElementsByClass(clef.Clef).getElementsByOffset(0.0): bestClef = True else: bestClef = False @@ -559,7 +559,7 @@ def fromStream(self, st): else: # probably a problem? or a voice... - if not st.getElementsByClass('Clef').getElementsByOffset(0.0): + if not st.getElementsByClass(clef.Clef).getElementsByOffset(0.0): bestClef = True else: bestClef = False @@ -2751,7 +2751,7 @@ def fixupNotationMeasured(self): # that place key/clef information in the containing stream if hasattr(first_measure, 'clef') and first_measure.clef is None: first_measure.makeMutable() # must mutate - outerClefs = part.getElementsByClass('Clef') + outerClefs = part.getElementsByClass(clef.Clef) if outerClefs: first_measure.clef = outerClefs.first() @@ -6107,7 +6107,7 @@ def measureStyle(self): mxMeasureStyle = None mxMultipleRest = None - rests = m.getElementsByClass('Rest') + rests = m.getElementsByClass(note.Rest) if rests: hasMMR = rests[0].getSpannerSites('MultiMeasureRest') if hasMMR: diff --git a/music21/musicxml/testPrimitive.py b/music21/musicxml/testPrimitive.py index e9910a0183..8d0552cf24 100644 --- a/music21/musicxml/testPrimitive.py +++ b/music21/musicxml/testPrimitive.py @@ -18183,14 +18183,14 @@ def testMidMeasureClef1(self): orig_stream.repeatAppend(note.Note('C4'), 2) orig_stream.append(clef.BassClef()) orig_stream.repeatAppend(note.Note('C4'), 2) - orig_clefs = orig_stream.flatten().getElementsByClass('Clef') + orig_clefs = orig_stream.flatten().getElementsByClass(clef.Clef) xml = musicxml.m21ToXml.GeneralObjectExporter().parse(orig_stream) self.assertEqual(xml.count(b''), 2) # clefs got out self.assertEqual(xml.count(b'') self.assertEqual(firstChord.offset, 1.0) diff --git a/music21/repeat.py b/music21/repeat.py index e196822104..0ec187e629 100644 --- a/music21/repeat.py +++ b/music21/repeat.py @@ -481,7 +481,7 @@ def insertRepeat(s, start, end, *, inPlace=False): >>> len(s.parts[0].flatten().getElementsByClass(bar.Repeat)) 2 - >>> len(s.flatten().getElementsByClass(bar.Repeat)) + >>> len(s[bar.Repeat]) 8 >>> s.parts[0].measure(3).leftBarline.direction 'start' @@ -724,6 +724,7 @@ def _setup(self): run several setup routines. ''' from music21 import stream + # get and store the source measure count; this is presumed to # be a Stream with Measures self._srcMeasureStream = self._src.getElementsByClass(stream.Measure).stream() @@ -3208,7 +3209,7 @@ def testRepeatExpressionOnStream(self): template.append(m) s = copy.deepcopy(template) s[3].insert(0, repeat.DaCapo()) - self.assertEqual(len(s.flatten().getElementsByClass(repeat.DaCapo)), 1) + self.assertEqual(len(s[repeat.DaCapo]), 1) raw = GEX.parse(s).decode('utf-8') @@ -3218,7 +3219,7 @@ def testRepeatExpressionOnStream(self): s = copy.deepcopy(template) s[0].timeSignature = meter.TimeSignature('4/4') s[3].insert(0, expressions.TextExpression('da capo')) - self.assertEqual(len(s.flatten().getElementsByClass(repeat.DaCapo)), 0) + self.assertEqual(len(s[repeat.DaCapo]), 0) raw = GEX.parse(s).decode('utf-8') self.assertGreater(raw.find('da capo'), 0, raw) @@ -3754,23 +3755,23 @@ def testExpandRepeatsImportedA(self): Also has grace notes, so it tests our importing of grace notes ''' - from music21 import corpus from music21 import stream + s = corpus.parse('ryansMammoth/BanjoReel') # s.show('text') self.assertEqual(len(s.parts), 1) self.assertEqual(len(s.parts[0].getElementsByClass(stream.Measure)), 11) self.assertEqual(len(s.parts[0].flatten().notes), 58) - bars = s.parts[0].flatten().getElementsByClass('Barline') + bars = s.parts[0][bar.Barline] self.assertEqual(len(bars), 3) s2 = s.expandRepeats() # s2.show('text') self.assertEqual(len(s2.parts[0].getElementsByClass(stream.Measure)), 20) - self.assertEqual(len(s2.parts[0].flatten().notes), 105) + self.assertEqual(len(s2.parts[0].recurse().notes), 105) def testExpandRepeatsImportedB(self): from music21 import corpus @@ -3795,10 +3796,10 @@ def testExpandRepeatsImportedC(self): from music21 import converter from music21.musicxml import testPrimitive s = converter.parse(testPrimitive.repeatExpressionsA) - self.assertEqual(len(s.flatten().getElementsByClass('RepeatExpression')), 3) + self.assertEqual(len(s['RepeatExpression']), 3) s = converter.parse(testPrimitive.repeatExpressionsB) - self.assertEqual(len(s.flatten().getElementsByClass('RepeatExpression')), 3) + self.assertEqual(len(s['RepeatExpression']), 3) # s.show() diff --git a/music21/roman.py b/music21/roman.py index c0d8674ce8..15cac563bb 100644 --- a/music21/roman.py +++ b/music21/roman.py @@ -3685,7 +3685,7 @@ def testYieldRemoveA(self): s.append(p) targetCount = 1 self.assertEqual( - len(s.flatten().getElementsByClass('KeySignature')), + len(s['KeySignature']), targetCount, ) # through sequential iteration diff --git a/music21/romanText/clercqTemperley.py b/music21/romanText/clercqTemperley.py index 710350bc94..eaf4b22420 100644 --- a/music21/romanText/clercqTemperley.py +++ b/music21/romanText/clercqTemperley.py @@ -986,7 +986,7 @@ def x_testA(self): # txt = f.read() # # s = clercqTemperley.CTSong(txt) - # for chord in s.toScore().flatten().getElementsByClass('Chord'): + # for chord in s.toScore().flatten().getElementsByClass(chord.Chord): # try: # x = chord.pitches # except: diff --git a/music21/stream/base.py b/music21/stream/base.py index 748594e47f..fc294b719f 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -980,7 +980,7 @@ def clef(self) -> Optional['music21.clef.Clef']: {0.0} {0.0} ''' - clefList = self.getElementsByClass('Clef').getElementsByOffset(0) + clefList = self.getElementsByClass(clef.Clef).getElementsByOffset(0) # casting to list added 20microseconds... return clefList.first() @@ -5180,6 +5180,8 @@ def toWrittenPitch(self, *, inPlace=False): v.3 -- inPlace defaults to False v.5 -- returns None if inPlace=True ''' + from music21 import spanner + if not inPlace: # make a copy returnObj = self.coreCopyAsDerivation('toWrittenPitch') else: @@ -5200,7 +5202,7 @@ def toWrittenPitch(self, *, inPlace=False): for container in returnObj.recurse(streamsOnly=True, includeSelf=True): container.atSoundingPitch = False - for ottava in returnObj.recurse().getElementsByClass('Ottava'): + for ottava in returnObj[spanner.Ottava]: ottava.undoTransposition() if not inPlace: @@ -13395,7 +13397,7 @@ def measure(self, >>> lastChord - Note that we still do a .getElementsByClass('Chord') since many pieces end + Note that we still do a .getElementsByClass(chord.Chord) since many pieces end with nothing but a rest... ''' if measureNumber < 0: diff --git a/music21/stream/iterator.py b/music21/stream/iterator.py index 08fdd461ae..5e309e9552 100644 --- a/music21/stream/iterator.py +++ b/music21/stream/iterator.py @@ -458,14 +458,14 @@ def __bool__(self) -> bool: >>> bool(iterator) True - >>> bool(iterator.getElementsByClass('Chord')) + >>> bool(iterator.getElementsByClass(chord.Chord)) False test false cache: - >>> len(iterator.getElementsByClass('Chord')) + >>> len(iterator.getElementsByClass(chord.Chord)) 0 - >>> bool(iterator.getElementsByClass('Chord')) + >>> bool(iterator.getElementsByClass(chord.Chord)) False ''' @@ -809,7 +809,7 @@ def stream(self, returnStreamSubClass=True) -> Union['music21.stream.Stream', St >>> s3.elementOffset(b, returnSpecial=True) - >>> s4 = s.iter().getElementsByClass('Barline').stream() + >>> s4 = s.iter().getElementsByClass(bar.Barline).stream() >>> s4.show('t') {0.0} @@ -1008,7 +1008,7 @@ def getElementsByClass(self: _SIter, >>> r = note.Rest() >>> s.append(r) >>> s.append(note.Note('D')) - >>> for el in s.iter().getElementsByClass('Rest'): + >>> for el in s.iter().getElementsByClass(note.Rest): ... print(el) @@ -1020,7 +1020,7 @@ def getElementsByClass(self: _SIter, >>> r.activeSite.id 's2' - >>> for el in s.iter().getElementsByClass('Rest'): + >>> for el in s.iter().getElementsByClass(note.Rest): ... print(el.activeSite.id) s1 @@ -1515,7 +1515,7 @@ class OffsetIterator(StreamIterator[M21ObjType]): [] [, ] - >>> for groupedElements in stream.iterator.OffsetIterator(s).getElementsByClass('Clef'): + >>> for groupedElements in stream.iterator.OffsetIterator(s).getElementsByClass(clef.Clef): ... print(groupedElements) [] ''' diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py index 71208c7383..4a3bb798e9 100644 --- a/music21/stream/makeNotation.py +++ b/music21/stream/makeNotation.py @@ -489,7 +489,7 @@ def makeMeasures( # del clefList clefObj = srcObj.clef or srcObj.getContextByClass('Clef') if clefObj is None: - clefObj = srcObj.getElementsByClass('Clef').getElementsByOffset(0).first() + clefObj = srcObj.getElementsByClass(clef.Clef).getElementsByOffset(0).first() # only return clefs that have offset = 0.0 if not clefObj: clefObj = clef.bestClef(srcObj, recurse=True) diff --git a/music21/stream/tests.py b/music21/stream/tests.py index 490772027d..1722e39f88 100644 --- a/music21/stream/tests.py +++ b/music21/stream/tests.py @@ -1433,7 +1433,7 @@ def testStripTiesChords(self): m.append(c) p.append(m) p2 = p.stripTies(matchByPitch=True) - chordsOut = list(p2.flatten().getElementsByClass('Chord')) + chordsOut = list(p2.flatten().getElementsByClass(chord.Chord)) self.assertEqual(len(chordsOut), 5) self.assertEqual(chordsOut[0].pitches, ch0.pitches) self.assertEqual(chordsOut[0].duration.quarterLength, 2.0) @@ -2319,7 +2319,7 @@ def testMakeMeasuresLastElementNoDuration(self): obj = expressions.TextExpression('FREEZE') s.insert(3, obj) s.makeMeasures(inPlace=True) - self.assertEqual(len(s.flatten().getElementsByClass('Expression')), 1) + self.assertEqual(len(s['Expression']), 1) def testRemove(self): '''Test removing components from a Stream. @@ -2393,7 +2393,7 @@ def testReplaceA1(self): sBach = corpus.parse('bach/bwv324.xml') partSoprano = sBach.parts.first() - c1 = partSoprano.flatten().getElementsByClass('Clef').first() + c1 = partSoprano.flatten().getElementsByClass(clef.Clef).first() self.assertIsInstance(c1, clef.TrebleClef) # now, replace with a different clef @@ -2401,7 +2401,7 @@ def testReplaceA1(self): partSoprano.flatten().replace(c1, c2, allDerived=True) # all views of the Stream have been updated - cTest = sBach.parts.first().flatten().getElementsByClass('Clef').first() + cTest = sBach.parts.first().flatten().getElementsByClass(clef.Clef).first() self.assertIsInstance(cTest, clef.AltoClef) def testReplaceB(self): @@ -2650,7 +2650,7 @@ def testMakeAccidentalsA(self): def testMakeAccidentalsB(self): s = corpus.parse('monteverdi/madrigal.5.3.rntxt') m34 = s.parts[0].getElementsByClass(Measure)[33] - c = m34.getElementsByClass('Chord') + c = m34.getElementsByClass(chord.Chord) # assuming not showing accidental b/c of key self.assertEqual(str(c[1].pitches), '(, ' + ', )') @@ -2659,7 +2659,7 @@ def testMakeAccidentalsB(self): s = corpus.parse('monteverdi/madrigal.5.4.rntxt') m74 = s.parts[0].getElementsByClass(Measure)[73] - c = m74.getElementsByClass('Chord') + c = m74.getElementsByClass(chord.Chord) # has correct pitches but natural not showing on C self.assertEqual(str(c[0].pitches), '(, , ' @@ -4326,7 +4326,7 @@ def testMakeNotationScoreA(self): self.assertEqual(len(post.getElementsByClass( 'Stream')[1].getElementsByClass(Measure)), 3) self.assertEqual(len(post.flatten().getElementsByClass('TimeSignature')), 2) - self.assertEqual(len(post.flatten().getElementsByClass('Clef')), 2) + self.assertEqual(len(post.flatten().getElementsByClass(clef.Clef)), 2) def testMakeNotationScoreB(self): '''Test makeNotation on Score objects @@ -4356,7 +4356,7 @@ def testMakeNotationScoreB(self): 'Stream')[1].getElementsByClass(Measure)), 4) self.assertEqual(len(post.flatten().getElementsByClass('TimeSignature')), 2) - self.assertEqual(len(post.flatten().getElementsByClass('Clef')), 2) + self.assertEqual(len(post.flatten().getElementsByClass(clef.Clef)), 2) def testMakeNotationScoreC(self): '''Test makeNotation on Score objects @@ -4384,7 +4384,7 @@ def testMakeNotationScoreC(self): Stream)[1].getElementsByClass(Measure)), 3) self.assertEqual(len(post.flatten().getElementsByClass('TimeSignature')), 2) - self.assertEqual(len(post.flatten().getElementsByClass('Clef')), 2) + self.assertEqual(len(post.flatten().getElementsByClass(clef.Clef)), 2) def testMakeNotationKeySignatureOneVoice(self): ''' @@ -4641,17 +4641,17 @@ def testMakeChordsBuiltA(self): s.insert(o, n) o += ql self.assertEqual(len(s), 9) - self.assertEqual(len(s.getElementsByClass('Chord')), 0) + self.assertEqual(len(s.getElementsByClass(chord.Chord)), 0) # do both in place and not in place, compare results sMod = s.chordify() s = s.chordify() for sEval in [s, sMod]: - self.assertEqual(len(sEval.getElementsByClass('Chord')), 3) + self.assertEqual(len(sEval.getElementsByClass(chord.Chord)), 3) # make sure we have all the original pitches for i in range(len(pitchCol)): match = [p.nameWithOctave for p in - sEval.getElementsByClass('Chord')[i].pitches] + sEval.getElementsByClass(chord.Chord)[i].pitches] self.assertEqual(match, list(pitchCol[i])) # print('post chordify') # s.show('t') @@ -4682,7 +4682,7 @@ def testMakeChordsBuiltB(self): s = s.chordify() for sEval in [s, sMod]: # of these 6 chords, only 2 have more than one note - self.assertEqual(len(sEval.getElementsByClass('Chord')), 6) + self.assertEqual(len(sEval.getElementsByClass(chord.Chord)), 6) self.assertEqual([c.offset for c in sEval], [0.0, 1.0, 1.5, 2.0, 3.0, 3.5]) # do the same, but reverse the short/long duration relation @@ -4733,19 +4733,25 @@ def testMakeChordsBuiltC(self): s1.insert(0.5, n6) sMod = s1.chordify(removeRedundantPitches=True) - self.assertEqual([p.nameWithOctave for p in sMod.getElementsByClass('Chord')[0].pitches], + self.assertEqual([p.nameWithOctave + for p in sMod.getElementsByClass(chord.Chord)[0].pitches], ['C2', 'G2']) - self.assertEqual([p.nameWithOctave for p in sMod.getElementsByClass('Chord')[1].pitches], + self.assertEqual([p.nameWithOctave + for p in sMod.getElementsByClass(chord.Chord)[1].pitches], ['E4', 'F#4']) # without redundant pitch gathering sMod = s1.chordify(removeRedundantPitches=False) - self.assertEqual([p.nameWithOctave for p in sMod.getElementsByClass('Chord')[0].pitches], - ['C2', 'C2', 'G2']) + self.assertEqual( + [p.nameWithOctave for p in sMod.getElementsByClass(chord.Chord)[0].pitches], + ['C2', 'C2', 'G2'] + ) - self.assertEqual([p.nameWithOctave for p in sMod.getElementsByClass('Chord')[1].pitches], - ['E4', 'E4', 'F#4']) + self.assertEqual( + [p.nameWithOctave for p in sMod.getElementsByClass(chord.Chord)[1].pitches], + ['E4', 'E4', 'F#4'] + ) def testMakeChordsBuiltD(self): # attempt to isolate case @@ -4776,8 +4782,8 @@ def testMakeChordsBuiltD(self): post = s.flatten().chordify() # post.show('t') - self.assertEqual(len(post.getElementsByClass('Rest')), 1) - self.assertEqual(len(post.getElementsByClass('Chord')), 6) + self.assertEqual(len(post.getElementsByClass(note.Rest)), 1) + self.assertEqual(len(post.getElementsByClass(chord.Chord)), 6) # post.show() def testGetElementAtOrBeforeBarline(self): @@ -4851,7 +4857,7 @@ def testElementsHighestTimeA(self): self.assertEqual(s.getElementAfterElement(n2), b1) # try to get elements by class - sub1 = s.getElementsByClass('Barline').stream() + sub1 = s.getElementsByClass(bar.Barline).stream() self.assertEqual(len(sub1), 1) # only found item is barline self.assertEqual(sub1[0], b1) @@ -5350,7 +5356,7 @@ def testChordifyImported(self): 39.0, 40.0, 40.5, 41.0, 42.0, 43.5, 45.0, 45.5, 46.0, 46.5, 47.0, 47.5, 48.0, 49.5, 51.0, 51.5, 52.0, 52.5, 53.0, 53.5, 54.0, 54.5, 55.0, 55.5, 56.0, 56.5, 57.0, 58.5, 59.5]) - self.assertEqual(len(post.flatten().getElementsByClass('Chord')), 71) + self.assertEqual(len(post[chord.Chord]), 71) # Careful! one version of the caching is screwing up m. 20 which definitely should # not have rests in it -- was creating 69 notes, not 71. @@ -5429,8 +5435,8 @@ def testChordifyA(self): s.insert(0, p1) s.insert(0, p2) post = s.chordify() - self.assertEqual(len(post.getElementsByClass('Chord')), 12) - self.assertEqual(str(post.getElementsByClass('Chord').first().pitches), + self.assertEqual(len(post.getElementsByClass(chord.Chord)), 12) + self.assertEqual(str(post.getElementsByClass(chord.Chord).first().pitches), '(, )') p1 = Part() @@ -5445,8 +5451,8 @@ def testChordifyA(self): s.insert(0, p1) s.insert(0, p2) post = s.chordify() - self.assertEqual(len(post.getElementsByClass('Chord')), 2) - self.assertEqual(str(post.getElementsByClass('Chord').first().pitches), + self.assertEqual(len(post.getElementsByClass(chord.Chord)), 2) + self.assertEqual(str(post.getElementsByClass(chord.Chord).first().pitches), '(, )') # post.show() @@ -5511,7 +5517,7 @@ def testChordifyC(self): self.assertFalse(m.hasVoices()) match.append(len(m.pitches)) self.assertEqual(match, [3, 9, 9, 25, 25, 21, 12, 6, 21, 29]) - self.assertEqual(len(post.flatten().getElementsByClass('Rest')), 4) + self.assertEqual(len(post.flatten().getElementsByClass(note.Rest)), 4) def testChordifyD(self): # test on a Stream of Streams. @@ -5524,7 +5530,7 @@ def testChordifyD(self): s3.insert(0, s2) post = s3.chordify() - self.assertEqual(len(post.getElementsByClass('Chord')), 8) + self.assertEqual(len(post.getElementsByClass(chord.Chord)), 8) def testChordifyE(self): s1 = Stream() @@ -5541,7 +5547,7 @@ def testChordifyE(self): # s1.show() post = s1.chordify() # post.show() - self.assertEqual(len(post.flatten().getElementsByClass('Chord')), 8) + self.assertEqual(len(post[chord.Chord]), 8) # noinspection SpellCheckingInspection def testOpusSearch(self): @@ -5775,7 +5781,7 @@ def testPartsToVoicesA(self): self.assertEqual(len(s1.parts), 2) p1 = s1.parts[0] - self.assertEqual(len(p1.flatten().getElementsByClass('Clef')), 1) + self.assertEqual(len(p1.flatten().getElementsByClass(clef.Clef)), 1) # p1.show('t') # look at individual measure; check counts; these should not @@ -5793,7 +5799,7 @@ def testPartsToVoicesA(self): # NOTE: we no longer get Clef here, as we return clefs in the # Part outside a Measure when using measures() # m = p1.measure(2) - # self.assertEqual(len(m1.flatten().getElementsByClass('Clef')), 1) + # self.assertEqual(len(m1.flatten().getElementsByClass(clef.Clef)), 1) # look at individual measure; check counts; these should not # change after measure extraction @@ -5808,9 +5814,9 @@ def testPartsToVoicesA(self): # m2Raw.show('t') - # self.assertEqual(len(m1.flatten().getElementsByClass('Clef')), 1) + # self.assertEqual(len(m1.flatten().getElementsByClass(clef.Clef)), 1) ex1 = p1.measures(1, 3) - self.assertEqual(len(ex1.flatten().getElementsByClass('Clef')), 1) + self.assertEqual(len(ex1.flatten().getElementsByClass(clef.Clef)), 1) # ex1.show() @@ -6175,7 +6181,7 @@ def testDerivationA(self): self.assertIs(s3.derivation.origin, s1) self.assertIsNot(s3.derivation.origin, s2) - s4 = s3.getElementsByClass('Chord').stream() + s4 = s3.getElementsByClass(chord.Chord).stream() self.assertEqual(len(s4), 10) self.assertIs(s4.derivation.origin, s3) @@ -6223,7 +6229,10 @@ def testDerivationA(self): self.assertEqual(mRange.derivation.rootDerivation, p1) self.assertEqual(mRange.flatten().notesAndRests.stream().derivation.rootDerivation, p1) - self.assertIs(s.flatten().getElementsByClass('Rest').stream().derivation.rootDerivation, s) + self.assertIs( + s.flatten().getElementsByClass(note.Rest).stream().derivation.rootDerivation, + s + ) # As of v3, we CAN use the activeSite to get the Part from the Measure, as # the activeSite was not set when doing the getElementsByClass operation @@ -6412,10 +6421,10 @@ def testRecurseA(self): def testRecurseB(self): s = corpus.parse('madrigal.5.8.rntxt') - self.assertEqual(len(s.flatten().getElementsByClass('KeySignature')), 1) + self.assertEqual(len(s['KeySignature']), 1) for e in s.recurse(classFilter='KeySignature'): e.activeSite.remove(e) - self.assertEqual(len(s.flatten().getElementsByClass('KeySignature')), 0) + self.assertEqual(len(s['KeySignature']), 0) def testTransposeScore(self): @@ -7191,7 +7200,7 @@ def testChordifyTagPartA(self): # test that each note has its original group idA = [] idB = [] - for c in post.flatten().getElementsByClass('Chord'): + for c in post.flatten().getElementsByClass(chord.Chord): for p in c.pitches: if 'a' in p.groups: idA.append(p.name) @@ -7208,7 +7217,7 @@ def testChordifyTagPartB(self): idBass = [] post = s.chordify(addPartIdAsGroup=True, removeRedundantPitches=False) - for c in post.flatten().getElementsByClass('Chord'): + for c in post.flatten().getElementsByClass(chord.Chord): for p in c.pitches: if 'Soprano' in p.groups: idSoprano.append(p.name) @@ -7389,7 +7398,7 @@ def testExtendTiesB(self): sChords = s.measures(9, 9).chordify() sChords.extendTies() post = [] - for ch in sChords.flatten().getElementsByClass('Chord'): + for ch in sChords.flatten().getElementsByClass(chord.Chord): post.append([repr(n.tie) for n in ch]) self.assertEqual(post, @@ -7550,7 +7559,7 @@ def testMeasuresA(self): s = corpus.parse('bwv66.6') ex = s.parts[0].measures(3, 6) - self.assertEqual(str(ex.flatten().getElementsByClass('Clef')[0]), + self.assertEqual(str(ex.flatten().getElementsByClass(clef.Clef)[0]), '') self.assertEqual(str(ex.flatten().getElementsByClass('Instrument')[0]), 'P1: Soprano: Instrument 1') @@ -7567,7 +7576,7 @@ def testMeasuresA(self): r = note.Rest(quarterLength=n.quarterLength) m.insert(o, r) # s.parts[0].show() - self.assertEqual(len(ex.flatten().getElementsByClass('Rest')), 5) + self.assertEqual(len(ex[note.Rest]), 5) def testMeasuresB(self): s = corpus.parse('luca/gloria') @@ -7602,7 +7611,7 @@ def testMeasuresC(self): n.activeSite.remove(n) r = note.Rest(quarterLength=n.quarterLength) site.insert(o, r) - self.assertEqual(len(ex.flatten().getElementsByClass('Rest')), 5) + self.assertEqual(len(ex[note.Rest]), 5) # ex.show() def testMeasuresSuffix(self): @@ -7700,7 +7709,7 @@ def testChordifyF(self): # there should only be 2 tuplet indications in the produced chords: start and stop... self.assertEqual(raw.count(' Date: Wed, 27 Apr 2022 01:17:14 -1000 Subject: [PATCH 4/6] faster on the bug fix. more bugs? --- music21/analysis/discrete.py | 2 +- music21/capella/fromCapellaXML.py | 4 ++-- music21/figuredBass/checker.py | 8 ++++---- music21/midi/translate.py | 8 ++++---- music21/musicxml/m21ToXml.py | 8 ++++---- music21/musicxml/partStaffExporter.py | 2 +- music21/musicxml/testPrimitive.py | 4 ++-- music21/musicxml/test_xmlToM21.py | 18 +++++++++--------- music21/repeat.py | 1 + music21/stream/base.py | 2 +- 10 files changed, 29 insertions(+), 28 deletions(-) diff --git a/music21/analysis/discrete.py b/music21/analysis/discrete.py index 7f1779f590..ff7fc0e2e1 100644 --- a/music21/analysis/discrete.py +++ b/music21/analysis/discrete.py @@ -1233,7 +1233,7 @@ def countMelodicIntervals(self, sStream, found=None, ignoreDirection=True, ignor # if this has parts, need to move through each at a time if sStream.hasPartLikeStreams(): - procList = list(sStream.getElementsByClass('Stream')) + procList = list(sStream.getElementsByClass(stream.Stream)) else: # assume a single list of notes, or sStream is a part procList = [sStream] diff --git a/music21/capella/fromCapellaXML.py b/music21/capella/fromCapellaXML.py index 4552ab1aca..02fa1d88ff 100644 --- a/music21/capella/fromCapellaXML.py +++ b/music21/capella/fromCapellaXML.py @@ -175,13 +175,13 @@ def partScoreFromSystemScore(self, systemScore): ''' # this line is redundant currently, since all we have in systemScore # are Systems, but later there will be other things. - systemStream = systemScore.getElementsByClass('System') + systemStream = systemScore.getElementsByClass(layout.System) partDictById = {} for thisSystem in systemStream: # this line is redundant currently, since all we have in # thisSystem are Parts, but later there will be other things. systemOffset = systemScore.elementOffset(thisSystem) - partStream = thisSystem.getElementsByClass('Part') + partStream = thisSystem.getElementsByClass(stream.Part) for j, thisPart in enumerate(partStream): if thisPart.id not in partDictById: newPart = stream.Part() diff --git a/music21/figuredBass/checker.py b/music21/figuredBass/checker.py index 7efd342fc1..bf9be20733 100644 --- a/music21/figuredBass/checker.py +++ b/music21/figuredBass/checker.py @@ -45,7 +45,7 @@ def getVoiceLeadingMoments(music21Stream): :width: 700 ''' allHarmonies = extractHarmonies(music21Stream) - allParts = music21Stream.getElementsByClass('Part').stream() + allParts = music21Stream.getElementsByClass(stream.Part).stream() newParts = [allParts[i].flatten().getElementsNotOfClass('GeneralNote').stream() for i in range(len(allParts))] paddingLeft = allParts[0].getElementsByClass(stream.Measure).first().paddingLeft @@ -107,7 +107,7 @@ def extractHarmonies(music21Stream): (11.0, 11.5) [ ] (11.5, 12.0) [ ] ''' - allParts = music21Stream.getElementsByClass('Part') + allParts = music21Stream.getElementsByClass(stream.Part) if len(allParts) < 2: raise Music21Exception('There must be at least two parts to extract harmonies') allHarmonies = createOffsetMapping(allParts[0]) @@ -247,7 +247,7 @@ def checkSinglePossibilities(music21Stream, functionToApply, color="#FF0000", de debugInfo.append(f"{'(Offset, End Time):'!s:25}Part Numbers:") allHarmonies = sorted(list(extractHarmonies(music21Stream).items())) - allParts = [p.flatten() for p in music21Stream.getElementsByClass('Part')] + allParts = [p.flatten() for p in music21Stream.getElementsByClass(stream.Part)] for (offsets, notes) in allHarmonies: vlm = [generalNoteToPitch(n) for n in notes] vlm_violations = functionToApply(vlm) @@ -314,7 +314,7 @@ def checkConsecutivePossibilities(music21Stream, functionToApply, color="#FF0000 debugInfo.append('(Offset A, End Time A): (Offset B, End Time B): Part Numbers:') allHarmonies = sorted(extractHarmonies(music21Stream).items()) - allParts = [p.flatten() for p in music21Stream.getElementsByClass('Part')] + allParts = [p.flatten() for p in music21Stream.getElementsByClass(stream.Part)] (previousOffsets, previousNotes) = allHarmonies[0] vlmA = [generalNoteToPitch(n) for n in previousNotes] initOffsetA = previousOffsets[0] diff --git a/music21/midi/translate.py b/music21/midi/translate.py index 0cb18e47f2..aefd530ead 100644 --- a/music21/midi/translate.py +++ b/music21/midi/translate.py @@ -2165,7 +2165,7 @@ def prepareStreamForMidi(s) -> stream.Stream: # this assumes that dynamics in a part/stream apply to all components # of that part stream # this sets the cachedRealized value for each Volume - for p in s.getElementsByClass('Stream'): + for p in s.getElementsByClass(stream.Stream): volume.realizeVolume(p) s.insert(0, conductor) @@ -2224,7 +2224,7 @@ def conductorStream(s: stream.Stream) -> stream.Part: ''' from music21 import tempo from music21 import meter - partsList = list(s.getElementsByClass('Stream').getElementsByOffset(0)) + partsList = list(s.getElementsByClass(stream.Stream).getElementsByOffset(0)) minPriority = min(p.priority for p in partsList) if partsList else 0 conductorPriority = minPriority - 1 @@ -2310,7 +2310,7 @@ def channelInstrumentData( # store streams in uniform list substreamList = [] if s.hasPartLikeStreams(): - for obj in s.getElementsByClass('Stream'): + for obj in s.getElementsByClass(stream.Stream): # Conductor track: don't consume a channel if (not obj[note.GeneralNote]) and obj[Conductor]: continue @@ -2546,7 +2546,7 @@ def streamHierarchyToMidiTracks( # store streams in uniform list: prepareStreamForMidi() ensures there are substreams substreamList = [] - for obj in s.getElementsByClass('Stream'): + for obj in s.getElementsByClass(stream.Stream): # prepareStreamForMidi() supplies defaults for these if obj.getElementsByClass(('MetronomeMark', 'TimeSignature')): # Ensure conductor track is first diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index f432f61ddc..ffc5fb7406 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -545,7 +545,7 @@ def fromStream(self, st): st2.metadata = copy.deepcopy(getMetadataFromContext(st)) return self.fromScore(st2) - elif st.getElementsByClass('Stream').first().isFlat: # like a part w/ measures... + elif st.getElementsByClass(stream.Stream).first().isFlat: # like a part w/ measures... st2 = stream.Part() st2.mergeAttributes(st) st2.elements = copy.deepcopy(st) @@ -1572,7 +1572,7 @@ def setPartsAndRefStream(self): ''' s = self.stream # environLocal.printDebug('streamToMx(): interpreting multipart') - streamOfStreams = s.getElementsByClass('Stream') + streamOfStreams = s.getElementsByClass(stream.Stream) for innerStream in streamOfStreams: # may need to copy element here # apply this stream's offset to elements @@ -6490,11 +6490,11 @@ def setMxPrint(self): return mxPrint = None - found = m.getElementsByClass('PageLayout') + found = m.getElementsByClass(layout.PageLayout) if found: pl = found[0] # assume only one per measure mxPrint = self.pageLayoutToXmlPrint(pl) - found = m.getElementsByClass('SystemLayout') + found = m.getElementsByClass(layout.SystemLayout) if found: sl = found[0] # assume only one per measure if mxPrint is None: diff --git a/music21/musicxml/partStaffExporter.py b/music21/musicxml/partStaffExporter.py index 77ce777f8d..a3b32b21b3 100644 --- a/music21/musicxml/partStaffExporter.py +++ b/music21/musicxml/partStaffExporter.py @@ -225,7 +225,7 @@ def joinableGroups(self) -> List[StaffGroup]: <... p2b>>, <... p6b>>] ''' - staffGroups = self.stream.getElementsByClass('StaffGroup') + staffGroups = self.stream.getElementsByClass(spanner.StaffGroup) joinableGroups: List[StaffGroup] = [] # Joinable groups must consist of only PartStaffs with Measures # and exist in self.stream diff --git a/music21/musicxml/testPrimitive.py b/music21/musicxml/testPrimitive.py index 8d0552cf24..f89093b4f2 100644 --- a/music21/musicxml/testPrimitive.py +++ b/music21/musicxml/testPrimitive.py @@ -18221,13 +18221,13 @@ def testMidMeasureClefs2(self): orig_stream[1].append(item) orig_clefs = [staff.flatten().getElementsByClass(clef.Clef).stream() for staff in - orig_stream.getElementsByClass('Part')] + orig_stream.getElementsByClass(stream.Part)] xml = musicxml.m21ToXml.GeneralObjectExporter().parse(orig_stream) new_stream = converter.parse(xml.decode('utf-8')) new_clefs = [staff.flatten().getElementsByClass(clef.Clef).stream() for staff in - new_stream.getElementsByClass('Part')] + new_stream.getElementsByClass(stream.Part)] self.assertEqual([len(clefs) for clefs in new_clefs], [len(clefs) for clefs in orig_clefs]) diff --git a/music21/musicxml/test_xmlToM21.py b/music21/musicxml/test_xmlToM21.py index 6457fef190..5720cedc24 100644 --- a/music21/musicxml/test_xmlToM21.py +++ b/music21/musicxml/test_xmlToM21.py @@ -66,7 +66,7 @@ def testBarRepeatConversion(self): # this is a good example with repeats s = corpus.parse('k80/movement3') for p in s.parts: - post = p.recurse().getElementsByClass(bar.Repeat) + post = p[bar.Repeat] self.assertEqual(len(post), 6) # a = corpus.parse('opus41no1/movement3') @@ -332,7 +332,7 @@ def testImportMetronomeMarksA(self): # has metronome marks defined, not with sound tag s = converter.parse(testPrimitive.metronomeMarks31c) # get all tempo indications - mms = s.flatten().getElementsByClass('TempoIndication') + mms = s[meter.TempoIndication] self.assertGreater(len(mms), 3) def testImportMetronomeMarksB(self): @@ -392,7 +392,7 @@ def testStaffGroupsA(self): from music21 import converter s = converter.parse(testPrimitive.staffGroupsNested41d) - staffGroups = s.getElementsByClass('StaffGroup') + staffGroups = s.getElementsByClass(spanner.StaffGroup) # staffGroups.show() self.assertEqual(len(staffGroups), 2) @@ -411,7 +411,7 @@ def testStaffGroupsPiano(self): from music21 import converter s = converter.parse(testPrimitive.pianoStaff43a) - sgs = s.getElementsByClass('StaffGroup') + sgs = s.getElementsByClass(spanner.StaffGroup) self.assertEqual(len(sgs), 1) self.assertEqual(sgs[0].symbol, 'brace') self.assertIs(sgs[0].barTogether, True) @@ -727,10 +727,10 @@ def testChordSymbolException(self): def testStaffLayout(self): from music21 import corpus c = corpus.parse('demos/layoutTest.xml') - layouts = c.flatten().getElementsByClass('LayoutBase').stream() - systemLayouts = layouts.getElementsByClass('SystemLayout') + layouts = c.flatten().getElementsByClass(layout.LayoutBase).stream() + systemLayouts = layouts.getElementsByClass(layout.SystemLayout) self.assertEqual(len(systemLayouts), 42) - staffLayouts = layouts.getElementsByClass('StaffLayout') + staffLayouts = layouts.getElementsByClass(layout.StaffLayout) self.assertEqual(len(staffLayouts), 20) pageLayouts = layouts.getElementsByClass('PageLayout') self.assertEqual(len(pageLayouts), 10) @@ -754,9 +754,9 @@ def testStaffLayout(self): def testStaffLayoutMore(self): from music21 import corpus c = corpus.parse('demos/layoutTestMore.xml') - layouts = c.flatten().getElementsByClass('LayoutBase').stream() + layouts = c.flatten().getElementsByClass(layout.LayoutBase).stream() self.assertEqual(len(layouts), 76) - systemLayouts = layouts.getElementsByClass('SystemLayout') + systemLayouts = layouts.getElementsByClass(layout.SystemLayout) sl0 = systemLayouts[0] self.assertEqual(sl0.distance, None) self.assertEqual(sl0.topDistance, 211.0) diff --git a/music21/repeat.py b/music21/repeat.py index 0ec187e629..587b020676 100644 --- a/music21/repeat.py +++ b/music21/repeat.py @@ -3755,6 +3755,7 @@ def testExpandRepeatsImportedA(self): Also has grace notes, so it tests our importing of grace notes ''' + from music21 import bar from music21 import corpus from music21 import stream diff --git a/music21/stream/base.py b/music21/stream/base.py index fc294b719f..17c103cb89 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -9032,7 +9032,7 @@ def sliceByQuarterLengths(self, quarterLengthList, *, target=None, return returnObj # exit if returnObj.hasPartLikeStreams(): - for p in returnObj.getElementsByClass('Part'): + for p in returnObj.getElementsByClass(stream.Part): p.sliceByQuarterLengths(quarterLengthList, target=target, addTies=addTies, inPlace=True) returnObj.coreElementsChanged() From 78768bfee583b312f5cb2505c28ccca79c853f6d Mon Sep 17 00:00:00 2001 From: Myke Cuthbert Date: Wed, 27 Apr 2022 01:59:02 -1000 Subject: [PATCH 5/6] lint/grammar/proper classes and imports --- music21/analysis/discrete.py | 15 ++++++++------- music21/capella/fromCapellaXML.py | 6 ++++-- music21/musicxml/partStaffExporter.py | 7 ++++--- music21/musicxml/test_xmlToM21.py | 6 +++--- music21/stream/base.py | 2 +- music21/tempo.py | 24 ++++++++++++------------ 6 files changed, 32 insertions(+), 28 deletions(-) diff --git a/music21/analysis/discrete.py b/music21/analysis/discrete.py index ff7fc0e2e1..b987ebff03 100644 --- a/music21/analysis/discrete.py +++ b/music21/analysis/discrete.py @@ -129,7 +129,7 @@ def clearSolutionsFound(self): def getColorsUsed(self): ''' - Based on solutions found so far with with this processor, + Based on solutions found so far with this processor, return the colors that have been used. ''' post = [] @@ -140,7 +140,7 @@ def getColorsUsed(self): def getSolutionsUsed(self): ''' - Based on solutions found so far with with this processor, + Based on solutions found so far with this processor, return the solutions that have been used. ''' post = [] @@ -167,7 +167,7 @@ def solutionUnitString(self): def solutionToColor(self, solution): ''' - Given a analysis specific result, return the appropriate color. + Given an analysis specific result, return the appropriate color. Must be able to handle None in the case that there is no result. ''' pass @@ -305,7 +305,7 @@ def _getSharpFlatCount(self, subStream) -> Tuple[int, int]: >>> p._getSharpFlatCount(s.flatten()) (87, 0) ''' - # pitches gets a flat representation + # ".pitches" gets a flat representation flatCount = 0 sharpCount = 0 for p in subStream.pitches: @@ -496,7 +496,7 @@ def solutionLegend(self, compress=False): if keyPitch.name not in valid: mask = True if mask: - # set as white so as to maintain spacing + # set as white to maintain spacing color = '#ffffff' keyStr = '' else: @@ -583,7 +583,7 @@ def _bestKeyEnharmonic(self, pitchObj, mode, sStream=None): flipEnharmonic = False # if pitchObj.accidental is not None: -# # if we have a sharp key and we need to favor flat, get enharmonic +# # if we have a sharp key, and we need to favor flat, get enharmonic # if pitchObj.accidental.alter > 0 and favor == 'flat': # flipEnharmonic = True # elif pitchObj.accidental.alter < 0 and favor == 'sharp': @@ -635,7 +635,7 @@ def process(self, sStream, storeAlternatives=False): # mode = None # solution = (None, mode, 0) - # see which has a higher correlation coefficient, the first major or the + # see which has a higher correlation coefficient, the first major or # the first minor if likelyKeysMajor is not None: sortList = [(coefficient, p, 'major') for @@ -1227,6 +1227,7 @@ def countMelodicIntervals(self, sStream, found=None, ignoreDirection=True, ignor # note that Stream.findConsecutiveNotes() and Stream.melodicIntervals() # offer similar approaches, but return Streams and manage offsets and durations, # components not needed here + from music21 import stream if found is None: found = {} diff --git a/music21/capella/fromCapellaXML.py b/music21/capella/fromCapellaXML.py index 02fa1d88ff..8dfa825189 100644 --- a/music21/capella/fromCapellaXML.py +++ b/music21/capella/fromCapellaXML.py @@ -22,6 +22,7 @@ import zipfile from io import StringIO +from typing import List, Optional from music21 import bar from music21 import chord @@ -29,6 +30,7 @@ from music21 import common from music21 import duration from music21 import exceptions21 +from music21 import layout from music21 import key from music21 import meter from music21 import note @@ -84,7 +86,7 @@ class CapellaImportException(exceptions21.Music21Exception): class CapellaImporter: ''' Object for importing .capx, CapellaXML files into music21 (from which they can be - converted to musicxml, MIDI, lilypond, etc. + converted to musicxml, MIDI, lilypond, etc.) Note that Capella stores files closer to their printed versions -- that is to say, Systems enclose all the parts for that system and have new clefs etc. @@ -194,7 +196,7 @@ def partScoreFromSystemScore(self, systemScore): newPart.coreElementsChanged() newScore = stream.Score() # ORDERED DICT - parts = [None for i in range(len(partDictById))] + parts: List[Optional['music21.stream.Part']] = [None for i in range(len(partDictById))] for partId in partDictById: partDict = partDictById[partId] parts[partDict['number']] = partDict['part'] diff --git a/music21/musicxml/partStaffExporter.py b/music21/musicxml/partStaffExporter.py index a3b32b21b3..2600eaacbb 100644 --- a/music21/musicxml/partStaffExporter.py +++ b/music21/musicxml/partStaffExporter.py @@ -13,7 +13,7 @@ A mixin to ScoreExporter that includes the capabilities for producing a single MusicXML `` from multiple music21 `PartStaff` objects. ''' -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional, Union, Type import unittest import warnings from xml.etree.ElementTree import Element, SubElement, Comment @@ -26,6 +26,7 @@ from music21 import stream # for typing from music21.musicxml import helpers from music21.musicxml.xmlObjects import MusicXMLExportException, MusicXMLWarning +from music21 import spanner def addStaffTags(measure: Element, staffNumber: int, tagList: Optional[List[str]] = None): ''' @@ -225,7 +226,7 @@ def joinableGroups(self) -> List[StaffGroup]: <... p2b>>, <... p6b>>] ''' - staffGroups = self.stream.getElementsByClass(spanner.StaffGroup) + staffGroups = self.stream.getElementsByClass(StaffGroup) joinableGroups: List[StaffGroup] = [] # Joinable groups must consist of only PartStaffs with Measures # and exist in self.stream @@ -517,7 +518,7 @@ def setEarliestAttributesAndClefsPartStaff(self, group: StaffGroup): ''' - def isMultiAttribute(m21Class: M21ObjType, + def isMultiAttribute(m21Class: Type[M21ObjType], comparison: str = '__eq__') -> bool: ''' Return True if any first instance of m21Class in any subsequent staff diff --git a/music21/musicxml/test_xmlToM21.py b/music21/musicxml/test_xmlToM21.py index 5720cedc24..af0df57fff 100644 --- a/music21/musicxml/test_xmlToM21.py +++ b/music21/musicxml/test_xmlToM21.py @@ -332,7 +332,7 @@ def testImportMetronomeMarksA(self): # has metronome marks defined, not with sound tag s = converter.parse(testPrimitive.metronomeMarks31c) # get all tempo indications - mms = s[meter.TempoIndication] + mms = s[tempo.TempoIndication] self.assertGreater(len(mms), 3) def testImportMetronomeMarksB(self): @@ -392,7 +392,7 @@ def testStaffGroupsA(self): from music21 import converter s = converter.parse(testPrimitive.staffGroupsNested41d) - staffGroups = s.getElementsByClass(spanner.StaffGroup) + staffGroups = s.getElementsByClass(layout.StaffGroup) # staffGroups.show() self.assertEqual(len(staffGroups), 2) @@ -411,7 +411,7 @@ def testStaffGroupsPiano(self): from music21 import converter s = converter.parse(testPrimitive.pianoStaff43a) - sgs = s.getElementsByClass(spanner.StaffGroup) + sgs = s.getElementsByClass(layout.StaffGroup) self.assertEqual(len(sgs), 1) self.assertEqual(sgs[0].symbol, 'brace') self.assertIs(sgs[0].barTogether, True) diff --git a/music21/stream/base.py b/music21/stream/base.py index 17c103cb89..fc294b719f 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -9032,7 +9032,7 @@ def sliceByQuarterLengths(self, quarterLengthList, *, target=None, return returnObj # exit if returnObj.hasPartLikeStreams(): - for p in returnObj.getElementsByClass(stream.Part): + for p in returnObj.getElementsByClass('Part'): p.sliceByQuarterLengths(quarterLengthList, target=target, addTies=addTies, inPlace=True) returnObj.coreElementsChanged() diff --git a/music21/tempo.py b/music21/tempo.py index 214db5e9a1..4b7fd9db0d 100644 --- a/music21/tempo.py +++ b/music21/tempo.py @@ -106,7 +106,7 @@ def convertTempoByReferent( 270.0 ''' - # find duration in seconds of of quarter length + # find duration in seconds of quarter length srcDurPerBeat = 60 / numberSrc # convert to dur for one quarter length dur = srcDurPerBeat / quarterLengthBeatSrc @@ -276,7 +276,7 @@ def setTextExpression(self, value): def applyTextFormatting(self, te=None, numberImplicit=False): ''' - Apply the default text formatting to the text expression version of of this tempo mark + Apply the default text formatting to the text expression version of this tempo mark ''' if te is None: # use the stored version if possible te = self._textExpression @@ -422,8 +422,8 @@ def __init__(self, text=None, number=None, referent=None, parentheses=False): self._updateNumberFromText() self._updateTextFromNumber() - # need to store a sounding value for the case where where - # a sounding different is different than the number given in the MM + # need to store a sounding value for the case where + # a sounding different is different from the number given in the MM self._numberSounding = None def _reprInternal(self): @@ -463,7 +463,7 @@ def _setReferent(self, value): elif common.isNum(value) or isinstance(value, str): self._referent = duration.Duration(value) elif not isinstance(value, duration.Duration): - # try get duration object, like from Note + # try to get duration object, like from Note self._referent = value.duration elif 'Duration' in value.classes: self._referent = value @@ -595,7 +595,7 @@ def getQuarterBPM(self, useNumberSounding=True): def setQuarterBPM(self, value, setNumber=True): ''' Given a value in BPM, use it to set the value of this MetronomeMark. - BPM values are assumed to be refer only to quarter notes; different beat values, + BPM values are assumed to refer only to quarter notes; different beat values, if defined here, will be scaled @@ -609,7 +609,7 @@ def setQuarterBPM(self, value, setNumber=True): if not setNumber: # convert this to a quarter bpm self._numberSounding = value - else: # go through property so as to set implicit status + else: # go through property to set implicit status self.number = value def _getDefaultNumber(self, tempoText): @@ -661,7 +661,7 @@ def _getDefaultText(self, number, spread=2): tempoNumber = number else: # try to convert tempoNumber = float(number) - # get a items and sort + # get items and sort matches = [] for tempoStr, tempoValue in defaultTempoValues.items(): matches.append([tempoValue, tempoStr]) @@ -831,7 +831,7 @@ class MetricModulation(TempoIndication): ''' A class for representing the relationship between two MetronomeMarks. Generally this relationship is one of equality, where the number is maintained but - the referent that number is applied to changes. + the referent that number is applied to each change. The basic definition of a MetricModulation is given by supplying two MetronomeMarks, one for the oldMetronome, the other for the newMetronome. High level properties, @@ -1105,7 +1105,7 @@ def updateByContext(self): number=mmLast.number) if mmOld is not None: self._oldMetronome = mmOld - # if we have an a new referent, then update number + # if we have a new referent, then update number if (self._newMetronome is not None and self._newMetronome.referent is not None and self._oldMetronome.number is not None): @@ -1151,7 +1151,7 @@ def setOtherByReferent( Set the other side of the metric modulation not based on equality, but on a direct translation of the tempo value. - referent can be a string type or a int/float quarter length + referent can be a string type or an int/float quarter length ''' if side is None: if self._oldMetronome is None: @@ -1469,7 +1469,7 @@ def testMetronomeModulationA(self): mmod1.oldMetronome = mm1 mmod1.newMetronome = mm2 - # this works, and new value is updated: + # this works and new value is updated: self.assertEqual(str(mmod1), '=' From 02d346387ed3435f76b8d6af6ba12e66656a8a72 Mon Sep 17 00:00:00 2001 From: Myke Cuthbert Date: Wed, 27 Apr 2022 02:02:40 -1000 Subject: [PATCH 6/6] unused import --- music21/musicxml/partStaffExporter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/music21/musicxml/partStaffExporter.py b/music21/musicxml/partStaffExporter.py index 2600eaacbb..2a17b0c30a 100644 --- a/music21/musicxml/partStaffExporter.py +++ b/music21/musicxml/partStaffExporter.py @@ -26,7 +26,6 @@ from music21 import stream # for typing from music21.musicxml import helpers from music21.musicxml.xmlObjects import MusicXMLExportException, MusicXMLWarning -from music21 import spanner def addStaffTags(measure: Element, staffNumber: int, tagList: Optional[List[str]] = None): '''