Source code for music21.beam

# -----------------------------------------------------------------------------
# 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)