# ------------------------------------------------------------------------------
# Name: romanText/translate.py
# Purpose: Translation routines for roman numeral analysis text files
#
# Authors: Christopher Ariza
# Michael Scott Asato Cuthbert
#
# Copyright: Copyright © 2011-2023 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
'''
Translation routines for roman numeral analysis text files, as defined
and demonstrated by Dmitri Tymoczko. Also used for the ClercqTemperley
format which is similar but a little different.
This module is really only needed for people extending the parser,
for others it's simple to get Harmony, RomanNumeral, Key (or KeySignature)
and other objects out of an rntxt file by running this:
>>> monteverdi = corpus.parse('monteverdi/madrigal.3.1.rntxt')
>>> monteverdi.show('text')
{0.0} <music21.metadata.Metadata object at 0x...>
{0.0} <music21.stream.Part ...>
{0.0} <music21.stream.Measure 1 offset=0.0>
{0.0} <music21.key.KeySignature of 1 flat>
{0.0} <music21.meter.TimeSignature 4/4>
{0.0} <music21.roman.RomanNumeral vi in F major>
{3.0} <music21.roman.RomanNumeral V[no3] in F major>
{4.0} <music21.stream.Measure 2 offset=4.0>
{0.0} <music21.roman.RomanNumeral I in F major>
{3.0} <music21.roman.RomanNumeral IV in F major>
...
Then the stream can be analyzed with something like this, storing
the data to make a histogram of scale degree usage within a key:
>>> degreeDictionary = {}
>>> for el in monteverdi.recurse():
... if isinstance(el, roman.RomanNumeral):
... print(f'{el.figure} {el.key}')
... for p in el.pitches:
... degree, accidental = el.key.getScaleDegreeAndAccidentalFromPitch(p)
... if accidental is None:
... degreeString = str(degree)
... else:
... degreeString = str(degree) + str(accidental.modifier)
... if degreeString not in degreeDictionary:
... degreeDictionary[degreeString] = 1
... else:
... degreeDictionary[degreeString] += 1
... degTuple = (str(p), degreeString)
... print(degTuple)
vi F major
('D5', '6')
('F5', '1')
('A5', '3')
V[no3] F major
('C5', '5')
('G5', '2')
I F major
('F4', '1')
('A4', '3')
('C5', '5')
...
V6 g minor
('F#5', '7#')
('A5', '2')
('D6', '5')
i g minor
('G4', '1')
('B-4', '3')
('D5', '5')
...
Now if we'd like we can get a Histogram of the data.
It's a little complex, but worth seeing in full:
>>> import operator
>>> histogram = graph.primitives.GraphHistogram()
>>> i = 0
>>> data = []
>>> xLabels = []
>>> values = []
>>> ddList = list(degreeDictionary.items())
>>> for deg,value in sorted(ddList, key=operator.itemgetter(1), reverse=True):
... data.append((i, degreeDictionary[deg]), )
... xLabels.append((i+.5, deg), )
... values.append(degreeDictionary[deg])
... i += 1
>>> histogram.data = data
These commands give nice labels for the data; optional:
>>> histogram.setIntegerTicksFromData(values, 'y')
>>> histogram.setTicks('x', xLabels)
>>> histogram.setAxisLabel('x', 'ScaleDegree')
Now generate the histogram:
>>> #_DOCS_HIDE histogram.process()
.. image:: images/romanTranslatePitchDistribution.*
:width: 600
OMIT_FROM_DOCS
>>> x = converter.parse('romantext: m1 a: VI')
>>> [str(p) for p in x[roman.RomanNumeral].first().pitches]
['F5', 'A5', 'C6']
>>> x = converter.parse('romantext: m1 a: vi')
>>> [str(p) for p in x[roman.RomanNumeral].first().pitches]
['F#5', 'A5', 'C#6']
>>> [str(p) for p in
... converter.parse('romantext: m1 a: vio'
... )[roman.RomanNumeral].first().pitches]
['F#5', 'A5', 'C6']
'''
from __future__ import annotations
import copy
import textwrap
import traceback
import typing as t
import unittest
from music21 import bar
from music21 import base
from music21 import common
from music21.common.types import OffsetQL
from music21 import environment
from music21 import exceptions21
from music21 import harmony
from music21 import key
from music21 import metadata
from music21 import meter
from music21 import note
from music21 import repeat
from music21 import roman
from music21.romanText import rtObjects
from music21 import spanner
from music21 import stream
from music21 import tie
environLocal = environment.Environment('romanText.translate')
ROMANTEXT_VERSION = 1.0
USE_RN_CACHE = False
# Not currently using rnCache because of problems with PivotChords,
# See mail from Dmitri, 30 September 2014
# ------------------------------------------------------------------------------
class RomanTextTranslateException(exceptions21.Music21Exception):
pass
[docs]
class RomanTextUnprocessedToken(base.ElementWrapper):
pass
[docs]
class RomanTextUnprocessedMetadata(base.Music21Object):
def __init__(self, tag='', data='', **keywords):
super().__init__(**keywords)
self.tag = tag
self.data = data
def _reprInternal(self) -> str:
return f'{self.tag}: {self.data}'
def _copySingleMeasure(rtTagged, p, kCurrent):
'''
Given a RomanText token, a Part used as the current container,
and the current Key, return a Measure copied from the past of the Part.
This is used in cases of definitions such as:
m23=m21
'''
m = None
# copy from a past location; need to change key
# environLocal.printDebug(['calling _copySingleMeasure()'])
targetNumber, unused_targetRepeat = rtTagged.getCopyTarget()
if len(targetNumber) > 1: # pragma: no cover
# this is an encoding error
raise RomanTextTranslateException(
'a single measure cannot define a copy operation for multiple measures')
# TODO: ignoring repeat letters
target = targetNumber[0]
for mPast in p.getElementsByClass(stream.Measure):
if mPast.number == target:
try:
m = copy.deepcopy(mPast)
except TypeError as te: # pragma: no cover
raise RomanTextTranslateException(
f'Failed to copy measure {mPast.number}:'
+ ' did you perhaps parse an RTOpus object with romanTextToStreamScore '
+ 'instead of romanTextToStreamOpus?'
) from te
m.number = rtTagged.number[0]
# update all keys
for rnPast in m.getElementsByClass(roman.RomanNumeral):
if kCurrent is None: # pragma: no cover
# should not happen
raise RomanTextTranslateException(
'attempting to copy a measure but no past key definitions are found')
if rnPast.followsKeyChange:
kCurrent = rnPast.key
elif rnPast.pivotChord is not None:
kCurrent = rnPast.pivotChord.key
else:
rnPast.key = kCurrent
if rnPast.secondaryRomanNumeral is not None:
newRN = roman.RomanNumeral(
rnPast.figure, copy.deepcopy(kCurrent)
)
newRN.writeAsChord = True
newRN.duration = copy.deepcopy(rnPast.duration)
newRN.lyrics = copy.deepcopy(rnPast.lyrics)
m.replace(rnPast, newRN)
break
return m, kCurrent
def _copyMultipleMeasures(rtMeasure: rtObjects.RTMeasure,
p: stream.Part,
kCurrent: key.Key|None):
'''
Given a RomanText token for a RTMeasure, a
Part used as the current container, and the current Key,
return a Measure range copied from the past of the Part.
This is used for cases such as:
m23-25 = m20-22
'''
# the key provided needs to be the current key
# environLocal.printDebug(['calling _copyMultipleMeasures()'])
targetNumbers, unused_targetRepeat = rtMeasure.getCopyTarget()
if len(targetNumbers) == 1: # pragma: no cover
# this is an encoding error
raise RomanTextTranslateException('a multiple measure range cannot copy a single measure')
# TODO: ignoring repeat letters
targetStart = targetNumbers[0]
targetEnd = targetNumbers[1]
if rtMeasure.number[1] - rtMeasure.number[0] != targetEnd - targetStart: # pragma: no cover
raise RomanTextTranslateException(
'both the source and destination sections need to have the same number of measures')
if rtMeasure.number[0] < targetEnd: # pragma: no cover
raise RomanTextTranslateException(
'the source section cannot overlap with the destination section')
measures = []
for mPast in p.getElementsByClass(stream.Measure):
if mPast.number in range(targetStart, targetEnd + 1):
try:
m = copy.deepcopy(mPast)
except TypeError as te: # pragma: no cover
raise RomanTextTranslateException(
f'Failed to copy measure {mPast.number} to measure range '
f'{targetStart}-{targetEnd}: did you perhaps parse an RTOpus '
'object with romanTextToStreamScore instead of '
'romanTextToStreamOpus?'
) from te
m.number = rtMeasure.number[0] + mPast.number - targetStart
measures.append(m)
# update all keys
allRNs = list(m.getElementsByClass(roman.RomanNumeral))
for rnPast in allRNs:
if kCurrent is None: # pragma: no cover
# should not happen
raise RomanTextTranslateException(
'attempting to copy a measure but no past key definitions are found')
if rnPast.followsKeyChange:
kCurrent = rnPast.key
elif rnPast.pivotChord is not None:
kCurrent = rnPast.pivotChord.key
else:
rnPast.key = kCurrent
if rnPast.secondaryRomanNumeral is not None:
newRN = roman.RomanNumeral(
rnPast.figure, copy.deepcopy(kCurrent)
)
newRN.writeAsChord = True
newRN.duration = copy.deepcopy(rnPast.duration)
newRN.lyrics = copy.deepcopy(rnPast.lyrics)
m.replace(rnPast, newRN)
if mPast.number == targetEnd:
break
return measures, kCurrent
def _getKeyAndPrefix(rtKeyOrString):
'''
Given an RTKey specification, return the Key and a string prefix based
on the tonic:
>>> romanText.translate._getKeyAndPrefix('c')
(<music21.key.Key of c minor>, 'c: ')
>>> romanText.translate._getKeyAndPrefix('F#')
(<music21.key.Key of F# major>, 'F#: ')
>>> romanText.translate._getKeyAndPrefix('Eb')
(<music21.key.Key of E- major>, 'E-: ')
>>> romanText.translate._getKeyAndPrefix('Bb')
(<music21.key.Key of B- major>, 'B-: ')
>>> romanText.translate._getKeyAndPrefix('bb')
(<music21.key.Key of b- minor>, 'b-: ')
>>> romanText.translate._getKeyAndPrefix('b#')
(<music21.key.Key of b# minor>, 'b#: ')
>>> romanText.translate._getKeyAndPrefix('Bbb')
(<music21.key.Key of B-- major>, 'B--: ')
'''
if isinstance(rtKeyOrString, str):
rtKeyOrString = key.convertKeyStringToMusic21KeyString(rtKeyOrString)
k = key.Key(rtKeyOrString)
else:
k = rtKeyOrString.getKey()
tonicName = k.tonic.name
if k.mode == 'minor':
tonicName = tonicName.lower()
prefix = tonicName + ': '
return k, prefix
# Cache each of the created keys so that we don't recreate them.
_rnKeyCache: dict[tuple[str, str], roman.RomanNumeral] = {}
[docs]
class PartTranslator:
'''
A refactoring of the previously massive romanTextToStreamScore function
to allow for more fine-grained testing (eventually), and to
get past the absurdly high number of nested blocks (the previous translator
was written under severe time constraints).
'''
def __init__(self, md=None):
if md is None:
md = metadata.Metadata()
self.md = md # global metadata object
self.p = stream.Part()
self.romanTextVersion = ROMANTEXT_VERSION
# ts indication are found in header, and also found elsewhere
self.tsCurrent = meter.TimeSignature('4/4') # create default 4/4
self.tsAtTimeOfLastChord = self.tsCurrent
self.tsSet = False # store if set to a measure
self.lastMeasureToken = None
self.lastMeasureNumber = 0
self.previousRn = None
self.keySigCurrent = None
self.setKeySigFromFirstKeyToken = True # set a keySignature
self.foundAKeySignatureSoFar = False
self.kCurrent, unused_prefixLyric = _getKeyAndPrefix('C') # default if none defined
self.prefixLyric = ''
self.sixthMinor = roman.Minor67Default.CAUTIONARY
self.seventhMinor = roman.Minor67Default.CAUTIONARY
self.repeatEndings = {}
# reset for each measure
self.currentMeasureToken = None
self.previousChordInMeasure = None
self.pivotChordPossible = False
self.numberOfAtomsInCurrentMeasure = 0
self.setKeyChangeToken = False
self.currentOffsetInMeasure = 0.0
[docs]
def translateTokens(self, tokens):
for token in tokens:
try:
self.translateOneLineToken(token)
except Exception as exc:
tracebackMessage = traceback.format_exc()
raise RomanTextTranslateException(
f'At line {token.lineNumber} for token {token}, '
f'an exception was raised: \n{tracebackMessage}') from exc
p = self.p
p.coreElementsChanged()
fixPickupMeasure(p)
p.makeBeams(inPlace=True)
p.makeAccidentals(inPlace=True)
_addRepeatsFromRepeatEndings(p, self.repeatEndings) # 1st and second endings
return p
[docs]
def translateOneLineToken(self, lineToken: rtObjects.RTTagged):
# noinspection SpellCheckingInspection
'''
Translates one line token and set the current settings.
A token in this case consists of an entire line's worth.
It might be a token such as 'Title: Neko Funjatta' or
a composite token such as 'm23 b4 IV6'
'''
md = self.md
# environLocal.printDebug(['token', t])
# most common case first
if lineToken.isMeasure():
measureToken = t.cast(rtObjects.RTMeasure, lineToken)
self.translateMeasureLineToken(measureToken)
elif lineToken.isTitle():
md.add('title', lineToken.data)
elif lineToken.isWork():
md.add('alternativeTitle', lineToken.data)
elif lineToken.isPiece():
md.add('alternativeTitle', lineToken.data)
elif lineToken.isComposer():
md.add('composer', lineToken.data)
elif lineToken.isMovement():
md.add('movementNumber', lineToken.data)
elif lineToken.isAnalyst():
md.add('analyst', lineToken.data)
elif lineToken.isProofreader():
md.add('proofreader', lineToken.data)
elif lineToken.isTimeSignature():
try:
self.tsCurrent = meter.TimeSignature(lineToken.data)
self.tsSet = False
except exceptions21.Music21Exception: # pragma: no cover
environLocal.warn(f'Could not parse TimeSignature tag: {lineToken.data!r}')
# environLocal.printDebug(['tsCurrent:', tsCurrent])
elif lineToken.isKeySignature():
self.parseKeySignatureTag(lineToken)
elif lineToken.isSixthMinor() or lineToken.isSeventhMinor():
self.setMinorRootParse(lineToken)
elif lineToken.isVersion():
try:
self.romanTextVersion = float(lineToken.data)
except ValueError: # pragma: no cover
environLocal.warn(f'Could not parse RTVersion tag: {lineToken.data!r}')
elif isinstance(lineToken, rtObjects.RTTagged):
otherMetadata = RomanTextUnprocessedMetadata(lineToken.tag, lineToken.data)
self.p.append(otherMetadata)
else: # pragma: no cover
unprocessed = RomanTextUnprocessedToken(lineToken)
self.p.append(unprocessed)
[docs]
def setMinorRootParse(self, rtTagged: rtObjects.RTTagged):
'''
Set Roman Numeral parsing standards from a token.
>>> pt = romanText.translate.PartTranslator()
>>> pt.sixthMinor
<Minor67Default.CAUTIONARY: 2>
>>> tag = romanText.rtObjects.RTTagged('SixthMinor: Flat')
>>> tag.isSixthMinor()
True
>>> pt.setMinorRootParse(tag)
>>> pt.sixthMinor
<Minor67Default.FLAT: 4>
Harmonic sets to FLAT for sixth and SHARP for seventh
>>> for config in 'flat sharp quality cautionary harmonic'.split():
... tag = romanText.rtObjects.RTTagged('Seventh Minor: ' + config)
... pt.setMinorRootParse(tag)
... print(pt.seventhMinor)
Minor67Default.FLAT
Minor67Default.SHARP
Minor67Default.QUALITY
Minor67Default.CAUTIONARY
Minor67Default.SHARP
>>> tag = romanText.rtObjects.RTTagged('Sixth Minor: harmonic')
>>> pt.setMinorRootParse(tag)
>>> print(pt.sixthMinor)
Minor67Default.FLAT
Unknown settings raise a `RomanTextTranslateException`
>>> tag = romanText.rtObjects.RTTagged('Seventh Minor: asdf')
>>> pt.setMinorRootParse(tag)
Traceback (most recent call last):
music21.romanText.translate.RomanTextTranslateException:
Cannot parse setting vi or vii parsing: 'asdf'
'''
tData = rtTagged.data.lower()
if tData == 'flat':
tEnum = roman.Minor67Default.FLAT
elif tData == 'sharp':
tEnum = roman.Minor67Default.SHARP
elif tData == 'quality':
tEnum = roman.Minor67Default.QUALITY
elif tData in ('courtesy', 'cautionary'):
tEnum = roman.Minor67Default.CAUTIONARY
elif tData == 'harmonic':
if rtTagged.isSixthMinor():
tEnum = roman.Minor67Default.FLAT
else:
tEnum = roman.Minor67Default.SHARP
else:
raise RomanTextTranslateException(
f'Cannot parse setting vi or vii parsing: {tData!r}')
if rtTagged.isSixthMinor():
self.sixthMinor = tEnum
else:
self.seventhMinor = tEnum
[docs]
def translateMeasureLineToken(self, measureLineToken: rtObjects.RTMeasure):
'''
Translate a measure token consisting of a single line such as::
m21 b3 V b4 C: IV
Or it might be a variant measure, or a copy instruction.
'''
p = self.p
skipsPriorMeasures = ((measureLineToken.number[0] > self.lastMeasureNumber + 1)
and (self.previousRn is not None))
isSingleMeasureCopy = (len(measureLineToken.number) == 1
and measureLineToken.isCopyDefinition)
isMultipleMeasureCopy = (len(measureLineToken.number) > 1)
# environLocal.printDebug(['handling measure token:', measureLineToken])
# if measureLineToken.number[0] % 10 == 0:
# print('at number ' + str(measureLineToken.number[0]))
if measureLineToken.variantNumber is not None:
# TODO(msc): parse variant numbers
# environLocal.printDebug([f' skipping variant: {measureLineToken}'])
return
if measureLineToken.variantLetter is not None:
# TODO(msc): parse variant letters
# environLocal.printDebug([f' skipping variant letter: {measureLineToken}'])
return
# if this measure number is more than 1 greater than the last
# defined measure number, and the previous chord is not None,
# then fill with copies of the last-defined measure
if skipsPriorMeasures:
self.fillToMeasureToken(measureLineToken)
# create a new measure or copy a past measure
if isSingleMeasureCopy: # if not a range
p.coreElementsChanged()
m, self.kCurrent = _copySingleMeasure(measureLineToken, p, self.kCurrent)
p.coreAppend(m)
self.lastMeasureNumber = m.number
self.lastMeasureToken = measureLineToken
romans = m.getElementsByClass(roman.RomanNumeral)
if romans:
self.previousRn = romans[-1]
elif isMultipleMeasureCopy:
p.coreElementsChanged()
measures, self.kCurrent = _copyMultipleMeasures(measureLineToken, p, self.kCurrent)
p.append(measures) # appendCore does not work with list
self.lastMeasureNumber = measures[-1].number
self.lastMeasureToken = measureLineToken
romans = measures[-1].getElementsByClass(roman.RomanNumeral)
if romans:
self.previousRn = romans[-1]
else:
m = self.translateSingleMeasure(measureLineToken)
if m.duration.quarterLength == 0:
self.fillMeasureFromPreviousRn(m)
p.coreAppend(m)
[docs]
def fillToMeasureToken(self, measureToken: rtObjects.RTMeasure):
'''
Create a series of measures which extend the previous RN until the measure number
implied by `measureToken`.
'''
p = self.p
for i in range(self.lastMeasureNumber + 1, measureToken.number[0]):
mFill = stream.Measure()
mFill.number = i
self.fillMeasureFromPreviousRn(mFill)
appendMeasureToRepeatEndingsDict(self.lastMeasureToken,
mFill,
self.repeatEndings, i)
p.coreAppend(mFill)
self.lastMeasureNumber = measureToken.number[0] - 1
self.lastMeasureToken = measureToken
[docs]
def fillMeasureFromPreviousRn(self, mFill: stream.Measure) -> None:
if self.previousRn is not None:
newRn = copy.deepcopy(self.previousRn)
newRn.lyric = ''
# set to entire bar duration and tie
newRn.duration = copy.deepcopy(self.tsAtTimeOfLastChord.barDuration)
if self.previousRn.tie is None:
self.previousRn.tie = tie.Tie('start')
else:
self.previousRn.tie.type = 'continue'
# set to stop for now; may extend on next iteration
newRn.tie = tie.Tie('stop')
self.previousRn = newRn
mFill.append(newRn)
[docs]
def parseKeySignatureTag(self, rtTagged: rtObjects.RTTagged):
'''
Parse a key signature tag which has already been determined to
be a key signature.
>>> tag = romanText.rtObjects.RTTagged('KeySignature: -4')
>>> tag.isKeySignature()
True
>>> tag.data
'-4'
>>> pt = romanText.translate.PartTranslator()
>>> pt.keySigCurrent is None
True
>>> pt.setKeySigFromFirstKeyToken
True
>>> pt.foundAKeySignatureSoFar
False
>>> pt.parseKeySignatureTag(tag)
>>> pt.keySigCurrent
<music21.key.KeySignature of 4 flats>
>>> pt.setKeySigFromFirstKeyToken
False
>>> pt.foundAKeySignatureSoFar
True
>>> tag = romanText.rtObjects.RTTagged('KeySignature: xyz')
>>> pt.parseKeySignatureTag(tag)
Traceback (most recent call last):
music21.romanText.translate.RomanTextTranslateException:
Cannot parse key signature: 'xyz'
'''
data = rtTagged.data
if data == '':
self.keySigCurrent = key.KeySignature(0)
elif data == 'Bb':
self.keySigCurrent = key.KeySignature(-1)
else:
try:
dataVal = int(data)
self.keySigCurrent = key.KeySignature(dataVal)
except ValueError as ve:
raise RomanTextTranslateException(f'Cannot parse key signature: {data!r}') from ve
self.setKeySigFromFirstKeyToken = False
# environLocal.printDebug(['keySigCurrent:', keySigCurrent])
self.foundAKeySignatureSoFar = True
[docs]
def translateSingleMeasure(self, measureToken):
'''
Given a measureToken, return a `stream.Measure` object with
the appropriate atoms set.
'''
self.currentMeasureToken = measureToken
m = stream.Measure()
m.number = measureToken.number[0]
appendMeasureToRepeatEndingsDict(measureToken, m, self.repeatEndings)
self.lastMeasureNumber = measureToken.number[0]
self.lastMeasureToken = measureToken
if not self.tsSet:
m.timeSignature = self.tsCurrent
self.tsSet = True # only set when changed
if not self.setKeySigFromFirstKeyToken and self.keySigCurrent is not None:
m.insert(0, self.keySigCurrent)
self.setKeySigFromFirstKeyToken = True # only set when changed
self.currentOffsetInMeasure = 0.0 # start offsets at zero
self.previousChordInMeasure = None
self.pivotChordPossible = False
self.numberOfAtomsInCurrentMeasure = len(measureToken.atoms)
# first RomanNumeral object after a key change should have this set to True
self.setKeyChangeToken = False
for i, a in enumerate(measureToken.atoms):
isLastAtomInMeasure = (i == self.numberOfAtomsInCurrentMeasure - 1)
self.translateSingleMeasureAtom(a, m, isLastAtomInMeasure=isLastAtomInMeasure)
# may need to adjust duration of last chord added
if self.tsCurrent is not None:
self.previousRn.quarterLength = (self.tsCurrent.barDuration.quarterLength
- self.currentOffsetInMeasure)
m.coreElementsChanged()
return m
[docs]
def translateSingleMeasureAtom(
self,
a: rtObjects.RTAtom,
m: stream.Measure,
*,
isLastAtomInMeasure: bool = False,
) -> None:
'''
Translate a single atom in a measure token.
`a` is the Atom
`m` is a `stream.Measure` object.
Uses coreInsert and coreAppend methods, so must have `m.coreElementsChanged()`
called afterward.
'''
if (isinstance(a, rtObjects.RTKey)
or (self.foundAKeySignatureSoFar is False
and isinstance(a, rtObjects.RTAnalyticKey))):
self.setAnalyticKey(a)
# insert at beginning of measure if at beginning
# -- for things like pickups.
if m.number <= 1:
m.coreInsert(0, self.kCurrent)
else:
m.coreInsert(self.currentOffsetInMeasure, self.kCurrent)
self.foundAKeySignatureSoFar = True
elif isinstance(a, rtObjects.RTKeySignature):
try: # this sets the keysignature but not the prefix text
thisSig = a.getKeySignature()
except (exceptions21.Music21Exception, ValueError) as ve: # pragma: no cover
raise RomanTextTranslateException(
f'cannot get key from {a.src} in line {self.currentMeasureToken.src}'
) from ve
# insert at beginning of measure if at beginning
# -- for things like pickups.
if m.number <= 1:
m.coreInsert(0, thisSig)
else:
m.coreInsert(self.currentOffsetInMeasure, thisSig)
self.foundAKeySignatureSoFar = True
elif isinstance(a, rtObjects.RTAnalyticKey):
self.setAnalyticKey(a)
elif isinstance(a, rtObjects.RTBeat):
# set new offset based on beat
try:
newOffset = a.getOffset(self.tsCurrent)
except ValueError as ve: # pragma: no cover
raise RomanTextTranslateException(
f'cannot properly get an offset from beat data {a.src} '
f'under timeSignature {self.tsCurrent} '
f'in line {self.currentMeasureToken.src}'
) from ve
if (self.previousChordInMeasure is None
and self.previousRn is not None
and newOffset > 0):
# setting a new beat before giving any chords
firstChord = copy.deepcopy(self.previousRn)
firstChord.quarterLength = newOffset
firstChord.lyric = ''
if self.previousRn.tie is None:
self.previousRn.tie = tie.Tie('start')
else:
self.previousRn.tie.type = 'continue'
firstChord.tie = tie.Tie('stop')
self.previousRn = firstChord
self.previousChordInMeasure = firstChord
m.coreInsert(0, firstChord)
self.pivotChordPossible = False
self.currentOffsetInMeasure = newOffset
elif isinstance(a, rtObjects.RTNoChord):
# use source to evaluation roman
self.tsAtTimeOfLastChord = self.tsCurrent
cs = harmony.NoChord()
m.coreInsert(self.currentOffsetInMeasure, cs)
rn = note.Rest()
if self.pivotChordPossible is False:
# probably best to find duration
if self.previousChordInMeasure is None:
pass # use default duration
else: # update duration of previous chord in Measure
oPrevious = self.previousChordInMeasure.getOffsetBySite(m)
newQL = self.currentOffsetInMeasure - oPrevious
if newQL <= 0: # pragma: no cover
raise RomanTextTranslateException(
f'too many notes in this measure: {self.currentMeasureToken.src}')
self.previousChordInMeasure.quarterLength = newQL
self.prefixLyric = ''
m.coreInsert(self.currentOffsetInMeasure, rn)
self.previousChordInMeasure = rn
self.previousRn = rn
self.pivotChordPossible = False
elif isinstance(a, rtObjects.RTChord):
self.processRTChord(a, m, self.currentOffsetInMeasure)
elif isinstance(a, rtObjects.RTRepeat):
if isinstance(a, rtObjects.RTRepeatStop) and isLastAtomInMeasure:
# This condition needs to be moved here because it is possible
# and indeed likely that an end repeat will have an offset of
# 0 in the case that there is 0 or 1 harmonies in the measure.
# Is it ok, however, that this condition will match if self.tsCurrent
# is None? Previously, that case was excluded below.
m.rightBarline = bar.Repeat(direction='end')
if self.tsCurrent is not None:
m.setElementOffset(
m.rightBarline, self.tsCurrent.barDuration.quarterLength
)
elif self.currentOffsetInMeasure == 0:
if isinstance(a, rtObjects.RTRepeatStart):
m.leftBarline = bar.Repeat(direction='start')
else:
rtt = RomanTextUnprocessedToken(a)
m.coreInsert(self.currentOffsetInMeasure, rtt)
elif (self.tsCurrent is not None
and self.tsCurrent.barDuration.quarterLength == self.currentOffsetInMeasure):
if isinstance(a, rtObjects.RTRepeatStop):
m.rightBarline = bar.Repeat(direction='end')
else:
rtt = RomanTextUnprocessedToken(a)
m.coreInsert(self.currentOffsetInMeasure, rtt)
else: # mid-measure repeat signs
rtt = RomanTextUnprocessedToken(a)
m.coreInsert(self.currentOffsetInMeasure, rtt)
else:
rtt = RomanTextUnprocessedToken(a)
m.coreInsert(self.currentOffsetInMeasure, rtt)
# environLocal.warn(f' Got an unknown token: {a}')
[docs]
def processRTChord(
self,
a: rtObjects.RTChord,
m: stream.Measure,
currentOffset: OffsetQL
) -> None:
'''
Process a single RTChord atom.
'''
# use source to evaluation roman
self.tsAtTimeOfLastChord = self.tsCurrent
try:
aSrc = a.src
# if kCurrent.mode == 'minor':
# if aSrc.lower().startswith('vi'): # vi or vii w/ or w/o o
# if aSrc.upper() == a.src: # VI or VII to bVI or bVII
# aSrc = 'b' + aSrc
cacheTuple = (aSrc, self.kCurrent.tonicPitchNameWithCase)
if USE_RN_CACHE and cacheTuple in _rnKeyCache: # pragma: no cover
# print('Got a match: ' + str(cacheTuple))
# Problems with Caches not picking up pivot chords
# Not faster, see below.
rn = copy.deepcopy(_rnKeyCache[cacheTuple])
else:
# print('No match for: ' + str(cacheTuple))
rn = roman.RomanNumeral(aSrc,
copy.deepcopy(self.kCurrent),
sixthMinor=self.sixthMinor,
seventhMinor=self.seventhMinor,
)
rn.writeAsChord = True
_rnKeyCache[cacheTuple] = rn
# surprisingly, not faster, and more dangerous
# rn = roman.RomanNumeral(aSrc, kCurrent)
# # SLOWEST!!!
# rn = roman.RomanNumeral(aSrc, kCurrent.tonicPitchNameWithCase)
# >>> from timeit import timeit as t
# >>> t('roman.RomanNumeral("IV", "c#")',
# ... 'from music21 import roman', number=1000)
# 45.75
# >>> t('roman.RomanNumeral("IV", k)',
# ... 'from music21 import roman, key; k = key.Key("c#")',
# ... number=1000)
# 16.09
# >>> t('roman.RomanNumeral("IV", copy.deepcopy(k))',
# ... 'from music21 import roman, key; import copy;
# ... k = key.Key("c#")', number=1000)
# 22.49
# # key cache, does not help much
# >>> t('copy.deepcopy(r)', 'from music21 import roman; import copy;
# ... r = roman.RomanNumeral("IV", "c#")', number=1000)
# 19.01
if self.setKeyChangeToken is True:
rn.followsKeyChange = True
self.setKeyChangeToken = False
else:
rn.followsKeyChange = False
except roman.RomanNumeralException: # pragma: no cover
# environLocal.printDebug(f' cannot create RN from: {a.src}')
rn = roman.RomanNumeral() # create placeholder
if self.pivotChordPossible is False:
# probably best to find duration
if self.previousChordInMeasure is None:
pass # use default duration
else: # update duration of previous chord in Measure
oPrevious = self.previousChordInMeasure.getOffsetBySite(m)
newQL = currentOffset - oPrevious
if newQL <= 0: # pragma: no cover
raise RomanTextTranslateException(
f'too many notes in this measure: {self.currentMeasureToken.src}')
self.previousChordInMeasure.quarterLength = newQL
rn.addLyric(self.prefixLyric + a.src)
self.prefixLyric = ''
m.coreInsert(currentOffset, rn)
self.previousChordInMeasure = rn
self.previousRn = rn
self.pivotChordPossible = True
else:
self.previousChordInMeasure.lyric += '//' + self.prefixLyric + a.src
self.previousChordInMeasure.pivotChord = rn
self.prefixLyric = ''
self.pivotChordPossible = False
[docs]
def setAnalyticKey(self, a):
'''
Indicates a change in the analyzed key, not a change in anything
else, such as the keySignature.
'''
try: # this sets the key and the keysignature
self.kCurrent, pl = _getKeyAndPrefix(a)
self.prefixLyric += pl
except (ValueError, exceptions21.Music21Exception) as ve: # pragma: no cover
raise RomanTextTranslateException(
f'cannot get analytic key from {a.src} in line {self.currentMeasureToken.src}'
) from ve
self.setKeyChangeToken = True
[docs]
def romanTextToStreamScore(rtHandler, inputM21=None):
'''
The main processing module for single-movement RomanText works.
Given a romanText handler or string, return or fill a Score Stream.
'''
# accept a string directly; mostly for testing
if isinstance(rtHandler, str):
rtf = rtObjects.RTFile()
tokenedRtHandler = rtf.readstr(rtHandler) # return handler, processes tokens
else:
tokenedRtHandler = rtHandler
# this could be just a Stream, but b/c we are creating metadata,
# perhaps better to match presentation of other scores.
if inputM21 is None:
s = stream.Score()
else:
s = inputM21
# metadata can be first
md = metadata.Metadata()
s.insert(0, md)
partTrans = PartTranslator(md)
p = partTrans.translateTokens(tokenedRtHandler.tokens)
s.insert(0, p)
return s
letterToNumDict = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7, 'h': 8}
[docs]
def appendMeasureToRepeatEndingsDict(rtMeasureObj: rtObjects.RTMeasure,
m: stream.Measure,
repeatEndings: dict,
measureNumber=None):
# noinspection PyShadowingNames
'''
Takes an RTMeasure object (which might represent one or more
measures; but currently only one) and a music21 stream.Measure object and
store it as a tuple in the repeatEndings dictionary to mark where the
translator should later mark for adding endings.
If the optional measureNumber is specified, we use that rather than the
token number to add to the dict.
This does not yet work for skipped measures.
>>> rtm = romanText.rtObjects.RTMeasure('m15a V6 b1.5 V6/5 b2 I b3 viio6')
>>> rtm.repeatLetter
['a']
>>> rtm2 = romanText.rtObjects.RTMeasure('m15b V6 b1.5 V6/5 b2 I')
>>> rtm2.repeatLetter
['b']
>>> repeatEndings = {}
>>> m1 = stream.Measure()
>>> m2 = stream.Measure()
>>> romanText.translate.appendMeasureToRepeatEndingsDict(rtm, m1, repeatEndings)
>>> repeatEndings
{1: [(15, <music21.stream.Measure 0a offset=0.0>)]}
>>> romanText.translate.appendMeasureToRepeatEndingsDict(rtm2, m2, repeatEndings)
>>> repeatEndings[1], repeatEndings[2]
([(15, <music21.stream.Measure 0a offset=0.0>)],
[(15, <music21.stream.Measure 0b offset=0.0>)])
>>> repeatEndings[2][0][1] is m2
True
'''
if not rtMeasureObj.repeatLetter:
return
m.numberSuffix = rtMeasureObj.repeatLetter[0]
for rl in rtMeasureObj.repeatLetter:
if rl is None or rl == '':
continue
if rl not in letterToNumDict: # pragma: no cover
raise RomanTextTranslateException(f'Improper repeat letter: {rl}')
repeatNumber = letterToNumDict[rl]
if repeatNumber not in repeatEndings:
repeatEndings[repeatNumber] = []
if measureNumber is None:
measureTuple = (rtMeasureObj.number[0], m)
else:
measureTuple = (measureNumber, m)
repeatEndings[repeatNumber].append(measureTuple)
def _consolidateRepeatEndings(repeatEndings):
# noinspection PyShadowingNames
'''
take repeatEndings, which is a dict of integers (repeat ending numbers) each
holding a list of tuples of measure numbers and measure objects that get this ending,
and return a list where contiguous endings should appear. Each element of the list is a
two-element tuple, where the first element is a list of measure objects that should have
a bracket and the second element is the repeat number.
Assumes that the list of measure numbers in each repeatEndings array is sorted.
For the sake of demo and testing, we will use strings instead of measure objects.
>>> repeatEndings = {1: [(5, 'm5a'), (6, 'm6a'), (17, 'm17'), (18, 'm18'),
... (19, 'm19'), (23, 'm23a')],
... 2: [(5, 'm5b'), (6, 'm6b'), (20, 'm20'), (21, 'm21'), (23, 'm23b')],
... 3: [(23, 'm23c')]}
>>> print(romanText.translate._consolidateRepeatEndings(repeatEndings))
[(['m5a', 'm6a'], 1), (['m17', 'm18', 'm19'], 1), (['m23a'], 1),
(['m5b', 'm6b'], 2), (['m20', 'm21'], 2), (['m23b'], 2), (['m23c'], 3)]
'''
returnList = []
for endingNumber in repeatEndings:
startMeasureNumber = None
lastMeasureNumber = None
measureList = []
for measureNumberUnderEnding, measureObject in repeatEndings[endingNumber]:
if startMeasureNumber is None:
startMeasureNumber = measureNumberUnderEnding
lastMeasureNumber = measureNumberUnderEnding
measureList.append(measureObject)
elif measureNumberUnderEnding > lastMeasureNumber + 1:
myTuple = (measureList, endingNumber)
returnList.append(myTuple)
startMeasureNumber = measureNumberUnderEnding
lastMeasureNumber = measureNumberUnderEnding
measureList = [measureObject]
else:
measureList.append(measureObject)
lastMeasureNumber = measureNumberUnderEnding
if startMeasureNumber is not None:
myTuple = (measureList, endingNumber)
returnList.append(myTuple)
return returnList
def _addRepeatsFromRepeatEndings(s, repeatEndings):
'''
Given a Stream and the repeatEndings dict, add repeats to the stream.
'''
consolidatedRepeats = _consolidateRepeatEndings(repeatEndings)
for repeatEndingTuple in consolidatedRepeats:
measureList, endingNumber = repeatEndingTuple[0], repeatEndingTuple[1]
rb = spanner.RepeatBracket(measureList, number=endingNumber)
rbOffset = measureList[0].getOffsetBySite(s)
# Adding repeat bracket to stream at beginning of repeated section.
# Maybe better at end?
s.insert(rbOffset, rb)
# should be 'if not max(endingNumbers)', but we can't tell that for each repeat.
if endingNumber == 1:
if measureList[-1].rightBarline is None:
measureList[-1].rightBarline = bar.Repeat(direction='end')
[docs]
def fixPickupMeasure(partObject):
# noinspection PyShadowingNames
'''
Fix a pickup measure if any.
We determine a pickup measure by being measure 0 and not having an RN
object at the beginning. Will also pad the last measure right and
cut the duration of the final chord to match.
Demonstration: an otherwise incorrect part
>>> p = stream.Part()
>>> m0 = stream.Measure()
>>> m0.number = 0
>>> k0 = key.Key('G')
>>> m0.insert(0, k0)
>>> m0.insert(0, meter.TimeSignature('4/4'))
>>> m0.insert(3, roman.RomanNumeral('I', k0, quarterLength=1.0))
>>> m1 = stream.Measure()
>>> m1.number = 1
>>> m1.append(roman.RomanNumeral('V', k0, quarterLength=4.0))
>>> m2 = stream.Measure()
>>> m2.number = 2
>>> m2.append(roman.RomanNumeral('I', k0, quarterLength=4.0))
>>> p.insert(0, m0)
>>> p.insert(4, m1)
>>> p.insert(8, m2)
After running fixPickupMeasure()
>>> romanText.translate.fixPickupMeasure(p)
>>> p.show('text')
{0.0} <music21.stream.Measure 0 offset=0.0>
{0.0} <music21.key.Key of G major>
{0.0} <music21.meter.TimeSignature 4/4>
{0.0} <music21.roman.RomanNumeral I in G major>
{1.0} <music21.stream.Measure 1 offset=1.0>
{0.0} <music21.roman.RomanNumeral V in G major>
{5.0} <music21.stream.Measure 2 offset=5.0>
{0.0} <music21.roman.RomanNumeral I in G major>
>>> m0.paddingLeft
3.0
>>> m2.paddingRight
1.0
>>> m2[roman.RomanNumeral].last().duration
<music21.duration.Duration 3.0>
'''
m0 = partObject.measure(0)
if m0 is None:
return
rnObjects = m0.getElementsByClass([roman.RomanNumeral, harmony.NoChord])
if not rnObjects:
return
if rnObjects[0].offset == 0:
return
leftPadding = rnObjects[0].offset
for el in m0:
if el.offset < leftPadding: # should be zero for Clefs, etc.
pass
else:
el.offset = el.offset - leftPadding
m0.paddingLeft = leftPadding
for el in partObject: # adjust all other measures etc. backwards
if el.offset > 0:
el.offset -= leftPadding
mLast = partObject.getElementsByClass(stream.Measure).last()
if mLast is m0:
return
lastRN = mLast.getElementsByClass([roman.RomanNumeral, harmony.NoChord]).last()
if lastRN and lastRN.duration.quarterLength > leftPadding:
curLastLength = mLast.duration.quarterLength
lastRN.duration.quarterLength -= curLastLength - leftPadding
mLast.paddingRight = curLastLength - leftPadding
with common.classTools.tempAttribute(partObject, 'autoSort', False):
# cast as list for speed and also temporarily disable sorting until all done.
partElements = list(partObject)
i = -1
while partElements[i] is not mLast:
# unprocessed metadata after last object
partElements[i].setOffsetBySite(partObject,
partElements[i].offset - mLast.paddingRight)
i -= 1
[docs]
def romanTextToStreamOpus(rtHandler, inputM21=None):
'''
The main processing routine for RomanText objects that may or may not
be multi movement.
Takes in a romanText.rtObjects.RTFile() object, or a string as rtHandler.
Runs `romanTextToStreamScore()` as its main work.
If inputM21 is None then it will create a Score or Opus object.
Return either a Score object, or, if a multi-movement work is defined, an
Opus object.
'''
if isinstance(rtHandler, str):
rtf = rtObjects.RTFile()
rtHandler = rtf.readstr(rtHandler) # return handler, processes tokens
if rtHandler.definesMovements(): # create an opus
if inputM21 is None:
s = stream.Opus()
else:
s = inputM21
# copy the common header to each of the sub-handlers
handlerBundles = rtHandler.splitByMovement(duplicateHeader=True)
# see if we have header information
for h in handlerBundles:
# print(h, len(h))
# append to opus
s.append(romanTextToStreamScore(h))
return s # an opus
else: # create a Score
return romanTextToStreamScore(rtHandler, inputM21=inputM21)
# ------------------------------------------------------------------------------
class TestSlow(unittest.TestCase): # pragma: no cover
'''
These tests are currently too slow to run every time.
'''
def testExternalA(self):
from music21.romanText import testFiles
for tf in testFiles.ALL:
rtf = rtObjects.RTFile()
rth = rtf.readstr(tf) # return handler, processes tokens
s = romanTextToStreamScore(rth)
s.show()
# noinspection SpellCheckingInspection
def testBasicA(self):
from music21.romanText import testFiles
for tf in testFiles.ALL:
rtf = rtObjects.RTFile()
rth = rtf.readstr(tf) # return handler, processes tokens
# will run romanTextToStreamScore on all but k273
s = romanTextToStreamOpus(rth)
s.show()
s = romanTextToStreamScore(testFiles.swv23)
self.assertEqual(s.metadata.composer, 'Heinrich Schutz')
# this is defined as a Piece tag, but shows up here as a title, after
# being set as an alternate title
self.assertEqual(s.metadata.title, 'Warum toben die Heiden, Psalmen Davids no. 2, SWV 23')
s = romanTextToStreamScore(testFiles.riemenschneider001)
self.assertEqual(s.metadata.composer, 'J. S. Bach')
self.assertEqual(s.metadata.title, 'Aus meines Herzens Grunde')
s = romanTextToStreamScore(testFiles.monteverdi_3_13)
self.assertEqual(s.metadata.composer, 'Claudio Monteverdi')
def testMeasureCopyingA(self):
from music21.romanText import testFiles
s = romanTextToStreamScore(testFiles.swv23)
mStream = s.parts[0].getElementsByClass(stream.Measure)
# the first four measures should all have the same content
rn1 = mStream[1].getElementsByClass(roman.RomanNumeral).first()
self.assertEqual([str(x) for x in rn1.pitches], ['D5', 'F#5', 'A5'])
self.assertEqual(str(rn1.figure), 'V')
rn2 = mStream[1].getElementsByClass(roman.RomanNumeral)[1]
self.assertEqual(str(rn2.figure), 'i')
# make sure that m2, m3, m4 have the same values
rn1 = mStream[2].getElementsByClass(roman.RomanNumeral)[0]
self.assertEqual(str(rn1.figure), 'V')
rn2 = mStream[2].getElementsByClass(roman.RomanNumeral)[1]
self.assertEqual(str(rn2.figure), 'i')
rn1 = mStream[3].getElementsByClass(roman.RomanNumeral)[0]
self.assertEqual(str(rn1.figure), 'V')
rn2 = mStream[3].getElementsByClass(roman.RomanNumeral)[1]
self.assertEqual(str(rn2.figure), 'i')
# test multiple measure copying
s = romanTextToStreamScore(testFiles.monteverdi_3_13)
mStream = s.parts[0].getElementsByClass(stream.Measure)
m1a = None
m2a = None
m3a = None
m1b = None
m2b = None
m3b = None
for m in mStream:
if m.number == 41: # m49-51 = m41-43
m1a = m
elif m.number == 42: # m49-51 = m41-43
m2a = m
elif m.number == 43: # m49-51 = m41-43
m3a = m
elif m.number == 49: # m49-51 = m41-43
m1b = m
elif m.number == 50: # m49-51 = m41-43
m2b = m
elif m.number == 51: # m49-51 = m41-43
m3b = m
rn = m1a.getElementsByClass(roman.RomanNumeral)[0]
self.assertEqual(str(rn.figure), 'IV')
rn = m1a.getElementsByClass(roman.RomanNumeral)[1]
self.assertEqual(str(rn.figure), 'I')
rn = m1b.getElementsByClass(roman.RomanNumeral)[0]
self.assertEqual(str(rn.figure), 'IV')
rn = m1b.getElementsByClass(roman.RomanNumeral)[1]
self.assertEqual(str(rn.figure), 'I')
rn = m2a.getElementsByClass(roman.RomanNumeral)[0]
self.assertEqual(str(rn.figure), 'I')
rn = m2a.getElementsByClass(roman.RomanNumeral)[1]
self.assertEqual(str(rn.figure), 'ii')
rn = m2b.getElementsByClass(roman.RomanNumeral)[0]
self.assertEqual(str(rn.figure), 'I')
rn = m2b.getElementsByClass(roman.RomanNumeral)[1]
self.assertEqual(str(rn.figure), 'ii')
rn = m3a.getElementsByClass(roman.RomanNumeral).first()
self.assertEqual(str(rn.figure), 'V/ii')
rn = m3b.getElementsByClass(roman.RomanNumeral).first()
self.assertEqual(str(rn.figure), 'V/ii')
def testMeasureCopyingB(self):
from music21 import converter
from music21.romanText import testFiles
s = converter.parse(testFiles.monteverdi_3_13)
m25 = s.measure(25)
rn = m25.flatten().getElementsByClass(roman.RomanNumeral)
self.assertEqual(rn[1].figure, 'III')
self.assertEqual(str(rn[1].key), 'd minor')
# TODO: this is getting the F#m even though the key and figure are
# correct
# self.assertEqual(str(rn[1].pitches), '[F4, A4, C5]')
# s.show()
def testOpus(self):
from music21.romanText import testFiles
o = romanTextToStreamOpus(testFiles.mozartK279)
self.assertEqual(o.scores[0].metadata.movementNumber, '1')
self.assertEqual(o.scores[0].metadata.composer, 'Mozart')
self.assertEqual(o.scores[1].metadata.movementNumber, '2')
self.assertEqual(o.scores[1].metadata.composer, 'Mozart')
self.assertEqual(o.scores[2].metadata.movementNumber, '3')
self.assertEqual(o.scores[2].metadata.composer, 'Mozart')
# test using converter.
from music21 import converter
s = converter.parse(testFiles.mozartK279)
self.assertTrue('Opus' in s.classes)
self.assertEqual(len(s.scores), 3)
# make sure a normal file is still a Score
s = converter.parse(testFiles.riemenschneider001)
self.assertTrue(isinstance(s, stream.Score))
class Test(unittest.TestCase):
def testMinor67set(self):
from music21.romanText import testFiles
s = romanTextToStreamScore(testFiles.testSetMinorRootParse)
chords = list(s[roman.RomanNumeral])
def pitchEqual(index, pitchStr):
ch = chords[index]
chPitches = ch.pitches
self.assertEqual(' '.join(p.name for p in chPitches), pitchStr)
pitchEqual(0, 'C E- G')
pitchEqual(1, 'B D F')
pitchEqual(3, 'G B D')
pitchEqual(4, 'A- C E-')
pitchEqual(7, 'B- D F')
pitchEqual(10, 'A C E')
def testPivotInCopyMultiple(self):
from music21 import converter
testCase = '''
m1 G: I
m2 I
m3 V D: I
m4 V
m5 G: I
m6-7 = m3-4
m8 I
'''
s = converter.parse(testCase, format='romanText')
m = s.measure(7).flatten()
self.assertEqual(m.getElementsByClass(roman.RomanNumeral).first().key.name, 'D major')
m = s.measure(8).flatten()
self.assertEqual(m.getElementsByClass(roman.RomanNumeral).first().key.name, 'D major')
def testPivotInCopyMultiple2(self):
'''
test whether a chord in a pivot situation outside of copying affects copying
'''
from music21 import converter
testCase = '''
m1 G: I
m2 V D: I
m3 G: IV
m4 V
m5 I
m6-7 = m4-5
m8 I
'''
s = converter.parse(testCase, format='romanText')
m = s.measure(5).flatten()
self.assertEqual(m.getElementsByClass(roman.RomanNumeral).first().key.name, 'G major')
def testPivotInCopySingle(self):
from music21 import converter
testCase = '''
m1 G: I
m2 I
m3 V D: I
m4 G: I
m5 = m3
m6 I
'''
s = converter.parse(testCase, format='romanText')
m = s.measure(6).flatten()
self.assertEqual(m.getElementsByClass(roman.RomanNumeral).first().key.name, 'D major')
def testSecondaryInCopyMultiple(self):
'''
test secondary dominants after copy
'''
testSecondaryInCopy = '''
Time Signature: 4/4
m1 g: i
m2 i6
m3 V7/v
m4 d: i
m5-6 = m2-3
m7 = m3
'''
s = romanTextToStreamScore(testSecondaryInCopy)
m = s.measure(6).flatten()
self.assertEqual(m.getElementsByClass(roman.RomanNumeral).first().pitchedCommonName,
'E-dominant seventh chord')
m = s.measure(7).flatten()
self.assertEqual(m.getElementsByClass(roman.RomanNumeral).first().pitchedCommonName,
'E-dominant seventh chord')
# s.show()
def testBasicB(self):
from music21.romanText import testFiles
unused_s = romanTextToStreamScore(testFiles.riemenschneider001)
# unused_s.show()
def testRomanTextString(self):
from music21 import converter
s = converter.parse('m1 KS1 I \n m2 V6/5 \n m3 I b3 V7 \n'
+ 'm4 KS-3 vi \n m5 a: i b3 V4/2 \n m6 I',
format='romantext')
rnStream = s.flatten().getElementsByClass(roman.RomanNumeral).stream()
self.assertEqual(rnStream[0].figure, 'I')
self.assertEqual(rnStream[1].figure, 'V6/5')
self.assertEqual(rnStream[2].figure, 'I')
self.assertEqual(rnStream[3].figure, 'V7')
self.assertEqual(rnStream[4].figure, 'vi')
self.assertEqual(rnStream[5].figure, 'i')
self.assertEqual(rnStream[6].figure, 'V4/2')
self.assertEqual(rnStream[7].figure, 'I')
rnStreamKey = s.flatten().getElementsByClass(key.KeySignature)
self.assertEqual(rnStreamKey[0].sharps, 1)
self.assertEqual(rnStreamKey[1].sharps, -3)
# s.show()
def testMeasureCopyingB(self):
from music21 import converter
from music21 import pitch
src = '''m1 G: IV || b3 d: III b4 ii
m2 v b2 III6 b3 iv6 b4 ii/o6/5
m3 i6/4 b3 V
m4-5 = m2-3
m6-7 = m4-5
'''
s = converter.parse(src, format='romantext')
rnStream = s.flatten().getElementsByClass(roman.RomanNumeral)
for elementNumber in [0, 6, 12]:
self.assertEqual(rnStream[elementNumber + 4].figure, 'III6')
self.assertEqual(str([str(p) for p in rnStream[elementNumber + 4].pitches]),
"['A4', 'C5', 'F5']")
x = rnStream[elementNumber + 4].pitches[2].accidental
if x is None:
x = pitch.Accidental('natural')
self.assertEqual(x.alter, 0)
self.assertEqual(rnStream[elementNumber + 5].figure, 'iv6')
self.assertEqual(str([str(p) for p in rnStream[elementNumber + 5].pitches]),
"['B-4', 'D5', 'G5']")
self.assertTrue(rnStream[elementNumber + 5].pitches[0].accidental.displayStatus)
def testNoChord(self):
from music21 import converter
from music21.harmony import NoChord
src = '''m1 G: IV || b3 d: III b4 NC
m2 b2 III6 b3 iv6 b4 ii/o6/5
m3 NC b3 G: V
'''
s = converter.parse(src, format='romantext')
p = s.parts[0]
m1 = p.getElementsByClass(stream.Measure).first()
r1 = m1.notesAndRests[-1]
self.assertIn('Rest', r1.classes)
self.assertEqual(r1.quarterLength, 1.0)
noChordObj = m1.getElementsByClass(harmony.Harmony).last()
self.assertIsInstance(noChordObj, NoChord)
m2 = p.getElementsByClass(stream.Measure)[1]
r2 = m2.notesAndRests[0]
self.assertIn('Rest', r2.classes)
self.assertEqual(r1.quarterLength, 1.0)
rn1 = m2.notesAndRests[1]
self.assertIn(roman.RomanNumeral, rn1.classSet)
# s.show()
def testUnprocessed(self):
from music21 import converter
from music21.romanText import translate
src = '''Note: Hello
m1 G: IV || b3 d: III b4 NC
varM1 I
Note: Hi
'''
s = converter.parse(src, format='romantext')
p = s.parts[0]
unprocessedElements = p[translate.RomanTextUnprocessedMetadata]
self.assertEqual(len(unprocessedElements), 3)
note1, var1, note2 = unprocessedElements
self.assertEqual(note1.tag, 'Note')
self.assertEqual(note2.tag, 'Note')
self.assertEqual(note1.data, 'Hello')
self.assertEqual(note2.data, 'Hi')
self.assertFalse(var1.tag)
self.assertIn(' I', var1.data)
def testUnprocessedWithAnacrusis(self):
from music21.romanText import translate
src = '''
Time Signature: 4/4
m0 b4 f: i
Note: Internal Note field after anacrusis.
m1 V
'''
s = translate.romanTextToStreamScore(src)
p = s.parts[0]
self.assertEqual(len(p), 3)
self.assertIsInstance(p[0], stream.Measure)
self.assertEqual(p[0].paddingLeft, 3.0)
self.assertIsInstance(p[1], translate.RomanTextUnprocessedMetadata)
self.assertEqual(p[1].offset, 0.0)
self.assertEqual(p[1].data, 'Internal Note field after anacrusis.')
self.assertIsInstance(p[2], stream.Measure)
self.assertEqual(p[2].offset, 1.0)
self.assertIsInstance(p[2][0], roman.RomanNumeral)
self.assertEqual(p[2].paddingRight, 1.0)
self.assertEqual(p[2].duration.quarterLength, 3.0)
def testSixthMinorParse(self):
from music21 import converter
src = '''SixthMinor: flat
m1 c: vi
'''
s = converter.parse(src, format='romantext')
p = s.parts[0]
ch0 = p.recurse().notes[0]
self.assertEqual(ch0.root().name, 'A-')
def testSetRTVersion(self):
src = '''RTVersion: 2.5
m1 C: I'''
rtf = rtObjects.RTFile()
rtHandler = rtf.readstr(src)
pt = PartTranslator()
pt.translateTokens(rtHandler.tokens)
self.assertEqual(pt.romanTextVersion, 2.5)
# gives warning, not raises
# src = '''RTVersion: XYZ
# m1 C: I'''
# rtf = rtObjects.RTFile()
# rtHandler = rtf.readstr(src)
# pt = PartTranslator()
# with self.assertRaises(RomanTextTranslateException):
# pt.translateTokens(rtHandler.tokens)
def testPivotChord(self):
from music21 import converter
src = '''m1 G: I b3 v d: i b4 V'''
s = converter.parse(src, format='romantext')
p = s.parts[0]
m1 = p.getElementsByClass(stream.Measure).first()
allRNs = m1.getElementsByClass(roman.RomanNumeral)
notPChord = allRNs[0]
pChord = allRNs[1]
self.assertEqual(pChord.key.tonic.step, 'G')
self.assertEqual(pChord.figure, 'v')
pivot = pChord.pivotChord
self.assertEqual(pivot.key.tonic.step, 'D')
self.assertEqual(pivot.figure, 'i')
self.assertIsNone(notPChord.pivotChord)
# s.show('text')
def testTimeSigChanges(self):
from music21 import converter
src = '''Time Signature: 4/4
m1 C: I
Time Signature: 2/4
m10 V
Time Signature: 4/4
m12 I
m14-25 = m1-12
'''
s = converter.parse(src, format='romantext')
p = s.parts[0]
m3 = p.getElementsByClass(stream.Measure)[2]
self.assertEqual(m3.getOffsetBySite(p), 8.0)
m10 = p.getElementsByClass(stream.Measure)[9]
self.assertEqual(m10.getOffsetBySite(p), 36.0)
m11 = p.getElementsByClass(stream.Measure)[10]
self.assertEqual(m11.getOffsetBySite(p), 38.0)
m12 = p.getElementsByClass(stream.Measure)[11]
self.assertEqual(m12.getOffsetBySite(p), 40.0)
m13 = p.getElementsByClass(stream.Measure)[12]
self.assertEqual(m13.getOffsetBySite(p), 44.0)
m16 = p.getElementsByClass(stream.Measure)[15]
self.assertEqual(m16.getOffsetBySite(p), 56.0)
m23 = p.getElementsByClass(stream.Measure)[22]
self.assertEqual(m23.getOffsetBySite(p), 84.0)
m24 = p.getElementsByClass(stream.Measure)[23]
self.assertEqual(m24.getOffsetBySite(p), 86.0)
m25 = p.getElementsByClass(stream.Measure)[24]
self.assertEqual(m25.getOffsetBySite(p), 88.0)
# testEndings (which was incomplete in any case) has been superseded by
# testRepeats below
def testTuplets(self):
from music21 import converter
c = converter.parse('m1 C: I b2.66 V', format='romantext')
n1 = c.flatten().notes[0]
n2 = c.flatten().notes[1]
self.assertEqual(n1.duration.quarterLength, common.opFrac(5 / 3))
self.assertEqual(n2.offset, common.opFrac(5 / 3))
self.assertEqual(n2.duration.quarterLength, common.opFrac(7 / 3))
c = converter.parse('TimeSignature: 6/8\nm1 C: I b2.66 V', format='romantext')
n1 = c.flatten().notes[0]
n2 = c.flatten().notes[1]
self.assertEqual(n1.duration.quarterLength, 5 / 2)
self.assertEqual(n2.offset, 5 / 2)
self.assertEqual(n2.duration.quarterLength, 1 / 2)
c = converter.parse('m1 C: I b2.66.5 V', format='romantext')
n1 = c.flatten().notes[0]
n2 = c.flatten().notes[1]
self.assertEqual(n1.duration.quarterLength, common.opFrac(11 / 6))
self.assertEqual(n2.offset, common.opFrac(11 / 6))
self.assertEqual(n2.duration.quarterLength, common.opFrac(13 / 6))
def testCopyEmptyMeasures(self) -> None:
from music21 import converter
empty_measures_with_copy = textwrap.dedent(''''
Time Signature: 2/4
m1 I
m2 V
m3 = m1
m4-5 = m1-2
''')
s = converter.parse(empty_measures_with_copy, format='romanText')
assert s.duration.quarterLength == 10
def testRepeats(self) -> None:
from music21 import converter
def _repeat_tester(
repeat_bar: bar.Repeat, direction: str, offset: float, mNumber: int
) -> None:
self.assertEqual(repeat_bar.direction, direction)
self.assertEqual(repeat_bar.offset, offset)
self.assertEqual(repeat_bar.activeSite.number, mNumber)
def _test_expanded(s: stream.Stream, quarterLength: float) -> None:
# NB repeat.Expander does not work on stream, only on part. Is this
# expected behavior?
p = next(s[stream.Part])
e = repeat.Expander(p)
self.assertTrue(e.repeatBarsAreCoherent())
p2 = e.process()
self.assertEqual(p2.quarterLength, quarterLength)
def _test_ending_contents(
rb: spanner.RepeatBracket, expectedMeasures: list[str]
) -> None:
measure_nos = [m.measureNumberWithSuffix() for m in rb[stream.Measure]]
self.assertEqual(measure_nos, expectedMeasures)
# Test simple repeats
simple_repeats = textwrap.dedent('''
Time Signature: 3/4
m1 ||: V
m2 I :||
''')
s = converter.parse(simple_repeats, format='romanText')
br_iter = s[bar.Repeat]
self.assertEqual(len(br_iter), 2)
start_repeat, end_repeat = br_iter
_repeat_tester(start_repeat, 'start', 0.0, 1)
_repeat_tester(end_repeat, 'end', 3.0, 2)
_test_expanded(s, 12.0)
single_bar_repeats = textwrap.dedent('''
Time Signature: 2/4
m1 ||: I :||
''')
s = converter.parse(single_bar_repeats, format='romanText')
br_iter = s[bar.Repeat]
self.assertEqual(len(br_iter), 2)
start_repeat, end_repeat = br_iter
_repeat_tester(start_repeat, 'start', 0.0, 1)
_repeat_tester(end_repeat, 'end', 2.0, 1)
_test_expanded(s, 4.0)
empty_bars_with_repeats = textwrap.dedent('''
Time Signature: 2/4
m1 I
m2 ||:
m3 :||
m4 ||: :||
''')
s = converter.parse(empty_bars_with_repeats, format='romanText')
br_iter = s[bar.Repeat]
self.assertEqual(len(br_iter), 4)
start_repeat1, end_repeat1, start_repeat2, end_repeat2 = br_iter
_repeat_tester(start_repeat1, 'start', 0.0, 2)
_repeat_tester(end_repeat1, 'end', 2.0, 3)
_repeat_tester(start_repeat2, 'start', 0.0, 4)
_repeat_tester(end_repeat2, 'end', 2.0, 4)
for measure in s[stream.Measure]:
rn_iter = measure[roman.RomanNumeral]
self.assertEqual(len(rn_iter), 1)
# mypy complains about the next line because
# RecursiveIterator.first() has X|None type, but we know
# it will not be None because we have just asserted that rn_iter
# has length 1
self.assertEqual(rn_iter.first().figure, 'I') # type: ignore
_test_expanded(s, 14.0)
three_endings = textwrap.dedent('''
Time Signature: 3/4
m1 ||: I
m2a IV :||
m2b V :||
m2c I
''')
s = converter.parse(three_endings, format='romanText')
br_iter = s[bar.Repeat]
self.assertEqual(len(br_iter), 3)
start_repeat, end_repeat1, end_repeat2 = br_iter
_repeat_tester(start_repeat, 'start', 0.0, 1)
_repeat_tester(end_repeat1, 'end', 3.0, 2)
_repeat_tester(end_repeat2, 'end', 3.0, 2)
rb_iter = s[spanner.RepeatBracket]
self.assertEqual(len(rb_iter), 3)
first_ending, second_ending, third_ending = rb_iter
_test_ending_contents(first_ending, ['2a'])
_test_ending_contents(second_ending, ['2b'])
_test_ending_contents(third_ending, ['2c'])
_test_expanded(s, 18.0)
more_complex_example = textwrap.dedent('''
TimeSignature: 3/4
m1 ||: I
m2a IV
m3a V :||
m2b V :||
m2c IV
m3c V
m4 I
''')
s = converter.parse(more_complex_example, format='romanText')
br_iter = s[bar.Repeat]
self.assertEqual(len(br_iter), 3)
start_repeat, end_repeat1, end_repeat2 = s[bar.Repeat]
_repeat_tester(start_repeat, 'start', 0.0, 1)
_repeat_tester(end_repeat1, 'end', 3.0, 3)
_repeat_tester(end_repeat2, 'end', 3.0, 2)
rb_iter = s[spanner.RepeatBracket]
self.assertEqual(len(rb_iter), 3)
first_ending, second_ending, third_ending = rb_iter
_test_ending_contents(first_ending, ['2a', '3a'])
_test_ending_contents(second_ending, ['2b'])
_test_ending_contents(third_ending, ['2c', '3c'])
_test_expanded(s, 27.0)
# ------------------------------------------------------------------------------
# define presented order in documentation
_DOC_ORDER: list[type] = []
if __name__ == '__main__':
import music21
music21.mainTest(Test) # , TestSlow)