Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
faed972
Use SpannerAnchors in xmlToM21.py:xmlDirectionTypeToSpanners, so we n…
gregchapman-dev Apr 13, 2025
2fe7c44
Better SpannerAnchor debug output. Bump version to re-import corpus.
gregchapman-dev Apr 15, 2025
ae3af61
lint.
gregchapman-dev Apr 15, 2025
c1e75d1
When splitting part staves, SpannerAnchors should stay in the staff w…
gregchapman-dev Apr 15, 2025
5806b93
In Spanner.fill, if you remove and re-add the endElement, also restor…
gregchapman-dev Apr 15, 2025
a8df0dc
Update some tests.
gregchapman-dev Apr 15, 2025
87fc100
MusicXML import: no more hidden rests from xmlForward.
gregchapman-dev Apr 16, 2025
3764320
Pick up more of PR #1636 (no rests for the forwards).
gregchapman-dev Apr 16, 2025
83eea9e
Update tests for new reality.
gregchapman-dev Apr 16, 2025
fa83430
More test updates.
gregchapman-dev Apr 16, 2025
ee3f686
One last test update.
gregchapman-dev Apr 16, 2025
6412ef9
Lint.
gregchapman-dev Apr 16, 2025
2b50e65
Merge branch 'master' into gregc/moreAccurateSpannerStartStop
gregchapman-dev Apr 16, 2025
1d8c47f
Remove some dead code.
gregchapman-dev Apr 16, 2025
05c34df
Remove more dead code.
gregchapman-dev Apr 17, 2025
9abc002
makeRests always produces exportable (non-complex duration) rests.
gregchapman-dev Apr 17, 2025
dc31653
Don't crash on export to MusicXML when a rest duration is complex (mo…
gregchapman-dev Apr 17, 2025
8cf3ffd
Add a test for (MusicXML) import/export/re-import of spanners with of…
gregchapman-dev Apr 18, 2025
725ad05
First bit of review.
gregchapman-dev Apr 25, 2025
a15e065
Works almost all the time; still have a problem importing MusicXML fi…
gregchapman-dev Apr 29, 2025
daea68f
Instead of looping over self.pendingAnchors (and missing any pending …
gregchapman-dev May 2, 2025
e36df6e
Looks like a spanner end element can be assigned before we see the pe…
gregchapman-dev May 2, 2025
39db861
Don't leave uncompleted spanners in the xmlToM21.py spannerBundle. T…
gregchapman-dev May 3, 2025
fa17fdd
Oops, make an exception for ArpeggioMarkSpanners.
gregchapman-dev May 3, 2025
5f4db7d
xmlOneSpanner also needs to deal with out-of-order start/stop due to …
gregchapman-dev May 6, 2025
15bbe7d
Bump version number in hopes that's why tests are failing.
gregchapman-dev May 6, 2025
86993ad
Fix that last regression (triggered by spanner start and spanner stop…
gregchapman-dev May 6, 2025
34e445b
A better fix for that regression; don't let 'continue' complete a spa…
gregchapman-dev May 7, 2025
6be5a0b
Fixes for all those review comments.
gregchapman-dev May 7, 2025
1a0cca8
An attempt at a more efficient implementation of insertFirstSpannedEl…
gregchapman-dev May 7, 2025
fe88d12
Document the new pendingFirstSpannedElementAssignment stuff, and add …
gregchapman-dev May 7, 2025
ebaef99
PendingSpannedElement APIs are now just the old ones, with some optio…
gregchapman-dev May 15, 2025
ab636c6
Merge branch 'master' into gregc/moreAccurateSpannerStartStop
gregchapman-dev May 15, 2025
e484070
Merge branch 'gregc/forwardIsHiddenRestOnlyForFinale' into gregc/more…
gregchapman-dev May 15, 2025
ec17b2e
Merge branch 'gregc/forwardIsHiddenRestOnlyForFinale' into gregc/more…
gregchapman-dev May 15, 2025
58624d6
Somehow in the merge I lost the removal of the makeRests() call. Fix…
gregchapman-dev May 15, 2025
2dc9b60
Fix another merge failure: put back in the "remove that last hidden r…
gregchapman-dev May 15, 2025
d512983
Merge branch 'master' into gregc/moreAccurateSpannerStartStop
gregchapman-dev May 22, 2025
78d2034
A few tweaks after the merge from master.
gregchapman-dev May 22, 2025
05f0cb2
Tweaks to the tweaks.
gregchapman-dev May 22, 2025
5dc3eee
Merge branch 'master' into pr/1768
mscuthbert Jun 19, 2025
f2c64c8
Merge branch 'gregc/moreAccurateSpannerStartStop' of https://github.c…
mscuthbert Jun 19, 2025
214cd70
Merge branch 'master' into gregc/moreAccurateSpannerStartStop
gregchapman-dev Jun 19, 2025
e68ee92
Bump version.
gregchapman-dev Jun 19, 2025
23d36ad
Fix bad merge; bump version numbers.
gregchapman-dev Jun 21, 2025
cfa44e7
New features in spanner.py: (1) Let pending spanner element assignmen…
gregchapman-dev Jul 26, 2025
1f91911
Undo makeRests change that was rejected.
gregchapman-dev Jul 29, 2025
ff948fa
Merge branch 'gregc/musicXmlWriterFixes' into gregc/moreAccurateSpann…
gregchapman-dev Aug 11, 2025
4a49120
Merge branch 'master' into gregc/pendingSpannerAssignmentWithOffset
gregchapman-dev Aug 12, 2025
38f67a8
Merge branch 'master' into gregc/moreAccurateSpannerStartStop
gregchapman-dev Aug 12, 2025
ac88879
Add some tests.
gregchapman-dev Aug 12, 2025
197a114
Better demonstration of old and new use of the PendingSpannedElementA…
gregchapman-dev Aug 12, 2025
4b96a01
Rename "clientInfo: t.Any|None" becomes "staffKey: int|None".
gregchapman-dev Aug 14, 2025
79aad8c
Change version numbers in the approved way.
gregchapman-dev Aug 14, 2025
00bebd9
Merge branch 'gregc/pendingSpannerAssignmentWithOffset' into gregc/mo…
gregchapman-dev Aug 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.7.2a4'
__version__ = '9.7.4'

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

>>> music21.VERSION_STR
'9.7.2a4'
'9.7.4'

Alternatively, after doing a complete import, these classes are available
under the module "base":
Expand Down
64 changes: 61 additions & 3 deletions music21/musicxml/test_m21ToXml.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import re
import unittest
from xml.etree.ElementTree import (
ElementTree, fromstring as et_fromstring
ElementTree, fromstring as et_fromstring, tostring as et_tostring
)

from music21 import articulations
Expand All @@ -29,6 +29,7 @@
from music21 import stream
from music21 import style
from music21 import tempo
from music21.common import opFrac

from music21.musicxml import helpers
from music21.musicxml import testPrimitive
Expand Down Expand Up @@ -195,7 +196,7 @@ def testSpannersWritePartStaffs(self):

# and written after the backup tag, i.e. on the LH?
xmlOut = self.getXml(s)
xmlAfterFirstBackup = xmlOut.split('</backup>\n')[1]
xmlAfterSecondBackup = xmlOut.split('</backup>\n')[1]

self.assertIn(
stripInnerSpaces(
Expand All @@ -205,7 +206,7 @@ def testSpannersWritePartStaffs(self):
</direction-type>
<staff>2</staff>
</direction>'''),
stripInnerSpaces(xmlAfterFirstBackup)
stripInnerSpaces(xmlAfterSecondBackup)
)

def testLowVoiceNumbers(self):
Expand Down Expand Up @@ -847,6 +848,63 @@ def testPedals(self):
for k in expectedResults2[i]:
self.assertEqual(mxPedal.get(k, ''), expectedResults2[i][k])

def testSpannersWithOffsets(self):
def gnfilter(overlaps):
removeKeys = []
for key, elList in overlaps.items():
gnCount = 0
for el in elList:
if isinstance(el, note.GeneralNote):
gnCount += 1
if gnCount < 2:
removeKeys.append(key)
for key in removeKeys:
del overlaps[key]
return overlaps

def check(s1, s2, classType):
s1Spanners = list(s1[classType])
s2Spanners = list(s2[classType])
for s1sp, s2sp in zip(s1Spanners, s2Spanners):
# check that the spanners start and stop at exactly the same score offset
s1StartOffset = s1sp.getFirst().getOffsetInHierarchy(s1)
s2StartOffset = s2sp.getFirst().getOffsetInHierarchy(s2)
self.assertEqual(s1StartOffset, s2StartOffset)
s1EndOffset = opFrac(
s1sp.getLast().getOffsetInHierarchy(s1) + s1sp.getLast().quarterLength
)
s2EndOffset = opFrac(
s2sp.getLast().getOffsetInHierarchy(s2) + s2sp.getLast().quarterLength
)
self.assertEqual(s1EndOffset, s2EndOffset)

# check that there are no overlapping GeneralNotes in those measures
s1StartVoice = s1.containerInHierarchy(s1sp.getFirst())
s1EndVoice = s1.containerInHierarchy(s1sp.getLast())
s1StartVoiceOverlaps = s1StartVoice.getOverlaps()
s1EndVoiceOverlaps = s1EndVoice.getOverlaps()
self.assertEqual(gnfilter(s1StartVoiceOverlaps), {})
self.assertEqual(gnfilter(s1EndVoiceOverlaps), {})

s2StartVoice = s2.containerInHierarchy(s2sp.getFirst())
s2EndVoice = s2.containerInHierarchy(s2sp.getLast())
s2StartVoiceOverlaps = s2StartVoice.getOverlaps()
s2EndVoiceOverlaps = s2EndVoice.getOverlaps()
self.assertEqual(gnfilter(s2StartVoiceOverlaps), {})
self.assertEqual(gnfilter(s2EndVoiceOverlaps), {})

s1 = converter.parse(testPrimitive.directions31a)
x = self.getET(s1)
xmlStr = et_tostring(x)
s2 = converter.parseData(xmlStr, format='musicxml')
check(s1, s2, dynamics.DynamicWedge)

s1 = converter.parse(testPrimitive.octaveShifts33d)
x = self.getET(s1)
xmlStr = et_tostring(x)
s2 = converter.parseData(xmlStr, format='musicxml')
check(s1, s2, spanner.Ottava)

def testArpeggios(self):
expectedResults = (
'arpeggiate',
Expand Down
94 changes: 72 additions & 22 deletions music21/musicxml/test_xmlToM21.py
Original file line number Diff line number Diff line change
Expand Up @@ -1134,14 +1134,19 @@ def testPedalMarks(self):
self.assertEqual(pm.pedalType, expressions.PedalType.Sustain)
spElements = pm.getSpannedElements()
self.assertEqual(len(spElements), 4)
expectedInstances = [
note.Note,
expressions.PedalBounce,
note.Note,
note.Note,
]
expectedOffsets = [0.0, 1.0, 1.0, 2.0]
for i, (el, expectedOffset) in enumerate(zip(spElements, expectedOffsets)):
if i == 1:
self.assertIsInstance(el, expressions.PedalBounce)
else:
self.assertIsInstance(el, note.Note)
self.assertEqual(el.fullName, 'C in octave 4 Quarter Note')
for i, (el, expectedOffset, expectedInstance) in enumerate(zip(
spElements, expectedOffsets, expectedInstances)):
self.assertIsInstance(el, expectedInstance)
self.assertEqual(el.offset, expectedOffset)
if expectedInstance == note.Note:
self.assertEqual(el.fullName, 'C in octave 4 Quarter Note')

s = converter.parse(testPrimitive.spanners33a)
pedals = list(s[expressions.PedalMark])
Expand All @@ -1152,14 +1157,18 @@ def testPedalMarks(self):
self.assertEqual(pm.pedalType, expressions.PedalType.Sustain)
spElements = pm.getSpannedElements()
self.assertEqual(len(spElements), 3)
expectedInstances = [
note.Note,
expressions.PedalBounce,
note.Note,
]
expectedOffsets = [0.0, 1.0, 1.0]
for i, (el, expectedOffset) in enumerate(zip(spElements, expectedOffsets)):
if i == 1:
self.assertIsInstance(el, expressions.PedalBounce)
else:
self.assertIsInstance(el, note.Note)
self.assertEqual(el.fullName, 'B in octave 4 Quarter Note')
for i, (el, expectedOffset, expectedInstance) in enumerate(zip(
spElements, expectedOffsets, expectedInstances)):
self.assertIsInstance(el, expectedInstance)
self.assertEqual(el.offset, expectedOffset)
if expectedInstance == note.Note:
self.assertEqual(el.fullName, 'B in octave 4 Quarter Note')

s = corpus.parse('beach')
pedals = list(s[expressions.PedalMark])
Expand All @@ -1169,7 +1178,7 @@ def testPedalMarks(self):
self.assertEqual(pm.pedalForm, expressions.PedalForm.Symbol)
self.assertEqual(pm.pedalType, expressions.PedalType.Sustain)
spElements = pm.getSpannedElements()
self.assertEqual(len(spElements), 2)
self.assertEqual(len(spElements), 3)
self.assertIsInstance(spElements[0], chord.Chord)
self.assertEqual(
spElements[0].fullName,
Expand All @@ -1179,6 +1188,11 @@ def testPedalMarks(self):
self.assertIsInstance(spElements[1], note.Note)
self.assertEqual(spElements[1].fullName, 'E-flat in octave 1 Whole Note')
self.assertEqual(spElements[1].offset, 0.)
self.assertEqual(spElements[1].quarterLength, 4.)
# The pedal "stop" happens a quarter-note _before_ the end of the last whole note
# (last whole note <duration> is 32, <pedal><offset> is -8)
self.assertEqual(spElements[2].offset, 3.)
self.assertIsInstance(spElements[2], spanner.SpannerAnchor)

s = corpus.parse('dichterliebe_no2')
pedals = list(s[expressions.PedalMark])
Expand All @@ -1190,9 +1204,18 @@ def testPedalMarks(self):
spElements = pm.getSpannedElements()
self.assertEqual(len(spElements), 5)
expectedOffsets = [1.5, 1.75, 0.0, 0.75, 1.0]
for i, (el, expectedOffset) in enumerate(zip(spElements, expectedOffsets)):
self.assertIsInstance(el, note.Note)
self.assertEqual(el.nameWithOctave, 'A3')
expectedInstances = [
note.Note,
note.Note,
note.Note,
note.Note,
note.Note,
]
for i, (el, expectedOffset, expectedInstance) in enumerate(zip(
spElements, expectedOffsets, expectedInstances)):
self.assertIsInstance(el, expectedInstance)
if expectedInstance == note.Note:
self.assertEqual(el.nameWithOctave, 'A3')
self.assertEqual(el.offset, expectedOffset)

def testNoChordImport(self):
Expand Down Expand Up @@ -1256,8 +1279,8 @@ def testLineHeight(self):
el2 = EL('<bracket type="stop" line-end="down" end-length="12.5" number="1"></bracket>')

mp = MeasureParser()
line = mp.xmlDirectionTypeToSpanners(el1)[0]
mp.xmlDirectionTypeToSpanners(el2)
line = mp.xmlDirectionTypeToSpanners(el1, 1, 0.0)[0]
mp.xmlDirectionTypeToSpanners(el2, 1, 1.0)
self.assertEqual(line.startHeight, 12.5)
self.assertEqual(line.endHeight, 12.5)

Expand Down Expand Up @@ -1374,6 +1397,7 @@ def testHiddenRests(self):
from music21 import corpus
from music21.musicxml import testPrimitive

# With most software, <forward> tags should map to no objects at all
# Voice 1: Half note, <forward> (quarter), quarter note
# Voice 2: <forward> (half), quarter note, <forward> (quarter)
s = converter.parse(testPrimitive.hiddenRestsNoFinale)
Expand Down Expand Up @@ -1580,11 +1604,37 @@ def testImportOttava(self):
[o.placement for o in ottava_objs],
['above', 'below', 'above', 'below']
)
ottavaPitches = []
for o in ottava_objs:
ottavaPitches.append([])
for p in o.getSpannedElements():
if hasattr(p, 'nameWithOctave'):
name = p.nameWithOctave
else:
name = repr(p)
ottavaPitches[-1].append(name)

self.assertEqual(
[[p.nameWithOctave for p in o.getSpannedElements()] for o in ottava_objs],
# TODO(bug): first element should be ['C7', 'A6']
# not reading <offset>-4</offset>
[['A6'], ['C3', 'B2'], ['A5', 'A5'], ['B3', 'C4']]
ottavaPitches, [
[
'<music21.spanner.SpannerAnchor at 0.5>',
'C5',
'<music21.spanner.SpannerAnchor at 1.0>'
],
[
'C3',
'<music21.spanner.SpannerAnchor at 2.0>'
],
[
'A5',
'A5',
'<music21.spanner.SpannerAnchor at 3.125>'
],
[
'B3',
'<music21.spanner.SpannerAnchor at 3.75>'
]
]
)

def testClearingTuplets(self):
Expand Down
Loading