1# -*- coding: utf-8 -*-
2# ------------------------------------------------------------------------------
3# Name:         abc/__init__.py
4# Purpose:      parses ABC Notation
5#
6# Authors:      Christopher Ariza
7#               Dylan J. Nagler
8#               Michael Scott Cuthbert
9#
10# Copyright:    Copyright © 2010, 2013 Michael Scott Cuthbert and the music21 Project
11# License:      BSD, see license.txt
12# ------------------------------------------------------------------------------
13'''
14ABC is a music format that, while being able to encode all sorts of scores, is especially
15strong at representing monophonic music, and folk music in particular.
16
17Modules in the `music21.abcFormat` package deal with importing ABC into music21.  Most people
18working with ABC data won't need to use this package.  To convert ABC from a file or URL
19to a :class:`~music21.stream.Stream` use the :func:`~music21.converter.parse` function of
20the `converter` module:
21
22>>> #_DOCS_SHOW from music21 import *
23>>> #_DOCS_SHOW abcScore = converter.parse('/users/ariza/myScore.abc')
24
25For users who will be editing ABC extensively or need a way to have music21 output ABC
26(which it doesn't do natively), we suggest using the open source EasyABC package:
27https://www.nilsliberg.se/ksp/easyabc/ .  You can set it up as a MusicXML reader through:
28
29>>> #_DOCS_SHOW us = environment.UserSettings()
30>>> #_DOCS_SHOW us['musicxmlPath'] = '/Applications/EasyABC.app'
31
32or wherever you have downloaded EasyABC to
33(PC users might need: 'c:/program files (x86)/easyabc/easyabc.exe')
34(Thanks to Norman Schmidt for the heads up)
35
36There is a two-step process in converting ABC files to Music21 Streams.  First this module
37reads in the text-based .abc file and converts all the information into ABCToken objects.  Then
38the function :func:`music21.abcFormat.translate.abcToStreamScore` of
39the :ref:`moduleAbcFormatTranslate` module
40translates those Tokens into music21 objects.
41'''
42__all__ = [
43    'translate',
44    'testFiles',
45    'ABCTokenException', 'ABCHandlerException', 'ABCFileException',
46    'ABCToken',
47    'ABCMetadata', 'ABCBar', 'ABCTuplet', 'ABCTie',
48    'ABCSlurStart', 'ABCParenStop', 'ABCCrescStart', 'ABCDimStart',
49    'ABCStaccato', 'ABCUpbow', 'ABCDownbow', 'ABCAccent', 'ABCStraccent',
50    'ABCTenuto', 'ABCGraceStart', 'ABCGraceStop', 'ABCBrokenRhythmMarker',
51    'ABCNote', 'ABCChord',
52    'ABCHandler', 'ABCHandlerBar',
53    'mergeLeadingMetaData',
54    'ABCFile',
55]
56
57import io
58import re
59import unittest
60from typing import Union, Optional, List, Tuple, Any
61
62from music21 import common
63from music21 import environment
64from music21 import exceptions21
65from music21 import prebase
66
67from music21.abcFormat import translate
68
69environLocal = environment.Environment('abcFormat')
70
71# for implementation
72# see http://abcnotation.com/abc2mtex/abc.txt
73
74# store symbol and m21 naming/class eq
75ABC_BARS = [
76    (':|1', 'light-heavy-repeat-end-first'),
77    (':|2', 'light-heavy-repeat-end-second'),
78    ('|]', 'light-heavy'),
79    ('||', 'light-light'),
80    ('[|', 'heavy-light'),
81    ('[1', 'regular-first'),  # preferred format
82    ('[2', 'regular-second'),
83    ('|1', 'regular-first'),  # gets converted
84    ('|2', 'regular-second'),
85    (':|', 'light-heavy-repeat-end'),
86    ('|:', 'heavy-light-repeat-start'),
87    ('::', 'heavy-heavy-repeat-bidirectional'),
88    # for comparison, single chars must go last
89    ('|', 'regular'),
90    (':', 'dotted'),
91]
92
93# store a mapping of ABC representation to pitch values
94_pitchTranslationCache = {}
95
96
97# ------------------------------------------------------------------------------
98# note inclusion of w: for lyrics
99reMetadataTag = re.compile('[A-Zw]:')
100rePitchName = re.compile('[a-gA-Gz]')
101reChordSymbol = re.compile('"[^"]*"')  # non greedy
102reChord = re.compile('[.*?]')  # non greedy
103reAbcVersion = re.compile(r'^%abc-((\d+)\.(\d+)\.?(\d+)?)')
104reDirective = re.compile(r'^%%([a-z\-]+)\s+([^\s]+)(.*)')
105
106
107# ------------------------------------------------------------------------------
108class ABCTokenException(exceptions21.Music21Exception):
109    pass
110
111
112class ABCHandlerException(exceptions21.Music21Exception):
113    pass
114
115
116class ABCFileException(exceptions21.Music21Exception):
117    pass
118#
119
120
121# ------------------------------------------------------------------------------
122class ABCToken(prebase.ProtoM21Object):
123    '''
124    ABC processing works with a multi-pass procedure. The first pass
125    breaks the data stream into a list of ABCToken objects. ABCToken
126    objects are specialized in subclasses.
127
128    The multi-pass procedure is conducted by an ABCHandler object.
129    The ABCHandler.tokenize() method breaks the data stream into
130    ABCToken objects. The :meth:`~music21.abcFormat.ABCHandler.tokenProcess` method first
131    calls the :meth:`~music21.abcFormat.ABCToken.preParse` method on each token,
132    then does contextual
133    adjustments to all tokens, then calls :meth:`~music21.abcFormat.ABCToken.parse` on all tokens.
134
135    The source ABC string itself is stored in self.src
136    '''
137    def __init__(self, src=''):
138        self.src: str = src  # store source character sequence
139
140    def _reprInternal(self):
141        return repr(self.src)
142
143    @staticmethod
144    def stripComment(strSrc):
145        '''
146        removes ABC-style comments from a string:
147
148        >>> ao = abcFormat.ABCToken()
149        >>> ao.stripComment('asdf')
150        'asdf'
151        >>> ao.stripComment('asdf%234')
152        'asdf'
153        >>> ao.stripComment('asdf  %     234')
154        'asdf  '
155        >>> ao.stripComment('[ceg]% this chord appears 50% more often than other chords do')
156        '[ceg]'
157
158        This is a static method, so it can also be called on the class itself:
159
160        >>> abcFormat.ABCToken.stripComment('b1 % a b-flat actually')
161        'b1 '
162
163        Changed: v6.2 -- made a staticmethod
164        '''
165        if '%' in strSrc:
166            return strSrc.split('%')[0]
167        return strSrc
168
169    def preParse(self):
170        '''
171        Dummy method that is called before contextual adjustments.
172        Designed to be subclassed or overridden.
173        '''
174        pass
175
176    def parse(self):
177        '''
178        Dummy method that reads self.src and loads attributes.
179        It is called after contextual adjustments.
180
181        It is designed to be subclassed or overridden.
182        '''
183        pass
184
185
186class ABCMetadata(ABCToken):
187    '''
188    Defines a token of metadata in ABC.
189
190    >>> md = abcFormat.ABCMetadata('I:linebreak')
191    >>> md.src
192    'I:linebreak'
193
194    Has two attributes, `tag` and `data` which are strings or None.
195    Initially both are set to None:
196
197    >>> print(md.tag)
198    None
199
200    After calling `preParse()`, these are separated:
201
202    >>> md.preParse()
203    >>> md.tag
204    'I'
205    >>> md.data
206    'linebreak'
207    '''
208    # given a logical unit, create an object
209    # may be a chord, notes, metadata, bars
210    def __init__(self, src=''):
211        super().__init__(src)
212        self.tag: Optional[str] = None
213        self.data: Optional[str] = None
214
215    def preParse(self) -> None:
216        '''
217        Called before contextual adjustments and needs
218        to have access to data.  Divides a token into
219        .tag (a single capital letter or w) and .data representations.
220
221        >>> x = abcFormat.ABCMetadata('T:tagData')
222        >>> x.preParse()
223        >>> x.tag
224        'T'
225        >>> x.data
226        'tagData'
227        '''
228        div = reMetadataTag.match(self.src).end()
229        strSrc = self.stripComment(self.src)  # remove any comments
230        self.tag = strSrc[:div - 1]  # do not get colon, :
231        self.data = strSrc[div:].strip()  # remove leading/trailing
232
233    def parse(self):
234        pass
235
236    def isDefaultNoteLength(self) -> bool:
237        '''
238        Returns True if the tag is "L", False otherwise.
239        '''
240        if self.tag == 'L':
241            return True
242        return False
243
244    def isReferenceNumber(self) -> bool:
245        '''
246        Returns True if the tag is "X", False otherwise.
247
248        >>> x = abcFormat.ABCMetadata('X:5')
249        >>> x.preParse()
250        >>> x.tag
251        'X'
252        >>> x.isReferenceNumber()
253        True
254        '''
255        if self.tag == 'X':
256            return True
257        return False
258
259    def isMeter(self) -> bool:
260        '''
261        Returns True if the tag is "M" for meter, False otherwise.
262        '''
263        if self.tag == 'M':
264            return True
265        return False
266
267    def isTitle(self) -> bool:
268        '''
269        Returns True if the tag is "T" for title, False otherwise.
270        '''
271        if self.tag == 'T':
272            return True
273        return False
274
275    def isComposer(self) -> bool:
276        '''
277        Returns True if the tag is "C" for composer, False otherwise.
278        '''
279        if self.tag == 'C':
280            return True
281        return False
282
283    def isOrigin(self) -> bool:
284        '''
285        Returns True if the tag is "O" for origin, False otherwise.
286        This value is set in the Metadata `localOfComposition` of field.
287        '''
288        if self.tag == 'O':
289            return True
290        return False
291
292    def isVoice(self) -> bool:
293        '''
294        Returns True if the tag is "V", False otherwise.
295        '''
296        if self.tag == 'V':
297            return True
298        return False
299
300    def isKey(self) -> bool:
301        '''
302        Returns True if the tag is "K", False otherwise.
303        Note that in some cases a Key will encode clef information.
304
305        (example from corpus: josquin/laDeplorationDeLaMorteDeJohannesOckeghem.abc)
306        '''
307        if self.tag == 'K':
308            return True
309        return False
310
311    def isTempo(self) -> bool:
312        '''
313        Returns True if the tag is "Q" for tempo, False otherwise.
314        '''
315        if self.tag == 'Q':
316            return True
317        return False
318
319    def getTimeSignatureParameters(self):
320        '''
321        If there is a time signature representation available,
322        get a numerator, denominator and an abbreviation symbol.
323        To get a music21 :class:`~music21.meter.TimeSignature` object, use
324        the :meth:`~music21.abcFormat.ABCMetadata.getTimeSignatureObject` method.
325
326        >>> am = abcFormat.ABCMetadata('M:2/2')
327        >>> am.preParse()
328        >>> am.isMeter()
329        True
330        >>> am.getTimeSignatureParameters()
331        (2, 2, 'normal')
332
333        >>> am = abcFormat.ABCMetadata('M:C|')
334        >>> am.preParse()
335        >>> am.getTimeSignatureParameters()
336        (2, 2, 'cut')
337
338        >>> am = abcFormat.ABCMetadata('M: none')
339        >>> am.preParse()
340        >>> am.getTimeSignatureParameters() is None
341        True
342
343        >>> am = abcFormat.ABCMetadata('M: FREI4/4')
344        >>> am.preParse()
345        >>> am.getTimeSignatureParameters()
346        (4, 4, 'normal')
347        '''
348        if not self.isMeter():
349            raise ABCTokenException('no time signature associated with this metadata')
350
351        if self.data.lower() == 'none':
352            return None
353        elif self.data == 'C':
354            n, d = 4, 4
355            symbol = 'common'  # m21 compat
356        elif self.data == 'C|':
357            n, d = 2, 2
358            symbol = 'cut'  # m21 compat
359        else:
360            n, d = self.data.split('/')
361            # using get number from string to handle odd cases such as
362            # FREI4/4
363            n = int(common.getNumFromStr(n.strip())[0])
364            d = int(common.getNumFromStr(d.strip())[0])
365            symbol = 'normal'  # m21 compat
366        return n, d, symbol
367
368    def getTimeSignatureObject(self):
369        '''
370        Return a music21 :class:`~music21.meter.TimeSignature`
371        object for this metadata tag, if isMeter is True, otherwise raise exception.
372
373        >>> am = abcFormat.ABCMetadata('M:2/2')
374        >>> am.preParse()
375        >>> ts = am.getTimeSignatureObject()
376        >>> ts
377        <music21.meter.TimeSignature 2/2>
378
379        >>> am = abcFormat.ABCMetadata('Q:40')
380        >>> am.getTimeSignatureObject()
381        Traceback (most recent call last):
382        music21.abcFormat.ABCTokenException: no time signature associated with
383            this non-metrical metadata.
384        '''
385        if not self.isMeter():
386            raise ABCTokenException(
387                'no time signature associated with this non-metrical metadata.')
388        from music21 import meter
389        parameters = self.getTimeSignatureParameters()
390        if parameters is None:
391            return None
392        else:
393            numerator, denominator, unused_symbol = parameters
394            return meter.TimeSignature(f'{numerator}/{denominator}')
395
396    def getKeySignatureParameters(self):
397        # noinspection SpellCheckingInspection
398        '''
399        Extract key signature parameters, include indications for mode,
400        and translate sharps count compatible with m21,
401        returning the number of sharps and the mode.
402
403        >>> from music21 import abcFormat
404
405        >>> am = abcFormat.ABCMetadata('K:Eb Lydian')
406        >>> am.preParse()
407        >>> am.getKeySignatureParameters()
408        (-2, 'lydian')
409
410        >>> am = abcFormat.ABCMetadata('K:APhry')
411        >>> am.preParse()
412        >>> am.getKeySignatureParameters()
413        (-1, 'phrygian')
414
415        >>> am = abcFormat.ABCMetadata('K:G Mixolydian')
416        >>> am.preParse()
417        >>> am.getKeySignatureParameters()
418        (0, 'mixolydian')
419
420        >>> am = abcFormat.ABCMetadata('K: Edor')
421        >>> am.preParse()
422        >>> am.getKeySignatureParameters()
423        (2, 'dorian')
424
425        >>> am = abcFormat.ABCMetadata('K: F')
426        >>> am.preParse()
427        >>> am.getKeySignatureParameters()
428        (-1, 'major')
429
430        >>> am = abcFormat.ABCMetadata('K:G')
431        >>> am.preParse()
432        >>> am.getKeySignatureParameters()
433        (1, 'major')
434
435        >>> am = abcFormat.ABCMetadata('K:Gm')
436        >>> am.preParse()
437        >>> am.getKeySignatureParameters()
438        (-2, 'minor')
439
440        >>> am = abcFormat.ABCMetadata('K:Hp')
441        >>> am.preParse()
442        >>> am.getKeySignatureParameters()
443        (2, None)
444
445        >>> am = abcFormat.ABCMetadata('K:G ionian')
446        >>> am.preParse()
447        >>> am.getKeySignatureParameters()
448        (1, 'ionian')
449
450        >>> am = abcFormat.ABCMetadata('K:G aeol')
451        >>> am.preParse()
452        >>> am.getKeySignatureParameters()
453        (-2, 'aeolian')
454
455        '''
456        # placing this import in method for now; key.py may import this module
457        from music21 import key
458
459        if not self.isKey():
460            raise ABCTokenException('no key signature associated with this metadata.')
461
462        # abc uses b for flat in key spec only
463        keyNameMatch = ['c', 'g', 'd', 'a', 'e', 'b', 'f#', 'g#', 'a#',
464                        'f', 'bb', 'eb', 'd#', 'ab', 'e#', 'db', 'c#', 'gb', 'cb',
465                        # HP or Hp are used for highland pipes
466                        'hp']
467
468        # if no match, provide defaults,
469        # this is probably an error or badly formatted
470        standardKeyStr = 'C'
471        stringRemain = ''
472        # first, get standard key indication
473        for target in sorted(keyNameMatch, key=len, reverse=True):
474            if target == self.data[:len(target)].lower():
475                # keep case
476                standardKeyStr = self.data[:len(target)]
477                stringRemain = self.data[len(target):]
478                break
479
480        if len(standardKeyStr) > 1 and standardKeyStr[1] == 'b':
481            standardKeyStr = standardKeyStr[0] + '-'
482
483        mode = None
484        stringRemain = stringRemain.strip()
485        if stringRemain == '':
486            # Assume mode is major by default
487            mode = 'major'
488        else:
489            # only first three characters are parsed
490            modeCandidate = stringRemain.lower()
491            for match, modeStr in (
492                ('dor', 'dorian'),
493                ('phr', 'phrygian'),
494                ('lyd', 'lydian'),
495                ('mix', 'mixolydian'),
496                ('maj', 'major'),
497                ('ion', 'ionian'),
498                ('aeo', 'aeolian'),
499                ('m', 'minor'),
500            ):
501                if modeCandidate.startswith(match):
502                    mode = modeStr
503                    break
504
505        # Special case for highland pipes
506        # replace a flat symbol if found; only the second char
507        if standardKeyStr == 'HP':
508            standardKeyStr = 'C'  # no sharp or flats
509            mode = None
510        elif standardKeyStr == 'Hp':
511            standardKeyStr = 'D'  # use F#, C#, Gn
512            mode = None
513
514        # not yet implemented: checking for additional chromatic alternations
515        # e.g.: K:D =c would write the key signature as two sharps
516        # (key of D) but then mark every  c  as  natural
517        return key.pitchToSharps(standardKeyStr, mode), mode
518
519    def getKeySignatureObject(self):
520        # noinspection SpellCheckingInspection,PyShadowingNames
521        '''
522        Return a music21 :class:`~music21.key.KeySignature` or :class:`~music21.key.Key`
523        object for this metadata tag.
524
525
526        >>> am = abcFormat.ABCMetadata('K:G')
527        >>> am.preParse()
528        >>> ks = am.getKeySignatureObject()
529        >>> ks
530        <music21.key.Key of G major>
531
532        >>> am = abcFormat.ABCMetadata('K:Gmin')
533        >>> am.preParse()
534        >>> ks = am.getKeySignatureObject()
535        >>> ks
536        <music21.key.Key of g minor>
537        >>> ks.sharps
538        -2
539
540        Note that capitalization does not matter
541        (http://abcnotation.com/wiki/abc:standard:v2.1#kkey)
542        so this should still be minor.
543
544        >>> am = abcFormat.ABCMetadata('K:GM')
545        >>> am.preParse()
546        >>> ks = am.getKeySignatureObject()
547        >>> ks
548        <music21.key.Key of g minor>
549        '''
550        if not self.isKey():
551            raise ABCTokenException('no key signature associated with this metadata')
552        from music21 import key
553        # return values of getKeySignatureParameters are sharps, mode
554        # need to unpack list w/ *
555        sharps, mode = self.getKeySignatureParameters()
556        ks = key.KeySignature(sharps)
557        if mode in (None, ''):
558            return ks
559        else:
560            return ks.asKey(mode)
561
562    def getClefObject(self) -> Tuple[Optional['music21.clef.Clef'], Optional[int]]:
563        '''
564        Extract any clef parameters stored in the key metadata token.
565        Assume that a clef definition suggests a transposition.
566        Return both the Clef and the transposition.
567
568        Returns a two-element tuple of clefObj and transposition in semitones
569
570        >>> am = abcFormat.ABCMetadata('K:Eb Lydian bass')
571        >>> am.preParse()
572        >>> am.getClefObject()
573        (<music21.clef.BassClef>, -24)
574        '''
575        if not self.isKey():
576            raise ABCTokenException(
577                'no key signature associated with this metadata; needed for getting Clef Object')
578
579        # placing this import in method for now; key.py may import this module
580        clefObj = None
581        t = None
582
583        from music21 import clef
584        if '-8va' in self.data.lower():
585            clefObj = clef.Treble8vbClef()
586            t = -12
587        elif 'bass' in self.data.lower():
588            clefObj = clef.BassClef()
589            t = -24
590
591        # if not defined, returns None, None
592        return clefObj, t
593
594    def getMetronomeMarkObject(self) -> Optional['music21.tempo.MetronomeMark']:
595        '''
596        Extract any tempo parameters stored in a tempo metadata token.
597
598        >>> am = abcFormat.ABCMetadata('Q: "Allegro" 1/4=120')
599        >>> am.preParse()
600        >>> am.getMetronomeMarkObject()
601        <music21.tempo.MetronomeMark Allegro Quarter=120.0>
602
603        >>> am = abcFormat.ABCMetadata('Q: 3/8=50 "Slowly"')
604        >>> am.preParse()
605        >>> am.getMetronomeMarkObject()
606        <music21.tempo.MetronomeMark Slowly Dotted Quarter=50.0>
607
608        >>> am = abcFormat.ABCMetadata('Q:1/2=120')
609        >>> am.preParse()
610        >>> am.getMetronomeMarkObject()
611        <music21.tempo.MetronomeMark animato Half=120.0>
612
613        >>> am = abcFormat.ABCMetadata('Q:1/4 3/8 1/4 3/8=40')
614        >>> am.preParse()
615        >>> am.getMetronomeMarkObject()
616        <music21.tempo.MetronomeMark grave Whole tied to Quarter (5 total QL)=40.0>
617
618        >>> am = abcFormat.ABCMetadata('Q:90')
619        >>> am.preParse()
620        >>> am.getMetronomeMarkObject()
621        <music21.tempo.MetronomeMark maestoso Quarter=90.0>
622
623        '''
624        if not self.isTempo():
625            raise ABCTokenException('no tempo associated with this metadata')
626        mmObj = None
627        from music21 import tempo
628        # see if there is a text expression in quotes
629        tempoStr = None
630        if '"' in self.data:
631            tempoStr = []
632            nonText = []
633            isOpen = False
634            for char in self.data:
635                if char == '"' and not isOpen:
636                    isOpen = True
637                    continue
638                if char == '"' and isOpen:
639                    isOpen = False
640                    continue
641                if isOpen:
642                    tempoStr.append(char)
643                else:  # gather all else
644                    nonText.append(char)
645            tempoStr = ''.join(tempoStr).strip()
646            nonText = ''.join(nonText).strip()
647        else:
648            nonText = self.data.strip()
649
650        # get a symbolic and numerical value if available
651        number = None
652        referent = None
653        if nonText:
654            if '=' in nonText:
655                durs, number = nonText.split('=')
656                number = float(number)
657                # there may be more than one dur divided by a space
658                referent = 0.0  # in quarter lengths
659                for dur in durs.split(' '):
660                    if dur.count('/') > 0:
661                        n, d = dur.split('/')
662                    else:  # this is an error case
663                        environLocal.printDebug(['incorrectly encoded / unparsable duration:', dur])
664                        n, d = '1', '1'
665                    # n and d might be strings...
666                    referent += (float(n) / float(d)) * 4
667            else:  # assume we just have a quarter definition, e.g., Q:90
668                number = float(nonText)
669
670        # print(nonText, tempoStr)
671        if tempoStr is not None or number is not None:
672            mmObj = tempo.MetronomeMark(text=tempoStr, number=number,
673                                        referent=referent)
674        # returns None if not defined
675        return mmObj
676
677    def getDefaultQuarterLength(self) -> float:
678        r'''
679        If there is a quarter length representation available, return it as a floating point value
680
681        >>> am = abcFormat.ABCMetadata('L:1/2')
682        >>> am.preParse()
683        >>> am.getDefaultQuarterLength()
684        2.0
685
686        >>> am = abcFormat.ABCMetadata('L:1/8')
687        >>> am.preParse()
688        >>> am.getDefaultQuarterLength()
689        0.5
690
691        >>> am = abcFormat.ABCMetadata('M:C|')
692        >>> am.preParse()
693        >>> am.getDefaultQuarterLength()
694        0.5
695
696
697        If taking from meter, find the "fraction" and if < 0.75 use sixteenth notes.
698        If >= 0.75 use eighth notes.
699
700        >>> am = abcFormat.ABCMetadata('M:2/4')
701        >>> am.preParse()
702        >>> am.getDefaultQuarterLength()
703        0.25
704
705        >>> am = abcFormat.ABCMetadata('M:3/4')
706        >>> am.preParse()
707        >>> am.getDefaultQuarterLength()
708        0.5
709
710
711        >>> am = abcFormat.ABCMetadata('M:6/8')
712        >>> am.preParse()
713        >>> am.getDefaultQuarterLength()
714        0.5
715
716
717        Meter is only used for default length if there is no L:
718
719        >>> x = 'L:1/4\nM:3/4\n\nf'
720        >>> sc = converter.parse(x, format='abc')
721        >>> sc.recurse().notes.first().duration.type
722        'quarter'
723        '''
724        # environLocal.printDebug(['getDefaultQuarterLength', self.data])
725        if self.isDefaultNoteLength() and '/' in self.data:
726            # should be in L:1/4 form
727            n, d = self.data.split('/')
728            n = int(n.strip())
729            # the notation L: 1/G is found in some essen files
730            # this is extremely uncommon and might be an error
731            if d == 'G':
732                d = 4  # assume a default
733            else:
734                d = int(d.strip())
735            # 1/4 is 1, 1/8 is 0.5
736            return n * 4 / d
737
738        elif self.isMeter():
739            # if meter auto-set a default not length
740            parameters = self.getTimeSignatureParameters()
741            if parameters is None:
742                return 0.5  # TODO: assume default, need to configure
743            n, d, unused_symbol = parameters
744            if n / d < 0.75:
745                return 0.25  # less than 0.75 the default is a sixteenth note
746            else:
747                return 0.5  # otherwise it is an eighth note
748        else:  # pragma: no cover
749            raise ABCTokenException(
750                f'no quarter length associated with this metadata: {self.data}')
751
752
753class ABCBar(ABCToken):
754    # given a logical unit, create an object
755    # may be a chord, notes, metadata, bars
756    def __init__(self, src):
757        super().__init__(src)
758        self.barType = None  # repeat or barline
759        self.barStyle = None  # regular, heavy-light, etc
760        self.repeatForm = None  # end, start, bidrectional, first, second
761
762    def parse(self):
763        '''
764        Assign the bar-type based on the source string.
765
766        >>> ab = abcFormat.ABCBar('|')
767        >>> ab.parse()
768        >>> ab
769        <music21.abcFormat.ABCBar '|'>
770
771        >>> ab.barType
772        'barline'
773        >>> ab.barStyle
774        'regular'
775
776        >>> ab = abcFormat.ABCBar('||')
777        >>> ab.parse()
778        >>> ab.barType
779        'barline'
780        >>> ab.barStyle
781        'light-light'
782
783        >>> ab = abcFormat.ABCBar('|:')
784        >>> ab.parse()
785        >>> ab.barType
786        'repeat'
787        >>> ab.barStyle
788        'heavy-light'
789        >>> ab.repeatForm
790        'start'
791        '''
792        for abcStr, barTypeString in ABC_BARS:
793            if abcStr == self.src.strip():
794                # this gets lists of elements like
795                # light-heavy-repeat-end
796                barTypeComponents = barTypeString.split('-')
797                # this is a list of attributes
798                if 'repeat' in barTypeComponents:
799                    self.barType = 'repeat'
800                elif ('first' in barTypeComponents
801                      or 'second' in barTypeComponents):
802                    self.barType = 'barline'
803                    # environLocal.printDebug(['got repeat 1/2:', self.src])
804                else:
805                    self.barType = 'barline'
806
807                # case of regular, dotted
808                if len(barTypeComponents) == 1:
809                    self.barStyle = barTypeComponents[0]
810
811                # case of light-heavy, light-light, etc
812                elif len(barTypeComponents) >= 2:
813                    # must get out cases of the start-tags for repeat boundaries
814                    # not yet handling
815                    if 'first' in barTypeComponents:
816                        self.barStyle = 'regular'
817                        self.repeatForm = 'first'  # not a repeat
818                    elif 'second' in barTypeComponents:
819                        self.barStyle = 'regular'
820                        self.repeatForm = 'second'  # not a repeat
821                    else:
822                        self.barStyle = barTypeComponents[0] + '-' + barTypeComponents[1]
823                # repeat form is either start/end for normal repeats
824                # get extra repeat information; start, end, first, second
825                if len(barTypeComponents) > 2:
826                    self.repeatForm = barTypeComponents[3]
827
828    def isRepeat(self):
829        if self.barType == 'repeat':
830            return True
831        else:
832            return False
833
834    def isRegular(self) -> bool:
835        '''
836        Return True if this is a regular, single, light bar line.
837
838        >>> ab = abcFormat.ABCBar('|')
839        >>> ab.parse()
840        >>> ab.isRegular()
841        True
842        '''
843        if self.barType != 'repeat' and self.barStyle == 'regular':
844            return True
845        else:
846            return False
847
848    def isRepeatBracket(self) -> Union[int, bool]:
849        '''
850        Return a number if this defines a repeat bracket for an alternate ending
851        otherwise returns False.
852
853        >>> ab = abcFormat.ABCBar('[2')
854        >>> ab.parse()
855        >>> ab.isRepeat()
856        False
857        >>> ab.isRepeatBracket()
858        2
859        '''
860        if self.repeatForm == 'first':
861            return 1  # we need a number
862        elif self.repeatForm == 'second':
863            return 2
864        else:
865            return False
866
867    def getBarObject(self) -> Optional['music21.bar.Barline']:
868        '''
869        Return a music21 bar object
870
871        >>> ab = abcFormat.ABCBar('|:')
872        >>> ab.parse()
873        >>> barObject = ab.getBarObject()
874        >>> barObject
875         <music21.bar.Repeat direction=start>
876        '''
877        from music21 import bar
878        if self.isRepeat():
879            if self.repeatForm in ('end', 'start'):
880                m21bar = bar.Repeat(direction=self.repeatForm)
881            # bidirectional repeat tokens should already have been replaced
882            # by end and start
883            else:  # pragma: no cover
884                environLocal.printDebug(
885                    [f'found an unsupported repeatForm in ABC: {self.repeatForm}']
886                )
887                m21bar = None
888        elif self.barStyle == 'regular':
889            m21bar = None  # do not need an object for regular
890        elif self.repeatForm in ('first', 'second'):
891            # do nothing, as this is handled in translation
892            m21bar = None
893        else:
894            m21bar = bar.Barline(self.barStyle)
895        return m21bar
896
897
898class ABCTuplet(ABCToken):
899    '''
900    ABCTuplet tokens always precede the notes they describe.
901
902    In ABCHandler.tokenProcess(), rhythms are adjusted.
903    '''
904    def __init__(self, src):
905        super().__init__(src)
906
907        # self.qlRemain = None  # how many ql are left of this tuplets activity
908        # how many notes are affected by this; this assumes equal duration
909        self.noteCount = None
910
911        # actual is tuplet represented value; 3 in 3:2
912        self.numberNotesActual = None
913        # self.durationActual = None
914
915        # normal is underlying duration representation; 2 in 3:2
916        self.numberNotesNormal = None
917        # self.durationNormal = None
918
919        # store an m21 tuplet object
920        self.tupletObj = None
921
922    def updateRatio(self, keySignatureObj=None):
923        # noinspection PyShadowingNames
924        '''
925        Cannot be called until local meter context
926        is established.
927
928        >>> at = abcFormat.ABCTuplet('(3')
929        >>> at.updateRatio()
930        >>> at.numberNotesActual, at.numberNotesNormal
931        (3, 2)
932
933        Generally a 5:n tuplet is 5 in the place of 2.
934
935        >>> at = abcFormat.ABCTuplet('(5')
936        >>> at.updateRatio()
937        >>> at.numberNotesActual, at.numberNotesNormal
938        (5, 2)
939
940        Unless it's in a meter.TimeSignature compound (triple) context:
941
942        >>> at = abcFormat.ABCTuplet('(5')
943        >>> at.updateRatio(meter.TimeSignature('6/8'))
944        >>> at.numberNotesActual, at.numberNotesNormal
945        (5, 3)
946
947        Six is 6:2, not 6:4!
948
949        >>> at = abcFormat.ABCTuplet('(6')
950        >>> at.updateRatio()
951        >>> at.numberNotesActual, at.numberNotesNormal
952        (6, 2)
953
954        >>> at = abcFormat.ABCTuplet('(6:4')
955        >>> at.updateRatio()
956        >>> at.numberNotesActual, at.numberNotesNormal
957        (6, 4)
958
959        >>> at = abcFormat.ABCTuplet('(6::6')
960        >>> at.updateRatio()
961        >>> at.numberNotesActual, at.numberNotesNormal
962        (6, 2)
963
964        2 is 2 in 3...
965
966        >>> at = abcFormat.ABCTuplet('(2')
967        >>> at.updateRatio()
968        >>> at.numberNotesActual, at.numberNotesNormal
969        (2, 3)
970
971
972        Some other types:
973
974        >>> for n in 1, 2, 3, 4, 5, 6, 7, 8, 9:
975        ...     at = abcFormat.ABCTuplet(f'({n}')
976        ...     at.updateRatio()
977        ...     print(at.numberNotesActual, at.numberNotesNormal)
978        1 1
979        2 3
980        3 2
981        4 3
982        5 2
983        6 2
984        7 2
985        8 3
986        9 2
987
988        Tuplets > 9 raise an exception:
989
990        >>> at = abcFormat.ABCTuplet('(10')
991        >>> at.updateRatio()
992        Traceback (most recent call last):
993        music21.abcFormat.ABCTokenException: cannot handle tuplet of form: '(10'
994        '''
995        if keySignatureObj is None:
996            normalSwitch = 2  # 4/4
997        elif keySignatureObj.beatDivisionCount == 3:  # if compound
998            normalSwitch = 3
999        else:
1000            normalSwitch = 2
1001
1002        splitTuplet = self.src.strip().split(':')
1003
1004        tupletNumber = splitTuplet[0]
1005        normalNotes = None
1006
1007        if len(splitTuplet) >= 2 and splitTuplet[1] != '':
1008            normalNotes = int(splitTuplet[1])
1009
1010        if tupletNumber == '(1':  # not sure if valid, but found
1011            a, n = 1, 1
1012        elif tupletNumber == '(2':
1013            a, n = 2, 3  # actual, normal
1014        elif tupletNumber == '(3':
1015            a, n = 3, 2  # actual, normal
1016        elif tupletNumber == '(4':
1017            a, n = 4, 3  # actual, normal
1018        elif tupletNumber == '(5':
1019            a, n = 5, normalSwitch  # actual, normal
1020        elif tupletNumber == '(6':
1021            a, n = 6, 2  # actual, normal
1022        elif tupletNumber == '(7':
1023            a, n = 7, normalSwitch  # actual, normal
1024        elif tupletNumber == '(8':
1025            a, n = 8, 3  # actual, normal
1026        elif tupletNumber == '(9':
1027            a, n = 9, normalSwitch  # actual, normal
1028        else:
1029            raise ABCTokenException(f'cannot handle tuplet of form: {tupletNumber!r}')
1030
1031        if normalNotes is None:
1032            normalNotes = n
1033
1034        self.numberNotesActual = a
1035        self.numberNotesNormal = normalNotes
1036
1037    def updateNoteCount(self):
1038        '''
1039        Update the note count of notes that are
1040        affected by this tuplet. Can be set by p:q:r style tuplets.
1041        Also creates a tuplet object.
1042
1043        >>> at = abcFormat.ABCTuplet('(6')
1044        >>> at.updateRatio()
1045        >>> at.updateNoteCount()
1046        >>> at.noteCount
1047        6
1048        >>> at.tupletObj
1049        <music21.duration.Tuplet 6/2>
1050
1051        >>> at = abcFormat.ABCTuplet('(6:4:12')
1052        >>> at.updateRatio()
1053        >>> at.updateNoteCount()
1054        >>> at.noteCount
1055        12
1056        >>> at.tupletObj
1057        <music21.duration.Tuplet 6/4>
1058
1059        >>> at = abcFormat.ABCTuplet('(6::18')
1060        >>> at.updateRatio()
1061        >>> at.updateNoteCount()
1062        >>> at.noteCount
1063        18
1064        '''
1065        if self.numberNotesActual is None:
1066            raise ABCTokenException('must set numberNotesActual with updateRatio()')
1067
1068        # nee dto
1069        from music21 import duration
1070        self.tupletObj = duration.Tuplet(
1071            numberNotesActual=self.numberNotesActual,
1072            numberNotesNormal=self.numberNotesNormal)
1073
1074        # copy value; this will be dynamically counted down
1075        splitTuplet = self.src.strip().split(':')
1076        if len(splitTuplet) >= 3 and splitTuplet[2] != '':
1077            self.noteCount = int(splitTuplet[2])
1078        else:
1079            self.noteCount = self.numberNotesActual
1080
1081        # self.qlRemain = self._tupletObj.totalTupletLength()
1082
1083
1084class ABCTie(ABCToken):
1085    '''
1086    Handles instances of ties '-' between notes in an ABC score.
1087    Ties are treated as an attribute of the note before the '-';
1088    the note after is marked as the end of the tie.
1089    '''
1090    def __init__(self, src):
1091        super().__init__(src)
1092        self.noteObj = None
1093
1094
1095class ABCSlurStart(ABCToken):
1096    '''
1097    ABCSlurStart tokens always precede the notes in a slur.
1098    For nested slurs, each open parenthesis gets its own token.
1099    '''
1100    def __init__(self, src):
1101        super().__init__(src)
1102        self.slurObj = None
1103
1104    def fillSlur(self):
1105        '''
1106        Creates a spanner object for each open paren associated with a slur;
1107        these slurs are filled with notes until end parens are read.
1108        '''
1109        from music21 import spanner
1110        self.slurObj = spanner.Slur()
1111
1112
1113class ABCParenStop(ABCToken):
1114    '''
1115    A general parenthesis stop;
1116    comes at the end of a tuplet, slur, or dynamic marking.
1117    '''
1118
1119
1120class ABCCrescStart(ABCToken):
1121    '''
1122    ABCCrescStart tokens always precede the notes in a crescendo.
1123    These tokens coincide with the string "!crescendo(";
1124    the closing string "!crescendo)" counts as an ABCParenStop.
1125    '''
1126
1127    def __init__(self, src):
1128        super().__init__(src)
1129        self.crescObj = None
1130
1131    def fillCresc(self):
1132        from music21 import dynamics
1133        self.crescObj = dynamics.Crescendo()
1134
1135
1136class ABCDimStart(ABCToken):
1137    '''
1138    ABCDimStart tokens always precede the notes in a diminuendo.
1139    They function identically to ABCCrescStart tokens.
1140    '''
1141    def __init__(self, src):    # previous typo?: used to be __init
1142        super().__init__(src)
1143        self.dimObj = None
1144
1145    def fillDim(self):
1146        from music21 import dynamics
1147        self.dimObj = dynamics.Diminuendo()
1148
1149
1150class ABCStaccato(ABCToken):
1151    '''
1152    ABCStaccato tokens "." precede a note or chord;
1153    they are a property of that note/chord.
1154    '''
1155
1156
1157class ABCUpbow(ABCToken):
1158    '''
1159    ABCStaccato tokens "." precede a note or chord;
1160    they are a property of that note/chord.
1161    '''
1162
1163
1164class ABCDownbow(ABCToken):
1165    '''
1166    ABCStaccato tokens "." precede a note or chord;
1167    they are a property of that note/chord.
1168    '''
1169
1170
1171class ABCAccent(ABCToken):
1172    '''
1173    ABCAccent tokens "K" precede a note or chord;
1174    they are a property of that note/chord.
1175    These appear as ">" in the output.
1176    '''
1177
1178
1179class ABCStraccent(ABCToken):
1180    '''
1181    ABCStraccent tokens "k" precede a note or chord;
1182    they are a property of that note/chord.
1183    These appear as "^" in the output.
1184    '''
1185
1186
1187class ABCTenuto(ABCToken):
1188    '''
1189    ABCTenuto tokens "M" precede a note or chord;
1190    they are a property of that note/chord.
1191    '''
1192
1193
1194class ABCGraceStart(ABCToken):
1195    '''
1196    Grace note start
1197    '''
1198
1199
1200class ABCGraceStop(ABCToken):
1201    '''
1202    Grace note end
1203    '''
1204
1205
1206class ABCBrokenRhythmMarker(ABCToken):
1207    '''
1208    Marks that rhythm is broken with '>>>'
1209    '''
1210
1211    def __init__(self, src):
1212        super().__init__(src)
1213        self.data = None
1214
1215    def preParse(self):
1216        '''Called before context adjustments: need to have access to data
1217
1218        >>> brokenRhythm = abcFormat.ABCBrokenRhythmMarker('>>>')
1219        >>> brokenRhythm.preParse()
1220        >>> brokenRhythm.data
1221        '>>>'
1222        '''
1223        self.data = self.src.strip()
1224
1225
1226class ABCNote(ABCToken):
1227    '''
1228    A model of an ABCNote.
1229
1230    General usage requires multi-pass processing. After being tokenized,
1231    each ABCNote needs a number of attributes updates. Attributes to
1232    be updated after tokenizing, and based on the linear sequence of
1233    tokens: `inBar`, `inBeam` (not used), `inGrace`,
1234    `activeDefaultQuarterLength`, `brokenRhythmMarker`, and
1235    `activeKeySignature`.
1236
1237    The `chordSymbols` list stores one or more chord symbols (ABC calls
1238    these guitar chords) associated with this note. This attribute is
1239    updated when parse() is called.
1240    '''
1241    def __init__(self, src='', carriedAccidental=None):
1242        super().__init__(src)
1243
1244        # store the ABC accidental string propagated in the measure that
1245        # must be applied to this note. Note must not be set if the
1246        # note already has an explicit accidental attached. (The explicit
1247        # accidental is now the one that will be carried forward.)
1248        self.carriedAccidental = carriedAccidental
1249
1250        # store chord string if connected to this note
1251        self.chordSymbols = []
1252
1253        # context attributes
1254        self.inBar = None
1255        self.inBeam = None
1256        self.inGrace = None
1257
1258        # provide default duration from handler; may change during piece
1259        self.activeDefaultQuarterLength = None
1260        # store if a broken symbol applies; pair of symbol, position (left, right)
1261        self.brokenRhythmMarker = None
1262
1263        # store key signature for pitch processing; this is an m21 object
1264        self.activeKeySignature = None
1265
1266        # store a tuplet if active
1267        self.activeTuplet = None
1268
1269        # store a spanner if active
1270        self.applicableSpanners = []
1271
1272        # store a tie if active
1273        self.tie = None
1274
1275        # store articulations if active
1276        self.articulations = []
1277
1278        # set to True if a modification of key signature
1279        # set to False if an altered tone part of a Key
1280        self.accidentalDisplayStatus = None
1281        # determined during parse() based on if pitch chars are present
1282        self.isRest = None
1283        # pitch/ duration attributes for m21 conversion
1284        # set with parse() based on all other contextual
1285        self.pitchName = None  # if None, a rest or chord
1286        self.quarterLength = None
1287
1288    @staticmethod
1289    def _splitChordSymbols(strSrc):
1290        '''
1291        Splits chord symbols from other string characteristics.
1292        Return list of chord symbols and clean, remain chars
1293
1294        Staticmethod:
1295
1296        >>> an = abcFormat.ABCNote()
1297        >>> an._splitChordSymbols('"C"e2')
1298        (['"C"'], 'e2')
1299        >>> an._splitChordSymbols('b2')
1300        ([], 'b2')
1301
1302        >>> abcFormat.ABCNote._splitChordSymbols('"D7""D"d2')
1303        (['"D7"', '"D"'], 'd2')
1304        '''
1305        if '"' in strSrc:
1306            chordSymbols = reChordSymbol.findall(strSrc)
1307            # might remove quotes from chord symbols here
1308
1309            # index of end of last match
1310            i = list(reChordSymbol.finditer(strSrc))[-1].end()
1311            return chordSymbols, strSrc[i:]
1312        else:
1313            return [], strSrc
1314
1315    def getPitchName(
1316        self,
1317        strSrc: str,
1318        forceKeySignature=None
1319    ) -> Tuple[Optional[str], Union[bool, None]]:
1320        '''
1321        Given a note or rest string without a chord symbol,
1322        return a music21 pitch string or None (if a rest),
1323        and the accidental display status. This value is paired
1324        with an accidental display status. Pitch alterations, and
1325        accidental display status, are adjusted if a key is
1326        declared in the Note.
1327
1328        >>> an = abcFormat.ABCNote()
1329        >>> an.getPitchName('e2')
1330        ('E5', None)
1331        >>> an.getPitchName('C')
1332        ('C4', None)
1333        >>> an.getPitchName('B,,')
1334        ('B2', None)
1335        >>> an.getPitchName('C,')
1336        ('C3', None)
1337        >>> an.getPitchName('c')
1338        ('C5', None)
1339        >>> an.getPitchName("c'")
1340        ('C6', None)
1341        >>> an.getPitchName("c''")
1342        ('C7', None)
1343        >>> an.getPitchName("^g")
1344        ('G#5', True)
1345        >>> an.getPitchName("_g''")
1346        ('G-7', True)
1347        >>> an.getPitchName('=c')
1348        ('Cn5', True)
1349
1350        If pitch is a rest (z) then the Pitch name is None:
1351
1352        >>> an.getPitchName('z4')
1353        (None, None)
1354
1355        Grace note:
1356
1357        >>> an.getPitchName('{c}')
1358        ('C5', None)
1359
1360
1361        Given an active KeySignature object, the pitch name might
1362        change:
1363
1364        >>> an.activeKeySignature = key.KeySignature(3)
1365        >>> an.getPitchName('c')
1366        ('C#5', False)
1367
1368
1369        Illegal pitch names raise an ABCHandlerException
1370
1371        >>> an.getPitchName('x')
1372        Traceback (most recent call last):
1373        music21.abcFormat.ABCHandlerException: cannot find any pitch information in: 'x'
1374        '''
1375        environLocal.printDebug(['getPitchName:', strSrc])
1376
1377        # skip some articulations parsed with the pitch
1378        # some characters are errors in parsing or encoding not yet handled
1379        if len(strSrc) > 1 and strSrc[0] in 'uT':
1380            strSrc = strSrc[1:]
1381        strSrc = strSrc.replace('T', '')
1382
1383        try:
1384            name = rePitchName.findall(strSrc)[0]
1385        except IndexError:  # no matches  # pragma: no cover
1386            raise ABCHandlerException(f'cannot find any pitch information in: {strSrc!r}')
1387
1388        if name == 'z':
1389            return (None, None)  # designates a rest
1390
1391        if forceKeySignature is not None:
1392            activeKeySignature = forceKeySignature
1393        else:  # may be None
1394            activeKeySignature = self.activeKeySignature
1395
1396        try:  # returns pStr, accidentalDisplayStatus
1397            return _pitchTranslationCache[(strSrc,
1398                                           self.carriedAccidental,
1399                                           str(activeKeySignature))]
1400        except KeyError:
1401            pass
1402
1403        if name.islower():
1404            octave = 5
1405        else:
1406            octave = 4
1407        # look in source string for register modification
1408        octave -= strSrc.count(',')
1409        octave += strSrc.count("'")
1410
1411        # get an accidental string
1412
1413        accString = ''
1414        for dummy in range(strSrc.count('_')):
1415            accString += '-'  # m21 symbols
1416        for dummy in range(strSrc.count('^')):
1417            accString += '#'  # m21 symbols
1418        for dummy in range(strSrc.count('=')):
1419            accString += 'n'  # m21 symbols
1420
1421        carriedAccString = ''
1422        if self.carriedAccidental:
1423            # No overriding accidental attached to this note
1424            # force carrying through the measure.
1425            for dummy in range(self.carriedAccidental.count('_')):
1426                carriedAccString += '-'  # m21 symbols
1427            for dummy in range(self.carriedAccidental.count('^')):
1428                carriedAccString += '#'  # m21 symbols
1429            for dummy in range(self.carriedAccidental.count('=')):
1430                carriedAccString += 'n'  # m21 symbols
1431
1432        if carriedAccString and accString:
1433            raise ABCHandlerException('Carried accidentals not rendered moot.')
1434        # if there is an explicit accidental, regardless of key, it should
1435        # be shown: this will works for naturals well
1436        if carriedAccString:
1437            # An accidental carrying through the measure is supposed to be applied.
1438            # This will be set iff no explicit accidental is attached to the note.
1439            accidentalDisplayStatus = None
1440        elif accString != '':
1441            accidentalDisplayStatus = True
1442        # if we do not have a key signature, and have accidentals, set to None
1443        elif activeKeySignature is None:
1444            accidentalDisplayStatus = None
1445        # pitches are key dependent: accidentals are not given
1446        # if we have a key and find a name, that does not have a n, must be
1447        # altered
1448        else:
1449            alteredPitches = activeKeySignature.alteredPitches
1450            # just the steps, no accidentals
1451            alteredPitchSteps = [p.step.lower() for p in alteredPitches]
1452            # includes #, -
1453            alteredPitchNames = [p.name.lower() for p in alteredPitches]
1454            # environLocal.printDebug(['alteredPitches', alteredPitches])
1455
1456            if name.lower() in alteredPitchSteps:
1457                # get the corresponding index in the name
1458                name = alteredPitchNames[alteredPitchSteps.index(name.lower())]
1459            # set to false, as do not need to show w/ key sig
1460            accidentalDisplayStatus = False
1461
1462        # making upper here, but this is not relevant
1463        if carriedAccString:
1464            pStr = f'{name.upper()}{carriedAccString}{octave}'
1465        else:
1466            pStr = f'{name.upper()}{accString}{octave}'
1467
1468        # store in global cache for faster speed
1469        _cacheKey = (
1470            strSrc,
1471            self.carriedAccidental,
1472            str(activeKeySignature)
1473        )
1474
1475        _pitchTranslationCache[_cacheKey] = (pStr, accidentalDisplayStatus)
1476        return (pStr, accidentalDisplayStatus)
1477
1478    def getQuarterLength(self, strSrc, forceDefaultQuarterLength=None) -> float:
1479        '''
1480        Called with parse(), after context processing, to calculate duration
1481
1482        >>> an = abcFormat.ABCNote()
1483        >>> an.activeDefaultQuarterLength = 0.5
1484        >>> an.getQuarterLength('e2')
1485        1.0
1486        >>> an.getQuarterLength('G')
1487        0.5
1488        >>> an.getQuarterLength('=c/2')
1489        0.25
1490        >>> an.getQuarterLength('A3/2')
1491        0.75
1492        >>> an.getQuarterLength('A/')
1493        0.25
1494
1495        >>> an.getQuarterLength('A//')
1496        0.125
1497        >>> an.getQuarterLength('A///')
1498        0.0625
1499
1500        >>> an = abcFormat.ABCNote()
1501        >>> an.activeDefaultQuarterLength = 0.5
1502        >>> an.brokenRhythmMarker = ('>', 'left')
1503        >>> an.getQuarterLength('A')
1504        0.75
1505        >>> an.brokenRhythmMarker = ('>', 'right')
1506        >>> an.getQuarterLength('A')
1507        0.25
1508
1509        >>> an.brokenRhythmMarker = ('<<', 'left')
1510        >>> an.getQuarterLength('A')
1511        0.125
1512        >>> an.brokenRhythmMarker = ('<<', 'right')
1513        >>> an.getQuarterLength('A')
1514        0.875
1515
1516        >>> an.brokenRhythmMarker = ('<<<', 'left')
1517        >>> an.getQuarterLength('A')
1518        0.0625
1519        >>> an.brokenRhythmMarker = ('<<<', 'right')
1520        >>> an.getQuarterLength('A')
1521        0.9375
1522
1523        >>> an.getQuarterLength('A', forceDefaultQuarterLength=1)
1524        1.875
1525        '''
1526        if forceDefaultQuarterLength is not None:
1527            activeDefaultQuarterLength = forceDefaultQuarterLength
1528        else:  # may be None
1529            activeDefaultQuarterLength = self.activeDefaultQuarterLength
1530
1531        if activeDefaultQuarterLength is None:
1532            raise ABCTokenException(
1533                'cannot calculate quarter length without a default quarter length')
1534
1535        numStr = []
1536        for c in strSrc:
1537            if c.isdigit() or c == '/':
1538                numStr.append(c)
1539        numStr = ''.join(numStr)
1540        numStr = numStr.strip()
1541
1542        # environLocal.printDebug(['numStr', numStr])
1543
1544        # get default
1545        if numStr == '':
1546            ql = activeDefaultQuarterLength
1547        # if only, shorthand for /2
1548        elif numStr == '/':
1549            ql = activeDefaultQuarterLength * 0.5
1550        elif numStr == '//':
1551            ql = activeDefaultQuarterLength * 0.25
1552        elif numStr == '///':
1553            ql = activeDefaultQuarterLength * 0.125
1554        # if a half fraction
1555        elif numStr.startswith('/'):
1556            ql = activeDefaultQuarterLength / int(numStr.split('/')[1])
1557        # uncommon usage: 3/ short for 3/2
1558        elif numStr.endswith('/'):
1559            n = int(numStr.split('/', maxsplit=1)[0].strip())
1560            d = 2
1561            ql = activeDefaultQuarterLength * n / d
1562        # if we have two, this is usually an error
1563        elif numStr.count('/') == 2:  # pragma: no cover
1564            environLocal.printDebug(['incorrectly encoded / unparsable duration:', numStr])
1565            ql = 1  # provide a default
1566
1567        # assume we have a complete fraction
1568        elif '/' in numStr:
1569            n, d = numStr.split('/')
1570            n = int(n.strip())
1571            d = int(d.strip())
1572            ql = activeDefaultQuarterLength * n / d
1573        # not a fraction; a multiplier
1574        else:
1575            ql = activeDefaultQuarterLength * int(numStr)
1576
1577        if self.brokenRhythmMarker is not None:
1578            symbol, direction = self.brokenRhythmMarker
1579            if symbol == '>':
1580                modPair = (1.5, 0.5)
1581            elif symbol == '<':
1582                modPair = (0.5, 1.5)
1583            elif symbol == '>>':
1584                modPair = (1.75, 0.25)
1585            elif symbol == '<<':
1586                modPair = (0.25, 1.75)
1587            elif symbol == '>>>':
1588                modPair = (1.875, 0.125)
1589            elif symbol == '<<<':
1590                modPair = (0.125, 1.875)
1591            else:  # pragma: no cover
1592                modPair = (1, 1)
1593
1594            # apply based on direction
1595            if direction == 'left':
1596                ql *= modPair[0]
1597            elif direction == 'right':
1598                ql *= modPair[1]
1599
1600        return ql
1601
1602    def parse(
1603        self,
1604        forceDefaultQuarterLength=None,
1605        forceKeySignature=None
1606    ) -> None:
1607        # environLocal.printDebug(['parse', self.src])
1608        self.chordSymbols, nonChordSymStr = self._splitChordSymbols(self.src)
1609        # get pitch name form remaining string
1610        # rests will have a pitch name of None
1611
1612        try:
1613            pn, accDisp = self.getPitchName(nonChordSymStr,
1614                                            forceKeySignature=forceKeySignature)
1615        except ABCHandlerException:
1616            environLocal.warn(['Could not get pitch information from note: ',
1617                               f'{nonChordSymStr}, assuming C'])
1618            pn = 'C'
1619            accDisp = False
1620
1621        self.pitchName, self.accidentalDisplayStatus = pn, accDisp
1622
1623        if self.pitchName is None:
1624            self.isRest = True
1625        else:
1626            self.isRest = False
1627
1628        self.quarterLength = self.getQuarterLength(
1629            nonChordSymStr,
1630            forceDefaultQuarterLength=forceDefaultQuarterLength)
1631
1632        # environLocal.printDebug(['ABCNote:', 'pitch name:', self.pitchName,
1633        #                            'ql:', self.quarterLength])
1634
1635
1636class ABCChord(ABCNote):
1637    '''
1638    A representation of an ABC Chord, which contains within its delimiters individual notes.
1639
1640    A subclass of ABCNote.
1641    '''
1642
1643    def __init__(self, src: str = ''):
1644        super().__init__(src)
1645        # store a list of component objects
1646        self.subTokens = []
1647
1648    def parse(self, forceKeySignature=None, forceDefaultQuarterLength=None):
1649        '''
1650        Handles the following types of chords:
1651
1652        * Chord without length modifier: [ceg]
1653
1654        * Chords with outer length modifier: [ceg]2, [ceg]/2
1655
1656        * Chords with inner length modifier: [c2e2g2], [c2eg]
1657
1658        * Chords with inner and outer length modifier: [c2e2g2]/2, [c/2e/2g/2]2
1659        '''
1660
1661        self.chordSymbols, nonChordSymStr = self._splitChordSymbols(self.src)
1662
1663        # position of the closing bracket
1664        pos = nonChordSymStr.index(']')
1665        # Length modifier string behind the chord brackets
1666        outerLengthModifierStr = nonChordSymStr[pos + 1:]
1667        # String in the chord brackets
1668        tokenStr = nonChordSymStr[1:pos]
1669
1670        # environLocal.printDebug(['ABCChord:', nonChordSymStr, 'tokenStr', tokenStr, '
1671        # outerLengthModifierStr', outerLengthModifierStr])
1672
1673        # Get the outer chord length modifier if present
1674        outer_lengthModifier = self.getQuarterLength(outerLengthModifierStr,
1675                                                     forceDefaultQuarterLength=1.0)
1676
1677        if forceKeySignature is not None:
1678            activeKeySignature = forceKeySignature
1679        else:  # may be None
1680            activeKeySignature = self.activeKeySignature
1681
1682        # create a handler for processing internal chord notes
1683        ah = ABCHandler()
1684        # only tokenizing; not calling process() as these objects
1685        # have no metadata
1686        # may need to supply key?
1687        ah.tokenize(tokenStr)
1688
1689        inner_quarterLength = 0
1690        # tokens contained here are each ABCNote instances
1691        for t in ah.tokens:
1692            # environLocal.printDebug(['ABCChord: subTokens', t])
1693            # parse any tokens individually, supply local data as necessary
1694            if isinstance(t, ABCNote):
1695                t.parse(
1696                    forceDefaultQuarterLength=self.activeDefaultQuarterLength,
1697                    forceKeySignature=activeKeySignature)
1698
1699                if t.isRest:
1700                    continue
1701
1702                # get the quarter length from the sub-tokens
1703                # All the notes within a chord should normally have the same length,
1704                # but if not, the chord duration is that of the first note.
1705                if not inner_quarterLength:
1706                    inner_quarterLength = t.quarterLength
1707
1708                self.subTokens.append(t)
1709
1710
1711        # When both inside and outside the chord length modifiers are used,
1712        # they should be multiplied. Example: [C2E2G2]3 has the same meaning as [CEG]6.
1713        self.quarterLength = outer_lengthModifier * inner_quarterLength
1714
1715
1716# ------------------------------------------------------------------------------
1717class ABCHandler:
1718    '''
1719    An ABCHandler is able to divide elements of a character stream into objects and handle
1720    store in a list, and passes global information to components
1721
1722    Optionally, specify the (major, minor, patch) version of ABC to process--
1723    e.g., (1.2.0). If not set, default ABC 1.3 parsing is performed.
1724
1725    If lineBreaksDefinePhrases is True then new lines within music elements
1726    define new phrases.  This is useful for parsing extra information from
1727    the Essen Folksong repertory
1728
1729    New in v6.3 -- lineBreaksDefinePhrases -- does not yet do anything
1730    '''
1731    def __init__(self, abcVersion=None, lineBreaksDefinePhrases=False):
1732        # tokens are ABC objects import n a linear stream
1733        self.abcVersion = abcVersion
1734        self.abcDirectives = {}
1735        self.tokens = []
1736        self.activeParens = []
1737        self.activeSpanners = []
1738        self.lineBreaksDefinePhrases = lineBreaksDefinePhrases
1739        self.pos = -1
1740        self.skipAhead = 0
1741        self.isFirstComment = True
1742        self.strSrc = ''
1743        self.srcLen = len(self.strSrc)  # just documenting this.
1744        self.currentCollectStr = ''
1745
1746    @staticmethod
1747    def _getLinearContext(source, i: int) -> Tuple[Any, Any, Any, Any]:
1748        '''
1749        Find the local context of a string or iterable of objects
1750        beginning at a particular index.
1751
1752        Returns a tuple of charPrev, charThis, charNext, charNextNext.
1753
1754        Staticmethod
1755
1756        >>> ah = abcFormat.ABCHandler()
1757        >>> ah._getLinearContext('12345', 0)
1758        (None, '1', '2', '3')
1759        >>> ah._getLinearContext('12345', 1)
1760        ('1', '2', '3', '4')
1761        >>> abcFormat.ABCHandler._getLinearContext('12345', 3)
1762        ('3', '4', '5', None)
1763        >>> abcFormat.ABCHandler._getLinearContext('12345', 4)
1764        ('4', '5', None, None)
1765
1766        >>> abcFormat.ABCHandler._getLinearContext([32, None, 8, 11, 53], 4)
1767        (11, 53, None, None)
1768        >>> ah._getLinearContext([32, None, 8, 11, 53], 2)
1769        (None, 8, 11, 53)
1770        >>> ah._getLinearContext([32, None, 8, 11, 53], 0)
1771        (None, 32, None, 8)
1772        '''
1773        # Note: this is performance critical method
1774        lastIndex = len(source) - 1
1775        if i > lastIndex:
1776            raise ABCHandlerException(f'bad index value {i}, max is {lastIndex}')
1777
1778        # find local area of iterable
1779        cPrev = None
1780        if i > 0:
1781            cPrev = source[i - 1]
1782
1783        # set current characters or items
1784        c = source[i]
1785
1786        cNext = None
1787        if i < len(source) - 1:
1788            cNext = source[i + 1]
1789
1790        # get 2 entries forward
1791        cNextNext = None
1792        if i < len(source) - 2:
1793            cNextNext = source[i + 2]
1794
1795        return cPrev, c, cNext, cNextNext
1796        # return cPrevNotSpace, cPrev, c, cNext, cNextNotSpace, cNextNext
1797
1798    @staticmethod
1799    def _getNextLineBreak(strSrc: str, i: int) -> Optional[int]:
1800        r'''
1801        Return index of next line break after character i.
1802
1803        Staticmethod
1804
1805        >>> ah = abcFormat.ABCHandler()
1806        >>> inputString = 'de  we\n wer bfg\n'
1807        >>> ah._getNextLineBreak(inputString, 0)
1808        6
1809        >>> inputString[0:6]
1810        'de  we'
1811
1812        from last line break
1813
1814        >>> abcFormat.ABCHandler._getNextLineBreak(inputString, 6)
1815        15
1816        >>> inputString[ah._getNextLineBreak(inputString, 0):]
1817        '\n wer bfg\n'
1818        '''
1819        lastIndex = len(strSrc) - 1
1820        for j in range(i + 1, lastIndex + 1):
1821            if strSrc[j] == '\n':
1822                return j
1823        return lastIndex + 1
1824
1825    @staticmethod
1826    def barlineTokenFilter(token: str) -> List[ABCBar]:
1827        '''
1828        Some single barline tokens are better replaced
1829        with two tokens. This method, given a token,
1830        returns a list of tokens. If there is no change
1831        necessary, the provided token will be returned in the list.
1832
1833        A staticmethod.  Call on the class itself.
1834
1835        >>> abcFormat.ABCHandler.barlineTokenFilter('::')
1836        [<music21.abcFormat.ABCBar ':|'>, <music21.abcFormat.ABCBar '|:'>]
1837
1838        >>> abcFormat.ABCHandler.barlineTokenFilter('|2')
1839        [<music21.abcFormat.ABCBar '|'>, <music21.abcFormat.ABCBar '[2'>]
1840
1841        >>> abcFormat.ABCHandler.barlineTokenFilter(':|1')
1842        [<music21.abcFormat.ABCBar ':|'>, <music21.abcFormat.ABCBar '[1'>]
1843
1844        If nothing matches, the original token is returned as an ABCBar object:
1845
1846        >>> abcFormat.ABCHandler.barlineTokenFilter('hi')
1847        [<music21.abcFormat.ABCBar 'hi'>]
1848        '''
1849        barTokens: List[ABCBar] = []
1850        if token == '::':
1851            # create a start and and an end
1852            barTokens.append(ABCBar(':|'))
1853            barTokens.append(ABCBar('|:'))
1854        elif token == '|1':
1855            # create a start and and an end
1856            barTokens.append(ABCBar('|'))
1857            barTokens.append(ABCBar('[1'))
1858        elif token == '|2':
1859            # create a start and and an end
1860            barTokens.append(ABCBar('|'))
1861            barTokens.append(ABCBar('[2'))
1862        elif token == ':|1':
1863            # create a start and and an end
1864            barTokens.append(ABCBar(':|'))
1865            barTokens.append(ABCBar('[1'))
1866        elif token == ':|2':
1867            # create a start and and an end
1868            barTokens.append(ABCBar(':|'))
1869            barTokens.append(ABCBar('[2'))
1870        else:  # append unaltered
1871            barTokens.append(ABCBar(token))
1872        return barTokens
1873
1874    # --------------------------------------------------------------------------
1875    # token processing
1876
1877    def _accidentalPropagation(self) -> str:
1878        '''
1879        Determine how accidentals should 'carry through the measure.'
1880
1881        >>> ah = abcFormat.ABCHandler(abcVersion=(1, 3, 0))
1882        >>> ah._accidentalPropagation()
1883        'not'
1884        >>> ah = abcFormat.ABCHandler(abcVersion=(2, 0, 0))
1885        >>> ah._accidentalPropagation()
1886        'pitch'
1887        '''
1888        minVersion = (2, 0, 0)
1889        if not self.abcVersion or self.abcVersion < minVersion:
1890            return 'not'
1891        if 'propagate-accidentals' in self.abcDirectives:
1892            return self.abcDirectives['propagate-accidentals']
1893        return 'pitch'  # Default per abc 2.1 standard
1894
1895    def parseCommentForVersionInformation(self, commentLine: str):
1896        '''
1897        If this is the first comment then searches for a version
1898        match and set it as .abcVersion
1899
1900        If not isFirstComment then does nothing:
1901
1902        >>> ah = abcFormat.ABCHandler()
1903        >>> ah.abcVersion is None
1904        True
1905        >>> ah.isFirstComment
1906        True
1907
1908        >>> ah.parseCommentForVersionInformation('%abc-2.3.2')
1909        >>> ah.abcVersion
1910        (2, 3, 2)
1911        >>> ah.isFirstComment
1912        False
1913
1914        Now will do nothing since isFirstComment is False
1915
1916        >>> ah.parseCommentForVersionInformation('%abc-4.9.7')
1917        >>> ah.abcVersion
1918        (2, 3, 2)
1919        '''
1920        if not self.isFirstComment:
1921            return
1922        self.isFirstComment = False
1923        verMats = reAbcVersion.match(commentLine)
1924        if verMats:
1925            abcMajor = int(verMats.group(2))
1926            abcMinor = int(verMats.group(3))
1927            if verMats.group(4):
1928                abcPatch = int(verMats.group(4))
1929            else:
1930                abcPatch = 0
1931            verTuple = (abcMajor, abcMinor, abcPatch)
1932            self.abcVersion = verTuple
1933
1934    def processComment(self):
1935        r'''
1936        Processes the comment at self.pos in self.strSrc, setting self.skipAhead,
1937        possibly self.abcVersion, and self.abcDirectives for the directiveKey.
1938
1939        TODO: store the comment in the stream also.
1940
1941        >>> from textwrap import dedent
1942        >>> ah = abcFormat.ABCHandler()
1943        >>> data = dedent("""
1944        ...    Hello % this is a comment
1945        ...    Bye
1946        ...    """)
1947        >>> ah.strSrc = data
1948        >>> ah.pos = 6
1949        >>> ah.processComment()
1950        >>> ah.skipAhead
1951        19
1952        >>> len(' this is a comment\n')
1953        19
1954        '''
1955        self.skipAhead = self._getNextLineBreak(
1956            self.strSrc, self.pos
1957        ) - (self.pos + 1)
1958        commentLine = self.strSrc[self.pos:self.pos + self.skipAhead + 1]
1959        self.parseCommentForVersionInformation(commentLine)
1960        directiveMatches = reDirective.match(commentLine)
1961        if directiveMatches:
1962            directiveKey = directiveMatches.group(1)
1963            directiveValue = directiveMatches.group(2)
1964            self.abcDirectives[directiveKey] = directiveValue
1965        # environLocal.printDebug(['got comment:', repr(self.strSrc[i:j + 1])])
1966
1967    @staticmethod
1968    def startsMetadata(c: str, cNext: Optional[str], cNextNext: Optional[str]) -> bool:
1969        '''
1970        Returns True if this context describes the start of a metadata section, like
1971
1972        A:something
1973
1974        Metadata: capital letter, with next char as ':' and some following character
1975
1976        >>> ah = abcFormat.ABCHandler
1977        >>> ah.startsMetadata('A', ':', 's')
1978        True
1979
1980        lowercase w: is a special case for lyric defs
1981
1982        >>> ah.startsMetadata('w', ':', 's')
1983        True
1984
1985        Following char must be ":"
1986
1987        >>> ah.startsMetadata('A', ' ', 's')
1988        False
1989
1990        Pipe after colon indicates not metadata (bar info).
1991        For example need to not misinterpret repeat bars as metadata
1992        e.g. `dAG FED:|2 dAG FGA|`
1993
1994        this is incorrect, but we can avoid it by
1995        looking for a leading pipe and returning False
1996
1997        >>> ah.startsMetadata('A', ':', '|')
1998        False
1999
2000        >>> ah.startsMetadata('A', ':', None)
2001        False
2002        '''
2003        if cNext != ':':
2004            return False
2005        elif cNextNext is None:
2006            return False
2007        elif cNextNext == '|':
2008            return False
2009        elif c == 'w':
2010            return True  # special case, w:...
2011        elif c.isalpha() and c.isupper():
2012            return True
2013        return False
2014
2015
2016    def tokenize(self, strSrc: str) -> None:
2017        '''
2018        Walk the abc string, creating ABC objects along the way.
2019
2020        This may be called separately from process(), in the case
2021        that pre/post parse processing is not needed.
2022
2023        >>> abch = abcFormat.ABCHandler()
2024        >>> abch.tokens
2025        []
2026        >>> abch.tokenize('X: 1')
2027        >>> abch.tokens
2028        [<music21.abcFormat.ABCMetadata 'X: 1'>]
2029
2030        >>> abch = abcFormat.ABCHandler()
2031        >>> abch.tokenize('(6f')
2032        >>> abch.tokens
2033        [<music21.abcFormat.ABCTuplet '(6'>, <music21.abcFormat.ABCNote 'f'>]
2034
2035        >>> abch = abcFormat.ABCHandler()
2036        >>> abch.tokenize('(6:4f')
2037        >>> abch.tokens
2038        [<music21.abcFormat.ABCTuplet '(6:4'>, <music21.abcFormat.ABCNote 'f'>]
2039
2040        >>> abch = abcFormat.ABCHandler()
2041        >>> abch.tokenize('(6:4:2f')
2042        >>> abch.tokens
2043        [<music21.abcFormat.ABCTuplet '(6:4:2'>, <music21.abcFormat.ABCNote 'f'>]
2044
2045        >>> abch = abcFormat.ABCHandler()
2046        >>> abch.tokenize('(6::2f')
2047        >>> abch.tokens
2048        [<music21.abcFormat.ABCTuplet '(6::2'>, <music21.abcFormat.ABCNote 'f'>]
2049        '''
2050        self.srcLen = len(strSrc)
2051        self.strSrc = strSrc
2052        self.pos = -1
2053        self.currentCollectStr = ''
2054        self.skipAhead = 0
2055        # noinspection SpellCheckingInspection
2056        accidentalsAndDecorations = '.~^=_HLMOPSTuv'
2057        accidentals = '^=_'
2058
2059        activeChordSymbol = ''  # accumulate, then prepend
2060        accidentalized = {}
2061        accidental = None
2062        abcPitch = None  # ABC substring defining any pitch within the current token
2063        self.isFirstComment = True
2064
2065        while self.pos < self.srcLen - 1:
2066            self.pos += 1
2067            self.pos += self.skipAhead
2068            self.skipAhead = 0
2069            if self.pos > self.srcLen - 1:
2070                break
2071
2072            q = self._getLinearContext(self.strSrc, self.pos)
2073            unused_cPrev, c, cNext, cNextNext = q
2074            # cPrevNotSpace, cPrev, c, cNext, cNextNotSpace, cNextNext = q
2075
2076            # comment lines, also encoding defs
2077            if c == '%':
2078                self.processComment()
2079                continue
2080
2081            if self.startsMetadata(c, cNext, cNextNext):
2082                # collect until end of line; add one to get line break
2083                j = self._getNextLineBreak(self.strSrc, self.pos)
2084                self.skipAhead = j - (self.pos + 1)
2085                self.currentCollectStr = self.strSrc[self.pos:j].strip()
2086                # environLocal.printDebug(['got metadata:', repr(self.currentCollectStr)])
2087                self.tokens.append(ABCMetadata(self.currentCollectStr))
2088                continue
2089
2090            # get bars: if not a space and not alphanumeric
2091            if not c.isspace() and not c.isalnum() and c not in ('~', '('):
2092                matchBars = False
2093                for barIndex in range(len(ABC_BARS)):
2094                    # first of bars tuple is symbol to match
2095                    # three possible sizes of bar indications: 3, 2, 1
2096                    barTokenArchetype = ABC_BARS[barIndex][0]
2097                    if len(barTokenArchetype) == 3:
2098                        if cNextNext is not None and (c + cNext + cNextNext == barTokenArchetype):
2099                            self.skipAhead = 2
2100                            matchBars = True
2101                            break
2102                    elif cNext is not None and (len(barTokenArchetype) == 2):
2103                        if c + cNext == barTokenArchetype:
2104                            self.skipAhead = 1
2105                            matchBars = True
2106                            break
2107                    elif len(barTokenArchetype) == 1:
2108                        if c == barTokenArchetype:
2109                            self.skipAhead = 0
2110                            matchBars = True
2111                            break
2112                if matchBars is True:
2113                    accidentalized = {}
2114                    accidental = None
2115                    j = self.pos + self.skipAhead + 1
2116                    self.currentCollectStr = self.strSrc[self.pos:j]
2117                    # filter and replace with 2 tokens if necessary
2118                    for tokenSub in self.barlineTokenFilter(self.currentCollectStr):
2119                        self.tokens.append(tokenSub)
2120                    # environLocal.printDebug(['got bars:', repr(self.currentCollectStr)])
2121                    # if self.currentCollectStr == '::':
2122                    #     # create a start and and an end
2123                    #     self.tokens.append(ABCBar(':|'))
2124                    #     self.tokens.append(ABCBar('|:'))
2125                    # else:
2126                    #     self.tokens.append(ABCBar(self.currentCollectStr))
2127                    continue
2128
2129            # get tuplet indicators: (2, (3, (p:q:r or (3::
2130            if c == '(' and cNext is not None and cNext.isdigit():
2131                self.skipAhead = 1
2132                j = self.pos + self.skipAhead + 1  # always two characters
2133                unused1, possibleColon, qChar, unused2 = self._getLinearContext(self.strSrc, j)
2134                if possibleColon == ':':
2135                    j += 1
2136                    self.skipAhead += 1
2137                    if qChar is not None and qChar.isdigit():
2138                        j += 1
2139                        self.skipAhead += 1
2140                    unused1, possibleColon, rChar, unused2 = self._getLinearContext(self.strSrc, j)
2141                    if possibleColon == ':':
2142                        j += 1  # include the r characters
2143                        self.skipAhead += 1
2144                        if rChar is not None and rChar.isdigit():
2145                            j += 1
2146                            self.skipAhead += 1
2147
2148                self.currentCollectStr = self.strSrc[self.pos:j]
2149                # environLocal.printDebug(['got tuplet start:', repr(self.currentCollectStr)])
2150                self.tokens.append(ABCTuplet(self.currentCollectStr))
2151                continue
2152
2153            # get broken rhythm modifiers: < or >, >>, up to <<<
2154            if c in '<>':
2155                j = self.pos + 1
2156                while j < self.srcLen - 1 and self.strSrc[j] in '<>':
2157                    j += 1
2158                self.currentCollectStr = self.strSrc[self.pos:j]
2159                # environLocal.printDebug(
2160                #     ['got bidrectional rhythm mod:', repr(self.currentCollectStr)])
2161                self.tokens.append(ABCBrokenRhythmMarker(self.currentCollectStr))
2162                self.skipAhead = j - (self.pos + 1)
2163                continue
2164
2165            # get dynamics. skip over the open paren to avoid confusion.
2166            # NB: Nested crescendos are not an issue (not proper grammar).
2167            if c == '!':
2168                exclaimDict = {'!crescendo(!': ABCCrescStart,
2169                               '!crescendo)!': ABCParenStop,
2170                               '!diminuendo(!': ABCDimStart,
2171                               '!diminuendo)!': ABCParenStop,
2172                               }
2173                j = self.pos + 1
2174                while j < self.pos + 20 and j < self.srcLen:  # a reasonable upper bound
2175                    if self.strSrc[j] == '!':
2176                        if self.strSrc[self.pos:j + 1] in exclaimDict:
2177                            exclaimClass = exclaimDict[self.strSrc[self.pos:j + 1]]
2178                            exclaimObject = exclaimClass(c)
2179                            self.tokens.append(exclaimObject)
2180                            self.skipAhead = j - self.pos  # not + 1
2181                            break
2182                        # NB: We're currently skipping over all other '!' expressions
2183                        else:
2184                            self.skipAhead = j - self.pos  # not + 1
2185                            break
2186                    j += 1
2187                # not found, continue...
2188                continue
2189
2190            # get slurs, ensuring that they're not confused for tuplets
2191            if c == '(' and cNext is not None and not cNext.isdigit():
2192                self.tokens.append(ABCSlurStart(c))
2193                continue
2194
2195            # get slur/tuplet ending; treat it as a general parenthesis stop
2196            if c == ')':
2197                self.tokens.append(ABCParenStop(c))
2198                continue
2199
2200            # get ties between two notes
2201            if c == '-':
2202                self.tokens.append(ABCTie(c))
2203                continue
2204
2205            # get chord symbols / guitar chords; collected and joined with
2206            # chord or notes
2207            if c == '"':
2208                j = self.pos + 1
2209                while j < self.srcLen - 1 and self.strSrc[j] != '"':
2210                    j += 1
2211                j += 1  # need character that caused break
2212                # there may be more than one chord symbol: need to accumulate
2213                activeChordSymbol += self.strSrc[self.pos:j]
2214                # environLocal.printDebug(['got chord symbol:', repr(activeChordSymbol)])
2215                self.skipAhead = j - (self.pos + 1)
2216                continue
2217
2218            # get chords
2219            if c == '[':
2220                j = self.pos + 1
2221
2222                # find closing chord bracket
2223                while j < self.srcLen - 1 and self.strSrc[j] != ']':
2224                    j += 1
2225
2226                j += 1  # need character that caused break
2227
2228                # find outer chord length modifier
2229                while j < self.srcLen and (self.strSrc[j].isdigit() or self.strSrc[j] in '/'):
2230                    j += 1
2231
2232                # prepend chord symbol
2233                if activeChordSymbol != '':
2234                    self.currentCollectStr = activeChordSymbol + self.strSrc[self.pos:j]
2235                    activeChordSymbol = ''  # reset
2236                else:
2237                    self.currentCollectStr = self.strSrc[self.pos:j]
2238
2239                # environLocal.printDebug(['got chord:', repr(self.currentCollectStr)])
2240                self.tokens.append(ABCChord(self.currentCollectStr))
2241                self.skipAhead = j - (self.pos + 1)
2242                # TODO: Chords need to be aware of accidentals too.
2243                # Also what happens to prefixes and suffixes attached to chords,
2244                # like ties.
2245                continue
2246
2247            if c == '.':
2248                self.tokens.append(ABCStaccato(c))
2249                continue
2250
2251            if c == 'u':
2252                self.tokens.append(ABCUpbow(c))
2253                continue
2254
2255            if c == '{':
2256                self.tokens.append(ABCGraceStart(c))
2257                continue
2258
2259            if c == '}':
2260                self.tokens.append(ABCGraceStop(c))
2261                continue
2262
2263            if c == 'v':
2264                self.tokens.append(ABCDownbow(c))
2265                continue
2266
2267            if c == 'K':
2268                self.tokens.append(ABCAccent(c))
2269                continue
2270
2271            if c == 'k':
2272                self.tokens.append(ABCStraccent(c))
2273                continue
2274
2275            if c == 'M':
2276                self.tokens.append(ABCTenuto(c))
2277                continue
2278
2279            # get the start of a note event: alpha, decoration, or accidental
2280            if c.isalpha() or c in '~^=_':
2281                # condition where we start with an alpha that is not an alpha
2282                # that comes before a pitch indication
2283                # From the 2.2 draft standard, we see the following "decorations"
2284                # defined:
2285                #     .       staccato mark
2286                #     ~       Irish roll
2287                #     H       fermata
2288                #     L       accent or emphasis
2289                #     M       lower mordent
2290                #     O       coda
2291                #     P       upper mordent
2292                #     S       segno
2293                #     T       trill
2294                #     u       up-bow
2295                #     v       down-bow
2296                #
2297                # Accidentals are these:
2298                #     ^       sharp
2299                #     ^^      double-sharp
2300                #     =       natural
2301                #     _       flat
2302                #     __      double-flat
2303                foundPitchAlpha = c.isalpha() and c not in accidentalsAndDecorations
2304                if foundPitchAlpha:
2305                    abcPitch = c
2306                if c in accidentals:
2307                    accidental = c
2308                j = self.pos + 1
2309
2310                while j <= self.srcLen - 1:
2311                    # if we have not found pitch alpha
2312                    # decorations and/or accidentals may precede note names
2313                    if not foundPitchAlpha and self.strSrc[j] in accidentalsAndDecorations:
2314                        j += 1
2315                        if self.strSrc[j] in accidentals:
2316                            accidental += self.strSrc[j]
2317                        continue
2318                    # only allow one pitch alpha to be a continue condition
2319                    elif (not foundPitchAlpha and self.strSrc[j].isalpha()
2320                          # noinspection SpellCheckingInspection
2321                          and self.strSrc[j] not in '~wuvhHLTSN'):
2322                        foundPitchAlpha = True
2323                        abcPitch = self.strSrc[j]
2324                        j += 1
2325                        continue
2326                    # continue conditions after alpha:
2327                    # , register modification (, ') or number, rhythm indication
2328                    # number, /,
2329                    elif self.strSrc[j].isdigit() or self.strSrc[j] in ',/,\'':
2330                        if self.strSrc[j] in ',\'':  # Register (octave) modification
2331                            abcPitch += self.strSrc[j]
2332                        j += 1
2333                        continue
2334                    else:  # space, all else: break
2335                        break
2336                # prepend chord symbol
2337                if activeChordSymbol != '':
2338                    self.currentCollectStr = activeChordSymbol + self.strSrc[self.pos:j]
2339                    activeChordSymbol = ''  # reset
2340                else:
2341                    self.currentCollectStr = self.strSrc[self.pos:j]
2342                # environLocal.printDebug(['got note event:', repr(self.currentCollectStr)])
2343
2344                # NOTE: skipping a number of articulations and other markers
2345                # that are not yet supported
2346                # some collections here are not yet supported; others may be
2347                # the result of errors in encoded files
2348                # v is up bow; might be: "^Segno"v which also should be dropped
2349                # H is fermata
2350                # . dot may be staccato, but should be attached to pitch
2351                if self.currentCollectStr in ('w', 'u', 'v', 'v.', 'h', 'H', 'vk',
2352                               'uk', 'U', '~',
2353                               '.', '=', 'V', 'v.', 'S', 's',
2354                               'i', 'I', 'ui', 'u.', 'Q', 'Hy', 'Hx',
2355                               'r', 'm', 'M', 'n', 'N', 'o', 'O', 'P',
2356                               'l', 'L', 'R',
2357                               'y', 'T', 't', 'x', 'Z'):
2358                    pass
2359                # these are bad chords, or other problematic notations like
2360                # "D.C."x
2361                elif (self.currentCollectStr.startswith('"')
2362                      and (self.currentCollectStr[-1] in ('u', 'v', 'k', 'K', 'Q', '.',
2363                                                          'y', 'T', 'w', 'h', 'x',)
2364                           or self.currentCollectStr.endswith('v.'))):
2365                    pass
2366                elif (self.currentCollectStr.startswith('x')
2367                      or self.currentCollectStr.startswith('H')
2368                      or self.currentCollectStr.startswith('Z')):
2369                    pass
2370                # not sure what =20 refers to
2371                elif (len(self.currentCollectStr) > 1
2372                      and self.currentCollectStr.startswith('=')
2373                      and self.currentCollectStr[1].isdigit()):
2374                    pass
2375                # only let valid self.currentCollectStr strings be parsed
2376                elif abcPitch:
2377                    pitchClass = abcPitch[0].upper()
2378                    carriedAccidental = None
2379                    propagation = self._accidentalPropagation()
2380                    if accidental:
2381                        # Remember the active accidentals in the measure
2382                        if propagation == 'octave':
2383                            accidentalized[abcPitch] = accidental
2384                        elif propagation == 'pitch':
2385                            accidentalized[pitchClass] = accidental
2386                        accidental = None
2387                    else:
2388                        if propagation == 'pitch' and pitchClass in accidentalized:
2389                            carriedAccidental = accidentalized[pitchClass]
2390                        elif propagation == 'octave' and abcPitch in accidentalized:
2391                            carriedAccidental = accidentalized[abcPitch]
2392                    abcNote = ABCNote(self.currentCollectStr, carriedAccidental=carriedAccidental)
2393                    self.tokens.append(abcNote)
2394                else:
2395                    self.tokens.append(ABCNote(self.currentCollectStr))
2396
2397                self.skipAhead = j - (self.pos + 1)
2398                continue
2399            # look for white space: can be used to determine beam groups
2400            # no action: normal continuation of 1 char
2401            pass
2402
2403    def tokenProcess(self):
2404        '''
2405        Process all token objects. First, calls preParse(), then
2406        does context assignments, then calls parse().
2407        '''
2408        # need a key object to get altered pitches
2409        from music21 import key
2410
2411        # pre-parse : call on objects that need preliminary processing
2412        # metadata, for example, is parsed
2413        # lastTimeSignature = None
2414        for t in self.tokens:
2415            # environLocal.printDebug(['tokenProcess: calling preParse()', t.src])
2416            t.preParse()
2417
2418        # context: iterate through tokens, supplying contextual data
2419        # as necessary to appropriate objects
2420        lastDefaultQL = None
2421        lastKeySignature = None
2422        lastTimeSignatureObj = None  # an m21 object
2423        lastTupletToken = None  # a token obj; keeps count of usage
2424        lastTieToken = None
2425        lastStaccToken = None
2426        lastUpToken = None
2427        lastDownToken = None
2428        lastAccToken = None
2429        lastStrAccToken = None
2430        lastTenutoToken = None
2431        lastGraceToken = None
2432        lastNoteToken = None
2433
2434        for i in range(len(self.tokens)):
2435            # get context of tokens
2436            q = self._getLinearContext(self.tokens, i)
2437            tPrev, t, tNext, unused_tNextNext = q
2438            # tPrevNotSpace, tPrev, t, tNext, tNextNotSpace, tNextNext = q
2439            # environLocal.printDebug(['tokenProcess: calling parse()', t])
2440
2441            if isinstance(t, ABCMetadata):
2442                if t.isMeter():
2443                    lastTimeSignatureObj = t.getTimeSignatureObject()
2444                # restart matching conditions; match meter twice ok
2445                if t.isDefaultNoteLength() or (t.isMeter() and lastDefaultQL is None):
2446                    lastDefaultQL = t.getDefaultQuarterLength()
2447                elif t.isKey():
2448                    sharpCount, mode = t.getKeySignatureParameters()
2449                    lastKeySignature = key.KeySignature(sharpCount)
2450                    if mode not in (None, ''):
2451                        lastKeySignature = lastKeySignature.asKey(mode)
2452
2453                if t.isReferenceNumber():
2454                    # reset any spanners or parens at the end of any piece
2455                    # in case they aren't closed.
2456                    self.activeParens = []
2457                    self.activeSpanners = []
2458                continue
2459            # broken rhythms need to be applied to previous and next notes
2460            if isinstance(t, ABCBrokenRhythmMarker):
2461                if (isinstance(tPrev, ABCNote)
2462                        and isinstance(tNext, ABCNote)):
2463                    # environLocal.printDebug(['tokenProcess: got broken rhythm marker', t.src])
2464                    tPrev.brokenRhythmMarker = (t.data, 'left')
2465                    tNext.brokenRhythmMarker = (t.data, 'right')
2466                else:
2467                    environLocal.printDebug(
2468                        ['broken rhythm marker '
2469                         + f'({t.src}) not positioned between two notes or chords'])
2470
2471            # need to update tuplets with currently active meter
2472            if isinstance(t, ABCTuplet):
2473                t.updateRatio(lastTimeSignatureObj)
2474                # set number of notes that will be altered
2475                # might need to do this with ql values, or look ahead to nxt
2476                # token
2477                t.updateNoteCount()
2478                lastTupletToken = t
2479                self.activeParens.append('Tuplet')
2480
2481            # notes within slur marks need to be added to the spanner
2482            if isinstance(t, ABCSlurStart):
2483                t.fillSlur()
2484                self.activeSpanners.append(t.slurObj)
2485                self.activeParens.append('Slur')
2486            elif isinstance(t, ABCParenStop):
2487                if self.activeParens:
2488                    p = self.activeParens.pop()
2489                    if p in ('Slur', 'Crescendo', 'Diminuendo'):
2490                        self.activeSpanners.pop()
2491
2492            if isinstance(t, ABCTie):
2493                # tPrev is usually an ABCNote but may be a GraceStop.
2494                if lastNoteToken and lastNoteToken.tie == 'stop':
2495                    lastNoteToken.tie = 'continue'
2496                elif lastNoteToken:
2497                    lastNoteToken.tie = 'start'
2498                lastTieToken = t
2499
2500            if isinstance(t, ABCStaccato):
2501                lastStaccToken = t
2502
2503            if isinstance(t, ABCUpbow):
2504                lastUpToken = t
2505
2506            if isinstance(t, ABCDownbow):
2507                lastDownToken = t
2508
2509            if isinstance(t, ABCAccent):
2510                lastAccToken = t
2511
2512            if isinstance(t, ABCStraccent):
2513                lastStrAccToken = t
2514
2515            if isinstance(t, ABCTenuto):
2516                lastTenutoToken = t
2517
2518            if isinstance(t, ABCCrescStart):
2519                t.fillCresc()
2520                self.activeSpanners.append(t.crescObj)
2521                self.activeParens.append('Crescendo')
2522
2523            if isinstance(t, ABCDimStart):
2524                t.fillDim()
2525                self.activeSpanners.append(t.dimObj)
2526                self.activeParens.append('Diminuendo')
2527
2528            if isinstance(t, ABCGraceStart):
2529                lastGraceToken = t
2530
2531            if isinstance(t, ABCGraceStop):
2532                lastGraceToken = None
2533
2534            # ABCChord inherits ABCNote, thus getting note is enough for both
2535            if isinstance(t, (ABCNote, ABCChord)):
2536                if lastDefaultQL is None:
2537                    raise ABCHandlerException(
2538                        'no active default note length provided for note processing. '
2539                        + f'tPrev: {tPrev}, t: {t}, tNext: {tNext}'
2540                    )
2541                t.activeDefaultQuarterLength = lastDefaultQL
2542                t.activeKeySignature = lastKeySignature
2543                t.applicableSpanners = self.activeSpanners[:]  # fast copy of a list
2544                # ends ties one note after they begin
2545                if lastTieToken is not None:
2546                    t.tie = 'stop'
2547                    lastTieToken = None
2548                if lastStaccToken is not None:
2549                    t.articulations.append('staccato')
2550                    lastStaccToken = None
2551                if lastUpToken is not None:
2552                    t.articulations.append('upbow')
2553                    lastUpToken = None
2554                if lastDownToken is not None:
2555                    t.articulations.append('downbow')
2556                    lastDownToken = None
2557                if lastAccToken is not None:
2558                    t.articulations.append('accent')
2559                    lastAccToken = None
2560                if lastStrAccToken is not None:
2561                    t.articulations.append('strongaccent')
2562                    lastStrAccToken = None
2563                if lastTenutoToken is not None:
2564                    t.articulations.append('tenuto')
2565                    lastTenutoToken = None
2566                if lastGraceToken is not None:
2567                    t.inGrace = True
2568                if lastTupletToken is None:
2569                    pass
2570                elif lastTupletToken.noteCount == 0:
2571                    lastTupletToken = None  # clear, no longer needed
2572                else:
2573                    lastTupletToken.noteCount -= 1  # decrement
2574                    # add a reference to the note
2575                    t.activeTuplet = lastTupletToken.tupletObj
2576                lastNoteToken = t
2577
2578        # parse : call methods to set attributes and parse abc string
2579        for t in self.tokens:
2580            # environLocal.printDebug(['tokenProcess: calling parse()', t])
2581            t.parse()
2582
2583    def process(self, strSrc: str) -> None:
2584        self.tokens = []
2585        self.tokenize(strSrc)
2586        self.tokenProcess()
2587        # return list of tokens; stored internally
2588
2589    # --------------------------------------------------------------------------
2590    # access tokens
2591
2592    def __len__(self):
2593        return len(self.tokens)
2594
2595    def __add__(self, other):
2596        '''
2597        Return a new handler adding the tokens in both
2598
2599        Contrived example appending two separate keys.
2600
2601        Used in polyphonic metadata merge
2602
2603
2604        >>> abcStr = 'M:6/8\\nL:1/8\\nK:G\\n'
2605        >>> ah1 = abcFormat.ABCHandler()
2606        >>> junk = ah1.process(abcStr)
2607        >>> len(ah1)
2608        3
2609
2610        >>> abcStr = 'M:3/4\\nL:1/4\\nK:D\\n'
2611        >>> ah2 = abcFormat.ABCHandler()
2612        >>> junk = ah2.process(abcStr)
2613        >>> len(ah2)
2614        3
2615
2616        >>> ah3 = ah1 + ah2
2617        >>> len(ah3)
2618        6
2619        >>> ah3.tokens[0] == ah1.tokens[0]
2620        True
2621        >>> ah3.tokens[3] == ah2.tokens[0]
2622        True
2623
2624        '''
2625        ah = self.__class__()  # will get the same class type
2626        ah.tokens = self.tokens + other.tokens
2627        return ah
2628
2629    # --------------------------------------------------------------------------
2630    # utility methods for post processing
2631
2632    def definesReferenceNumbers(self):
2633        '''
2634        Return True if this token structure defines more than 1 reference number,
2635        usually implying multiple pieces encoded in one file.
2636
2637
2638        >>> abcStr = 'X:5\\nM:6/8\\nL:1/8\\nK:G\\nB3 A3 | G6 | B3 A3 | G6 ||'
2639        >>> ah = abcFormat.ABCHandler()
2640        >>> junk = ah.process(abcStr)
2641        >>> ah.definesReferenceNumbers()  # only one returns False
2642        False
2643
2644
2645        >>> abcStr = 'X:5\\nM:6/8\\nL:1/8\\nK:G\\nB3 A3 | G6 | B3 A3 | G6 ||\\n'
2646        >>> abcStr += 'X:6\\nM:6/8\\nL:1/8\\nK:G\\nB3 A3 | G6 | B3 A3 | G6 ||'
2647        >>> ah = abcFormat.ABCHandler()
2648        >>> junk = ah.process(abcStr)
2649        >>> ah.definesReferenceNumbers()  # two tokens so returns True
2650        True
2651        '''
2652        if not self.tokens:
2653            raise ABCHandlerException('must process tokens before calling split')
2654        count = 0
2655        for i in range(len(self.tokens)):
2656            t = self.tokens[i]
2657            if isinstance(t, ABCMetadata):
2658                if t.isReferenceNumber():
2659                    count += 1
2660                    if count > 1:
2661                        return True
2662        return False
2663
2664    def splitByReferenceNumber(self):
2665        # noinspection PyShadowingNames
2666        r'''
2667        Split tokens by reference numbers.
2668
2669        Returns a dictionary of ABCHandler instances, where the reference number
2670        is used to access the music. If no reference numbers are defined,
2671        the tune is available under the dictionary entry None.
2672
2673
2674        >>> abcStr = 'X:5\nM:6/8\nL:1/8\nK:G\nB3 A3 | G6 | B3 A3 | G6 ||'
2675        >>> abcStr += 'X:6\nM:6/8\nL:1/8\nK:G\nB3 A3 | G6 | B3 A3 | G6 ||'
2676        >>> ah = abcFormat.ABCHandler()
2677        >>> junk = ah.process(abcStr)
2678        >>> len(ah)
2679        28
2680        >>> ahDict = ah.splitByReferenceNumber()
2681        >>> 5 in ahDict
2682        True
2683        >>> 6 in ahDict
2684        True
2685        >>> 7 in ahDict
2686        False
2687
2688        Each entry is its own ABCHandler object.
2689
2690        >>> ahDict[5]
2691        <music21.abcFormat.ABCHandler object at 0x10b0cf5f8>
2692        >>> len(ahDict[5].tokens)
2693        14
2694
2695        Header information (except for comments) should be appended to all pieces.
2696
2697        >>> abcStrWHeader = '%abc-2.1\nO: Irish\n' + abcStr
2698        >>> ah = abcFormat.ABCHandler()
2699        >>> junk = ah.process(abcStrWHeader)
2700        >>> len(ah)
2701        29
2702        >>> ahDict = ah.splitByReferenceNumber()
2703        >>> 5 in ahDict
2704        True
2705        >>> 6 in ahDict
2706        True
2707        >>> 7 in ahDict
2708        False
2709
2710        Did we get the origin header in each score?
2711
2712        >>> ahDict[5].tokens[0]
2713        <music21.abcFormat.ABCMetadata 'O: Irish'>
2714        >>> ahDict[6].tokens[0]
2715        <music21.abcFormat.ABCMetadata 'O: Irish'>
2716        '''
2717        if not self.tokens:
2718            raise ABCHandlerException('must process tokens before calling split')
2719
2720        ahDict = {}
2721
2722        # tokens in this list are prepended to all tunes:
2723        prependToAllList = []
2724        activeTokens = []
2725        currentABCHandler = None
2726
2727        for i, t in enumerate(self.tokens):
2728            if isinstance(t, ABCMetadata) and t.isReferenceNumber():
2729                if currentABCHandler is not None:
2730                    currentABCHandler.tokens = activeTokens
2731                    activeTokens = []
2732                currentABCHandler = ABCHandler()
2733                referenceNumber = int(t.data)
2734                ahDict[referenceNumber] = currentABCHandler
2735
2736            if currentABCHandler is None:
2737                prependToAllList.append(t)
2738            else:
2739                activeTokens.append(t)
2740
2741        if currentABCHandler is not None:
2742            currentABCHandler.tokens = activeTokens
2743
2744        if not ahDict:
2745            ahDict[None] = ABCHandler()
2746
2747        for thisABCHandler in ahDict.values():
2748            thisABCHandler.tokens = prependToAllList[:] + thisABCHandler.tokens
2749
2750        return ahDict
2751
2752    def getReferenceNumber(self):
2753        '''
2754        If tokens are processed, get the first
2755        reference number defined.
2756
2757
2758        >>> abcStr = 'X:5\\nM:6/8\\nL:1/8\\nK:G\\nB3 A3 | G6 | B3 A3 | G6 ||'
2759        >>> ah = abcFormat.ABCHandler()
2760        >>> junk = ah.process(abcStr)
2761        >>> ah.getReferenceNumber()
2762        '5'
2763        '''
2764        if not self.tokens:
2765            raise ABCHandlerException('must process tokens before calling split')
2766        for t in self.tokens:
2767            if isinstance(t, ABCMetadata):
2768                if t.isReferenceNumber():
2769                    return t.data
2770        return None
2771
2772    def definesMeasures(self):
2773        '''
2774        Returns True if this token structure defines Measures in a normal Measure form.
2775        Otherwise False
2776
2777        >>> abcStr = ('M:6/8\\nL:1/8\\nK:G\\nV:1 name="Whistle" ' +
2778        ...     'snm="wh"\\nB3 A3 | G6 | B3 A3 | G6 ||\\nV:2 name="violin" ' +
2779        ...     'snm="v"\\nBdB AcA | GAG D3 | BdB AcA | GAG D6 ||\\nV:3 name="Bass" ' +
2780        ...     'snm="b" clef=bass\\nD3 D3 | D6 | D3 D3 | D6 ||')
2781        >>> ah = abcFormat.ABCHandler()
2782        >>> junk = ah.process(abcStr)
2783        >>> ah.definesMeasures()
2784        True
2785
2786        >>> abcStr = 'M:6/8\\nL:1/8\\nK:G\\nB3 A3 G6 B3 A3 G6'
2787        >>> ah = abcFormat.ABCHandler()
2788        >>> junk = ah.process(abcStr)
2789        >>> ah.definesMeasures()
2790        False
2791        '''
2792        if not self.tokens:
2793            raise ABCHandlerException('must process tokens before calling split')
2794        count = 0
2795        for i in range(len(self.tokens)):
2796            t = self.tokens[i]
2797            if isinstance(t, ABCBar):
2798                # must define at least 2 regular barlines
2799                # this leave out cases where only double bars are given
2800                if t.isRegular():
2801                    count += 1
2802                    # forcing the inclusion of two measures to count
2803                    if count >= 2:
2804                        return True
2805        return False
2806
2807    def splitByVoice(self) -> List['ABCHandler']:
2808        # noinspection PyShadowingNames
2809        '''
2810        Given a processed token list, look for voices. If voices exist,
2811        split into parts: common metadata, then next voice, next voice, etc.
2812
2813        Each part is returned as a ABCHandler instance.
2814
2815        >>> abcStr = ('M:6/8\\nL:1/8\\nK:G\\nV:1 name="Whistle" ' +
2816        ...     'snm="wh"\\nB3 A3 | G6 | B3 A3 | G6 ||\\nV:2 name="violin" ' +
2817        ...     'snm="v"\\nBdB AcA | GAG D3 | BdB AcA | GAG D6 ||\\nV:3 name="Bass" ' +
2818        ...     'snm="b" clef=bass\\nD3 D3 | D6 | D3 D3 | D6 ||')
2819        >>> ah = abcFormat.ABCHandler()
2820        >>> ah.process(abcStr)
2821        >>> tokenColls = ah.splitByVoice()
2822        >>> tokenColls[0]
2823        <music21.abcFormat.ABCHandler object at 0x...>
2824
2825        Common headers are first
2826
2827        >>> [t.src for t in tokenColls[0].tokens]
2828        ['M:6/8', 'L:1/8', 'K:G']
2829
2830        Then each voice
2831
2832        >>> [t.src for t in tokenColls[1].tokens]
2833        ['V:1 name="Whistle" snm="wh"', 'B3', 'A3', '|', 'G6', '|', 'B3', 'A3', '|', 'G6', '||']
2834        >>> [t.src for t in tokenColls[2].tokens]
2835        ['V:2 name="violin" snm="v"', 'B', 'd', 'B', 'A', 'c', 'A', '|',
2836         'G', 'A', 'G', 'D3', '|', 'B', 'd', 'B', 'A', 'c', 'A', '|', 'G', 'A', 'G', 'D6', '||']
2837        >>> [t.src for t in tokenColls[3].tokens]
2838        ['V:3 name="Bass" snm="b" clef=bass', 'D3', 'D3', '|', 'D6', '|',
2839         'D3', 'D3', '|', 'D6', '||']
2840
2841        Then later the metadata can be merged at the start of each voice...
2842
2843        >>> mergedTokens = tokenColls[0] + tokenColls[1]
2844        >>> mergedTokens
2845        <music21.abcFormat.ABCHandler object at 0x...>
2846        >>> [t.src for t in mergedTokens.tokens]
2847        ['M:6/8', 'L:1/8', 'K:G', 'V:1 name="Whistle" snm="wh"',
2848         'B3', 'A3', '|', 'G6', '|', 'B3', 'A3', '|', 'G6', '||']
2849        '''
2850        # TODO: this procedure should also be responsible for
2851        #     breaking the passage into voice/lyric pairs
2852
2853        if not self.tokens:
2854            raise ABCHandlerException('must process tokens before calling split')
2855
2856        voiceCount = 0
2857        pos = []
2858        for i in range(len(self.tokens)):
2859            t = self.tokens[i]
2860            if isinstance(t, ABCMetadata):
2861                if t.isVoice():
2862                    # if first char is a number
2863                    # can be V:3 name="Bass" snm="b" clef=bass
2864                    if t.data[0].isdigit():
2865                        pos.append(i)  # store position
2866                        voiceCount += 1
2867
2868        abcHandlers = []
2869        # no voices, or definition of one voice, or use of V: field for
2870        # something else
2871        if voiceCount <= 1:
2872            ah = self.__class__()  # just making a copy
2873            ah.tokens = self.tokens
2874            abcHandlers.append(ah)
2875        # two or more voices
2876        else:
2877            # collect start and end pairs of split
2878            pairs = []
2879            pairs.append([0, pos[0]])
2880            i = pos[0]
2881            for x in range(1, len(pos)):
2882                j = pos[x]
2883                pairs.append([i, j])
2884                i = j
2885            # add last
2886            pairs.append([i, len(self)])
2887
2888            for x, y in pairs:
2889                ah = self.__class__()
2890                ah.tokens = self.tokens[x:y]
2891                abcHandlers.append(ah)
2892
2893        return abcHandlers
2894
2895    @staticmethod
2896    def _buildMeasureBoundaryIndices(
2897        positionList: List[int],
2898        lastValidIndex: int
2899    ) -> List[List[int]]:
2900        '''
2901        Staticmethod
2902
2903        Given a list of indices of a list marking the position of
2904        each barline or implied barline, and the last valid index,
2905        return a list of two-element lists, each indicating
2906        the start and positions of a measure.
2907
2908        Here's an easy case that makes this method look worthless:
2909
2910        >>> AH = abcFormat.ABCHandler
2911        >>> AH._buildMeasureBoundaryIndices([8, 12, 16], 20)
2912        [[0, 8], [8, 12], [12, 16], [16, 20]]
2913
2914        But in this case, we need to see that 12 and 13 don't represent different measures but
2915        probably represent an end and new barline (repeat bar), etc.
2916
2917        >>> AH._buildMeasureBoundaryIndices([8, 12, 13, 16], 20)
2918        [[0, 8], [8, 12], [13, 16], [16, 20]]
2919
2920        Here 115 is both the last barline and the last index, so there is no [115, 115] entry.
2921
2922        >>> bi = [9, 10, 16, 23, 29, 36, 42, 49, 56, 61, 62, 64, 70, 77, 84, 90, 96, 103, 110, 115]
2923        >>> AH._buildMeasureBoundaryIndices(bi, 115)
2924        [[0, 9], [10, 16], [16, 23], [23, 29], [29, 36], [36, 42], [42, 49], [49, 56], [56, 61],
2925         [62, 64], [64, 70], [70, 77], [77, 84], [84, 90], [90, 96],
2926         [96, 103], [103, 110], [110, 115]]
2927
2928        '''
2929        # collect start and end pairs of split
2930        pairs = []
2931        # first chunk is metadata, as first token is probably not a bar
2932        pairs.append([0, positionList[0]])
2933        i = positionList[0]  # get first bar position stored
2934        # iterate through every other bar position (already have first)
2935        for x in range(1, len(positionList)):
2936            j = positionList[x]
2937            if j == i + 1:  # a span of one is skipped
2938                i = j
2939                continue
2940            pairs.append([i, j])
2941            i = j  # the end becomes the new start
2942        # add last valid index
2943        if i != lastValidIndex:
2944            pairs.append([i, lastValidIndex])
2945        # environLocal.printDebug(['splitByMeasure(); pairs pre filter', pairs])
2946        return pairs
2947
2948    def splitByMeasure(self) -> List['ABCHandlerBar']:
2949        '''
2950        Divide a token list by Measures, also
2951        defining start and end bars of each Measure.
2952
2953        If a component does not have notes, leave
2954        as an empty bar. This is often done with leading metadata.
2955
2956        Returns a list of ABCHandlerBar instances.
2957        The first usually defines only Metadata
2958
2959        TODO: Test and examples
2960        '''
2961        if not self.tokens:
2962            raise ABCHandlerException('must process tokens before calling split')
2963
2964        abcBarHandlers = []
2965        barIndices = self.tokensToBarIndices()
2966
2967        # barCount = 0  # not used
2968        # noteCount = 0  # not used
2969
2970        # environLocal.printDebug(['splitByMeasure(); raw bar positions', barIndices])
2971        measureIndices = self._buildMeasureBoundaryIndices(barIndices, len(self) - 1)
2972        # for x, y in pairs:
2973        #     environLocal.printDebug(['boundary indices:', x, y])
2974        #     environLocal.printDebug(['    values at x, y', self.tokens[x], self.tokens[y]])
2975
2976        # iterate through start and end pairs
2977        for x, y in measureIndices:
2978            ah = ABCHandlerBar()
2979            # this will get the first to last
2980            # shave of tokens if not needed
2981            xClip = x
2982            yClip = y
2983
2984            # check if first is a bar; if so, assign and remove
2985            if isinstance(self.tokens[x], ABCBar):
2986                lbCandidate = self.tokens[x]
2987                # if we get an end repeat, probably already assigned this
2988                # in the last measure, so skip
2989                # environLocal.printDebug(['reading pairs, got token:', lbCandidate,
2990                #    'lbCandidate.barType', lbCandidate.barType,
2991                #    'lbCandidate.repeatForm', lbCandidate.repeatForm])
2992                # skip end repeats assigned (improperly) to the left
2993                if (lbCandidate.barType == 'repeat'
2994                        and lbCandidate.repeatForm == 'end'):
2995                    pass
2996                else:  # assign
2997                    ah.leftBarToken = lbCandidate
2998                    # environLocal.printDebug(['splitByMeasure(); assigning left bar token',
2999                    #                        lbCandidate])
3000                # always trim if we have a bar
3001                xClip = x + 1
3002                # ah.tokens = ah.tokens[1:]  # remove first, as not done above
3003
3004            # if x boundary is metadata, do not include it (as it is likely in the previous
3005            # measure) unless it is at the beginning.
3006            elif x != 0 and isinstance(self.tokens[x], ABCMetadata):
3007                xClip = x + 1
3008            else:
3009                # if we find a note in the x-clip position, it is likely a pickup the
3010                # first note after metadata. this we keep, b/c it
3011                # should be part of this branch
3012                pass
3013
3014            if y >= len(self):
3015                yTestIndex = len(self)
3016            else:
3017                yTestIndex = y
3018
3019            if isinstance(self.tokens[yTestIndex], ABCBar):
3020                rbCandidate = self.tokens[yTestIndex]
3021                # if a start repeat, save it to be placed as a left barline
3022                if not (rbCandidate.barType == 'repeat'
3023                        and rbCandidate.repeatForm == 'start'):
3024                    # environLocal.printDebug(['splitByMeasure(); assigning right bar token',
3025                    #                             lbCandidate])
3026                    ah.rightBarToken = self.tokens[yTestIndex]
3027                # always trim if we have a bar
3028                # ah.tokens = ah.tokens[:-1]  # remove last
3029                yClip = y - 1
3030            # if y boundary is metadata, include it
3031            elif isinstance(self.tokens[yTestIndex], ABCMetadata):
3032                pass  # no change
3033            # if y position is a note/chord, and this is the last index,
3034            # must included it
3035            elif not (isinstance(self.tokens[yTestIndex], (ABCNote, ABCChord))
3036                      and yTestIndex == len(self.tokens) - 1):
3037                # if we find a note in the yClip position, it is likely
3038                # a pickup, the first note after metadata. we do not include this
3039                yClip = yTestIndex - 1
3040
3041            # environLocal.printDebug(['clip boundaries: x,y', xClip, yClip])
3042            # boundaries are inclusive; need to add one here
3043            ah.tokens = self.tokens[xClip:yClip + 1]
3044            # after bar assign, if no bars known, reject
3045            if not ah:
3046                continue
3047            abcBarHandlers.append(ah)
3048
3049        # for sub in abcBarHandlers:
3050        #     environLocal.printDebug(['concluded splitByMeasure:', sub,
3051        #            'leftBarToken', sub.leftBarToken, 'rightBarToken', sub.rightBarToken,
3052        #            'len(sub)', len(sub), 'sub.hasNotes()', sub.hasNotes()])
3053        #     for t in sub.tokens:
3054        #         print('\t', t)
3055        return abcBarHandlers
3056
3057    def tokensToBarIndices(self) -> List[int]:
3058        '''
3059        Return a list of indices indicating which tokens in self.tokens are
3060        bar lines or the last piece of metadata before a note or chord.
3061        '''
3062        barIndices = []
3063        tNext = None
3064        for i, t in enumerate(self.tokens):
3065            try:
3066                tNext = self.tokens[i + 1]
3067            except IndexError:
3068                tNext = None
3069
3070            # either we get a bar, or we just complete metadata and we
3071            # encounter a note (a pickup)
3072            if isinstance(t, ABCBar):  # or (barCount == 0 and noteCount > 0):
3073                # environLocal.printDebug(['splitByMeasure()', 'found bar', t])
3074                barIndices.append(i)  # store position
3075                # barCount += 1  # not used
3076            # case of end of metadata and start of notes in a pickup
3077            # tag the last metadata as the end
3078            elif (isinstance(t, ABCMetadata)
3079                  and tNext is not None
3080                  and isinstance(tNext, (ABCNote, ABCChord))):
3081                barIndices.append(i)  # store position
3082
3083        return barIndices
3084
3085    def hasNotes(self) -> bool:
3086        '''
3087        If tokens are processed, return True if ABCNote or
3088        ABCChord classes are defined
3089
3090
3091        >>> abcStr = 'M:6/8\\nL:1/8\\nK:G\\n'
3092        >>> ah1 = abcFormat.ABCHandler()
3093        >>> junk = ah1.process(abcStr)
3094        >>> ah1.hasNotes()
3095        False
3096
3097        >>> abcStr = 'M:6/8\\nL:1/8\\nK:G\\nc1D2'
3098        >>> ah2 = abcFormat.ABCHandler()
3099        >>> junk = ah2.process(abcStr)
3100        >>> ah2.hasNotes()
3101        True
3102        '''
3103        if not self.tokens:
3104            raise ABCHandlerException('must process tokens before calling')
3105        count = 0
3106        for t in self.tokens:
3107            if isinstance(t, (ABCNote, ABCChord)):
3108                count += 1
3109        # environLocal.printDebug(['hasNotes', count])
3110        if count > 0:
3111            return True
3112        else:
3113            return False
3114
3115    def getTitle(self) -> Optional[str]:
3116        '''
3117        Get the first title tag. Used for testing.
3118
3119        Requires tokens to have been processed.
3120        '''
3121        if not self.tokens:
3122            raise ABCHandlerException('must process tokens before calling split')
3123        for t in self.tokens:
3124            if isinstance(t, ABCMetadata):
3125                if t.isTitle():
3126                    return t.data
3127        return None
3128
3129
3130class ABCHandlerBar(ABCHandler):
3131    '''
3132    A Handler specialized for storing bars. All left
3133    and right bars are collected and assigned to attributes.
3134    '''
3135    # divide elements of a character stream into objects and handle
3136    # store in a list, and pass global information to components
3137
3138    def __init__(self):
3139        # tokens are ABC objects in a linear stream
3140        super().__init__()
3141
3142        self.leftBarToken = None
3143        self.rightBarToken = None
3144
3145    def __add__(self, other):
3146        ah = self.__class__()  # will get the same class type
3147        ah.tokens = self.tokens + other.tokens
3148        # get defined tokens
3149        for barAttr in ('leftBarToken', 'rightBarToken'):
3150            bOld = getattr(self, barAttr)
3151            bNew = getattr(other, barAttr)
3152            if bNew is None and bOld is None:
3153                pass  # nothing to do
3154            elif bNew is not None and bOld is None:  # get new
3155                setattr(ah, barAttr, bNew)
3156            elif bNew is None and bOld is not None:  # get old
3157                setattr(ah, barAttr, bOld)
3158            else:
3159                # if both ar the same, assign one
3160                if bOld.src == bNew.src:
3161                    setattr(ah, barAttr, bNew)
3162                else:
3163                    # might resolve this by ignoring standard bars and favoring
3164                    # repeats or styled bars
3165                    environLocal.printDebug(['cannot handle two non-None bars yet: got bNew, bOld',
3166                                             bNew, bOld])
3167                    # raise ABCHandlerException('cannot handle two non-None bars yet')
3168                    setattr(ah, barAttr, bNew)
3169
3170        return ah
3171
3172
3173def mergeLeadingMetaData(barHandlers: List[ABCHandlerBar]) -> List[ABCHandlerBar]:
3174    '''
3175    Given a list of ABCHandlerBar objects, return a list of ABCHandlerBar
3176    objects where leading metadata is merged, if possible,
3177    with the bar data following.
3178
3179    This consolidates all metadata in bar-like entities.
3180    '''
3181    mCount = 0
3182    metadataPos = []  # store indices of handlers that are all metadata
3183    for i in range(len(barHandlers)):
3184        if barHandlers[i].hasNotes():
3185            mCount += 1
3186        else:
3187            metadataPos.append(i)
3188    # environLocal.printDebug(['mergeLeadingMetaData()',
3189    #                        'metadataPosList', metadataPos, 'mCount', mCount])
3190    # merge meta data into bars for processing
3191    mergedHandlers = []
3192    if mCount <= 1:  # if only one true measure, do not create measures
3193        ahb = ABCHandlerBar()
3194        for h in barHandlers:
3195            ahb += h  # concatenate all
3196        mergedHandlers.append(ahb)
3197    else:
3198        # when we have metadata, we need to pass its tokens with those
3199        # of the measure that follows it; if we have trailing meta data,
3200        # we can pass but do not create a measure
3201        i = 0
3202        while i < len(barHandlers):
3203            # if we find metadata and it is not the last valid index
3204            # merge into a single handler
3205            if i in metadataPos and i != len(barHandlers) - 1:
3206                mergedHandlers.append(barHandlers[i] + barHandlers[i + 1])
3207                i += 2
3208            else:
3209                mergedHandlers.append(barHandlers[i])
3210                i += 1
3211
3212    return mergedHandlers
3213
3214# ------------------------------------------------------------------------------
3215
3216
3217class ABCFile(prebase.ProtoM21Object):
3218    '''
3219    ABC File or String access
3220
3221    The abcVersion attribution optionally specifies the (major, minor, patch)
3222    version of ABC to process-- e.g., (1.2.0).
3223    If not set, default ABC 1.3 parsing is performed.
3224    '''
3225    def __init__(self, abcVersion=None):
3226        self.abcVersion = abcVersion
3227        self.file = None
3228        self.filename = None
3229
3230    def open(self, filename):
3231        '''
3232        Open a file for reading
3233        '''
3234        # try:
3235        self.file = io.open(filename, encoding='utf-8')  # pylint: disable=consider-using-with
3236        # except
3237        # self.file = io.open(filename, encoding='latin-1')
3238        self.filename = filename
3239
3240    def openFileLike(self, fileLike):
3241        '''
3242        Assign a file-like object, such as those provided by
3243        StringIO, as an open file object.
3244
3245        >>> from io import StringIO
3246        >>> fileLikeOpen = StringIO()
3247        '''
3248        self.file = fileLike  # already 'open'
3249
3250    def _reprInternal(self):
3251        return ''
3252
3253    def close(self):
3254        self.file.close()
3255
3256    def read(self, number=None):
3257        '''
3258        Read a file. Note that this calls readstr,
3259        which processes all tokens.
3260
3261        If `number` is given, a work number will be extracted if possible.
3262        '''
3263        return self.readstr(self.file.read(), number)
3264
3265    @staticmethod
3266    def extractReferenceNumber(strSrc: str, number: int) -> str:
3267        '''
3268        Extract the string data relating to a single reference number
3269        from a file that defines multiple songs or pieces.
3270
3271        This method permits loading a single work from a collection/opus
3272        without parsing the entire file.
3273
3274        Here is sample data that is not correct ABC but demonstrates the basic concept:
3275
3276        >>> fileData = """
3277        ...   X:1
3278        ...   Hello
3279        ...   X:2
3280        ...   Aloha
3281        ...   X:3
3282        ...   Goodbye
3283        ...   """
3284
3285        >>> file2 = abcFormat.ABCFile.extractReferenceNumber(fileData, 2)
3286        >>> print(file2)
3287        X:2
3288        Aloha
3289
3290        If the number does not exist, raises an ABCFileException:
3291
3292        >>> abcFormat.ABCFile.extractReferenceNumber(fileData, 99)
3293        Traceback (most recent call last):
3294        music21.abcFormat.ABCFileException: cannot find requested
3295            reference number in source file: 99
3296
3297
3298        If the same number is defined twice in one file (should not be) only
3299        the first data is returned.
3300
3301        Changed in v6.2: now a static method.
3302        '''
3303        collect = []
3304        gather = False
3305        for line in strSrc.split('\n'):
3306            # must be a single line definition
3307            # rstrip because of '\r\n' carriage returns
3308            if line.strip().startswith('X:') and line.replace(' ', '').rstrip() == f'X:{number}':
3309                gather = True
3310            elif line.strip().startswith('X:') and not gather:
3311                # some numbers are like X:0490 but we may request them as 490...
3312                try:
3313                    forcedNum = int(line.replace(' ', '').rstrip().replace('X:', ''))
3314                    if forcedNum == int(number):
3315                        gather = True
3316                except TypeError:
3317                    pass
3318            # if already gathering and find another ref number definition
3319            # stop gathering
3320            elif gather and line.strip().startswith('X:'):
3321                break
3322
3323            if gather:
3324                collect.append(line)
3325
3326        if not collect:
3327            raise ABCFileException(
3328                f'cannot find requested reference number in source file: {number}')
3329
3330        referenceNumbers = '\n'.join(collect)
3331        return referenceNumbers
3332
3333    def readstr(self, strSrc: str, number: Optional[int] = None) -> ABCHandler:
3334        '''
3335        Read a string and process all Tokens.
3336        Returns a ABCHandler instance.
3337        '''
3338        if number is not None:
3339            # will raise exception if cannot be found
3340            strSrc = self.extractReferenceNumber(strSrc, number)
3341
3342        handler = ABCHandler(abcVersion=self.abcVersion)
3343        # return the handler instance
3344        handler.process(strSrc)
3345        return handler
3346
3347
3348# ------------------------------------------------------------------------------
3349class Test(unittest.TestCase):
3350
3351    def testTokenization(self):
3352        from music21.abcFormat import testFiles
3353
3354        for (tf, countTokens, noteTokens, chordTokens) in [
3355            (testFiles.fyrareprisarn, 241, 152, 0),
3356            (testFiles.mysteryReel, 192, 153, 0),
3357            (testFiles.aleIsDear, 291, 206, 32),
3358            (testFiles.testPrimitive, 100, 75, 2),
3359            (testFiles.williamAndNancy, 127, 93, 0),
3360            (testFiles.morrisonsJig, 178, 137, 0),
3361        ]:
3362
3363            handler = ABCHandler()
3364            handler.tokenize(tf)
3365            tokens = handler.tokens  # get private for testing
3366            self.assertEqual(len(tokens), countTokens)
3367            countNotes = 0
3368            countChords = 0
3369            for o in tokens:
3370                if isinstance(o, ABCChord):
3371                    countChords += 1
3372                elif isinstance(o, ABCNote):
3373                    countNotes += 1
3374
3375            self.assertEqual(countNotes, noteTokens)
3376            self.assertEqual(countChords, chordTokens)
3377
3378    def testRe(self):
3379
3380        src = 'A: this is a test'
3381        post = reMetadataTag.match(src).end()
3382        self.assertEqual(src[:post], 'A:')
3383        self.assertEqual(src[post:], ' this is a test')
3384
3385        src = 'Q: this is a test % and a following comment'
3386        post = reMetadataTag.match(src).end()
3387        self.assertEqual(src[:post], 'Q:')
3388
3389        # chord symbol matches
3390        src = 'd|"G"e2d B2d|"C"gfe "D7"d2d|"G"e2d B2d|"A7""C"gfe "D7""D"d2c|'
3391        post = reChordSymbol.findall(src)
3392        self.assertEqual(post, ['"G"', '"C"', '"D7"', '"G"', '"A7"',
3393                                '"C"', '"D7"', '"D"'])
3394
3395        # get index of last match of many
3396        i = list(reChordSymbol.finditer(src))[-1].end()
3397
3398        src = '=d2'
3399        self.assertEqual(rePitchName.findall(src)[0], 'd')
3400
3401        src = 'A3/2'
3402        self.assertEqual(rePitchName.findall(src)[0], 'A')
3403
3404    def testTokenProcessMetadata(self):
3405        from music21.abcFormat import testFiles
3406
3407        # noinspection SpellCheckingInspection
3408        for (tf, titleEncoded, meterEncoded, keyEncoded) in [
3409            (testFiles.fyrareprisarn, 'Fyrareprisarn', '3/4', 'F'),
3410            (testFiles.mysteryReel, 'Mystery Reel', 'C|', 'G'),
3411            (testFiles.aleIsDear, 'Ale is Dear, The', '4/4', 'D', ),
3412            (testFiles.kitchGirl, 'Kitchen Girl', '4/4', 'D'),
3413            (testFiles.williamAndNancy, 'William and Nancy', '6/8', 'G'),
3414        ]:
3415
3416            handler = ABCHandler()
3417            handler.tokenize(tf)
3418            handler.tokenProcess()
3419
3420            tokens = handler.tokens  # get private for testing
3421            for t in tokens:
3422                if isinstance(t, ABCMetadata):
3423                    if t.tag == 'T':
3424                        self.assertEqual(t.data, titleEncoded)
3425                    elif t.tag == 'M':
3426                        self.assertEqual(t.data, meterEncoded)
3427                    elif t.tag == 'K':
3428                        self.assertEqual(t.data, keyEncoded)
3429
3430    def testTokenProcess(self):
3431        from music21.abcFormat import testFiles
3432
3433        for tf in [
3434            testFiles.fyrareprisarn,
3435            testFiles.mysteryReel,
3436            testFiles.aleIsDear,
3437            testFiles.testPrimitive,
3438            testFiles.kitchGirl,
3439            testFiles.williamAndNancy,
3440        ]:
3441
3442            handler = ABCHandler()
3443            handler.tokenize(tf)
3444            handler.tokenProcess()
3445
3446    def testNoteParse(self):
3447        from music21 import key
3448
3449        an = ABCNote()
3450
3451        # with a key signature, matching steps are assumed altered
3452        an.activeKeySignature = key.KeySignature(3)
3453        self.assertEqual(an.getPitchName('c'), ('C#5', False))
3454
3455        an.activeKeySignature = None
3456        self.assertEqual(an.getPitchName('c'), ('C5', None))
3457        self.assertEqual(an.getPitchName('^c'), ('C#5', True))
3458
3459        an.activeKeySignature = key.KeySignature(-3)
3460        self.assertEqual(an.getPitchName('B'), ('B-4', False))
3461
3462        an.activeKeySignature = None
3463        self.assertEqual(an.getPitchName('B'), ('B4', None))
3464        self.assertEqual(an.getPitchName('_B'), ('B-4', True))
3465
3466    def testSplitByMeasure(self):
3467
3468        from music21.abcFormat import testFiles
3469
3470        ah = ABCHandler()
3471        ah.process(testFiles.hectorTheHero)
3472        ahm = ah.splitByMeasure()
3473
3474        for i, l, r in [(0, None, None),  # meta data
3475                        (2, '|:', '|'),
3476                        (3, '|', '|'),
3477                        (-2, '[1', ':|'),
3478                        (-1, '[2', '|'),
3479                        ]:
3480            # print('expecting', i, l, r, ahm[i].tokens)
3481            # print('have', ahm[i].leftBarToken, ahm[i].rightBarToken)
3482            # print()
3483            if l is None:
3484                self.assertEqual(ahm[i].leftBarToken, None)
3485            else:
3486                self.assertEqual(ahm[i].leftBarToken.src, l)
3487
3488            if r is None:
3489                self.assertEqual(ahm[i].rightBarToken, None)
3490            else:
3491                self.assertEqual(ahm[i].rightBarToken.src, r)
3492
3493        # for ahSub in ah.splitByMeasure():
3494        #     environLocal.printDebug(['split by measure:', ahSub.tokens])
3495        #     environLocal.printDebug(['leftBar:', ahSub.leftBarToken,
3496        #        'rightBar:', ahSub.rightBarToken, '\n'])
3497
3498        ah = ABCHandler()
3499        ah.process(testFiles.theBeggerBoy)
3500        ahm = ah.splitByMeasure()
3501
3502        for i, l, r in [(0, None, None),  # meta data
3503                        (1, None, '|'),
3504                        (-1, '||', None),  # trailing lyric meta data
3505                        ]:
3506            # print(i, l, r, ahm[i].tokens)
3507            if l is None:
3508                self.assertEqual(ahm[i].leftBarToken, None)
3509            else:
3510                self.assertEqual(ahm[i].leftBarToken.src, l)
3511
3512            if r is None:
3513                self.assertEqual(ahm[i].rightBarToken, None)
3514            else:
3515                self.assertEqual(ahm[i].rightBarToken.src, r)
3516
3517        # test a simple string with no bars
3518        ah = ABCHandler()
3519        ah.process('M:6/8\nL:1/8\nK:G\nc1D2')
3520        ahm = ah.splitByMeasure()
3521
3522        for i, l, r in [(0, None, None),  # meta data
3523                        (-1, None, None),  # note data, but no bars
3524                        ]:
3525            # print(i, l, r, ahm[i].tokens)
3526            if l is None:
3527                self.assertEqual(ahm[i].leftBarToken, None)
3528            else:
3529                self.assertEqual(ahm[i].leftBarToken.src, l)
3530
3531            if r is None:
3532                self.assertEqual(ahm[i].rightBarToken, None)
3533            else:
3534                self.assertEqual(ahm[i].rightBarToken.src, r)
3535
3536    def testMergeLeadingMetaData(self):
3537        from music21.abcFormat import testFiles
3538
3539        # a case of leading and trailing meta data
3540        ah = ABCHandler()
3541        ah.process(testFiles.theBeggerBoy)
3542        ahm = ah.splitByMeasure()
3543
3544        self.assertEqual(len(ahm), 14)
3545
3546        mergedHandlers = mergeLeadingMetaData(ahm)
3547
3548        # after merging, one less handler as leading meta data is merged
3549        self.assertEqual(len(mergedHandlers), 13)
3550        # the last handler is all trailing metadata
3551        self.assertTrue(mergedHandlers[0].hasNotes())
3552        self.assertFalse(mergedHandlers[-1].hasNotes())
3553        self.assertTrue(mergedHandlers[-2].hasNotes())
3554        # these are all ABCHandlerBar instances with bars defined
3555        self.assertEqual(mergedHandlers[-2].rightBarToken.src, '||')
3556
3557        # a case of only leading meta data
3558        ah = ABCHandler()
3559        ah.process(testFiles.theAleWifesDaughter)
3560        ahm = ah.splitByMeasure()
3561
3562        self.assertEqual(len(ahm), 10)
3563
3564        mergedHandlers = mergeLeadingMetaData(ahm)
3565        # after merging, one less handler as leading meta data is merged
3566        self.assertEqual(len(mergedHandlers), 10)
3567        # all handlers have notes
3568        self.assertTrue(mergedHandlers[0].hasNotes())
3569        self.assertTrue(mergedHandlers[-1].hasNotes())
3570        self.assertTrue(mergedHandlers[-2].hasNotes())
3571        # these are all ABCHandlerBar instances with bars defined
3572        self.assertEqual(mergedHandlers[-1].rightBarToken.src, '|]')
3573
3574        # test a simple string with no bars
3575        ah = ABCHandler()
3576        ah.process('M:6/8\nL:1/8\nK:G\nc1D2')
3577        ahm = ah.splitByMeasure()
3578
3579        # split by measure divides meta data
3580        self.assertEqual(len(ahm), 2)
3581        mergedHandlers = mergeLeadingMetaData(ahm)
3582        # after merging, meta data is merged back
3583        self.assertEqual(len(mergedHandlers), 1)
3584        # and it has notes
3585        self.assertTrue(mergedHandlers[0].hasNotes())
3586
3587    def testSplitByReferenceNumber(self):
3588        from music21.abcFormat import testFiles
3589
3590        # a case of leading and trailing meta data
3591        ah = ABCHandler()
3592        ah.process(testFiles.theBeggerBoy)
3593        ahs = ah.splitByReferenceNumber()
3594        self.assertEqual(len(ahs), 1)
3595        self.assertEqual(list(ahs.keys()), [5])
3596        self.assertEqual(len(ahs[5]), 88)  # tokens
3597        self.assertEqual(ahs[5].tokens[0].src, 'X:5')  # first is retained
3598        # noinspection SpellCheckingInspection
3599        self.assertEqual(ahs[5].getTitle(), 'The Begger Boy')  # tokens
3600
3601        ah = ABCHandler()
3602        ah.process(testFiles.testPrimitivePolyphonic)  # has no reference num
3603        self.assertEqual(len(ah), 47)  # tokens
3604
3605        ahs = ah.splitByReferenceNumber()
3606        self.assertEqual(len(ahs), 1)
3607        self.assertEqual(list(ahs.keys()), [None])
3608        self.assertEqual(ahs[None].tokens[0].src, 'M:6/8')  # first is retained
3609        self.assertEqual(len(ahs[None]), 47)  # tokens
3610
3611        ah = ABCHandler()
3612        ah.process(testFiles.valentineJigg)  # has no reference num
3613        self.assertEqual(len(ah), 244)  # total tokens
3614
3615        ahs = ah.splitByReferenceNumber()
3616        self.assertEqual(len(ahs), 3)
3617        self.assertEqual(sorted(list(ahs.keys())), [166, 167, 168])
3618
3619        self.assertEqual(ahs[168].tokens[0].src, 'X:168')  # first is retained
3620        self.assertEqual(ahs[168].getTitle(), '168  The Castle Gate   (HJ)')
3621        self.assertEqual(len(ahs[168]), 89)  # tokens
3622
3623        self.assertEqual(ahs[166].tokens[0].src, 'X:166')  # first is retained
3624        # noinspection SpellCheckingInspection
3625        self.assertEqual(ahs[166].getTitle(), '166  Valentine Jigg   (Pe)')
3626        self.assertEqual(len(ahs[166]), 67)  # tokens
3627
3628        self.assertEqual(ahs[167].tokens[0].src, 'X:167')  # first is retained
3629        self.assertEqual(ahs[167].getTitle(), '167  The Dublin Jig     (HJ)')
3630        self.assertEqual(len(ahs[167]), 88)  # tokens
3631
3632    def testExtractReferenceNumber(self):
3633        from music21 import corpus
3634        fp = corpus.getWork('essenFolksong/test0')
3635
3636        af = ABCFile()
3637        af.open(fp)
3638        ah = af.read(5)  # returns a parsed handler
3639        af.close()
3640        self.assertEqual(len(ah), 74)
3641
3642        af = ABCFile()
3643        af.open(fp)
3644        ah = af.read(7)  # returns a parsed handler
3645        af.close()
3646        self.assertEqual(len(ah), 84)
3647
3648        fp = corpus.getWork('essenFolksong/han1')
3649        af = ABCFile()
3650        af.open(fp)
3651        ah = af.read(339)  # returns a parsed handler
3652        af.close()
3653        self.assertEqual(len(ah), 101)
3654
3655    def testSlurs(self):
3656        from music21.abcFormat import testFiles
3657        ah = ABCHandler()
3658        ah.process(testFiles.slurTest)
3659        self.assertEqual(len(ah), 70)  # number of tokens
3660
3661    def testTies(self):
3662        from music21.abcFormat import testFiles
3663        ah = ABCHandler()
3664        ah.process(testFiles.tieTest)
3665        self.assertEqual(len(ah), 73)  # number of tokens
3666
3667    def testCresc(self):
3668        from music21.abcFormat import testFiles
3669        ah = ABCHandler()
3670        ah.process(testFiles.crescTest)
3671        self.assertEqual(len(ah), 75)
3672        tokens = ah.tokens
3673        i = 0
3674        for t in tokens:
3675            if isinstance(t, ABCCrescStart):
3676                i += 1
3677        self.assertEqual(i, 1)
3678
3679    def testDim(self):
3680        from music21.abcFormat import testFiles
3681        ah = ABCHandler()
3682        ah.process(testFiles.dimTest)
3683        self.assertEqual(len(ah), 75)
3684        tokens = ah.tokens
3685        i = 0
3686        for t in tokens:
3687            if isinstance(t, ABCDimStart):
3688                i += 1
3689        self.assertEqual(i, 1)
3690
3691    def testStaccato(self):
3692        from music21.abcFormat import testFiles
3693        ah = ABCHandler()
3694        ah.process(testFiles.staccTest)
3695        self.assertEqual(len(ah), 80)
3696
3697    def testBow(self):
3698        from music21.abcFormat import testFiles
3699        ah = ABCHandler()
3700        ah.process(testFiles.bowTest)
3701        self.assertEqual(len(ah), 83)
3702        tokens = ah.tokens
3703        i = 0
3704        j = 0
3705        for t in tokens:
3706            if isinstance(t, ABCUpbow):
3707                i += 1
3708            elif isinstance(t, ABCDownbow):
3709                j += 1
3710        self.assertEqual(i, 2)
3711        self.assertEqual(j, 1)
3712
3713    def testAcc(self):
3714        from music21.abcFormat import testFiles
3715        from music21 import abcFormat
3716        ah = abcFormat.ABCHandler()
3717        ah.process(testFiles.accTest)
3718        # noinspection SpellCheckingInspection
3719        tokensCorrect = '''<music21.abcFormat.ABCMetadata 'X: 979'>
3720<music21.abcFormat.ABCMetadata 'T: Staccato test, plus accents and tenuto marks'>
3721<music21.abcFormat.ABCMetadata 'M: 2/4'>
3722<music21.abcFormat.ABCMetadata 'L: 1/16'>
3723<music21.abcFormat.ABCMetadata 'K: Edor'>
3724<music21.abcFormat.ABCNote 'B,2'>
3725<music21.abcFormat.ABCBar '|'>
3726<music21.abcFormat.ABCDimStart '!'>
3727<music21.abcFormat.ABCStaccato '.'>
3728<music21.abcFormat.ABCNote 'E'>
3729<music21.abcFormat.ABCNote '^D'>
3730<music21.abcFormat.ABCStaccato '.'>
3731<music21.abcFormat.ABCNote 'E'>
3732<music21.abcFormat.ABCTie '-'>
3733<music21.abcFormat.ABCNote 'E'>
3734<music21.abcFormat.ABCParenStop '!'>
3735<music21.abcFormat.ABCSlurStart '('>
3736<music21.abcFormat.ABCTuplet '(3'>
3737<music21.abcFormat.ABCStaccato '.'>
3738<music21.abcFormat.ABCNote 'G'>
3739<music21.abcFormat.ABCStaccato '.'>
3740<music21.abcFormat.ABCNote 'F'>
3741<music21.abcFormat.ABCStaccato '.'>
3742<music21.abcFormat.ABCAccent 'K'>
3743<music21.abcFormat.ABCNote 'G'>
3744<music21.abcFormat.ABCParenStop ')'>
3745<music21.abcFormat.ABCNote 'B'>
3746<music21.abcFormat.ABCNote 'A'>
3747<music21.abcFormat.ABCParenStop ')'>
3748<music21.abcFormat.ABCBar '|'>
3749<music21.abcFormat.ABCNote 'E'>
3750<music21.abcFormat.ABCNote '^D'>
3751<music21.abcFormat.ABCTenuto 'M'>
3752<music21.abcFormat.ABCNote 'E'>
3753<music21.abcFormat.ABCNote 'F'>
3754<music21.abcFormat.ABCTuplet '(3'>
3755<music21.abcFormat.ABCSlurStart '('>
3756<music21.abcFormat.ABCNote 'G'>
3757<music21.abcFormat.ABCTie '-'>
3758<music21.abcFormat.ABCNote 'G'>
3759<music21.abcFormat.ABCNote 'G'>
3760<music21.abcFormat.ABCParenStop ')'>
3761<music21.abcFormat.ABCParenStop ')'>
3762<music21.abcFormat.ABCNote 'B'>
3763<music21.abcFormat.ABCStraccent 'k'>
3764<music21.abcFormat.ABCTenuto 'M'>
3765<music21.abcFormat.ABCNote 'A'>
3766<music21.abcFormat.ABCBar '|'>
3767<music21.abcFormat.ABCSlurStart '('>
3768<music21.abcFormat.ABCNote 'E'>
3769<music21.abcFormat.ABCSlurStart '('>
3770<music21.abcFormat.ABCNote '^D'>
3771<music21.abcFormat.ABCNote 'E'>
3772<music21.abcFormat.ABCParenStop ')'>
3773<music21.abcFormat.ABCNote 'F'>
3774<music21.abcFormat.ABCParenStop ')'>
3775<music21.abcFormat.ABCTuplet '(3'>
3776<music21.abcFormat.ABCSlurStart '('>
3777<music21.abcFormat.ABCStraccent 'k'>
3778<music21.abcFormat.ABCNote 'G'>
3779<music21.abcFormat.ABCAccent 'K'>
3780<music21.abcFormat.ABCNote 'F'>
3781<music21.abcFormat.ABCParenStop ')'>
3782<music21.abcFormat.ABCNote 'G'>
3783<music21.abcFormat.ABCParenStop ')'>
3784<music21.abcFormat.ABCNote 'A'>
3785<music21.abcFormat.ABCTie '-'>
3786<music21.abcFormat.ABCNote 'A'>
3787<music21.abcFormat.ABCBar '|'>
3788<music21.abcFormat.ABCSlurStart '('>
3789<music21.abcFormat.ABCNote 'E'>
3790<music21.abcFormat.ABCNote '^D'>
3791<music21.abcFormat.ABCNote 'E'>
3792<music21.abcFormat.ABCNote 'F'>
3793<music21.abcFormat.ABCTuplet '(3'>
3794<music21.abcFormat.ABCSlurStart '('>
3795<music21.abcFormat.ABCNote 'G'>
3796<music21.abcFormat.ABCNote 'F'>
3797<music21.abcFormat.ABCNote 'G'>
3798<music21.abcFormat.ABCParenStop ')'>
3799<music21.abcFormat.ABCParenStop ')'>
3800<music21.abcFormat.ABCParenStop ')'>
3801<music21.abcFormat.ABCNote 'B'>
3802<music21.abcFormat.ABCNote 'A'>
3803<music21.abcFormat.ABCBar '|'>
3804<music21.abcFormat.ABCNote 'G6'>
3805'''.splitlines()
3806        tokensReceived = [str(x) for x in ah.tokens]
3807        self.assertEqual(tokensCorrect, tokensReceived)
3808
3809        self.assertEqual(len(ah), 86)
3810        tokens = ah.tokens
3811        i = 0
3812        j = 0
3813        k = 0
3814        for t in tokens:
3815            if isinstance(t, abcFormat.ABCAccent):
3816                i += 1
3817            elif isinstance(t, abcFormat.ABCStraccent):
3818                j += 1
3819            elif isinstance(t, abcFormat.ABCTenuto):
3820                k += 1
3821        self.assertEqual(i, 2)
3822        self.assertEqual(j, 2)
3823        self.assertEqual(k, 2)
3824
3825    def testGrace(self):
3826        from music21.abcFormat import testFiles
3827        ah = ABCHandler()
3828        ah.process(testFiles.graceTest)
3829        self.assertEqual(len(ah), 85)
3830
3831    def testGuineaPig(self):
3832        from music21.abcFormat import testFiles
3833        ah = ABCHandler()
3834        ah.process(testFiles.guineapigTest)
3835        self.assertEqual(len(ah), 105)
3836
3837
3838# ------------------------------------------------------------------------------
3839# define presented order in documentation
3840_DOC_ORDER = [ABCFile, ABCHandler, ABCHandlerBar]
3841
3842
3843if __name__ == '__main__':
3844    # sys.arg test options will be used in mainTest()
3845    import music21
3846    music21.mainTest(Test)
3847