# ------------------------------------------------------------------------------
# Name: repeat.py
# Purpose: Base classes for processing repeats
#
# Authors: Christopher Ariza
# Daniel Manesh
# Michael Scott Asato Cuthbert
#
# Copyright: Copyright © 2011-2012, 16, 19 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
'''
This module provides the base class for all RepeatMark objects: entities that denote repeats.
Some RepeatMark objects are Expression objects; others are Bar objects. See for instance,
the :class:`~music21.bar.Repeat` which represents a normal barline repeat.
'''
from __future__ import annotations
import copy
import string
import typing as t
from music21 import environment
from music21 import exceptions21
from music21 import expressions
from music21 import prebase
from music21 import spanner
from music21 import style
if t.TYPE_CHECKING:
from music21 import stream
environLocal = environment.Environment('repeat')
# ------------------------------------------------------------------------------
[docs]
class RepeatMark(prebase.ProtoM21Object):
'''
Base class of all repeat objects, including RepeatExpression objects and
Repeat (Barline) objects.
This object is used for multiple-inheritance of such objects and to filter by class in order
to get all things that mark repeats.
The RepeatMark is not itself a :class:`~music21.base.Music21Object` so you should use multiple
inheritance to put these things in Streams.
The following demonstration shows how a user might see if a Stream has any repeats in it.
>>> class PartialRepeat(repeat.RepeatMark, base.Music21Object):
... def __init__(self, **keywords):
... super().__init__(**keywords)
>>> s = stream.Stream()
>>> s.append(note.Note())
>>> s.append(PartialRepeat())
>>> repeats = s.getElementsByClass('RepeatMark') # not a Music21Object, so use quotes
>>> if repeats:
... print(f'Stream has {len(repeats)} repeat(s) in it')
Stream has 1 repeat(s) in it
'''
# ------------------------------------------------------------------------------
class RepeatExpressionException(exceptions21.Music21Exception):
pass
[docs]
class RepeatExpression(RepeatMark, expressions.Expression):
'''
This class models any mark added to a Score to mark
repeat start and end points that are designated by
text expressions or symbols, such as D.S. Al Coda, etc.
N.B. Repeat(Barline) objects are not RepeatExpression objects,
but both are RepeatMark subclasses.
This class stores internally a
:class:`~music21.expressions.TextExpression`. This object
is used for rendering text output in translation. A
properly configured TextExpression object can also be
used to create an instance of a RepeatExpressions.
'''
_styleClass = style.TextStyle
def __init__(self, **keywords):
super().__init__(**keywords)
# store a text version of this expression
self._textExpression = None
# store a list of alternative text representations
self._textAlternatives = []
# store a default text justification
self.style.justify = 'center'
# for those that have symbols, declare if the symbol is to be used
self.useSymbol = False
# for musicxml compatibility
self.style.absoluteY = 20 # two staff lines above
def _reprInternal(self):
content = self.getText()
if content is not None and len(content) > 16:
return repr(content[:16] + '...')
elif content is not None:
return repr(content)
else:
return ''
[docs]
def getText(self):
'''
Get the text used for this expression.
'''
return self._textExpression.content
[docs]
def setText(self, value):
'''
Set the text of this repeat expression.
This is also the primary way that the stored TextExpression object is created.
'''
if self._textExpression is None:
self._textExpression = expressions.TextExpression(value)
self._textExpression.style = self.style # link style
self.applyTextFormatting()
else:
self._textExpression.content = value
[docs]
def applyTextFormatting(self, te=None):
'''
Apply the default text formatting to the text expression version of this repeat.
'''
if te is None: # use the stored version if possible
te = self._textExpression
if te is not None:
te.style.justify = self.style.justify
return te
[docs]
def setTextExpression(self, value):
'''
Directly set a TextExpression object.
'''
if not isinstance(value, expressions.TextExpression):
raise RepeatExpressionException(
f'must set with a TextExpression object, not: {value}')
self._textExpression = value
self.applyTextFormatting()
[docs]
def getTextExpression(self):
'''
Return a copy of the TextExpression stored in this object.
'''
if self._textExpression is None:
return None
else:
return copy.deepcopy(self._textExpression)
[docs]
def isValidText(self, value):
'''
Return True or False if the supplied text could be used for this RepeatExpression.
'''
def stripText(s):
# remove all spaces, punctuation, and make lower
s = s.strip()
s = s.replace(' ', '')
s = s.replace('.', '')
s = s.lower()
return s
for candidate in self._textAlternatives:
candidate = stripText(candidate)
value = stripText(value)
if value == candidate:
return True
return False
[docs]
class RepeatExpressionMarker(RepeatExpression):
'''
Some repeat expressions are markers of positions
in the score to jump to; these classes model those markers,
such as Coda, Segno, and Fine, which are subclassed below.
'''
def __init__(self, **keywords):
super().__init__(**keywords)
# these are generally centered
self.style.justify = 'center'
[docs]
class Coda(RepeatExpressionMarker):
'''
The coda symbol, or the word coda, as placed in a score.
>>> rm = repeat.Coda()
'''
# note that only Coda and Segno have non-text expression forms
def __init__(self, text=None, **keywords):
super().__init__(**keywords)
# default text expression is coda
self.style.justify = 'center'
self._textAlternatives = ['Coda', 'to Coda', 'al Coda']
if text is not None and self.isValidText(text):
self.setText(text)
self.useSymbol = False
else:
self.setText(self._textAlternatives[0])
self.useSymbol = True
[docs]
class Segno(RepeatExpressionMarker):
'''
The segno sign as placed in a score.
>>> rm = repeat.Segno()
>>> rm.useSymbol
True
'''
# note that only Coda and Segno have non-text expression forms
def __init__(self, **keywords):
super().__init__(**keywords)
# default text expression is coda
self._textAlternatives = ['Segno']
self.setText(self._textAlternatives[0])
self.useSymbol = True
[docs]
class Fine(RepeatExpressionMarker):
'''
The fine word as placed in a score.
>>> rm = repeat.Fine()
'''
def __init__(self, **keywords):
super().__init__(**keywords)
# default text expression is coda
self._textAlternatives = ['fine']
self.setText(self._textAlternatives[0])
self.style.justify = 'right'
[docs]
class RepeatExpressionCommand(RepeatExpression):
'''
Some repeat expressions are commands, instructing
the reader to go somewhere else. DaCapo and
related are examples.
'''
def __init__(self, **keywords):
super().__init__(**keywords)
# whether any internal repeats encountered within a jumped region are also repeated.
self.repeatAfterJump = False
# generally these should be right aligned, as they are placed
# at the end of the measure they alter
self.style.justify = 'right'
[docs]
class DaCapo(RepeatExpressionCommand):
'''
The Da Capo command, indicating a return to the beginning
and a continuation to the end. By default,
`repeatAfterJump` is False, indicating that any repeats
encountered on the Da Capo repeat not be repeated.
'''
def __init__(self, text=None, **keywords):
super().__init__(**keywords)
# default text expression is coda
self._textAlternatives = ['Da Capo', 'D.C.']
if text is not None and self.isValidText(text):
self.setText(text)
else:
self.setText(self._textAlternatives[0])
[docs]
class DaCapoAlFine(RepeatExpressionCommand):
'''
The Da Capo al Fine command, indicating a return to
the beginning and a continuation to the
:class:`~music21.repeat.Fine` object. By default,
`repeatAfterJump` is False, indicating that any
repeats encountered on the Da Capo repeat not
be repeated.
>>> rm = repeat.DaCapoAlFine()
'''
def __init__(self, text=None, **keywords):
super().__init__(**keywords)
# default text expression is coda
self._textAlternatives = ['Da Capo al fine', 'D.C. al fine']
if text is not None and self.isValidText(text):
self.setText(text)
else:
self.setText(self._textAlternatives[0])
[docs]
class DaCapoAlCoda(RepeatExpressionCommand):
'''
The Da Capo al Coda command, indicating a return
to the beginning and a continuation to the
:class:`~music21.repeat.Coda` object. The music
resumes at a second :class:`~music21.repeat.Coda`
object. By default, `repeatAfterJump` is False,
indicating that any repeats encountered on the
Da Capo repeat not be repeated.
>>> rm = repeat.DaCapoAlCoda()
'''
def __init__(self, text=None, **keywords):
super().__init__(**keywords)
self._textAlternatives = ['Da Capo al Coda', 'D.C. al Coda']
if text is not None and self.isValidText(text):
self.setText(text)
else:
self.setText(self._textAlternatives[0])
[docs]
class AlSegno(RepeatExpressionCommand):
'''
Jump to the sign. Presumably a forward jump, not a repeat.
>>> rm = repeat.AlSegno()
'''
def __init__(self, text=None, **keywords):
super().__init__(**keywords)
self._textAlternatives = ['al Segno']
if text is not None and self.isValidText(text):
self.setText(text)
else:
self.setText(self._textAlternatives[0])
[docs]
class DalSegno(RepeatExpressionCommand):
'''
The Dal Segno command, indicating a return to the segno
and a continuation to the end. By default, `repeatAfterJump`
is False, indicating that any repeats encountered on
the Da Capo repeat not be repeated.
>>> rm = repeat.DaCapoAlFine()
'''
def __init__(self, text=None, **keywords):
super().__init__(**keywords)
self._textAlternatives = ['Dal Segno', 'D.S.']
if text is not None and self.isValidText(text):
self.setText(text)
else:
self.setText(self._textAlternatives[0])
[docs]
class DalSegnoAlFine(RepeatExpressionCommand):
'''
The Dal Segno al Fine command, indicating a return to the
segno and a continuation to the :class:`~music21.repeat.Fine`
object. By default, `repeatAfterJump` is False, indicating
that any repeats encountered on the Dal Segno repeat not
be repeated.
>>> rm = repeat.DaCapoAlFine()
'''
def __init__(self, text=None, **keywords):
super().__init__(**keywords)
self._textAlternatives = ['Dal Segno al fine', 'D.S. al fine']
if text is not None and self.isValidText(text):
self.setText(text)
else:
self.setText(self._textAlternatives[0])
[docs]
class DalSegnoAlCoda(RepeatExpressionCommand):
'''
The Dal Segno al Coda command, indicating a return to the
beginning and a continuation to the :class:`~music21.repeat.Coda`
object. The music resumes at a second
:class:`~music21.repeat.Coda` object. By default,
`repeatAfterJump` is False, indicating that any repeats encountered
on the Da Segno repeat not be repeated.
>>> rm = repeat.DaCapoAlCoda()
'''
def __init__(self, text=None, **keywords):
super().__init__(**keywords)
self._textAlternatives = ['Dal Segno al Coda', 'D.S. al Coda']
if text is not None and self.isValidText(text):
self.setText(text)
else:
self.setText(self._textAlternatives[0])
# ------------------------------------------------------------------------------
# store a list of one each of RepeatExpression objects; these are used for
# testing TextExpressions
# 500 microseconds to run
repeatExpressionReference = [
Coda(), Segno(), Fine(), DaCapo(), DaCapoAlFine(),
DaCapoAlCoda(), AlSegno(), DalSegno(), DalSegnoAlFine(), DalSegnoAlCoda(),
]
# ------------------------------
[docs]
def insertRepeatEnding(s, start, end, endingNumber: int = 1, *, inPlace=False):
'''
Designates a range of measures as being repeated endings (i.e. first and second endings)
within a stream s, where s either contains measures,
or contains parts which contain measures. Start and end are integers
corresponding to the first and last measure
number of the "repeatNum" ending. e.g. if start=6, end=7, and repeatNum=2,
the method adds a second ending
from measures 6 to 7.
Does not (yet) add a :class:`~music21.repeat.RepeatMark` to the end of the first ending.
Example: create first and second endings over measures 4-6 and measures 11-13 of a chorale,
respectively.
>>> c1 = corpus.parse('bwv10.7.mxl')
>>> repeat.insertRepeatEnding(c1, 4, 6, 1, inPlace=True)
>>> repeat.insertRepeatEnding(c1, 11, 13, 2, inPlace=True)
We now have 8 repeatBrackets since each part gets its own first and second ending.
>>> repeatBrackets = c1.flatten().getElementsByClass(spanner.RepeatBracket)
>>> len(repeatBrackets)
8
>>> len(c1.parts.first().getElementsByClass(spanner.RepeatBracket))
2
'''
if not inPlace:
s.coreCopyAsDerivation('insertRepeatEnding')
if s is None:
return None # or raise an exception!
if not s.hasMeasures():
for part in s.parts:
insertRepeatEnding(part, start, end, endingNumber, inPlace=True)
if inPlace:
return
else:
return s
measures = [s.measure(i) for i in range(start, end + 1)]
rb = spanner.RepeatBracket(measures, number=endingNumber)
# adding repeat bracket to stream at beginning of repeated section.
# Maybe better at end?
rbOffset = measures[0].getOffsetBySite(s)
s.insert(rbOffset, rb)
if inPlace is True:
return
else:
return s
[docs]
def insertRepeat(s, start, end, *, inPlace=False):
# noinspection PyShadowingNames
'''
Given a stream s, inserts a start-repeat at the beginning of the
bar specified by start and inserts an end-repeat at the bar specified
by barEnd. Only alters the stream s if inPlace=True.
>>> from copy import deepcopy
>>> chorale1 = corpus.parse('bwv10.7.mxl')
>>> s = repeat.insertRepeat(chorale1, 3, 6, inPlace=False)
>>> m4 = search.translateStreamToString( chorale1.parts[1].measure(4).notesAndRests)
>>> resm4 = search.translateStreamToString( s.parts[1].measure(4).notesAndRests)
>>> m6 = search.translateStreamToString( chorale1.parts[1].measure(4).notesAndRests)
>>> resm6 = search.translateStreamToString( s.parts[1].measure(4).notesAndRests)
>>> m7 = search.translateStreamToString( chorale1.parts[1].measure(4).notesAndRests)
>>> resm7 = search.translateStreamToString( s.parts[1].measure(4).notesAndRests)
>>> m4 == resm4
True
>>> m6 == resm6
True
>>> m7 == resm7
True
We should have 2 repeats in each part (a start and end) for a total of 8 repeats
>>> len(s.parts[0].flatten().getElementsByClass(bar.Repeat))
2
>>> len(s[bar.Repeat])
8
>>> s.parts[0].measure(3).leftBarline.direction
'start'
>>> s.parts[0].measure(6).rightBarline.direction
'end'
'''
if s is None:
return None
if not inPlace:
s.coreCopyAsDerivation('insertRepeat')
if not s.hasMeasures():
for part in s.parts:
insertRepeat(part, start, end, inPlace=True)
if inPlace:
return
else:
return s
from music21 import bar
s.measure(end).rightBarline = bar.Repeat(direction='end', times=2)
# Place a starting repeat, if needed
if start != 1 or RepeatFinder(s).getQuarterLengthOfPickupMeasure() != 0:
s.measure(start).leftBarline = bar.Repeat(direction='start')
if inPlace:
return
else:
return s
[docs]
def deleteMeasures(s, toDelete, *, inPlace=False, correctMeasureNumbers=True):
# noinspection PyShadowingNames
'''
Given a Stream `s` and a list of numbers, toDelete, removes each measure
with a number corresponding to a number in toDelete and then renumbers
the remaining measures in the Stream.
TODO: Move to someplace more appropriate.
>>> from copy import deepcopy
>>> chorale1 = corpus.parse('bwv10.7.mxl')
>>> s = deepcopy(chorale1)
>>> repeat.deleteMeasures(s, [6, 3, 4], inPlace=True)
>>> m2 = search.translateStreamToString(chorale1.parts[1].measure(2).notesAndRests)
>>> resm2 = search.translateStreamToString(s.parts[1].measure(2).notesAndRests)
>>> m2 == resm2
True
>>> m5 = search.translateStreamToString(chorale1.parts[1].measure(5).notesAndRests)
>>> resm3 = search.translateStreamToString(s.parts[1].measure(3).notesAndRests)
>>> m5 == resm3
True
>>> m7 = search.translateStreamToString(chorale1.parts[1].measure(7).notesAndRests)
>>> resm4 = search.translateStreamToString(s.parts[1].measure(4).notesAndRests)
>>> m7 == resm4
True
>>> lenS = len(s.parts[0].getElementsByClass(stream.Measure))
>>> lenChorale1 = len(chorale1.parts[0].getElementsByClass(stream.Measure))
>>> lenS + 3 == lenChorale1
True
OMIT_FROM_DOCS
>>> chorale3 = corpus.parse('bwv102.7.mxl')
>>> s = repeat.deleteMeasures(chorale3, [2, 3])
>>> (len(s.parts[2].getElementsByClass(stream.Measure)) ==
... len(chorale3.parts[2].getElementsByClass(stream.Measure)) - 2)
True
>>> s = repeat.deleteMeasures(chorale3, [2, 3])
>>> (len(s.parts[2].getElementsByClass(stream.Measure)) ==
... len(chorale3.parts[2].getElementsByClass(stream.Measure)) - 2)
True
>>> s = repeat.deleteMeasures(chorale3, [999, 1001001])
>>> len(s.parts[2]) == len(chorale3.parts[2])
True
'''
from music21 import stream
if s is None:
return None
if not inPlace:
s = s.coreCopyAsDerivation('deleteMeasures')
if s.hasMeasures():
for mNumber in toDelete:
try:
removeMe = s.measure(mNumber)
except exceptions21.Music21Exception: # More specific?
removeMe = None
if removeMe is not None:
s.remove(removeMe)
else:
for part in s.parts:
deleteMeasures(part,
toDelete,
inPlace=True,
correctMeasureNumbers=correctMeasureNumbers)
if inPlace:
return None
else:
return s
# correct the measure numbers
if correctMeasureNumbers:
measures = list(s.getElementsByClass(stream.Measure))
if measures:
i = measures[0].number
# if we deleted the first measure. TODO: test this case
if i not in (0, 1):
i = 1 # can simplify to one line with above.
for measure in measures:
measure.number = i
i += 1
if inPlace:
return None
else:
return s
# from musicxml
# DaCapo indicates to go back to the beginning of the movement. When used it always has
# the value "yes".
#
# Segno and DalSegno are used for backwards jumps to a segno sign; coda and ToCoda
# are used for forward jumps to a coda sign.
# By default, a DalSegno or DaCapo attribute indicates that the jump should occur
# the first time through, while a ToCoda attribute indicates the jump should occur
# the second time through. The time that jumps occur can be changed by using
# the time-only attribute.
# finale defines:
# d.c. al fine: da capo al fine: go back to beginning and repeat complete or up to word fine
# d.c. al coda: da capo al coda: repeat from beginning to an indicated place and
# then play the tail part; two coda symbols are used
# d.s. al fine: da segno al fine: go back to segno, and play until fine
# d.s. al coda: go back to segno, then when al coda is reached, jump to coda
# to coda #
# fine
# coda sign: circle symbol with cross
# segno
# go to measure number
class ExpanderException(exceptions21.Music21Exception):
pass
[docs]
class Expander[StreamType: 'music21.stream.Stream']:
'''
The Expander object can expand a single Part or Part-like Stream with repeats. Nested
repeats given with :class:`~music21.bar.Repeat` objects, or
repeats and sections designated with
:class:`~music21.repeat.RepeatExpression` objects, are all expanded.
This class is a utility processor. Direct usage is more commonly
from the :meth:`~music21.stream.Stream.expandRepeats` method.
To use this object directly, call :meth:`~music21.repeat.Expander.process` on the
score
>>> s = converter.parse('tinynotation: 3/4 A2. C4 D E F2.')
>>> s.makeMeasures(inPlace=True)
>>> s.measure(2).leftBarline = bar.Repeat(direction='start')
>>> s.measure(2).rightBarline = bar.Repeat(direction='end', times=3)
>>> s.show('text')
{0.0} <music21.stream.Measure 1 offset=0.0>
{0.0} <music21.clef.BassClef>
{0.0} <music21.meter.TimeSignature 3/4>
{0.0} <music21.note.Note A>
{3.0} <music21.stream.Measure 2 offset=3.0>
{0.0} <music21.bar.Repeat direction=start>
{0.0} <music21.note.Note C>
{1.0} <music21.note.Note D>
{2.0} <music21.note.Note E>
{3.0} <music21.bar.Repeat direction=end times=3>
{6.0} <music21.stream.Measure 3 offset=6.0>
{0.0} <music21.note.Note F>
{3.0} <music21.bar.Barline type=final>
>>> e = repeat.Expander(s)
>>> e.repeatBarsAreCoherent()
True
>>> s2 = e.process()
>>> s2.show('text')
{0.0} <music21.stream.Measure 1 offset=0.0>
{0.0} <music21.clef.BassClef>
{0.0} <music21.meter.TimeSignature 3/4>
{0.0} <music21.note.Note A>
{3.0} <music21.stream.Measure 2 offset=3.0>
{0.0} <music21.bar.Barline type=double>
{0.0} <music21.note.Note C>
{1.0} <music21.note.Note D>
{2.0} <music21.note.Note E>
{3.0} <music21.bar.Barline type=double>
{6.0} <music21.stream.Measure 2a offset=6.0>
{0.0} <music21.bar.Barline type=double>
{0.0} <music21.note.Note C>
{1.0} <music21.note.Note D>
{2.0} <music21.note.Note E>
{3.0} <music21.bar.Barline type=double>
{9.0} <music21.stream.Measure 2b offset=9.0>
{0.0} <music21.bar.Barline type=double>
{0.0} <music21.note.Note C>
{1.0} <music21.note.Note D>
{2.0} <music21.note.Note E>
{3.0} <music21.bar.Barline type=double>
{12.0} <music21.stream.Measure 3 offset=12.0>
{0.0} <music21.note.Note F>
{3.0} <music21.bar.Barline type=final>
* Changed in v9: Expander must be initialized with a Stream object.
OMIT_FROM_DOCS
TODO: Note bug: barline style = double for each!
Clefs and TimesSignatures should only be in first one!
THIS IS IN OMIT
'''
def __init__(self, streamObj: StreamType):
from music21 import stream
self._src: StreamType = streamObj
# get and store the source measure count; this is presumed to
# be a Stream with Measures
self._srcMeasureStream: stream.Stream[stream.Measure] = self._src.getElementsByClass(
stream.Measure
).stream()
# see if there are any repeat brackets
self._repeatBrackets: stream.Stream[spanner.RepeatBracket] = (
self._src.flatten().getElementsByClass(spanner.RepeatBracket).stream()
)
self._srcMeasureCount: int = len(self._srcMeasureStream)
if self._srcMeasureCount == 0:
raise ExpanderException('no measures found in the source stream to be expanded')
# store counts of all non barline elements.
# doing class matching by string as problems matching in some test cases
reStream: stream.Stream[RepeatExpression] = (
self._srcMeasureStream.flatten().getElementsByClass(RepeatExpression).stream()
)
self._codaCount = len(reStream.getElementsByClass(Coda))
self._segnoCount = len(reStream.getElementsByClass(Segno))
self._fineCount = len(reStream.getElementsByClass(Fine))
self._dcCount = len(reStream.getElementsByClass(DaCapo))
self._dcafCount = len(reStream.getElementsByClass(DaCapoAlFine))
self._dcacCount = len(reStream.getElementsByClass(DaCapoAlCoda))
self._asCount = len(reStream.getElementsByClass(AlSegno))
self._dsCount = len(reStream.getElementsByClass(DalSegno))
self._dsafCount = len(reStream.getElementsByClass(DalSegnoAlFine))
self._dsacCount = len(reStream.getElementsByClass(DalSegnoAlCoda))
[docs]
def process(self, deepcopy: bool = True) -> StreamType:
'''
This is the main call for Expander.
Processes all repeats. Note that this processing only
happens for Measures contained in the given Stream.
Other objects in that Stream are neither processed nor copied.
If deepcopy is False then it will leave the stream in an unusual state, but acceptable if
the source stream has already been deep-copied and will be discarded later.
'''
canExpand = self.isExpandable()
if canExpand is False:
raise ExpanderException(
'cannot expand Stream: badly formed repeats or repeat expressions')
# need to copy source measures, as may later measures before copying
# them, and this can result in orphaned spanners
if deepcopy is not False:
srcStream = self._srcMeasureStream.coreCopyAsDerivation('expandRepeats')
else:
srcStream = self._srcMeasureStream
if canExpand is None:
return t.cast(StreamType, srcStream)
# these must be copied, otherwise we have the original still
self._repeatBrackets = copy.deepcopy(self._repeatBrackets)
# srcStream = self._srcMeasureStream
# after copying, update repeat brackets (as spanners)
for m in srcStream:
# uses the spanner bundle stored on this Stream
self._repeatBrackets.spannerBundle.replaceSpannedElement(
m.derivation.origin, m)
# srcStream = self._srcMeasureStream
# post = copy.deepcopy(self._srcMeasureStream)
hasDaCapoOrSegno = self._daCapoOrSegno()
# will deepcopy
if hasDaCapoOrSegno is None:
post = self._processRecursiveRepeatBars(srcStream)
else: # we have a segno or capo
post = self._processRepeatExpressionAndRepeats(srcStream)
# TODO: need to copy spanners from each sub-group into their newest connects;
# must be done here as more than one connection is made
post.mergeAttributes(srcStream)
return post
[docs]
def measureMap(self, returnType='index'):
'''
Returns a list where for each measure in the expanded stream, the index of the
measure in the original
stream is given. if returnType = 'measureNumber' then the str(measureNumber)
of the original instead of the
index of the original is used -- suffixes are important here for endings etc..
Inefficient, because the entire stream is expanded before making this call,
rather than just seeing
what needs to be expanded and returning that.
>>> s = converter.parse('tinynotation: 3/4 A2. C4 D E F2. G4 a b c2.')
>>> s.makeMeasures(inPlace=True)
>>> s.measure(2).leftBarline = bar.Repeat(direction='start')
>>> s.measure(2).rightBarline = bar.Repeat(direction='end', times=3)
>>> s.measure(4).leftBarline = bar.Repeat(direction='start')
>>> s.measure(4).rightBarline = bar.Repeat(direction='end', times=2)
>>> e = repeat.Expander(s)
>>> e.measureMap()
[0, 1, 1, 1, 2, 3, 3, 4]
>>> e.measureMap(returnType='measureNumber')
['1', '2', '2a', '2b', '3', '4', '4a', '5']
'''
from music21 import stream
measureNumberList = []
measureNumberNoSuffixList = []
post = self.process()
measureContainingStreams = post
for i, m in enumerate(measureContainingStreams.getElementsByClass(stream.Measure)):
measureNumberList.append(m.measureNumberWithSuffix())
measureNumberNoSuffixList.append(m.number)
if returnType == 'measureNumber':
return measureNumberList
measureNumberDict = {}
measureNumberNoSuffixDict = {}
for i, m in enumerate(self._srcMeasureStream):
measureNumberDict[m.measureNumberWithSuffix()] = i
measureNumberNoSuffixDict[m.number] = i
# could be overwritten if the same measureNumber is used multiple times.
indexList = []
for i, measureNumberWithSuffix in enumerate(measureNumberList):
try:
indexList.append(measureNumberDict[measureNumberWithSuffix])
except KeyError:
indexList.append(measureNumberNoSuffixDict[measureNumberNoSuffixList[i]])
return indexList
def _stripRepeatBarlines(self, m, newType='double'):
'''
Given a measure, strip barlines if they are repeats, and
replace with Barlines that are of the same style. Modify in place.
'''
# bar import repeat to make Repeat inherit from RepeatMark
from music21 import bar
lb = m.leftBarline
rb = m.rightBarline
if lb is not None and 'music21.bar.Repeat' in lb.classSet:
# environLocal.printDebug([f'inserting new barline: {newStyle}'])
m.leftBarline = bar.Barline(newType)
if rb is not None and 'music21.bar.Repeat' in rb.classSet:
m.rightBarline = bar.Barline(newType)
def _stripRepeatExpressions(self, streamObj: stream.Stream):
'''
Given a Stream of measures or a Measure, strip all RepeatExpression
objects in place.
'''
from music21 import stream
if not streamObj.hasMeasures():
# it probably is a measure; place in temporary containers
mList = [streamObj]
else:
mList = list(streamObj.getElementsByClass(stream.Measure))
for m in mList:
remove = []
for e in m.getElementsByClass(RepeatExpression):
remove.append(e)
for e in remove:
m.remove(e)
[docs]
def repeatBarsAreCoherent(self):
'''
Check that all repeat bars are paired properly. Returns True or False.
'''
startCount = 0
endCount = 0
countBalance = 0
for m in self._srcMeasureStream:
lb = m.leftBarline
rb = m.rightBarline
if lb is not None and 'music21.bar.Repeat' in lb.classSet:
if lb.direction == 'start':
startCount += 1
countBalance += 1
# ends of repeats may be encountered on left of bar
elif lb.direction == 'end':
if countBalance == 0: # the first repeat found
startCount += 1 # simulate first
countBalance += 1 # simulate first
endCount += 1
countBalance -= 1
if rb is not None and 'music21.bar.Repeat' in rb.classSet:
if rb.direction == 'end':
# if this is the first repeat found, then we
# have an acceptable case where the first repeat is omitted
if countBalance == 0: # the first repeat found
startCount += 1 # simulate first
countBalance += 1 # simulate first
endCount += 1
countBalance -= 1
else:
raise ExpanderException(
f'a right barline is found that cannot be processed: {m}, {rb}'
)
if countBalance not in (0, 1):
environLocal.printDebug([f'Repeats are not balanced: countBalance: {countBalance}'])
return False
if startCount not in (endCount, endCount - 1):
environLocal.printDebug([
'start count not the same as end count: '
f'{startCount} / {endCount}'
])
return False
# environLocal.printDebug([
# 'matched start and end repeat barline count of: '
# f'{startCount}/{endCount}'
# ])
return True
def _daCapoOrSegno(self):
'''
Return a DaCapo object if this piece uses any form of DaCapo; return
a Segno object if this piece uses any form of Segno. Returns None if
incoherent or the piece uses neither.
'''
sumDc = self._dcCount + self._dcafCount + self._dcacCount
# for now, only accepting one segno
sumDs = (self._dsCount
+ self._dsacCount
+ self._dsafCount
+ self._asCount)
# environLocal.printDebug(['_daCapoOrSegno', sumDc, sumDs])
if sumDc == 1 and sumDs == 0:
return DaCapo
elif sumDs == 1 and sumDc == 0:
return Segno
else:
return None
def _getRepeatExpressionCommandType(self):
'''
Return the class of the repeat expression command. This should
only be called if it has been determined that there is one
repeat command in this Stream.
'''
if self._dcCount == 1:
return 'DaCapo'
elif self._dcacCount == 1:
return 'DaCapoAlCoda'
elif self._dcafCount == 1:
return 'DaCapoAlFine'
elif self._asCount == 1:
return 'AlSegno'
elif self._dsCount == 1:
return 'DalSegno'
elif self._dsacCount == 1:
return 'DalSegnoAlCoda'
elif self._dsafCount == 1:
return 'DalSegnoAlFine'
else:
raise ExpanderException('no repeat command found')
def _getRepeatExpressionCommand(self, streamObj):
'''
Get the instance found in this stream; assumes that there is one.
'''
return streamObj.flatten().getElementsByClass(RepeatExpressionCommand).first()
def _daCapoIsCoherent(self):
'''
Check if a DC statement is coherent.
'''
# there can be only one da capo statement for the provided span
sumDc = self._dcCount + self._dcafCount + self._dcacCount
if sumDc > 1:
return False
# if dc, there can be no codas
if self._dcCount == 1 and self._codaCount == 0:
# environLocal.printDebug(['returning true on dc'])
return True
# if we have a da capo al fine, must have one fine
elif self._dcafCount == 1 and self._fineCount == 1:
# environLocal.printDebug(['returning true on dcaf'])
return True
# if we have a da capo al coda, must have two coda signs
elif self._dcacCount == 1 and self._codaCount == 2:
# environLocal.printDebug(['returning true on dcac'])
return True
# return false for all other cases
return False
def _dalSegnoIsCoherent(self):
'''
Check if a dal segno statement is coherent.
'''
# there can be only one da segno statement for the provided span
sumDs = (self._asCount
+ self._dsCount
+ self._dsacCount
+ self._dsafCount)
if sumDs > 1:
return False
# if al segno, there can be no codas, and one segno
if (self._asCount == 1
and self._segnoCount == 1
and self._codaCount == 0):
# environLocal.printDebug(['returning true on as'])
return True
if (self._dsCount == 1
and self._segnoCount == 1
and self._codaCount == 0):
# environLocal.printDebug(['returning true on ds'])
return True
# if we have a da capo al fine, must have one fine
elif (self._dsafCount == 1
and self._codaCount == 0
and self._segnoCount == 1
and self._fineCount == 1):
# environLocal.printDebug(['returning true on dsaf'])
return True
# if we have a da capo al coda, must have two coda signs
elif (self._dsacCount == 1
and self._codaCount == 2
and self._segnoCount == 1
and self._fineCount == 0):
# environLocal.printDebug(['returning true on dsac'])
return True
# return false for all other cases
return False
def _groupRepeatBracketIndices(self, streamObj):
# noinspection PyShadowingNames
'''
Return a list of dictionaries that contains two
entries: one for all indices that are involved with
a collection of repeat bracket, and the repeat brackets
themselves.
This is used to handle when there are more than
one group of repeat brackets per Stream.
>>> s = converter.parse('tinynotation: 3/4 A2. C4 D E F2. G4 a b c2.')
>>> s.makeMeasures(inPlace=True)
>>> s.measure(2).leftBarline = bar.Repeat(direction='start')
>>> s.measure(2).rightBarline = bar.Repeat(direction='end', times=3)
>>> rb = spanner.RepeatBracket(s.measure(2))
>>> s.insert(0, rb)
>>> s.measure(4).leftBarline = bar.Repeat(direction='start')
>>> s.measure(4).rightBarline = bar.Repeat(direction='end', times=2)
>>> e = repeat.Expander(s)
>>> from pprint import pprint as pp
>>> pp(e._groupRepeatBracketIndices(s))
[{'measureIndices': [2],
'repeatBrackets': [<music21.spanner.RepeatBracket
<music21.stream.Measure 2 offset=3.0>>]}]
'''
groups = []
mEnumerated = list(enumerate(streamObj))
# use known self._repeatBrackets and correlate with indices
# store numbers to find when we have a new group
foundRBNumbers = []
groupIndices = {
'repeatBrackets': [],
'measureIndices': [],
}
i = 0
# for i in range(len(streamObj)):
while i < len(streamObj):
m = streamObj[i]
# shiftedIndex = False
for rb in self._repeatBrackets:
# environLocal.printDebug(['_groupRepeatBracketIndices', rb])
# match = False
if rb.isFirst(m): # for this rb, is this the first measures
if rb.numberRange[0] in foundRBNumbers:
# we have a new group
groups.append(groupIndices)
foundRBNumbers = []
groupIndices = {'repeatBrackets': [], 'measureIndices': []}
# store rb numbers to monitor when we are in a new group
foundRBNumbers += rb.numberRange # concat list
# groupIndices['measureIndices'].append(i)
# need to jump to the index of the last measure this
# rb contains; need to add indices for measures found within
groupIndices['repeatBrackets'].append(rb)
mLast = rb.getLast()
for iSub, mSub in mEnumerated:
# when we are at or higher index then our
# current context
# need to include
if iSub >= i and id(mSub) != id(mLast):
groupIndices['measureIndices'].append(iSub)
elif id(mLast) == id(mSub): # add last
groupIndices['measureIndices'].append(iSub)
# cannot skip ahead here b/c looking for overlaps
# as error checking
# i = iSub + 1 # go to next index in outer loop
# shiftedIndex = True
# match = True
break
# if not shiftedIndex:
i += 1
if groupIndices:
groups.append(groupIndices)
return groups
def _repeatBracketsAreCoherent(self):
'''
Check if repeat brackets are coherent.
This must be done for each group of brackets, not for the entire Stream.
'''
for group in self._groupRepeatBracketIndices(self._srcMeasureStream):
# get for each group and look at one at a time.
rBrackets = group['repeatBrackets']
# environLocal.printDebug(['_repeatBracketsAreCoherent',
# "group['measureIndices']", group['measureIndices']])
# environLocal.printDebug(['_repeatBracketsAreCoherent',
# "group['repeatBrackets']", group['repeatBrackets']])
# the numbers must be consecutive
if not rBrackets:
return True
# accept if any single repeat bracket
if len(rBrackets) == 1:
pass
elif len(rBrackets) > 1:
# spanner numbers must be in order, integers, and consecutive
target = []
for rb in rBrackets:
# number here may be a string 1,2
# get number list will return inclusive values; i.e.,
# 1,3 will
# return 1, 2, 3
target += rb.numberRange
match = list(range(1, max(target) + 1)) # max of target + 1
if match != target:
environLocal.printDebug([
f'repeat brackets are not numbered consecutively: {match}, {target}'])
return False
# there needs to be a repeat mark after each bracket except the last
spannedMeasureIds = []
for rbCount, rb in enumerate(rBrackets):
# environLocal.printDebug(['rbCount', rbCount, rb])
# get the last, which is a measure, see if it has a repeat
m = rb.getLast()
# check that they do not overlap: look at all components (starts
# and ends) and make sure that none have been found already
for m in rb.getSpannedElements():
if id(m) in spannedMeasureIds:
environLocal.printDebug(['found overlapping repeat brackets'])
return False
spannedMeasureIds.append(id(m))
# check the right bar while iterating
rightBar = m.rightBarline
if rightBar is None or 'Repeat' not in rightBar.classes:
# all but the last must have repeat bars; except if we just
# have one bracket or the last
if (len(rBrackets) == 1
or rbCount < len(rBrackets) - 1):
environLocal.printDebug([
'repeat brackets are not terminated with a repeat barline'
])
return False
return True
def _hasRepeat(self, streamObj: stream.Stream) -> bool:
'''
Return True if this Stream of Measures has a repeat
pair still to process.
'''
# environLocal.printDebug(['hasRepeat', streamObj])
for i in range(len(streamObj)):
m = streamObj[i]
lb = m.leftBarline
rb = m.rightBarline
# this does not check for well-balanced formations,
# only presence
if (lb is not None
and 'music21.bar.Repeat' in lb.classSet):
return True
if (rb is not None
and 'music21.bar.Repeat' in rb.classSet
and rb.direction == 'end'):
return True
return False
[docs]
def findInnermostRepeatIndices(self, streamObj):
'''
Find the innermost repeat bars. Return raw index values.
For a single measure, this could be [2]
For many contiguous measures, this might be [2, 3, 4, 5]
The provided Stream must be a Stream only of Measures.
>>> s = converter.parse('tinynotation: 3/4 A2. C4 D E F2. G4 a b c2.')
>>> s.makeMeasures(inPlace=True)
>>> s.measure(2).leftBarline = bar.Repeat(direction='start')
>>> s.measure(2).rightBarline = bar.Repeat(direction='end', times=3)
>>> s.measure(4).leftBarline = bar.Repeat(direction='start')
>>> s.measure(4).rightBarline = bar.Repeat(direction='end', times=2)
>>> e = repeat.Expander(s)
>>> e.findInnermostRepeatIndices(s)
[1]
>>> s.measure(2).rightBarline = None
>>> s.measure(4).leftBarline = None
>>> e2 = repeat.Expander(s)
>>> e2.findInnermostRepeatIndices(s)
[1, 2, 3]
'''
# need to find only the first open and closed pair
startIndices = []
barRepeatIndices = []
# use index values instead of an iterator
for i in range(len(streamObj)):
# iterate through each measure
m = streamObj[i]
try:
lb = m.leftBarline
rb = m.rightBarline
except AttributeError:
continue # probably not a measure
if lb is not None and 'music21.bar.Repeat' in lb.classSet:
if lb.direction == 'start':
startIndices.append(i)
# an end may be placed on the left barline; of the next measure
# meaning that we only want up until the previous
elif lb.direction == 'end':
# environLocal.printDebug([f'found an end in left barline: {lb}'])
if not startIndices:
# get from first to this one
barRepeatIndices = range(i)
break
else: # otherwise get the last start index
barRepeatIndices = range(startIndices[-1], i)
break
if (rb is not None
and 'music21.bar.Repeat' in rb.classSet
and rb.direction == 'end'):
# if this is the first end found and no starts found,
# assume we are counting from zero
if not startIndices: # get from first to this one
barRepeatIndices = range(i + 1)
break
else: # otherwise get the last start index
barRepeatIndices = range(startIndices[-1], i + 1)
break
return list(barRepeatIndices)
def _getEndRepeatBar(self, streamObj, index):
'''
Get the last measure to be processed in the repeat,
as well as the measure that has the end barline.
These may not be the same: if an end repeat bar is
placed on the left of a measure that is not actually
being copied.
The `index` parameter is the index of the last
measure to be copied. The streamObj must only have Measures.
'''
mLast = streamObj[index]
rb = mLast.rightBarline
# if right barline of end is a repeat
if (rb is not None
and 'music21.bar.Repeat' in rb.classSet
and rb.direction == 'end'):
mEndBarline = mLast # they are the same
repeatTimes = rb.times
else:
# try the next measure
if index >= len(streamObj) - 1:
raise ExpanderException(
f'cannot find an end Repeat bar after the given end: {index}')
mEndBarline = streamObj[index + 1]
lb = mEndBarline.leftBarline
if (lb is not None
and 'music21.bar.Repeat' in lb.classSet
and lb.direction == 'end'):
repeatTimes = lb.times
else:
raise ExpanderException('cannot find an end Repeat bar in the expected position')
# the default is 2 times, or 1 repeat
if repeatTimes is None:
repeatTimes = 2
return mLast, mEndBarline, repeatTimes
[docs]
def processInnermostRepeatBars(self,
streamObj,
repeatIndices=None,
repeatTimes=None,
returnExpansionOnly=False):
'''
Process and return a new Stream of Measures, likely a Part.
If `repeatIndices` are given, only these indices will be copied.
All the included indices must be listed, not just the start and end.
If `returnExpansionOnly` is True, only the expanded portion is
returned, the rest of the Stream is not retained.
>>> s = converter.parse('tinynotation: 3/4 A2. C4 D E F2. G4 a b c2.')
>>> s.makeMeasures(inPlace=True)
>>> s.measure(2).leftBarline = bar.Repeat(direction='start')
>>> s.measure(2).rightBarline = bar.Repeat(direction='end', times=3)
>>> s.measure(4).leftBarline = bar.Repeat(direction='start')
>>> s.measure(4).rightBarline = bar.Repeat(direction='end', times=2)
processInnermostRepeatBars only will expand the first set of repeats.
>>> e = repeat.Expander(s)
>>> s2 = e.processInnermostRepeatBars(s)
>>> s2.show('text')
{0.0} <music21.stream.Measure 1 offset=0.0>
{0.0} <music21.clef.BassClef>
{0.0} <music21.meter.TimeSignature 3/4>
{0.0} <music21.note.Note A>
{3.0} <music21.stream.Measure 2 offset=3.0>
{0.0} <music21.bar.Barline type=double>
{0.0} <music21.note.Note C>
{1.0} <music21.note.Note D>
{2.0} <music21.note.Note E>
{3.0} <music21.bar.Barline type=double>
{6.0} <music21.stream.Measure 2a offset=6.0>
{0.0} <music21.bar.Barline type=double>
{0.0} <music21.note.Note C>
{1.0} <music21.note.Note D>
{2.0} <music21.note.Note E>
{3.0} <music21.bar.Barline type=double>
{9.0} <music21.stream.Measure 2b offset=9.0>
{0.0} <music21.bar.Barline type=double>
{0.0} <music21.note.Note C>
{1.0} <music21.note.Note D>
{2.0} <music21.note.Note E>
{3.0} <music21.bar.Barline type=double>
{12.0} <music21.stream.Measure 3 offset=12.0>
{0.0} <music21.note.Note F>
{15.0} <music21.stream.Measure 4 offset=15.0>
{0.0} <music21.bar.Repeat direction=start>
{0.0} <music21.note.Note G>
{1.0} <music21.note.Note A>
{2.0} <music21.note.Note B>
{3.0} <music21.bar.Repeat direction=end times=2>
{18.0} <music21.stream.Measure 5 offset=18.0>
{0.0} <music21.note.Note C>
{3.0} <music21.bar.Barline type=final>
Calling it again will complete the job, as .process() does
>>> s3 = e.processInnermostRepeatBars(s2)
>>> s3.show('text')
{0.0} <music21.stream.Measure 1 offset=0.0>
...
{3.0} <music21.stream.Measure 2 offset=3.0>
...
{6.0} <music21.stream.Measure 2a offset=6.0>
...
{9.0} <music21.stream.Measure 2b offset=9.0>
...
{12.0} <music21.stream.Measure 3 offset=12.0>
...
{15.0} <music21.stream.Measure 4 offset=15.0>
{0.0} <music21.bar.Barline type=double>
{0.0} <music21.note.Note G>
{1.0} <music21.note.Note A>
{2.0} <music21.note.Note B>
{3.0} <music21.bar.Barline type=double>
{18.0} <music21.stream.Measure 4a offset=18.0>
{0.0} <music21.bar.Barline type=double>
{0.0} <music21.note.Note G>
{1.0} <music21.note.Note A>
{2.0} <music21.note.Note B>
{3.0} <music21.bar.Barline type=double>
{21.0} <music21.stream.Measure 5 offset=21.0>
...
Should work even if no start repeat is given:
>>> s = converter.parse('tinynotation: 3/4 A2. C4 D E F2. G4 a b c2.')
>>> s.makeMeasures(inPlace=True)
>>> s.measure(2).rightBarline = bar.Repeat(direction='end')
>>> e = repeat.Expander(s)
>>> s2 = e.processInnermostRepeatBars(s)
>>> s2.show('text')
{0.0} <music21.stream.Measure 1 offset=0.0>
...
{3.0} <music21.stream.Measure 2 offset=3.0>
...
{6.0} <music21.stream.Measure 1a offset=6.0>
...
{9.0} <music21.stream.Measure 2a offset=9.0>
...
{3.0} <music21.bar.Barline type=double>
{12.0} <music21.stream.Measure 3 offset=12.0>
...
{15.0} <music21.stream.Measure 4 offset=15.0>
...
{18.0} <music21.stream.Measure 5 offset=18.0>
...
{3.0} <music21.bar.Barline type=final>
'''
lowercase_alphabet = string.ascii_lowercase
# get class from src
new = streamObj.__class__()
# can provide indices
forcedIndices = False
if repeatIndices is None:
# find innermost
repeatIndices = self.findInnermostRepeatIndices(streamObj)
else: # use passed
forcedIndices = True
# environLocal.printDebug(['got new repeat indices:', repeatIndices])
# renumber measures starting with the first number found here
# number = streamObj[0].number
# if number is None:
# number = 1
# handling of end repeat as left barline
stripFirstNextMeasure = False
# use index values instead of an iterator
i = 0
repeatTimesFound = 0
while i < len(streamObj):
if not repeatIndices:
break
# environLocal.printDebug(['processing measure index:', i,
# 'repeatIndices', repeatIndices])
# if this index is the start of the repeat
if i == repeatIndices[0]:
mEndBarline = None
mLast = None
try:
mLast, mEndBarline, repeatTimesFound = self._getEndRepeatBar(streamObj,
repeatIndices[-1])
except ExpanderException:
# this may fail if we have supplied arbitrary repeat indices
if not forcedIndices:
raise # raise error
# otherwise let pass; mLast is mEndBarline, as we want
if repeatTimes is None: # if not passed as arg
repeatTimes = repeatTimesFound
for times in range(repeatTimes):
# environLocal.printDebug(['repeat times:', times])
# copy the range of measures; this will include the first
# always copying from the same source
for j in repeatIndices:
mSub = copy.deepcopy(streamObj[j])
# We must do this for each pass, b/c we are not changing
# the source stream
# environLocal.printDebug(['got j, repeatIndices', j, repeatIndices])
if j in [repeatIndices[0], repeatIndices[-1]]:
self._stripRepeatBarlines(mSub)
# mSub.number = number
# only keep repeat expressions found at the end
# only remove anything if we have 2 or more repeats
# and this is not the last time
if repeatTimes >= 2 and times < repeatTimes - 1:
self._stripRepeatExpressions(mSub)
if times != 0:
mSub.numberSuffix = lowercase_alphabet[(times - 1) % 26] # just in case
new.append(mSub)
# renumber at end
# number += 1
# check if we need to clear repeats from next bar
if mLast is not mEndBarline:
stripFirstNextMeasure = True
# set i to next measure after mLast
i = repeatIndices[-1] + 1
# if is not in repeat indices, just add this measure
else:
# iterate through each measure, always add first
if not returnExpansionOnly:
# TODO: this deepcopy is necessary to avoid a problem in
# testExpandRepeatH; the copy is not actually used, but
# for some reason removes an id clash when inserting into
# new
junk = copy.deepcopy(streamObj[i])
# cannot deepcopy here, as we might orphan a spanner
# attached to bracket after this repeat segment
m = streamObj[i]
# environLocal.printDebug(['about to insert m into new', 'id(m)', id(m),
# 'new', id(new), 'all ids:', [id(e) for e in new]])
# m = copy.deepcopy(streamObj[i])
# new.show('t')
if stripFirstNextMeasure:
# environLocal.printDebug(['got stripFirstNextMeasure'])
self._stripRepeatBarlines(m)
# change in source too
self._stripRepeatBarlines(streamObj[i])
stripFirstNextMeasure = False
# renumber at end
# m.number = number
new.append(m) # this may be the first version
# number += 1
i += 1
# return the complete stream with just the expanded measures
return new
def _processInnermostRepeatsAndBrackets(self,
streamObj,
repeatBracketsMemo=None):
'''
Return a new complete Stream with repeats and brackets
expanded.
The `repeatBracketsMemo` is a dictionary that stores
id(rb): rb entries for all RepeatBrackets.
This is not recursively applied here, but done in __processRecursiveRepeatBars.
'''
if repeatBracketsMemo is None:
repeatBracketsMemo = {}
# possible replace calls to above with this
# need to remove groups that are already used
groups = self._groupRepeatBracketIndices(streamObj)
# if we do not group when we are expected to do so, it is probably b/c spanners have
# been orphaned.
# environLocal.printDebug(['got groups:', groups])
if not groups: # none found:
return self.processInnermostRepeatBars(streamObj)
# need to find innermost repeat, and then see if it has any
# repeat brackets that are relevant for the span
# this group may ultimately extend beyond the innermost, as this may
# be the first of a few brackets
innermost = self.findInnermostRepeatIndices(streamObj)
groupFocus = None # find a group to apply to, or None
for group in groups:
if not innermost:
continue
rBrackets = group['repeatBrackets']
mStart = streamObj[innermost[0]]
mEnd = streamObj[innermost[-1]]
for rb in rBrackets:
if id(rb) in repeatBracketsMemo:
# environLocal.printDebug(['skipping rb as in memo keys:', rb])
break # do not need to look at components
elif rb.hasSpannedElement(mStart) or rb.hasSpannedElement(mEnd):
# environLocal.printDebug(['matched rb as component' ])
groupFocus = group
break
else:
pass
# environLocal.printDebug(['rb does not have measure as a spanned element',
# 'rb', rb, 'mEnd', mEnd])
if groupFocus is not None:
break
# if the innermost measures are not part of a group, process normally
if groupFocus is None:
# environLocal.printDebug(['cannot find innermost in a group:',
# 'innermost', innermost, 'groupFocus', groupFocus])
return self.processInnermostRepeatBars(streamObj)
# else: # have innermost in a bracket
rBrackets = groupFocus['repeatBrackets']
# get all measures before bracket
streamObjPre = streamObj[:innermost[0]]
streamBracketRepeats = [] # store a list
# will need to know index of last measure copied
highestIndexRepeated = None
# store for each rb {repeatBracket:[], validIndices=[]}
boundaries = []
# it is critical that the brackets are in order as presented
for rb in rBrackets:
repeatBracketsMemo[id(rb)] = rb
startIndex = innermost[0]
# first measure under spanner is not the start index, but the
# measure that begins the spanned repeat bracket
mFirst = rb.getFirst()
mLast = rb.getLast()
endIndex = None
bracketStartIndex = None # not the startIndex
# iterate over all provided measures to find the last index
for i, m in enumerate(streamObj):
if id(m) == id(mLast):
endIndex = i
# use if: start and end may be the same
if id(m) == id(mFirst):
bracketStartIndex = i
# error check: probably orphaned spanners
if endIndex is None or bracketStartIndex is None:
raise ExpanderException(
'failed to find start or end index of bracket expansion')
# if mLast does not have a repeat bar, it is probably not a repeat
mLastRightBar = mLast.rightBarline
if (mLastRightBar is not None
and 'music21.bar.Repeat' in mLastRightBar.classSet):
indices = list(range(startIndex, endIndex + 1))
# condition of when to repeat next is not always clear
# if we have [1 x :|[2 x | x still need to repeat
else:
indices = list(range(startIndex, endIndex + 1))
# indices = None # mark as not for repeating
# get bracket indices
bracketIndices = list(range(bracketStartIndex, endIndex + 1))
# remove last found bracket indices from next indices to copy
if indices is not None:
# go over all past if bracketIndices and remove
for data in boundaries:
for q in data['bracketIndices']:
if q in indices:
indices.remove(q)
data = {'repeatBracket': rb, 'validIndices': indices,
'bracketIndices': bracketIndices}
boundaries.append(data)
for data in boundaries:
# environLocal.printDebug(['processing data bundle:', data])
# each number in a bracket corresponds to one or more repeat
# find indices to process and times based on repeat brackets
# TODO: need to pass parameter to keep opening repeat until
# are at the last repeat under this bracket
if data['validIndices'] is not None:
# repeat times is the number of elements in the list
repeatTimes = len(data['repeatBracket'].numberRange)
# just get the expanded section
# streamObj.show('t')
out = self.processInnermostRepeatBars(
streamObj,
repeatIndices=data['validIndices'],
repeatTimes=repeatTimes,
returnExpansionOnly=True
)
# environLocal.printDebug(['got bracket segment:',
# [n.name for n in out.flatten().pitches]])
streamBracketRepeats.append(out)
# highest index will always be the last copied, up until end
highestIndexRepeated = max(data['validIndices'])
new = streamObj.__class__()
for m in streamObjPre:
new.append(m)
for sub in streamBracketRepeats:
for m in sub:
self._stripRepeatBarlines(m)
new.append(m)
# need 1 more than the highest measure counted
streamObjPost = streamObj[highestIndexRepeated + 1:]
for m in streamObjPost:
new.append(m)
return new
# raise ExpanderException('condition for inner expansion not caught')
[docs]
def getRepeatExpressionIndex(self, streamObj, target):
# noinspection PyShadowingNames
'''
Return a list of index positions of a Measure given a
stream of measures. This requires the provided stream
to only have measures.
>>> s = converter.parse('tinynotation: 3/4 A2. C4 D E F2.')
>>> s.makeMeasures(inPlace=True)
>>> s.measure(3).append(repeat.Segno())
>>> e = repeat.Expander(s)
getRepeatExpressionIndex returns the measureIndex not measure number
>>> e.getRepeatExpressionIndex(s.getElementsByClass(stream.Measure), 'Segno')
[2]
'''
post = []
for i, m in enumerate(streamObj):
for e in m.getElementsByClass(RepeatExpression):
if (target in e.classes
or (not isinstance(target, str) and isinstance(e, target))):
post.append(i)
# if not found
if post:
return post
return None
[docs]
def isExpandable(self) -> bool|None:
'''
Return True or False if this Stream is expandable, that is,
if it has balanced repeats or sensible Da Capo or Dal Segno
indications.
Return None if there's nothing to expand (a third case).
'''
match = self._daCapoOrSegno()
# if neither repeats nor segno/capo, then not expandable
if match is None and not self._hasRepeat(self._srcMeasureStream):
environLocal.printDebug(
'no dc/segno, no repeats; is expandable but will not do anything'
)
return None
if not self.repeatBarsAreCoherent():
environLocal.printDebug('repeat bars not coherent')
return False
if not self._repeatBracketsAreCoherent():
environLocal.printDebug('repeat brackets are not coherent')
return False
if match is not None:
if match == DaCapo:
if not self._daCapoIsCoherent():
environLocal.printDebug('dc not coherent')
return False
elif match == Segno:
if not self._dalSegnoIsCoherent():
environLocal.printDebug('ds not coherent')
return False
return True
def _processRecursiveRepeatBars(self, streamObj, makeDeepCopy=False):
'''
Recursively expand any number of nested repeat bars.
Will also expand all repeat brackets.
If makeDeepCopy is True, then it will make a deepcopy of the stream. Otherwise
assumes it has already been done.
'''
# This assumes just a stream of measures.
# Also assumes the stream has already been copied unless makeDeepCopy is True.
if makeDeepCopy is True:
streamObj = copy.deepcopy(streamObj)
repeatBracketsMemo = {} # store completed brackets
maxProcesses = 100 # safety check
while maxProcesses > 0:
maxProcesses = maxProcesses - 1
# environLocal.printDebug(['process(): top of loop'])
# post.show('t')
# post = self.processInnermostRepeatBars(post)
streamObj = self._processInnermostRepeatsAndBrackets(
streamObj,
repeatBracketsMemo=repeatBracketsMemo)
# post.show('t')
if self._hasRepeat(streamObj):
pass
# environLocal.printDebug(['process() calling:
# self.findInnermostRepeatIndices(post)',
# self.findInnermostRepeatIndices(post)])
else:
break # nothing left to process
return streamObj
def _processRepeatExpressionAndRepeats(self, streamObj):
'''
Process and return a new Stream of Measures, likely a Part.
Expand any repeat expressions found within.
'''
# Should already be a stream of measures.
# Assumes that the stream has already been copied.
capoOrSegno = self._daCapoOrSegno()
recType = self._getRepeatExpressionCommandType() # a string form
recObj = self._getRepeatExpressionCommand(streamObj)
jumpBack = self.getRepeatExpressionIndex(streamObj, recType)[0]
# start position is dependent on capo or segno
if capoOrSegno is DaCapo:
start = 0
elif capoOrSegno is Segno:
start = self.getRepeatExpressionIndex(streamObj, 'Segno')[0]
else: # pragma: no cover
raise ValueError('Must be DaCapo or Segno')
# this is either fine or the end
if recType in ['DaCapoAlFine', 'DalSegnoAlFine']:
end = self.getRepeatExpressionIndex(streamObj, 'Fine')[0]
else:
end = len(streamObj) - 1
# environLocal.printDebug(['got end/fine:', end])
# coda jump/start
if recType in ['DaCapoAlCoda', 'DalSegnoAlCoda']:
# there must always be two coda symbols; the jump and start
codas = self.getRepeatExpressionIndex(streamObj, 'Coda')
codaJump = codas[0]
codaStart = codas[1]
else:
codaJump = None
codaStart = None
# store segments of measure indices to build
# expand repeats for sections later
indexSegments = []
# get from beginning to jump back
indexSegments.append([0, jumpBack])
# get from post-jump start to
if recType in ['DaCapoAlCoda', 'DalSegnoAlCoda']:
# if there is a coda, then start to coda jump, coda to end
indexSegments.append([start, codaJump])
indexSegments.append([codaStart, len(streamObj) - 1])
else: # no coda (fine or normal end), get start to end
indexSegments.append([start, end])
# process measures
new = streamObj.__class__()
# try to start from first number
number = streamObj[0].number
if number is None:
number = 1
# environLocal.printDebug(['_processRepeatExpressionAndRepeats',
# 'index segments', indexSegments])
# recType.repeatAfterJump
# build segments form the source, copying as necessary, and
# expanding repeats
for subCount, sub in enumerate(indexSegments):
# 1 is the second group, and is always pre coda
subStream = streamObj.__class__()
# get all values inclusive for each range
for i in range(sub[0], sub[1] + 1):
# great a subgroup
m = copy.deepcopy(streamObj[i])
m.number = number
subStream.append(m)
number += 1
if self._hasRepeat(subStream):
# if we are in the jump section, check setting of repeats after
# jump; otherwise, take all repeats
if subCount == 1 and not recObj.repeatAfterJump:
pass
else:
subStream = self._processRecursiveRepeatBars(subStream)
# update measure number from last option
number = subStream[-1].number + 1 # add for next
# no matter what, always strip repeat bars
for m in subStream:
self._stripRepeatBarlines(m, newType='double')
# add sub to new
for m in subStream:
new.append(m)
# can strip all repeat expressions in place
self._stripRepeatExpressions(new)
return new
_DOC_ORDER = ['process', 'measureMap']
# ---------------------------------------------------------
class UnequalPartsLengthException(exceptions21.Music21Exception):
pass
class InsufficientLengthException(exceptions21.Music21Exception):
pass
class NoInternalStreamException(exceptions21.Music21Exception):
pass
# TODO: change terminology for defaultHash. It's not really a hash function.
[docs]
class RepeatFinder:
'''
An object for finding and simplifying repeated sections of music. Must be passed a stream
which contains either measures or parts containing measures.
To collapse a repeated section, call RepeatFinder.simplify() and to see
which sections of a piece
are repeated, call either RepeatFinder.getMeasureSimilarityList() or
RepeatFinder.getSimilarMeasureGroups (see below for full documentation).
If the internal stream passed to RepeatFinder is altered in any way
(e.g. if you call simplify() with inplace=True)
then to ensure proper functionality, you should use a new RepeatFinder object.
Below is an example of calling simplify on a Bach chorale.
>>> chorale = corpus.parse('bwv117.4.mxl')
>>> #_DOCS_SHOW chorale.show()
Only the first 8 bars are displayed below
.. image:: images/repeat-SimplifyExample_Chorale.*
:width: 600
>>> #_DOCS_SHOW repeat.RepeatFinder(chorale).simplify().show()
The same chorale as before, but simplified
.. image:: images/repeat-SimplifyExample_ChoraleSimplified.*
:width: 600
'''
_DOC_ORDER = ['simplify',
'getMeasureSimilarityList',
'getSimilarMeasureGroups',
'getQuarterLengthOfPickupMeasure',
'hasPickup']
_DOC_ATTR: dict[str, str] = {
'defaultHash': r'''A function that takes a stream of notes and rests and
returns a string or an
integer such that two measures are equal if their
hashes are equal under the '==' operator''',
's': 'The internal stream which is being analyzed for repeated sections',
'correctMeasureNumbers': 'boolean whether measure numbers should be corrected'}
def __init__(self, inpStream=None, defaultMeasureHashFunction=None):
from music21 import search
self.s = inpStream
if defaultMeasureHashFunction is None:
self.defaultHash = search.translateStreamToString
else:
self.defaultHash = defaultMeasureHashFunction
self._mList = None
self._mGroups = None
self.correctMeasureNumbers = True
# self.quarterLengthOfPickup = None
# self.hasPickup = None
[docs]
def getQuarterLengthOfPickupMeasure(self):
'''
Looks at RepeatFinder's internal stream and returns the duration of the pickup bar
in quarterLengths. If there is no pickup, returns 0.0.
Raises an exception if RepeatFinder's internal stream is too short
(i.e. fewer than 3 measures long).
>>> noPickup = corpus.parse('bwv10.7.mxl')
>>> repeat.RepeatFinder(noPickup).getQuarterLengthOfPickupMeasure()
0.0
>>> #_DOCS_SHOW noPickup.parts[0].measures(0, 5).show()
.. image:: images/repeat-rf_noPickup.*
:width: 600
>>> hasPickup = corpus.parse('bwv101.7.mxl')
>>> repeat.RepeatFinder(hasPickup).getQuarterLengthOfPickupMeasure()
1.0
>>> #_DOCS_SHOW hasPickup.parts[0].measures(0, 2).show()
.. image:: images/repeat-rf_hasPickup.*
:width: 600
>>> tooShort = noPickup.parts[0].measures(1, 2)
>>> repeat.RepeatFinder(tooShort).getQuarterLengthOfPickupMeasure()
Traceback (most recent call last):
music21.repeat.InsufficientLengthException: Cannot determine length
of pickup given fewer than 3 measures
OMIT_FROM_DOCS
>>> repeat.RepeatFinder().getQuarterLengthOfPickupMeasure()
Traceback (most recent call last):
music21.repeat.NoInternalStreamException:
RepeatFinder must be initialized with a stream
'''
if self.s is None:
raise NoInternalStreamException(
'RepeatFinder must be initialized with a stream'
)
if self.s.hasMeasures():
s2 = self.s
else:
s2 = self.s.parts[0]
mOffsets = list(s2.measureOffsetMap().keys())
if len(mOffsets) < 3:
raise InsufficientLengthException(
'Cannot determine length of pickup given fewer than 3 measures'
)
pickup = mOffsets[1] - mOffsets[0]
normMeasure = mOffsets[2] - mOffsets[1]
return pickup % normMeasure
[docs]
def hasPickup(self):
'''
Returns True when the RepeatFinder's internal stream has a pickup bar.
Raises an exception if the internal stream is too short
(i.e. fewer than 3 bars long).
>>> noPickup = corpus.parse('bwv10.7.mxl')
>>> repeat.RepeatFinder(noPickup).hasPickup()
False
>>> #_DOCS_SHOW noPickup.parts[0].measures(0, 5).show()
.. image:: images/repeat-rf_noPickup.*
:width: 600
>>> hasPickup = corpus.parse('bwv101.7.mxl')
>>> repeat.RepeatFinder(hasPickup).hasPickup()
True
>>> #_DOCS_SHOW hasPickup.parts[0].measures(0, 2).show()
.. image:: images/repeat-rf_hasPickup.*
:width: 600
OMIT_FROM_DOCS
>>> repeat.RepeatFinder(noPickup.parts[0].measures(1, 2)).hasPickup()
Traceback (most recent call last):
music21.repeat.InsufficientLengthException: Cannot determine length of pickup
given fewer than 3 measures
>>> repeat.RepeatFinder().hasPickup()
Traceback (most recent call last):
music21.repeat.NoInternalStreamException: RepeatFinder must be initialized with a stream
'''
if self.s is None:
raise NoInternalStreamException('RepeatFinder must be initialized with a stream')
return self.getQuarterLengthOfPickupMeasure() != 0.0
[docs]
def getMeasureSimilarityList(self):
# noinspection PyShadowingNames
'''
Returns a list mList = [ [2, 3], [3], ... ] such that measures i and j are the
same (with i < j) if and only
if mList[i] contains j. NOTE: this function refers to the very first measure
as measure 0 regardless of whether
s starts with measure 0 or 1 (i.e. treats a pickup bar as an entire measure).
For instance, if getMeasureSimilarityList returned [[4], [5], [6], [7, 8],
[], [], [], [8], []], we would know that the first
four measures repeat and the 4th, 8th, and 9th measures are the same.
Measures are considered the same if the defaultHash maps
them to two values which are
equal under the '==' operator.
>>> chorale = corpus.parse('bwv154.3.mxl')
Expand the repeats:
>>> chorale = repeat.Expander(chorale.parts[0]).process()
Search for similarity:
>>> repeat.RepeatFinder(chorale).getMeasureSimilarityList()
[[4, 12], [5, 13], [6], [7], [12], [13], [], [], [], [], [], [], [], [], [], []]
>>> chorale2 = corpus.parse('bwv153.5.mxl')
>>> chorale2 = repeat.Expander(chorale2.parts[0]).process()
>>> repeat.RepeatFinder(chorale2).getMeasureSimilarityList() # bwv153.5 has a pickup
[[5], [6], [7], [8], [9], [], [], [], [], [], [15], [], [], [], [19], [], [], [], [], []]
>>> hashFunction = lambda m : str(len(m))
>>> repeat.RepeatFinder(chorale.measures(1, 8),
... defaultMeasureHashFunction=hashFunction).getMeasureSimilarityList()
[[1, 2, 4, 5, 6, 8, 10], [2, 4, 5, 6, 8, 10], [4, 5, 6, 8, 10],
[7, 9, 11], [5, 6, 8, 10], [6, 8, 10], [8, 10], [9, 11], [10], [11], [], []]
OMIT_FROM_DOCS
>>> repeat.RepeatFinder().getMeasureSimilarityList()
Traceback (most recent call last):
music21.repeat.NoInternalStreamException: RepeatFinder must be initialized with a stream
'''
from music21 import stream
if self.s is None:
raise NoInternalStreamException('RepeatFinder must be initialized with a stream')
if self._mList is not None:
return self._mList
hashFunction = self.defaultHash
s = self.s
# Check for different parts and change mLists to a list of
# measure-streams: [<measures from part1>, <measures from part2>, ... ]
if s.hasMeasures():
mLists = [s.getElementsByClass(stream.Measure)]
else:
mLists = [p.getElementsByClass(stream.Measure) for p in s.parts]
# Check for unequal lengths
for mThis, mNext in zip(mLists, mLists[1:]):
if len(mThis) != len(mNext):
raise UnequalPartsLengthException(
'Parts must each have the same number of measures.')
# Change mList so each element of mList is a list of hashed measures
# for each measure in a part.
# May look something like [['sd2k1j', 'ej2k', 'r9u3kj'...],
# ['fjk2', '23ijf9', ... ], ... ]
for i in range(len(mLists)):
mLists[i] = [
hashFunction(mLists[i][j].notesAndRests)
for j in range(len(mLists[i]))
]
# mLists is now one list for the whole stream, containing
# a tuple with the hashed measure over each part,
# i.e. mLists = [(part1_measure1_hash, part2_measure1_hash, ...),
# (part1_measure2_hash, part2_measure2_hash, ... ), ... ]
mLists = list(zip(*mLists))
tempDict = {}
# maps the measure-hashes to the lowest examined measure number with that hash.
# initialize res
res = [[] for _ in range(len(mLists))]
for i in range(len(mLists) - 1, -1, -1):
# mHash is the concatenation of the measure i for each part.
mHash = ''.join(mLists[i])
if mHash in tempDict:
# We found a repeated measure
res[i].append(tempDict[mHash])
res[i].extend(res[tempDict[mHash]])
# tempDict now stores the earliest known measure with mHash.
tempDict[mHash] = i
self._mList = res
return res
def _getSimilarMeasuresHelper(self, measures, source, compare, resDict, useDict):
'''
Recursive helper function used by getSimilarMeasureGroupsFromList.
Should only be called if the "source" measure of a piece is the same
as the "compare" measure.
When called, updates resDict such that resDict[(source, compare)] is equal to
( sourceList=[source, source + 1, source + 2, ...],
compareList=[compare, compare + 1, compare + 2, ...]), where
measure sourceList[i] is the same as measure compareList[i]
Inputs::
measures - A list returned from getMeasureSimilarityList.
source - The measure number which you are considering
compare - The measure number which is compared to the source measure
(this is a repeat of the source measure)
resDict - A dictionary of memoized results where for each key (i,j),
there is stored a tuple
([i, i + 1, i + 2, ...], [j, j + 1, j + 2, ...])
where measures i and j are equal,
and measure i + 1 and j + 1 are equal,
and measures i + 2 and j + 2 are equal, etc.
useDict - A dictionary for each input that maps to False if
and only if the function calls
the same input m and i. Has the result that if resDict((i, j)) maps to
something like ([i, i + 1, i + 2...],
[j, j + 1, j + 2, ...]), then
useDict(i,j) is only False if resDict(i - 1, j - 1) is
something like ([i - 1, i, i + 1, ...],
[j - 1, j, j + 1, ..])
See getSimilarMeasureGroupsFromList documentation for tests.
'''
if (source, compare) in resDict:
return # resDict[(source, compare)]
elif compare + 1 in measures[source + 1]:
# we have a repeated section at least 2 measures in length;
# check to see how far it goes
nextOne = self._getSimilarMeasuresHelper(measures, source + 1, compare + 1,
resDict, useDict)
# make sure we don't have overlap
res = ([source], [compare])
res[0].extend(nextOne[0])
res[1].extend(nextOne[1])
if res[0][-1] < res[1][0]:
# If there is no overlap, then resDict[(source + 1, compare + 1)]
# is no longer useful because
# the information is already stored in resDict[(source, compare)]
useDict[(source + 1, compare + 1)] = False
else:
# truncate the result so we don't have overlap.
# (i.e. avoid something like ([1, 2, 3],[2, 3, 4])
while res[0][-1] >= res[1][0]:
res = (res[0][:-1], res[1][:-1])
else:
res = ([source], [compare])
resDict[(source, compare)] = res
useDict[(source, compare)] = True
return res
def _getSimilarMeasureTuples(self, mList, hasPickup=False):
# noinspection PyShadowingNames
'''
Input is a list formatted as the output described in getMeasureSimilarityList().
Output is a list of tuples of the form (l1, l2), where l1 and l2 are
lists of measure numbers such that measure l1[i]
is identical to measure l2[i] and l1[-1] < l2[0]
For all tuples t1 and t2, it is guaranteed that we never
have t1.l1 contains t2.l1 or t2.l2 contains t2.l2
>>> mList = [[5, 6], [7], [8], [9], [11, 12], [6, 13],
... [], [], [], [], [], [12], [], []]
>>> res1 = repeat.RepeatFinder()._getSimilarMeasureTuples(mList, False)
>>> ([1, 2, 3, 4], [7, 8, 9, 10]) in res1
True
>>> ([1], [6]) in res1
True
>>> ([5],[12]) in res1
True
>>> ([5, 6], [13, 14]) in res1
True
>>> ([6],[7]) in res1
True
>>> ([12],[13]) in res1
True
>>> len(res1)
6
>>> mList = [[], [5, 10], [6, 11], [7, 12], [8, 13],
... [10], [11], [12], [13], [], [], [], [], []]
>>> res2 = repeat.RepeatFinder()._getSimilarMeasureTuples(mList, True)
>>> ([1, 2, 3, 4], [5, 6, 7, 8]) in res2
True
>>> ([1, 2, 3, 4], [10, 11, 12, 13]) in res2
True
>>> ([5, 6, 7, 8], [10, 11, 12, 13]) in res2
True
>>> len(res2)
3
>>> s = stream.Stream()
>>> n1 = note.Note('C')
>>> n2 = note.Note('D')
>>> n3 = note.Note('E')
>>> n4 = note.Note('F')
>>> for i in range(3):
... m = stream.Measure()
... s.append(m)
... m.append(n1)
... m.append(n2)
... m.append(n3)
... m.append(n4)
>>> rf = repeat.RepeatFinder()
>>> res3 = rf._getSimilarMeasureTuples([[1, 2], [2], []], False)
>>> ([1], [2]) in res3
True
>>> ([2], [3]) in res3
True
>>> ([1], [3]) in res3
True
>>> len(res3)
3
>>> s = stream.Stream()
>>> n1 = note.Note('C')
>>> n2 = note.Note('D')
>>> n3 = note.Note('E')
>>> n4 = note.Note('F')
>>> for i in range(5):
... m = stream.Measure()
... s.append(m)
... m.append(n1)
... m.append(n2)
... m.append(n3)
... m.append(n4)
>>> rf = repeat.RepeatFinder()
>>> res3 = rf._getSimilarMeasureTuples([[1, 2, 3, 4], [2, 3, 4], [3, 4], [4], []], False)
>>> ([1, 2], [3, 4]) in res3
True
>>> ([1, 2], [4, 5]) in res3
True
>>> ([2, 3], [4, 5]) in res3
True
>>> ([1],[3]) in res3
False
>>> ([1],[4]) in res3
False
>>> ([1],[5]) in res3
True
'''
pickupCorrection = int(not hasPickup)
res = {}
useful = {}
for m in range(len(mList)):
for i in mList[m]:
self._getSimilarMeasuresHelper(mList, m, i, res, useful)
# add correct value to res
for k in list(res): # convert to list so we can alter it
if not useful[k]:
del res[k]
realRes = []
for mTuple in res.values():
appendTup = ([i + pickupCorrection for i in mTuple[0]],
[j + pickupCorrection for j in mTuple[1]])
realRes.append(appendTup)
self._mGroups = realRes
return realRes
[docs]
def simplify(self, repeatThreshold=4, repeatEndingThreshold=3, *, inPlace=False):
# noinspection PyShadowingNames
'''
Takes the piece stored in the RepeatFinder object and collapses repeated sections by
replacing them with repeat signs. Includes first and second endings where appropriate.
Only detects repeated sections which are repeatThreshold bars long and only
detects repeat endings
where the repeated sections is more than repeatEndingThreshold bars long.
If inPlace is True, does not return a new music21 object, but instead alters
the stream passed to
the RepeatFinder object.
In the below example, we have an 8-measure stream where the last four measures
are identical to the
first four.
>>> s = stream.Stream()
>>> notes = [note.Note('D'), note.Note('E-'), note.Note('C'), note.Note('B3'),
... note.Note('D'), note.Note('E-'), note.Note('C'), note.Note('B3')]
>>> for i in range(8):
... m = stream.Measure()
... m.number = i + 1
... myNote = notes[i]
... myNote.duration.quarterLength = 4.0
... m.append(myNote)
... s.append(m)
>>> #_DOCS_SHOW s.show()
.. image:: images/repeat-RepeatFinderDSCH.*
:width: 600
>>> s2 = repeat.RepeatFinder(s).simplify()
>>> #_DOCS_SHOW s2.show()
.. image:: images/repeat-RepeatFinderDSCHsimplified.*
:width: 600
OMIT_FROM_DOCS
>>> c1 = corpus.parse('bwv115.6.mxl') # has a repeated section
>>> c1p0 = repeat.Expander(c1.parts[0]).process()
>>> c1simple = repeat.RepeatFinder(c1p0).simplify()
>>> m4 = search.translateStreamToString(c1p0.measure(3).notesAndRests)
>>> m5 = search.translateStreamToString(c1p0.measure(4).notesAndRests)
>>> m9 = search.translateStreamToString(
... c1p0.getElementsByClass(stream.Measure)[7].notesAndRests)
>>> resm4 = search.translateStreamToString(c1simple.measure(3).notesAndRests)
>>> resm5 = search.translateStreamToString(c1simple.measure(4).notesAndRests)
>>> m4 == resm4
True
>>> m5 == resm5
True
>>> m9 == resm5
True
>>> initialRepeats = c1p0[bar.Repeat]
>>> len(initialRepeats)
0
>>> resRepeats = c1simple[bar.Repeat]
>>> len(resRepeats)
1
>>> s = stream.Stream()
>>> for i in range(1, 6):
... m = stream.Measure()
... s.append(m)
... m.number = i
... m.append(note.Note('D'))
... m.append(note.Note('E-'))
... m.append(note.Note('C'))
... m.append(note.Note('B3'))
>>> s2 = repeat.RepeatFinder(s).simplify(repeatThreshold=2)
>>> len(s2.getElementsByClass(stream.Measure))
3
>>> len(s2.flatten().getElementsByClass(bar.Repeat))
1
'''
mList = self.getMeasureSimilarityList()
mGroups = self._getSimilarMeasureTuples(mList, self.hasPickup())
processed = {}
# Want to give priority first to the longest repeated sections,
# and then to the repeated sections that happen earlier.
# We sort the tuples of mGroups accordingly
# def myComp(x, y):
# if len(x[0]) != len(y[0]):
# return len(y[0])-len(x[0])
# elif x[0][0] != y[0][0]:
# return x[0][0]-y[0][0]
# else:
# return x[1][0] - y[1][0]
mGroups = sorted(mGroups,
key=lambda x: (-1 * len(x[0]), x[0][0], x[1][0])
)
# mGroups = sorted(mGroups, cmp=myComp)
if inPlace:
s = self.s
else:
s = self.s.coreCopyAsDerivation('RepeatFinder.simplify')
if s is None:
raise NoInternalStreamException(
'This function only works when RepeatFinder is initialized with a stream'
)
repeatEndingBars = [] # (measureStart, measureOfFirstEnding, repeatSignMeasure)
toDelete = []
repeatBars = []
for mGroup in mGroups:
# make sure we haven't already processed these measures
alreadyProcessed = False
measureNumbers = mGroup[0] + mGroup[1]
for mNum in measureNumbers:
if mNum in processed:
alreadyProcessed = True
break
if alreadyProcessed:
continue
distance = mGroup[1][0] - mGroup[0][-1] - 1
maxAcceptableDistance = min(16.0, len(mGroup[0]) / 2.0 + 1)
# talk about this line more in documentation
if len(mGroup[0]) >= repeatThreshold and distance == 0:
startBar, endBar = mGroup[0][0], mGroup[0][-1]
repeatBars.append((startBar, endBar))
toDelete.extend(mGroup[1])
elif (len(mGroup[0]) >= repeatEndingThreshold
and maxAcceptableDistance >= distance > 0):
startingBar = mGroup[0][0]
firstEndingBar = mGroup[0][-1] + 1
repeatSignBar = mGroup[1][0] - 1
toProcessTuple = (startingBar, firstEndingBar, repeatSignBar)
repeatEndingBars.append(toProcessTuple)
toDelete.extend(mGroup[1])
else:
continue
# only add the measure numbers to the list of processed measures if
# those measures were actually part of a repeat or repeat ending
for mNum in measureNumbers:
processed[mNum] = True
for startingBar, firstEndingBar, repeatSignBar in repeatEndingBars:
# print(startingBar, firstEndingBar, repeatSignBar)
insertRepeat(s, startingBar, repeatSignBar, inPlace=True)
lengthOfRepeatEnding = repeatSignBar - firstEndingBar + 1
lengthOfRepeatedSection = firstEndingBar - startingBar + 1
startOfSecondEnding = repeatSignBar + lengthOfRepeatedSection
insertRepeatEnding(s,
firstEndingBar,
repeatSignBar,
1,
inPlace=True)
insertRepeatEnding(s,
startOfSecondEnding,
startOfSecondEnding + lengthOfRepeatEnding,
2,
inPlace=True)
for startBar, endBar in repeatBars:
insertRepeat(s, startBar, endBar, inPlace=True)
# might want to look at stream._removeOrExpand and stream._fixMeasureNumbers
deleteMeasures(s,
toDelete,
inPlace=True,
correctMeasureNumbers=self.correctMeasureNumbers)
if not inPlace:
return s
[docs]
def getSimilarMeasureGroups(self, threshold=1):
'''
Returns a list of tuples containing information on repeated groups of measures.
Specifically, returns a list of tuples of the form (l1, l2)
where l1 and l2 are lists
of measure numbers such that measure l1[i] is the same as measure l2[i].
>>> chorale = corpus.parse('bwv117.4.mxl')
Expand repeats
>>> chorale = repeat.Expander(chorale.parts[0]).process()
>>> #_DOCS_SHOW chorale.show()
Measures 0-4 are the same as measures 5-9.
.. image:: images/repeat-SimplifyExample_Chorale.*
:width: 600
>>> repeat.RepeatFinder(chorale).getSimilarMeasureGroups()
[([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), ([1, 2, 3, 4, 5], [6, 7, 8, 9, 10]), ([0], [10])]
Notice that although measures 2-3 are the same as measures 6-7, we
don't have ([2, 3], [6, 7]) in our result, since ([1, 2, 3], [5, 6, 7])
already contains that information.
'''
# see if we've already done this computation
if self._mGroups is None:
if self._mList is None:
mList = self.getMeasureSimilarityList()
else:
mList = self._mList
mGroups = self._getSimilarMeasureTuples(mList, self.hasPickup())
else:
mGroups = self._mGroups
mGroups = [x for x in mGroups if len(x[0]) >= threshold]
# only want long enough measure groups
# sort them giving first priority to larger groups, then to groups that occur earlier
# def aGoodOrder(x, y):
# if len(x[0]) != len(y[0]):
# return len(y[0]) - len(x[0])
# elif x[0][0] != y[0][0]:
# return x[0][0] - y[0][0]
# else:
# return x[1][0] - y[1][0]
# mGroups = sorted(mGroups, cmp=aGoodOrder)
mGroups = sorted(mGroups, key=lambda x: (-1 * len(x[0]), x[0][0], x[1][0]))
return mGroups
# ------------------------------------------------------------------------------
# Tests have been moved to test/test_repeat.py in order to get better import tests.
# define presented order in documentation
_DOC_ORDER = [RepeatExpression, RepeatExpressionMarker, Coda, Segno, Fine,
RepeatExpressionCommand, DaCapo, DaCapoAlFine,
DaCapoAlCoda, AlSegno, DalSegno, DalSegnoAlFine, DalSegnoAlCoda, RepeatFinder]
if __name__ == '__main__':
import music21
music21.mainTest()