-
Notifications
You must be signed in to change notification settings - Fork 407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix issue #1335: voice numbers written in MusicXML must be unique… #1336
Changes from 4 commits
6760792
2f3b9bb
3c0203e
e7ff0d7
f1d13a9
1628687
5635a38
19063be
11037c9
edacf1f
4ec4d6a
6a7027b
da3ba8c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1490,6 +1490,7 @@ def parse(self): | |
# before attempting to identify and count instruments | ||
self._populatePartExporterList() | ||
self.groupsToJoin = self.joinableGroups() | ||
self.prePartProcess() | ||
self.parsePartlikeScore() | ||
else: | ||
self.parseFlatScore() | ||
|
@@ -1735,6 +1736,66 @@ def parseFlatScore(self): | |
pp.parse() | ||
self.partExporterList.append(pp) | ||
|
||
def prePartProcess(self): | ||
''' | ||
Scans the parts/measures/voices in each joinable group to compute | ||
the voiceNumberOffset needed in each part. Parts that aren't in a | ||
joinable group get offset 0. | ||
|
||
Called automatically by .parse() | ||
''' | ||
partExporterForPart: t.Dict[stream.Part, PartExporter] = {} | ||
|
||
# Compute partExp.staffGroup and partExp.maxNumVoices | ||
for partExp in self.partExporterList: | ||
partExporterForPart[partExp.stream] = partExp | ||
|
||
joinableGroup = None | ||
for sg in self.groupsToJoin: | ||
if partExp.stream in sg: | ||
joinableGroup = sg | ||
break | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a bit of an O(n^2) approach. It's probably fine since n=num parts, and likely to be very fast, but in general there are ways to do this in O(n) time. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nevermind, see above. |
||
|
||
partExp.staffGroup = joinableGroup | ||
if partExp.staffGroup is None: | ||
partExp.maxVoiceNum = 0 | ||
continue | ||
|
||
# part is in a staffGroup, so we need to find the maximum number | ||
# of voices used in any measure in this part. We also find the | ||
# maximum "low enough to not be considered a memory location" | ||
# voice id. The maxVoiceNum for a part is the higher of | ||
# maxNumVoices-1 and maxNonMemLocationVoiceId. | ||
maxNumVoices = 0 | ||
maxNonMemLocationVoiceId = 0 | ||
for measure in partExp.stream[stream.Measure]: | ||
voices = tuple(measure[stream.Voice]) | ||
jacobtylerwalls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
maxNumVoices = max(maxNumVoices, len(voices)) | ||
for voice in voices: | ||
if (isinstance(voice.id, int) | ||
and voice.id < defaults.minIdNumberToConsiderMemoryLocation): | ||
maxNonMemLocationVoiceId = max(maxNonMemLocationVoiceId, voice.id) | ||
partExp.maxVoiceNum = max(maxNumVoices - 1, maxNonMemLocationVoiceId) | ||
jacobtylerwalls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Compute partExp.voiceNumberOffset | ||
for partExp in self.partExporterList: | ||
if partExp.staffGroup is None: | ||
partExp.voiceNumberOffset = 0 | ||
continue | ||
|
||
staffGroup = partExp.staffGroup | ||
|
||
staffGroupStreamList = staffGroup.getSpannedElements() | ||
partIndexInGroup = staffGroupStreamList.index(partExp.stream) | ||
voiceNumberOffset = 0 | ||
# add up the offsetForPart of each part before us in the group | ||
for i in range(0, partIndexInGroup): | ||
pex = partExporterForPart[staffGroup[i]] | ||
offsetForPart = pex.maxVoiceNum + 1 | ||
voiceNumberOffset += offsetForPart | ||
|
||
partExp.voiceNumberOffset = voiceNumberOffset | ||
|
||
def postPartProcess(self): | ||
''' | ||
calls .joinPartStaffs() from the | ||
|
@@ -2548,6 +2609,18 @@ def __init__(self, | |
# has changed | ||
self.lastDivisions = None | ||
|
||
# voiceNumberOffset is the offset that must be applied to any voice numbers automatically | ||
# generated in this part, in order to avoid voice numbers (either explicitly set, or | ||
# automatically generated) in other parts in this staffGroup. This is computed in a | ||
# pre-pass over all the voices in all the measures in all the parts (in ScoreExporter). | ||
self.voiceNumberOffset: int = 0 | ||
|
||
# The maximum voiceNum found in the voices in all the measures in this part. | ||
jacobtylerwalls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.maxVoiceNum: int = 0 | ||
|
||
# The staffGroup to which this part belongs (if it belongs to one) | ||
self.staffGroup: t.Optional[layout.StaffGroup] = None | ||
|
||
self.spannerBundle = partObj.spannerBundle | ||
|
||
def parse(self): | ||
|
@@ -3020,7 +3093,7 @@ def __init__(self, | |
self.measureOffsetStart = 0.0 | ||
self.offsetInMeasure = 0.0 | ||
self.currentVoiceId: t.Optional[int] = None | ||
self.nextFreeVoiceNumber = 1 | ||
self.nextFreeVoiceNumber: int = 1 | ||
|
||
self.rbSpanners: t.List[spanner.RepeatBracket] = [] # repeatBracket spanners | ||
|
||
|
@@ -3076,6 +3149,18 @@ def mainElementsParse(self): | |
# Assumes voices are flat... | ||
self.parseFlatElements(v, backupAfterwards=backupAfterwards) | ||
|
||
def getVoiceNumberOffset(self) -> int: | ||
''' | ||
Returns the offset that should be added to any voice numbers in this measure, | ||
in order to avoid voice numbers in other simultaneous measures within a staff | ||
group. This voice number offset is pre-computed for each part before exporting | ||
any measures. So we just get it from the parent partExporter here. | ||
''' | ||
partExp: t.Optional[PartExporter] = self.parent | ||
if partExp is None: | ||
return 0 | ||
return partExp.voiceNumberOffset | ||
|
||
def parseFlatElements(self, m, *, backupAfterwards=False): | ||
''' | ||
Deals with parsing all the elements in .elements, assuming that .elements is flat. | ||
|
@@ -3096,8 +3181,12 @@ def parseFlatElements(self, m, *, backupAfterwards=False): | |
if isinstance(m.id, int) and m.id < defaults.minIdNumberToConsiderMemoryLocation: | ||
voiceId = m.id | ||
elif isinstance(m.id, int): | ||
# This voice id is actually a memory location, so we need to change it | ||
# to a low number so it can be used in MusicXML. Note that we need to | ||
# add an offset to make it unique across the entire staffGroup. | ||
voiceId = self.nextFreeVoiceNumber | ||
self.nextFreeVoiceNumber += 1 | ||
voiceId += self.getVoiceNumberOffset() | ||
else: | ||
voiceId = m.id | ||
else: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this title should be more explicit: renumberVoicesWithinPartGroups, or something like that. Then I think for simplicity, it would be better to call two functions -- setPartExporterStaffGroups() before this is called, and then this function, which should itself call a helper function for every partExporter with a staff group. Doing more than one thing per function leads to added complexity.
Once you have a partExporter with the spannedElements, there's no need for all the simultaneous measure finding, just do something like this:
I think that would make the logic much simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And then we can just renumber voices here and not make the PartExporter have to keep track of them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, I can't believe I didn't think of OffsetIterator!