# ------------------------------------------------------------------------------
# Name: romanText/rtObjects.py
# Purpose: music21 objects for processing roman numeral analysis text files
#
# Authors: Christopher Ariza
# Michael Scott Asato Cuthbert
#
# Copyright: Copyright © 2011-2012, 2019 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
'''
Translation routines for roman numeral analysis text files, as defined
and demonstrated by Dmitri Tymoczko, Mark Gotham, Michael Scott Cuthbert,
and Christopher Ariza in ISMIR 2019.
'''
from __future__ import annotations
import fractions
import io
import pathlib
import re
import typing as t
import unittest
from music21 import common
from music21.common.types import OffsetQL
from music21 import exceptions21
from music21 import environment
from music21 import key
from music21 import prebase
if t.TYPE_CHECKING:
from music21 import meter
environLocal = environment.Environment('romanText.rtObjects')
# alternate endings might end with a, b, c for
# nonzero or more for everything after the first number
# letterToNumDict in romanText/translate.py implies that reMeasureTag should
# match a-h (at least), not a-b
reMeasureTag = re.compile(r'm[0-9]+[a-h]*-*[0-9]*[a-h]*')
reVariant = re.compile(r'var[0-9]+')
reVariantLetter = re.compile(r'var([A-Z]+)')
reOptKeyOpenAtom = re.compile(r'\?\([A-Ga-g]+[b#]*:')
reOptKeyCloseAtom = re.compile(r'\?\)[A-Ga-g]+[b#]*:?')
# ?g:( ?
reKeyAtom = re.compile('[A-Ga-g]+[b#]*;:')
reAnalyticKeyAtom = re.compile('[A-Ga-g]+[b#]*:')
reKeySignatureAtom = re.compile(r'KS-?[0-7]')
# must distinguish b3 from bVII; there may be b1.66.5
reBeatAtom = re.compile(r'b[1-9.]+')
reRepeatStartAtom = re.compile(r'\|\|:')
reRepeatStopAtom = re.compile(r':\|\|')
reNoChordAtom = re.compile('(NC|N.C.|nc)')
# ------------------------------------------------------------------------------
class RomanTextException(exceptions21.Music21Exception):
pass
class RTTokenException(exceptions21.Music21Exception):
pass
class RTHandlerException(exceptions21.Music21Exception):
pass
# ------------------------------------------------------------------------------
[docs]
class RTToken(prebase.ProtoM21Object):
'''
Stores each linear, logical entity of a RomanText.
A multi-pass parsing procedure is likely necessary, as RomanText permits
variety of groupings and markings.
>>> rtt = romanText.rtObjects.RTToken('||:')
>>> rtt
<music21.romanText.rtObjects.RTToken '||:'>
A standard RTToken returns `False` for all the following.
>>> rtt.isComposer() or rtt.isTitle() or rtt.isPiece()
False
>>> rtt.isAnalyst() or rtt.isProofreader()
False
>>> rtt.isTimeSignature() or rtt.isKeySignature() or rtt.isNote()
False
>>> rtt.isForm() or rtt.isPedal() or rtt.isMeasure() or rtt.isWork()
False
>>> rtt.isMovement() or rtt.isVersion() or rtt.isAtom()
False
'''
def __init__(self, src: str = ''):
self.src: str = src # store source character sequence
self.lineNumber: int = 0
def _reprInternal(self) -> str:
return repr(self.src)
[docs]
def isComposer(self) -> bool:
return False
[docs]
def isTitle(self) -> bool:
return False
[docs]
def isPiece(self) -> bool:
return False
[docs]
def isAnalyst(self) -> bool:
return False
[docs]
def isProofreader(self) -> bool:
return False
[docs]
def isTimeSignature(self) -> bool:
return False
[docs]
def isKeySignature(self) -> bool:
return False
[docs]
def isNote(self) -> bool:
return False
[docs]
def isMeasure(self) -> bool:
return False
[docs]
def isPedal(self) -> bool:
return False
[docs]
def isWork(self) -> bool:
return False
[docs]
def isMovement(self) -> bool:
return False
[docs]
def isVersion(self) -> bool:
return False
[docs]
def isAtom(self) -> bool:
'''
Atoms are any untagged data; generally only found inside a
measure definition.
'''
return False
[docs]
class RTTagged(RTToken):
'''
In romanText, some data elements are tags, that is a tag name, a colon,
optional whitespace, and data. In non-RTTagged elements, there is just
data.
All tagged tokens are subclasses of this class. Examples are:
Title: Die Jahrzeiten
Composer: Fanny Mendelssohn
>>> rtTag = romanText.rtObjects.RTTagged('Title: Die Jahrzeiten')
>>> rtTag.tag
'Title'
>>> rtTag.data
'Die Jahrzeiten'
>>> rtTag.isTitle()
True
>>> rtTag.isComposer()
False
'''
def __init__(self, src: str = ''):
super().__init__(src)
# try to split off tag from data
self.tag: str = ''
self.data: str = ''
if ':' in src:
iFirst = src.find(':') # first index found at
self.tag = src[:iFirst].strip()
# add one to skip colon
self.data = src[iFirst + 1:].strip()
else: # we do not have a clear tag; perhaps store all as data
self.data = src
[docs]
def isComposer(self) -> bool:
'''
True is the tag represents a composer.
>>> rth = romanText.rtObjects.RTTagged('Composer: Claudio Monteverdi')
>>> rth.isComposer()
True
>>> rth.isTitle()
False
>>> rth.isWork()
False
>>> rth.data
'Claudio Monteverdi'
'''
if self.tag.lower() == 'composer':
return True
return False
[docs]
def isTitle(self) -> bool:
'''
True if tag represents a title, otherwise False.
>>> tag = romanText.rtObjects.RTTagged('Title: This is a title.')
>>> tag.isTitle()
True
>>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
>>> tag.isTitle()
False
'''
if self.tag.lower() == 'title':
return True
return False
[docs]
def isPiece(self) -> bool:
'''
True if tag represents a piece, otherwise False.
>>> tag = romanText.rtObjects.RTTagged('Piece: This is a piece.')
>>> tag.isPiece()
True
>>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
>>> tag.isPiece()
False
'''
if self.tag.lower() == 'piece':
return True
return False
[docs]
def isAnalyst(self) -> bool:
'''
True if tag represents an analyst, otherwise False.
>>> tag = romanText.rtObjects.RTTagged('Analyst: This is an analyst.')
>>> tag.isAnalyst()
True
>>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
>>> tag.isAnalyst()
False
'''
if self.tag.lower() == 'analyst':
return True
return False
[docs]
def isProofreader(self) -> bool:
'''
True if tag represents a proofreader, otherwise False.
>>> tag = romanText.rtObjects.RTTagged('Proofreader: This is a proofreader.')
>>> tag.isProofreader()
True
>>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
>>> tag.isProofreader()
False
'''
if self.tag.lower() in ('proofreader', 'proof reader'):
return True
return False
[docs]
def isTimeSignature(self) -> bool:
'''
True if tag represents a time signature, otherwise False.
>>> tag = romanText.rtObjects.RTTagged('TimeSignature: This is a time signature.')
>>> tag.isTimeSignature()
True
>>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
>>> tag.isTimeSignature()
False
TimeSignature header data can be found intermingled with measures.
'''
if self.tag.lower() in ('timesignature', 'time signature'):
return True
return False
[docs]
def isKeySignature(self) -> bool:
'''
True if tag represents a key signature, otherwise False.
>>> tag = romanText.rtObjects.RTTagged('KeySignature: This is a key signature.')
>>> tag.isKeySignature()
True
>>> tag.data
'This is a key signature.'
KeySignatures are a type of tagged data found outside of measures,
such as "Key Signature: -1" meaning one flat.
Key signatures are generally numbers representing the number of sharps (or
negative for flats). Non-standard key signatures are not supported.
>>> tag = romanText.rtObjects.RTTagged('KeySignature: -3')
>>> tag.data
'-3'
music21 supports one legacy key signature type: `KeySignature: Bb` which
represents a one-flat signature. Important to note: no other key signatures
of this type are supported. (For instance, `KeySignature: Ab` has no effect)
>>> tag = romanText.rtObjects.RTTagged('KeySignature: Bb')
>>> tag.data
'Bb'
Testing that `.isKeySignature` returns `False` for non-key signatures:
>>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
>>> tag.isKeySignature()
False
N.B.: this is not the same as a key definition found inside a
Measure. These are represented by RTKey rtObjects, defined below, and are
not RTTagged rtObjects, but RTAtom subclasses.
'''
if self.tag.lower() in ('keysignature', 'key signature'):
return True
else:
return False
[docs]
def isNote(self) -> bool:
'''
True if tag represents a note, otherwise False.
>>> tag = romanText.rtObjects.RTTagged('Note: This is a note.')
>>> tag.isNote()
True
>>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
>>> tag.isNote()
False
'''
if self.tag.lower() == 'note':
return True
return False
[docs]
def isPedal(self) -> bool:
'''
True if tag represents a pedal, otherwise False.
>>> tag = romanText.rtObjects.RTTagged('Pedal: This is a pedal.')
>>> tag.isPedal()
True
>>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
>>> tag.isPedal()
False
'''
if self.tag.lower() in ['pedal']:
return True
return False
[docs]
def isVersion(self) -> bool:
'''
True if tag defines the version of RomanText standard used,
otherwise False.
Pieces without the tag are defined to conform to RomanText 1.0,
the version described in the ISMIR publication.
>>> rth = romanText.rtObjects.RTTagged('RTVersion: 1.0')
>>> rth.isTitle()
False
>>> rth.isVersion()
True
>>> rth.tag
'RTVersion'
>>> rth.data
'1.0'
'''
if self.tag.lower() == 'rtversion':
return True
else:
return False
[docs]
def isWork(self) -> bool:
'''
True if tag represents a work, otherwise False.
The "work" is not defined as a header tag, but is used to represent
all tags, often placed after Composer, for the work or pieces designation.
>>> rth = romanText.rtObjects.RTTagged('Work: BWV232')
>>> rth.isWork()
True
>>> rth.tag
'Work'
>>> rth.data
'BWV232'
For historical reasons, the tag 'Madrigal' also designates a work.
>>> rth = romanText.rtObjects.RTTagged('Madrigal: 4.12')
>>> rth.isTitle()
False
>>> rth.isWork()
True
>>> rth.tag
'Madrigal'
>>> rth.data
'4.12'
'''
if self.tag.lower() in ('work', 'madrigal'):
return True
else:
return False
[docs]
def isMovement(self) -> bool:
'''
True if tag represents a movement, otherwise False.
>>> tag = romanText.rtObjects.RTTagged('Movement: This is a movement.')
>>> tag.isMovement()
True
>>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
>>> tag.isMovement()
False
'''
if self.tag.lower() == 'movement':
return True
return False
[docs]
def isSixthMinor(self) -> bool:
'''
True if tag represents a configuration setting for setting vi/vio/VI in minor
>>> tag = romanText.rtObjects.RTTagged('Sixth Minor: Flat')
>>> tag.isSixthMinor()
True
>>> tag.data
'Flat'
'''
return self.tag.lower() in ('sixthminor', 'sixth minor') # e.g. 'Sixth Minor: FLAT'
[docs]
def isSeventhMinor(self) -> bool:
'''
True if tag represents a configuration setting for setting vii/viio/VII in minor
>>> tag = romanText.rtObjects.RTTagged('Seventh Minor: Courtesy')
>>> tag.isSeventhMinor()
True
>>> tag.data
'Courtesy'
'''
return self.tag.lower() in ('seventhminor', 'seventh minor')
[docs]
class RTMeasure(RTToken):
'''
In RomanText, measures are given one per line and always start with 'm'.
For instance:
m4 i b3 v b4 VI
m5 b2 g: IV b4 V
m6 i
m7 D: V
Measure ranges can be used and copied, such as:
m8-m9=m4-m5
RTMeasure objects can also define variant readings for a measure:
m1 ii
m1var1 ii b2 ii6 b3 IV
Variants are not part of the tag, but are read into an attribute.
Endings are indicated by a single letter after the measure number, such as
"a" for first ending.
>>> rtm = romanText.rtObjects.RTMeasure('m15a V6 b1.5 V6/5 b2 I b3 viio6')
>>> rtm.data
'V6 b1.5 V6/5 b2 I b3 viio6'
>>> rtm.number
[15]
>>> rtm.repeatLetter
['a']
>>> rtm.isMeasure()
True
'''
def __init__(self, src: str = ''):
super().__init__(src)
# try to split off tag from data
self.tag: str = '' # the measure number or range
self.data: str = '' # only chord, phrase, and similar definitions
self.number: list[int] = [] # one or more measure numbers
self.repeatLetter: list[str] = [] # one or more repeat letters
self.variantNumber: int | None = None # a one-measure or short variant
# a longer-variant that
# defines a different way of reading a large section
self.variantLetter: str | None = None
# store boolean if this measure defines copying another range
self.isCopyDefinition: bool = False
# store processed tokens associated with this measure
self.atoms: list[RTAtom] = []
if src:
self._parseAttributes(src)
def _getMeasureNumberData(self, src: str) -> tuple[list[int], list[str]]:
'''
Return the number of numbers as a list, as well as any repeat
indications.
>>> rtm = romanText.rtObjects.RTMeasure()
>>> rtm._getMeasureNumberData('m77')
([77], [''])
>>> rtm._getMeasureNumberData('m123b-432b')
([123, 432], ['b', 'b'])
'''
# note: this is separate procedure b/c it is used to get copy
# boundaries
if '-' in src: # it is a range
mnStart, mnEnd = src.split('-')
proc = [mnStart, mnEnd]
else:
proc = [src] # treat as one
number = []
repeatLetter = []
for mn in proc:
# append in order, start, end
numStr, alphaStr = common.getNumFromStr(mn)
number.append(int(numStr))
# remove all 'm' in alpha
alphaStr = alphaStr.replace('m', '')
repeatLetter.append(alphaStr)
return number, repeatLetter
def _parseAttributes(self, src: str) -> None:
# assume that we have already checked that this is a measure
g = reMeasureTag.match(src)
if g is None: # not measure tag found
raise RTHandlerException(f'found no measure tag: {src}')
iEnd = g.end() # get end index
rawTag = src[:iEnd].strip()
self.tag = rawTag
rawData = src[iEnd:].strip() # may have variant
# get the number list from the tag
self.number, self.repeatLetter = self._getMeasureNumberData(rawTag)
# strip a variant indication off of rawData if found
g = reVariant.match(rawData)
if g is not None: # there is a variant tag
varStr = g.group(0)
self.variantNumber = int(common.getNumFromStr(varStr)[0])
self.data = rawData[g.end():].strip()
else:
self.data = rawData
g = reVariantLetter.match(rawData)
if g is not None: # there is a variant letter tag
varStr = g.group(1)
self.variantLetter = varStr
self.data = rawData[g.end():].strip()
if self.data.startswith('='):
self.isCopyDefinition = True
def _reprInternal(self) -> str:
if len(self.number) == 1:
numberStr = str(self.number[0])
else:
numberStr = f'{self.number[0]}-{self.number[1]}'
return numberStr
[docs]
def isMeasure(self) -> bool:
return True
[docs]
def getCopyTarget(self) -> tuple[list[int], list[str]]:
'''
If this measure defines a copy operation, return two lists defining
the measures to copy; the second list has the repeat data.
>>> rtm = romanText.rtObjects.RTMeasure('m35-36 = m29-30')
>>> rtm.number
[35, 36]
>>> rtm.getCopyTarget()
([29, 30], ['', ''])
>>> rtm = romanText.rtObjects.RTMeasure('m4 = m1')
>>> rtm.number
[4]
>>> rtm.getCopyTarget()
([1], [''])
'''
# remove equal sign
rawData = self.data.replace('=', '').strip()
return self._getMeasureNumberData(rawData)
[docs]
class RTAtom(RTToken):
'''
In RomanText, definitions of chords, phrases boundaries, open/close
parenthesis, beat indicators, etc. appear within measures (RTMeasure
objects). These individual elements will be called Atoms, as they are data
that is not tagged.
Each atom store a reference to its container (normally an RTMeasure).
>>> chordIV = romanText.rtObjects.RTAtom('IV')
>>> beat4 = romanText.rtObjects.RTAtom('b4')
>>> beat4
<music21.romanText.rtObjects.RTAtom 'b4'>
>>> beat4.isAtom()
True
However, see RTChord, RTBeat, etc. which are subclasses of RTAtom
specifically for storing chords, beats, etc.
'''
def __init__(self, src: str = '', container: RTMeasure | None = None):
# this stores the source
super().__init__(src)
self.container: RTMeasure | None = container
[docs]
def isAtom(self) -> bool:
return True
# for lower level distinctions, use isinstance(), as each type has its own subclass.
[docs]
class RTChord(RTAtom):
r'''
An RTAtom subclass that defines a chord. Also contains a reference to
the container.
>>> chordIV = romanText.rtObjects.RTChord('IV')
>>> chordIV
<music21.romanText.rtObjects.RTChord 'IV'>
'''
def __init__(self, src: str = '', container: RTMeasure | None = None):
super().__init__(src, container)
# store offset within measure
self.offset: OffsetQL | None = None
# store a quarterLength duration
self.quarterLength: OffsetQL | None = None
[docs]
class RTNoChord(RTAtom):
r'''
An RTAtom subclass that defines absence of a chord. Also contains a
reference to the container.
>>> chordNC = romanText.rtObjects.RTNoChord('NC')
>>> chordNC
<music21.romanText.rtObjects.RTNoChord 'NC'>
>>> rth = romanText.rtObjects.RTHandler()
>>> rth.tokenizeAtoms('nc NC N.C.')
[<music21.romanText.rtObjects.RTNoChord 'nc'>,
<music21.romanText.rtObjects.RTNoChord 'NC'>,
<music21.romanText.rtObjects.RTNoChord 'N.C.'>]
'''
def __init__(self, src: str = '', container: RTMeasure | None = None):
super().__init__(src, container)
# store offset within measure
self.offset: OffsetQL | None = None
# store a quarterLength duration
self.quarterLength: OffsetQL | None = None
[docs]
class RTBeat(RTAtom):
r'''
An RTAtom subclass that defines a beat definition. Also contains a
reference to the container.
>>> beatFour = romanText.rtObjects.RTBeat('b4')
>>> beatFour
<music21.romanText.rtObjects.RTBeat 'b4'>
'''
[docs]
def getBeatFloatOrFrac(self) -> OffsetQL:
'''
Gets the beat number as a float or fraction. Time signature independent
>>> RTB = romanText.rtObjects.RTBeat
Simple ones:
>>> RTB('b1').getBeatFloatOrFrac()
1.0
>>> RTB('b2').getBeatFloatOrFrac()
2.0
etc.
with easy float:
>>> RTB('b1.5').getBeatFloatOrFrac()
1.5
>>> RTB('b1.25').getBeatFloatOrFrac()
1.25
with harder:
>>> RTB('b1.33').getBeatFloatOrFrac()
Fraction(4, 3)
>>> RTB('b2.66').getBeatFloatOrFrac()
Fraction(8, 3)
>>> RTB('b1.2').getBeatFloatOrFrac()
Fraction(6, 5)
A third digit of 0.5 adds 1/2 of 1/DENOM of before. Here DENOM is 3 (in 5/3) so
we add 1/6 to 5/3 to get 11/6:
>>> RTB('b1.66').getBeatFloatOrFrac()
Fraction(5, 3)
>>> RTB('b1.66.5').getBeatFloatOrFrac()
Fraction(11, 6)
Similarly 0.25 adds 1/4 of 1/DENOM, to get 21/12 or 7/4 or 1.75:
>>> RTB('b1.66.25').getBeatFloatOrFrac()
1.75
And 0.75 adds 3/4 of 1/DENOM to get 23/12:
>>> RTB('b1.66.75').getBeatFloatOrFrac()
Fraction(23, 12)
A weird way of writing 'b1.5':
>>> RTB('b1.33.5').getBeatFloatOrFrac()
1.5
'''
beatStr = self.src.replace('b', '')
# there may be more than one decimal in the number, such as
# 1.66.5, to show halfway through 2/3rd of a beat
parts = beatStr.split('.')
mainBeat = int(parts[0])
if len(parts) > 1: # 1.66
fracPart = common.addFloatPrecision('.' + parts[1])
else:
fracPart = 0.0
if len(parts) > 2: # 1.66.5
fracPartDivisor = float('.' + parts[2]) # 0.5
if isinstance(fracPart, float):
fracPart = fractions.Fraction.from_float(fracPart)
denom = fracPart.denominator
fracBeatFrac = common.opFrac(1. / (denom / fracPartDivisor))
else:
fracBeatFrac = 0.0
if len(parts) > 3:
environLocal.printDebug([f'got unexpected beat: {self.src}'])
raise RTTokenException(f'cannot handle specification: {self.src}')
beat = common.opFrac(mainBeat + fracPart + fracBeatFrac)
return beat
[docs]
def getOffset(self, timeSignature: meter.TimeSignature) -> OffsetQL:
'''
Given a time signature, return the offset position specified by this
beat.
>>> rtb = romanText.rtObjects.RTBeat('b1.5')
>>> rtb.getOffset(meter.TimeSignature('3/4'))
0.5
>>> rtb.getOffset(meter.TimeSignature('6/8'))
0.75
>>> rtb.getOffset(meter.TimeSignature('2/2'))
1.0
>>> rtb = romanText.rtObjects.RTBeat('b2')
>>> rtb.getOffset(meter.TimeSignature('3/4'))
1.0
>>> rtb.getOffset(meter.TimeSignature('6/8'))
1.5
>>> rtb = romanText.rtObjects.RTBeat('b1.66')
>>> rtb.getOffset(meter.TimeSignature('6/8'))
1.0
>>> rtc = romanText.rtObjects.RTBeat('b1.66.5')
>>> rtc.getOffset(meter.TimeSignature('6/8'))
1.25
'''
beat = self.getBeatFloatOrFrac()
# environLocal.printDebug(['using beat value:', beat])
# TODO: check for exceptions/errors if this beat is bad
try:
post = timeSignature.getOffsetFromBeat(beat)
except exceptions21.TimeSignatureException:
environLocal.printDebug([
'bad beat specification: '
f'{self.src} in a meter of {timeSignature}'
])
post = 0.0
return post
[docs]
class RTKeyTypeAtom(RTAtom):
'''
RTKeyTypeAtoms contain utility functions for all Key-type tokens, i.e.
RTKey, RTAnalyticKey, but not KeySignature.
>>> gMinor = romanText.rtObjects.RTKeyTypeAtom('g;:')
>>> gMinor
<music21.romanText.rtObjects.RTKeyTypeAtom 'g;:'>
>>> gMinor.getKey()
<music21.key.Key of g minor>
'''
footerStrip: str = ';:'
[docs]
def getKey(self) -> key.Key:
'''
This returns a Key, not a KeySignature object
'''
myKey = self.src.rstrip(self.footerStrip)
myKey = key.convertKeyStringToMusic21KeyString(myKey)
return key.Key(myKey)
[docs]
def getKeySignature(self) -> key.KeySignature:
'''
Get a KeySignature object.
'''
myKey = self.getKey()
return key.KeySignature(myKey.sharps)
[docs]
class RTKey(RTKeyTypeAtom):
'''
An RTKey(RTAtom) defines both a change in KeySignature and a change
in the analyzed Key.
They are defined by ";:" after the Key.
>>> gMinor = romanText.rtObjects.RTKey('g;:')
>>> gMinor
<music21.romanText.rtObjects.RTKey 'g;:'>
>>> gMinor.getKey()
<music21.key.Key of g minor>
>>> bMinor = romanText.rtObjects.RTKey('bb;:')
>>> bMinor
<music21.romanText.rtObjects.RTKey 'bb;:'>
>>> bMinor.getKey()
<music21.key.Key of b- minor>
>>> bMinor.getKeySignature()
<music21.key.KeySignature of 5 flats>
>>> eFlatMajor = romanText.rtObjects.RTKey('Eb;:')
>>> eFlatMajor
<music21.romanText.rtObjects.RTKey 'Eb;:'>
>>> eFlatMajor.getKey()
<music21.key.Key of E- major>
'''
footerStrip = ';:'
[docs]
class RTAnalyticKey(RTKeyTypeAtom):
'''
An RTAnalyticKey(RTKeyTypeAtom) only defines a change in the key
being analyzed. It does not in itself create a :class:`~music21.key.Key`
object.
>>> gMinor = romanText.rtObjects.RTAnalyticKey('g:')
>>> gMinor
<music21.romanText.rtObjects.RTAnalyticKey 'g:'>
>>> gMinor.getKey()
<music21.key.Key of g minor>
>>> bMinor = romanText.rtObjects.RTAnalyticKey('bb:')
>>> bMinor
<music21.romanText.rtObjects.RTAnalyticKey 'bb:'>
>>> bMinor.getKey()
<music21.key.Key of b- minor>
'''
footerStrip = ':'
[docs]
class RTKeySignature(RTAtom):
'''
An RTKeySignature(RTAtom) only defines a change in the KeySignature.
It does not in itself create a :class:`~music21.key.Key` object, nor
does it change the analysis taking place.
The number after KS defines the number of sharps (negative for flats).
>>> gMinor = romanText.rtObjects.RTKeySignature('KS-2')
>>> gMinor
<music21.romanText.rtObjects.RTKeySignature 'KS-2'>
>>> gMinor.getKeySignature()
<music21.key.KeySignature of 2 flats>
>>> aMajor = romanText.rtObjects.RTKeySignature('KS3')
>>> aMajor.getKeySignature()
<music21.key.KeySignature of 3 sharps>
'''
[docs]
def getKeySignature(self) -> key.KeySignature:
numSharps = int(self.src[2:])
return key.KeySignature(numSharps)
[docs]
class RTOpenParens(RTAtom):
'''
A simple open parenthesis Atom with a sensible default
>>> romanText.rtObjects.RTOpenParens('(')
<music21.romanText.rtObjects.RTOpenParens '('>
'''
def __init__(self, src: str = '(', container: RTMeasure | None = None):
super().__init__(src, container)
[docs]
class RTCloseParens(RTAtom):
'''
A simple close parenthesis Atom with a sensible default
>>> romanText.rtObjects.RTCloseParens(')')
<music21.romanText.rtObjects.RTCloseParens ')'>
'''
def __init__(self, src: str = ')', container: RTMeasure | None = None):
super().__init__(src, container)
[docs]
class RTOptionalKeyOpen(RTAtom):
'''
Marks the beginning of an optional Key area which does not
affect the roman numeral analysis. (For instance, it is
possible to analyze in Bb major, while remaining in g minor)
>>> possibleKey = romanText.rtObjects.RTOptionalKeyOpen('?(Bb:')
>>> possibleKey
<music21.romanText.rtObjects.RTOptionalKeyOpen '?(Bb:'>
>>> possibleKey.getKey()
<music21.key.Key of B- major>
'''
[docs]
def getKey(self) -> key.Key:
# alter flat symbol
if self.src == '?(b:':
return key.Key('b')
else:
keyStr = self.src.replace('b', '-')
keyStr = keyStr.replace(':', '')
keyStr = keyStr.replace('?', '')
keyStr = keyStr.replace('(', '')
# environLocal.printDebug(['create a key from:', keyStr])
return key.Key(keyStr)
[docs]
class RTOptionalKeyClose(RTAtom):
'''
Marks the end of an optional Key area which does not affect the roman
numeral analysis.
For example, it is possible to analyze in Bb major, while remaining in g
minor.
>>> possibleKey = romanText.rtObjects.RTOptionalKeyClose('?)Bb:')
>>> possibleKey
<music21.romanText.rtObjects.RTOptionalKeyClose '?)Bb:'>
>>> possibleKey.getKey()
<music21.key.Key of B- major>
'''
[docs]
def getKey(self) -> key.Key:
# alter flat symbol
if self.src in ('?)b:', '?)b'):
return key.Key('b')
else:
keyStr = self.src.replace('b', '-')
keyStr = keyStr.replace(':', '')
keyStr = keyStr.replace('?', '')
keyStr = keyStr.replace(')', '')
# environLocal.printDebug(['create a key from:', keyStr])
return key.Key(keyStr)
[docs]
class RTPhraseMarker(RTAtom):
'''
A Phrase Marker:
>>> rtPhraseMarker = romanText.rtObjects.RTPhraseMarker('')
>>> rtPhraseMarker
<music21.romanText.rtObjects.RTPhraseMarker ''>
'''
[docs]
class RTPhraseBoundary(RTPhraseMarker):
'''
>>> phrase = romanText.rtObjects.RTPhraseBoundary('||')
>>> phrase
<music21.romanText.rtObjects.RTPhraseBoundary '||'>
'''
def __init__(self, src: str = '||', container: RTMeasure | None = None):
super().__init__(src, container)
[docs]
class RTEllisonStart(RTPhraseMarker):
'''
>>> phrase = romanText.rtObjects.RTEllisonStart('|*')
>>> phrase
<music21.romanText.rtObjects.RTEllisonStart '|*'>
'''
def __init__(self, src: str = '|*', container: RTMeasure | None = None):
super().__init__(src, container)
[docs]
class RTEllisonStop(RTPhraseMarker):
'''
>>> phrase = romanText.rtObjects.RTEllisonStop('*|')
>>> phrase
<music21.romanText.rtObjects.RTEllisonStop '*|'>
'''
def __init__(self, src: str = '*|', container: RTMeasure | None = None):
super().__init__(src, container)
[docs]
class RTRepeat(RTAtom):
'''
>>> repeat = romanText.rtObjects.RTRepeat('||:')
>>> repeat
<music21.romanText.rtObjects.RTRepeat '||:'>
'''
[docs]
class RTRepeatStart(RTRepeat):
'''
>>> repeat = romanText.rtObjects.RTRepeatStart()
>>> repeat
<music21.romanText.rtObjects.RTRepeatStart '||:'>
'''
def __init__(self, src: str = '||:', container: RTMeasure | None = None):
super().__init__(src, container)
[docs]
class RTRepeatStop(RTRepeat):
'''
>>> repeat = romanText.rtObjects.RTRepeatStop()
>>> repeat
<music21.romanText.rtObjects.RTRepeatStop ':||'>
'''
def __init__(self, src: str = ':||', container: RTMeasure | None = None):
super().__init__(src, container)
# ------------------------------------------------------------------------------
[docs]
class RTHandler:
# divide elements of a character stream into rtObjects and handle
# store in a list, and pass global information to components
def __init__(self) -> None:
# tokens are ABC rtObjects in a linear stream
# tokens are strongly divided between header and body, so can
# divide here
self._tokens: list[RTToken] = []
self.currentLineNumber: int = 0
[docs]
def splitAtHeader(self, lines: list[str]) -> tuple[list[str], list[str]]:
'''
Divide string into header and non-header; this is done before
tokenization.
>>> rth = romanText.rtObjects.RTHandler()
>>> rth.splitAtHeader(['Title: s', 'Time Signature:', '', 'm1 g: i'])
(['Title: s', 'Time Signature:', ''], ['m1 g: i'])
'''
# iterate over lines and find the first measure definition
iStartBody = None
for i, line in enumerate(lines):
if reMeasureTag.match(line.strip()) is not None:
# found a measure definition
iStartBody = i
break
if iStartBody is None:
raise RomanTextException('Cannot find the first measure definition in this file. '
+ 'Dumping contexts: %s', lines)
return lines[:iStartBody], lines[iStartBody:]
[docs]
def tokenizeHeader(self, lines: list[str]) -> list[RTTagged]:
'''
In the header, we only have :class:`~music21.romanText.base.RTTagged`
tokens. We can this process these all as the same class.
'''
post: list[RTTagged] = []
for i, line in enumerate(lines):
line = line.strip()
if line == '':
continue
# wrap each line in a header token
rtt = RTTagged(line)
rtt.lineNumber = i + 1
post.append(rtt)
self.currentLineNumber = len(lines) + 1
return post
[docs]
def tokenizeBody(self, lines: list[str]) -> list[RTToken]:
'''
In the body, we may have measure, time signature, or note
declarations, as well as possible other tagged definitions.
'''
post: list[RTToken] = []
startLineNumber = self.currentLineNumber
for i, line in enumerate(lines):
currentLineNumber = startLineNumber + i
try:
line = line.strip()
if line == '':
continue
# first, see if it is a measure definition, if not, than assume it is tagged data
if reMeasureTag.match(line) is not None:
rtm = RTMeasure(line)
rtm.lineNumber = currentLineNumber
# note: could place these in-line, after post
rtm.atoms = self.tokenizeAtoms(rtm.data, container=rtm)
for a in rtm.atoms:
a.lineNumber = currentLineNumber
post.append(rtm)
else:
# store items in a measure tag outside the measure
rtt = RTTagged(line)
rtt.lineNumber = currentLineNumber
post.append(rtt)
except Exception as exc:
import traceback
tracebackMessage = traceback.format_exc()
raise RTHandlerException(
f'At line {currentLineNumber} ({line}) an exception was raised:\n'
f'{tracebackMessage}'
) from exc
return post
[docs]
def tokenizeAtoms(
self,
line: str,
container: RTMeasure | None = None
) -> list[RTAtom]:
'''
Given a line of data stored in measure consisting only of Atoms,
tokenize and return a list.
>>> rth = romanText.rtObjects.RTHandler()
>>> rth.tokenizeAtoms('IV b3 ii7 b4 ii')
[<music21.romanText.rtObjects.RTChord 'IV'>,
<music21.romanText.rtObjects.RTBeat 'b3'>,
<music21.romanText.rtObjects.RTChord 'ii7'>,
<music21.romanText.rtObjects.RTBeat 'b4'>,
<music21.romanText.rtObjects.RTChord 'ii'>]
>>> rth.tokenizeAtoms('V7 b2 V13 b3 V7 iio6/5[no5]')
[<music21.romanText.rtObjects.RTChord 'V7'>,
<music21.romanText.rtObjects.RTBeat 'b2'>,
<music21.romanText.rtObjects.RTChord 'V13'>,
<music21.romanText.rtObjects.RTBeat 'b3'>,
<music21.romanText.rtObjects.RTChord 'V7'>,
<music21.romanText.rtObjects.RTChord 'iio6/5[no5]'>]
>>> tokenList = rth.tokenizeAtoms('I b2 I b2.25 V/ii b2.5 bVII b2.75 V g: IV')
>>> tokenList
[<music21.romanText.rtObjects.RTChord 'I'>,
<music21.romanText.rtObjects.RTBeat 'b2'>,
<music21.romanText.rtObjects.RTChord 'I'>,
<music21.romanText.rtObjects.RTBeat 'b2.25'>,
<music21.romanText.rtObjects.RTChord 'V/ii'>,
<music21.romanText.rtObjects.RTBeat 'b2.5'>,
<music21.romanText.rtObjects.RTChord 'bVII'>,
<music21.romanText.rtObjects.RTBeat 'b2.75'>,
<music21.romanText.rtObjects.RTChord 'V'>,
<music21.romanText.rtObjects.RTAnalyticKey 'g:'>,
<music21.romanText.rtObjects.RTChord 'IV'>]
>>> tokenList[-2].getKey()
<music21.key.Key of g minor>
>>> rth.tokenizeAtoms('= m3')
[]
>>> tokenList = rth.tokenizeAtoms('g;: ||: V b2 ?(Bb: VII7 b3 III b4 ?)Bb: i :||')
>>> tokenList
[<music21.romanText.rtObjects.RTKey 'g;:'>,
<music21.romanText.rtObjects.RTRepeatStart '||:'>,
<music21.romanText.rtObjects.RTChord 'V'>,
<music21.romanText.rtObjects.RTBeat 'b2'>,
<music21.romanText.rtObjects.RTOptionalKeyOpen '?(Bb:'>,
<music21.romanText.rtObjects.RTChord 'VII7'>,
<music21.romanText.rtObjects.RTBeat 'b3'>,
<music21.romanText.rtObjects.RTChord 'III'>,
<music21.romanText.rtObjects.RTBeat 'b4'>,
<music21.romanText.rtObjects.RTOptionalKeyClose '?)Bb:'>,
<music21.romanText.rtObjects.RTChord 'i'>,
<music21.romanText.rtObjects.RTRepeatStop ':||'>]
'''
post: list[RTAtom] = []
# break by spaces
for word in line.split(' '):
word = word.strip()
if word == '':
continue
elif word == '=':
# if an = is found, this is a copy definition, and no atoms here
break
elif word == '||':
post.append(RTPhraseBoundary(word, container))
elif word == '(':
post.append(RTOpenParens(word, container))
elif word == ')':
post.append(RTCloseParens(word, container))
elif reBeatAtom.match(word) is not None:
post.append(RTBeat(word, container))
# from here, all that is left is keys or chords
elif reOptKeyOpenAtom.match(word) is not None:
post.append(RTOptionalKeyOpen(word, container))
elif reOptKeyCloseAtom.match(word) is not None:
post.append(RTOptionalKeyClose(word, container))
elif reKeyAtom.match(word) is not None:
post.append(RTKey(word, container))
elif reAnalyticKeyAtom.match(word) is not None:
post.append(RTAnalyticKey(word, container))
elif reKeySignatureAtom.match(word) is not None:
post.append(RTKeySignature(word, container))
elif reRepeatStartAtom.match(word) is not None:
post.append(RTRepeatStart(word, container))
elif reRepeatStopAtom.match(word) is not None:
post.append(RTRepeatStop(word, container))
elif reNoChordAtom.match(word) is not None:
post.append(RTNoChord(word, container))
else: # only option is that it is a chord
post.append(RTChord(word, container))
return post
[docs]
def tokenize(self, src: str) -> None:
'''
Walk the RT string, creating RT rtObjects along the way.
'''
# break into lines
lines = src.split('\n')
linesHeader, linesBody = self.splitAtHeader(lines)
# environLocal.printDebug([linesHeader])
self._tokens += self.tokenizeHeader(linesHeader)
self._tokens += self.tokenizeBody(linesBody)
[docs]
def process(self, src: str) -> None:
'''
Given an entire specification as a single source string, strSrc, tokenize it.
This is usually provided in a file.
'''
self._tokens = []
self.tokenize(src)
[docs]
def definesMovements(self, countRequired: int = 2) -> bool:
'''
Return True if more than one movement is defined in a RT file.
>>> rth = romanText.rtObjects.RTHandler()
>>> rth.process('Movement: 1 \\n Movement: 2 \\n \\n m1')
>>> rth.definesMovements()
True
>>> rth.process('Movement: 1 \\n m1')
>>> rth.definesMovements()
False
'''
if not self._tokens:
raise RTHandlerException('must create tokens first')
count = 0
for token in self._tokens:
if token.isMovement():
count += 1
if count >= countRequired:
return True
return False
[docs]
def definesMovement(self) -> bool:
'''
Return True if this handler has 1 or more movement.
>>> rth = romanText.rtObjects.RTHandler()
>>> rth.process('Movement: 1 \\n \\n m1')
>>> rth.definesMovements()
False
>>> rth.definesMovement()
True
'''
return self.definesMovements(countRequired=1)
[docs]
def splitByMovement(self, duplicateHeader: bool = True) -> list[RTHandler]:
# noinspection PyShadowingNames
'''
If we have movements defined, return a list of RTHandler rtObjects,
representing header information and each movement, in order.
>>> rth = romanText.rtObjects.RTHandler()
>>> rth.process('Title: Test \\n Movement: 1 \\n m1 \\n Movement: 2 \\n m1')
>>> post = rth.splitByMovement(False)
>>> len(post)
3
>>> len(post[0])
1
>>> post[0].__class__
<class 'music21.romanText.rtObjects.RTHandler'>
>>> len(post[1]), len(post[2])
(2, 2)
>>> post = rth.splitByMovement(duplicateHeader=True)
>>> len(post)
2
>>> len(post[0]), len(post[1])
(3, 3)
'''
post: list[RTHandler] = []
sub: list[RTToken] = []
for token in self._tokens:
if token.isMovement():
# when finding a movement, we are ending a previous
# and starting a new; this may just have metadata
rth = RTHandler()
rth.tokens = sub
post.append(rth)
sub = []
sub.append(token)
if sub:
rth = RTHandler()
rth.tokens = sub
post.append(rth)
if duplicateHeader:
alt: list[RTHandler] = []
# if no movement in this first handler, assume it is header info
if not post[0].definesMovement():
handlerHead = post[0]
iStart = 1
else:
handlerHead = None
iStart = 0
for h in post[iStart:]:
if handlerHead is not None:
h = handlerHead + h # add metadata
alt.append(h)
# reassign
post = alt
return post
# --------------------------------------------------------------------------
# access tokens
@property
def tokens(self) -> list[RTToken]:
'''
Get or set tokens for this Handler.
'''
if not self._tokens:
raise RTHandlerException('must process tokens before calling split')
return self._tokens
@tokens.setter
def tokens(self, tokens: list[RTToken]) -> None:
'''
Assign tokens to this Handler.
'''
self._tokens = tokens
def __len__(self) -> int:
return len(self._tokens)
def __add__(self, other: RTHandler) -> RTHandler:
'''
Return a new handler adding the tokens in both
'''
rth = self.__class__() # will get the same class type
rth.tokens = self._tokens + other._tokens
return rth
# ------------------------------------------------------------------------------
[docs]
class RTFile(prebase.ProtoM21Object):
'''
Roman Text File access.
'''
def __init__(self) -> None:
self.file: t.IO[str] | None = None
self.filename: str | pathlib.Path | None = None
[docs]
def open(self, filename: str | pathlib.Path) -> None:
'''
Open a file for reading, trying a variety of encodings and then
trying them again with an "ignore" flag if it is not possible.
'''
encoding: str | None
for encoding in ('utf-8', 'macintosh', 'latin-1', 'utf-16'):
try:
# pylint: disable=consider-using-with
self.file = io.open(filename, encoding=encoding)
if self.file is not None:
break
except UnicodeDecodeError:
pass
if self.file is None:
for encoding in ('utf-8', 'macintosh', 'latin-1', 'utf-16', None):
try:
# pylint: disable=consider-using-with
self.file = io.open(filename, encoding=encoding, errors='ignore')
if self.file is not None:
break
except UnicodeDecodeError:
pass
if self.file is None:
raise RomanTextException(
f'Cannot parse file {filename}, possibly a broken codec?')
self.filename = filename
[docs]
def openFileLike(self, fileLike: t.IO[str]) -> None:
'''
Assign a file-like object, such as those provided by StringIO, as an
open file object.
'''
self.file = fileLike # already 'open'
def _reprInternal(self) -> str:
return ''
[docs]
def close(self) -> None:
openFile = t.cast('t.IO[str]', self.file)
openFile.close()
[docs]
def read(self) -> RTHandler:
'''
Read a file. Note that this calls readstr, which processes all tokens.
If `number` is given, a work number will be extracted if possible.
'''
openFile = t.cast('t.IO[str]', self.file)
return self.readstr(openFile.read())
[docs]
def readstr(self, strSrc: str) -> RTHandler:
'''
Read a string and process all Tokens. Returns a ABCHandler instance.
'''
handler = RTHandler()
# return the handler instance
handler.process(strSrc)
return handler
# ------------------------------------------------------------------------------
class Test(unittest.TestCase):
def testBasicA(self) -> None:
from music21.romanText import testFiles
for fileStr in testFiles.ALL:
f = RTFile()
unused_rth = f.readstr(fileStr) # get a handler from a string
def testReA(self) -> None:
# gets the index of the end of the measure indication
g = reMeasureTag.match('m1 g: V b2 i')
assert g is not None, 'reMeasureTag did not match'
self.assertEqual(g.end(), 2)
self.assertEqual(g.group(0), 'm1')
self.assertEqual(reMeasureTag.match('Time Signature: 2/2'), None)
g = reMeasureTag.match('m3-4=m1-2')
assert g is not None, 'reMeasureTag did not match'
self.assertEqual(g.end(), 4)
self.assertEqual(g.start(), 0)
self.assertEqual(g.group(0), 'm3-4')
g = reMeasureTag.match('m123-432=m1120-24234')
assert g is not None, 'reMeasureTag did not match'
self.assertEqual(g.group(0), 'm123-432')
g = reMeasureTag.match('m231a IV6 b4 C: V')
assert g is not None, 'reMeasureTag did not match'
self.assertEqual(g.group(0), 'm231a')
g = reMeasureTag.match('m123b-432b=m1120a-24234a')
assert g is not None, 'reMeasureTag did not match'
self.assertEqual(g.group(0), 'm123b-432b')
g = reMeasureTag.match('m231var1 IV6 b4 C: V')
assert g is not None, 'reMeasureTag did not match'
self.assertEqual(g.group(0), 'm231')
# this only works if it starts the string
g = reVariant.match('var1 IV6 b4 C: V')
assert g is not None, 'reVariant did not match'
self.assertEqual(g.group(0), 'var1')
g = reAnalyticKeyAtom.match('Bb:')
assert g is not None, 'reAnalyticKeyAtom did not match'
self.assertEqual(g.group(0), 'Bb:')
g = reAnalyticKeyAtom.match('F#:')
assert g is not None, 'reAnalyticKeyAtom did not match'
self.assertEqual(g.group(0), 'F#:')
g = reAnalyticKeyAtom.match('f#:')
assert g is not None, 'reAnalyticKeyAtom did not match'
self.assertEqual(g.group(0), 'f#:')
g = reAnalyticKeyAtom.match('b:')
assert g is not None, 'reAnalyticKeyAtom did not match'
self.assertEqual(g.group(0), 'b:')
g = reAnalyticKeyAtom.match('bb:')
assert g is not None, 'reAnalyticKeyAtom did not match'
self.assertEqual(g.group(0), 'bb:')
g = reAnalyticKeyAtom.match('g:')
assert g is not None, 'reAnalyticKeyAtom did not match'
self.assertEqual(g.group(0), 'g:')
# beats do not have a colon
self.assertEqual(reKeyAtom.match('b2'), None)
self.assertEqual(reKeyAtom.match('b2.5'), None)
g = reBeatAtom.match('b2.5')
assert g is not None, 'reBeatAtom did not match'
self.assertEqual(g.group(0), 'b2.5')
g = reBeatAtom.match('bVII')
self.assertEqual(g, None)
g = reBeatAtom.match('b1.66.5')
assert g is not None, 'reBeatAtom did not match'
self.assertEqual(g.group(0), 'b1.66.5')
def testMeasureAttributeProcessing(self) -> None:
rtm = RTMeasure('m17var1 vi b2 IV b2.5 viio6/4 b3.5 I')
self.assertEqual(rtm.data, 'vi b2 IV b2.5 viio6/4 b3.5 I')
self.assertEqual(rtm.number, [17])
self.assertEqual(rtm.tag, 'm17')
self.assertEqual(rtm.variantNumber, 1)
rtm = RTMeasure('m17varC vi b2 IV b2.5 viio6/4 b3.5 I')
self.assertEqual(rtm.data, 'vi b2 IV b2.5 viio6/4 b3.5 I')
self.assertEqual(rtm.variantLetter, 'C')
rtm = RTMeasure('m20 vi b2 ii6/5 b3 V b3.5 V7')
self.assertEqual(rtm.data, 'vi b2 ii6/5 b3 V b3.5 V7')
self.assertEqual(rtm.number, [20])
self.assertEqual(rtm.tag, 'm20')
self.assertEqual(rtm.variantNumber, None)
self.assertFalse(rtm.isCopyDefinition)
rtm = RTMeasure('m0 b3 G: I')
self.assertEqual(rtm.data, 'b3 G: I')
self.assertEqual(rtm.number, [0])
self.assertEqual(rtm.tag, 'm0')
self.assertEqual(rtm.variantNumber, None)
self.assertFalse(rtm.isCopyDefinition)
rtm = RTMeasure('m59 = m57')
self.assertEqual(rtm.data, '= m57')
self.assertEqual(rtm.number, [59])
self.assertEqual(rtm.tag, 'm59')
self.assertEqual(rtm.variantNumber, None)
self.assertTrue(rtm.isCopyDefinition)
rtm = RTMeasure('m3-4 = m1-2')
self.assertEqual(rtm.data, '= m1-2')
self.assertEqual(rtm.number, [3, 4])
self.assertEqual(rtm.tag, 'm3-4')
self.assertEqual(rtm.variantNumber, None)
self.assertTrue(rtm.isCopyDefinition)
def testTokenDefinition(self) -> None:
# test that we are always getting the right number of tokens
from music21.romanText import testFiles
rth = RTHandler()
rth.process(testFiles.mozartK279)
count = 0
for token in rth._tokens:
if token.isMovement():
count += 1
self.assertEqual(count, 3)
rth.process(testFiles.riemenschneider001)
count = 0
for token in rth._tokens:
if token.isMeasure():
# print(token.src)
count += 1
# 21, 2 variants, and one pickup
self.assertEqual(count, 21 + 2 + 1)
count = 0
for token in rth._tokens:
if isinstance(token, RTMeasure):
for a in token.atoms:
if isinstance(a, RTAnalyticKey):
count += 1
self.assertEqual(count, 1)
# ------------------------------------------------------------------------------
# define presented order in documentation
# _DOC_ORDER = []
if __name__ == '__main__':
import music21
music21.mainTest(Test)