Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -47,7 +47,7 @@
'''
from __future__ import annotations

__version__ = '11.0.0b9'
__version__ = '11.0.0b10'

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 @@ -26,7 +26,7 @@
<class 'music21.base.Music21Object'>

>>> music21.VERSION_STR
'11.0.0b9'
'11.0.0b10'

Alternatively, after doing a complete import, these classes are available
under the module "base":
Expand Down
66 changes: 66 additions & 0 deletions music21/musicxml/test_xmlToM21.py
Original file line number Diff line number Diff line change
Expand Up @@ -1392,6 +1392,72 @@ def testArpeggioMarkSpanners(self) -> None:
for spanned, ch in zip(sp, chords):
self.assertIs(spanned, ch)

@staticmethod
def arpeggiatedChordsXml(partIds: list[str]) -> str:
'''
One part per id, each with two measures holding one arpeggiated chord,
every <arpeggiate> with number="1", as MuseScore writes them.

AI-assisted (Claude).
'''
def mxChord(steps: tuple[str, str]) -> str:
return ''.join(
f'<note>{"<chord/>" if i else ""}'
f'<pitch><step>{step}</step><octave>4</octave></pitch><duration>4</duration>'
'<notations><arpeggiate number="1"/></notations></note>'
for i, step in enumerate(steps)
)

mxParts = ''.join(
f'<part id="{partId}">'
f'<measure number="1"><attributes><divisions>1</divisions></attributes>'
f'{mxChord(("C", "E"))}</measure>'
f'<measure number="2">{mxChord(("D", "F"))}</measure>'
'</part>'
for partId in partIds
)
mxScoreParts = ''.join(
f'<score-part id="{partId}"><part-name/></score-part>' for partId in partIds
)
return (f'<score-partwise><part-list>{mxScoreParts}</part-list>'
f'{mxParts}</score-partwise>')

def testArpeggioMarkSpannersSameNumberDifferentOffsets(self) -> None:
'''
The same number at two offsets makes two arpeggios, not one.

AI-assisted (Claude).
'''
from music21 import converter

s = t.cast(stream.Score,
converter.parse(self.arpeggiatedChordsXml(['P1']), format='musicxml'))
spanners = s.spannerBundle.getByClass(expressions.ArpeggioMarkSpanner)
chords = s.parts[0][chord.Chord]
self.assertEqual(
[[id(spanned) for spanned in sp] for sp in spanners],
[[id(ch)] for ch in chords],
)

def testArpeggioMarkSpannersSameNumberDifferentParts(self) -> None:
'''
The same number at the same offset in two parts makes two arpeggios:
number only tells apart arpeggios within a part, where they may
cross staves (see testArpeggioMarkSpanners).

AI-assisted (Claude).
'''
from music21 import converter

s = t.cast(stream.Score,
converter.parse(self.arpeggiatedChordsXml(['P1', 'P2']), format='musicxml'))
spanners = s.spannerBundle.getByClass(expressions.ArpeggioMarkSpanner)
chords = [ch for p in s.parts for ch in p[chord.Chord]]
self.assertEqual(
[[id(spanned) for spanned in sp] for sp in spanners],
[[id(ch)] for ch in chords],
)

def testHiddenRests(self):
from music21 import converter
from music21 import corpus
Expand Down
15 changes: 9 additions & 6 deletions music21/musicxml/xmlToM21.py
Original file line number Diff line number Diff line change
Expand Up @@ -1489,6 +1489,9 @@ def __init__(self,
# a dict of clefs per staff number -- needed for converting rests w/ steps
self.lastClefs: dict[int, clef.Clef|None] = {}
self.activeTuplets: list[duration.Tuplet|None] = [None] * 7
# keyed by number and offset: number only tells apart simultaneous
# arpeggios, which may cross staves but not parts
self.arpeggioSpanners: dict[tuple[str, OffsetQL], expressions.ArpeggioMarkSpanner] = {}

self.maxStaves = 1 # will be changed in measure parsing

Expand Down Expand Up @@ -3873,15 +3876,15 @@ def flatten(mx, name):
arpeggio = expressions.ArpeggioMark(arpeggioType)
n.expressions.append(arpeggio)
else:
sb = self.spannerBundle.getByClassIdLocalComplete(
expressions.ArpeggioMarkSpanner, idFound, False)
if sb:
# if we already have a spanner matching
arpeggioSpanner = t.cast(expressions.ArpeggioMarkSpanner, sb[0])
else:
arpeggioSpanners = self.parent.arpeggioSpanners
offset = opFrac(self.parent.lastMeasureOffset + self.offsetMeasureNote)
numberAndOffset = (idFound, offset)
arpeggioSpanner = arpeggioSpanners.get(numberAndOffset)
if arpeggioSpanner is None:
arpeggioSpanner = expressions.ArpeggioMarkSpanner(arpeggioType=arpeggioType)
arpeggioSpanner.idLocal = idFound
self.spannerBundle.append(arpeggioSpanner)
arpeggioSpanners[numberAndOffset] = arpeggioSpanner
arpeggioSpanner.addSpannedElements(n)

mostRecentOrnament: expressions.Ornament|None = None
Expand Down
Loading