Skip to content

Commit

Permalink
Refactor separateOutPartStaves (#822) (MSC)
Browse files Browse the repository at this point in the history
  • Loading branch information
mscuthbert authored and Myke Cuthbert committed Feb 5, 2021
1 parent b6af236 commit c79932c
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 57 deletions.
130 changes: 81 additions & 49 deletions music21/musicxml/xmlToM21.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# import sys
# import traceback
import unittest
from typing import List, Optional, Dict, Tuple
from typing import List, Optional, Dict, Tuple, Set

import xml.etree.ElementTree as ET

Expand Down Expand Up @@ -1506,7 +1506,7 @@ def parse(self):
if self.maxStaves > 1:
self.separateOutPartStaves()
else:
self.stream.addGroupForElements(self.partId) # set group for components
self.stream.addGroupForElements(self.partId) # set group for components (recurse?)
self.stream.groups.append(self.partId) # set group for stream itself

def parseXmlScorePart(self):
Expand Down Expand Up @@ -1653,6 +1653,8 @@ def parseMeasures(self):
def separateOutPartStaves(self):
'''
Take a `Part` with multiple staves and make them a set of `PartStaff` objects.
There must be more than one staff to do this.
'''
# Elements in these classes appear only on the staff to which they are assigned.
# All other classes appear on every staff, except for spanners, which remain on the first.
Expand All @@ -1665,61 +1667,71 @@ def separateOutPartStaves(self):
'StaffLayout',
]

def separateOneStaff(streamPartStaff: stream.PartStaff, staffNumber: int):
partStaffId = f'{self.partId}-Staff{staffNumber}'
streamPartStaff.id = partStaffId
uniqueStaffKeys: List[int] = self._getUniqueStaffKeys()
partStaffs: List[stream.PartStaff] = []
appendedElementIds: Set[int] = set() # id = id(el) not el.id

# remove all elements that are not part of this staff
mStream = list(streamPartStaff.getElementsByClass('Measure'))
for i, staffReference in enumerate(self.staffReferenceList):
staffExclude = self._getStaffExclude(staffReference, staffNumber)
if not staffExclude:
def copy_into_partStaff(source, target, omitTheseElementIds):
for sourceElem in source.getElementsByClass(STAFF_SPECIFIC_CLASSES):
idSource = id(sourceElem)
if idSource in omitTheseElementIds:
continue
if idSource in appendedElementIds:
targetElem = copy.deepcopy(sourceElem)
else:
targetElem = sourceElem # do not make a copy if not yet in staff.
appendedElementIds.add(idSource)
sourceOffset = source.elementOffset(sourceElem, stringReturns=True)
if sourceOffset != 'highestTime':
target.coreInsert(sourceElem.offset, targetElem)
else:
target.coreStoreAtEnd(targetElem)
target.coreElementsChanged()

for staffIndex, staffKey in enumerate(uniqueStaffKeys):
# staffIndex should be staffKey - 1, but you never know...
removeClasses = STAFF_SPECIFIC_CLASSES[:]
if staffIndex != 0: # spanners only on the first staff.
removeClasses.append('Spanner')
newPartStaff = self.stream.template(removeClasses=removeClasses, fillWithRests=False)
partStaffId = f'{self.partId}-Staff{staffKey}'
newPartStaff.id = partStaffId
newPartStaff.addGroupForElements(partStaffId) # set group for components (recurse?)
newPartStaff.groups.append(partStaffId)
partStaffs.append(newPartStaff)
self.parent.m21PartObjectsById[partStaffId] = newPartStaff
elementsIdsNotToGoInThisStaff: Set[int] = set()
for staffReference in self.staffReferenceList:
excludeOneMeasure = self._getStaffExclude(
staffReference,
staffKey
)
for el in excludeOneMeasure:
elementsIdsNotToGoInThisStaff.add(id(el))

m = mStream[i]
for eRemove in staffExclude:
m.remove(eRemove, recurse=True)
# after adjusting voices see if voices can be reduced or
# removed
# environLocal.printDebug(['calling flattenUnnecessaryVoices: voices before:',
# len(m.voices)])
m.flattenUnnecessaryVoices(force=False, inPlace=True)
# environLocal.printDebug(['calling flattenUnnecessaryVoices: voices after:',
# len(m.voices)])

streamPartStaff.addGroupForElements(partStaffId)
streamPartStaff.groups.append(partStaffId)
self.parent.stream.insert(0, streamPartStaff)
self.parent.m21PartObjectsById[partStaffId] = streamPartStaff

uniqueStaffKeys = self._getUniqueStaffKeys()
templates = []
for unused_key in uniqueStaffKeys[1:]:
# Add Spanner to the list of removeClasses; leave them in first staff only
template = self.stream.template(
removeClasses=STAFF_SPECIFIC_CLASSES + ['Spanner'], fillWithRests=False)
templates.append(template)

# Populate elements from source into copy (template)
for sourceMeasure, copyMeasure in zip(
self.stream.getElementsByClass('Measure'),
template.getElementsByClass('Measure')
newPartStaff.getElementsByClass('Measure')
):
for elem in sourceMeasure.getElementsByClass(STAFF_SPECIFIC_CLASSES):
copyMeasure.insert(elem.offset, elem)
copy_into_partStaff(sourceMeasure, copyMeasure, elementsIdsNotToGoInThisStaff)
for sourceVoice, copyVoice in zip(sourceMeasure.voices, copyMeasure.voices):
for elem in sourceVoice.getElementsByClass(STAFF_SPECIFIC_CLASSES):
copyVoice.insert(elem.offset, elem)
copy_into_partStaff(sourceVoice, copyVoice, elementsIdsNotToGoInThisStaff)
copyMeasure.flattenUnnecessaryVoices(force=False, inPlace=True)

score = self.parent.stream
for partStaff in partStaffs:
score.coreInsert(0, partStaff)
score.coreElementsChanged()

modelAndCopies = [self.stream] + templates
for staff, outerStaffNumber in zip(modelAndCopies, uniqueStaffKeys):
separateOneStaff(staff, outerStaffNumber)
self.appendToScoreAfterParse = False # ensures that the original stream is not appended.
# and thus that these next two lines are not needed:
# score.remove(originalPartStaff)
# del self.parent.m21PartObjectsById[originalPartStaff.id]

staffGroup = layout.StaffGroup(modelAndCopies, name=self.stream.partName, symbol='brace')
staffGroup = layout.StaffGroup(partStaffs, name=self.stream.partName, symbol='brace')
staffGroup.style.hideObjectOnPrint = True # in truth, hide the name, not the brace
self.parent.stream.insert(0, staffGroup)

self.appendToScoreAfterParse = False

def _getStaffExclude(
self,
Expand Down Expand Up @@ -5706,11 +5718,17 @@ def testMultipleStavesPerPartA(self):
self.assertIsInstance(s.parts[0], stream.PartStaff)
self.assertIsInstance(s.parts[1], stream.PartStaff)

# make sure both staves get identical key signatures, but not the same object
keySigs = s.recurse().getElementsByClass('KeySignature')
self.assertEqual(len(keySigs), 2)
self.assertEqual(keySigs[0], keySigs[1])
self.assertIsNot(keySigs[0], keySigs[1])

def testMultipleStavesPerPartB(self):
from music21 import converter
from music21.musicxml import testFiles

s = converter.parse(testFiles.moussorgskyPromenade) # @UndefinedVariable
s = converter.parse(testFiles.moussorgskyPromenade)
self.assertEqual(len(s.parts), 2)

self.assertEqual(len(s.parts[0].flat.getElementsByClass('Note')), 19)
Expand All @@ -5726,11 +5744,25 @@ def testMultipleStavesPerPartC(self):
from music21 import corpus
s = corpus.parse('schoenberg/opus19/movement2')
self.assertEqual(len(s.parts), 2)
self.assertEqual(len(s.getElementsByClass('PartStaff')), 2)

s = corpus.parse('schoenberg/opus19/movement6')
self.assertEqual(len(s.parts), 2)
# test that all elements are unique
setElementIds = set()
for el in s.recurse():
setElementIds.add(id(el))
self.assertEqual(len(setElementIds), len(s.recurse()))

# s.show()

def testMultipleStavesInPartWithBarline(self):
from music21 import converter
from music21.musicxml import testPrimitive
s = converter.parse(testPrimitive.mixedVoices1a)
self.assertEqual(len(s.getElementsByClass('PartStaff')), 2)
self.assertEqual(len(s.recurse().getElementsByClass('Barline')), 2)
lastMeasure = s.parts[0].getElementsByClass('Measure')[-1]
lastElement = lastMeasure[-1]
lastOffset = lastMeasure.elementOffset(lastElement, stringReturns=True)
self.assertEqual(lastOffset, 'highestTime')

def testSpannersA(self):
from music21 import converter
Expand Down
43 changes: 35 additions & 8 deletions music21/stream/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2934,7 +2934,7 @@ def analyze(self, *args, **keywords):
# methods that act on individual elements without requiring
# @ coreElementsChanged to fire

def addGroupForElements(self, group, classFilter=None):
def addGroupForElements(self, group, classFilter=None, *, recurse=False):
'''
Add the group to the groups attribute of all elements.
if `classFilter` is set then only those elements whose objects
Expand Down Expand Up @@ -2962,12 +2962,31 @@ def addGroupForElements(self, group, classFilter=None):
1
>>> c[0].name
'B-'

If recurse is True then all sub-elements will get the group:

>>> s = converter.parse('tinyNotation: 4/4 c4 d e f g a b- b')
>>> s.addGroupForElements('scaleNote', 'Note')
>>> s.flat.notes[3].groups
[]
>>> s.addGroupForElements('scaleNote', 'Note', recurse=True)
>>> s.flat.notes[3].groups
['scaleNote']

No group will be added more than once:

>>> s.addGroupForElements('scaleNote', 'Note', recurse=True)
>>> s.flat.notes[3].groups
['scaleNote']

Added in v6.7.1 -- recurse
'''
sIterator = self.iter
sIterator = self.iter if not recurse else self.recurse()
if classFilter is not None:
sIterator = sIterator.addFilter(filters.ClassFilter(classFilter))
for el in sIterator:
el.groups.append(group)
if group not in el.groups:
el.groups.append(group)

# --------------------------------------------------------------------------
# getElementsByX(self): anything that returns a collection of Elements
Expand Down Expand Up @@ -4196,35 +4215,43 @@ def optionalAddRest():

restQL = restInfo['endTime'] - restInfo['offset']
restObj = note.Rest(quarterLength=restQL)
out.insert(restInfo['offset'], restObj)
out.coreInsert(restInfo['offset'], restObj)
restInfo['offset'] = None
restInfo['endTime'] = None

for el in self:
elOffset = self.elementOffset(el, stringReturns=True)
if el.isStream and (retainVoices or ('Voice' not in el.classes)):
optionalAddRest()
outEl = el.template(fillWithRests=fillWithRests,
removeClasses=removeClasses,
retainVoices=retainVoices)
out.insert(el.offset, outEl)
if elOffset != 'highestTime':
out.coreInsert(elOffset, outEl)
else:
out.coreStoreAtEnd(outEl)

elif (removeClasses is True
or el.classSet.intersection(removeClasses)
or (not retainVoices and 'Voice' in el.classes)):
# remove this element
if fillWithRests and el.duration.quarterLength:
endTime = el.offset + el.duration.quarterLength
endTime = elOffset + el.duration.quarterLength
if restInfo['offset'] is None:
restInfo['offset'] = el.offset
restInfo['offset'] = elOffset
restInfo['endTime'] = endTime
elif endTime > restInfo['endTime']:
restInfo['endTime'] = endTime
else:
optionalAddRest()
elNew = copy.deepcopy(el)
out.insert(el.offset, elNew)
if elOffset != 'highestTime':
out.coreInsert(elOffset, elNew)
else:
out.coreStoreAtEnd(elNew)

optionalAddRest()
out.coreElementsChanged()

return out

Expand Down

0 comments on commit c79932c

Please sign in to comment.