1# -*- coding: utf-8 -*-
2# ------------------------------------------------------------------------------
3# Name:         romanText/translate.py
4# Purpose:      Translation routines for roman numeral analysis text files
5#
6# Authors:      Christopher Ariza
7#               Michael Scott Cuthbert
8#
9# Copyright:    Copyright © 2011-2012, 2016, 2019 Michael Scott Cuthbert and the music21 Project
10# License:      BSD, see license.txt
11# ------------------------------------------------------------------------------
12'''
13Translation routines for roman numeral analysis text files, as defined
14and demonstrated by Dmitri Tymoczko.  Also used for the ClercqTemperley
15format which is similar but a little different.
16
17This module is really only needed for people extending the parser,
18for others it's simple to get Harmony, RomanNumeral, Key (or KeySignature)
19and other objects out of an rntxt file by running this:
20
21
22>>> monteverdi = corpus.parse('monteverdi/madrigal.3.1.rntxt')
23>>> monteverdi.show('text')
24{0.0} <music21.metadata.Metadata object at 0x...>
25{0.0} <music21.stream.Part ...>
26    {0.0} <music21.stream.Measure 1 offset=0.0>
27        {0.0} <music21.key.KeySignature of 1 flat>
28        {0.0} <music21.meter.TimeSignature 4/4>
29        {0.0} <music21.roman.RomanNumeral vi in F major>
30        {3.0} <music21.roman.RomanNumeral V[no3] in F major>
31    {4.0} <music21.stream.Measure 2 offset=4.0>
32        {0.0} <music21.roman.RomanNumeral I in F major>
33        {3.0} <music21.roman.RomanNumeral IV in F major>
34    ...
35
36Then the stream can be analyzed with something like this, storing
37the data to make a histogram of scale degree usage within a key:
38
39>>> degreeDictionary = {}
40>>> for el in monteverdi.recurse():
41...    if isinstance(el, roman.RomanNumeral):
42...         print(f'{el.figure} {el.key}')
43...         for p in el.pitches:
44...              degree, accidental = el.key.getScaleDegreeAndAccidentalFromPitch(p)
45...              if accidental is None:
46...                   degreeString = str(degree)
47...              else:
48...                   degreeString = str(degree) + str(accidental.modifier)
49...              if degreeString not in degreeDictionary:
50...                   degreeDictionary[degreeString] = 1
51...              else:
52...                   degreeDictionary[degreeString] += 1
53...              degTuple = (str(p), degreeString)
54...              print(degTuple)
55    vi F major
56    ('D5', '6')
57    ('F5', '1')
58    ('A5', '3')
59    V[no3] F major
60    ('C5', '5')
61    ('G5', '2')
62    I F major
63    ('F4', '1')
64    ('A4', '3')
65    ('C5', '5')
66    ...
67    V6 g minor
68    ('F#5', '7#')
69    ('A5', '2')
70    ('D6', '5')
71    i g minor
72    ('G4', '1')
73    ('B-4', '3')
74    ('D5', '5')
75    ...
76
77Now if we'd like we can get a Histogram of the data.
78It's a little complex, but worth seeing in full:
79
80>>> import operator
81>>> histogram = graph.primitives.GraphHistogram()
82>>> i = 0
83>>> data = []
84>>> xLabels = []
85>>> values = []
86>>> ddList = list(degreeDictionary.items())
87>>> for deg,value in sorted(ddList, key=operator.itemgetter(1), reverse=True):
88...    data.append((i, degreeDictionary[deg]), )
89...    xLabels.append((i+.5, deg), )
90...    values.append(degreeDictionary[deg])
91...    i += 1
92>>> histogram.data = data
93
94
95These commands give nice labels for the data; optional:
96
97>>> histogram.setIntegerTicksFromData(values, 'y')
98>>> histogram.setTicks('x', xLabels)
99>>> histogram.setAxisLabel('x', 'ScaleDegree')
100
101Now generate the histogram:
102
103>>> #_DOCS_HIDE histogram.process()
104
105.. image:: images/romanTranslatePitchDistribution.*
106    :width: 600
107
108
109OMIT_FROM_DOCS
110
111>>> x = converter.parse('romantext: m1 a: VI')
112>>> [str(p) for p in x[roman.RomanNumeral].first().pitches]
113['F5', 'A5', 'C6']
114
115>>> x = converter.parse('romantext: m1 a: vi')
116>>> [str(p) for p in x[roman.RomanNumeral].first().pitches]
117['F#5', 'A5', 'C#6']
118
119>>> [str(p) for p in
120...  converter.parse('romantext: m1 a: vio'
121...                  )[roman.RomanNumeral].first().pitches]
122['F#5', 'A5', 'C6']
123'''
124import copy
125import traceback
126import unittest
127
128from music21 import bar
129from music21 import base
130from music21 import common
131from music21 import exceptions21
132from music21 import harmony
133from music21 import key
134from music21 import metadata
135from music21 import meter
136from music21 import note
137from music21 import roman
138from music21 import stream
139from music21 import tie
140from music21.romanText import rtObjects
141
142from music21 import environment
143_MOD = 'romanText.translate'
144environLocal = environment.Environment(_MOD)
145
146ROMANTEXT_VERSION = 1.0
147
148
149USE_RN_CACHE = False
150# Not currently using rnCache because of problems with PivotChords,
151# See mail from Dmitri, 30 September 2014
152
153# ------------------------------------------------------------------------------
154
155
156class RomanTextTranslateException(exceptions21.Music21Exception):
157    pass
158
159
160class RomanTextUnprocessedToken(base.ElementWrapper):
161    pass
162
163
164class RomanTextUnprocessedMetadata(base.Music21Object):
165    def __init__(self, tag='', data=''):
166        super().__init__()
167        self.tag = tag
168        self.data = data
169
170    def _reprInternal(self) -> str:
171        return f'{self.tag}: {self.data}'
172
173
174def _copySingleMeasure(t, p, kCurrent):
175    '''
176    Given a RomanText token, a Part used as the current container,
177    and the current Key, return a Measure copied from the past of the Part.
178
179    This is used in cases of definitions such as:
180    m23=m21
181    '''
182    m = None
183    # copy from a past location; need to change key
184    # environLocal.printDebug(['calling _copySingleMeasure()'])
185    targetNumber, unused_targetRepeat = t.getCopyTarget()
186    if len(targetNumber) > 1:  # pragma: no cover
187        # this is an encoding error
188        raise RomanTextTranslateException(
189            'a single measure cannot define a copy operation for multiple measures')
190    # TODO: ignoring repeat letters
191    target = targetNumber[0]
192    for mPast in p.getElementsByClass('Measure'):
193        if mPast.number == target:
194            try:
195                m = copy.deepcopy(mPast)
196            except TypeError:  # pragma: no cover
197                raise RomanTextTranslateException(
198                    f'Failed to copy measure {mPast.number}:'
199                    + ' did you perhaps parse an RTOpus object with romanTextToStreamScore '
200                    + 'instead of romanTextToStreamOpus?')
201            m.number = t.number[0]
202            # update all keys
203            for rnPast in m.getElementsByClass('RomanNumeral'):
204                if kCurrent is None:  # pragma: no cover
205                    # should not happen
206                    raise RomanTextTranslateException(
207                        'attempting to copy a measure but no past key definitions are found')
208                if rnPast.editorial.get('followsKeyChange'):
209                    kCurrent = rnPast.key
210                elif rnPast.pivotChord is not None:
211                    kCurrent = rnPast.pivotChord.key
212                else:
213                    rnPast.key = kCurrent
214                if rnPast.secondaryRomanNumeral is not None:
215                    newRN = roman.RomanNumeral(rnPast.figure, kCurrent)
216                    newRN.duration = copy.deepcopy(rnPast.duration)
217                    newRN.lyrics = copy.deepcopy(rnPast.lyrics)
218                    m.replace(rnPast, newRN)
219
220            break
221    return m, kCurrent
222
223
224def _copyMultipleMeasures(t, p, kCurrent):
225    '''
226    Given a RomanText token for a RTMeasure, a
227    Part used as the current container, and the current Key,
228    return a Measure range copied from the past of the Part.
229
230    This is used for cases such as:
231    m23-25 = m20-22
232    '''
233    # the key provided needs to be the current key
234    # environLocal.printDebug(['calling _copyMultipleMeasures()'])
235
236    targetNumbers, unused_targetRepeat = t.getCopyTarget()
237    if len(targetNumbers) == 1:   # pragma: no cover
238        # this is an encoding error
239        raise RomanTextTranslateException('a multiple measure range cannot copy a single measure')
240    # TODO: ignoring repeat letters
241    targetStart = targetNumbers[0]
242    targetEnd = targetNumbers[1]
243
244    if t.number[1] - t.number[0] != targetEnd - targetStart:  # pragma: no cover
245        raise RomanTextTranslateException(
246            'both the source and destination sections need to have the same number of measures')
247    if t.number[0] < targetEnd:  # pragma: no cover
248        raise RomanTextTranslateException(
249            'the source section cannot overlap with the destination section')
250
251    measures = []
252    for mPast in p.getElementsByClass('Measure'):
253        if mPast.number in range(targetStart, targetEnd + 1):
254            try:
255                m = copy.deepcopy(mPast)
256            except TypeError:  # pragma: no cover
257                raise RomanTextTranslateException(
258                    'Failed to copy measure {0} to measure range {1}-{2}: '.format(
259                        mPast.number, targetStart, targetEnd)
260                    + 'did you perhaps parse an RTOpus object with romanTextToStreamScore '
261                    + 'instead of romanTextToStreamOpus?')
262
263            m.number = t.number[0] + mPast.number - targetStart
264            measures.append(m)
265            # update all keys
266            allRNs = list(m.getElementsByClass('RomanNumeral'))
267            for rnPast in allRNs:
268                if kCurrent is None:  # pragma: no cover
269                    # should not happen
270                    raise RomanTextTranslateException(
271                        'attempting to copy a measure but no past key definitions are found')
272                if rnPast.editorial.get('followsKeyChange'):
273                    kCurrent = rnPast.key
274                elif rnPast.pivotChord is not None:
275                    kCurrent = rnPast.pivotChord.key
276                else:
277                    rnPast.key = kCurrent
278                if rnPast.secondaryRomanNumeral is not None:
279                    newRN = roman.RomanNumeral(rnPast.figure, kCurrent)
280                    newRN.duration = copy.deepcopy(rnPast.duration)
281                    newRN.lyrics = copy.deepcopy(rnPast.lyrics)
282                    m.replace(rnPast, newRN)
283
284        if mPast.number == targetEnd:
285            break
286    return measures, kCurrent
287
288
289def _getKeyAndPrefix(rtKeyOrString):
290    '''Given an RTKey specification, return the Key and a string prefix based
291    on the tonic:
292
293    >>> romanText.translate._getKeyAndPrefix('c')
294    (<music21.key.Key of c minor>, 'c: ')
295    >>> romanText.translate._getKeyAndPrefix('F#')
296    (<music21.key.Key of F# major>, 'F#: ')
297    >>> romanText.translate._getKeyAndPrefix('Eb')
298    (<music21.key.Key of E- major>, 'E-: ')
299    >>> romanText.translate._getKeyAndPrefix('Bb')
300    (<music21.key.Key of B- major>, 'B-: ')
301    >>> romanText.translate._getKeyAndPrefix('bb')
302    (<music21.key.Key of b- minor>, 'b-: ')
303    >>> romanText.translate._getKeyAndPrefix('b#')
304    (<music21.key.Key of b# minor>, 'b#: ')
305    '''
306    if isinstance(rtKeyOrString, str):
307        rtKeyOrString = key.convertKeyStringToMusic21KeyString(rtKeyOrString)
308        k = key.Key(rtKeyOrString)
309    else:
310        k = rtKeyOrString.getKey()
311    tonicName = k.tonic.name
312    if k.mode == 'minor':
313        tonicName = tonicName.lower()
314    prefix = tonicName + ': '
315    return k, prefix
316
317
318# Cache each of the created keys so that we don't recreate them.
319_rnKeyCache = {}
320
321
322class PartTranslator:
323    '''
324    A refactoring of the previously massive romanTextToStreamScore function
325    to allow for more fine-grained testing (eventually), and to
326    get past the absurdly high number of nested blocks (the previous translator
327    was written under severe time constraints).
328    '''
329
330    def __init__(self, md=None):
331        if md is None:
332            md = metadata.Metadata()
333        self.md = md  # global metadata object
334        self.p = stream.Part()
335
336        self.romanTextVersion = ROMANTEXT_VERSION
337
338        # ts indication are found in header, and also found elsewhere
339        self.tsCurrent = meter.TimeSignature('4/4')  # create default 4/4
340        self.tsAtTimeOfLastChord = self.tsCurrent
341        self.tsSet = False  # store if set to a measure
342        self.lastMeasureToken = None
343        self.lastMeasureNumber = 0
344        self.previousRn = None
345        self.keySigCurrent = None
346        self.setKeySigFromFirstKeyToken = True  # set a keySignature
347        self.foundAKeySignatureSoFar = False
348        self.kCurrent, unused_prefixLyric = _getKeyAndPrefix('C')  # default if none defined
349        self.prefixLyric = ''
350
351        self.sixthMinor = roman.Minor67Default.CAUTIONARY
352        self.seventhMinor = roman.Minor67Default.CAUTIONARY
353
354        self.repeatEndings = {}
355
356        # reset for each measure
357        self.currentMeasureToken = None
358        self.previousChordInMeasure = None
359        self.pivotChordPossible = False
360        self.numberOfAtomsInCurrentMeasure = 0
361        self.setKeyChangeToken = False
362        self.currentOffsetInMeasure = 0.0
363
364    def translateTokens(self, tokens):
365        for t in tokens:
366            try:
367                self.translateOneLineToken(t)
368            except Exception:  # pylint: disable=broad-except
369                tracebackMessage = traceback.format_exc()
370                raise RomanTextTranslateException(
371                    f'At line {t.lineNumber} for token {t}, '
372                    + f'an exception was raised: \n{tracebackMessage}')
373
374        p = self.p
375        p.coreElementsChanged()
376        fixPickupMeasure(p)
377        p.makeBeams(inPlace=True)
378        p.makeAccidentals(inPlace=True)
379        _addRepeatsFromRepeatEndings(p, self.repeatEndings)  # 1st and second endings...
380        return p
381
382    def translateOneLineToken(self, t):
383        # noinspection SpellCheckingInspection
384        '''
385        Translates one token t and set the current settings.
386
387        A token in this case consists of an entire line's worth.
388        It might be a token such as 'Title: Neko Funjatta' or
389        a composite token such as 'm23 b4 IV6'
390        '''
391        md = self.md
392        # environLocal.printDebug(['token', t])
393
394        # most common case first...
395        if t.isMeasure():
396            self.translateMeasureLineToken(t)
397
398        elif t.isTitle():
399            md.title = t.data
400
401        elif t.isWork():
402            md.alternativeTitle = t.data
403
404        elif t.isPiece():
405            md.alternativeTitle = t.data
406
407        elif t.isComposer():
408            md.composer = t.data
409
410        elif t.isMovement():
411            md.movementNumber = t.data
412
413        elif t.isTimeSignature():
414            try:
415                self.tsCurrent = meter.TimeSignature(t.data)
416                self.tsSet = False
417            except exceptions21.Music21Exception:  # pragma: no cover
418                environLocal.warn(f'Could not parse TimeSignature tag: {t.data!r}')
419
420            # environLocal.printDebug(['tsCurrent:', tsCurrent])
421
422        elif t.isKeySignature():
423            self.parseKeySignatureTag(t)
424
425        elif t.isSixthMinor() or t.isSeventhMinor():
426            self.setMinorRootParse(t)
427
428        elif t.isVersion():
429            try:
430                self.romanTextVersion = float(t.data)
431            except ValueError:  # pragma: no cover
432                environLocal.warn(f'Could not parse RTVersion tag: {t.data!r}')
433
434        elif isinstance(t, rtObjects.RTTagged):
435            otherMetadata = RomanTextUnprocessedMetadata(t.tag, t.data)
436            self.p.append(otherMetadata)
437
438        else:  # pragma: no cover
439            unprocessed = RomanTextUnprocessedToken(t)
440            self.p.append(unprocessed)
441
442    def setMinorRootParse(self, t):
443        '''
444        Set Roman Numeral parsing standards from a token.
445
446        >>> pt = romanText.translate.PartTranslator()
447        >>> pt.sixthMinor
448        <Minor67Default.CAUTIONARY: 2>
449
450        >>> tag = romanText.rtObjects.RTTagged('SixthMinor: Flat')
451        >>> tag.isSixthMinor()
452        True
453        >>> pt.setMinorRootParse(tag)
454        >>> pt.sixthMinor
455        <Minor67Default.FLAT: 4>
456
457        Harmonic sets to FLAT for sixth and SHARP for seventh
458
459        >>> for config in 'flat sharp quality cautionary harmonic'.split():
460        ...     tag = romanText.rtObjects.RTTagged('Seventh Minor: ' + config)
461        ...     pt.setMinorRootParse(tag)
462        ...     print(pt.seventhMinor)
463        Minor67Default.FLAT
464        Minor67Default.SHARP
465        Minor67Default.QUALITY
466        Minor67Default.CAUTIONARY
467        Minor67Default.SHARP
468
469        >>> tag = romanText.rtObjects.RTTagged('Sixth Minor: harmonic')
470        >>> pt.setMinorRootParse(tag)
471        >>> print(pt.sixthMinor)
472        Minor67Default.FLAT
473
474
475        Unknown settings raise a `RomanTextTranslateException`
476
477        >>> tag = romanText.rtObjects.RTTagged('Seventh Minor: asdf')
478        >>> pt.setMinorRootParse(tag)
479        Traceback (most recent call last):
480        music21.romanText.translate.RomanTextTranslateException:
481            Cannot parse setting vi or vii parsing: 'asdf'
482        '''
483        tData = t.data.lower()
484        if tData == 'flat':
485            tEnum = roman.Minor67Default.FLAT
486        elif tData == 'sharp':
487            tEnum = roman.Minor67Default.SHARP
488        elif tData == 'quality':
489            tEnum = roman.Minor67Default.QUALITY
490        elif tData in ('courtesy', 'cautionary'):
491            tEnum = roman.Minor67Default.CAUTIONARY
492        elif tData == 'harmonic':
493            if t.isSixthMinor():
494                tEnum = roman.Minor67Default.FLAT
495            else:
496                tEnum = roman.Minor67Default.SHARP
497        else:
498            raise RomanTextTranslateException(
499                f'Cannot parse setting vi or vii parsing: {tData!r}')
500
501        if t.isSixthMinor():
502            self.sixthMinor = tEnum
503        else:
504            self.seventhMinor = tEnum
505
506    def translateMeasureLineToken(self, t):
507        '''
508        Translate a measure token consisting of a single line such as::
509
510            m21 b3 V b4 C: IV
511
512        Or it might be a variant measure, or a copy instruction.
513        '''
514        p = self.p
515        skipsPriorMeasures = ((t.number[0] > self.lastMeasureNumber + 1)
516                              and (self.previousRn is not None))
517        isSingleMeasureCopy = (len(t.number) == 1 and t.isCopyDefinition)
518        isMultipleMeasureCopy = (len(t.number) > 1)
519
520        # environLocal.printDebug(['handling measure token:', t])
521        # if t.number[0] % 10 == 0:
522        #    print('at number ' + str(t.number[0]))
523        if t.variantNumber is not None:
524            # TODO(msc): parse variant numbers
525            # environLocal.printDebug([f' skipping variant: {t}'])
526            return
527        if t.variantLetter is not None:
528            # TODO(msc): parse variant letters
529            # environLocal.printDebug([f' skipping variant: {t}'])
530            return
531
532        # if this measure number is more than 1 greater than the last
533        # defined measure number, and the previous chord is not None,
534        # then fill with copies of the last-defined measure
535        if skipsPriorMeasures:
536            self.fillToMeasureToken(t)
537
538        # create a new measure or copy a past measure
539        if isSingleMeasureCopy:  # if not a range
540            p.coreElementsChanged()
541            m, self.kCurrent = _copySingleMeasure(t, p, self.kCurrent)
542            p.coreAppend(m)
543            self.lastMeasureNumber = m.number
544            self.lastMeasureToken = t
545            romans = m.getElementsByClass(roman.RomanNumeral)
546            if romans:
547                self.previousRn = romans[-1]
548
549        elif isMultipleMeasureCopy:
550            p.coreElementsChanged()
551            measures, self.kCurrent = _copyMultipleMeasures(t, p, self.kCurrent)
552            p.append(measures)  # appendCore does not work with list
553            self.lastMeasureNumber = measures[-1].number
554            self.lastMeasureToken = t
555            romans = measures[-1].getElementsByClass(roman.RomanNumeral)
556            if romans:
557                self.previousRn = romans[-1]
558
559        else:
560            m = self.translateSingleMeasure(t)
561            p.coreAppend(m)
562
563    def fillToMeasureToken(self, t):
564        '''
565        Create a series of measures which extend the previous RN until the measure number
566        implied by t.
567        '''
568        p = self.p
569        for i in range(self.lastMeasureNumber + 1, t.number[0]):
570            mFill = stream.Measure()
571            mFill.number = i
572            if self.previousRn is not None:
573                newRn = copy.deepcopy(self.previousRn)
574                newRn.lyric = ''
575                # set to entire bar duration and tie
576                newRn.duration = copy.deepcopy(self.tsAtTimeOfLastChord.barDuration)
577                if self.previousRn.tie is None:
578                    self.previousRn.tie = tie.Tie('start')
579                else:
580                    self.previousRn.tie.type = 'continue'
581                # set to stop for now; may extend on next iteration
582                newRn.tie = tie.Tie('stop')
583                self.previousRn = newRn
584                mFill.append(newRn)
585            appendMeasureToRepeatEndingsDict(self.lastMeasureToken,
586                                             mFill,
587                                             self.repeatEndings, i)
588            p.coreAppend(mFill)
589        self.lastMeasureNumber = t.number[0] - 1
590        self.lastMeasureToken = t
591
592    def parseKeySignatureTag(self, t):
593        '''
594        Parse a key signature tag which has already been determined to
595        be a key signature.
596
597        >>> tag = romanText.rtObjects.RTTagged('KeySignature: -4')
598        >>> tag.isKeySignature()
599        True
600        >>> tag.data
601        '-4'
602
603        >>> pt = romanText.translate.PartTranslator()
604        >>> pt.keySigCurrent is None
605        True
606        >>> pt.setKeySigFromFirstKeyToken
607        True
608        >>> pt.foundAKeySignatureSoFar
609        False
610
611        >>> pt.parseKeySignatureTag(tag)
612        >>> pt.keySigCurrent
613        <music21.key.KeySignature of 4 flats>
614        >>> pt.setKeySigFromFirstKeyToken
615        False
616        >>> pt.foundAKeySignatureSoFar
617        True
618
619        >>> tag = romanText.rtObjects.RTTagged('KeySignature: xyz')
620        >>> pt.parseKeySignatureTag(tag)
621        Traceback (most recent call last):
622        music21.romanText.translate.RomanTextTranslateException:
623            Cannot parse key signature: 'xyz'
624        '''
625        data = t.data
626        if data == '':
627            self.keySigCurrent = key.KeySignature(0)
628        elif data == 'Bb':
629            self.keySigCurrent = key.KeySignature(-1)
630        else:
631            try:
632                dataVal = int(data)
633                self.keySigCurrent = key.KeySignature(dataVal)
634            except ValueError:
635                raise RomanTextTranslateException(f'Cannot parse key signature: {data!r}')
636        self.setKeySigFromFirstKeyToken = False
637        # environLocal.printDebug(['keySigCurrent:', keySigCurrent])
638        self.foundAKeySignatureSoFar = True
639
640    def translateSingleMeasure(self, measureToken):
641        '''
642        Given a measureToken, return a `stream.Measure` object with
643        the appropriate atoms set.
644        '''
645        self.currentMeasureToken = measureToken
646        m = stream.Measure()
647        m.number = measureToken.number[0]
648        appendMeasureToRepeatEndingsDict(measureToken, m, self.repeatEndings)
649        self.lastMeasureNumber = measureToken.number[0]
650        self.lastMeasureToken = measureToken
651
652        if not self.tsSet:
653            m.timeSignature = self.tsCurrent
654            self.tsSet = True  # only set when changed
655        if not self.setKeySigFromFirstKeyToken and self.keySigCurrent is not None:
656            m.insert(0, self.keySigCurrent)
657            self.setKeySigFromFirstKeyToken = True  # only set when changed
658
659        self.currentOffsetInMeasure = 0.0  # start offsets at zero
660        self.previousChordInMeasure = None
661        self.pivotChordPossible = False
662        self.numberOfAtomsInCurrentMeasure = len(measureToken.atoms)
663        # first RomanNumeral object after a key change should have this set to True
664        self.setKeyChangeToken = False
665
666        for i, a in enumerate(measureToken.atoms):
667            isLastAtomInMeasure = (i == self.numberOfAtomsInCurrentMeasure - 1)
668            self.translateSingleMeasureAtom(a, m, isLastAtomInMeasure=isLastAtomInMeasure)
669
670        # may need to adjust duration of last chord added
671        if self.tsCurrent is not None:
672            self.previousRn.quarterLength = (self.tsCurrent.barDuration.quarterLength
673                                                - self.currentOffsetInMeasure)
674        m.coreElementsChanged()
675        return m
676
677    def translateSingleMeasureAtom(self, a, m, *, isLastAtomInMeasure=False):
678        '''
679        Translate a single atom in a measure token.
680
681        a is the Atom
682        m is a `stream.Measure` object.
683
684        Uses coreInsert and coreAppend methods, so must have `m.coreElementsChanged()`
685        called afterwards.
686        '''
687        if (isinstance(a, rtObjects.RTKey)
688                or (self.foundAKeySignatureSoFar is False
689                    and isinstance(a, rtObjects.RTAnalyticKey))):
690            self.setAnalyticKey(a)
691            # insert at beginning of measure if at beginning
692            #    -- for things like pickups.
693            if m.number <= 1:
694                m.coreInsert(0, self.kCurrent)
695            else:
696                m.coreInsert(self.currentOffsetInMeasure, self.kCurrent)
697            self.foundAKeySignatureSoFar = True
698
699        elif isinstance(a, rtObjects.RTKeySignature):
700            try:  # this sets the keysignature but not the prefix text
701                thisSig = a.getKeySignature()
702            except (exceptions21.Music21Exception, ValueError):  # pragma: no cover
703                raise RomanTextTranslateException(
704                    f'cannot get key from {a.src} in line {self.currentMeasureToken.src}')
705            # insert at beginning of measure if at beginning
706            #     -- for things like pickups.
707            if m.number <= 1:
708                m.coreInsert(0, thisSig)
709            else:
710                m.coreInsert(self.currentOffsetInMeasure, thisSig)
711            self.foundAKeySignatureSoFar = True
712
713        elif isinstance(a, rtObjects.RTAnalyticKey):
714            self.setAnalyticKey(a)
715
716        elif isinstance(a, rtObjects.RTBeat):
717            # set new offset based on beat
718            try:
719                newOffset = a.getOffset(self.tsCurrent)
720            except ValueError:  # pragma: no cover
721                raise RomanTextTranslateException(
722                    'cannot properly get an offset from '
723                    + f'beat data {a.src}'
724                    + 'under timeSignature {0} in line {1}'.format(
725                        self.tsCurrent,
726                        self.currentMeasureToken.src))
727            if (self.previousChordInMeasure is None
728                    and self.previousRn is not None
729                    and newOffset > 0):
730                # setting a new beat before giving any chords
731                firstChord = copy.deepcopy(self.previousRn)
732                firstChord.quarterLength = newOffset
733                firstChord.lyric = ''
734                if self.previousRn.tie is None:
735                    self.previousRn.tie = tie.Tie('start')
736                else:
737                    self.previousRn.tie.type = 'continue'
738                firstChord.tie = tie.Tie('stop')
739                self.previousRn = firstChord
740                self.previousChordInMeasure = firstChord
741                m.coreInsert(0, firstChord)
742            self.pivotChordPossible = False
743            self.currentOffsetInMeasure = newOffset
744
745        elif isinstance(a, rtObjects.RTNoChord):
746            # use source to evaluation roman
747            self.tsAtTimeOfLastChord = self.tsCurrent
748            cs = harmony.NoChord()
749            m.coreInsert(self.currentOffsetInMeasure, cs)
750
751            rn = note.Rest()
752            if self.pivotChordPossible is False:
753                # probably best to find duration
754                if self.previousChordInMeasure is None:
755                    pass  # use default duration
756                else:  # update duration of previous chord in Measure
757                    oPrevious = self.previousChordInMeasure.getOffsetBySite(m)
758                    newQL = self.currentOffsetInMeasure - oPrevious
759                    if newQL <= 0:  # pragma: no cover
760                        raise RomanTextTranslateException(
761                            f'too many notes in this measure: {self.currentMeasureToken.src}')
762                    self.previousChordInMeasure.quarterLength = newQL
763                self.prefixLyric = ''
764                m.coreInsert(self.currentOffsetInMeasure, rn)
765                self.previousChordInMeasure = rn
766                self.previousRn = rn
767                self.pivotChordPossible = False
768
769        elif isinstance(a, rtObjects.RTChord):
770            self.processRTChord(a, m, self.currentOffsetInMeasure)
771        elif isinstance(a, rtObjects.RTRepeat):
772            if self.currentOffsetInMeasure == 0:
773                if isinstance(a, rtObjects.RTRepeatStart):
774                    m.leftBarline = bar.Repeat(direction='start')
775                else:
776                    rtt = RomanTextUnprocessedToken(a)
777                    m.coreInsert(self.currentOffsetInMeasure, rtt)
778            elif (self.tsCurrent is not None
779                    and (self.tsCurrent.barDuration.quarterLength == self.currentOffsetInMeasure
780                         or isLastAtomInMeasure)):
781                if isinstance(a, rtObjects.RTRepeatStop):
782                    m.rightBarline = bar.Repeat(direction='end')
783                else:
784                    rtt = RomanTextUnprocessedToken(a)
785                    m.coreInsert(self.currentOffsetInMeasure, rtt)
786            else:  # mid measure repeat signs
787                rtt = RomanTextUnprocessedToken(a)
788                m.coreInsert(self.currentOffsetInMeasure, rtt)
789
790        else:
791            rtt = RomanTextUnprocessedToken(a)
792            m.coreInsert(self.currentOffsetInMeasure, rtt)
793            # environLocal.warn(f' Got an unknown token: {a}')
794
795    def processRTChord(self, a, m, currentOffset):
796        '''
797        Process a single RTChord atom.
798        '''
799        # use source to evaluation roman
800        self.tsAtTimeOfLastChord = self.tsCurrent
801        try:
802            aSrc = a.src
803            # if kCurrent.mode == 'minor':
804            #     if aSrc.lower().startswith('vi'):  # vi or vii w/ or w/o o
805            #         if aSrc.upper() == a.src:  # VI or VII to bVI or bVII
806            #             aSrc = 'b' + aSrc
807            cacheTuple = (aSrc, self.kCurrent.tonicPitchNameWithCase)
808            if USE_RN_CACHE and cacheTuple in _rnKeyCache:  # pragma: no cover
809                # print('Got a match: ' + str(cacheTuple))
810                # Problems with Caches not picking up pivot chords...
811                #    Not faster, see below.
812                rn = copy.deepcopy(_rnKeyCache[cacheTuple])
813            else:
814                # print('No match for: ' + str(cacheTuple))
815                rn = roman.RomanNumeral(aSrc,
816                                        copy.deepcopy(self.kCurrent),
817                                        sixthMinor=self.sixthMinor,
818                                        seventhMinor=self.seventhMinor,
819                                        )
820                _rnKeyCache[cacheTuple] = rn
821            # surprisingly, not faster... and more dangerous
822            # rn = roman.RomanNumeral(aSrc, kCurrent)
823            # # SLOWEST!!!
824            # rn = roman.RomanNumeral(aSrc, kCurrent.tonicPitchNameWithCase)
825
826            # >>> from timeit import timeit as t
827            # >>> t('roman.RomanNumeral("IV", "c#")',
828            # ...     'from music21 import roman', number=1000)
829            # 45.75
830            # >>> t('roman.RomanNumeral("IV", k)',
831            # ...     'from music21 import roman, key; k = key.Key("c#")',
832            # ...     number=1000)
833            # 16.09
834            # >>> t('roman.RomanNumeral("IV", copy.deepcopy(k))',
835            # ...    'from music21 import roman, key; import copy;
836            # ...     k = key.Key("c#")', number=1000)
837            # 22.49
838            # # key cache, does not help much...
839            # >>> t('copy.deepcopy(r)', 'from music21 import roman; import copy;
840            # ...        r = roman.RomanNumeral("IV", "c#")', number=1000)
841            # 19.01
842
843            if self.setKeyChangeToken is True:
844                rn.editorial.followsKeyChange = True
845                self.setKeyChangeToken = False
846            else:
847                rn.editorial.followsKeyChange = False
848        except (roman.RomanNumeralException,
849                exceptions21.Music21CommonException):  # pragma: no cover
850            # environLocal.printDebug(f' cannot create RN from: {a.src}')
851            rn = note.Note()  # create placeholder
852
853        if self.pivotChordPossible is False:
854            # probably best to find duration
855            if self.previousChordInMeasure is None:
856                pass  # use default duration
857            else:  # update duration of previous chord in Measure
858                oPrevious = self.previousChordInMeasure.getOffsetBySite(m)
859                newQL = currentOffset - oPrevious
860                if newQL <= 0:  # pragma: no cover
861                    raise RomanTextTranslateException(
862                        f'too many notes in this measure: {self.currentMeasureToken.src}')
863                self.previousChordInMeasure.quarterLength = newQL
864
865            rn.addLyric(self.prefixLyric + a.src)
866            self.prefixLyric = ''
867            m.coreInsert(currentOffset, rn)
868            self.previousChordInMeasure = rn
869            self.previousRn = rn
870            self.pivotChordPossible = True
871        else:
872            self.previousChordInMeasure.lyric += '//' + self.prefixLyric + a.src
873            self.previousChordInMeasure.pivotChord = rn
874            self.prefixLyric = ''
875            self.pivotChordPossible = False
876
877    def setAnalyticKey(self, a):
878        '''
879        Indicates a change in the analyzed key, not a change in anything
880        else, such as the keySignature.
881        '''
882        try:  # this sets the key and the keysignature
883            self.kCurrent, pl = _getKeyAndPrefix(a)
884            self.prefixLyric += pl
885        except:  # pragma: no cover
886            raise RomanTextTranslateException(
887                f'cannot get analytic key from {a.src} in line {self.currentMeasureToken.src}')
888        self.setKeyChangeToken = True
889
890
891def romanTextToStreamScore(rtHandler, inputM21=None):
892    '''
893    The main processing module for single-movement RomanText works.
894
895    Given a romanText handler or string, return or fill a Score Stream.
896    '''
897    # accept a string directly; mostly for testing
898    if isinstance(rtHandler, str):
899        rtf = rtObjects.RTFile()
900        tokenedRtHandler = rtf.readstr(rtHandler)  # return handler, processes tokens
901    else:
902        tokenedRtHandler = rtHandler
903
904    # this could be just a Stream, but b/c we are creating metadata,
905    # perhaps better to match presentation of other scores.
906    if inputM21 is None:
907        s = stream.Score()
908    else:
909        s = inputM21
910
911    # metadata can be first
912    md = metadata.Metadata()
913    s.insert(0, md)
914
915    partTrans = PartTranslator(md)
916    p = partTrans.translateTokens(tokenedRtHandler.tokens)
917    s.insert(0, p)
918
919    return s
920
921
922letterToNumDict = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7, 'h': 8}
923
924
925def appendMeasureToRepeatEndingsDict(t, m, repeatEndings, measureNumber=None):
926    '''Takes an RTMeasure object (t), (which might represent one or more
927    measures; but currently only one) and a music21 stream.Measure object and
928    store it as a tuple in the repeatEndings dictionary to mark where the
929    translator should later mark for adding endings.
930
931    If the optional measureNumber is specified, we use that rather than the
932    token number to add to the dict.
933
934    This does not yet work for skipped measures.
935
936    >>> rtm = romanText.rtObjects.RTMeasure('m15a V6 b1.5 V6/5 b2 I b3 viio6')
937    >>> rtm.repeatLetter
938    ['a']
939    >>> rtm2 = romanText.rtObjects.RTMeasure('m15b V6 b1.5 V6/5 b2 I')
940    >>> rtm2.repeatLetter
941    ['b']
942    >>> repeatEndings = {}
943    >>> m1 = stream.Measure()
944    >>> m2 = stream.Measure()
945    >>> romanText.translate.appendMeasureToRepeatEndingsDict(rtm, m1, repeatEndings)
946    >>> repeatEndings
947    {1: [(15, <music21.stream.Measure 0a offset=0.0>)]}
948    >>> romanText.translate.appendMeasureToRepeatEndingsDict(rtm2, m2, repeatEndings)
949    >>> repeatEndings[1], repeatEndings[2]
950    ([(15, <music21.stream.Measure 0a offset=0.0>)],
951     [(15, <music21.stream.Measure 0b offset=0.0>)])
952    >>> repeatEndings[2][0][1] is m2
953    True
954    '''
955    if not t.repeatLetter:
956        return
957
958    m.numberSuffix = t.repeatLetter[0]
959
960    for rl in t.repeatLetter:
961        if rl is None or rl == '':
962            continue
963        if rl not in letterToNumDict:  # pragma: no cover
964            raise RomanTextTranslateException(f'Improper repeat letter: {rl}')
965        repeatNumber = letterToNumDict[rl]
966        if repeatNumber not in repeatEndings:
967            repeatEndings[repeatNumber] = []
968        if measureNumber is None:
969            measureTuple = (t.number[0], m)
970        else:
971            measureTuple = (measureNumber, m)
972        repeatEndings[repeatNumber].append(measureTuple)
973
974
975def _consolidateRepeatEndings(repeatEndings):
976    '''
977    take repeatEndings, which is a dict of integers (repeat ending numbers) each
978    holding a list of tuples of measure numbers and measure objects that get this ending,
979    and return a list where contiguous endings should appear.  Each element of the list is a
980    two-element tuple, where the first element is a list of measure objects that should have
981    a bracket and the second element is the repeat number.
982
983    Assumes that the list of measure numbers in each repeatEndings array is sorted.
984
985    For the sake of demo and testing, we will use strings instead of measure objects.
986
987
988    >>> repeatEndings = {1: [(5, 'm5a'), (6, 'm6a'), (17, 'm17'), (18, 'm18'),
989    ...                      (19, 'm19'), (23, 'm23a')],
990    ...                  2: [(5, 'm5b'), (6, 'm6b'), (20, 'm20'), (21, 'm21'), (23, 'm23b')],
991    ...                  3: [(23, 'm23c')]}
992    >>> print(romanText.translate._consolidateRepeatEndings(repeatEndings))
993    [(['m5a', 'm6a'], 1), (['m17', 'm18', 'm19'], 1), (['m23a'], 1),
994     (['m5b', 'm6b'], 2), (['m20', 'm21'], 2), (['m23b'], 2), (['m23c'], 3)]
995    '''
996    returnList = []
997
998    for endingNumber in repeatEndings:
999        startMeasureNumber = None
1000        lastMeasureNumber = None
1001        measureList = []
1002        for measureNumberUnderEnding, measureObject in repeatEndings[endingNumber]:
1003            if startMeasureNumber is None:
1004                startMeasureNumber = measureNumberUnderEnding
1005                lastMeasureNumber = measureNumberUnderEnding
1006                measureList.append(measureObject)
1007            elif measureNumberUnderEnding > lastMeasureNumber + 1:
1008                myTuple = (measureList, endingNumber)
1009                returnList.append(myTuple)
1010                startMeasureNumber = measureNumberUnderEnding
1011                lastMeasureNumber = measureNumberUnderEnding
1012                measureList = [measureObject]
1013            else:
1014                measureList.append(measureObject)
1015                lastMeasureNumber = measureNumberUnderEnding
1016        if startMeasureNumber is not None:
1017            myTuple = (measureList, endingNumber)
1018            returnList.append(myTuple)
1019
1020    return returnList
1021
1022
1023def _addRepeatsFromRepeatEndings(s, repeatEndings):
1024    '''
1025    given a Stream and the repeatEndings dict, add repeats to the stream...
1026    '''
1027    from music21 import spanner
1028    consolidatedRepeats = _consolidateRepeatEndings(repeatEndings)
1029    for repeatEndingTuple in consolidatedRepeats:
1030        measureList, endingNumber = repeatEndingTuple[0], repeatEndingTuple[1]
1031        rb = spanner.RepeatBracket(measureList, number=endingNumber)
1032        rbOffset = measureList[0].getOffsetBySite(s)
1033        # Adding repeat bracket to stream at beginning of repeated section.
1034        # Maybe better at end?
1035        s.insert(rbOffset, rb)
1036        # should be 'if not max(endingNumbers)', but we can't tell that for each repeat.
1037        if endingNumber == 1:
1038            if measureList[-1].rightBarline is None:
1039                measureList[-1].rightBarline = bar.Repeat(direction='end')
1040
1041
1042def fixPickupMeasure(partObject):
1043    '''Fix a pickup measure if any.
1044
1045    We determine a pickup measure by being measure 0 and not having an RN
1046    object at the beginning.
1047
1048    Demonstration: an otherwise incorrect part
1049
1050    >>> p = stream.Part()
1051    >>> m0 = stream.Measure()
1052    >>> m0.number = 0
1053    >>> k0 = key.Key('G')
1054    >>> m0.insert(0, k0)
1055    >>> m0.insert(0, meter.TimeSignature('4/4'))
1056    >>> m0.insert(2, roman.RomanNumeral('V', k0))
1057    >>> m1 = stream.Measure()
1058    >>> m1.number = 1
1059    >>> m2 = stream.Measure()
1060    >>> m2.number = 2
1061    >>> p.insert(0, m0)
1062    >>> p.insert(4, m1)
1063    >>> p.insert(8, m2)
1064
1065    After running fixPickupMeasure()
1066
1067    >>> romanText.translate.fixPickupMeasure(p)
1068    >>> p.show('text')
1069    {0.0} <music21.stream.Measure 0 offset=0.0>
1070        {0.0} <music21.key.Key of G major>
1071        {0.0} <music21.meter.TimeSignature 4/4>
1072        {0.0} <music21.roman.RomanNumeral V in G major>
1073    {2.0} <music21.stream.Measure 1 offset=2.0>
1074    <BLANKLINE>
1075    {6.0} <music21.stream.Measure 2 offset=6.0>
1076    <BLANKLINE>
1077    >>> m0.paddingLeft
1078    2.0
1079    '''
1080    m0 = partObject.measure(0)
1081    if m0 is None:
1082        return
1083    rnObjects = m0.getElementsByClass('RomanNumeral')
1084    if not rnObjects:
1085        return
1086    if rnObjects[0].offset == 0:
1087        return
1088    newPadding = rnObjects[0].offset
1089    for el in m0:
1090        if el.offset < newPadding:  # should be zero for Clefs, etc.
1091            pass
1092        else:
1093            el.offset = el.offset - newPadding
1094    m0.paddingLeft = newPadding
1095    for el in partObject:  # adjust all other measures backwards
1096        if el.offset > 0:
1097            el.offset -= newPadding
1098
1099
1100def romanTextToStreamOpus(rtHandler, inputM21=None):
1101    '''The main processing routine for RomanText objects that may or may not
1102    be multi movement.
1103
1104    Takes in a romanText.rtObjects.RTFile() object, or a string as rtHandler.
1105
1106    Runs `romanTextToStreamScore()` as its main work.
1107
1108    If inputM21 is None then it will create a Score or Opus object.
1109
1110    Return either a Score object, or, if a multi-movement work is defined, an
1111    Opus object.
1112    '''
1113    if isinstance(rtHandler, str):
1114        rtf = rtObjects.RTFile()
1115        rtHandler = rtf.readstr(rtHandler)  # return handler, processes tokens
1116
1117    if rtHandler.definesMovements():  # create an opus
1118        if inputM21 is None:
1119            s = stream.Opus()
1120        else:
1121            s = inputM21
1122        # copy the common header to each of the sub-handlers
1123        handlerBundles = rtHandler.splitByMovement(duplicateHeader=True)
1124        # see if we have header information
1125        for h in handlerBundles:
1126            # print(h, len(h))
1127            # append to opus
1128            s.append(romanTextToStreamScore(h))
1129        return s  # an opus
1130    else:  # create a Score
1131        return romanTextToStreamScore(rtHandler, inputM21=inputM21)
1132
1133
1134# ------------------------------------------------------------------------------
1135
1136
1137class TestSlow(unittest.TestCase):  # pragma: no cover
1138    '''
1139    These tests are currently too slow to run every time.
1140    '''
1141    def testExternalA(self):
1142        from music21.romanText import testFiles
1143
1144        for tf in testFiles.ALL:
1145            rtf = rtObjects.RTFile()
1146            rth = rtf.readstr(tf)  # return handler, processes tokens
1147            s = romanTextToStreamScore(rth)
1148            s.show()
1149
1150    # noinspection SpellCheckingInspection
1151    def testBasicA(self):
1152        from music21.romanText import testFiles
1153
1154        for tf in testFiles.ALL:
1155            rtf = rtObjects.RTFile()
1156            rth = rtf.readstr(tf)  # return handler, processes tokens
1157            # will run romanTextToStreamScore on all but k273
1158            s = romanTextToStreamOpus(rth)
1159            s.show()
1160
1161        s = romanTextToStreamScore(testFiles.swv23)
1162        self.assertEqual(s.metadata.composer, 'Heinrich Schutz')
1163        # this is defined as a Piece tag, but shows up here as a title, after
1164        # being set as an alternate title
1165        self.assertEqual(s.metadata.title, 'Warum toben die Heiden, Psalmen Davids no. 2, SWV 23')
1166
1167        s = romanTextToStreamScore(testFiles.riemenschneider001)
1168        self.assertEqual(s.metadata.composer, 'J. S. Bach')
1169        self.assertEqual(s.metadata.title, 'Aus meines Herzens Grunde')
1170
1171        s = romanTextToStreamScore(testFiles.monteverdi_3_13)
1172        self.assertEqual(s.metadata.composer, 'Claudio Monteverdi')
1173
1174    def testMeasureCopyingA(self):
1175        from music21.romanText import testFiles
1176
1177        s = romanTextToStreamScore(testFiles.swv23)
1178        mStream = s.parts[0].getElementsByClass('Measure')
1179        # the first four measures should all have the same content
1180        rn1 = mStream[1].getElementsByClass('RomanNumeral').first()
1181        self.assertEqual([str(x) for x in rn1.pitches], ['D5', 'F#5', 'A5'])
1182        self.assertEqual(str(rn1.figure), 'V')
1183        rn2 = mStream[1].getElementsByClass('RomanNumeral')[1]
1184        self.assertEqual(str(rn2.figure), 'i')
1185
1186        # make sure that m2, m3, m4 have the same values
1187        rn1 = mStream[2].getElementsByClass('RomanNumeral')[0]
1188        self.assertEqual(str(rn1.figure), 'V')
1189        rn2 = mStream[2].getElementsByClass('RomanNumeral')[1]
1190        self.assertEqual(str(rn2.figure), 'i')
1191
1192        rn1 = mStream[3].getElementsByClass('RomanNumeral')[0]
1193        self.assertEqual(str(rn1.figure), 'V')
1194        rn2 = mStream[3].getElementsByClass('RomanNumeral')[1]
1195        self.assertEqual(str(rn2.figure), 'i')
1196
1197        # test multiple measure copying
1198        s = romanTextToStreamScore(testFiles.monteverdi_3_13)
1199        mStream = s.parts[0].getElementsByClass('Measure')
1200
1201        m1a = None
1202        m2a = None
1203        m3a = None
1204        m1b = None
1205        m2b = None
1206        m3b = None
1207
1208        for m in mStream:
1209            if m.number == 41:  # m49-51 = m41-43
1210                m1a = m
1211            elif m.number == 42:  # m49-51 = m41-43
1212                m2a = m
1213            elif m.number == 43:  # m49-51 = m41-43
1214                m3a = m
1215            elif m.number == 49:  # m49-51 = m41-43
1216                m1b = m
1217            elif m.number == 50:  # m49-51 = m41-43
1218                m2b = m
1219            elif m.number == 51:  # m49-51 = m41-43
1220                m3b = m
1221
1222        rn = m1a.getElementsByClass('RomanNumeral')[0]
1223        self.assertEqual(str(rn.figure), 'IV')
1224        rn = m1a.getElementsByClass('RomanNumeral')[1]
1225        self.assertEqual(str(rn.figure), 'I')
1226
1227        rn = m1b.getElementsByClass('RomanNumeral')[0]
1228        self.assertEqual(str(rn.figure), 'IV')
1229        rn = m1b.getElementsByClass('RomanNumeral')[1]
1230        self.assertEqual(str(rn.figure), 'I')
1231
1232        rn = m2a.getElementsByClass('RomanNumeral')[0]
1233        self.assertEqual(str(rn.figure), 'I')
1234        rn = m2a.getElementsByClass('RomanNumeral')[1]
1235        self.assertEqual(str(rn.figure), 'ii')
1236
1237        rn = m2b.getElementsByClass('RomanNumeral')[0]
1238        self.assertEqual(str(rn.figure), 'I')
1239        rn = m2b.getElementsByClass('RomanNumeral')[1]
1240        self.assertEqual(str(rn.figure), 'ii')
1241
1242        rn = m3a.getElementsByClass('RomanNumeral').first()
1243        self.assertEqual(str(rn.figure), 'V/ii')
1244        rn = m3b.getElementsByClass('RomanNumeral').first()
1245        self.assertEqual(str(rn.figure), 'V/ii')
1246
1247    def testMeasureCopyingB(self):
1248        from music21 import converter
1249        from music21.romanText import testFiles
1250        s = converter.parse(testFiles.monteverdi_3_13)
1251        m25 = s.measure(25)
1252        rn = m25.flatten().getElementsByClass('RomanNumeral')
1253        self.assertEqual(rn[1].figure, 'III')
1254        self.assertEqual(str(rn[1].key), 'd minor')
1255
1256        # TODO: this is getting the F#m even though the key and figure are
1257        # correct
1258        # self.assertEqual(str(rn[1].pitches), '[F4, A4, C5]')
1259
1260        # s.show()
1261
1262    def testOpus(self):
1263        from music21.romanText import testFiles
1264
1265        o = romanTextToStreamOpus(testFiles.mozartK279)
1266        self.assertEqual(o.scores[0].metadata.movementNumber, '1')
1267        self.assertEqual(o.scores[0].metadata.composer, 'Mozart')
1268        self.assertEqual(o.scores[1].metadata.movementNumber, '2')
1269        self.assertEqual(o.scores[1].metadata.composer, 'Mozart')
1270        self.assertEqual(o.scores[2].metadata.movementNumber, '3')
1271        self.assertEqual(o.scores[2].metadata.composer, 'Mozart')
1272
1273        # test using converter.
1274        from music21 import converter
1275        s = converter.parse(testFiles.mozartK279)
1276        self.assertTrue('Opus' in s.classes)
1277        self.assertEqual(len(s.scores), 3)
1278
1279        # make sure a normal file is still a Score
1280        s = converter.parse(testFiles.riemenschneider001)
1281        self.assertTrue(isinstance(s, stream.Score))
1282
1283
1284class Test(unittest.TestCase):
1285    def testMinor67set(self):
1286        from music21.romanText import testFiles
1287        s = romanTextToStreamScore(testFiles.testSetMinorRootParse)
1288        chords = list(s.recurse().getElementsByClass('RomanNumeral'))
1289
1290        def pitchEqual(index, pitchStr):
1291            ch = chords[index]
1292            chPitches = ch.pitches
1293            self.assertEqual(' '.join(p.name for p in chPitches), pitchStr)
1294
1295        pitchEqual(0, 'C E- G')
1296        pitchEqual(1, 'B D F')
1297        pitchEqual(3, 'G B D')
1298        pitchEqual(4, 'A- C E-')
1299        pitchEqual(7, 'B- D F')
1300        pitchEqual(10, 'A C E')
1301
1302    def testPivotInCopyMultiple(self):
1303        from music21 import converter
1304        testCase = '''
1305m1 G: I
1306m2 I
1307m3 V D: I
1308m4 V
1309m5 G: I
1310m6-7 = m3-4
1311m8 I
1312'''
1313        s = converter.parse(testCase, format='romanText')
1314        m = s.measure(7).flatten()
1315        self.assertEqual(m.getElementsByClass('RomanNumeral').first().key.name, 'D major')
1316        m = s.measure(8).flatten()
1317        self.assertEqual(m.getElementsByClass('RomanNumeral').first().key.name, 'D major')
1318
1319    def testPivotInCopyMultiple2(self):
1320        '''
1321        test whether a chord in a pivot situation outside of copying affects copying
1322        '''
1323
1324        from music21 import converter
1325        testCase = '''
1326m1 G: I
1327m2 V D: I
1328m3 G: IV
1329m4 V
1330m5 I
1331m6-7 = m4-5
1332m8 I
1333'''
1334        s = converter.parse(testCase, format='romanText')
1335        m = s.measure(5).flatten()
1336        self.assertEqual(m.getElementsByClass('RomanNumeral').first().key.name, 'G major')
1337
1338    def testPivotInCopySingle(self):
1339        from music21 import converter
1340        testCase = '''
1341m1 G: I
1342m2 I
1343m3 V D: I
1344m4 G: I
1345m5 = m3
1346m6 I
1347'''
1348        s = converter.parse(testCase, format='romanText')
1349        m = s.measure(6).flatten()
1350        self.assertEqual(m.getElementsByClass('RomanNumeral').first().key.name, 'D major')
1351
1352    def testSecondaryInCopyMultiple(self):
1353        '''
1354        test secondary dominants after copy
1355        '''
1356
1357        testSecondaryInCopy = '''
1358Time Signature: 4/4
1359m1 g: i
1360m2 i6
1361m3 V7/v
1362m4 d: i
1363m5-6 = m2-3
1364m7 = m3
1365'''
1366
1367        s = romanTextToStreamScore(testSecondaryInCopy)
1368        m = s.measure(6).flatten()
1369        self.assertEqual(m.getElementsByClass('RomanNumeral').first().pitchedCommonName,
1370                         'E-dominant seventh chord')
1371        m = s.measure(7).flatten()
1372        self.assertEqual(m.getElementsByClass('RomanNumeral').first().pitchedCommonName,
1373                         'E-dominant seventh chord')
1374        # s.show()
1375
1376    def testBasicB(self):
1377        from music21.romanText import testFiles
1378
1379        unused_s = romanTextToStreamScore(testFiles.riemenschneider001)
1380        # unused_s.show()
1381
1382    def testRomanTextString(self):
1383        from music21 import converter
1384        s = converter.parse('m1 KS1 I \n m2 V6/5 \n m3 I b3 V7 \n'
1385                            + 'm4 KS-3 vi \n m5 a: i b3 V4/2 \n m6 I',
1386                            format='romantext')
1387
1388        rnStream = s.flatten().getElementsByClass('RomanNumeral').stream()
1389        self.assertEqual(rnStream[0].figure, 'I')
1390        self.assertEqual(rnStream[1].figure, 'V6/5')
1391        self.assertEqual(rnStream[2].figure, 'I')
1392        self.assertEqual(rnStream[3].figure, 'V7')
1393        self.assertEqual(rnStream[4].figure, 'vi')
1394        self.assertEqual(rnStream[5].figure, 'i')
1395        self.assertEqual(rnStream[6].figure, 'V4/2')
1396        self.assertEqual(rnStream[7].figure, 'I')
1397
1398        rnStreamKey = s.flatten().getElementsByClass('KeySignature')
1399        self.assertEqual(rnStreamKey[0].sharps, 1)
1400        self.assertEqual(rnStreamKey[1].sharps, -3)
1401
1402        # s.show()
1403
1404    def testMeasureCopyingB(self):
1405        from music21 import converter
1406        from music21 import pitch
1407
1408        src = '''m1 G: IV || b3 d: III b4 ii
1409m2 v b2 III6 b3 iv6 b4 ii/o6/5
1410m3 i6/4 b3 V
1411m4-5 = m2-3
1412m6-7 = m4-5
1413'''
1414        s = converter.parse(src, format='romantext')
1415        rnStream = s.flatten().getElementsByClass('RomanNumeral')
1416
1417        for elementNumber in [0, 6, 12]:
1418            self.assertEqual(rnStream[elementNumber + 4].figure, 'III6')
1419            self.assertEqual(str([str(p) for p in rnStream[elementNumber + 4].pitches]),
1420                             "['A4', 'C5', 'F5']")
1421
1422            x = rnStream[elementNumber + 4].pitches[2].accidental
1423            if x is None:
1424                x = pitch.Accidental('natural')
1425            self.assertEqual(x.alter, 0)
1426
1427            self.assertEqual(rnStream[elementNumber + 5].figure, 'iv6')
1428            self.assertEqual(str([str(p) for p in rnStream[elementNumber + 5].pitches]),
1429                             "['B-4', 'D5', 'G5']")
1430
1431            self.assertTrue(rnStream[elementNumber + 5].pitches[0].accidental.displayStatus)
1432
1433    def testNoChord(self):
1434        from music21 import converter
1435        from music21.harmony import NoChord
1436
1437        src = '''m1 G: IV || b3 d: III b4 NC
1438m2 b2 III6 b3 iv6 b4 ii/o6/5
1439m3 NC b3 G: V
1440'''
1441        s = converter.parse(src, format='romantext')
1442        p = s.parts[0]
1443        m1 = p.getElementsByClass('Measure').first()
1444        r1 = m1.notesAndRests[-1]
1445        self.assertIn('Rest', r1.classes)
1446        self.assertEqual(r1.quarterLength, 1.0)
1447        noChordObj = m1.getElementsByClass('Harmony').last()
1448        self.assertIsInstance(noChordObj, NoChord)
1449
1450        m2 = p.getElementsByClass('Measure')[1]
1451        r2 = m2.notesAndRests[0]
1452        self.assertIn('Rest', r2.classes)
1453        self.assertEqual(r1.quarterLength, 1.0)
1454        rn1 = m2.notesAndRests[1]
1455        self.assertIn('RomanNumeral', rn1.classes)
1456        # s.show()
1457
1458    def testUnProcessed(self):
1459        from music21 import converter
1460
1461        src = '''Note: Hello
1462m1 G: IV || b3 d: III b4 NC
1463varM1 I
1464Note: Hi
1465'''
1466        s = converter.parse(src, format='romantext')
1467        p = s.parts[0]
1468        unprocessedElements = p.recurse().getElementsByClass('RomanTextUnprocessedMetadata')
1469        self.assertEqual(len(unprocessedElements), 3)
1470        note1, var1, note2 = unprocessedElements
1471        self.assertEqual(note1.tag, 'Note')
1472        self.assertEqual(note2.tag, 'Note')
1473        self.assertEqual(note1.data, 'Hello')
1474        self.assertEqual(note2.data, 'Hi')
1475        self.assertFalse(var1.tag)
1476        self.assertIn(' I', var1.data)
1477
1478    def testSixthMinorParse(self):
1479        from music21 import converter
1480
1481        src = '''SixthMinor: flat
1482m1 c: vi
1483'''
1484        s = converter.parse(src, format='romantext')
1485        p = s.parts[0]
1486        ch0 = p.recurse().notes[0]
1487        self.assertEqual(ch0.root().name, 'A-')
1488
1489    def testSetRTVersion(self):
1490        src = '''RTVersion: 2.5
1491m1 C: I'''
1492        rtf = rtObjects.RTFile()
1493        rtHandler = rtf.readstr(src)
1494        pt = PartTranslator()
1495        pt.translateTokens(rtHandler.tokens)
1496        self.assertEqual(pt.romanTextVersion, 2.5)
1497
1498        # gives warning, not raises...
1499        #         src = '''RTVersion: XYZ
1500        # m1 C: I'''
1501        #         rtf = rtObjects.RTFile()
1502        #         rtHandler = rtf.readstr(src)
1503        #         pt = PartTranslator()
1504        #         with self.assertRaises(RomanTextTranslateException):
1505        #             pt.translateTokens(rtHandler.tokens)
1506
1507    def testPivotChord(self):
1508        from music21 import converter
1509
1510        src = '''m1 G: I b3 v d: i b4 V'''
1511        s = converter.parse(src, format='romantext')
1512        p = s.parts[0]
1513        m1 = p.getElementsByClass('Measure').first()
1514        allRNs = m1.getElementsByClass('RomanNumeral')
1515        notPChord = allRNs[0]
1516        pChord = allRNs[1]
1517        self.assertEqual(pChord.key.tonic.step, 'G')
1518        self.assertEqual(pChord.figure, 'v')
1519        pivot = pChord.pivotChord
1520        self.assertEqual(pivot.key.tonic.step, 'D')
1521        self.assertEqual(pivot.figure, 'i')
1522
1523        self.assertIsNone(notPChord.pivotChord)
1524        # s.show('text')
1525
1526    def testTimeSigChanges(self):
1527        from music21 import converter
1528        src = '''Time Signature: 4/4
1529        m1 C: I
1530        Time Signature: 2/4
1531        m10 V
1532        Time Signature: 4/4
1533        m12 I
1534        m14-25 = m1-12
1535        '''
1536        s = converter.parse(src, format='romantext')
1537        p = s.parts[0]
1538        m3 = p.getElementsByClass('Measure')[2]
1539        self.assertEqual(m3.getOffsetBySite(p), 8.0)
1540        m10 = p.getElementsByClass('Measure')[9]
1541        self.assertEqual(m10.getOffsetBySite(p), 36.0)
1542        m11 = p.getElementsByClass('Measure')[10]
1543        self.assertEqual(m11.getOffsetBySite(p), 38.0)
1544        m12 = p.getElementsByClass('Measure')[11]
1545        self.assertEqual(m12.getOffsetBySite(p), 40.0)
1546        m13 = p.getElementsByClass('Measure')[12]
1547        self.assertEqual(m13.getOffsetBySite(p), 44.0)
1548
1549        m16 = p.getElementsByClass('Measure')[15]
1550        self.assertEqual(m16.getOffsetBySite(p), 56.0)
1551        m23 = p.getElementsByClass('Measure')[22]
1552        self.assertEqual(m23.getOffsetBySite(p), 84.0)
1553        m24 = p.getElementsByClass('Measure')[23]
1554        self.assertEqual(m24.getOffsetBySite(p), 86.0)
1555        m25 = p.getElementsByClass('Measure')[24]
1556        self.assertEqual(m25.getOffsetBySite(p), 88.0)
1557
1558    def testEndings(self):
1559        # has first and second endings...
1560
1561        from music21.romanText import testFiles
1562        from music21 import converter
1563        unused_s = converter.parse(testFiles.mozartK283_2_opening, format='romanText')
1564        # s.show('text')
1565
1566    def testTuplets(self):
1567        from music21 import converter
1568        c = converter.parse('m1 C: I b2.66 V', format='romantext')
1569        n1 = c.flatten().notes[0]
1570        n2 = c.flatten().notes[1]
1571        self.assertEqual(n1.duration.quarterLength, common.opFrac(5 / 3))
1572        self.assertEqual(n2.offset, common.opFrac(5 / 3))
1573        self.assertEqual(n2.duration.quarterLength, common.opFrac(7 / 3))
1574
1575        c = converter.parse('TimeSignature: 6/8\nm1 C: I b2.66 V', format='romantext')
1576        n1 = c.flatten().notes[0]
1577        n2 = c.flatten().notes[1]
1578        self.assertEqual(n1.duration.quarterLength, 5 / 2)
1579        self.assertEqual(n2.offset, 5 / 2)
1580        self.assertEqual(n2.duration.quarterLength, 1 / 2)
1581
1582        c = converter.parse('m1 C: I b2.66.5 V', format='romantext')
1583        n1 = c.flatten().notes[0]
1584        n2 = c.flatten().notes[1]
1585        self.assertEqual(n1.duration.quarterLength, common.opFrac(11 / 6))
1586        self.assertEqual(n2.offset, common.opFrac(11 / 6))
1587        self.assertEqual(n2.duration.quarterLength, common.opFrac(13 / 6))
1588
1589
1590# ------------------------------------------------------------------------------
1591
1592# define presented order in documentation
1593_DOC_ORDER = []
1594
1595
1596if __name__ == '__main__':
1597    import music21
1598    music21.mainTest(Test)  # , TestSlow)
1599
1600