# -----------------------------------------------------------------------------
# Name: meter.core.py
# Purpose: Component objects for meters
#
# Authors: Christopher Ariza
# Michael Scott Asato Cuthbert
#
# Copyright: Copyright © 2009-2024 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# -----------------------------------------------------------------------------
'''
This module defines two component objects for defining nested metrical structures:
:class:`~music21.meter.core.MeterTerminal` and :class:`~music21.meter.core.MeterSequence`.
'''
from __future__ import annotations
from collections.abc import Sequence
import copy
import typing as t
from music21 import common
from music21.common.numberTools import opFrac
from music21.common.objects import SlottedObjectMixin
from music21.common.types import OffsetQLIn
from music21.duration import Duration, DurationException
from music21 import environment
from music21.exceptions21 import MeterException
from music21.meter import tools
from music21 import prebase
environLocal = environment.Environment('meter.core')
# -----------------------------------------------------------------------------
[docs]
class MeterTerminal(prebase.ProtoM21Object, SlottedObjectMixin):
'''
A MeterTerminal is a nestable primitive of rhythmic division.
>>> a = meter.MeterTerminal('2/4')
>>> a.duration.quarterLength
2.0
>>> a = meter.MeterTerminal('3/8')
>>> a.duration.quarterLength
1.5
>>> a = meter.MeterTerminal('5/2')
>>> a.duration.quarterLength
10.0
'''
# CLASS VARIABLES #
__slots__ = (
'_denominator',
'_duration',
'_numerator',
'_overriddenDuration',
'_weight',
)
# INITIALIZER #
def __init__(self, slashNotation: str|None = None, weight: int|float = 1):
# because of how they are copied, MeterTerminals must not have any
# initialization parameters without defaults
self._duration: Duration|None = None
self._numerator: int = 0
self._denominator: int = 1
self._weight: int|float = 1 # do not use setter here -- bad override in MeterSequence
self._overriddenDuration: Duration|None = None
if slashNotation is not None:
# assign directly to values, not properties, to avoid
# calling _ratioChanged more than necessary
values = tools.slashToTuple(slashNotation) # raise MeterException early if problem.
self._numerator = values.numerator
self._denominator = values.denominator
self._ratioChanged() # sets self._duration
# this will set the underlying weight attribute directly for data checking
# explicitly calling base class method to avoid problems
# in the derived class MeterSequence
self._weight = weight
# SPECIAL METHODS #
def __deepcopy__(self, memo=None):
'''
Helper method to for the deepcopy function in copy.py.
Do not call this directly.
Defining a custom __deepcopy__ here is a performance boost,
particularly in not copying _duration, directly assigning _weight, and
other benefits.
'''
# call class to get a new, empty instance
new = self.__class__()
# for name in dir(self):
new._numerator = self._numerator
new._denominator = self._denominator
new._ratioChanged() # faster than copying dur
# new._duration = copy.deepcopy(self._duration, memo)
new._weight = self._weight # these are numbers
return new
def _reprInternal(self):
return str(self)
def __str__(self):
return str(int(self.numerator)) + '/' + str(int(self.denominator))
[docs]
def ratioEqual(self, other):
'''
Compare the numerator and denominator of another object.
Note that these have to be exact matches; 3/4 is not the same as 6/8
>>> a = meter.MeterTerminal('3/4')
>>> b = meter.MeterTerminal('6/4')
>>> c = meter.MeterTerminal('2/4')
>>> d = meter.MeterTerminal('3/4')
>>> a.ratioEqual(b)
False
>>> a.ratioEqual(c)
False
>>> a.ratioEqual(d)
True
'''
if other is None:
return False
if (other.numerator == self.numerator
and other.denominator == self.denominator):
return True
else:
return False
# -------------------------------------------------------------------------
[docs]
def subdivideByCount(self, countRequest=None):
'''
returns a MeterSequence made up of taking this MeterTerminal and
subdividing it into the given number of parts. Each of those parts
is a MeterTerminal
>>> a = meter.MeterTerminal('3/4')
>>> b = a.subdivideByCount(3)
>>> b
<music21.meter.core.MeterSequence {1/4+1/4+1/4}>
>>> len(b)
3
>>> b[0]
<music21.meter.core.MeterTerminal 1/4>
What happens if we do this?
>>> a = meter.MeterTerminal('5/8')
>>> b = a.subdivideByCount(2)
>>> b
<music21.meter.core.MeterSequence {2/8+3/8}>
>>> len(b)
2
>>> b[0]
<music21.meter.core.MeterTerminal 2/8>
>>> b[1]
<music21.meter.core.MeterTerminal 3/8>
But what if you want to divide into 3/8+2/8 or something else?
for that, see the :meth:`~music21.meter.MeterSequence.load` method
of :class:`~music21.meter.MeterSequence`.
'''
# elevate to meter sequence
ms = MeterSequence()
# cannot set the weight of this MeterSequence w/o having offsets
# pass this MeterTerminal as an argument
# when subdividing, use autoWeight
ms.load(self, countRequest, autoWeight=True, targetWeight=self.weight)
return ms
[docs]
def subdivideByList(self, numeratorList):
'''
Return a MeterSequence dividing this
MeterTerminal according to the numeratorList
>>> a = meter.MeterTerminal('3/4')
>>> b = a.subdivideByList([1, 1, 1])
>>> b
<music21.meter.core.MeterSequence {1/4+1/4+1/4}>
>>> len(b)
3
>>> b[0]
<music21.meter.core.MeterTerminal 1/4>
Unequal subdivisions work:
>>> c = a.subdivideByList([1, 2])
>>> c
<music21.meter.core.MeterSequence {1/4+2/4}>
>>> len(c)
2
>>> (c[0], c[1])
(<music21.meter.core.MeterTerminal 1/4>, <music21.meter.core.MeterTerminal 2/4>)
So does subdividing by strings
>>> c = a.subdivideByList(['2/4', '1/4'])
>>> len(c)
2
>>> (c[0], c[1])
(<music21.meter.core.MeterTerminal 2/4>, <music21.meter.core.MeterTerminal 1/4>)
See :meth:`~music21.meter.MeterSequence.partitionByList` method
of :class:`~music21.meter.MeterSequence` for more details.
'''
# elevate to meter sequence
ms = MeterSequence()
ms.load(self) # do not need to autoWeight here
ms.partitionByList(numeratorList) # this will split weight
return ms
[docs]
def subdivideByOther(self, other: 'music21.meter.MeterSequence'):
'''
Return a MeterSequence based on another MeterSequence
>>> a = meter.MeterSequence('1/4+1/4+1/4')
>>> a
<music21.meter.core.MeterSequence {1/4+1/4+1/4}>
>>> b = meter.MeterSequence('3/8+3/8')
>>> a.subdivideByOther(b)
<music21.meter.core.MeterSequence {{3/8+3/8}}>
>>> terminal = meter.MeterTerminal('1/4')
>>> divider = meter.MeterSequence('1/8+1/8')
>>> terminal.subdivideByOther(divider)
<music21.meter.core.MeterSequence {{1/8+1/8}}>
'''
# elevate to meter sequence
ms = MeterSequence()
if other.duration.quarterLength != self.duration.quarterLength:
raise MeterException(f'cannot subdivide by other: {other}')
ms.load(other) # do not need to autoWeight here
# ms.partitionByOtherMeterSequence(other) # this will split weight
return ms
[docs]
def subdivide(
self,
value: Sequence[int | str] | MeterSequence | int
):
'''
Subdivision takes a MeterTerminal and, making it into a collection of MeterTerminals,
Returns a MeterSequence.
This is different from partitioning a MeterSequence. `subdivide` does not happen
in place and instead returns a new object.
If an integer is provided, assume it is a partition count.
'''
if common.isListLike(value):
return self.subdivideByList(value)
elif isinstance(value, MeterSequence):
return self.subdivideByOther(value)
elif common.isNum(value):
return self.subdivideByCount(value)
else:
raise MeterException(f'cannot process partition argument {value}')
# -------------------------------------------------------------------------
# properties
@property
def weight(self) -> float|int:
'''
Return or set the weight of a MeterTerminal
>>> a = meter.MeterTerminal('2/4')
>>> a.weight = 0.5
>>> a.weight
0.5
'''
return self._weight
@weight.setter
def weight(self, value: float|int):
self._weight = value
@property
def numerator(self) -> int:
'''
Return or set the numerator of the MeterTerminal
>>> a = meter.MeterTerminal('2/4')
>>> a.numerator
2
>>> a.duration.quarterLength
2.0
>>> a.numerator = 11
>>> a.duration.quarterLength
11.0
'''
return self._numerator
@numerator.setter
def numerator(self, value: int):
self._numerator = value
self._ratioChanged()
@property
def denominator(self) -> int:
'''
Get or set the denominator of the meter terminal
>>> a = meter.MeterTerminal('2/4')
>>> a.denominator
4
>>> a.duration.quarterLength
2.0
>>> a.denominator = 8
>>> a.duration.quarterLength
1.0
>>> a.denominator = 7
Traceback (most recent call last):
music21.exceptions21.MeterException: bad denominator value: 7
'''
return self._denominator
@denominator.setter
def denominator(self, value: int):
# use duration.typeFromNumDict?
if value not in tools.validDenominatorsSet:
raise MeterException(f'bad denominator value: {value}')
self._denominator = value
self._ratioChanged()
def _ratioChanged(self):
'''
If ratio has been changed, call this to update duration
'''
# NOTE: this is a performance critical method and should only be
# called when necessary
self._duration = Duration()
try:
self._duration.quarterLength = (
(4.0 * self.numerator) / self.denominator
)
except DurationException:
environLocal.printDebug(
['DurationException encountered',
'numerator/denominator',
self.numerator,
self.denominator
]
)
@property
def duration(self):
'''
duration gets or sets a duration value that
is equal in length of the terminal.
>>> a = meter.MeterTerminal()
>>> a.numerator = 3
>>> a.denominator = 8
>>> d = a.duration
>>> d.type
'quarter'
>>> d.dots
1
>>> d.quarterLength
1.5
'''
if self._overriddenDuration:
return self._overriddenDuration
else:
return self._duration
@duration.setter
def duration(self, value: Duration):
self._overriddenDuration = value
@property
def depth(self):
'''
Return how many levels deep this part is -- the depth of a terminal is always 1
'''
return 1
# -----------------------------------------------------------------------------
[docs]
class MeterSequence(MeterTerminal):
'''
A meter sequence is a list of MeterTerminals, or other MeterSequences
'''
# CLASS VARIABLES #
__slots__ = (
'_levelListCache',
'_partition',
'parenthesis',
'summedNumerator',
)
# INITIALIZER #
def __init__(
self,
value: str | MeterTerminal | Sequence[MeterTerminal] | Sequence[str] | None = None,
partitionRequest: t.Any|None = None,
):
super().__init__()
self._numerator: int = 1 # rationalized
self._denominator: int = 0 # lowest common multiple
self._partition: list[MeterTerminal|MeterSequence] = []
self._levelListCache: dict[tuple[int, bool], list[MeterTerminal]] = {}
# this attribute is only used in MeterTerminals, and note
# in MeterSequences; a MeterSequence's weight is based solely
# on the sum of its component parts.
# del self._weight -- no -- screws up pickling -- cannot del a slotted object
# Bool stores whether this meter was provided as a summed numerator
self.summedNumerator: bool = False
# An optional parameter used only in meter display sequences.
# Needed in cases where a meter component is parenthetical
self.parenthesis: bool = False
if value is not None:
self.load(value, partitionRequest)
# SPECIAL METHODS #
def __deepcopy__(self, memo=None):
'''
Helper method to copy.py's deepcopy function. Call it from there.
Defining a custom __deepcopy__ here is a performance boost,
particularly in not copying _duration and other benefits.
Notably, self._levelListCache is not copied,
which may not be needed in the copy and may be large.
>>> from copy import deepcopy
>>> ms1 = meter.MeterSequence('4/4+3/8')
>>> ms2 = deepcopy(ms1)
>>> ms2
<music21.meter.core.MeterSequence {4/4+3/8}>
'''
# call class to get a new, empty instance
new = self.__class__()
# for name in dir(self):
new._numerator = self._numerator
new._denominator = self._denominator
# noinspection PyArgumentList
new._partition = copy.deepcopy(self._partition, memo)
new._ratioChanged() # faster than copying dur
# new._duration = copy.deepcopy(self._duration, memo)
new._overriddenDuration = self._overriddenDuration
new.summedNumerator = self.summedNumerator
new.parenthesis = self.parenthesis
return new
[docs]
def __getitem__(self, key: int) -> MeterTerminal:
'''
Get an MeterTerminal (or MeterSequence) from _partition
>>> a = meter.MeterSequence('4/4', 4)
>>> a[3].numerator
1
'''
if abs(key) >= len(self):
raise IndexError
return self._partition[key]
def __iter__(self):
'''
Support iteration of top level partitions
>>> a = meter.MeterSequence('4/4', 2)
>>> for x in a:
... print(repr(x))
<music21.meter.core.MeterTerminal 1/2>
<music21.meter.core.MeterTerminal 1/2>
'''
return iter(self._partition)
def __len__(self):
'''
Return the length of the partition list
>>> a = meter.MeterSequence('4/4', 4)
>>> a
<music21.meter.core.MeterSequence {1/4+1/4+1/4+1/4}>
>>> len(a)
4
'''
return len(self._partition)
def __setitem__(self, key: int, value: MeterTerminal):
'''
Insert items at index positions.
>>> a = meter.MeterSequence('4/4', 4)
>>> a
<music21.meter.core.MeterSequence {1/4+1/4+1/4+1/4}>
>>> a[0]
<music21.meter.core.MeterTerminal 1/4>
>>> a[0] = a[0].subdivide(2)
>>> a
<music21.meter.core.MeterSequence {{1/8+1/8}+1/4+1/4+1/4}>
>>> a[0][0] = a[0][0].subdivide(2)
>>> a
<music21.meter.core.MeterSequence {{{1/16+1/16}+1/8}+1/4+1/4+1/4}>
>>> a[3]
<music21.meter.core.MeterTerminal 1/4>
>>> a[3] = a[0][0]
Traceback (most recent call last):
...
music21.exceptions21.MeterException: cannot insert {1/16+1/16} into space of 1/4
'''
# comparison of numerator and denominator
if not isinstance(value, MeterTerminal):
raise MeterException('values in MeterSequences must be MeterTerminals or '
f'MeterSequences, not {value}')
if value.ratioEqual(self[key]):
self._partition[key] = value
else:
raise MeterException(f'cannot insert {value} into space of {self[key]}')
# clear cache
self._levelListCache = {}
def __str__(self):
return '{' + self.partitionDisplay + '}'
@property
def partitionDisplay(self):
'''
Property -- Display the partition as a str without the surrounding curly brackets.
>>> a = meter.MeterSequence('4/4')
>>> a.partitionDisplay
'4/4'
>>> a = meter.MeterSequence('2/4+6/8')
>>> a.partitionDisplay
'2/4+6/8'
partitionDisplay is most useful for non-divided meter sequences. This is less helpful:
>>> a = meter.MeterSequence('4/4', 4)
>>> a.partitionDisplay
'1/4+1/4+1/4+1/4'
'''
msg = []
for mt in self._partition:
msg.append(str(mt))
return '+'.join(msg)
# -------------------------------------------------------------------------
def _clearPartition(self) -> None:
'''
This will not sync with .numerator and .denominator if called alone
'''
self._partition = []
# clear cache
self._levelListCache = {}
def _addTerminal(self, value: MeterTerminal|str) -> None:
'''
Add an object to the partition list. This does not update numerator and denominator.
???: (targetWeight is the expected total Weight for this MeterSequence. This
would be self.weight, but often partitions are cleared before _addTerminal is called.)
'''
# NOTE: this is a performance critical method
if isinstance(value, MeterTerminal): # may be a MeterSequence
mt = value
else: # assume it is a string
mt = MeterTerminal(value)
# if isinstance(value, str):
# mt = MeterTerminal(value)
# elif isinstance(value, MeterTerminal): # may be a MeterSequence
# mt = value
# else:
# raise MeterException(f'cannot add {value} to this sequence')
self._partition.append(mt)
# clear cache
self._levelListCache = {}
[docs]
def getPartitionOptions(self) -> tools.MeterOptions:
'''
Return either a cached or a new set of division/partition options.
Calls `tools.divisionOptionsAlgo` and `tools.divisionOptionsPreset`
(which will be empty except if the numerator is 5).
Works on anything that has a .numerator and .denominator.
>>> meter.MeterSequence('3/4').getPartitionOptions()
(('1/4', '1/4', '1/4'),
('1/8', '1/8', '1/8', '1/8', '1/8', '1/8'),
('1/16', '1/16', '1/16', '1/16', '1/16', '1/16', '1/16',
'1/16', '1/16', '1/16', '1/16', '1/16'),
('3/4',), ('6/8',), ('12/16',), ('24/32',), ('48/64',), ('96/128',))
The additional 2 + 2 + 1 and 2 + 1 + 2 options for numerator 5 are at the end.
>>> meter.MeterSequence('5/32').getPartitionOptions()
(('2/32', '3/32'),
('3/32', '2/32'),
('1/32', '1/32', '1/32', '1/32', '1/32'),
('1/64', '1/64', '1/64', '1/64', '1/64',
'1/64', '1/64', '1/64', '1/64', '1/64'),
('5/32',), ('10/64',), ('20/128',),
('2/32', '2/32', '1/32'), ('2/32', '1/32', '2/32'))
'''
# all-string python dictionaries are optimized; use string key
n = int(self.numerator)
d = int(self.denominator)
opts = []
opts.extend(list(tools.divisionOptionsAlgo(n, d)))
opts.extend(list(tools.divisionOptionsPreset(n, d)))
# store for access later
return tuple(opts)
# -------------------------------------------------------------------------
[docs]
def partitionByCount(self, countRequest: int, loadDefault: bool = True) -> None:
'''
Divide the current MeterSequence into the requested number of parts.
If it is not possible to divide it into the requested number, and
loadDefault is `True`, then give the default partition:
This will destroy any established structure in the stored partition.
>>> a = meter.MeterSequence('4/4')
>>> a
<music21.meter.core.MeterSequence {4/4}>
>>> a.partitionByCount(2)
>>> a
<music21.meter.core.MeterSequence {1/2+1/2}>
>>> str(a)
'{1/2+1/2}'
>>> a.partitionByCount(4)
>>> a
<music21.meter.core.MeterSequence {1/4+1/4+1/4+1/4}>
>>> str(a)
'{1/4+1/4+1/4+1/4}'
The partitions are not guaranteed to be the same length if the
meter is irregular:
>>> b = meter.MeterSequence('5/8')
>>> b.partitionByCount(2)
>>> b
<music21.meter.core.MeterSequence {2/8+3/8}>
This relies on a pre-defined exemption for partitioning 5 by 3:
>>> b.partitionByCount(3)
>>> str(b)
'{2/8+2/8+1/8}'
Here we use loadDefault=True to get the default partition in case
there is no known way to do this:
>>> a = meter.MeterSequence('5/8')
>>> a.partitionByCount(11)
>>> str(a)
'{2/8+3/8}'
If loadDefault is False then an error is raised:
>>> a.partitionByCount(11, loadDefault=False)
Traceback (most recent call last):
music21.exceptions21.MeterException: Cannot set partition by 11 (5/8)
'''
opts = self.getPartitionOptions()
optMatch = None
# get the first encountered load string with the desired
# number of beats
for opt in opts:
if len(opt) == countRequest:
optMatch = opt
break
# if no matches this method provides a default
if optMatch is None:
if loadDefault and opts:
optMatch = opts[0]
else:
raise MeterException(
f'Cannot set partition by {countRequest} ({self.numerator}/{self.denominator})'
)
targetWeight = self.weight
# environLocal.printDebug(['partitionByCount, targetWeight', targetWeight])
self._clearPartition() # weight will now be zero
for mStr in optMatch:
self._addTerminal(mStr)
self.weight = targetWeight
# clear cache
self._levelListCache = {}
[docs]
def partitionByList(self, numeratorList: Sequence[int] | Sequence[str]) -> None:
'''
Given a numerator list, partition MeterSequence into a new list
of MeterTerminals
>>> a = meter.MeterSequence('4/4')
>>> a.partitionByList([1, 1, 1, 1])
>>> str(a)
'{1/4+1/4+1/4+1/4}'
This divides it into two equal parts:
>>> a.partitionByList([1, 1])
>>> str(a)
'{1/2+1/2}'
And now into one big part:
>>> a.partitionByList([1])
>>> str(a)
'{1/1}'
Here we divide 4/4 very unconventionally:
>>> a.partitionByList(['3/4', '1/8', '1/8'])
>>> a
<music21.meter.core.MeterSequence {3/4+1/8+1/8}>
But the basics of the MeterSequence must be observed:
>>> a.partitionByList(['3/4', '1/8', '5/8'])
Traceback (most recent call last):
music21.exceptions21.MeterException: Cannot set partition by ['3/4', '1/8', '5/8']
'''
optMatch: MeterSequence | None | tuple[str, ...] = None
# assume a list of terminal definitions
if isinstance(numeratorList[0], str):
# TODO: working with private methods of a created MeterSequence
test = MeterSequence()
for mtStr in numeratorList:
test._addTerminal(t.cast(str, mtStr))
test._updateRatio()
# if durations are equal, this can be used as a partition
if self.duration.quarterLength == test.duration.quarterLength:
optMatch = test
else:
raise MeterException(f'Cannot set partition by {numeratorList}')
elif sum(t.cast(list[int], numeratorList)) in [self.numerator * x for x in range(1, 9)]:
for i in range(1, 9):
if sum(t.cast(list[int], numeratorList)) == self.numerator * i:
optMatchInner: list[str] = []
for n in numeratorList:
optMatchInner.append(f'{n}/{self.denominator * i}')
optMatch = tuple(optMatchInner)
break
# last resort: search options
else:
opts = self.getPartitionOptions()
for opt in opts:
# get numerators as numbers
nFound = [int(x.split('/')[0]) for x in opt]
if nFound == numeratorList:
optMatch = opt
break
if optMatch is None:
raise MeterException(
f'Cannot set partition by {numeratorList} ({self.numerator}/{self.denominator})'
)
# Since we have a numerator/denominator match, set this MeterSequence
targetWeight = self.weight
self._clearPartition() # clears self.weight
for mStr in optMatch:
self._addTerminal(mStr)
self.weight = targetWeight
# clear cache
self._levelListCache = {}
[docs]
def partitionByOtherMeterSequence(self, other: MeterSequence) -> None:
'''
Set partition to that found in another
MeterSequence.
>>> a = meter.MeterSequence('4/4', 4)
>>> str(a)
'{1/4+1/4+1/4+1/4}'
>>> b = meter.MeterSequence('4/4', 2)
>>> a.partitionByOtherMeterSequence(b)
>>> len(a)
2
>>> str(a)
'{1/2+1/2}'
'''
if (self.numerator == other.numerator
and self.denominator == other.denominator):
targetWeight = self.weight
self._clearPartition()
for mt in other:
self._addTerminal(copy.deepcopy(mt))
self.weight = targetWeight
else:
raise MeterException('Cannot set partition for unequal MeterSequences')
# clear cache
self._levelListCache = {}
[docs]
def partition(
self,
value: int | Sequence[str] | Sequence[MeterTerminal] | Sequence[int] | MeterSequence,
loadDefault=False
) -> None:
'''
Partitioning creates and sets a number of MeterTerminals
that make up this MeterSequence.
A simple way to partition based on argument time. Single integers
are treated as beat counts; lists are treated as numerator lists;
MeterSequence objects are partitioned by calling partitionByOtherMeterSequence().
>>> a = meter.MeterSequence('5/4+3/8')
>>> len(a)
2
>>> str(a)
'{5/4+3/8}'
>>> b = meter.MeterSequence('13/8')
>>> len(b)
1
>>> str(b)
'{13/8}'
>>> b.partition(13)
>>> len(b)
13
>>> str(b)
'{1/8+1/8+1/8+...+1/8}'
>>> a.partition(b)
>>> len(a)
13
>>> str(a)
'{1/8+1/8+1/8+...+1/8}'
Demo of loadDefault: if impossible, then do it another way:
>>> c = meter.MeterSequence('3/128')
>>> c.partition(2)
Traceback (most recent call last):
music21.exceptions21.MeterException: Cannot set partition by 2 (3/128)
>>> c = meter.MeterSequence('3/128')
>>> c.partition(2, loadDefault=True)
>>> len(c)
3
>>> str(c)
'{1/128+1/128+1/128}'
* Changed in v9.3: if given a list it must either be all numbers, all strings,
or all MeterTerminals, not a mix (which was undocumented and buggy)
'''
if common.isListLike(value):
self.partitionByList(value)
elif isinstance(value, MeterSequence):
self.partitionByOtherMeterSequence(value)
elif isinstance(value, int):
self.partitionByCount(value, loadDefault=loadDefault)
else:
raise MeterException(f'cannot process partition argument {value}')
[docs]
def subdividePartitionsEqual(self, divisions: int|None = None) -> None:
'''
Subdivide all partitions by equally-spaced divisions,
given a divisions value. Manipulates this MeterSequence in place.
Divisions value may optionally be a MeterSequence,
from which a top-level partitioning structure is derived.
Example: First we will do a normal partition (not subdivided partition)
>>> ms = meter.MeterSequence('2/4')
>>> ms
<music21.meter.core.MeterSequence {2/4}>
>>> len(ms)
1
>>> ms[0]
<music21.meter.core.MeterTerminal 2/4>
>>> len(ms[0])
Traceback (most recent call last):
TypeError: object of type 'MeterTerminal' has no len()
Divide the Sequence into two parts, so now there are two
MeterTerminals of 1/4 each:
>>> ms.partition(2)
>>> ms
<music21.meter.core.MeterSequence {1/4+1/4}>
>>> len(ms)
2
>>> ms[0]
<music21.meter.core.MeterTerminal 1/4>
>>> ms[1]
<music21.meter.core.MeterTerminal 1/4>
But what happens if we want to divide each of those into 1/8+1/8 are replace
them by MeterSequences? subdividePartitionsEqual is what is needed.
>>> ms.subdividePartitionsEqual(2)
>>> ms
<music21.meter.core.MeterSequence {{1/8+1/8}+{1/8+1/8}}>
Length is still 2, but each of the components are now MeterSequences of their
own:
>>> len(ms)
2
>>> ms[0]
<music21.meter.core.MeterSequence {1/8+1/8}>
>>> ms[1]
<music21.meter.core.MeterSequence {1/8+1/8}>
There is not a way (the authors know of) to get to the next level.
You would just need to do them individually.
>>> ms[0].subdividePartitionsEqual(2)
>>> ms
<music21.meter.core.MeterSequence {{{1/16+1/16}+{1/16+1/16}}+{1/8+1/8}}>
>>> ms[1].subdividePartitionsEqual(2)
>>> ms
<music21.meter.core.MeterSequence {{{1/16+1/16}+{1/16+1/16}}+{{1/16+1/16}+{1/16+1/16}}}>
If None is given as a parameter, then it will try to find something logical.
>>> ms = meter.MeterSequence('2/4+3/4')
>>> ms.subdividePartitionsEqual(None)
>>> ms
<music21.meter.core.MeterSequence {{1/4+1/4}+{1/4+1/4+1/4}}>
If any partition cannot be divided by the given count, a MeterException is raised:
>>> ms = meter.MeterSequence('5/8+3/8')
>>> len(ms)
2
>>> ms.subdividePartitionsEqual(5)
Traceback (most recent call last):
music21.exceptions21.MeterException: Cannot set partition by 5 (3/8)
'''
divisionsLocal: int = 1
for i in range(len(self)):
if divisions is None: # get dynamically
partitionNumerator: int = self[i].numerator
if partitionNumerator in (1, 2, 4, 8, 16, 32, 64):
divisionsLocal = 2
elif partitionNumerator == 3:
divisionsLocal = 3
elif partitionNumerator in (6, 9, 12, 15, 18, 21, 24, 27):
divisionsLocal = partitionNumerator // 3
else:
# TODO: get from the smallest prime number
divisionsLocal = partitionNumerator
else:
divisionsLocal = divisions
# environLocal.printDebug(['got divisions:', divisionsLocal,
# 'for numerator', self[i].numerator, 'denominator', self[i].denominator])
self[i] = self[i].subdivide(divisionsLocal)
# clear cache
self._levelListCache = {}
def _subdivideNested(self, processObjList, divisions):
# noinspection PyShadowingNames
'''
Recursive nested call routine. Returns a list of the MeterSequences at the newly created
level.
>>> ms = meter.MeterSequence('2/4')
>>> ms.partition(2)
>>> ms
<music21.meter.core.MeterSequence {1/4+1/4}>
>>> ms[0]
<music21.meter.core.MeterTerminal 1/4>
>>> post = ms._subdivideNested([ms], 2)
>>> ms
<music21.meter.core.MeterSequence {{1/8+1/8}+{1/8+1/8}}>
>>> ms[0]
<music21.meter.core.MeterSequence {1/8+1/8}>
>>> post
[<music21.meter.core.MeterSequence {1/8+1/8}>, <music21.meter.core.MeterSequence {1/8+1/8}>]
>>> ms[0] is post[0]
True
>>> post2 = ms._subdivideNested(post, 2) # pass post here
>>> ms
<music21.meter.core.MeterSequence {{{1/16+1/16}+{1/16+1/16}}+{{1/16+1/16}+{1/16+1/16}}}>
>>> post2
[<music21.meter.core.MeterSequence {1/16+1/16}>,
<music21.meter.core.MeterSequence {1/16+1/16}>,
<music21.meter.core.MeterSequence {1/16+1/16}>,
<music21.meter.core.MeterSequence {1/16+1/16}>]
Notice that since we gave a list of lists, post2 is now one level down
>>> post2[0] is ms[0]
False
>>> post2[0] is ms[0][0]
True
'''
for obj in processObjList:
obj.subdividePartitionsEqual(divisions)
# gather references for recursive processing
post = []
for obj in processObjList:
for sub in obj:
post.append(sub)
# clear cache
self._levelListCache = {}
return post
[docs]
def subdivideNestedHierarchy(self, depth, firstPartitionForm=None,
normalizeDenominators=True):
'''
Create nested structure down to a specified depth;
the first division is set to one; the second division
may be by 2 or 3; remaining divisions are always by 2.
This a destructive procedure that will remove
any existing partition structures.
`normalizeDenominators`, if True, will reduce all denominators to the same minimum level.
>>> ms = meter.MeterSequence('4/4')
>>> ms.subdivideNestedHierarchy(1)
>>> ms
<music21.meter.core.MeterSequence {{1/2+1/2}}>
>>> ms.subdivideNestedHierarchy(2)
>>> ms
<music21.meter.core.MeterSequence {{{1/4+1/4}+{1/4+1/4}}}>
>>> ms.subdivideNestedHierarchy(3)
>>> ms
<music21.meter.core.MeterSequence {{{{1/8+1/8}+{1/8+1/8}}+{{1/8+1/8}+{1/8+1/8}}}}>
I think you get the picture!
The effects above are not cumulative. Users can skip directly to
whatever level of hierarchy they want.
>>> ms2 = meter.MeterSequence('4/4')
>>> ms2.subdivideNestedHierarchy(3)
>>> ms2
<music21.meter.core.MeterSequence {{{{1/8+1/8}+{1/8+1/8}}+{{1/8+1/8}+{1/8+1/8}}}}>
'''
# as a hierarchical representation, zeroth subdivision must be 1
self.partition(1)
depthCount = 0
# initial divisions are often based on numerator or are provided
# by looking at the number of top-level beat partitions
# thus, 6/8 will have 2, 18/4 should have 5
if isinstance(firstPartitionForm, MeterSequence):
# change self in place, as we cannot re-assign to self:
# self = self.subdivideByOther(firstPartitionForm.getLevel(0))
self.load(firstPartitionForm.getLevel(0))
depthCount += 1
else: # can be just a number
if firstPartitionForm is None:
firstPartitionForm = self.numerator
# use a fixed mapping for first divider; may be a good algo solution
if firstPartitionForm in [1, 2, 4, 8, 16, 32]:
divFirst = 2
elif firstPartitionForm in [3]:
divFirst = 3
# elif firstPartitionForm in [6, 9, 12, 15, 18]:
# divFirst = firstPartitionForm / 3
# otherwise, set the first div to the number of beats; in 18/4
# this should be 6
else: # set to numerator
divFirst = firstPartitionForm
# use partitions equal, divide by number
self.subdividePartitionsEqual(divFirst)
# self[h] = self[h].subdivide(divFirst)
depthCount += 1
# environLocal.printDebug(['subdivideNestedHierarchy(): firstPartitionForm:',
# firstPartitionForm, ': self: ', self])
# all other partitions are recursive; start first with list
post = [self[0]]
while depthCount < depth:
# setting divisions to None will get either 2/3 for all components
post = self._subdivideNested(post, divisions=None)
depthCount += 1
# need to detect cases of unequal denominators
if not normalizeDenominators:
continue
while True:
d = []
for ref in post:
if ref.denominator not in d:
d.append(ref.denominator)
# if we have more than one denominator; we need to normalize
# environLocal.printDebug(['subdivideNestedHierarchy():', 'd', d,
# 'post', post, 'depthCount', depthCount])
if len(d) > 1:
postNew = []
for i in range(len(post)):
# if this is a lower denominator (1/4 not 1/8),
# process again
if post[i].denominator == min(d):
postNew += self._subdivideNested([post[i]],
divisions=None)
else: # keep original if no problem
postNew.append(post[i])
post = postNew # reassigning to original
else:
break
# clear cache; done in self._subdivideNested and possibly not
# needed here
self._levelListCache = {}
# environLocal.printDebug(['subdivideNestedHierarchy(): post nested processing:', self])
# --------------------------------------------------------------------------
@property
def partitionStr(self):
'''
Return the number of top-level partitions in this MeterSequence as a string.
>>> ms = meter.MeterSequence('2/4+2/4')
>>> ms
<music21.meter.core.MeterSequence {2/4+2/4}>
>>> ms.partitionStr
'Duple'
>>> ms = meter.MeterSequence('6/4', 6)
>>> ms
<music21.meter.core.MeterSequence {1/4+1/4+1/4+1/4+1/4+1/4}>
>>> ms.partitionStr
'Sextuple'
>>> ms = meter.MeterSequence('6/4', 2)
>>> ms.partitionStr
'Duple'
>>> ms = meter.MeterSequence('6/4', 3)
>>> ms.partitionStr
'Triple'
Anything larger than 8 is simply the number followed by '-uple'
>>> ms = meter.MeterSequence('13/4', 13)
>>> ms.partitionStr
'13-uple'
Single partition:
>>> ms = meter.MeterSequence('3/4', 1)
>>> ms.partitionStr
'Single'
'''
count = len(self)
countName = ('Empty', # should not happen
'Single',
'Duple', 'Triple', 'Quadruple', 'Quintuple',
'Sextuple', 'Septuple', 'Octuple')
if count < len(countName):
return countName[count]
else:
return str(count) + '-uple'
# --------------------------------------------------------------------------
# loading is always destructive
[docs]
def load(self,
value: str | MeterTerminal | Sequence[MeterTerminal] | Sequence[str],
partitionRequest: (int
| Sequence[str]
| Sequence[MeterTerminal]
| Sequence[int]
| MeterSequence
| None) = None,
autoWeight: bool = False,
targetWeight: int|float|None = None):
'''
This method is called when a MeterSequence is created, or if a MeterSequence is re-set.
User can enter a list of values or an abbreviated slash notation.
autoWeight, if True, will attempt to set weights.
targetWeight, if given, will be used instead of self.weight
loading is a destructive operation.
>>> a = meter.MeterSequence()
>>> a.load('4/4', 4)
>>> a
<music21.meter.core.MeterSequence {1/4+1/4+1/4+1/4}>
>>> str(a)
'{1/4+1/4+1/4+1/4}'
>>> a.load('4/4', 2) # request 2 beats
>>> a
<music21.meter.core.MeterSequence {1/2+1/2}>
>>> str(a)
'{1/2+1/2}'
>>> a.load('5/8', 2) # request 2 beats
>>> str(a)
'{2/8+3/8}'
>>> a.load('5/8+4/4')
>>> str(a)
'{5/8+4/4}'
'''
# NOTE: this is a performance critical method
if autoWeight:
if targetWeight is None:
# get from current MeterSequence
targetWeight = self.weight # store old
else: # None will not set any value
targetWeight = None
# environLocal.printDebug(['calling load in MeterSequence, got targetWeight', targetWeight])
self._clearPartition()
if isinstance(value, str):
ratioList, self.summedNumerator = tools.slashMixedToFraction(value)
for n, d in ratioList:
slashNotation = f'{n}/{d}'
self._addTerminal(MeterTerminal(slashNotation))
self._updateRatio()
if targetWeight is not None:
self.weight = targetWeight
elif isinstance(value, MeterTerminal):
# if we have a single MeterTerminal and autoWeight is active
# set this terminal to the old weight
if targetWeight is not None:
value.weight = targetWeight
self._addTerminal(value)
self._updateRatio()
# do not need to set weight, as based on terminal
# environLocal.printDebug([
# 'created MeterSequence from MeterTerminal; old weight, new weight',
# value.weight, self.weight])
elif common.isIterable(value): # a list of Terminals or Sequence es
for obj in value:
# environLocal.printDebug(f'creating MeterSequence with {obj}')
self._addTerminal(obj)
self._updateRatio()
if targetWeight is not None:
self.weight = targetWeight
else:
raise MeterException(f'cannot create a MeterSequence with a {value!r}')
if partitionRequest is not None:
self.partition(partitionRequest)
# clear cache
self._levelListCache = {}
def _updateRatio(self):
'''
Look at _partition to determine the total
numerator and denominator values for this sequence
This should only be called internally, as MeterSequences
are supposed to be immutable (mostly)
'''
fTuple = tuple((mt.numerator, mt.denominator) for mt in self._partition)
# clear first to avoid partial updating
# can only set to private attributes
# self._numerator, self._denominator = None, 1
self._numerator, self._denominator = tools.fractionSum(fTuple)
# must call ratio changed directly as not using properties
self._ratioChanged()
# --------------------------------------------------------------------------
# properties
# do not permit setting of numerator/denominator
@property
def weight(self) -> int | float:
'''
Get the weight for the MeterSequence, or set the weight and thereby change the weights
for each object in this MeterSequence.
By default, all the partitions of a MeterSequence's weights sum to 1.0
>>> a = meter.MeterSequence('3/4')
>>> a.weight
1.0
>>> a.partition(3)
>>> a
<music21.meter.core.MeterSequence {1/4+1/4+1/4}>
>>> a.weight
1.0
>>> a[0].weight
0.3333...
But this MeterSequence might be embedded in another one, so perhaps
its weight should be 0.5?
>>> a.weight = 0.5
>>> a[0].weight
0.16666...
When creating a new MeterSequence from MeterTerminals, the sequence has
the weight of the sum of those creating it.
>>> downbeat = meter.MeterTerminal('1/4', 0.5)
>>> upbeat = meter.MeterTerminal('1/4', 0.25)
>>> accentSequence = meter.MeterSequence([downbeat, upbeat])
>>> accentSequence.weight
0.75
Changing the weight of the child sequence will affect the parent, since this is
not cached, but recomputed on each call.
>>> downbeat.weight = 0.375
>>> accentSequence.weight
0.625
Changing the weight on the parent sequence will reset weights on the children
>>> accentSequence.weight = 1.0
>>> (downbeat.weight, upbeat.weight)
(0.5, 0.5)
Assume this MeterSequence is a whole, not a part of some larger MeterSequence.
Thus, we cannot use numerator/denominator relationship
as a scalar.
'''
summation = 0.0
for obj in self._partition:
summation += obj.weight # may be a MeterTerminal or MeterSequence
return summation
@weight.setter
def weight(self, value: int | float) -> None:
# environLocal.printDebug(['calling setWeight with value', value])
if not common.isNum(value):
raise MeterException('weight values must be numbers')
try:
totalRatio = self._numerator / self._denominator
except TypeError as te:
raise MeterException(
'Something wrong with the type of '
f'this numerator {self._numerator} {type(self._numerator)} '
f'or this denominator {self._denominator} {type(self._denominator)}'
) from te
for mt in self._partition:
# for mt in self:
partRatio = mt._numerator / mt._denominator
mt.weight = value * (partRatio / totalRatio)
# mt.weight = (partRatio/totalRatio) #* totalRatio
# environLocal.printDebug(['setting weight based on part, total, weight',
# partRatio, totalRatio, mt.weight])
def _getFlatList(self):
'''
Return a flattened version of this
MeterSequence as a list of MeterTerminals.
This return a list and not a new MeterSequence b/c MeterSequence objects
are generally immutable and thus it does not make sense
to concatenate them.
>>> a = meter.MeterSequence('3/4')
>>> a.partition(3)
>>> b = a._getFlatList()
>>> b
[<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterTerminal 1/4>]
>>> len(b)
3
>>> a[1] = a[1].subdivide(4)
>>> len(a)
3
>>> a
<music21.meter.core.MeterSequence {1/4+{1/16+1/16+1/16+1/16}+1/4}>
>>> b = a._getFlatList()
>>> len(b)
6
>>> b
[<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterTerminal 1/16>,
<music21.meter.core.MeterTerminal 1/16>,
<music21.meter.core.MeterTerminal 1/16>,
<music21.meter.core.MeterTerminal 1/16>,
<music21.meter.core.MeterTerminal 1/4>]
>>> a[1][2] = a[1][2].subdivide(4)
>>> a
<music21.meter.core.MeterSequence {1/4+{1/16+1/16+{1/64+1/64+1/64+1/64}+1/16}+1/4}>
>>> b = a._getFlatList()
>>> len(b)
9
'''
# Is this the same as getLevelList(0)?
mtList = []
for obj in self._partition: # or for obj in self
if not isinstance(obj, MeterSequence):
mtList.append(obj)
else: # its a meter sequence
mtList += obj._getFlatList()
return mtList
[docs]
def flatten(self) -> MeterSequence:
'''
Return a new MeterSequence composed of the flattened representation.
Here a sequence is already flattened:
>>> ms = meter.MeterSequence('3/4', 3)
>>> ms
<music21.meter.core.MeterSequence {1/4+1/4+1/4}>
>>> b = ms.flatten()
>>> b
<music21.meter.core.MeterSequence {1/4+1/4+1/4}>
>>> len(b)
3
>>> b is ms
False
Now take the original MeterSequence and subdivide the second beat into 4 parts:
>>> ms[1] = ms[1].subdivide(4)
>>> ms
<music21.meter.core.MeterSequence {1/4+{1/16+1/16+1/16+1/16}+1/4}>
>>> b = ms.flatten()
>>> len(b)
6
>>> b
<music21.meter.core.MeterSequence {1/4+1/16+1/16+1/16+1/16+1/4}>
>>> ms[1][2] = ms[1][2].subdivide(4)
>>> ms
<music21.meter.core.MeterSequence {1/4+{1/16+1/16+{1/64+1/64+1/64+1/64}+1/16}+1/4}>
>>> b = ms.flatten()
>>> len(b)
9
>>> b
<music21.meter.core.MeterSequence {1/4+1/16+1/16+1/64+1/64+1/64+1/64+1/16+1/4}>
'''
post = MeterSequence()
post.load(self._getFlatList())
return post
@property
def flatWeight(self):
'''
Return a list of flat weight values
'''
post = []
for mt in self._getFlatList():
post.append(mt.weight)
return post
@property
def depth(self):
'''
Return how many unique levels deep this part is
This should be optimized to store values unless the structure has changed.
'''
depth = 0 # start with 0, will count this level
lastMatch = None
while True:
test = self.getLevelList(depth)
if test != lastMatch:
depth += 1
lastMatch = test
else:
break
return depth
# --------------------------------------------------------------------------
# alternative representations
[docs]
def getLevelList(self, levelCount: int, flat: bool = True) -> list[MeterTerminal]:
'''
Recursive utility function that gets everything at a certain level.
If flat is True, it guarantees to return a list of MeterTerminals and not
MeterSequences. Otherwise, there may be Sequences in there.
Example: a Sequence representing something in 4/4 divided as
1 quarter, 2 eighth, 1 quarter, ((2-sixteenths) + 1 eighth).
>>> b = meter.MeterSequence('4/4', 4)
>>> b[1] = b[1].subdivide(2)
>>> b[3] = b[3].subdivide(2)
>>> b[3][0] = b[3][0].subdivide(2)
>>> b
<music21.meter.core.MeterSequence {1/4+{1/8+1/8}+1/4+{{1/16+1/16}+1/8}}>
Get the top level of this structure, flattening everything underneath:
>>> b.getLevelList(0)
[<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterTerminal 1/4>]
One level down:
>>> b.getLevelList(1)
[<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterTerminal 1/8>,
<music21.meter.core.MeterTerminal 1/8>,
<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterTerminal 1/8>,
<music21.meter.core.MeterTerminal 1/8>]
Without flattening, first two levels:
>>> b.getLevelList(0, flat=False)
[<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterSequence {1/8+1/8}>,
<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterSequence {{1/16+1/16}+1/8}>]
(Note that levelList 0, flat=False is essentially the same as iterating
over a MeterSequence)
>>> list(b)
[<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterSequence {1/8+1/8}>,
<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterSequence {{1/16+1/16}+1/8}>]
>>> b.getLevelList(1, flat=False)
[<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterTerminal 1/8>,
<music21.meter.core.MeterTerminal 1/8>,
<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterSequence {1/16+1/16}>,
<music21.meter.core.MeterTerminal 1/8>]
Generally, these level lists will be converted back to MeterSequences:
>>> meter.MeterSequence(b.getLevelList(0))
<music21.meter.core.MeterSequence {1/4+1/4+1/4+1/4}>
>>> meter.MeterSequence(b.getLevelList(1))
<music21.meter.core.MeterSequence {1/4+1/8+1/8+1/4+1/8+1/8}>
>>> meter.MeterSequence(b.getLevelList(2))
<music21.meter.core.MeterSequence {1/4+1/8+1/8+1/4+1/16+1/16+1/8}>
>>> meter.MeterSequence(b.getLevelList(3))
<music21.meter.core.MeterSequence {1/4+1/8+1/8+1/4+1/16+1/16+1/8}>
OMIT_FROM_DOCS
Test that cache is used and does not get manipulated
>>> b = meter.MeterSequence('3/4', 3)
>>> (0, True) in b._levelListCache
False
>>> o = b.getLevelList(0)
Mess with the list:
>>> o.append(meter.core.MeterTerminal('1/8'))
>>> o
[<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterTerminal 1/8>]
Cache is populated:
>>> (0, True) in b._levelListCache
True
But a new list is created.
>>> b.getLevelList(0)
[<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterTerminal 1/4>,
<music21.meter.core.MeterTerminal 1/4>]
>>> b.getLevelList(0)[0] is o[0]
True
'''
cacheKey = (levelCount, flat)
try: # check in cache
return list(tuple(self._levelListCache[cacheKey]))
except KeyError:
pass
mtList: list[MeterTerminal] = []
for i in range(len(self._partition)):
# environLocal.printDebug(['getLevelList weight', i, self[i].weight])
partition_i: MeterTerminal|MeterSequence = self._partition[i]
if not isinstance(partition_i, MeterSequence):
mt = self[i] # a MeterTerminal
mtList.append(mt)
else: # it is a MeterSequence
if levelCount > 0: # retain this sequence but get lower level
# reduce level by 1 when recursing; do not
# change levelCount here
mtList += partition_i.getLevelList(
levelCount - 1, flat)
else: # level count is at zero
if flat: # make sequence into a terminal
mt = MeterTerminal(
f'{partition_i.numerator}/{partition_i.denominator}'
)
# set weight to that of the sequence
mt.weight = partition_i.weight
mtList.append(mt)
else: # it is not a terminal, it is a meter sequence
mtList.append(partition_i)
# store in cache but let this be manipulated
self._levelListCache[cacheKey] = list(tuple(mtList))
return mtList
[docs]
def getLevel(self, level=0, flat=True):
'''
Return a complete MeterSequence with the same numerator/denominator
relationship but that represents any partitions found at the requested
level. A sort of flatness with variable depth.
>>> b = meter.MeterSequence('4/4', 4)
>>> b[1] = b[1].subdivide(2)
>>> b[3] = b[3].subdivide(2)
>>> b[3][0] = b[3][0].subdivide(2)
>>> b
<music21.meter.core.MeterSequence {1/4+{1/8+1/8}+1/4+{{1/16+1/16}+1/8}}>
>>> b.getLevel(0)
<music21.meter.core.MeterSequence {1/4+1/4+1/4+1/4}>
>>> b.getLevel(1)
<music21.meter.core.MeterSequence {1/4+1/8+1/8+1/4+1/8+1/8}>
>>> b.getLevel(2)
<music21.meter.core.MeterSequence {1/4+1/8+1/8+1/4+1/16+1/16+1/8}>
'''
return MeterSequence(self.getLevelList(level, flat))
[docs]
def getLevelSpan(self, level=0):
'''
For a given level, return the time span of each terminal or sequence
>>> b = meter.MeterSequence('4/4', 4)
>>> b[1] = b[1].subdivide(2)
>>> b[3] = b[3].subdivide(2)
>>> b[3][0] = b[3][0].subdivide(2)
>>> b
<music21.meter.core.MeterSequence {1/4+{1/8+1/8}+1/4+{{1/16+1/16}+1/8}}>
>>> b.getLevelSpan(0)
[(0.0, 1.0), (1.0, 2.0), (2.0, 3.0), (3.0, 4.0)]
>>> b.getLevelSpan(1)
[(0.0, 1.0), (1.0, 1.5), (1.5, 2.0), (2.0, 3.0), (3.0, 3.5), (3.5, 4.0)]
>>> b.getLevelSpan(2)
[(0.0, 1.0), (1.0, 1.5), (1.5, 2.0), (2.0, 3.0), (3.0, 3.25), (3.25, 3.5), (3.5, 4.0)]
'''
ms = self.getLevelList(level, flat=True)
mapping = []
pos = 0.0
for i in range(len(ms)):
start = pos
end = opFrac(pos + ms[i].duration.quarterLength)
mapping.append((start, end))
pos = end
return mapping
[docs]
def getLevelWeight(self, level=0):
'''
The weightList is an array of weights found in the components.
The MeterSequence has a ._weight attribute, but it is not used here
>>> a = meter.MeterSequence('4/4', 4)
>>> a.getLevelWeight()
[0.25, 0.25, 0.25, 0.25]
>>> b = meter.MeterSequence('4/4', 4)
>>> b.getLevelWeight(0)
[0.25, 0.25, 0.25, 0.25]
>>> b[1] = b[1].subdivide(2)
>>> b[3] = b[3].subdivide(2)
>>> b.getLevelWeight(0)
[0.25, 0.25, 0.25, 0.25]
>>> b[3][0] = b[3][0].subdivide(2)
>>> b
<music21.meter.core.MeterSequence {1/4+{1/8+1/8}+1/4+{{1/16+1/16}+1/8}}>
>>> b.getLevelWeight(0)
[0.25, 0.25, 0.25, 0.25]
>>> b.getLevelWeight(1)
[0.25, 0.125, 0.125, 0.25, 0.125, 0.125]
>>> b.getLevelWeight(2)
[0.25, 0.125, 0.125, 0.25, 0.0625, 0.0625, 0.125]
'''
post = []
for mt in self.getLevelList(level):
post.append(mt.weight)
return post
[docs]
def setLevelWeight(self, weightList, level=0):
'''
The `weightList` is an array of weights to be applied to a
single level of the MeterSequence.
>>> a = meter.MeterSequence('4/4', 4)
>>> a.setLevelWeight([1, 2, 3, 4])
>>> a.getLevelWeight()
[1, 2, 3, 4]
>>> b = meter.MeterSequence('4/4', 4)
>>> b.setLevelWeight([2, 3])
>>> b.getLevelWeight(0)
[2, 3, 2, 3]
>>> b[1] = b[1].subdivide(2)
>>> b[3] = b[3].subdivide(2)
>>> b.getLevelWeight(0)
[2, 3.0, 2, 3.0]
>>> b[3][0] = b[3][0].subdivide(2)
>>> b
<music21.meter.core.MeterSequence {1/4+{1/8+1/8}+1/4+{{1/16+1/16}+1/8}}>
>>> b.getLevelWeight(0)
[2, 3.0, 2, 3.0]
>>> b.getLevelWeight(1)
[2, 1.5, 1.5, 2, 1.5, 1.5]
>>> b.getLevelWeight(2)
[2, 1.5, 1.5, 2, 0.75, 0.75, 1.5]
'''
levelObjs = self.getLevelList(level)
for i in range(len(levelObjs)):
mt = levelObjs[i]
mt.weight = weightList[i % len(weightList)]
# --------------------------------------------------------------------------
# given a quarter note position, return the active index
[docs]
def offsetToIndex(self, qLenPos, includeCoincidentBoundaries=False) -> int:
'''
Given an offset in quarterLengths (0.0 through self.duration.quarterLength), return
the index of the active MeterTerminal or MeterSequence
>>> a = meter.MeterSequence('4/4')
>>> a.offsetToIndex(0.5)
0
>>> a.offsetToIndex(3.5)
0
>>> a.partition(4)
>>> a.offsetToIndex(0.5)
0
>>> a.offsetToIndex(3.5)
3
>>> a.partition([1, 2, 1])
>>> len(a)
3
>>> a.offsetToIndex(2.9)
1
>>> a[a.offsetToIndex(2.9)]
<music21.meter.core.MeterTerminal 2/4>
>>> a = meter.MeterSequence('4/4')
>>> a.offsetToIndex(5.0)
Traceback (most recent call last):
music21.exceptions21.MeterException: cannot access from qLenPos 5.0
where total duration is 4.0
Negative numbers also raise an exception:
>>> a.offsetToIndex(-0.5)
Traceback (most recent call last):
music21.exceptions21.MeterException: cannot access from qLenPos -0.5
where total duration is 4.0
'''
if qLenPos >= self.duration.quarterLength or qLenPos < 0:
raise MeterException(
f'cannot access from qLenPos {qLenPos} '
f'where total duration is {self.duration.quarterLength}'
)
qPos = 0
match = -1 # no match -- will not happen.
for i in range(len(self)):
start = qPos
end = opFrac(qPos + self[i].duration.quarterLength)
# if adjoining ends are permitted, first match is found
if includeCoincidentBoundaries:
if start <= qLenPos <= end:
match = i
break
else:
# note that this is <=, meaning that the first boundary
# is coincident.
if start <= qLenPos < end:
match = i
break
qPos = opFrac(qPos + self[i].duration.quarterLength)
return match
[docs]
def offsetToAddress(self, qLenPos, includeCoincidentBoundaries=False):
'''
Give a list of values that show all indices necessary to access
the exact terminal at a given qLenPos.
The len of the returned list also provides the depth at the specified qLen.
>>> a = meter.MeterSequence('3/4', 3)
>>> a[1] = a[1].subdivide(4)
>>> a
<music21.meter.core.MeterSequence {1/4+{1/16+1/16+1/16+1/16}+1/4}>
>>> len(a)
3
>>> a.offsetToAddress(0.5)
[0]
>>> a[0]
<music21.meter.core.MeterTerminal 1/4>
>>> a.offsetToAddress(1.0)
[1, 0]
>>> a.offsetToAddress(1.5)
[1, 2]
>>> a[1][2]
<music21.meter.core.MeterTerminal 1/16>
>>> a.offsetToAddress(1.99)
[1, 3]
>>> a.offsetToAddress(2.5)
[2]
'''
if qLenPos >= self.duration.quarterLength or qLenPos < 0:
raise MeterException(f'cannot access from qLenPos {qLenPos}')
start = 0
qPos = 0
match = []
i = None
for i in range(len(self)):
start = qPos
end = opFrac(qPos + self[i].duration.quarterLength)
# if adjoining ends are permitted, first match is found
if includeCoincidentBoundaries:
if start <= qLenPos <= end:
match.append(i)
break
else:
if start <= qLenPos < end:
match.append(i)
break
qPos = opFrac(qPos + self[i].duration.quarterLength)
if i is not None and isinstance(self[i], MeterSequence): # recurse
# qLenPosition needs to be relative to this subdivision
# start is our current position that this subdivision
# starts at
qLenPosShift = opFrac(qLenPos - start)
match += self[i].offsetToAddress(qLenPosShift,
includeCoincidentBoundaries)
return match
[docs]
def offsetToSpan(self, qLenPos, permitMeterModulus=False):
'''
Given a qLenPos, return the span of the active region.
Only applies to the top most level of partitions
If `permitMeterModulus` is True, quarter length positions
greater than the duration of the Meter will be accepted
as the modulus of the total meter duration.
>>> a = meter.MeterSequence('3/4', 3)
>>> a.offsetToSpan(0.5)
(0, 1.0)
>>> a.offsetToSpan(1.5)
(1.0, 2.0)
This is the same as 1.5:
>>> a.offsetToSpan(4.5, permitMeterModulus=True)
(1.0, 2.0)
Make sure it works for tuplets even with so-so rounding:
>>> a.offsetToSpan(4.33333336, permitMeterModulus=True)
(1.0, 2.0)
'''
qLenPos = opFrac(qLenPos)
if qLenPos >= self.duration.quarterLength or qLenPos < 0:
if not permitMeterModulus:
# environLocal.printDebug(['exceeding range:', self,
# 'self.duration', self.duration])
raise MeterException(
f'cannot access qLenPos {qLenPos} when total duration is '
f'{self.duration.quarterLength} and ts is {self}'
)
# environLocal.printDebug(['offsetToSpan', 'got qLenPos old', qLenPos])
qLenPos = qLenPos % self.duration.quarterLength
# environLocal.printDebug(['offsetToSpan', 'got qLenPos old', qLenPos])
iMatch = self.offsetToIndex(qLenPos)
pos = 0
start = None
end = None
for i in range(len(self)):
# print(i, iMatch, self[i])
if i == iMatch:
start = pos
end = opFrac(pos + self[i].duration.quarterLength)
else:
pos = opFrac(pos + self[i].duration.quarterLength)
# environLocal.printDebug(['start, end', start, end])
return start, end
[docs]
def offsetToWeight(self, qLenPos):
'''
Given a lenPos, return the weight of the active region.
Only applies to the top-most level of partitions
>>> a = meter.MeterSequence('3/4', 3)
>>> a.offsetToWeight(0.0)
Fraction(1, 3)
>>> a.offsetToWeight(1.5)
Fraction(1, 3)
'''
# Not sure what this does!
qLenPos = opFrac(qLenPos)
if qLenPos >= self.duration.quarterLength or qLenPos < 0:
raise MeterException(
f'cannot access qLenPos {qLenPos} when total duration is '
f'{self.duration.quarterLength} and ts is {self}'
)
iMatch = self.offsetToIndex(qLenPos)
return opFrac(self[iMatch].weight)
[docs]
def offsetToDepth(self, qLenPos: OffsetQLIn, align: str = 'quantize', index: int|None = None):
'''
Given a qLenPos, return the maximum available depth at this position. Align can be
start, quantize, or end.
>>> b = meter.MeterSequence('4/4', 4)
>>> b[1] = b[1].subdivide(2)
>>> b[3] = b[3].subdivide(2)
>>> b[3][0] = b[3][0].subdivide(2)
>>> b
<music21.meter.core.MeterSequence {1/4+{1/8+1/8}+1/4+{{1/16+1/16}+1/8}}>
>>> b.offsetToDepth(0)
3
>>> b.offsetToDepth(0.25) # quantizing active by default
3
>>> b.offsetToDepth(1)
3
>>> b.offsetToDepth(1.5)
2
>>> b.offsetToDepth(-1)
Traceback (most recent call last):
music21.exceptions21.MeterException: cannot access from qLenPos -1.0
* Changed in v7: `index` can be provided, if known, for a long
`MeterSequence` to improve performance.
'''
# TODO: Change the aligns to be StrEnums.
qLenPos = opFrac(qLenPos)
if qLenPos >= self.duration.quarterLength or qLenPos < 0:
raise MeterException(f'cannot access from qLenPos {qLenPos}')
srcMatch = ''
# need to quantize by lowest level
mapMin = self.getLevelSpan(self.depth - 1)
msMin = self.getLevel(self.depth - 1)
if index is None:
index = msMin.offsetToIndex(qLenPos)
qStart, unused_qEnd = mapMin[index]
if align == 'quantize':
posMatch = opFrac(qStart)
else:
posMatch = qLenPos
score = 0
for level in range(self.depth):
mapping = self.getLevelSpan(level) # get mapping for each level
for start, end in mapping:
if align in ('start', 'quantize'):
srcMatch = start
elif align == 'end':
srcMatch = end
if srcMatch == posMatch:
score += 1
return score
if __name__ == '__main__':
import music21
music21.mainTest()