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

Improve typing on Meter.Core #1722

Merged
merged 8 commits into from
Sep 18, 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
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
Loading