# -----------------------------------------------------------------------------
# Name: beam.py
# Purpose: music21 classes for representing beams and beam groups
#
# Authors: Michael Scott Asato Cuthbert
# Christopher Ariza
#
# Copyright: Copyright © 2009-2012, 2019 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# -----------------------------------------------------------------------------
'''
The module defines Beam and Beams (note plural) objects.
The Beams object holds multiple Beam objects (e.g., a 32nd note might have
three Beam objects in its Beam object).
The Beams object is stored in :class:`~music21.note.Note` and
:class:`~music21.chord.Chord` objects as their :attr:`~music21.note.Note.beams`
attributes. Beams objects can largely be treated as a list.
See `meter.TimeSignature`. :meth:`~music21.meter.TimeSignature.getBeams` for a
way of getting beam information for a measure given the meter. The
`meter.TimeSignature`. :attr:`~music21.meter.TimeSignature.beamSequence`
attribute holds information about how to beam given the TimeSignature.
Run `Stream`. :meth:`~music21.stream.Stream.makeBeams` to set beaming
information automatically given the current meter.
Suppose you had a measure of two eighths and a quarter and wanted to explicitly
beam the two eighth notes. You could do this:
>>> m = stream.Measure()
>>> n1 = note.Note('C4', quarterLength=0.5)
>>> n2 = note.Note('D4', quarterLength=0.5)
>>> n3 = note.Note('E4', quarterLength=1.0)
>>> m.append(n1)
>>> m.append(n2)
>>> m.append(n3)
>>> n1.beams.fill('eighth', type='start')
>>> n2.beams.fill('eighth', type='stop')
>>> n1.beams
<music21.beam.Beams <music21.beam.Beam 1/start>>
>>> n2.beams
<music21.beam.Beams <music21.beam.Beam 1/stop>>
But suppose you wanted something harder: two 16ths, an 8th, a quarter, with the
first 3 notes beamed? The first note and 3rd are easy to do, using the method
above:
>>> m = stream.Measure()
>>> n1 = note.Note('C4', quarterLength=0.25)
>>> n2 = note.Note('D4', quarterLength=0.25)
>>> n3 = note.Note('E4', quarterLength=0.5)
>>> n4 = note.Note('F4', quarterLength=1.0)
>>> for n in [n1, n2, n3, n4]:
... m.append(n)
>>> n1.beams.fill('16th', type='start')
>>> n3.beams.fill('eighth', type='stop')
but the second note has an 8th beam that continues and a 16th beam that stops.
So you will need to set them separately:
>>> n2.beams.append('continue')
>>> n2.beams.append('stop')
>>> n2.beams
<music21.beam.Beams <music21.beam.Beam 1/continue>/<music21.beam.Beam 2/stop>>
To get rid of beams on a note do:
>>> n2.beams.beamsList = []
'''
from __future__ import annotations
from collections.abc import Iterable
import typing as t
import unittest
from music21 import exceptions21
from music21 import duration
from music21 import environment
from music21 import prebase
from music21 import style
from music21.common.objects import EqualSlottedObjectMixin
if t.TYPE_CHECKING:
from music21 import base
environLocal = environment.Environment('beam')
class BeamException(exceptions21.Music21Exception):
pass
beamableDurationTypes = (
duration.typeFromNumDict[8],
duration.typeFromNumDict[16], duration.typeFromNumDict[32],
duration.typeFromNumDict[64], duration.typeFromNumDict[128],
duration.typeFromNumDict[256], duration.typeFromNumDict[512],
duration.typeFromNumDict[1024], duration.typeFromNumDict[2048],
) # be sure to add to .fill if extended
[docs]
class Beam(prebase.ProtoM21Object, EqualSlottedObjectMixin, style.StyleMixin):
'''
A Beam is an object representation of one single beam, that is, one
horizontal line connecting two notes together (or less commonly a note to a
rest). Thus, it takes two separate Beam objects to represent the beaming of
a 16th note.
The Beams object (note the plural) is the object that handles groups of
Beam objects; it is defined later on.
Here are two ways to define the start of a beam
>>> b1 = beam.Beam(type='start')
>>> b2 = beam.Beam('start')
Here is a partial beam (that is, one that does not connect to any other
note, such as the second beam of a dotted eighth, sixteenth group)
Two ways of doing the same thing
>>> b3 = beam.Beam(number=1, type='partial', direction='left')
>>> b3
<music21.beam.Beam 1/partial/left>
>>> b4 = beam.Beam('partial', 'left')
>>> b4.number = 1
>>> b4
<music21.beam.Beam 1/partial/left>
All attributes must be the same for equality:
>>> b3 == b4
True
>>> b2
<music21.beam.Beam None/start>
>>> b2 == b3
False
'''
# CLASS VARIABLES #
__slots__ = (
'direction',
'id',
'independentAngle',
'number',
'type',
)
# INITIALIZER #
# pylint: disable=redefined-builtin
def __init__(self, type=None, direction=None, number=None): # type is okay @ReservedAssignment
super().__init__() # must call for style.
self.type = type # start, stop, continue, partial
self.direction = direction # left or right for partial
self.independentAngle = None
# represents which beam line referred to
# 8th, 16th, etc. represented as 1, 2, ...
self.number = number
# this should be called something else.
self.id = id(self)
# PRIVATE METHODS #
def _reprInternal(self):
out = f'{self.number}/{self.type}'
if self.direction is not None:
out += f'/{self.direction}'
return out
# -----------------------------------------------------------------------------
[docs]
class Beams(prebase.ProtoM21Object, EqualSlottedObjectMixin):
'''
The Beams object stores in it attribute beamsList (a list) all the Beam
objects defined above. Thus, len(beam.Beams) tells you how many beams the
note currently has on it, and iterating over a Beams object gives you each
Beam.
>>> n = note.Note(type='16th')
>>> isinstance(n.beams, beam.Beams)
True
>>> n.beams.fill(2, 'start')
>>> len(n.beams)
2
>>> for thisBeam in n.beams:
... thisBeam.type
'start'
'start'
>>> print(n.beams)
<music21.beam.Beams <music21.beam.Beam 1/start>/<music21.beam.Beam 2/start>>
'''
# CLASS VARIABLES #
__slots__ = (
'beamsList',
'feathered',
'id',
)
_DOC_ATTR: dict[str, str] = {
'feathered': '''
Boolean determining if this is a feathered beam or not
(does nothing for now).''',
}
# INITIALIZER #
def __init__(self) -> None:
# no need for super() call w/ ProtoM21 and EqualSlottedObject
self.beamsList: list[Beam] = []
self.feathered: bool = False
# this should not be called .id.
self.id = id(self)
# SPECIAL METHODS #
def __iter__(self):
return iter(self.beamsList)
def __len__(self):
return len(self.beamsList)
[docs]
def __eq__(self, other):
return isinstance(other, self.__class__) and repr(self) == repr(other)
def __hash__(self):
return id(self) >> 4
def _reprInternal(self):
msg = []
for beam in self.beamsList:
msg.append(str(beam))
return '/'.join(msg)
# STATIC METHODS #
[docs]
@staticmethod
def naiveBeams(srcList: Iterable[base.Music21Object]):
# noinspection PyShadowingNames
'''
Given a list or iterator of elements, return a list of None or Beams for
each element: None if the element is a quarter or larger or
if the element is a Rest, and the fullest possible set of beams
for the duration if it is a beamable. Each beam object has type of None.
staticmethod, does not need instance:
>>> durList = [0, -1, -2, -3]
>>> srcList = [note.Note(quarterLength=2 ** x) for x in durList]
>>> srcList.append(note.Rest(type='32nd'))
>>> beam.Beams.naiveBeams(srcList)
[None,
<music21.beam.Beams <music21.beam.Beam 1/None>>,
<music21.beam.Beams <music21.beam.Beam 1/None>/<music21.beam.Beam 2/None>>,
<music21.beam.Beams <music21.beam.Beam 1/None>/<music21.beam.Beam
2/None>/<music21.beam.Beam 3/None>>,
None]
'''
beamsList: list[Beams|None] = []
for el in srcList:
# if a dur cannot be beamable under any circumstance, replace
# it with None; this includes Rests
if el.duration.type not in beamableDurationTypes:
beamsList.append(None) # placeholder
elif 'NotRest' not in el.classSet:
beamsList.append(None) # placeholder
else:
# we have a beamable duration
b = Beams()
# set the necessary number of internal beamsList, that is,
# one for each horizontal line in the beams group
# this does not set type or direction
b.fill(el.duration.type)
beamsList.append(b)
return beamsList
[docs]
@staticmethod
def removeSandwichedUnbeamables(beamsList: list[Beams|None]):
# noinspection PyShadowingNames
'''
Go through the naiveBeamsList and remove beams from objects surrounded
by None objects -- you can't beam to nothing!
Modifies beamsList in place.
>>> N = note.Note
>>> R = note.Rest
>>> e = 'eighth'
>>> nList = [N(type=e), R(type=e), N(type=e), N(type=e),
... R(type=e), N(type=e), R(type=e), N(type=e)]
>>> beamsList = beam.Beams.naiveBeams(nList)
>>> beamsList
[<music21.beam.Beams <music21.beam.Beam 1/None>>,
None,
<music21.beam.Beams <music21.beam.Beam 1/None>>,
<music21.beam.Beams <music21.beam.Beam 1/None>>,
None,
<music21.beam.Beams <music21.beam.Beam 1/None>>,
None,
<music21.beam.Beams <music21.beam.Beam 1/None>>]
>>> beamsList2 = beam.Beams.removeSandwichedUnbeamables(beamsList)
>>> beamsList2 is beamsList
True
>>> beamsList2
[None,
None,
<music21.beam.Beams <music21.beam.Beam 1/None>>,
<music21.beam.Beams <music21.beam.Beam 1/None>>,
None,
None,
None,
None]
'''
beamLast = None
for i in range(len(beamsList)):
if i != len(beamsList) - 1:
beamNext = beamsList[i + 1]
else:
beamNext = None
if beamLast is None and beamNext is None:
beamsList[i] = None
beamLast = beamsList[i]
return beamsList
[docs]
@staticmethod
def mergeConnectingPartialBeams(beamsList):
'''
Partial-right followed by partial-left must also be connected, even if otherwise
over an archetypeSpan, such as 16th notes 2 and 3 in a quarter note span where
16ths are not beamed by default.
'''
# sanitize two partials in a row:
for i, (bThis, bNext) in enumerate(zip(beamsList[:-1], beamsList[1:])):
if not bThis or not bNext:
continue
bThisNum = bThis.getNumbers()
if not bThisNum:
continue
for thisNum in bThisNum:
thisBeam = bThis.getByNumber(thisNum)
if thisBeam.type != 'partial' or thisBeam.direction != 'right':
continue
if thisNum not in bNext.getNumbers():
continue
nextBeam = bNext.getByNumber(thisNum)
if nextBeam.type == 'partial' and nextBeam.direction == 'right':
continue
if nextBeam.type in ('continue', 'stop'):
environLocal.warn(
f'Found a messed up beam pair {bThis}, {bNext}, '
f'at index {i} of \n{beamsList}'
)
continue
thisBeam.type = 'start'
thisBeam.direction = None
if nextBeam.type == 'partial':
nextBeam.type = 'stop'
elif nextBeam.type == 'start':
nextBeam.type = 'continue'
nextBeam.direction = None
# now fix partial-lefts that follow stops:
for bThis, bPrev in zip(beamsList[1:], beamsList[:-1]):
if not bThis or not bPrev:
continue
bThisNum = bThis.getNumbers()
if not bThisNum:
continue
for thisNum in bThisNum:
thisBeam = bThis.getByNumber(thisNum)
if thisBeam.type != 'partial' or thisBeam.direction != 'left':
continue
if thisNum not in bPrev.getNumbers():
continue
prevBeam = bPrev.getByNumber(thisNum)
if prevBeam.type != 'stop':
continue
thisBeam.type = 'stop'
thisBeam.direction = None
prevBeam.type = 'continue'
return beamsList
[docs]
@staticmethod
def sanitizePartialBeams(beamsList: list[Beams|None]) -> list[Beams|None]:
'''
It is possible at a late stage to have beams that only consist of partials
or beams with a 'start' followed by 'partial/left' or possibly 'stop' followed
by 'partial/right'; beams entirely consisting of partials are removed
and the direction of irrational partials is fixed.
'''
for i, beamsObj in enumerate(beamsList):
if beamsObj is None:
continue
allTypes = beamsObj.getTypes()
# clear elements that have partial beams with no full beams:
if 'start' not in allTypes and 'stop' not in allTypes and 'continue' not in allTypes:
# nothing but partials
beamsList[i] = None
continue
# make sure a partial-left does not follow a start or a partial-right does not
# follow a stop
hasStart = False
hasStop = False
b: Beam
for b in beamsObj.beamsList:
if b.type == 'start':
hasStart = True
continue
if b.type == 'stop':
hasStop = True
continue
if hasStart and b.type == 'partial' and b.direction == 'left':
b.direction = 'right'
elif hasStop and b.type == 'partial' and b.direction == 'right':
b.direction = 'left'
return beamsList
# PUBLIC METHODS #
# pylint: disable=redefined-builtin
[docs]
def append(self, type=None, direction=None): # type is okay @ReservedAssignment
'''
Append a new Beam object to this Beams object, automatically creating the Beam
object and incrementing the number count.
>>> beams = beam.Beams()
>>> beams.append('start')
>>> beams.beamsList
[<music21.beam.Beam 1/start>]
>>> beams.append('partial', 'right')
>>> beams.beamsList
[<music21.beam.Beam 1/start>, <music21.beam.Beam 2/partial/right>]
A beam object can also be specified:
>>> beams = beam.Beams()
>>> beam1 = beam.Beam(type='start', number=1)
>>> beams.append(beam1)
>>> beams.beamsList
[<music21.beam.Beam 1/start>]
'''
if isinstance(type, str):
obj = Beam(type, direction)
obj.number = len(self.beamsList) + 1
else:
obj = type
self.beamsList.append(obj)
[docs]
def fill(self, level=None, type=None): # type is okay @ReservedAssignment
'''
A quick way of setting the beams list for a particular duration, for
instance, `fill('16th')` will clear the current list of beams in the
Beams object and add two beams. `fill(2)` will do the same (though note
that that is an int, not a string).
It does not do anything to the direction that the beams are going in,
or by default. Either set type here or call `setAll()` on the Beams
object afterwards.
Both "eighth" and "8th" work. Adding more than nine beams (i.e. things
like 4096th notes) raises an error.
>>> a = beam.Beams()
>>> a.fill('16th')
>>> len(a)
2
>>> a.fill('32nd', type='start')
>>> len(a)
3
>>> a.beamsList[2]
<music21.beam.Beam 3/start>
>>> a.beamsList[2].type
'start'
Filling a smaller number wipes larger numbers of beams:
>>> a.fill('eighth', type='start')
>>> len(a)
1
OMIT_FROM_DOCS
>>> a.fill(4)
>>> len(a)
4
>>> a.fill('128th')
>>> len(a)
5
>>> a.fill('256th')
>>> len(a)
6
>>> a.fill(12)
Traceback (most recent call last):
music21.beam.BeamException: cannot fill beams for level 12
'''
self.beamsList = []
# 8th, 16th, etc. represented as 1, 2, ...
if level in (1, '8th', duration.typeFromNumDict[8]): # eighth
count = 1
elif level in (2, duration.typeFromNumDict[16]):
count = 2
elif level in (3, duration.typeFromNumDict[32]):
count = 3
elif level in (4, duration.typeFromNumDict[64]):
count = 4
elif level in (5, duration.typeFromNumDict[128]):
count = 5
elif level in (6, duration.typeFromNumDict[256]):
count = 6
elif level in (7, duration.typeFromNumDict[512]):
count = 7
elif level in (8, duration.typeFromNumDict[1024]):
count = 8
elif level in (9, duration.typeFromNumDict[2048]):
count = 9
else:
raise BeamException(f'cannot fill beams for level {level}')
for i in range(1, count + 1):
obj = Beam()
obj.number = i
self.beamsList.append(obj)
if type is not None:
self.setAll(type)
[docs]
def getByNumber(self, number):
'''
Gets an internal beam object by number.
>>> a = beam.Beams()
>>> a.fill('16th')
>>> a.setAll('start')
>>> a.getByNumber(2).type
'start'
>>> a.getByNumber(30)
Traceback (most recent call last):
IndexError: beam number 30 cannot be accessed
'''
if number not in self.getNumbers():
raise IndexError(f'beam number {number} cannot be accessed')
for beam in self.beamsList:
if beam.number == number:
return beam
[docs]
def getNumbers(self):
'''
Returns a list of all defined beam numbers; it should normally be a set
of consecutive integers, but it might not be.
>>> a = beam.Beams()
>>> a.fill('32nd')
>>> a.getNumbers()
[1, 2, 3]
'''
return [x.number for x in self.beamsList]
[docs]
def getTypeByNumber(self, number):
'''
Get beam type, with direction, by number.
>>> a = beam.Beams()
>>> a.fill('16th')
>>> a.setAll('start')
>>> a.setByNumber(2, 'partial-right')
>>> a.getTypeByNumber(2)
'partial-right'
>>> a.getTypeByNumber(1)
'start'
'''
beamObj = self.getByNumber(number)
if beamObj.direction is None:
return beamObj.type
else:
return f'{beamObj.type}-{beamObj.direction}'
[docs]
def getTypes(self):
'''
Returns a list of all beam types defined for the current beams.
>>> a = beam.Beams()
>>> a.fill('16th')
>>> a.setAll('start')
>>> a.getTypes()
['start', 'start']
'''
return [x.type for x in self.beamsList]
[docs]
def setAll(self, type, direction=None): # type is okay @ReservedAssignment
'''
`setAll` is a method of convenience that sets the type
of each of the beam objects within the beamsList to the specified type.
It also takes an optional "direction" attribute that sets the direction
for each beam (otherwise the direction of each beam is set to None).
Acceptable directions (start, stop, continue, etc.) are listed under
Beam() above.
>>> a = beam.Beams()
>>> a.fill('16th')
>>> a.setAll('start')
>>> a.getTypes()
['start', 'start']
>>> a.setAll('sexy')
Traceback (most recent call last):
music21.beam.BeamException: beam type cannot be sexy
'''
if type not in ('start', 'stop', 'continue', 'partial'):
raise BeamException(f'beam type cannot be {type}')
for beam in self.beamsList:
beam.type = type
beam.direction = direction
[docs]
def setByNumber(self, number, type, direction=None): # type is okay @ReservedAssignment
'''
Set an internal beam object by number, or rhythmic symbol level.
>>> a = beam.Beams()
>>> a.fill('16th')
>>> a.setAll('start')
>>> a.setByNumber(1, 'continue')
>>> a.beamsList[0].type
'continue'
>>> a.setByNumber(2, 'stop')
>>> a.beamsList[1].type
'stop'
>>> a.setByNumber(2, 'partial-right')
>>> a.beamsList[1].type
'partial'
>>> a.beamsList[1].direction
'right'
>>> a.setByNumber(30, 'stop')
Traceback (most recent call last):
IndexError: beam number 30 cannot be accessed
>>> a.setByNumber(2, 'crazy')
Traceback (most recent call last):
music21.beam.BeamException: beam type cannot be crazy
'''
# permit providing one argument hyphenated
if '-' in type:
type, direction = type.split('-') # type is okay @ReservedAssignment
if type not in ['start', 'stop', 'continue', 'partial']:
raise BeamException(f'beam type cannot be {type}')
if number not in self.getNumbers():
raise IndexError(f'beam number {number} cannot be accessed')
for beam in self.beamsList:
if beam.number == number:
beam.type = type
beam.direction = direction
# -----------------------------------------------------------------------------
class Test(unittest.TestCase):
def testCopyAndDeepcopy(self):
from music21.test.commonTest import testCopyAll
testCopyAll(self, globals())
# -----------------------------------------------------------------------------
# define presented order in documentation
_DOC_ORDER = [Beams, Beam]
if __name__ == '__main__':
import music21
music21.mainTest(Test)