1# -*- coding: utf-8 -*-
2# -------------------------------------------------------------------------------
3# Name:         tinyNotation.py
4# Purpose:      A simple notation input format.
5#
6# Authors:      Michael Scott Cuthbert
7#
8# Copyright:    Copyright © 2009-2012, 2015 Michael Scott Cuthbert and the music21 Project
9# License:      BSD, see license.txt
10# -------------------------------------------------------------------------------
11'''
12tinyNotation is a simple way of specifying single line melodies
13that uses a notation somewhat similar to Lilypond but with WAY fewer
14options.  It was originally developed to notate trecento (medieval Italian)
15music, but it is pretty useful for a lot of short examples, so we have
16made it a generally supported music21 format.
17
18
19N.B.: TinyNotation is not meant to expand to cover every single case.  Instead
20it is meant to be subclassable to extend to the cases *your* project needs.
21
22Here are the most important rules by default:
23
241. Note names are: a,b,c,d,e,f,g and r for rest
252. Flats, sharps, and naturals are notated as #,- (not b), and (if needed) n.
26   If the accidental is above the staff (i.e., editorial), enclose it in
27   parentheses: (#), etc.  Make sure that flats in the key signatures are
28   explicitly specified.
293. Note octaves are specified as follows::
30
31     CC to BB = from C below bass clef to second-line B in bass clef
32     C to B = from bass clef C to B below middle C.
33     c  to b = from middle C to the middle of treble clef
34     c' to b' = from C in treble clef to B above treble clef
35
36   Octaves below and above these are specified by further doublings of
37   letter (CCC) or apostrophes (c'') -- this is one of the note name
38   standards found in many music theory books.
394. After the note name, a number may be placed indicating the note
40   length: 1 = whole note, 2 = half, 4 = quarter, 8 = eighth, 16 = sixteenth.
41   etc.  If the number is omitted then it is assumed to be the same
42   as the previous note.  I.e., c8 B c d  is a string of eighth notes.
435. After the number, a ~ can be placed to show a tie to the next note.
44   A "." indicates a dotted note.  (If you are entering
45   data via Excel or other spreadsheet, be sure that "capitalize the
46   first letter of sentences" is turned off under "Tools->AutoCorrect,"
47   otherwise the next letter will be capitalized, and the octave will
48   be screwed up.)
496. For triplets use this notation:  `trip{c4 d8}`  indicating that these
50   two notes both have "3s" over them.  For 4 in the place of 3,
51   use `quad{c16 d e8}`.  No other tuplets are supported.
52
53Here is an example of TinyNotation in action.
54
55>>> stream1 = converter.parse("tinyNotation: 3/4 E4 r f# g=lastG trip{b-8 a g} c4~ c")
56>>> stream1.show('text')
57{0.0} <music21.stream.Measure 1 offset=0.0>
58    {0.0} <music21.clef.TrebleClef>
59    {0.0} <music21.meter.TimeSignature 3/4>
60    {0.0} <music21.note.Note E>
61    {1.0} <music21.note.Rest quarter>
62    {2.0} <music21.note.Note F#>
63{3.0} <music21.stream.Measure 2 offset=3.0>
64    {0.0} <music21.note.Note G>
65    {1.0} <music21.note.Note B->
66    {1.3333} <music21.note.Note A>
67    {1.6667} <music21.note.Note G>
68    {2.0} <music21.note.Note C>
69{6.0} <music21.stream.Measure 3 offset=6.0>
70    {0.0} <music21.note.Note C>
71    {1.0} <music21.bar.Barline type=final>
72>>> stream1.recurse().getElementById('lastG').step
73'G'
74>>> stream1.flatten().notesAndRests[1].isRest
75True
76>>> stream1.flatten().notesAndRests[0].octave
773
78>>> stream1.flatten().notes[-2].tie.type
79'start'
80>>> stream1.flatten().notes[-1].tie.type
81'stop'
82
83Changing time signatures are supported:
84
85>>> s1 = converter.parse('tinynotation: 3/4 C4 D E 2/4 F G A B 1/4 c')
86>>> s1.show('t')
87{0.0} <music21.stream.Measure 1 offset=0.0>
88    {0.0} <music21.clef.BassClef>
89    {0.0} <music21.meter.TimeSignature 3/4>
90    {0.0} <music21.note.Note C>
91    {1.0} <music21.note.Note D>
92    {2.0} <music21.note.Note E>
93{3.0} <music21.stream.Measure 2 offset=3.0>
94    {0.0} <music21.meter.TimeSignature 2/4>
95    {0.0} <music21.note.Note F>
96    {1.0} <music21.note.Note G>
97{5.0} <music21.stream.Measure 3 offset=5.0>
98    {0.0} <music21.note.Note A>
99    {1.0} <music21.note.Note B>
100{7.0} <music21.stream.Measure 4 offset=7.0>
101    {0.0} <music21.meter.TimeSignature 1/4>
102    {0.0} <music21.note.Note C>
103    {1.0} <music21.bar.Barline type=final>
104
105
106
107Here is an equivalent way of doing the example above, but using the lower level
108:class:`music21.tinyNotation.Converter` object:
109
110>>> tnc = tinyNotation.Converter('3/4 E4 r f# g=lastG trip{b-8 a g} c4~ c')
111>>> stream2 = tnc.parse().stream
112>>> len(stream1.recurse()) == len(stream2.recurse())
113True
114
115This lower level is needed in case you want to add additional features.  For instance,
116here we will set the "modifierStar" to change the color of notes:
117
118>>> class ColorModifier(tinyNotation.Modifier):
119...     def postParse(self, m21Obj):
120...         m21Obj.style.color = self.modifierData
121...         return m21Obj
122
123>>> tnc = tinyNotation.Converter('3/4 C4*pink* D4*green* E4*blue*')
124>>> tnc.modifierStar = ColorModifier
125>>> s = tnc.parse().stream
126>>> for n in s.recurse().getElementsByClass('Note'):
127...     print(n.step, n.style.color)
128C pink
129D green
130E blue
131
132Or more usefully, and often desired:
133
134>>> class HarmonyModifier(tinyNotation.Modifier):
135...     def postParse(self, n):
136...         cs = harmony.ChordSymbol(n.pitch.name + self.modifierData)
137...         cs.duration = n.duration
138...         return cs
139>>> tnc = tinyNotation.Converter('4/4 C2_maj7 D4_m E-_sus4')
140>>> tnc.modifierUnderscore = HarmonyModifier
141>>> s = tnc.parse().stream
142>>> s.show('text')
143{0.0} <music21.stream.Measure 1 offset=0.0>
144    {0.0} <music21.clef.BassClef>
145    {0.0} <music21.meter.TimeSignature 4/4>
146    {0.0} <music21.harmony.ChordSymbol Cmaj7>
147    {2.0} <music21.harmony.ChordSymbol Dm>
148    {3.0} <music21.harmony.ChordSymbol E-sus4>
149    {4.0} <music21.bar.Barline type=final>
150>>> for cs in s.recurse().getElementsByClass('ChordSymbol'):
151...     print([p.name for p in cs.pitches])
152['C', 'E', 'G', 'B']
153['D', 'F', 'A']
154['E-', 'A-', 'B-']
155
156The supported modifiers are:
157    * `=data` (`modifierEquals`, default action is to set `.id`)
158    * `_data` (`modifierUnderscore`, default action is to set `.lyric`)
159    * `[data]` (`modifierSquare`, no default action)
160    * `<data>` (`modifierAngle`, no default action)
161    * `(data)` (`modifierParens`, no default action)
162    * `*data*` (`modifierStar`, no default action)
163
164
165Another example: TinyNotation does not support key signatures -- well, no problem! Let's
166create a new Token type and add it to the tokenMap
167
168>>> class KeyToken(tinyNotation.Token):
169...     def parse(self, parent):
170...         keyName = self.token
171...         return key.Key(keyName)
172>>> keyMapping = (r'k(.*)', KeyToken)
173>>> tnc = tinyNotation.Converter('4/4 kE- G1 kf# A1')
174>>> tnc.tokenMap.append(keyMapping)
175>>> s = tnc.parse().stream
176>>> s.show('text')
177{0.0} <music21.stream.Measure 1 offset=0.0>
178    {0.0} <music21.clef.BassClef>
179    {0.0} <music21.key.Key of E- major>
180    {0.0} <music21.meter.TimeSignature 4/4>
181    {0.0} <music21.note.Note G>
182{4.0} <music21.stream.Measure 2 offset=4.0>
183    {0.0} <music21.key.Key of f# minor>
184    {0.0} <music21.note.Note A>
185    {4.0} <music21.bar.Barline type=final>
186
187
188TokenMap should be passed a string, representing a regular expression with exactly one
189group (which can be the entire expression), and a subclass of :class:`~music21.tinyNotation.Token`
190which will handle the parsing of the string.
191
192Tokens can take advantage of the `parent` variable, which is a reference to the `Converter`
193object, to use the `.stateDict` dictionary to store information about state.  For instance,
194the `NoteOrRestToken` uses `parent.stateDict['lastDuration']` to get access to the last
195duration.
196
197There is also the concept of "State" which affects multiple tokens.  The best way to create
198a new State is to define a subclass of the :class:`~music21.tinyNotation.State`  and add it
199to `bracketStateMapping` of the converter.  Here's one that a lot of people have asked for
200over the years:
201
202>>> class ChordState(tinyNotation.State):
203...    def affectTokenAfterParse(self, n):
204...        super().affectTokenAfterParse(n)
205...        return None  # do not append Note object
206...    def end(self):
207...        ch = chord.Chord(self.affectedTokens)
208...        ch.duration = self.affectedTokens[0].duration
209...        return ch
210>>> tnc = tinyNotation.Converter("2/4 C4 chord{C4 e g'} F.4 chord{D8 F# A}")
211>>> tnc.bracketStateMapping['chord'] = ChordState
212>>> s = tnc.parse().stream
213>>> s.show('text')
214{0.0} <music21.stream.Measure 1 offset=0.0>
215    {0.0} <music21.clef.BassClef>
216    {0.0} <music21.meter.TimeSignature 2/4>
217    {0.0} <music21.note.Note C>
218    {1.0} <music21.chord.Chord C3 E4 G5>
219{2.0} <music21.stream.Measure 2 offset=2.0>
220    {0.0} <music21.note.Note F>
221    {1.5} <music21.chord.Chord D3 F#3 A3>
222    {2.0} <music21.bar.Barline type=final>
223
224If you want to create a very different dialect, you can subclass tinyNotation.Converter
225and set it up once to use the mappings above.   See
226:class:`~music21.alpha.trecento.notation.TrecentoTinyConverter` (especially the code)
227for details on how to do that.
228'''
229import collections
230import copy
231import re
232import sre_parse
233import typing
234import unittest
235
236from music21 import note
237from music21 import duration
238from music21 import common
239from music21 import exceptions21
240from music21 import stream
241from music21 import tie
242from music21 import expressions
243from music21 import meter
244from music21 import pitch
245
246from music21 import environment
247_MOD = 'tinyNotation'
248environLocal = environment.Environment(_MOD)
249
250
251class TinyNotationException(exceptions21.Music21Exception):
252    pass
253
254
255class State:
256    '''
257    State tokens apply something to
258    every note found within it.
259
260    State objects can have "autoExpires" set, which is False if it does not expire
261    or an integer if it expires after a certain number of tokens have been processed.
262
263    >>> tnc = tinyNotation.Converter()
264    >>> ts = tinyNotation.TieState(tnc, '~')
265    >>> isinstance(ts, tinyNotation.State)
266    True
267    >>> ts.autoExpires
268    2
269    '''
270    autoExpires = False  # expires after N tokens or never.
271
272    def __init__(self, parent=None, stateInfo=None):
273        self.affectedTokens = []
274        self.parent = common.wrapWeakref(parent)
275        self.stateInfo = stateInfo
276        # print('Adding state', self, parent.activeStates)
277
278    def start(self):
279        '''
280        called when the state is initiated
281        '''
282        pass
283
284    def end(self):
285        '''
286        called just after removing state
287        '''
288        return None
289
290    def affectTokenBeforeParse(self, tokenStr):
291        '''
292        called to modify the string of a token.
293        '''
294        return tokenStr
295
296    def affectTokenAfterParseBeforeModifiers(self, m21Obj):
297        '''
298        called after the object has been acquired but before modifiers have been applied.
299        '''
300        return m21Obj
301
302    def affectTokenAfterParse(self, m21Obj):
303        '''
304        called to modify the tokenObj after parsing
305
306        tokenObj may be None if another
307        state has deleted it.
308        '''
309        self.affectedTokens.append(m21Obj)
310        if self.autoExpires is not False:
311            if len(self.affectedTokens) == self.autoExpires:
312                self.end()
313                # this is a hack that should be done away with...
314                p = common.unwrapWeakref(self.parent)
315                for i in range(len(p.activeStates)):
316                    backCount = -1 * (i + 1)
317                    if p.activeStates[backCount] is self:
318                        p.activeStates.pop(backCount)
319                        break
320        return m21Obj
321
322
323class TieState(State):
324    '''
325    A TieState is an auto-expiring state that applies a tie start to this note and a
326    tie stop to the next note.
327    '''
328    autoExpires = 2
329
330    def end(self):
331        '''
332        end the tie state by applying tie ties to the appropriate notes
333        '''
334        if self.affectedTokens[0].tie is None:
335            self.affectedTokens[0].tie = tie.Tie('start')
336        else:
337            self.affectedTokens[0].tie.type = 'continue'
338        if len(self.affectedTokens) > 1:  # could be end.
339            self.affectedTokens[1].tie = tie.Tie('stop')
340
341
342class TupletState(State):
343    '''
344    a tuplet state applies tuplets to notes while parsing and sets 'start' and 'stop'
345    on the first and last note when end is called.
346    '''
347    actual = 3
348    normal = 2
349
350    def end(self):
351        '''
352        end a tuplet by putting start on the first note and stop on the last.
353        '''
354        if not self.affectedTokens:
355            return None
356        self.affectedTokens[0].duration.tuplets[0].type = 'start'
357        self.affectedTokens[-1].duration.tuplets[0].type = 'stop'
358        return None
359
360    def affectTokenAfterParse(self, n):
361        '''
362        puts a tuplet on the note
363        '''
364        super().affectTokenAfterParse(n)
365        newTup = duration.Tuplet()
366        newTup.durationActual = duration.durationTupleFromTypeDots(n.duration.type, 0)
367        newTup.durationNormal = duration.durationTupleFromTypeDots(n.duration.type, 0)
368        newTup.numberNotesActual = self.actual
369        newTup.numberNotesNormal = self.normal
370        n.duration.appendTuplet(newTup)
371        return n
372
373
374class TripletState(TupletState):
375    '''
376    a 3:2 tuplet
377    '''
378    actual = 3
379    normal = 2
380
381
382class QuadrupletState(TupletState):
383    '''
384    a 4:3 tuplet
385    '''
386    actual = 4
387    normal = 3
388
389
390class Modifier:
391    '''
392    a modifier is something that changes the current
393    token, like setting the Id or Lyric.
394    '''
395
396    def __init__(self, modifierData, modifierString, parent):
397        self.modifierData = modifierData
398        self.modifierString = modifierString
399        self.parent = common.wrapWeakref(parent)
400
401    def preParse(self, tokenString):
402        '''
403        called before the tokenString has been
404        turned into an object
405        '''
406        pass
407
408    def postParse(self, m21Obj):
409        '''
410        called after the tokenString has been
411        turned into an m21Obj.  m21Obj may be None
412
413        Important: must return the m21Obj, or a different object!
414        '''
415        return m21Obj
416
417
418class IdModifier(Modifier):
419    '''
420    sets the .id of the m21Obj, called with = by default
421    '''
422
423    def postParse(self, m21Obj):
424        if hasattr(m21Obj, 'id'):
425            m21Obj.id = self.modifierData
426        return m21Obj
427
428class LyricModifier(Modifier):
429    '''
430    sets the .lyric of the m21Obj, called with _ by default
431    '''
432
433    def postParse(self, m21Obj):
434        if hasattr(m21Obj, 'lyric'):
435            m21Obj.lyric = self.modifierData
436        return m21Obj
437
438
439
440class Token:
441    '''
442    A single token made from the parser.
443
444    Call .parse(parent) to make it work.
445    '''
446
447    def __init__(self, token=''):
448        self.token = token
449
450    def parse(self, parent):
451        '''
452        do NOT store parent -- probably
453        too slow
454        '''
455        return None
456
457
458class TimeSignatureToken(Token):
459    '''
460    Represents a single time signature, like 1/4
461    '''
462
463    def parse(self, parent):
464        tsObj = meter.TimeSignature(self.token)
465        parent.stateDict['currentTimeSignature'] = tsObj
466        return tsObj
467
468
469class NoteOrRestToken(Token):
470    '''
471    represents a Note or Rest.  Chords are represented by Note objects
472    '''
473
474    def __init__(self, token=''):
475        super().__init__(token)
476        self.durationMap = [
477            (r'(\d+)', 'durationType'),
478            (r'(\.+)', 'dots'),
479        ]  # tie will be dealt with later.
480
481
482        self.durationFound = False
483
484    def applyDuration(self, n, t, parent):
485        '''
486        takes the information in the string `t` and creates a Duration object for the
487        note or rest `n`.
488        '''
489        for pm, method in self.durationMap:
490            searchSuccess = re.search(pm, t)
491            if searchSuccess:
492                callFunc = getattr(self, method)
493                t = callFunc(n, searchSuccess, pm, t, parent)
494
495        if self.durationFound is False and hasattr(parent, 'stateDict'):
496            n.duration.quarterLength = parent.stateDict['lastDuration']
497
498        # do this by quarterLength here, so that applied tuplets do not persist.
499        if hasattr(parent, 'stateDict'):
500            parent.stateDict['lastDuration'] = n.duration.quarterLength
501
502        return t
503
504    def durationType(self, element, search, pm, t, parent):
505        '''
506        The result of a successful search for a duration type: puts a Duration in the right place.
507        '''
508        self.durationFound = True
509        typeNum = int(search.group(1))
510        if typeNum == 0:
511            if parent.stateDict['currentTimeSignature'] is not None:
512                element.duration = copy.deepcopy(
513                    parent.stateDict['currentTimeSignature'].barDuration
514                )
515                element.expressions.append(expressions.Fermata())
516        else:
517            try:
518                element.duration.type = duration.typeFromNumDict[typeNum]
519            except KeyError as ke:
520                raise TinyNotationException(
521                    f'Cannot parse token with duration {typeNum}'
522                ) from ke
523        t = re.sub(pm, '', t)
524        return t
525
526    def dots(self, element, search, pm, t, parent):
527        '''
528        adds the appropriate number of dots to the right place.
529
530        Subclassed in TrecentoNotation where two dots has a different meaning.
531        '''
532        element.duration.dots = len(search.group(1))
533        t = re.sub(pm, '', t)
534        return t
535
536
537class RestToken(NoteOrRestToken):
538    '''
539    A token starting with 'r', representing a rest.
540    '''
541
542    def parse(self, parent=None):
543        r = note.Rest()
544        self.applyDuration(r, self.token, parent)
545        return r
546
547
548class NoteToken(NoteOrRestToken):
549    '''
550    A NoteToken represents a single Note with pitch
551
552    >>> c3 = tinyNotation.NoteToken('C')
553    >>> c3
554    <music21.tinyNotation.NoteToken object at 0x10b07bf98>
555    >>> n = c3.parse()
556    >>> n
557    <music21.note.Note C>
558    >>> n.nameWithOctave
559    'C3'
560
561    >>> bFlat6 = tinyNotation.NoteToken("b''-")
562    >>> bFlat6
563    <music21.tinyNotation.NoteToken object at 0x10b07bf98>
564    >>> n = bFlat6.parse()
565    >>> n
566    <music21.note.Note B->
567    >>> n.nameWithOctave
568    'B-6'
569
570    '''
571    pitchMap = collections.OrderedDict([
572        ('lowOctave', r'([A-G]+)'),
573        ('highOctave', r'([a-g])(\'*)'),
574        ('editorialAccidental', r'\(([\#\-n]+)\)(.*)'),
575        ('sharps', r'(\#+)'),
576        ('flats', r'(\-+)'),
577        ('natural', r'(n)'),
578    ])
579
580    def __init__(self, token=''):
581        super().__init__(token)
582        self.isEditorial = False
583
584    def parse(self, parent=None):
585        '''
586        Extract the pitch from the note and then returns the Note.
587        '''
588        t = self.token
589
590        n = note.Note()
591        t = self.processPitchMap(n, t)
592        if parent:
593            self.applyDuration(n, t, parent)
594        return n
595
596    def processPitchMap(self, n, t):
597        '''
598        processes the pitchMap on the object.
599        '''
600        for method, pm in self.pitchMap.items():
601            searchSuccess = re.search(pm, t)
602            if searchSuccess:
603                callFunc = getattr(self, method)
604                t = callFunc(n, searchSuccess, pm, t)
605        return t
606
607    def editorialAccidental(self, n, search, pm, t):
608        '''
609        indicates that the accidental is in parentheses, so set it up to be stored in ficta.
610        '''
611        self.isEditorial = True
612        t = search.group(1) + search.group(2)
613        return t
614
615    def _addAccidental(self, n, alter, pm, t):
616        # noinspection PyShadowingNames
617        r'''
618        helper function for all accidental types.
619
620        >>> nToken = tinyNotation.NoteToken('BB--')
621        >>> n = note.Note('B')
622        >>> n.octave = 2
623        >>> tPost = nToken._addAccidental(n, -2, r'(\-+)', 'BB--')
624        >>> tPost
625        'BB'
626        >>> n.pitch.accidental
627        <music21.pitch.Accidental double-flat>
628
629        >>> nToken = tinyNotation.NoteToken('BB(--)')
630        >>> nToken.isEditorial = True
631        >>> n = note.Note('B')
632        >>> n.octave = 2
633        >>> tPost = nToken._addAccidental(n, -2, r'(\-+)', 'BB--')
634        >>> tPost
635        'BB'
636        >>> n.editorial.ficta
637        <music21.pitch.Accidental double-flat>
638        '''
639        acc = pitch.Accidental(alter)
640        if self.isEditorial:
641            n.editorial.ficta = acc
642        else:
643            n.pitch.accidental = acc
644        t = re.sub(pm, '', t)
645        return t
646
647    def sharps(self, n, search, pm, t):
648        # noinspection PyShadowingNames
649        r'''
650        called when one or more sharps have been found and adds the appropriate accidental to it.
651
652        >>> import re
653        >>> tStr = 'C##'
654        >>> nToken = tinyNotation.NoteToken(tStr)
655        >>> n = note.Note('C')
656        >>> n.octave = 3
657        >>> searchResult = re.search(nToken.pitchMap['sharps'], tStr)
658        >>> tPost = nToken.sharps(n, searchResult, nToken.pitchMap['sharps'], tStr)
659        >>> tPost
660        'C'
661        >>> n.pitch.accidental
662        <music21.pitch.Accidental double-sharp>
663        '''
664        alter = len(search.group(1))
665        return self._addAccidental(n, alter, pm, t)
666
667    def flats(self, n, search, pm, t):
668        # noinspection PyShadowingNames
669        '''
670        called when one or more flats have been found and calls adds
671        the appropriate accidental to it.
672
673        >>> import re
674        >>> tStr = 'BB--'
675        >>> nToken = tinyNotation.NoteToken(tStr)
676        >>> n = note.Note('B')
677        >>> n.octave = 2
678        >>> searchResult = re.search(nToken.pitchMap['flats'], tStr)
679        >>> tPost = nToken.flats(n, searchResult, nToken.pitchMap['flats'], tStr)
680        >>> tPost
681        'BB'
682        >>> n.pitch.accidental
683        <music21.pitch.Accidental double-flat>
684        '''
685        alter = -1 * len(search.group(1))
686        return self._addAccidental(n, alter, pm, t)
687
688    def natural(self, n, search, pm, t):
689        # noinspection PyShadowingNames
690        '''
691        called when an explicit natural has been found.  All pitches are natural without
692        being specified, so not needed. Adds a natural accidental to it.
693
694        >>> import re
695        >>> tStr = 'En'
696        >>> nToken = tinyNotation.NoteToken(tStr)
697        >>> n = note.Note('E')
698        >>> n.octave = 3
699        >>> searchResult = re.search(nToken.pitchMap['natural'], tStr)
700        >>> tPost = nToken.natural(n, searchResult, nToken.pitchMap['natural'], tStr)
701        >>> tPost
702        'E'
703        >>> n.pitch.accidental
704        <music21.pitch.Accidental natural>
705        '''
706        return self._addAccidental(n, 0, pm, t)
707
708    def lowOctave(self, n, search, pm, t):
709        # noinspection PyShadowingNames
710        '''
711        Called when a note of octave 3 or below is encountered.
712
713        >>> import re
714        >>> tStr = 'BBB'
715        >>> nToken = tinyNotation.NoteToken(tStr)
716        >>> n = note.Note('B')
717        >>> searchResult = re.search(nToken.pitchMap['lowOctave'], tStr)
718        >>> tPost = nToken.lowOctave(n, searchResult, nToken.pitchMap['lowOctave'], tStr)
719        >>> tPost
720        ''
721        >>> n.octave
722        1
723        '''
724        stepName = search.group(1)[0].upper()
725        octaveNum = 4 - len(search.group(1))
726        n.step = stepName
727        n.octave = octaveNum
728        t = re.sub(pm, '', t)
729        return t
730
731    def highOctave(self, n, search, pm, t):
732        # noinspection PyShadowingNames
733        '''
734        Called when a note of octave 4 or higher is encountered.
735
736        >>> import re
737        >>> tStr = "e''"
738        >>> nToken = tinyNotation.NoteToken(tStr)
739        >>> n = note.Note('E')
740        >>> searchResult = re.search(nToken.pitchMap['highOctave'], tStr)
741        >>> tPost = nToken.highOctave(n, searchResult, nToken.pitchMap['highOctave'], tStr)
742        >>> tPost
743        ''
744        >>> n.octave
745        6
746        '''
747        stepName = search.group(1)[0].upper()
748        octaveNum = 4 + len(search.group(2))
749        n.step = stepName
750        n.octave = octaveNum
751        t = re.sub(pm, '', t)
752        return t
753
754
755def _getDefaultTokenMap() -> typing.List[
756        typing.Tuple[
757            str,
758            typing.Type[Token]
759        ]
760]:
761    """
762    Returns the default tokenMap for TinyNotation.
763
764    Based on the following grammar (in Extended Backus-Naur form)
765    (https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form)
766
767    (* Items in parentheses are grouped *)
768    (* Items in curly braces appear zero or more times *)
769    (* Items in square brackets may appear exactly zero or one time *)
770    (* Items in double quotes are literal strings *)
771    (* Items between question marks should be interpreted as English *)
772    (* Each rule is ended by a semicolon *)
773
774    TINY-NOTATION = TOKEN, { WHITESPACE, TOKEN } ;
775    WHITESPACE = ( " " | ? Carriage return ? ) , { " " | ? Carriage return ? } ;
776    TOKEN = ( TIME-SIGNATURE | TUPLET | REST | NOTE );
777    TIME-SIGNATURE = INTEGER, "/", INTEGER ;
778    INTEGER = DIGIT, { DIGIT } ;
779    DIGIT = ( "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ) ;
780    TUPLET = ( "trip" | "quad" | ALPHANUMERIC ), "{",
781        [ WHITESPACE ],
782        ( REST | NOTE ),
783        { WHITESPACE, ( REST | NOTE ) },
784        [ WHITESPACE ],
785    "}" ;
786    REST = "r", [ DURATION ], [ MODIFIER ] ;
787    DURATION = ( EVEN-NUMBER, { "." } | { "." }, EVEN-NUMBER | ".", { "." } ) ;
788    EVEN-NUMBER = { INTEGER }, ( "0" | "2" | "4" | "6" | "8" ) ;
789    NOTE = PITCH, [ DURATION ], [ TIE ], { MODIFIER } ;
790    PITCH = (
791        ( LOW-A | LOW-B | LOW-C | LOW-D | LOW-E | LOW-F | LOW-G ), [ ACCIDENTAL ] |
792        ( "a" | "b" | "c" | "d" | "e" | "f" | "g" ), [ ACCIDENTAL ], { "'" } |
793        ( "a" | "b" | "c" | "d" | "e" | "f" | "g" ), { "'" }, [ ACCIDENTAL ]
794    ) ;
795    LOW-A = "A", { "A" } ;
796    LOW-B = "B", { "B" } ;
797    LOW-C = "C", { "C" } ;
798    LOW-D = "D", { "D" } ;
799    LOW-E = "E", { "E" } ;
800    LOW-F = "F", { "F" } ;
801    LOW-G = "G", { "G" } ;
802    ACCIDENTAL = ( EDITORIAL | SHARPS | FLATS | NATURAL ) ;
803    EDITORIAL = "(", ( SHARPS | FLATS | NATURAL ), ")" ;
804    SHARPS = "#", { "#" } ;
805    FLATS = "-", { "-" } ;
806    NATURAL = "n" ;
807    TIE = "~" ;
808    MODIFIER = (
809        EQUALS-MODIFIER |
810        UNDERSCORE-MODIFIER |
811        SQUARE-MODIFIER |
812        ANGLE-MODIFIER |
813        PARENS-MODIFIER |
814        STAR-MODIFIER
815    ) ;
816    EQUALS-MODIFIER = "=", EQUALS-DATA ;
817    UNDERSCORE-MODIFIER = "_", UNDERSCORE-DATA ;
818    SQUARE-MODIFIER = "[", ALPHANUMERIC, "]" ;
819    ANGLE-MODIFIER = "<", ALPHANUMERIC, ">" ;
820    PARENS-MODIFIER = "(", ALPHANUMERIC, ")" ;
821    STAR-MODIFIER = "*", ALPHANUMERIC, "*" ;
822    (* The following is just shorthand. *)
823    ALPHANUMERIC = ? At least one alphanumeric character. So "a-z", "A-Z", or "0-9" ? ;
824    EQUALS-DATA = ? At least one non-whitespace, non-"_" character. ? ;
825    UNDERSCORE-DATA = ? At least one non-whitespace, non-"=" character. ? ;
826    """
827    sharpsFlatsOrNaturalRegex = r'#+|-+|n'
828    editorialRegex = fr'\((?:{sharpsFlatsOrNaturalRegex})\)'
829    accidentalRegex = fr'{editorialRegex}|(?:{sharpsFlatsOrNaturalRegex})'
830
831    lowNoteRegex = fr'(?:A+|B+|C+|D+|E+|F+|G+)(?:{accidentalRegex})?'
832    highNoteRegex = (
833        r'(?:a|b|c|d|e|f|g)'
834        + fr"(?:(?:{accidentalRegex})?'*|'*(?:{accidentalRegex})?)"
835    )
836    noteNameRegex = fr'{lowNoteRegex}|{highNoteRegex}'
837
838    durationRegex = r'\d+\.*|\.*\d+|\.+'
839
840    tieStateRegex = r'~'
841
842    equalsRegex = r'=[^\s_]*'
843    starRegex = r'\*.*?\*'
844    angleRegex = r'<.*?>'
845    parensRegex = r'\(.*?\)'
846    squareRegex = r'\[.*?]'
847    underscoreRegex = r'_[^\s=]'
848    modifierRegex = (
849        fr'{equalsRegex}|{starRegex}|{angleRegex}|'
850        + fr'{parensRegex}|{squareRegex}|{underscoreRegex}'
851    )
852
853    return [
854        (r'^(\d+\/\d+)$', TimeSignatureToken),
855        (
856            fr'^r((?:{durationRegex})?(?:{modifierRegex})*)$',
857            RestToken
858        ),
859        (
860            (
861                fr'^((?:{noteNameRegex})(?:{durationRegex})?'
862                + fr'(?:{tieStateRegex})?(?:{modifierRegex})*)$'
863            ),
864            NoteToken
865        ),  # last
866    ]
867
868
869class Converter:
870    '''
871    Main conversion object for TinyNotation.
872
873    Accepts keywords:
874
875    * `makeNotation=False` to get "classic" TinyNotation formats without
876       measures, Clefs, etc.
877    * `raiseExceptions=True` to make errors become exceptions.
878
879
880    >>> tnc = tinyNotation.Converter('4/4 C##4 D e-8 f~ f f# g4 trip{f8 e d} C2=hello')
881    >>> tnc.parse()
882    <music21.tinyNotation.Converter object at 0x10aeefbe0>
883    >>> tnc.stream.show('text')
884    {0.0} <music21.stream.Measure 1 offset=0.0>
885        {0.0} <music21.clef.TrebleClef>
886        {0.0} <music21.meter.TimeSignature 4/4>
887        {0.0} <music21.note.Note C##>
888        {1.0} <music21.note.Note D>
889        {2.0} <music21.note.Note E->
890        {2.5} <music21.note.Note F>
891        {3.0} <music21.note.Note F>
892        {3.5} <music21.note.Note F#>
893    {4.0} <music21.stream.Measure 2 offset=4.0>
894        {0.0} <music21.note.Note G>
895        {1.0} <music21.note.Note F>
896        {1.3333} <music21.note.Note E>
897        {1.6667} <music21.note.Note D>
898        {2.0} <music21.note.Note C>
899        {4.0} <music21.bar.Barline type=final>
900
901
902    Or, breaking down what Parse does bit by bit:
903
904    >>> tnc = tinyNotation.Converter('4/4 C##4 D e-8 f~ f f# g4 trip{f8 e d} C2=hello')
905    >>> tnc.stream
906    <music21.stream.Part 0x10acee860>
907    >>> tnc.makeNotation
908    True
909    >>> tnc.stringRep
910    '4/4 C##4 D e-8 f~ f f# g4 trip{f8 e d} C2=hello'
911    >>> tnc.activeStates
912    []
913    >>> tnc.preTokens
914    []
915    >>> tnc.splitPreTokens()
916    >>> tnc.preTokens
917    ['4/4', 'C##4', 'D', 'e-8', 'f~', 'f', 'f#', 'g4', 'trip{f8', 'e', 'd}', 'C2=hello']
918    >>> tnc.setupRegularExpressions()
919
920    Then we parse the time signature:
921
922    >>> tnc.parseOne(0, tnc.preTokens[0])
923    >>> tnc.stream.coreElementsChanged()
924    >>> tnc.stream.show('text')
925    {0.0} <music21.meter.TimeSignature 4/4>
926
927    Then the first note:
928
929    >>> tnc.parseOne(1, tnc.preTokens[1])
930    >>> tnc.stream.coreElementsChanged()
931    >>> tnc.stream.show('text')
932    {0.0} <music21.meter.TimeSignature 4/4>
933    {0.0} <music21.note.Note C##>
934
935    The next notes to 'g4' are pretty similar:
936
937    >>> for i in range(2, 8):
938    ...     tnc.parseOne(i, tnc.preTokens[i])
939    >>> tnc.stream.coreElementsChanged()
940    >>> tnc.stream.show('text')
941    {0.0} <music21.meter.TimeSignature 4/4>
942    {0.0} <music21.note.Note C##>
943    {1.0} <music21.note.Note D>
944    {2.0} <music21.note.Note E->
945    {2.5} <music21.note.Note F>
946    {3.0} <music21.note.Note F>
947    {3.5} <music21.note.Note F#>
948    {4.0} <music21.note.Note G>
949
950    The next note starts a "State" since it has a triplet:
951
952    >>> tnc.preTokens[8]
953    'trip{f8'
954    >>> tnc.parseOne(8, tnc.preTokens[8])
955    >>> tnc.activeStates
956    [<music21.tinyNotation.TripletState object at 0x10ae9dba8>]
957    >>> tnc.activeStates[0].affectedTokens
958    [<music21.note.Note F>]
959
960    The state is still active for the next token:
961
962    >>> tnc.preTokens[9]
963    'e'
964    >>> tnc.parseOne(9, tnc.preTokens[9])
965    >>> tnc.activeStates
966    [<music21.tinyNotation.TripletState object at 0x10ae9dba8>]
967    >>> tnc.activeStates[0].affectedTokens
968    [<music21.note.Note F>, <music21.note.Note E>]
969
970    But the next token closes the state:
971
972    >>> tnc.preTokens[10]
973    'd}'
974    >>> tnc.parseOne(10, tnc.preTokens[10])
975    >>> tnc.activeStates
976    []
977    >>> tnc.stream.coreElementsChanged()
978    >>> tnc.stream.show('text')
979    {0.0} <music21.meter.TimeSignature 4/4>
980    ...
981    {4.0} <music21.note.Note G>
982    {5.0} <music21.note.Note F>
983    {5.3333} <music21.note.Note E>
984    {5.6667} <music21.note.Note D>
985
986    The last token has a modifier, which is an IdModifier:
987
988    >>> tnc.preTokens[11]
989    'C2=hello'
990    >>> tnc.parseOne(11, tnc.preTokens[11])
991    >>> tnc.stream.coreElementsChanged()
992    >>> tnc.stream.show('text')
993    {0.0} <music21.meter.TimeSignature 4/4>
994    ...
995    {5.6667} <music21.note.Note D>
996    {6.0} <music21.note.Note C>
997    >>> tnc.stream[-1].id
998    'hello'
999
1000    Then calling tnc.postParse() runs the makeNotation:
1001
1002    >>> tnc.postParse()
1003    >>> tnc.stream.show('text')
1004    {0.0} <music21.stream.Measure 1 offset=0.0>
1005        {0.0} <music21.clef.TrebleClef>
1006        {0.0} <music21.meter.TimeSignature 4/4>
1007        {0.0} <music21.note.Note C##>
1008        {1.0} <music21.note.Note D>
1009        {2.0} <music21.note.Note E->
1010        {2.5} <music21.note.Note F>
1011        {3.0} <music21.note.Note F>
1012        {3.5} <music21.note.Note F#>
1013    {4.0} <music21.stream.Measure 2 offset=4.0>
1014        {0.0} <music21.note.Note G>
1015        {1.0} <music21.note.Note F>
1016        {1.3333} <music21.note.Note E>
1017        {1.6667} <music21.note.Note D>
1018        {2.0} <music21.note.Note C>
1019        {4.0} <music21.bar.Barline type=final>
1020
1021    Normally invalid notes or other tokens pass freely and drop the token:
1022
1023    >>> x = converter.parse('tinyNotation: 4/4 c2 d3 e2')
1024    >>> x.show('text')
1025    {0.0} <music21.stream.Measure 1 offset=0.0>
1026        {0.0} <music21.clef.TrebleClef>
1027        {0.0} <music21.meter.TimeSignature 4/4>
1028        {0.0} <music21.note.Note C>
1029        {2.0} <music21.note.Note E>
1030        {4.0} <music21.bar.Barline type=final>
1031
1032    But with the keyword 'raiseExceptions=True' a `TinyNotationException`
1033    is raised:
1034
1035    >>> x = converter.parse('tinyNotation: 4/4 c2 d3 e2', raiseExceptions=True)
1036    Traceback (most recent call last):
1037    music21.tinyNotation.TinyNotationException: Could not parse token: 'd3'
1038    '''
1039    bracketStateMapping = {
1040        'trip': TripletState,
1041        'quad': QuadrupletState,
1042    }
1043    _modifierEqualsRe = re.compile(r'=([A-Za-z0-9]*)')
1044    _modifierStarRe = re.compile(r'\*(.*?)\*')
1045    _modifierAngleRe = re.compile(r'<(.*?)>')
1046    _modifierParensRe = re.compile(r'\((.*?)\)')
1047    _modifierSquareRe = re.compile(r'\[(.*?)]')
1048    _modifierUnderscoreRe = re.compile(r'_(.*)')
1049
1050    def __init__(self, stringRep='', **keywords):
1051        self.stream = None
1052        self.stateDict = None
1053        self.stringRep = stringRep
1054        self.activeStates = []
1055        self.preTokens = None
1056
1057        self.generalBracketStateRe = re.compile(r'(\w+){')
1058        self.tieStateRe = re.compile(r'~')
1059
1060        self.tokenMap = _getDefaultTokenMap()
1061        self.modifierEquals = IdModifier
1062        self.modifierStar = None
1063        self.modifierAngle = None
1064        self.modifierParens = None
1065        self.modifierSquare = None
1066        self.modifierUnderscore = LyricModifier
1067
1068        self.keywords = keywords
1069
1070        self.makeNotation = keywords.get('makeNotation', True)
1071        self.raiseExceptions = keywords.get('raiseExceptions', False)
1072
1073
1074        self.stateDictDefault = {'currentTimeSignature': None,
1075                                 'lastDuration': 1.0
1076                                 }
1077        self.load(stringRep)
1078        # will be filled by self.setupRegularExpressions()
1079        self._tokenMapRe = None
1080
1081    def load(self, stringRep):
1082        '''
1083        Loads a stringRepresentation into `.stringRep`
1084        and resets the parsing state.
1085
1086        >>> tnc = tinyNotation.Converter()
1087        >>> tnc.load('4/4 c2 d e f')
1088        >>> s = tnc.parse().stream
1089        >>> tnc.load('4/4 f e d c')
1090        >>> s2 = tnc.parse().stream
1091        >>> ns2 = s2.flatten().notes
1092
1093        Check that the duration of 2.0 from the first load did not carry over.
1094
1095        >>> ns2[0].duration.quarterLength
1096        1.0
1097        >>> len(ns2)
1098        4
1099        '''
1100        self.stream = stream.Part()
1101        self.stateDict = copy.copy(self.stateDictDefault)
1102        self.stringRep = stringRep
1103        self.activeStates = []
1104        self.preTokens = []
1105
1106    def splitPreTokens(self):
1107        '''
1108        splits the string into textual preTokens.
1109
1110        Right now just splits on spaces, but might be smarter to ignore spaces in
1111        quotes, etc. later.
1112        '''
1113        self.preTokens = self.stringRep.split()  # do something better alter.
1114
1115    def setupRegularExpressions(self):
1116        '''
1117        Regular expressions get compiled for faster
1118        usage.  This is called automatically by .parse(), but can be
1119        called separately for testing.  It is also important that it
1120        is not called in __init__ since subclasses should override the
1121        tokenMap, etc. for a class.
1122        '''
1123        self._tokenMapRe = []
1124        for rePre, classCall in self.tokenMap:
1125            try:
1126                self._tokenMapRe.append((re.compile(rePre), classCall))
1127            except sre_parse.error as e:
1128                raise TinyNotationException(
1129                    f'Error in compiling token, {rePre}: {e}'
1130                ) from e
1131
1132
1133    def parse(self):
1134        '''
1135        splitPreTokens, setupRegularExpressions, then runs
1136        through each preToken, and runs postParse.
1137        '''
1138        if self.preTokens == [] and self.stringRep != '':
1139            self.splitPreTokens()
1140        if self._tokenMapRe is None:
1141            self.setupRegularExpressions()
1142
1143        for i, t in enumerate(self.preTokens):
1144            self.parseOne(i, t)
1145        self.postParse()
1146        return self
1147
1148
1149    def parseOne(self, i, t):
1150        '''
1151        parse a single token at position i, with
1152        text t, possibly adding it to the stream.
1153
1154        Checks for state changes, modifiers, tokens, and end-state brackets.
1155        '''
1156        t = self.parseStartStates(t)
1157        t, numberOfStatesToEnd = self.parseEndStates(t)
1158        t, activeModifiers = self.parseModifiers(t)
1159
1160        # this copy is done so that an activeState can
1161        # remove itself from this list:
1162        for stateObj in self.activeStates[:]:
1163            t = stateObj.affectTokenBeforeParse(t)
1164
1165        m21Obj = None
1166        tokenObj = None
1167
1168        # parse token with state:
1169        hasMatch = False
1170        for tokenRe, tokenClass in self._tokenMapRe:
1171            matchSuccess = tokenRe.match(t)
1172            if matchSuccess is None:
1173                continue
1174
1175            hasMatch = True
1176            tokenData = matchSuccess.group(1)
1177            tokenObj = tokenClass(tokenData)
1178            try:
1179                m21Obj = tokenObj.parse(self)
1180                if m21Obj is not None:  # can only match one.
1181                    break
1182            except TinyNotationException as excep:
1183                if self.raiseExceptions:
1184                    raise TinyNotationException(f'Could not parse token: {t!r}') from excep
1185
1186        if not hasMatch and self.raiseExceptions:
1187            raise TinyNotationException(f'Could not parse token: {t!r}')
1188
1189        if m21Obj is not None:
1190            for stateObj in self.activeStates[:]:  # iterate over copy so we can remove.
1191                m21Obj = stateObj.affectTokenAfterParseBeforeModifiers(m21Obj)
1192
1193        if m21Obj is not None:
1194            for modObj in activeModifiers:
1195                m21Obj = modObj.postParse(m21Obj)
1196
1197        if m21Obj is not None:
1198            for stateObj in self.activeStates[:]:  # iterate over copy so we can remove.
1199                m21Obj = stateObj.affectTokenAfterParse(m21Obj)
1200
1201        if m21Obj is not None:
1202            self.stream.coreAppend(m21Obj)
1203
1204        for i in range(numberOfStatesToEnd):
1205            stateToRemove = self.activeStates.pop()
1206            possibleObj = stateToRemove.end()
1207            if possibleObj is not None:
1208                self.stream.coreAppend(possibleObj)
1209
1210
1211    def parseStartStates(self, t):
1212        # noinspection PyShadowingNames
1213        '''
1214        Changes the states in self.activeStates, and starts the state given the current data.
1215        Returns a newly processed token.
1216
1217        A contrived example:
1218
1219        >>> tnc = tinyNotation.Converter()
1220        >>> tnc.setupRegularExpressions()
1221        >>> len(tnc.activeStates)
1222        0
1223        >>> tIn = 'trip{quad{f8~'
1224        >>> tOut = tnc.parseStartStates(tIn)
1225        >>> tOut
1226        'f8'
1227        >>> len(tnc.activeStates)
1228        3
1229        >>> tripState = tnc.activeStates[0]
1230        >>> tripState
1231        <music21.tinyNotation.TripletState object at 0x10afaa630>
1232
1233        >>> quadState = tnc.activeStates[1]
1234        >>> quadState
1235        <music21.tinyNotation.QuadrupletState object at 0x10adcb0b8>
1236
1237        >>> tieState = tnc.activeStates[2]
1238        >>> tieState
1239        <music21.tinyNotation.TieState object at 0x10afab048>
1240
1241        >>> tieState.parent
1242        <weakref at 0x10adb31d8; to 'Converter' at 0x10adb42e8>
1243        >>> tieState.parent() is tnc
1244        True
1245        >>> tieState.stateInfo
1246        '~'
1247        >>> quadState.stateInfo
1248        'quad{'
1249
1250
1251        Note that the affected tokens haven't yet been added:
1252
1253        >>> tripState.affectedTokens
1254        []
1255
1256        Unknown state gives a warning or if `.raisesException=True` raises a
1257        TinyNotationException
1258
1259        >>> tnc.raiseExceptions = True
1260        >>> tIn = 'blah{f8~'
1261        >>> tOut = tnc.parseStartStates(tIn)
1262        Traceback (most recent call last):
1263        music21.tinyNotation.TinyNotationException: Incorrect bracket state: 'blah'
1264        '''
1265        bracketMatchSuccess = self.generalBracketStateRe.search(t)
1266        while bracketMatchSuccess:
1267            stateData = bracketMatchSuccess.group(0)
1268            bracketType = bracketMatchSuccess.group(1)
1269            t = self.generalBracketStateRe.sub('', t, count=1)
1270            bracketMatchSuccess = self.generalBracketStateRe.search(t)
1271            if bracketType not in self.bracketStateMapping:
1272                msg = f'Incorrect bracket state: {bracketType!r}'
1273                if self.raiseExceptions:
1274                    raise TinyNotationException(msg)
1275
1276                # else  # pragma: no cover
1277                environLocal.warn(msg)
1278                continue
1279
1280            stateObj = self.bracketStateMapping[bracketType](self, stateData)
1281            stateObj.start()
1282            self.activeStates.append(stateObj)
1283
1284
1285        tieMatchSuccess = self.tieStateRe.search(t)
1286        if tieMatchSuccess:
1287            stateData = tieMatchSuccess.group(0)
1288            t = self.tieStateRe.sub('', t)
1289            tieState = TieState(self, stateData)
1290            tieState.start()
1291            self.activeStates.append(tieState)
1292
1293        return t
1294
1295    def parseEndStates(self, t):
1296        '''
1297        Trims the endState token ('}') from the t string
1298        and then returns a two-tuple of the new token and number
1299        of states to remove:
1300
1301        >>> tnc = tinyNotation.Converter()
1302        >>> tnc.parseEndStates('C4')
1303        ('C4', 0)
1304        >>> tnc.parseEndStates('C4}}')
1305        ('C4', 2)
1306        '''
1307        endBrackets = t.count('}')
1308        t = t.replace('}', '')
1309        return t, endBrackets
1310
1311    def parseModifiers(self, t):
1312        '''
1313        Parses `modifierEquals`, `modifierUnderscore`, `modifierStar`, etc.
1314        for a given token and returns the modified token and a
1315        (possibly empty) list of activeModifiers.
1316
1317        Modifiers affect only the current token.  To affect
1318        multiple tokens, use a :class:`~music21.tinyNotation.State` object.
1319        '''
1320        activeModifiers = []
1321
1322        for modifierName in ('Equals', 'Star', 'Angle', 'Parens', 'Square', 'Underscore'):
1323            modifierClass = getattr(self, 'modifier' + modifierName, None)
1324            if modifierClass is None:
1325                continue
1326            modifierRe = getattr(self, '_modifier' + modifierName + 'Re', None)
1327            foundIt = modifierRe.search(t)
1328            if foundIt is not None:  # is not None is necessary
1329                modifierData = foundIt.group(1)
1330                t = modifierRe.sub('', t)
1331                modifierObject = modifierClass(modifierData, t, self)
1332                activeModifiers.append(modifierObject)
1333
1334        for modObj in activeModifiers:
1335            modObj.preParse(t)
1336
1337        return t, activeModifiers
1338
1339    def postParse(self):
1340        '''
1341        Called after all the tokens have been run.
1342
1343        Currently runs `.makeMeasures` on `.stream` unless `.makeNotation` is `False`.
1344        '''
1345        if self.makeNotation is not False:
1346            self.stream.makeMeasures(inPlace=True)
1347
1348
1349class Test(unittest.TestCase):
1350    parseTest = '1/4 trip{C8~ C~_hello C=mine} F~ F~ 2/8 F F# quad{g--16 a## FF(n) g#} g16 F0'
1351
1352    def testOne(self) -> None:
1353        c = Converter(self.parseTest)
1354        c.parse()
1355        s = c.stream
1356        sfn = s.flatten().notes
1357        self.assertEqual(sfn[0].tie.type, 'start')
1358        self.assertEqual(sfn[1].tie.type, 'continue')
1359        self.assertEqual(sfn[2].tie.type, 'stop')
1360        self.assertEqual(sfn[0].step, 'C')
1361        self.assertEqual(sfn[0].octave, 3)
1362        self.assertEqual(sfn[1].lyric, 'hello')
1363        self.assertEqual(sfn[2].id, 'mine')
1364        self.assertEqual(sfn[6].pitch.accidental.alter, 1)
1365        self.assertEqual(sfn[7].pitch.accidental.alter, -2)
1366        self.assertEqual(sfn[9].editorial.ficta.alter, 0)
1367        self.assertEqual(sfn[12].duration.quarterLength, 1.0)
1368        self.assertEqual(sfn[12].expressions[0].classes, expressions.Fermata().classes)
1369
1370    def testRaiseExceptions(self) -> None:
1371        error_states = [
1372            {
1373                'string': 'h',
1374                'reason': 'h is not a valid note',
1375            },
1376            {
1377                'string': 'a;',
1378                'reason': 'a semicolon is not a valid character or modifier',
1379            },
1380            {
1381                'string': 'r;',
1382                'reason': 'a semicolon is not a valid character or modifier',
1383            },
1384            {
1385                'string': '4/4;',
1386                'reason': 'a semicolon is not a valid character or modifier',
1387            },
1388            {
1389                'string': 'ABC',
1390                'reason': (
1391                    'only the same upper-cased letter may be repeated to '
1392                    + 'indicate lower octaves'
1393                ),
1394            },
1395            {
1396                'string': 'aaa',
1397                'reason': (
1398                    'the same lower-cased letter may not be repeated to '
1399                    + 'indicate higher octaves. Instead use apostrophes.'
1400                ),
1401            },
1402        ]
1403
1404        for error_state in error_states:
1405            with self.assertRaises(TinyNotationException, msg=(
1406                    'Should have raised a TinyNotationException for input '
1407                    + f"'{error_state['string']}' because {error_state['reason']}."
1408            )):
1409                converter = Converter(error_state['string'], raiseExceptions=True)
1410                converter.parse()
1411
1412    def testGetDefaultTokenMap(self) -> None:
1413        defaultTokenMap = _getDefaultTokenMap()
1414
1415        self.assertEqual(
1416            len(defaultTokenMap),
1417            3,
1418            (
1419                'There should be three valid token types by default: Time '
1420                + 'signatures, Notes, and Rests'
1421            )
1422        )
1423
1424        validTokenTypeCounts = {
1425            NoteToken: 0,
1426            RestToken: 0,
1427            TimeSignatureToken: 0,
1428        }
1429
1430        for regex, tokenType in defaultTokenMap:
1431            self.assertIn(
1432                tokenType,
1433                validTokenTypeCounts,
1434                (
1435                    'Found unexpected token type in default token map:'
1436                    + f'{tokenType.__class__.__name__}.'
1437                )
1438            )
1439            validTokenTypeCounts[tokenType] += 1
1440            self.assertGreater(
1441                len(regex),
1442                0,
1443                (
1444                    'Should provide a non-empty string for the regular '
1445                    + 'expression in the default token map for tokens of type '
1446                    + f'{tokenType.__class__.__name__}.'
1447                )
1448            )
1449
1450        for tokenType in validTokenTypeCounts:
1451            self.assertEqual(
1452                validTokenTypeCounts[tokenType],
1453                1,
1454                (
1455                    'Should have found each valid token type exactly once in '
1456                    + 'the default token map.'
1457                )
1458            )
1459
1460
1461
1462class TestExternal(unittest.TestCase):
1463    show = True
1464
1465    def testOne(self):
1466        c = Converter(Test.parseTest)
1467        c.parse()
1468        if self.show:
1469            c.stream.show('musicxml.png')
1470
1471
1472# TODO: Chords
1473# ------------------------------------------------------------------------------
1474# define presented order in documentation
1475_DOC_ORDER = [Converter, Token, State, Modifier]
1476
1477if __name__ == '__main__':
1478    import music21
1479    music21.mainTest(Test)
1480