Skip to content
2 changes: 1 addition & 1 deletion music21/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
'''
from __future__ import annotations

__version__ = '9.6.0b5'
__version__ = '9.6.0b25'

def get_version_tuple(vv):
v = vv.split('.')
Expand Down
3 changes: 1 addition & 2 deletions music21/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@
>>> music21.Music21Object
<class 'music21.base.Music21Object'>

>>> music21.VERSION_STR
'9.6.0b5'
'9.6.0b25'

Alternatively, after doing a complete import, these classes are available
under the module "base":
Expand Down
88 changes: 82 additions & 6 deletions music21/musicxml/testPrimitive.py
Original file line number Diff line number Diff line change
Expand Up @@ -18823,7 +18823,83 @@
</score-partwise>
'''

hiddenRests = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
hiddenRestsFinale = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
<score-partwise version="3.1">
<identification>
<encoding>
<software>Finale 2014 for Mac</software>
</encoding>
</identification>
<part-list>
<score-part id="P1">
<part-name print-object="no">MusicXML Part</part-name>
</score-part>
</part-list>
<part id="P1">
<measure number="1">
<attributes>
<divisions>2</divisions>
<time>
<beats>4</beats>
<beat-type>4</beat-type>
</time>
<clef>
<sign>G</sign>
<line>2</line>
</clef>
</attributes>
<note>
<pitch>
<step>E</step>
<octave>5</octave>
</pitch>
<duration>4</duration>
<voice>1</voice>
<type>half</type>
<stem>up</stem>
</note>
<forward>
<duration>2</duration>
<voice>1</voice>
</forward>
<note>
<pitch>
<step>E</step>
<octave>4</octave>
</pitch>
<duration>2</duration>
<voice>1</voice>
<type>quarter</type>
<stem>up</stem>
</note>
<backup>
<duration>8</duration>
</backup>
<forward>
<duration>4</duration>
<voice>2</voice>
</forward>
<note>
<pitch>
<step>F</step>
<octave>4</octave>
</pitch>
<duration>2</duration>
<voice>2</voice>
<type>quarter</type>
<stem>down</stem>
</note>
<forward>
<duration>2</duration>
<voice>2</voice>
</forward>
</measure>
</part>
</score-partwise>
'''

hiddenRestsNoFinale = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
<score-partwise version="3.1">
<part-list>
Expand Down Expand Up @@ -18946,7 +19022,6 @@
</score-partwise>
'''


tupletsImplied = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
<score-partwise version="3.1">
Expand Down Expand Up @@ -20600,10 +20675,11 @@
mixedVoices1a, mixedVoices1b, mixedVoices2, # 37
colors01, triplets01, textBoxes01, octaveShifts33d, # 40
unicodeStrNoNonAscii, unicodeStrWithNonAscii, # 44
tremoloTest, hiddenRests, multiDigitEnding, tupletsImplied, pianoStaffPolymeter, # 46
arpeggio32d, multiStaffArpeggios, multiMeasureEnding, # 51
pianoStaffPolymeterWithClefOctaveChange, multipleFingeringsOnChord, # 54
pianoStaffWithOttava, pedalLines, pedalSymLines # 56
tremoloTest, hiddenRestsFinale, hiddenRestsNoFinale, multiDigitEnding, # 46
tupletsImplied, pianoStaffPolymeter, arpeggio32d, multiStaffArpeggios, # 50
multiMeasureEnding, pianoStaffPolymeterWithClefOctaveChange, # 54
multipleFingeringsOnChord, pianoStaffWithOttava, # 56
pedalLines, pedalSymLines # 58
]


Expand Down
14 changes: 12 additions & 2 deletions music21/musicxml/test_xmlToM21.py
Original file line number Diff line number Diff line change
Expand Up @@ -1326,7 +1326,17 @@ def testHiddenRests(self):

# Voice 1: Half note, <forward> (quarter), quarter note
# Voice 2: <forward> (half), quarter note, <forward> (quarter)
s = converter.parse(testPrimitive.hiddenRests)
s = converter.parse(testPrimitive.hiddenRestsNoFinale)
v1, v2 = s.recurse().voices
# No rests should have been added
self.assertFalse(v1.getElementsByClass(note.Rest))
self.assertFalse(v2.getElementsByClass(note.Rest))

# Finale uses <forward> tags to represent hidden rests,
# so we want to have rests here
# Voice 1: Half note, <forward> (quarter), quarter note
# Voice 2: <forward> (half), quarter note, <forward> (quarter)
s = converter.parse(testPrimitive.hiddenRestsFinale)
v1, v2 = s.recurse().voices
self.assertEqual(v1.duration.quarterLength, v2.duration.quarterLength)

Expand Down Expand Up @@ -1367,7 +1377,7 @@ def testHiddenRestImpliedVoice(self):

self.assertEqual(len(MP.stream.voices), 2)
self.assertEqual(len(MP.stream.voices[0].elements), 1)
self.assertEqual(len(MP.stream.voices[1].elements), 2)
self.assertEqual(len(MP.stream.voices[1].elements), 1)
self.assertEqual(MP.stream.voices[1].id, 'non-integer-value')

def testMultiDigitEnding(self):
Expand Down
34 changes: 19 additions & 15 deletions music21/musicxml/xmlToM21.py
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,7 @@ def __init__(self):
self.parts = []

self.musicXmlVersion = defaults.musicxmlVersion
self.wasWrittenByFinale = False

@jacobtylerwalls jacobtylerwalls May 17, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to localize this in xmlForward() along the lines I suggested here, checking metadataObj.software. A helper function could be extracted to do this checking.

I see from your commit message you tried this and found that setEncoding() is lossy -- we lose information about whether music21 was already in the software tag on the document -- but I think that shouldn't stop us from doing the right thing; we should just have at most one parser-level variable about that rather than one parser-level variable (and lower-order variables) about Finale-ness.

Checking in xmlForward should be fast, since we can depend on the metadata object always existing at the the top of the stream (no long O(n) searches finding no metadatas anywhere):

md = self.xmlMetadata(mxScore)
s.coreInsert(0, md)

You'd have to change that coreInsert (as modeled in that diff I linked).

@mscuthbert do you agree that would achieve better "locality of behavior" than adding more parser attributes for edge cases?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming I'm understanding this correctly, I'm not sure much simplicity is gained by replacing self.wasWrittenByFinale with self.music21WasActuallyInTheDocsSoftwareMetadata (or some other better name). Maybe a rename with the existing semantics? self.applyFinaleWorkarounds, or something like that, that feels more like it is a legit parser-level variable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also going to argue that performing the "loop over the software metadata" for every <forward> element is going to be significantly more expensive than doing it once per document.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, do it once. I believe that the order of software tags written out by music21 is the order we found was most common at the time (17 years ago) for how to layer them. So we really only need to look if the top layer is Finale.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.applyFinaleWorkarounds sounds very good. Or if we want to make it very generic self.softwareWorkarounds: set[str] = {'finale'}


def scoreFromFile(self, filename):
'''
Expand Down Expand Up @@ -1326,9 +1327,17 @@ def processEncoding(self, encoding: ET.Element, md: metadata.Metadata) -> None:
# TODO: encoder (text + type = role) multiple
# TODO: encoding date multiple
# TODO: encoding-description (string) multiple
finaleFound: bool = False
nonFinaleFound: bool = False
for software in encoding.findall('software'):
if softwareText := strippedText(software):
if 'Finale' in softwareText:
finaleFound = True
else:
nonFinaleFound = True
md.add('software', softwareText)
if finaleFound and not nonFinaleFound:
self.wasWrittenByFinale = True

for supports in encoding.findall('supports'):
# todo: element: required
Expand Down Expand Up @@ -2579,12 +2588,6 @@ def parse(self):
# 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
v.makeRests(refStreamOrTimeRange=self.stream,
Comment thread
gregchapman-dev marked this conversation as resolved.
fillGaps=True,
inPlace=True,
hideRests=True)
self.stream.coreElementsChanged()

if (self.restAndNoteCount['rest'] == 1
Expand Down Expand Up @@ -2630,18 +2633,19 @@ def xmlForward(self, mxObj: ET.Element):
if durationText := strippedText(mxDuration):
change = opFrac(float(durationText) / self.divisions)

# Create hidden rest (in other words, a spacer)
# old Finale documents close incomplete final measures with <forward>
# this will be removed afterward by removeEndForwardRest()
r = note.Rest(quarterLength=change)
r.style.hideObjectOnPrint = True
self.addToStaffReference(mxObj, r)
self.insertInMeasureOrVoice(mxObj, r)
if self.parent.parent.wasWrittenByFinale:
Comment thread
gregchapman-dev marked this conversation as resolved.
Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the few times I wish Python were Javascript -- need to check for grandparent existence...

if (self.parent
    and self.parent.parent
    and self.parent.parent.wasWrittenbyFinale
):

Vote YES on PEP 505

# Create hidden rest (in other words, a spacer)
# old Finale documents close incomplete final measures with <forward>
# this will be removed afterward by removeEndForwardRest()
r = note.Rest(quarterLength=change)
r.style.hideObjectOnPrint = True
self.addToStaffReference(mxObj, r)
self.insertInMeasureOrVoice(mxObj, r)
# xmlToNote() sets None
self.endedWithForwardTag = r

# Allow overfilled measures for now -- TODO(someday): warn?
self.offsetMeasureNote += change
# xmlToNote() sets None
self.endedWithForwardTag = r

def xmlPrint(self, mxPrint: ET.Element):
'''
Expand Down