Skip to content

Commit

Permalink
Merge pull request #1722 from cuthbertLab/meter-typing
Browse files Browse the repository at this point in the history
Improve typing on Meter.Core
  • Loading branch information
mscuthbert authored Sep 18, 2024
2 parents 7726e66 + f3fac08 commit cfabad1
Show file tree
Hide file tree
Showing 4 changed files with 509 additions and 219 deletions.
22 changes: 12 additions & 10 deletions music21/common/numberTools.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,6 @@ def _preFracLimitDenominator(n: int, d: int) -> tuple[int, int]:
0.25, 0.375, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0
])

@overload
def opFrac(num: None) -> None:
pass

@overload
def opFrac(num: int) -> float:
pass
Expand All @@ -249,13 +245,13 @@ def opFrac(num: float|Fraction) -> float|Fraction:
pass

# no type checking due to accessing protected attributes (for speed)
def opFrac(num: OffsetQLIn|None) -> OffsetQL|None:
def opFrac(num: OffsetQLIn) -> OffsetQL:
'''
opFrac -> optionally convert a number to a fraction or back.
Important music21 function for working with offsets and quarterLengths
Takes in a number (or None) and converts it to a Fraction with denominator
Takes in a number and converts it to a Fraction with denominator
less than limitDenominator if it is not binary expressible; otherwise return a float.
Or if the Fraction can be converted back to a binary expressible
float then do so.
Expand Down Expand Up @@ -290,8 +286,14 @@ def opFrac(num: OffsetQLIn|None) -> OffsetQL|None:
Fraction(10, 81)
>>> common.opFrac(0.000001)
0.0
>>> common.opFrac(None) is None
True
Please check against None before calling, but None is changed to 0.0
>>> common.opFrac(None)
0.0
* Changed in v9.3: opFrac(None) should not be called. If it is called,
it now returns 0.0
'''
# This is a performance critical operation, tuned to go as fast as possible.
# hence redundancy -- first we check for type (no inheritance) and then we
Expand Down Expand Up @@ -340,8 +342,8 @@ def opFrac(num: OffsetQLIn|None) -> OffsetQL|None:
return num._numerator / (d + 0.0) # type: ignore
else:
return num # leave non-power of two fractions alone
elif num is None:
return None
elif num is None: # undocumented -- used to be documented to return None. callers must check.
return 0.0

# class inheritance only check AFTER "type is" checks... this is redundant but highly optimized.
elif isinstance(num, int):
Expand Down
102 changes: 73 additions & 29 deletions music21/meter/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ class TimeSignatureBase(base.Music21Object):
pass

class TimeSignature(TimeSignatureBase):
# noinspection GrazieInspection
r'''
The `TimeSignature` object represents time signatures in musical scores
(4/4, 3/8, 2/4+5/16, Cut, etc.).
Expand All @@ -282,7 +283,7 @@ class TimeSignature(TimeSignatureBase):
>>> ts = meter.TimeSignature('3/4')
>>> m1.insert(0, ts)
>>> m1.insert(0, note.Note('C#3', type='half'))
>>> n = note.Note('D3', type='quarter') # we will need this later
>>> n = note.Note('D3', type='quarter')
>>> m1.insert(1.0, n)
>>> m1.number = 1
>>> p.insert(0, m1)
Expand Down Expand Up @@ -912,7 +913,7 @@ def beatDuration(self) -> duration.Duration:
Return a :class:`~music21.duration.Duration` object equal to the beat unit
of this Time Signature, if and only if this TimeSignature has a uniform beat unit.
Otherwise raises an exception in v7.1 but will change to returning NaN
Otherwise, raises an exception in v7.1 but will change to returning NaN
soon fasterwards.
>>> ts = meter.TimeSignature('3/4')
Expand Down Expand Up @@ -984,7 +985,8 @@ def beatDivisionCount(self) -> int:
return 1

# need to see if first-level subdivisions are partitioned
if not isinstance(self.beatSequence[0], MeterSequence):
beat_seq_0 = self.beatSequence[0]
if not isinstance(beat_seq_0, MeterSequence):
return 1

# getting length here gives number of subdivisions
Expand All @@ -993,7 +995,7 @@ def beatDivisionCount(self) -> int:

# convert this to a set; if length is 1, then all beats are uniform
if len(set(post)) == 1:
return len(self.beatSequence[0]) # all are the same
return len(beat_seq_0) # all are the same
else:
return 1

Expand Down Expand Up @@ -1053,18 +1055,40 @@ def beatDivisionDurations(self) -> list[duration.Duration]:
Value returned of non-uniform beat divisions will change at any time
after v7.1 to avoid raising an exception.
OMIT_FROM_DOCS
Previously a time signature with beatSequence containing only
MeterTerminals would raise exceptions.
>>> ts = meter.TimeSignature('2/128')
>>> ts.beatSequence[0]
<music21.meter.core.MeterTerminal 1/128>
>>> ts.beatDivisionDurations
[<music21.duration.Duration 0.03125>]
>>> ts = meter.TimeSignature('1/128')
>>> ts.beatSequence[0]
<music21.meter.core.MeterTerminal 1/128>
>>> ts.beatDivisionDurations
[<music21.duration.Duration 0.03125>]
'''
post = []
if len(self.beatSequence) == 1:
raise TimeSignatureException(
'cannot determine beat division for a non-partitioned beat')
for mt in self.beatSequence:
for subMt in mt:
post.append(subMt.duration.quarterLength)
if isinstance(mt, MeterSequence):
for subMt in mt:
post.append(subMt.duration.quarterLength)
else:
post.append(mt.duration.quarterLength)
if len(set(post)) == 1: # all the same
out = []
for subMt in self.beatSequence[0]:
out.append(subMt.duration)
beat_seq_0 = self.beatSequence[0]
if isinstance(beat_seq_0, MeterSequence):
for subMt in beat_seq_0:
if subMt.duration is not None: # should not be:
out.append(subMt.duration)
elif beat_seq_0.duration is not None: # MeterTerminal w/ non-empty duration.
out.append(beat_seq_0.duration)
return out
else:
raise TimeSignatureException(f'non uniform beat division: {post}')
Expand Down Expand Up @@ -1260,8 +1284,8 @@ def _setDefaultAccentWeights(self, depth: int = 3) -> None:
firstPartitionForm = self.beatSequence
cacheKey = _meterSequenceAccentArchetypesNoneCache # cannot cache based on beat form

# environLocal.printDebug(['_setDefaultAccentWeights(): firstPartitionForm set to',
# firstPartitionForm, 'self.beatSequence: ', self.beatSequence, tsStr])
# environLocal.printDebug('_setDefaultAccentWeights(): firstPartitionForm set to',
# firstPartitionForm, 'self.beatSequence: ', self.beatSequence, tsStr)
# using cacheKey speeds up TS creation from 2300 microseconds to 500microseconds
try:
self.accentSequence = copy.deepcopy(
Expand Down Expand Up @@ -1305,7 +1329,11 @@ def _setDefaultAccentWeights(self, depth: int = 3) -> None:

# --------------------------------------------------------------------------
# access data for other processing
def getBeams(self, srcList, measureStartOffset=0.0) -> list[beam.Beams|None]:
def getBeams(
self,
srcList: stream.Stream|t.Sequence[base.Music21Object],
measureStartOffset: OffsetQL = 0.0,
) -> list[beam.Beams|None]:
'''
Given a qLen position and an iterable of Music21Objects, return a list of Beams objects.
Expand Down Expand Up @@ -1405,11 +1433,18 @@ def getBeams(self, srcList, measureStartOffset=0.0) -> list[beam.Beams|None]:
beamsList = beam.Beams.naiveBeams(srcList) # hold maximum Beams objects, all with type None
beamsList = beam.Beams.removeSandwichedUnbeamables(beamsList)

def fixBeamsOneElementDepth(i, el, depth):
def fixBeamsOneElementDepth(i: int, el: base.Music21Object, depth: int):
'''
Note that this can compute the beams for non-Note things like rests
they just cannot be applied to the object.
'''
beams = beamsList[i]
if beams is None:
return

if t.TYPE_CHECKING:
assert isinstance(beams, beam.Beams)

beamNumber = depth + 1
# see if there is a component defined for this beam number
# if not, continue
Expand All @@ -1421,7 +1456,7 @@ def fixBeamsOneElementDepth(i, el, depth):

start = opFrac(pos)
end = opFrac(pos + dur.quarterLength)
startNext = end
startNext: OffsetQL = end

isLast = (i == len(srcList) - 1)
isFirst = (i == 0)
Expand Down Expand Up @@ -1464,7 +1499,8 @@ def fixBeamsOneElementDepth(i, el, depth):
beamType = 'partial-right'

# if last in complete measure or not in a measure, always stop
elif isLast and (not srcStream.isMeasure or srcStream.paddingRight == 0.0):
elif (isLast and (not isinstance(srcStream, stream.Measure)
or srcStream.paddingRight == 0.0)):
beamType = 'stop'
# get a partial beam if we cannot form a beam
if (beamPrevious is None
Expand Down Expand Up @@ -1903,27 +1939,33 @@ def getOffsetFromBeat(self, beat):
>>> ts1.getOffsetFromBeat(3.25)
2.25
Get the offset from beat 8/3 (2.6666): give a Fraction, get a Fraction.
>>> from fractions import Fraction
>>> ts1.getOffsetFromBeat(Fraction(8, 3)) # 2.66666
>>> ts1.getOffsetFromBeat(Fraction(8, 3))
Fraction(5, 3)
>>> ts1 = meter.TimeSignature('6/8')
>>> ts1.getOffsetFromBeat(1)
0.0
>>> ts1.getOffsetFromBeat(2)
1.5
Check that 2.5 is 2.5 + (0.5 * 1.5):
>>> ts1.getOffsetFromBeat(2.5)
2.25
Decimals only need to be pretty close to work.
(But Fractions are better as demonstrated above)
>>> ts1.getOffsetFromBeat(2.33)
2.0
>>> ts1.getOffsetFromBeat(2.5) # will be + 0.5 * 1.5
2.25
>>> ts1.getOffsetFromBeat(2.66)
2.5
Works for asymmetrical meters as well:
>>> ts3 = meter.TimeSignature('3/8+2/8') # will partition as 2 beat
>>> ts3.getOffsetFromBeat(1)
0.0
Expand All @@ -1936,16 +1978,18 @@ def getOffsetFromBeat(self, beat):
Let's try this on a real piece, a 4/4 chorale with a one beat pickup. Here we get the
normal offset from the active TimeSignature, but we subtract out the pickup length which
is in a `Measure`'s :attr:`~music21.stream.Measure.paddingLeft` property.
normal offset for beat 4 from the active TimeSignature, but we subtract out
the pickup length which is in a `Measure`'s :attr:`~music21.stream.Measure.paddingLeft`
property, and thus see the distance from the beginning of the measure to beat 4 in
quarter notes
>>> c = corpus.parse('bwv1.6')
>>> for m in c.parts.first().getElementsByClass(stream.Measure):
... ts = m.timeSignature or m.getContextByClass(meter.TimeSignature)
... print('%s %s' % (m.number, ts.getOffsetFromBeat(4.5) - m.paddingLeft))
0 0.5
1 3.5
2 3.5
... print(m.number, ts.getOffsetFromBeat(4.0) - m.paddingLeft)
0 0.0
1 3.0
2 3.0
...
'''
# divide into integer and floating point components
Expand Down
Loading

0 comments on commit cfabad1

Please sign in to comment.