Skip to content
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 Stream splitByQuarterLengths #1736

Merged
merged 3 commits into from
Oct 28, 2024
Merged
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
1 change: 1 addition & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ disable=
too-many-lines, # yes, someday
too-many-return-statements, # we'll see
too-many-instance-attributes, # maybe later
too-many-positional-arguments, # try to get down to 6 first...
# no-self-use, # moved to optional extension.
invalid-name, # these are good music21 names; fix the regexp instead...
too-few-public-methods, # never remove or set to 1
Expand Down
66 changes: 55 additions & 11 deletions music21/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,25 +148,60 @@ class ContextSortTuple(t.NamedTuple):
class _SplitTuple(tuple):
'''
>>> st = base._SplitTuple([1, 2])
>>> st.spannerList = [3]
>>> st.spannerList = [expressions.Trill()]
>>> st
(1, 2)
>>> st.spannerList
[3]
[<music21.expressions.Trill>]
>>> a, b = st
>>> a
1
>>> b
2
>>> st.__class__
<class 'music21.base._SplitTuple'>

OMIT_FROM_DOCS

Might have been a mistake to make an implicit return make sure that
normal tuple comparisons work, but that things do not hash the same.

st2 has the same (1, 2) value as st1 but no spanners

>>> st2 = base._SplitTuple([1, 2])
>>> st == st2
False
>>> c = set()
>>> c.add(st)
>>> st2 in c
False

>>> st3 = base._SplitTuple([1, 2])
>>> st2 == st3
True

>>> st_big = base._SplitTuple([1, 3])
>>> st2 < st_big
True
'''
def __new__(cls, tupEls):
# noinspection PyTypeChecker
return super(_SplitTuple, cls).__new__(cls, tuple(tupEls))

def __init__(self, tupEls):
self.spannerList = []
def __init__(self, tupEls: t.Any) -> None:
self.spannerList: list[spanner.Spanner] = []

def __eq__(self, other):
if not isinstance(other, _SplitTuple):
return False
if self.spannerList != other.spannerList:
return False
return super().__eq__(other)

def __hash__(self):
h1 = super().__hash__()
h2 = id(self.spannerList)
return hash((h1, h2))

# -----------------------------------------------------------------------------
# make subclass of set once that is defined properly
Expand Down Expand Up @@ -1368,7 +1403,8 @@ def getContextByClass(
context for b would be much harder to get without this method, since in
order to do it, it searches backwards within the measure, finds that
there's nothing there. It goes to the previous measure and searches
that one backwards until it gets the proper TimeSignature of 2/4:
inside that one from the end backwards until it gets the proper
TimeSignature of 2/4:

>>> b.getContextByClass(meter.TimeSignature)
<music21.meter.TimeSignature 2/4>
Expand All @@ -1388,7 +1424,7 @@ class name:
part. This is all you need to know for most uses. The rest of the
docs are for advanced uses:

The method searches both Sites as well as associated objects to find a
The method searches Sites and also associated objects to find a
matching class. Returns `None` if no match is found.

A reference to the caller is required to find the offset of the object
Expand Down Expand Up @@ -3064,7 +3100,7 @@ def splitAtQuarterLength(
Split an Element into two Elements at a provided
`quarterLength` (offset) into the Element.

Returns a specialized tuple that also has
Returns a specialized tuple (_SplitTuple) that also has
a .spannerList element which is a list of spanners
that were created during the split, such as by splitting a trill
note into more than one trill.
Expand Down Expand Up @@ -3110,7 +3146,6 @@ def splitAtQuarterLength(
[<music21.expressions.TrillExtension <music21.note.Note C#><music21.note.Note C#>>]



Make sure that ties and accidentals remain as they should be:

>>> d = note.Note('D#4')
Expand Down Expand Up @@ -3172,6 +3207,9 @@ def splitAtQuarterLength(
'''
from music21 import chord
from music21 import note

st: _SplitTuple

quarterLength = opFrac(quarterLength)

if quarterLength > self.duration.quarterLength:
Expand Down Expand Up @@ -3317,11 +3355,15 @@ def splitByQuarterLengths(
displayTiedAccidentals=False
) -> _SplitTuple:
'''
Given a list of quarter lengths, return a list of
Given a list of quarter lengths, return a "SplitTuple" of
Music21Object objects, copied from this Music21Object,
that are partitioned and tied with the specified quarter
length list durations.

THe SplitTuple will also have a .spannerList which
contains a list of spanner created during the split, such as by splitting a trill
note into more than one trill.

TODO: unite into a "split" function -- document obscure uses.

>>> n = note.Note()
Expand All @@ -3341,13 +3383,15 @@ def splitByQuarterLengths(
if len(quarterLengthList) == 1:
# return a copy of self in a list
return _SplitTuple([copy.deepcopy(self)])
elif len(quarterLengthList) <= 1:
elif not quarterLengthList:
raise Music21ObjectException(
f'cannot split by this quarter length list: {quarterLengthList}.')

eList = []
eList: list[Music21Object] = []
spannerList = [] # this does not fully work with trills over multiple splits yet.
eRemain = copy.deepcopy(self)

st: _SplitTuple
for qlSplit in quarterLengthList[:-1]:
st = eRemain.splitAtQuarterLength(qlSplit,
addTies=addTies,
Expand Down
32 changes: 16 additions & 16 deletions music21/figuredBass/resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ def augmentedSixthToDominant(
assert isinstance(fifth, pitch.Pitch)
assert isinstance(other, pitch.Pitch)

howToResolve = [(lambda p: p.name == bass.name, '-m2'),
(lambda p: p.name == root.name, 'm2'),
(lambda p: p.name == fifth.name, '-m2'),
(lambda p: p.name == other.name and augSixthType == 3, 'd1'),
(lambda p: p.name == other.name and augSixthType == 2, '-m2')]
howToResolve = [(lambda p: p and bass and p.name == bass.name, '-m2'),
(lambda p: p and root and p.name == root.name, 'm2'),
(lambda p: p and fifth and p.name == fifth.name, '-m2'),
(lambda p: p and other and p.name == other.name and augSixthType == 3, 'd1'),
(lambda p: p and other and p.name == other.name and augSixthType == 2, '-m2')]

return _resolvePitches(augSixthPossib, howToResolve)

Expand Down Expand Up @@ -199,12 +199,12 @@ def augmentedSixthToMajorTonic(
assert isinstance(fifth, pitch.Pitch)
assert isinstance(other, pitch.Pitch)

howToResolve = [(lambda p: p.name == bass.name, '-m2'),
(lambda p: p.name == root.name, 'm2'),
(lambda p: p.name == fifth.name, 'P1'),
(lambda p: p.name == other.name and augSixthType == 1, 'M2'),
(lambda p: p.name == other.name and augSixthType == 2, 'A1'),
(lambda p: p.name == other.name and augSixthType == 3, 'm2')]
howToResolve = [(lambda p: p and bass and p.name == bass.name, '-m2'),
(lambda p: p and root and p.name == root.name, 'm2'),
(lambda p: p and fifth and p.name == fifth.name, 'P1'),
(lambda p: p and other and p.name == other.name and augSixthType == 1, 'M2'),
(lambda p: p and other and p.name == other.name and augSixthType == 2, 'A1'),
(lambda p: p and other and p.name == other.name and augSixthType == 3, 'm2')]

return _resolvePitches(augSixthPossib, howToResolve)

Expand Down Expand Up @@ -284,11 +284,11 @@ def augmentedSixthToMinorTonic(
assert isinstance(fifth, pitch.Pitch)
assert isinstance(other, pitch.Pitch)

howToResolve = [(lambda p: p.name == bass.name, '-m2'),
(lambda p: p.name == root.name, 'm2'),
(lambda p: p.name == fifth.name, 'P1'),
(lambda p: p.name == other.name and augSixthType == 1, 'm2'),
(lambda p: p.name == other.name and augSixthType == 3, 'd2')]
howToResolve = [(lambda p: p and bass and p.name == bass.name, '-m2'),
(lambda p: p and root and p.name == root.name, 'm2'),
(lambda p: p and fifth and p.name == fifth.name, 'P1'),
(lambda p: p and other and p.name == other.name and augSixthType == 1, 'm2'),
(lambda p: p and other and p.name == other.name and augSixthType == 3, 'd2')]

return _resolvePitches(augSixthPossib, howToResolve)

Expand Down
17 changes: 10 additions & 7 deletions music21/stream/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3221,7 +3221,7 @@ def splitAtQuarterLength(self,
retainOrigin=True,
addTies=True,
displayTiedAccidentals=False,
searchContext=True):
searchContext=True) -> base._SplitTuple:
'''
This method overrides the method on Music21Object to provide
similar functionality for Streams.
Expand All @@ -3230,6 +3230,8 @@ def splitAtQuarterLength(self,

* Changed in v7: all but quarterLength are keyword only
'''
keySignatures: list[key.KeySignature] = []

quarterLength = opFrac(quarterLength)
if retainOrigin:
sLeft = self
Expand All @@ -3247,19 +3249,20 @@ def splitAtQuarterLength(self,
if timeSignatures:
sRight.keySignature = copy.deepcopy(timeSignatures[0])
if searchContext:
keySignatures = sLeft.getContextByClass(key.KeySignature)
if keySignatures is not None:
keySignatures = [keySignatures]
ksContext = sLeft.getContextByClass(key.KeySignature)
if ksContext is not None:
keySignatures = [ksContext]
else:
keySignatures = sLeft.getElementsByClass(key.KeySignature)
keySignatures = list(sLeft.getElementsByClass(key.KeySignature))

if keySignatures:
sRight.keySignature = copy.deepcopy(keySignatures[0])
endClef = sLeft.getContextByClass(clef.Clef)
if endClef is not None:
sRight.clef = copy.deepcopy(endClef)

if quarterLength > sLeft.highestTime: # nothing to do
return sLeft, sRight
return base._SplitTuple([sLeft, sRight])

# use quarterLength as start time
targets = sLeft.getElementsByOffset(
Expand Down Expand Up @@ -3303,7 +3306,7 @@ def splitAtQuarterLength(self,
sRight.insert(target.getOffsetBySite(sLeft) - quarterLength, target)
sLeft.remove(target)

return sLeft, sRight
return base._SplitTuple([sLeft, sRight])

# --------------------------------------------------------------------------
def recurseRepr(self,
Expand Down
33 changes: 33 additions & 0 deletions music21/stream/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
from music21 import tie
from music21 import variant

from music21.base import Music21Exception, _SplitTuple

from music21.musicxml import m21ToXml

from music21.midi import translate as midiTranslate
Expand Down Expand Up @@ -7964,6 +7966,37 @@ def testSplitAtQuarterLengthC(self):
# sLeft.show()
# sRight.show()

def testSplitByQuarterLengths(self):
'''
Was not returning splitTuples before
'''
m = Measure([
note.Note(quarterLength=8.0)
])

with self.assertRaisesRegex(Music21Exception,
'cannot split by quarter length list whose sum is not equal'):
m.splitByQuarterLengths([1.0, 2.0])

parts = m.splitByQuarterLengths([1.0, 2.0, 5.0])
self.assertIsInstance(parts, _SplitTuple)
self.assertEqual(len(parts), 3)
self.assertIsInstance(parts[0], Measure)
self.assertEqual(parts[0].quarterLength, 1.0)
self.assertEqual(len(parts[0]), 1)
self.assertEqual(parts[0][0].quarterLength, 1.0)

self.assertEqual(parts[1].quarterLength, 2.0)
self.assertEqual(len(parts[1]), 1)
self.assertEqual(parts[1][0].quarterLength, 2.0)

self.assertEqual(parts[2].quarterLength, 5.0)
self.assertEqual(len(parts[2]), 1)
self.assertEqual(parts[2][0].quarterLength, 5.0)
self.assertEqual(parts[2][0].duration.type, 'complex')



def testGracesInStream(self):
'''
testing grace notes
Expand Down
5 changes: 4 additions & 1 deletion music21/test/testLint.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from __future__ import annotations

# this requires pylint to be installed and available from the command line
# Anything changed here also needs to be changed at .pylintrc

import argparse
import os

Expand Down Expand Up @@ -48,7 +50,7 @@

def main(fnAccept=None, strict=False):
'''
`fnAccept` is a list of one or more files to test. Otherwise runs all.
`fnAccept` is a list of one or more files to test. Otherwise, runs all.
'''
poolSize = common.cpus()

Expand All @@ -75,6 +77,7 @@ def main(fnAccept=None, strict=False):
'too-many-lines', # yes, someday.
'too-many-return-statements', # we'll see
'too-many-instance-attributes', # maybe later
'too-many-positional-arguments', # let's get this at least to max 6.
'inconsistent-return-statements', # would be nice
'protected-access', # this is an important one, but for now we do a lot of
# x = copy.deepcopy(self); x._volume = ... which is not a problem...
Expand Down
Loading