-
Notifications
You must be signed in to change notification settings - Fork 406
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
Interpret empty <forward>
tags as rests on Finale documents only
#1636
base: master
Are you sure you want to change the base?
Interpret empty <forward>
tags as rests on Finale documents only
#1636
Conversation
No longer create hidden rests while parsing forward tags in musicXML files, as this led to problems. If there are unfilled gaps, they will be filled later by makeRests anyways if required.
Inspired by cuthbertLab#991
We simply wait until staves have been separated before creating rests. testHiddenRestImpliedVoice is modified since we no longer import the empty forward tag as a trailing duration.
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.
Hi @TimFelixBeyer thanks for submitting a patch. I want to suggest a different solution, which is not that we stop interpreting <forward>
tags as hidden rests but that we just infer the correct voice number after a <backup>
tag, i.e. that we make the solution from #1030 more robust. WDYT?
I completely agree with your logic about not 'inventing' elements where possible. In my mind it would therefore follow that we should also not interpret elements as rests (since they aren't rests). |
I guess without getting into the ontology of nothingness 😰, I think the reason for the status quo is that creating hidden rests in notation programs (at least Finale) will be exported as forward tags. And there is a slight difference between an explicit nothing marked as "do not show" versus an implicit nothing "document silent/incomplete". |
I'm not super familiar with notation programs like finale, if forward tags are usually used to represent hidden rests, it totally makes sense to me to keep them. On the other hand if we decide that |
But makeRests() is only called on export, so gaps/silence can still be represented on import or on export with makeNotation=False. |
Maybe I’m missing something, but I don’t think this is accurate: music21/music21/musicxml/xmlToM21.py Line 2565 in fdb126c
|
Ah, but only in voices, not measures. I suppose the reason could be that incomplete voices are more likely to lead to display problems than incomplete measures but I suppose we could test out removing that call. |
The call to music21/music21/musicxml/test_xmlToM21.py Line 1245 in f457a2b
I think it just depends on what the preference of the |
I think a survey of other major musicxml writers would be helpful. What do MuseScore and Sibelius do? Dorico? |
In musicxml this is just "empty space". In fact, it is even less than that. Forward and Backward are just used to reposition a "cursor" so voices can also start mid-measure. If this is not handled, correct xml import is guaranteed to be wrong in a lot of cases as I illustrated in #1633 |
Sorry for the long delay. And thanks for the thoughtful responses. Agree we need to do something here, so I'll try to help suggest the least breaking change to get this in.
Actually it was there before, but #444 just extended the "gap finding" to apply two additional arguments in order to find mid-measure and trailing rests. (makeRests defaults to only finding leading rests for some reason).
Actually, that doesn't sound so bad. I bet we could just guard the fix for #444 under a check for the software tag, which has already been parsed (although somewhat inconveniently inserted with coreInsert(), so at this juncture has to be checked against _elements): (Pdb) self.parent.parent.stream._elements[0].software
('Finale v26.3 for Mac') Then #444 becomes something like: if finale:
v = makeRests(all four arguments)
else:
v = makeRests(the two arguments before #444) Then, @TimFelixBeyer you are right that removing the code in xmlForward to create leading hidden rests solves #1633, so I'm okay with that change and the related cleanups. (Finally, the leading hidden rest that remains once we put #444 under a Finale guard doesn't cause problems with parsing and showing #1633, so I don't think we need to deal with that here to get this change in. If we want to remove it eventually, we can discuss it separately.) Does that sound like a reasonable way to get #1633 fixed? LMK! |
Thanks for your reply! What do you think about this alternative: if finale:
v = makeRests(all four arguments)
else:
pass Or alternatively we just have two implementations for the forward tag, one for finale, which inserts hidden rests and one which doesn't do anything. Then we could skip all calls to |
Alright, I'm on board. Getting rid of makeRests altogether on import for non-Finale sounds good. |
@jacobtylerwalls It should now all be implemented as discussed and should fix #1633 and #1715. I also added a test for finale and non-finale hidden-rest-on- Also had to include a minor mypy fix to |
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.
Great! Two little cleanups, and then in my opinion this will be g2g.
@@ -867,6 +867,21 @@ def xmlRootToScore(self, mxScore, inputM21=None): | |||
self.spannerBundle.remove(sp) | |||
|
|||
s.coreElementsChanged() | |||
# Fill gaps with rests where needed | |||
for m in s[stream.Measure]: |
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.
Why move this out of the MeasureParser? If it's just to have access to the md
instance, we could just query for it in the measure parser (with getContextByClass, which is fast). To do that, we likely need to avoid coreInsert
'ing it when inserting for the reason mentioned earlier.
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.
I had a look - I moved it there since it didn't play nice with partwise musicxml files. I think this is because the rests inserted by makeRests
don't have an entry in staffReference
, and therefore can't be placed in the correct part until after the parts have been pulled apart.
One example is schoenberg op19.2. - testHiddenRests
fails when keeping this inside the measure parser because a hole in voice 2 is filled with a half rest. this voice is supposed to be in the top staff, but the rest ends up in the bottom staff, where we end up with two voices instead of no voices (one with the newly inserted rest, and one with the actual data for the bottom staff).
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.
I wasn't able to quickly resolve this in another way - lmk if you have any thoughts.
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.
I think this test wasn't failing before because we got lucky and this particular case happened in the last measure of the piece, where the incorrect rest was removed by removeEndForwardRest
.
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.
Thanks for the ping. I wanted to see what you were talking about, so here's what I stubbed out:
diff --git a/music21/musicxml/test_xmlToM21.py b/music21/musicxml/test_xmlToM21.py
index ab1e99879..c8fe4421c 100644
--- a/music21/musicxml/test_xmlToM21.py
+++ b/music21/musicxml/test_xmlToM21.py
@@ -1547,4 +1547,4 @@ class Test(unittest.TestCase):
if __name__ == '__main__':
import music21
- music21.mainTest(Test)
+ music21.mainTest('noDocTest', Test, runTest="testHiddenRests")
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index 0caa04f55..b385aeaab 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -821,7 +821,7 @@ class MusicXMLImporter(XMLParserBase):
self.musicXmlVersion = mxVersion
md = self.xmlMetadata(mxScore)
- s.coreInsert(0, md)
+ s.insert(0, md)
mxDefaults = mxScore.find('defaults')
if mxDefaults is not None:
@@ -867,21 +867,7 @@ class MusicXMLImporter(XMLParserBase):
self.spannerBundle.remove(sp)
s.coreElementsChanged()
- # Fill gaps with rests where needed
- for m in s[stream.Measure]:
- for v in m.voices:
- if v: # do not bother with empty voices
- # the musicDataMethods use insertCore, thus the voices need to run
- # coreElementsChanged
- v.coreElementsChanged()
- # Fill mid-measure gaps, and find end of measure gaps by ref to measure stream
- # https://github.com/cuthbertlab/music21/issues/444
- # but only when the score comes from Finale
- if any('Finale' in software for software in md.software):
- v.makeRests(refStreamOrTimeRange=m,
- fillGaps=True,
- inPlace=True,
- hideRests=True)
+
s.definesExplicitSystemBreaks = self.definesExplicitSystemBreaks
s.definesExplicitPageBreaks = self.definesExplicitPageBreaks
for p in s.parts:
@@ -2558,8 +2544,25 @@ class MeasureParser(SoundTagMixin, XMLParserBase):
if methName is not None:
meth = getattr(self, methName)
meth(mxObj)
+ md = self.parent.parent.stream[metadata.Metadata].first()
for v in self.stream[stream.Voice]:
- v.coreElementsChanged()
+ if v: # do not bother with empty voices
+ # the musicDataMethods use insertCore, thus the voices need to run
+ # coreElementsChanged
+ v.coreElementsChanged()
+ # Fill mid-measure gaps, and find end of measure gaps by ref to measure stream
+ # https://github.com/cuthbertlab/music21/issues/444
+ # but only when the score comes from Finale
+ if md and any('Finale' in software for software in md.software):
+ old_ids = {id(el) for el in v}
+ v.makeRests(refStreamOrTimeRange=self.stream,
+ fillGaps=True,
+ inPlace=True,
+ hideRests=True)
+ for el in v:
+ if id(el) not in old_ids:
+ self.addToStaffReference(self.mxMeasure, el)
+
self.stream.coreElementsChanged()
if (self.restAndNoteCount['rest'] == 1
Now I get:
File "/Users/jwalls/music21/music21/musicxml/test_xmlToM21.py", line 1282, in testHiddenRests
self.assertIsInstance(hiddenRest, note.Rest)
File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/unittest/case.py", line 1294, in assertIsInstance
self.fail(self._formatMessage(msg, standardMsg))
File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/unittest/case.py", line 715, in fail
raise self.failureException(msg)
AssertionError: <music21.chord.Chord E-5 F#5 B-5 D6> is not an instance of <class 'music21.note.Rest'>
But when I look at this piece on IMSLP, that seems correct. No rests at the end of the file. So my next thought was to look at how the file is encoded in the version in the music21 corpus.
I have to run, but my intent is to get you an answer in the next few days, unless you already know the answer and can enlighten me :-)
Thanks for your persistence.
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.
I had a look - I moved it there since it didn't play nice with partwise musicxml files.
Do you mean MusicXML files where some parts have more than one staff? Almost all MusicXML files are Partwise, and I don't think music21 even imports Timewise since I've never seen one in the wild.
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.
Yeah I meant multi-staff parts, sorry. Basically anything with a <staff>
tag that forces m21 to later use separateOutPartStaves
to move something to a different staff.
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.
@jacobtylerwalls i'm not 100% how addToStaffReference
is supposed to work, but I'm pretty sure measure elements don't have a <staff>
element assigned to them, only notes or rests do. Therefore the added rests will probably always be added to the "NO_STAFF_ASSIGNED" bucket. I think the right way to do this would be to find either the following or preceding note in the same voice and put the added rests in the same staff as that note.
Maybe separateOutPartStaves
is smart enough to do that already?
if v: # do not bother with empty voices | ||
# the musicDataMethods use insertCore, thus the voices need to run | ||
# coreElementsChanged | ||
v.coreElementsChanged() |
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.
Note to self: if we decide to leave this here after all, this is probably not needed (but still think MeasureParser is better if I'm not missing something)
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.
It's been a while since I've thought through this PR, but I think that this is correct.
<forward>
tags as rests on Finale only
<forward>
tags as rests on Finale only<forward>
tags as rests on Finale documents only
if self.lastMeasureParser is None: # pragma: no cover | ||
return # should not happen | ||
lmp = self.lastMeasureParser | ||
self.lastMeasureParser = None # clean memory |
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.
I think we can remove this instance attribute now.
Nice -- I suppose -- to know that there's not a future version of Finale on its way which might break this. |
Any thoughts on my comment? |
btw -- I am mostly staying out of this for now, because Jacob is really the expert on the |
Fixes #1633
No longer create hidden rests while parsing forward tags in musicXML files, as this led to problems. If there are unfilled gaps, they will be filled later by makeRests anyways if required.
testHiddenRestImpliedVoice
is modified since we no longer importthe empty forward tag as a trailing duration.
This also allows us to remove some code to deal with trailing rests.