# ------------------------------------------------------------------------------
# Name: abc/__init__.py
# Purpose: parses ABC Notation
#
# Authors: Christopher Ariza
# Michael Scott Asato Cuthbert
# Dylan J. Nagler
#
# Copyright: Copyright © 2010-22 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
'''
ABC is a music format that, while being able to encode all sorts of scores, is especially
strong at representing monophonic music, and folk music in particular.
Modules in the `music21.abcFormat` package deal with importing ABC into music21. Most people
working with ABC data won't need to use this package. To convert ABC from a file or URL
to a :class:`~music21.stream.Stream` use the :func:`~music21.converter.parse` function of
the `converter` module:
>>> #_DOCS_SHOW from music21 import *
>>> #_DOCS_SHOW abcScore = converter.parse('/users/ariza/myScore.abc')
For users who will be editing ABC extensively or need a way to have music21 output ABC
(which it doesn't do natively), we suggest using the open source EasyABC package:
https://easyabc.sourceforge.net . You can set it up as a MusicXML reader through:
>>> #_DOCS_SHOW us = environment.UserSettings()
>>> #_DOCS_SHOW us['musicxmlPath'] = '/Applications/EasyABC.app'
or wherever you have downloaded EasyABC to
(PC users might need: 'c:/program files (x86)/easyabc/easyabc.exe')
(Thanks to Norman Schmidt for the heads-up)
There is a two-step process in converting ABC files to music21 Streams. First this module
reads in the text-based .abc file and converts all the information into ABCToken objects. Then
the function :func:`music21.abcFormat.translate.abcToStreamScore` of
the :ref:`moduleAbcFormatTranslate` module
translates those Tokens into music21 objects.
Music21 implements the entire v1.6 ABC standard (January 1997) found at
https://abcnotation.com/standard/abc_v1.6.txt . The system aims to support the
v2.1 ABC standard (December 2011) found at
https://abcnotation.com/wiki/abc:standard:v2.1 but some parts (e.g. 4.10 Variant Endings) are not
implemented. No support is yet given for the draft 2.2 standard.
'''
from __future__ import annotations
__all__ = [
'ABCAccent',
'ABCBar',
'ABCBrokenRhythmMarker',
'ABCChord',
'ABCCrescStart',
'ABCDimStart',
'ABCDownbow',
'ABCFile',
'ABCFileException',
'ABCGraceStart',
'ABCGraceStop',
'ABCHandler',
'ABCHandlerBar',
'ABCHandlerException',
'ABCMetadata',
'ABCNote',
'ABCParenStop',
'ABCSlurStart',
'ABCStaccato',
'ABCStraccent',
'ABCTenuto',
'ABCTie',
'ABCToken',
'ABCTokenException',
'ABCTuplet',
'ABCUpbow',
'mergeLeadingMetaData',
'testFiles',
'translate',
]
from collections.abc import Sequence
import io
import re
import typing as t
import unittest
from music21 import common
from music21.common.types import OffsetQL
from music21 import defaults
from music21 import environment
from music21 import exceptions21
from music21 import prebase
from music21.abcFormat import translate
if t.TYPE_CHECKING:
import pathlib
from music21 import bar
from music21 import clef
from music21 import duration
from music21 import dynamics
from music21 import key
from music21 import meter
from music21 import note
from music21 import tempo
from music21 import spanner
_T = t.TypeVar('_T')
environLocal = environment.Environment('abcFormat')
# for implementation
# see http://abcnotation.com/abc2mtex/abc.txt
# store symbol and m21 naming/class eq
ABC_BARS = [
(':|1', 'light-heavy-repeat-end-first'),
(':|2', 'light-heavy-repeat-end-second'),
('|]', 'light-heavy'),
('||', 'light-light'),
('[|', 'heavy-light'),
('[1', 'regular-first'), # preferred format
('[2', 'regular-second'),
('|1', 'regular-first'), # gets converted
('|2', 'regular-second'),
(':|', 'light-heavy-repeat-end'),
('|:', 'heavy-light-repeat-start'),
('::', 'heavy-heavy-repeat-bidirectional'),
# for comparison, single chars must go last
('|', 'regular'),
(':', 'dotted'),
]
ABC_BARS_DICT = {d[0]: d[1] for d in ABC_BARS}
# store a mapping of ABC representation to pitch values
# key is (srcStr, carriedAccidental, str(keySignature)
# value is (pitchName (m21), accidentalDisplayStatus)
_pitchTranslationCache: dict[tuple[str, str, str], tuple[str, bool|None]] = {}
# ------------------------------------------------------------------------------
# note inclusion of w: for lyrics
reMetadataTag = re.compile('[A-Zw]:')
rePitchName = re.compile('[a-gA-Gz]')
reChordSymbol = re.compile('"[^"]*"') # non-greedy
reChord = re.compile('[.*?]') # non-greedy
reAbcVersion = re.compile(r'%abc-(\d+)\.(\d+)\.?(\d+)?')
reDirective = re.compile(r'^%%([a-z\-]+)\s+(\S+)(.*)')
# ------------------------------------------------------------------------------
class ABCTokenException(exceptions21.Music21Exception):
pass
class ABCHandlerException(exceptions21.Music21Exception):
pass
class ABCFileException(exceptions21.Music21Exception):
pass
#
# ------------------------------------------------------------------------------
[docs]
class ABCToken(prebase.ProtoM21Object, common.objects.EqualSlottedObjectMixin):
'''
ABC processing works with a multi-pass procedure. The first pass
breaks the data stream into a list of ABCToken objects. ABCToken
objects are specialized in subclasses.
The multi-pass procedure is conducted by an ABCHandler object.
The ABCHandler.tokenize() method breaks the data stream into
ABCToken objects. The :meth:`~music21.abcFormat.ABCHandler.tokenProcess` method first
calls the :meth:`~music21.abcFormat.ABCToken.preParse` method on each token,
then does contextual
adjustments to all tokens, then calls :meth:`~music21.abcFormat.ABCToken.parse` on all tokens.
The source ABC string itself is stored in self.src.
Because of how copying ABCTokens works, all tokens must have default parameters in their
initializers.
'''
__slots__ = ('src',)
def __init__(self, src: str = '') -> None:
self.src: str = src # store source character sequence
def _reprInternal(self) -> str:
return repr(self.src)
[docs]
def preParse(self) -> None:
'''
Dummy method that is called before contextual adjustments.
Designed to be subclassed or overridden.
'''
pass
[docs]
def parse(self) -> None:
'''
Dummy method that reads self.src and loads attributes.
It is called after contextual adjustments.
It is designed to be subclassed or overridden.
'''
pass
[docs]
class ABCBar(ABCToken):
'''
An ABCBar token represents a barline, possibly with repeat information.
Currently 4.10 Variant
'''
# given a logical unit, create an object
# may be a chord, notes, metadata, bars
__slots__ = ('barType', 'barStyle', 'repeatForm')
def __init__(self, src: str = '') -> None:
super().__init__(src)
self.barType = '' # repeat or barline
self.barStyle = '' # regular, heavy-light, etc
self.repeatForm = '' # end, start, bidirectional, first, second
[docs]
def parse(self) -> None:
'''
Assign the bar-type based on the source string.
>>> ab = abcFormat.ABCBar('|')
>>> ab.parse()
>>> ab
<music21.abcFormat.ABCBar '|'>
>>> ab.barType
'barline'
>>> ab.barStyle
'regular'
>>> ab = abcFormat.ABCBar('||')
>>> ab.parse()
>>> ab.barType
'barline'
>>> ab.barStyle
'light-light'
>>> ab = abcFormat.ABCBar('|:')
>>> ab.parse()
>>> ab.barType
'repeat'
>>> ab.barStyle
'heavy-light'
>>> ab.repeatForm
'start'
'''
src_strip: str = self.src.strip()
if src_strip not in ABC_BARS_DICT:
return
barTypeString = ABC_BARS_DICT[src_strip]
# this gets lists of elements like
# light-heavy-repeat-end
barTypeComponents = barTypeString.split('-')
# this is a list of attributes
if 'repeat' in barTypeComponents:
self.barType = 'repeat'
elif ('first' in barTypeComponents
or 'second' in barTypeComponents):
self.barType = 'barline'
# environLocal.printDebug(['got repeat 1/2:', self.src])
else:
self.barType = 'barline'
# case of regular, dotted
if len(barTypeComponents) == 1:
self.barStyle = barTypeComponents[0]
# case of light-heavy, light-light, etc
elif len(barTypeComponents) >= 2:
# must get out cases of the start-tags for repeat boundaries
# not yet handling
if 'first' in barTypeComponents:
self.barStyle = 'regular'
self.repeatForm = 'first' # not a repeat
elif 'second' in barTypeComponents:
self.barStyle = 'regular'
self.repeatForm = 'second' # not a repeat
else:
self.barStyle = barTypeComponents[0] + '-' + barTypeComponents[1]
# repeat form is either start/end for normal repeats
# get extra repeat information; start, end, first, second
if len(barTypeComponents) > 2:
self.repeatForm = barTypeComponents[3]
[docs]
def isRepeat(self) -> bool:
if self.barType == 'repeat':
return True
else:
return False
[docs]
def isRegular(self) -> bool:
'''
Return True if this is a regular, single, light bar line.
>>> ab = abcFormat.ABCBar('|')
>>> ab.parse()
>>> ab.isRegular()
True
'''
if self.barType != 'repeat' and self.barStyle == 'regular':
return True
else:
return False
[docs]
def isRepeatBracket(self) -> int|t.Literal[False]:
'''
Return a number if this defines a repeat bracket for an alternate ending
otherwise returns False.
>>> ab = abcFormat.ABCBar('[2')
>>> ab.parse()
>>> ab.isRepeat()
False
>>> ab.isRepeatBracket()
2
'''
if self.repeatForm == 'first':
return 1 # we need a number
elif self.repeatForm == 'second':
return 2
else:
return False
[docs]
def getBarObject(self) -> bar.Barline|None:
'''
Return a music21 bar object
>>> ab = abcFormat.ABCBar('|:')
>>> ab.parse()
>>> barObject = ab.getBarObject()
>>> barObject
<music21.bar.Repeat direction=start>
'''
from music21 import bar
m21bar: bar.Barline|None
if self.isRepeat():
if self.repeatForm in ('end', 'start'):
m21bar = bar.Repeat(direction=self.repeatForm)
# bidirectional repeat tokens should already have been replaced
# by end and start
else: # pragma: no cover
environLocal.printDebug(
[f'found an unsupported repeatForm in ABC: {self.repeatForm}']
)
m21bar = None
elif self.barStyle == 'regular':
m21bar = None # do not need an object for regular
elif self.repeatForm in ('first', 'second'):
# do nothing, as this is handled in translation
m21bar = None
else:
m21bar = bar.Barline(self.barStyle)
return m21bar
[docs]
class ABCTuplet(ABCToken):
'''
ABCTuplet tokens always precede the notes they describe.
In ABCHandler.tokenProcess(), rhythms are adjusted.
'''
__slots__ = ('noteCount', 'numberNotesActual', 'numberNotesNormal', 'tupletObj')
def __init__(self, src: str = '') -> None:
super().__init__(src)
# self.qlRemain = None # how many ql are left of this tuplets activity
# how many notes are affected by this; this assumes equal duration
self.noteCount: int = -1 # -1 = not defined
# actual is tuplet represented value; 3 in 3:2
self.numberNotesActual: int = -1 # -1 = not defined
# normal is underlying duration representation; 2 in 3:2
self.numberNotesNormal: int = 1
# store an m21 tuplet object
self.tupletObj: duration.Tuplet|None = None
[docs]
def updateRatio(
self,
timeSignatureObj: meter.TimeSignature|None = None
) -> None:
# noinspection PyShadowingNames
'''
Cannot be called until local meter context
is established.
>>> at = abcFormat.ABCTuplet('(3')
>>> at.updateRatio()
>>> at.numberNotesActual, at.numberNotesNormal
(3, 2)
Generally a 5:n tuplet is 5 in the place of 2.
>>> at = abcFormat.ABCTuplet('(5')
>>> at.updateRatio()
>>> at.numberNotesActual, at.numberNotesNormal
(5, 2)
Unless it's in a meter.TimeSignature compound (triple) context:
>>> at = abcFormat.ABCTuplet('(5')
>>> at.updateRatio(meter.TimeSignature('6/8'))
>>> at.numberNotesActual, at.numberNotesNormal
(5, 3)
Six is 6:2, not 6:4!
>>> at = abcFormat.ABCTuplet('(6')
>>> at.updateRatio()
>>> at.numberNotesActual, at.numberNotesNormal
(6, 2)
>>> at = abcFormat.ABCTuplet('(6:4')
>>> at.updateRatio()
>>> at.numberNotesActual, at.numberNotesNormal
(6, 4)
>>> at = abcFormat.ABCTuplet('(6::6')
>>> at.updateRatio()
>>> at.numberNotesActual, at.numberNotesNormal
(6, 2)
2 is 2 in 3...
>>> at = abcFormat.ABCTuplet('(2')
>>> at.updateRatio()
>>> at.numberNotesActual, at.numberNotesNormal
(2, 3)
Some other types:
>>> for n in 1, 2, 3, 4, 5, 6, 7, 8, 9:
... at = abcFormat.ABCTuplet(f'({n}')
... at.updateRatio()
... print(at.numberNotesActual, at.numberNotesNormal)
1 1
2 3
3 2
4 3
5 2
6 2
7 2
8 3
9 2
Tuplets > 9 raise an exception:
>>> at = abcFormat.ABCTuplet('(10')
>>> at.updateRatio()
Traceback (most recent call last):
music21.abcFormat.ABCTokenException: cannot handle tuplet of form: '(10'
'''
if timeSignatureObj is None:
normalSwitch = 2 # 4/4
elif timeSignatureObj.beatDivisionCount == 3: # if compound
normalSwitch = 3
else:
normalSwitch = 2
splitTuplet = self.src.strip().split(':')
tupletNumber = splitTuplet[0]
normalNotes: int|None = None
if len(splitTuplet) >= 2 and splitTuplet[1] != '':
normalNotes = int(splitTuplet[1])
a: int
n: int
if tupletNumber == '(1': # not sure if valid, but found
a, n = 1, 1
elif tupletNumber == '(2':
a, n = 2, 3 # actual, normal
elif tupletNumber == '(3':
a, n = 3, 2 # actual, normal
elif tupletNumber == '(4':
a, n = 4, 3 # actual, normal
elif tupletNumber == '(5':
a, n = 5, normalSwitch # actual, normal
elif tupletNumber == '(6':
a, n = 6, 2 # actual, normal
elif tupletNumber == '(7':
a, n = 7, normalSwitch # actual, normal
elif tupletNumber == '(8':
a, n = 8, 3 # actual, normal
elif tupletNumber == '(9':
a, n = 9, normalSwitch # actual, normal
else:
raise ABCTokenException(f'cannot handle tuplet of form: {tupletNumber!r}')
if normalNotes is None:
normalNotes = n
self.numberNotesActual = a
self.numberNotesNormal = normalNotes
[docs]
def updateNoteCount(self) -> None:
'''
Update the note count of notes that are
affected by this tuplet. Can be set by p:q:r style tuplets.
Also creates a tuplet object.
>>> at = abcFormat.ABCTuplet('(6')
>>> at.updateRatio()
>>> at.updateNoteCount()
>>> at.noteCount
6
>>> at.tupletObj
<music21.duration.Tuplet 6/2>
>>> at = abcFormat.ABCTuplet('(6:4:12')
>>> at.updateRatio()
>>> at.updateNoteCount()
>>> at.noteCount
12
>>> at.tupletObj
<music21.duration.Tuplet 6/4>
>>> at = abcFormat.ABCTuplet('(6::18')
>>> at.updateRatio()
>>> at.updateNoteCount()
>>> at.noteCount
18
'''
if self.numberNotesActual == -1:
raise ABCTokenException('must set numberNotesActual with updateRatio()')
# need to create the tuplet object
from music21 import duration
self.tupletObj = duration.Tuplet(
numberNotesActual=self.numberNotesActual,
numberNotesNormal=self.numberNotesNormal)
# copy value; this will be dynamically counted down
splitTuplet = self.src.strip().split(':')
if len(splitTuplet) >= 3 and splitTuplet[2] != '':
self.noteCount = int(splitTuplet[2])
else:
self.noteCount = self.numberNotesActual
# self.qlRemain = self._tupletObj.totalTupletLength()
[docs]
class ABCTie(ABCToken):
'''
Handles instances of ties '-' between notes in an ABC score.
Ties are treated as an attribute of the note before the '-';
the note after is marked as the end of the tie.
'''
__slots__ = ('noteObj',)
def __init__(self, src: str = '') -> None:
super().__init__(src)
self.noteObj: note.Note|None = None
[docs]
class ABCSlurStart(ABCToken):
'''
ABCSlurStart tokens always precede the notes in a slur.
For nested slurs, each open parenthesis gets its own token.
'''
__slots__ = ('slurObj',)
def __init__(self, src: str = '') -> None:
super().__init__(src)
self.slurObj: spanner.Slur|None = None
[docs]
def fillSlur(self) -> None:
'''
Creates a spanner object for each open paren associated with a slur;
these slurs are filled with notes until end parens are read.
'''
from music21 import spanner
self.slurObj = spanner.Slur()
[docs]
class ABCParenStop(ABCToken):
'''
A general parenthesis stop;
comes at the end of a tuplet, slur, or dynamic marking.
'''
__slots__ = ()
[docs]
class ABCCrescStart(ABCToken):
'''
ABCCrescStart tokens always precede the notes in a crescendo.
These tokens coincide with the string "!crescendo(";
the closing string "!crescendo)" counts as an ABCParenStop.
'''
__slots__ = ('crescObj',)
def __init__(self, src: str = '') -> None:
super().__init__(src)
self.crescObj: dynamics.Crescendo|None = None
[docs]
def fillCresc(self) -> None:
from music21 import dynamics
self.crescObj = dynamics.Crescendo()
[docs]
class ABCDimStart(ABCToken):
'''
ABCDimStart tokens always precede the notes in a diminuendo.
They function identically to ABCCrescStart tokens.
'''
__slots__ = ('dimObj',)
def __init__(self, src: str = '') -> None:
super().__init__(src)
self.dimObj: dynamics.Diminuendo|None = None
[docs]
def fillDim(self) -> None:
from music21 import dynamics
self.dimObj = dynamics.Diminuendo()
[docs]
class ABCStaccato(ABCToken):
'''
ABCStaccato tokens "." precede a note or chord;
they are a property of that note/chord.
'''
__slots__ = ()
[docs]
class ABCUpbow(ABCToken):
'''
ABCUpbow tokens "u" precede a note or chord;
they are a property of that note/chord.
'''
__slots__ = ()
[docs]
class ABCDownbow(ABCToken):
'''
ABCDownbow tokens "v" precede a note or chord;
they are a property of that note/chord.
'''
__slots__ = ()
[docs]
class ABCAccent(ABCToken):
'''
ABCAccent tokens "K" precede a note or chord;
they are a property of that note/chord.
These appear as ">" in the output.
'''
__slots__ = ()
[docs]
class ABCStraccent(ABCToken):
'''
ABCStraccent tokens "k" precede a note or chord;
they are a property of that note/chord.
These appear as "^" in the output.
'''
__slots__ = ()
[docs]
class ABCTenuto(ABCToken):
'''
ABCTenuto tokens "M" precede a note or chord;
they are a property of that note/chord.
'''
__slots__ = ()
[docs]
class ABCGraceStart(ABCToken):
'''
Grace note start
'''
__slots__ = ()
[docs]
class ABCGraceStop(ABCToken):
'''
Grace note end
'''
__slots__ = ()
[docs]
class ABCBrokenRhythmMarker(ABCToken):
'''
Marks that rhythm is broken with '>>>'
'''
__slots__ = ('data',)
def __init__(self, src: str = '') -> None:
super().__init__(src)
self.data: str = ''
[docs]
def preParse(self) -> None:
'''
Called before context adjustments: need to have access to data
>>> brokenRhythm = abcFormat.ABCBrokenRhythmMarker('>>>')
>>> brokenRhythm.preParse()
>>> brokenRhythm.data
'>>>'
'''
self.data = self.src.strip()
[docs]
class ABCNote(ABCToken):
'''
A model of an ABCNote.
General usage requires multi-pass processing. After being tokenized,
each ABCNote needs a number of attributes updates. Attributes to
be updated after tokenizing, and based on the linear sequence of
tokens: `inBar`, `inBeam` (not used), `inGrace`,
`activeDefaultQuarterLength`, `brokenRhythmMarker`, and
`activeKeySignature`.
The `chordSymbols` list stores one or more chord symbols (ABC calls
these guitar chords) associated with this note. This attribute is
updated when parse() is called.
'''
__slots__ = ('carriedAccidental', 'chordSymbols', 'inBar', 'inBeam',
'inGrace', 'activeDefaultQuarterLength', 'brokenRhythmMarker',
'activeKeySignature', 'activeTuplet', 'applicableSpanners', 'tie',
'articulations', 'accidentalDisplayStatus', 'isRest',
'pitchName', 'quarterLength',)
def __init__(self, src: str = '', carriedAccidental: str = '') -> None:
super().__init__(src)
# store the ABC accidental string propagated in the measure that
# must be applied to this note. Note must not be set if the
# note already has an explicit accidental attached. (The explicit
# accidental is now the one that will be carried forward.)
self.carriedAccidental: str = carriedAccidental
# store chord string if connected to this note
self.chordSymbols: list[str] = []
# context attributes
self.inBar: bool|None = None
self.inBeam: bool|None = None
self.inGrace: bool|None = None
# provide default duration from handler; may change during piece
self.activeDefaultQuarterLength: float|None = None
# store if a broken symbol applies; a pair of symbols, position (left, right)
self.brokenRhythmMarker: tuple[str, str]|None = None
# store key signature for pitch processing; this is an M21Object
self.activeKeySignature: key.KeySignature|None = None
# store a tuplet if active
self.activeTuplet: duration.Tuplet|None = None
# store a spanner if active
self.applicableSpanners: list[spanner.Spanner] = []
# store a tie type if active
self.tie: str|None = None
# store articulations if active
self.articulations: list[str] = []
# set to True if a modification of key signature
# set to False if an altered tone part of a Key
self.accidentalDisplayStatus: bool|None = None
# determined during parse() based on if pitch chars are present
self.isRest: bool = False
# Pitch and duration attributes for m21 conversion
# they are set via parse() based on other contextual information.
self.pitchName: str|None = None # if None, a rest or chord
self.quarterLength: OffsetQL = 0.0
@staticmethod
def _splitChordSymbols(strSrc: str) -> tuple[list[str], str]:
'''
Splits chord symbols from other string characteristics.
Returns a 2-tuple of a list of chord symbols, and clean remaining chars.
Staticmethod:
>>> an = abcFormat.ABCNote()
>>> an._splitChordSymbols('"C"e2')
(['"C"'], 'e2')
>>> an._splitChordSymbols('b2')
([], 'b2')
>>> abcFormat.ABCNote._splitChordSymbols('"D7""D"d2')
(['"D7"', '"D"'], 'd2')
'''
if '"' in strSrc:
chordSymbols = reChordSymbol.findall(strSrc)
# might remove quotes from chord symbols here
# index of end of last match
i = list(reChordSymbol.finditer(strSrc))[-1].end()
return chordSymbols, strSrc[i:]
else:
return [], strSrc
[docs]
def getPitchName(
self,
strSrc: str,
forceKeySignature: key.KeySignature|None = None
) -> tuple[str|None, bool|None]:
'''
Given a note or rest string without a chord symbol,
return a music21 pitch string or None (if a rest),
and the accidental display status. This value is paired
with an accidental display status. Pitch alterations, and
accidental display status, are adjusted if a key is
declared in the Note.
>>> an = abcFormat.ABCNote()
>>> an.getPitchName('e2')
('E5', None)
>>> an.getPitchName('C')
('C4', None)
>>> an.getPitchName('B,,')
('B2', None)
>>> an.getPitchName('C,')
('C3', None)
>>> an.getPitchName('c')
('C5', None)
>>> an.getPitchName("c'")
('C6', None)
>>> an.getPitchName("c''")
('C7', None)
>>> an.getPitchName("^g")
('G#5', True)
>>> an.getPitchName("_g''")
('G-7', True)
>>> an.getPitchName('=c')
('Cn5', True)
If pitch is a rest (z) then the Pitch name is None:
>>> an.getPitchName('z4')
(None, None)
Grace note:
>>> an.getPitchName('{c}')
('C5', None)
Given an active KeySignature object, the pitch name might
change:
>>> an.activeKeySignature = key.KeySignature(3)
>>> an.getPitchName('c')
('C#5', False)
Illegal pitch names raise an ABCHandlerException:
>>> an.getPitchName('x')
Traceback (most recent call last):
music21.abcFormat.ABCHandlerException: cannot find any pitch information in: 'x'
'''
environLocal.printDebug(['getPitchName:', strSrc])
# skip some articulations parsed with the pitch
# some characters are errors in parsing or encoding not yet handled
if len(strSrc) > 1 and strSrc[0] in 'uT':
strSrc = strSrc[1:]
strSrc = strSrc.replace('T', '')
try:
name = rePitchName.findall(strSrc)[0]
except IndexError: # no matches # pragma: no cover
raise ABCHandlerException(f'cannot find any pitch information in: {strSrc!r}')
if name == 'z':
return (None, None) # designates a rest
activeKeySignature: key.KeySignature|None
if forceKeySignature is not None:
activeKeySignature = forceKeySignature
else: # may be None
activeKeySignature = self.activeKeySignature
try: # returns pStr, accidentalDisplayStatus
return _pitchTranslationCache[(strSrc,
self.carriedAccidental,
str(activeKeySignature))]
except KeyError:
pass
if name.islower():
octave = 5
else:
octave = 4
# look in source string for register modification
octave -= strSrc.count(',')
octave += strSrc.count("'")
# get an accidental string
accString = ''
for dummy in range(strSrc.count('_')):
accString += '-' # m21 symbols
for dummy in range(strSrc.count('^')):
accString += '#' # m21 symbols
for dummy in range(strSrc.count('=')):
accString += 'n' # m21 symbols
carriedAccString = ''
if self.carriedAccidental:
# No overriding accidental attached to this note
# force carrying through the measure.
for dummy in range(self.carriedAccidental.count('_')):
carriedAccString += '-' # m21 symbols
for dummy in range(self.carriedAccidental.count('^')):
carriedAccString += '#' # m21 symbols
for dummy in range(self.carriedAccidental.count('=')):
carriedAccString += 'n' # m21 symbols
if carriedAccString and accString:
raise ABCHandlerException('Carried accidentals not rendered moot.')
# if there is an explicit accidental, regardless of key, it should
# be shown: this will work for naturals well
accidentalDisplayStatus: bool|None
if carriedAccString:
# An accidental carrying through the measure is supposed to be applied.
# This will be set iff no explicit accidental is attached to the note.
accidentalDisplayStatus = None
elif accString != '':
accidentalDisplayStatus = True
# if we do not have a key signature, and have accidentals, set to None
elif activeKeySignature is None:
accidentalDisplayStatus = None
# pitches are key dependent: accidentals are not given
# if we have a key and find a name, that does not have an "n", must be
# altered
else:
alteredPitches = activeKeySignature.alteredPitches
# just the steps, no accidentals
alteredPitchSteps = [p.step.lower() for p in alteredPitches]
# includes #, -
alteredPitchNames = [p.name.lower() for p in alteredPitches]
# environLocal.printDebug(['alteredPitches', alteredPitches])
if name.lower() in alteredPitchSteps:
# get the corresponding index in the name
name = alteredPitchNames[alteredPitchSteps.index(name.lower())]
# set to false, as do not need to show w/ key sig
accidentalDisplayStatus = False
# making upper here, but this is not relevant
if carriedAccString:
pStr = f'{name.upper()}{carriedAccString}{octave}'
else:
pStr = f'{name.upper()}{accString}{octave}'
# store in global cache for faster speed
_cacheKey = (
strSrc,
self.carriedAccidental,
str(activeKeySignature)
)
_pitchTranslationCache[_cacheKey] = (pStr, accidentalDisplayStatus)
return (pStr, accidentalDisplayStatus)
[docs]
def getQuarterLength(self,
strSrc: str,
forceDefaultQuarterLength: float|None = None) -> OffsetQL:
'''
Called with parse(), after context processing, to calculate duration.
>>> an = abcFormat.ABCNote()
>>> an.activeDefaultQuarterLength = 0.5
>>> an.getQuarterLength('e2')
1.0
>>> an.getQuarterLength('G')
0.5
>>> an.getQuarterLength('=c/2')
0.25
>>> an.getQuarterLength('A3/2')
0.75
>>> an.getQuarterLength('A/')
0.25
>>> an.getQuarterLength('A//')
0.125
>>> an.getQuarterLength('A///')
0.0625
>>> an = abcFormat.ABCNote()
>>> an.activeDefaultQuarterLength = 0.5
>>> an.brokenRhythmMarker = ('>', 'left')
>>> an.getQuarterLength('A')
0.75
>>> an.brokenRhythmMarker = ('>', 'right')
>>> an.getQuarterLength('A')
0.25
>>> an.brokenRhythmMarker = ('<<', 'left')
>>> an.getQuarterLength('A')
0.125
>>> an.brokenRhythmMarker = ('<<', 'right')
>>> an.getQuarterLength('A')
0.875
>>> an.brokenRhythmMarker = ('<<<', 'left')
>>> an.getQuarterLength('A')
0.0625
>>> an.brokenRhythmMarker = ('<<<', 'right')
>>> an.getQuarterLength('A')
0.9375
>>> an.getQuarterLength('A', forceDefaultQuarterLength=1.0)
1.875
'''
activeDefaultQuarterLength: float|None
if forceDefaultQuarterLength is not None:
activeDefaultQuarterLength = forceDefaultQuarterLength
else: # may be None
activeDefaultQuarterLength = self.activeDefaultQuarterLength
if activeDefaultQuarterLength is None:
raise ABCTokenException(
'cannot calculate quarter length without a default quarter length')
numStrList: list[str] = []
for c in strSrc:
if c.isdigit() or c == '/':
numStrList.append(c)
numStr = ''.join(numStrList)
numStr = numStr.strip()
# environLocal.printDebug(['numStr', numStr])
# get default
if numStr == '':
ql: OffsetQL = activeDefaultQuarterLength
# if only, shorthand for /2
elif numStr == '/':
ql = activeDefaultQuarterLength * 0.5
elif numStr == '//':
ql = activeDefaultQuarterLength * 0.25
elif numStr == '///':
ql = activeDefaultQuarterLength * 0.125
# if a half fraction
elif numStr.startswith('/'):
ql = common.opFrac(activeDefaultQuarterLength / int(numStr.split('/')[1]))
# uncommon usage: 3/ short for 3/2
elif numStr.endswith('/'):
n = int(numStr.split('/', maxsplit=1)[0].strip())
d = 2
ql = activeDefaultQuarterLength * n / d
# if we have two, this is usually an error
elif numStr.count('/') == 2: # pragma: no cover
environLocal.printDebug(['incorrectly encoded / unparsable duration:', numStr])
ql = 1 # provide a default
# assume we have a complete fraction
elif '/' in numStr:
nStr, dStr = numStr.split('/')
if t.TYPE_CHECKING:
assert nStr is not None
assert dStr is not None
n = int(nStr.strip())
d = int(dStr.strip())
ql = common.opFrac(activeDefaultQuarterLength * n / d)
# not a fraction; a multiplier
else:
ql = activeDefaultQuarterLength * int(numStr)
if self.brokenRhythmMarker is not None:
symbol, direction = self.brokenRhythmMarker
if symbol == '>':
modPair = (1.5, 0.5)
elif symbol == '<':
modPair = (0.5, 1.5)
elif symbol == '>>':
modPair = (1.75, 0.25)
elif symbol == '<<':
modPair = (0.25, 1.75)
elif symbol == '>>>':
modPair = (1.875, 0.125)
elif symbol == '<<<':
modPair = (0.125, 1.875)
else: # pragma: no cover
modPair = (1, 1)
# apply based on direction
if direction == 'left':
ql *= modPair[0]
elif direction == 'right':
ql *= modPair[1]
return ql
[docs]
def parse(
self,
forceDefaultQuarterLength: float|None = None,
forceKeySignature: key.KeySignature|None = None
) -> None:
# environLocal.printDebug(['parse', self.src])
self.chordSymbols, nonChordSymStr = self._splitChordSymbols(self.src)
# get pitch name from remaining string
# rests will have a pitch name of None
pn: str|None
accDisp: bool|None
try:
pn, accDisp = self.getPitchName(nonChordSymStr,
forceKeySignature=forceKeySignature)
except ABCHandlerException:
environLocal.warn(['Could not get pitch information from note: ',
f'{nonChordSymStr}, assuming C'])
pn = 'C'
accDisp = False
self.pitchName, self.accidentalDisplayStatus = pn, accDisp
if self.pitchName is None:
self.isRest = True
else:
self.isRest = False
self.quarterLength = self.getQuarterLength(
nonChordSymStr,
forceDefaultQuarterLength=forceDefaultQuarterLength)
# environLocal.printDebug(['ABCNote:', 'pitch name:', self.pitchName,
# 'ql:', self.quarterLength])
[docs]
class ABCChord(ABCNote):
'''
A representation of an ABC Chord, which contains within its delimiters individual notes.
A subclass of ABCNote.
'''
def __init__(self, src: str = '') -> None:
super().__init__(src)
# store a list of component objects
self.subTokens: list[ABCToken] = []
[docs]
def parse(
self,
forceDefaultQuarterLength: float|None = None,
forceKeySignature: key.KeySignature|None = None
) -> None:
'''
Handles the following types of chords:
* Chord without length modifier: [ceg]
* Chords with outer length modifier: [ceg]2, [ceg]/2
* Chords with inner length modifier: [c2e2g2], [c2eg]
* Chords with inner and outer length modifier: [c2e2g2]/2, [c/2e/2g/2]2
'''
self.chordSymbols, nonChordSymStr = self._splitChordSymbols(self.src)
# position of the closing bracket
try:
pos = nonChordSymStr.index(']')
except ValueError:
raise ABCHandlerException(
f'Bad chord indicator: {self.src}: no closing bracket found.'
)
# Length modifier string behind the chord brackets
outerLengthModifierStr = nonChordSymStr[pos + 1:]
# String in the chord brackets
tokenStr = nonChordSymStr[1:pos]
# environLocal.printDebug(['ABCChord:', nonChordSymStr, 'tokenStr', tokenStr, '
# outerLengthModifierStr', outerLengthModifierStr])
# Get the outer chord length modifier if present
outer_lengthModifier = self.getQuarterLength(outerLengthModifierStr,
forceDefaultQuarterLength=1.0)
activeKeySignature: key.KeySignature|None
if forceKeySignature is not None:
activeKeySignature = forceKeySignature
else: # may be None
activeKeySignature = self.activeKeySignature
# create a handler for processing internal chord notes
ah = ABCHandler()
# only tokenizing; not calling process() as these objects
# have no metadata
# may need to supply key?
ah.tokenize(tokenStr)
inner_quarterLength: OffsetQL = 0.0
# tokens contained here are each ABCNote instances
for token in ah.tokens:
# environLocal.printDebug(['ABCChord: subTokens', t])
# parse any tokens individually, supply local data as necessary
if isinstance(token, ABCNote):
token.parse(
forceDefaultQuarterLength=self.activeDefaultQuarterLength,
forceKeySignature=activeKeySignature)
if token.isRest:
continue
# get the quarter length from the sub-tokens
# All the notes within a chord should normally have the same length,
# but if not, the chord duration is that of the first note.
if not inner_quarterLength:
inner_quarterLength = token.quarterLength
self.subTokens.append(token)
# When both inside and outside the chord length modifiers are used,
# they should be multiplied. Example: [C2E2G2]3 has the same meaning as [CEG]6.
self.quarterLength = outer_lengthModifier * inner_quarterLength
# ------------------------------------------------------------------------------
[docs]
class ABCHandler:
'''
An ABCHandler is able to divide elements of a character stream into objects,
store them in a list, and pass global information to components.
Optionally, specify the (major, minor, patch) version of ABC to process--
e.g., (1.2.0). If not set, default ABC 1.3 parsing is performed.
If lineBreaksDefinePhrases is True then new lines within music elements
define new phrases. This is useful for parsing extra information from
the Essen Folksong repertory.
* New in v6.3: lineBreaksDefinePhrases -- does not yet do anything.
'''
def __init__(self,
abcVersion: tuple[int, ...] = defaults.abcVersionDefault,
lineBreaksDefinePhrases: bool = False) -> None:
# tokens are ABC objects in a linear stream
self.abcVersion: tuple[int, ...] = abcVersion
self.abcDirectives: dict[str, str] = {}
self.tokens: list[ABCToken] = []
self.activeParens: list[str] = [] # e.g. ['Crescendo', 'Slur']
self.activeSpanners: list[spanner.Spanner] = []
self.lineBreaksDefinePhrases: bool = lineBreaksDefinePhrases
self.pos = -1
self.skipAhead = 0
self.strSrc = ''
self.srcLen: int = len(self.strSrc) # just documenting this.
self.currentCollectStr = ''
@staticmethod
def _getLinearContext(source: Sequence[_T],
i: int) -> tuple[_T|None, _T, _T|None, _T|None]:
'''
Find the local context of a string or iterable of objects
beginning at a particular index.
Returns a tuple of charPrev, charThis, charNext, charNextNext.
Staticmethod
>>> ah = abcFormat.ABCHandler()
>>> ah._getLinearContext('12345', 0)
(None, '1', '2', '3')
>>> ah._getLinearContext('12345', 1)
('1', '2', '3', '4')
>>> abcFormat.ABCHandler._getLinearContext('12345', 3)
('3', '4', '5', None)
>>> abcFormat.ABCHandler._getLinearContext('12345', 4)
('4', '5', None, None)
>>> abcFormat.ABCHandler._getLinearContext([32, None, 8, 11, 53], 4)
(11, 53, None, None)
>>> ah._getLinearContext([32, None, 8, 11, 53], 2)
(None, 8, 11, 53)
>>> ah._getLinearContext([32, None, 8, 11, 53], 0)
(None, 32, None, 8)
'''
# Note: this is performance critical method
lastIndex = len(source) - 1
if i > lastIndex:
raise ABCHandlerException(f'bad index value {i}, max is {lastIndex}')
# find local area of iterable
cPrev = None
if i > 0:
cPrev = source[i - 1]
# set current characters or items
c = source[i]
cNext = None
if i < len(source) - 1:
cNext = source[i + 1]
# get 2 entries forward
cNextNext = None
if i < len(source) - 2:
cNextNext = source[i + 2]
return cPrev, c, cNext, cNextNext
# return cPrevNotSpace, cPrev, c, cNext, cNextNotSpace, cNextNext
@staticmethod
def _getNextLineBreak(strSrc: str, i: int) -> int:
r'''
Return index of next line break after character i.
Staticmethod
>>> ah = abcFormat.ABCHandler()
>>> inputString = 'de we\n wer bfg\n'
>>> ah._getNextLineBreak(inputString, 0)
6
>>> inputString[0:6]
'de we'
from last line break
>>> abcFormat.ABCHandler._getNextLineBreak(inputString, 6)
15
>>> inputString[ah._getNextLineBreak(inputString, 0):]
'\n wer bfg\n'
'''
lastIndex = len(strSrc) - 1
for j in range(i + 1, lastIndex + 1):
if strSrc[j] == '\n':
return j
return lastIndex + 1
[docs]
@staticmethod
def barlineTokenFilter(token: str) -> list[ABCBar]:
'''
Some single barline tokens are better replaced
with two tokens. This method, given a token,
returns a list of tokens. If there is no change
necessary, the provided token will be returned within
the list.
A staticmethod. Call on the class itself.
>>> abcFormat.ABCHandler.barlineTokenFilter('::')
[<music21.abcFormat.ABCBar ':|'>, <music21.abcFormat.ABCBar '|:'>]
>>> abcFormat.ABCHandler.barlineTokenFilter('|2')
[<music21.abcFormat.ABCBar '|'>, <music21.abcFormat.ABCBar '[2'>]
>>> abcFormat.ABCHandler.barlineTokenFilter(':|1')
[<music21.abcFormat.ABCBar ':|'>, <music21.abcFormat.ABCBar '[1'>]
If nothing matches, the original token is returned as an ABCBar object:
>>> abcFormat.ABCHandler.barlineTokenFilter('hi')
[<music21.abcFormat.ABCBar 'hi'>]
'''
barTokens: list[ABCBar] = []
if token == '::':
# create a start and an end
barTokens.append(ABCBar(':|'))
barTokens.append(ABCBar('|:'))
elif token == '|1':
# create a start and an end
barTokens.append(ABCBar('|'))
barTokens.append(ABCBar('[1'))
elif token == '|2':
# create a start and an end
barTokens.append(ABCBar('|'))
barTokens.append(ABCBar('[2'))
elif token == ':|1':
# create a start and an end
barTokens.append(ABCBar(':|'))
barTokens.append(ABCBar('[1'))
elif token == ':|2':
# create a start and an end
barTokens.append(ABCBar(':|'))
barTokens.append(ABCBar('[2'))
else: # append unaltered
barTokens.append(ABCBar(token))
return barTokens
# --------------------------------------------------------------------------
# token processing
def _accidentalPropagation(self) -> str:
'''
Determine how accidentals should 'carry through the measure.'
>>> ah = abcFormat.ABCHandler(abcVersion=(1, 3, 0))
>>> ah._accidentalPropagation()
'not'
>>> ah = abcFormat.ABCHandler(abcVersion=(2, 0, 0))
>>> ah._accidentalPropagation()
'pitch'
'''
minVersion = (2, 0, 0)
if self.abcVersion < minVersion:
return 'not'
if 'propagate-accidentals' in self.abcDirectives:
return self.abcDirectives['propagate-accidentals']
return 'pitch' # Default per abc 2.1 standard
[docs]
@staticmethod
def returnAbcVersionFromMatch(verMats: re.Match) -> tuple[int, int, int]:
r'''
Given a match from a regular expression return the parsed ABC version
>>> import re
>>> match = re.match(r'(\d+).(\d+).(\d+)', '2.3.4')
>>> ah = abcFormat.ABCHandler()
>>> ah.returnAbcVersionFromMatch(match)
(2, 3, 4)
>>> match = re.match(r'(\d+).(\d+).?(\d?)', '1.7')
>>> ah.returnAbcVersionFromMatch(match)
(1, 7, 0)
'''
abcMajor = int(verMats.group(1))
abcMinor = int(verMats.group(2))
if verMats.group(3):
abcPatch = int(verMats.group(3))
else:
abcPatch = 0
return (abcMajor, abcMinor, abcPatch)
# environLocal.printDebug(['got comment:', repr(self.strSrc[i:j + 1])])
[docs]
def tokenize(self, strSrc: str) -> None:
'''
Walk the abc string, creating ABC objects along the way.
This may be called separately from process(), in the case
that pre-/post-parse processing is not needed.
>>> abch = abcFormat.ABCHandler()
>>> abch.tokens
[]
>>> abch.tokenize('X: 1')
>>> abch.tokens
[<music21.abcFormat.ABCMetadata 'X: 1'>]
>>> abch = abcFormat.ABCHandler()
>>> abch.tokenize('(6f')
>>> abch.tokens
[<music21.abcFormat.ABCTuplet '(6'>, <music21.abcFormat.ABCNote 'f'>]
>>> abch = abcFormat.ABCHandler()
>>> abch.tokenize('(6:4f')
>>> abch.tokens
[<music21.abcFormat.ABCTuplet '(6:4'>, <music21.abcFormat.ABCNote 'f'>]
>>> abch = abcFormat.ABCHandler()
>>> abch.tokenize('(6:4:2f')
>>> abch.tokens
[<music21.abcFormat.ABCTuplet '(6:4:2'>, <music21.abcFormat.ABCNote 'f'>]
>>> abch = abcFormat.ABCHandler()
>>> abch.tokenize('(6::2f')
>>> abch.tokens
[<music21.abcFormat.ABCTuplet '(6::2'>, <music21.abcFormat.ABCNote 'f'>]
'''
self.srcLen = len(strSrc)
self.strSrc = strSrc
self.pos = -1
self.currentCollectStr = ''
self.skipAhead = 0
# noinspection SpellCheckingInspection
accidentalsAndDecorations = '.~^=_HLMOPSTuv'
accidentals = '^=_'
activeChordSymbol = '' # accumulate, then prepend
accidentalized: dict[str, str] = {}
accidental: str = ''
abcPitch: str = '' # ABC substring defining any pitch within the current token
while self.pos < self.srcLen - 1:
self.pos += 1
self.pos += self.skipAhead
self.skipAhead = 0
if self.pos > self.srcLen - 1:
break
q = self._getLinearContext(self.strSrc, self.pos)
unused_cPrev, c, cNext, cNextNext = q
# cPrevNotSpace, cPrev, c, cNext, cNextNotSpace, cNextNext = q
# comment lines, also encoding defs
if c == '%':
self.processComment()
continue
if self.startsMetadata(c, cNext, cNextNext):
# collect until end of line; add one to get line break
j = self._getNextLineBreak(self.strSrc, self.pos)
self.skipAhead = j - (self.pos + 1)
self.currentCollectStr = self.strSrc[self.pos:j].strip()
# environLocal.printDebug(['got metadata:', repr(self.currentCollectStr)])
self.tokens.append(ABCMetadata(self.currentCollectStr))
continue
# get bars: if not a space and not alphanumeric
if not c.isspace() and not c.isalnum() and c not in ('~', '('):
matchBars = False
for barIndex in range(len(ABC_BARS)):
# first of bars tuple is symbol to match
# three possible sizes of bar indications: 3, 2, 1
barTokenArchetype = ABC_BARS[barIndex][0]
if len(barTokenArchetype) == 3:
if (cNext is not None
and cNextNext is not None
and c + cNext + cNextNext == barTokenArchetype):
self.skipAhead = 2
matchBars = True
break
elif cNext is not None and (len(barTokenArchetype) == 2):
if c + cNext == barTokenArchetype:
self.skipAhead = 1
matchBars = True
break
elif len(barTokenArchetype) == 1:
if c == barTokenArchetype:
self.skipAhead = 0
matchBars = True
break
if matchBars is True:
accidentalized = {}
accidental = ''
j = self.pos + self.skipAhead + 1
self.currentCollectStr = self.strSrc[self.pos:j]
# filter and replace with 2 tokens if necessary
for tokenSub in self.barlineTokenFilter(self.currentCollectStr):
self.tokens.append(tokenSub)
# environLocal.printDebug(['got bars:', repr(self.currentCollectStr)])
# if self.currentCollectStr == '::':
# # create a start and an end
# self.tokens.append(ABCBar(':|'))
# self.tokens.append(ABCBar('|:'))
# else:
# self.tokens.append(ABCBar(self.currentCollectStr))
continue
# get tuplet indicators: (2, (3, (p:q:r or (3::
if c == '(' and cNext is not None and cNext.isdigit():
self.skipAhead = 1
j = self.pos + self.skipAhead + 1 # always two characters
unused1, possibleColon, qChar, unused2 = self._getLinearContext(self.strSrc, j)
if possibleColon == ':':
j += 1
self.skipAhead += 1
if qChar is not None and qChar.isdigit():
j += 1
self.skipAhead += 1
unused1, possibleColon, rChar, unused2 = self._getLinearContext(self.strSrc, j)
if possibleColon == ':':
j += 1 # include the r characters
self.skipAhead += 1
if rChar is not None and rChar.isdigit():
j += 1
self.skipAhead += 1
self.currentCollectStr = self.strSrc[self.pos:j]
# environLocal.printDebug(['got tuplet start:', repr(self.currentCollectStr)])
self.tokens.append(ABCTuplet(self.currentCollectStr))
continue
# get broken rhythm modifiers: < or >, >>, up to <<<
if c in '<>':
j = self.pos + 1
while j < self.srcLen - 1 and self.strSrc[j] in '<>':
j += 1
self.currentCollectStr = self.strSrc[self.pos:j]
# environLocal.printDebug(
# ['got bidirectional rhythm mod:', repr(self.currentCollectStr)])
self.tokens.append(ABCBrokenRhythmMarker(self.currentCollectStr))
self.skipAhead = j - (self.pos + 1)
continue
# get dynamics. skip over the open paren to avoid confusion.
# NB: Nested crescendos are not an issue (not proper grammar).
if c == '!':
exclaimDict = {'!crescendo(!': ABCCrescStart,
'!crescendo)!': ABCParenStop,
'!diminuendo(!': ABCDimStart,
'!diminuendo)!': ABCParenStop,
}
j = self.pos + 1
while j < self.pos + 20 and j < self.srcLen: # a reasonable upper bound
if self.strSrc[j] == '!':
if self.strSrc[self.pos:j + 1] in exclaimDict:
exclaimClass = exclaimDict[self.strSrc[self.pos:j + 1]]
exclaimObject = exclaimClass(c)
self.tokens.append(exclaimObject)
self.skipAhead = j - self.pos # not + 1
break
# NB: We're currently skipping over all other '!' expressions
else:
self.skipAhead = j - self.pos # not + 1
break
j += 1
# not found, continue
continue
# get slurs, ensuring that they're not confused for tuplets
if c == '(' and cNext is not None and not cNext.isdigit():
self.tokens.append(ABCSlurStart(c))
continue
# get slur/tuplet ending; treat it as a general parenthesis stop
if c == ')':
self.tokens.append(ABCParenStop(c))
continue
# get ties between two notes
if c == '-':
self.tokens.append(ABCTie(c))
continue
# get chord symbols / guitar chords; collected and joined with
# chord or notes
if c == '"':
j = self.pos + 1
while j < self.srcLen - 1 and self.strSrc[j] != '"':
j += 1
j += 1 # need character that caused break
# there may be more than one chord symbol: need to accumulate
activeChordSymbol += self.strSrc[self.pos:j]
# environLocal.printDebug(['got chord symbol:', repr(activeChordSymbol)])
self.skipAhead = j - (self.pos + 1)
continue
# get chords
if c == '[':
j = self.pos + 1
# find closing chord bracket
while j < self.srcLen - 1 and self.strSrc[j] != ']':
j += 1
j += 1 # need character that caused break
# find outer chord length modifier
while j < self.srcLen and (self.strSrc[j].isdigit() or self.strSrc[j] in '/'):
j += 1
# prepend chord symbol
if activeChordSymbol != '':
self.currentCollectStr = activeChordSymbol + self.strSrc[self.pos:j]
activeChordSymbol = '' # reset
else:
self.currentCollectStr = self.strSrc[self.pos:j]
# environLocal.printDebug(['got chord:', repr(self.currentCollectStr)])
self.tokens.append(ABCChord(self.currentCollectStr))
self.skipAhead = j - (self.pos + 1)
# TODO: Chords need to be aware of accidentals too.
# Also what happens to prefixes and suffixes attached to chords,
# like ties.
continue
if c == '.':
self.tokens.append(ABCStaccato(c))
continue
if c == 'u':
self.tokens.append(ABCUpbow(c))
continue
if c == '{':
self.tokens.append(ABCGraceStart(c))
continue
if c == '}':
self.tokens.append(ABCGraceStop(c))
continue
if c == 'v':
self.tokens.append(ABCDownbow(c))
continue
if c == 'K':
self.tokens.append(ABCAccent(c))
continue
if c == 'k':
self.tokens.append(ABCStraccent(c))
continue
if c == 'M':
self.tokens.append(ABCTenuto(c))
continue
# get the start of a note event: alpha, decoration, or accidental
if c.isalpha() or c in '~^=_':
# condition where we start with an alpha that is not an alpha
# that comes before a pitch indication
# From the 2.2 draft standard, we see the following "decorations"
# defined:
# . staccato mark
# ~ Irish roll
# H fermata
# L accent or emphasis
# M lower mordent
# O coda
# P upper mordent
# S segno
# T trill
# u up-bow
# v down-bow
#
# Accidentals are these:
# ^ sharp
# ^^ double-sharp
# = natural
# _ flat
# __ double-flat
foundPitchAlpha = c.isalpha() and c not in accidentalsAndDecorations
if foundPitchAlpha:
abcPitch = c
if c in accidentals:
accidental = c
j = self.pos + 1
while j <= self.srcLen - 1:
# if we have not found pitch alpha
# decorations and/or accidentals may precede note names
if not foundPitchAlpha and self.strSrc[j] in accidentalsAndDecorations:
j += 1
if self.strSrc[j] in accidentals:
accidental += self.strSrc[j]
continue
# only allow one pitch, alpha, to be a "continue" condition
elif (not foundPitchAlpha and self.strSrc[j].isalpha()
# noinspection SpellCheckingInspection
and self.strSrc[j] not in '~wuvhHLTSN'):
foundPitchAlpha = True
abcPitch = self.strSrc[j]
j += 1
continue
# continue conditions after alpha:
# , register modification (, ') or number, rhythm indication
# number, /,
elif self.strSrc[j].isdigit() or self.strSrc[j] in ",/,'":
if self.strSrc[j] in ",'": # Register (octave) modification
abcPitch += self.strSrc[j]
j += 1
continue
else: # space, all else: break
break
# prepend chord symbol
if activeChordSymbol != '':
self.currentCollectStr = activeChordSymbol + self.strSrc[self.pos:j]
activeChordSymbol = '' # reset
else:
self.currentCollectStr = self.strSrc[self.pos:j]
# environLocal.printDebug(['got note event:', repr(self.currentCollectStr)])
# NOTE: skipping a number of articulations and other markers
# that are not yet supported
# some collections here are not yet supported; others may be
# the result of errors in encoded files
# v is up bow; might be: "^Segno"v which also should be dropped
# H is fermata
# . dot may be staccato, but should be attached to pitch
if self.currentCollectStr in ('w', 'u', 'v', 'v.', 'h', 'H', 'vk',
'uk', 'U', '~',
'.', '=', 'V', 'v.', 'S', 's',
'i', 'I', 'ui', 'u.', 'Q', 'Hy', 'Hx',
'r', 'm', 'M', 'n', 'N', 'o', 'O', 'P',
'l', 'L', 'R',
'y', 'T', 't', 'x', 'Z'):
pass
# these are bad chords, or other problematic notations like
# "D.C."x
elif (self.currentCollectStr.startswith('"')
and (self.currentCollectStr[-1] in ('u', 'v', 'k', 'K', 'Q', '.',
'y', 'T', 'w', 'h', 'x',)
or self.currentCollectStr.endswith('v.'))):
pass
elif (self.currentCollectStr.startswith('x')
or self.currentCollectStr.startswith('H')
or self.currentCollectStr.startswith('Z')):
pass
# not sure what =20 refers to
elif (len(self.currentCollectStr) > 1
and self.currentCollectStr.startswith('=')
and self.currentCollectStr[1].isdigit()):
pass
# only let valid self.currentCollectStr strings be parsed
elif abcPitch:
pitchClass: str = abcPitch[0].upper()
carriedAccidental = ''
propagation = self._accidentalPropagation()
if accidental:
# Remember the active accidentals in the measure
if propagation == 'octave':
accidentalized[abcPitch] = accidental
elif propagation == 'pitch':
accidentalized[pitchClass] = accidental
accidental = ''
else:
if propagation == 'pitch' and pitchClass in accidentalized:
carriedAccidental = accidentalized[pitchClass]
elif propagation == 'octave' and abcPitch in accidentalized:
carriedAccidental = accidentalized[abcPitch]
abcNote = ABCNote(self.currentCollectStr, carriedAccidental=carriedAccidental)
self.tokens.append(abcNote)
else:
self.tokens.append(ABCNote(self.currentCollectStr))
self.skipAhead = j - (self.pos + 1)
continue
# look for white space: can be used to determine beam groups
# no action: normal continuation of 1 char
pass
[docs]
def tokenProcess(self) -> None:
'''
Process all token objects. First, calls preParse(), then
does context assignments, then calls parse().
'''
# need a key object to get altered pitches
from music21 import key
# pre-parse : call on objects that need preliminary processing
# metadata, for example, is parsed
# lastTimeSignature = None
for token in self.tokens:
# environLocal.printDebug(['tokenProcess: calling preParse()', token.src])
token.preParse()
# context: iterate through tokens, supplying contextual data
# as necessary to appropriate objects
lastDefaultQL = None
lastKeySignature = None
lastTimeSignatureObj = None # an m21 object
lastTupletToken: ABCTuplet|None = None # a token obj; keeps count of usage
lastTieToken: ABCTie|None = None
lastStaccToken = None
lastUpToken = None
lastDownToken = None
lastAccToken = None
lastStrAccToken = None
lastTenutoToken = None
lastGraceToken = None
lastNoteToken = None
for i in range(len(self.tokens)):
# get context of tokens
q = self._getLinearContext(self.tokens, i)
tPrev, token, tNext, unused_tNextNext = q
# tPrevNotSpace, tPrev, t, tNext, tNextNotSpace, tNextNext = q
# environLocal.printDebug(['tokenProcess: calling parse()', t])
if isinstance(token, ABCMetadata):
if token.isMeter():
lastTimeSignatureObj = token.getTimeSignatureObject()
# restart matching conditions; match meter twice ok
if token.isDefaultNoteLength() or (token.isMeter() and lastDefaultQL is None):
lastDefaultQL = token.getDefaultQuarterLength()
elif token.isKey():
sharpCount, mode = token.getKeySignatureParameters()
lastKeySignature = key.KeySignature(sharpCount)
if mode not in (None, ''):
lastKeySignature = lastKeySignature.asKey(mode)
if token.isReferenceNumber():
# reset any spanners or parens at the end of any piece
# in case they aren't closed.
self.activeParens = []
self.activeSpanners = []
continue
# broken rhythms need to be applied to previous and next notes
if isinstance(token, ABCBrokenRhythmMarker):
if (isinstance(tPrev, ABCNote)
and isinstance(tNext, ABCNote)):
# environLocal.printDebug(['tokenProcess: got broken rhythm marker', t.src])
tPrev.brokenRhythmMarker = (token.data, 'left')
tNext.brokenRhythmMarker = (token.data, 'right')
else:
environLocal.printDebug(
['broken rhythm marker '
f'({token.src}) not positioned between two notes or chords'])
# need to update tuplets with currently active meter
if isinstance(token, ABCTuplet):
token.updateRatio(lastTimeSignatureObj)
# set number of notes that will be altered
# might need to do this with ql values, or look ahead to next
# token
token.updateNoteCount()
lastTupletToken = token
self.activeParens.append('Tuplet')
# notes within slur marks need to be added to the spanner
if isinstance(token, ABCSlurStart):
token.fillSlur()
slurObj = token.slurObj
if t.TYPE_CHECKING:
assert slurObj is not None
self.activeSpanners.append(slurObj)
self.activeParens.append('Slur')
elif isinstance(token, ABCParenStop):
if self.activeParens:
p = self.activeParens.pop()
if p in ('Slur', 'Crescendo', 'Diminuendo'):
self.activeSpanners.pop()
if isinstance(token, ABCTie):
# tPrev is usually an ABCNote but may be a GraceStop.
if lastNoteToken and lastNoteToken.tie == 'stop':
lastNoteToken.tie = 'continue'
elif lastNoteToken:
lastNoteToken.tie = 'start'
lastTieToken = token
if isinstance(token, ABCStaccato):
lastStaccToken = token
if isinstance(token, ABCUpbow):
lastUpToken = token
if isinstance(token, ABCDownbow):
lastDownToken = token
if isinstance(token, ABCAccent):
lastAccToken = token
if isinstance(token, ABCStraccent):
lastStrAccToken = token
if isinstance(token, ABCTenuto):
lastTenutoToken = token
if isinstance(token, ABCCrescStart):
token.fillCresc()
if t.TYPE_CHECKING:
assert token.crescObj is not None
self.activeSpanners.append(token.crescObj)
self.activeParens.append('Crescendo')
if isinstance(token, ABCDimStart):
token.fillDim()
if t.TYPE_CHECKING:
assert token.dimObj is not None
self.activeSpanners.append(token.dimObj)
self.activeParens.append('Diminuendo')
if isinstance(token, ABCGraceStart):
lastGraceToken = token
if isinstance(token, ABCGraceStop):
lastGraceToken = None
# ABCChord inherits ABCNote, thus getting note is enough for both
if isinstance(token, (ABCNote, ABCChord)):
if lastDefaultQL is None:
raise ABCHandlerException(
'no active default note length provided for note processing. '
f'tPrev: {tPrev}, token: {token}, tNext: {tNext}'
)
token.activeDefaultQuarterLength = lastDefaultQL
token.activeKeySignature = lastKeySignature
token.applicableSpanners = self.activeSpanners[:] # fast copy of a list
# This block ends tie objects one note after they begin
if lastTieToken is not None:
token.tie = 'stop'
lastTieToken = None
if lastStaccToken is not None:
token.articulations.append('staccato')
lastStaccToken = None
if lastUpToken is not None:
token.articulations.append('upbow')
lastUpToken = None
if lastDownToken is not None:
token.articulations.append('downbow')
lastDownToken = None
if lastAccToken is not None:
token.articulations.append('accent')
lastAccToken = None
if lastStrAccToken is not None:
token.articulations.append('strongaccent')
lastStrAccToken = None
if lastTenutoToken is not None:
token.articulations.append('tenuto')
lastTenutoToken = None
if lastGraceToken is not None:
token.inGrace = True
if lastTupletToken is None:
pass
elif lastTupletToken.noteCount == 0:
lastTupletToken = None # clear, no longer needed
else:
lastTupletToken.noteCount -= 1 # decrement
# add a reference to the note
token.activeTuplet = lastTupletToken.tupletObj
lastNoteToken = token
# parse : call methods to set attributes and parse abc string
for token in self.tokens:
# environLocal.printDebug(['tokenProcess: calling parse()', t])
token.parse()
[docs]
def process(self, strSrc: str) -> None:
self.parseHeaderForVersionInformation(strSrc[:100])
self.tokens = []
self.tokenize(strSrc)
self.tokenProcess()
# return list of tokens; stored internally
# --------------------------------------------------------------------------
# access tokens
def __len__(self) -> int:
return len(self.tokens)
def __add__(self, other: ABCHandler) -> ABCHandler:
'''
Return a new handler adding the tokens in both
Contrived example appending two separate keys.
Used in polyphonic metadata merge
>>> abcStr = 'M:6/8\\nL:1/8\\nK:G\\n'
>>> ah1 = abcFormat.ABCHandler()
>>> junk = ah1.process(abcStr)
>>> len(ah1)
3
>>> abcStr = 'M:3/4\\nL:1/4\\nK:D\\n'
>>> ah2 = abcFormat.ABCHandler()
>>> junk = ah2.process(abcStr)
>>> len(ah2)
3
>>> ah3 = ah1 + ah2
>>> len(ah3)
6
>>> ah3.tokens[0] == ah1.tokens[0]
True
>>> ah3.tokens[3] == ah2.tokens[0]
True
'''
ah = self.__class__() # will get the same class type
ah.tokens = self.tokens + other.tokens
return ah
# --------------------------------------------------------------------------
# utility methods for post-processing
[docs]
def definesReferenceNumbers(self) -> bool:
'''
Return True if this token structure defines more than 1 reference number,
usually implying multiple pieces encoded in one file.
>>> abcStr = 'X:5\\nM:6/8\\nL:1/8\\nK:G\\nB3 A3 | G6 | B3 A3 | G6 ||'
>>> ah = abcFormat.ABCHandler()
>>> junk = ah.process(abcStr)
>>> ah.definesReferenceNumbers() # only one returns False
False
>>> abcStr = 'X:5\\nM:6/8\\nL:1/8\\nK:G\\nB3 A3 | G6 | B3 A3 | G6 ||\\n'
>>> abcStr += 'X:6\\nM:6/8\\nL:1/8\\nK:G\\nB3 A3 | G6 | B3 A3 | G6 ||'
>>> ah = abcFormat.ABCHandler()
>>> junk = ah.process(abcStr)
There are two tokens, so this returns True
>>> ah.definesReferenceNumbers()
True
'''
if not self.tokens:
raise ABCHandlerException('must process tokens before calling split')
count = 0
for i in range(len(self.tokens)):
token = self.tokens[i]
if isinstance(token, ABCMetadata):
if token.isReferenceNumber():
count += 1
if count > 1:
return True
return False
[docs]
def splitByReferenceNumber(self) -> dict[int|None, ABCHandler]:
# noinspection PyShadowingNames
r'''
Split tokens by reference numbers.
Returns a dictionary of ABCHandler instances, where the reference number
is used to access the music. If no reference numbers are defined,
the tune is available under the dictionary entry None.
>>> abcStr = 'X:5\nM:6/8\nL:1/8\nK:G\nB3 A3 | G6 | B3 A3 | G6 ||'
>>> abcStr += 'X:6\nM:6/8\nL:1/8\nK:G\nB3 A3 | G6 | B3 A3 | G6 ||'
>>> ah = abcFormat.ABCHandler()
>>> junk = ah.process(abcStr)
>>> len(ah)
28
>>> ahDict = ah.splitByReferenceNumber()
>>> 5 in ahDict
True
>>> 6 in ahDict
True
>>> 7 in ahDict
False
Each entry is its own ABCHandler object.
>>> ahDict[5]
<music21.abcFormat.ABCHandler object at 0x10b0cf5f8>
>>> len(ahDict[5].tokens)
14
Header information (except for comments) should be appended to all pieces.
>>> from textwrap import dedent
>>> abcEarly = dedent("""X:4
... M:6/8
... L:1/8
... K:F
... I:abc-version 1.6
... B=3 B3 | G6 | B3 A3 | G6 ||
... """)
>>> abcStrWHeader = '%abc-2.1\nO: Irish\n' + abcEarly + abcStr
>>> ah = abcFormat.ABCHandler()
>>> junk = ah.process(abcStrWHeader)
>>> len(ah)
44
>>> ahDict = ah.splitByReferenceNumber()
Did we get the origin header in each score?
>>> ahDict[5].tokens[0]
<music21.abcFormat.ABCMetadata 'O: Irish'>
>>> ahDict[6].tokens[0]
<music21.abcFormat.ABCMetadata 'O: Irish'>
Before parsing all the tokens should have the 2.1 version:
>>> ahDict[4].abcVersion
(2, 1, 0)
>>> ahDict[5].abcVersion
(2, 1, 0)
After parsing, the abcVersion should be set for score 4 and
revert for score 5.
>>> for f in ahDict:
... _ = abcFormat.translate.abcToStreamScore(ahDict[f])
>>> ahDict[4].abcVersion
(1, 6, 0)
>>> ahDict[5].abcVersion
(2, 1, 0)
'''
if not self.tokens:
raise ABCHandlerException('must process tokens before calling split')
ahDict: dict[int|None, ABCHandler] = {}
# tokens in this list are prepended to all tunes:
prependToAllList = []
activeTokens = []
currentABCHandler = None
for i, token in enumerate(self.tokens):
if isinstance(token, ABCMetadata) and token.isReferenceNumber():
if currentABCHandler is not None:
currentABCHandler.tokens = activeTokens
activeTokens = []
currentABCHandler = ABCHandler(abcVersion=self.abcVersion)
referenceNumber = int(token.data)
ahDict[referenceNumber] = currentABCHandler
if currentABCHandler is None:
prependToAllList.append(token)
else:
activeTokens.append(token)
if currentABCHandler is not None:
currentABCHandler.tokens = activeTokens
if not ahDict:
ahDict[None] = ABCHandler(abcVersion=self.abcVersion)
for thisABCHandler in ahDict.values():
thisABCHandler.tokens = prependToAllList[:] + thisABCHandler.tokens
return ahDict
[docs]
def getReferenceNumber(self) -> str|None:
'''
If tokens are processed, get the first
reference number defined.
>>> abcStr = 'X:5\\nM:6/8\\nL:1/8\\nK:G\\nB3 A3 | G6 | B3 A3 | G6 ||'
>>> ah = abcFormat.ABCHandler()
>>> junk = ah.process(abcStr)
>>> ah.getReferenceNumber()
'5'
'''
if not self.tokens:
raise ABCHandlerException('must process tokens before calling split')
for token in self.tokens:
if isinstance(token, ABCMetadata):
if token.isReferenceNumber():
return token.data
return None
[docs]
def definesMeasures(self) -> bool:
'''
Returns True if this token structure defines Measures in a normal Measure form.
Otherwise False
>>> abcStr = ('M:6/8\\nL:1/8\\nK:G\\nV:1 name="Whistle" '
... 'snm="wh"\\nB3 A3 | G6 | B3 A3 | G6 ||\\nV:2 name="violin" '
... 'snm="v"\\nBdB AcA | GAG D3 | BdB AcA | GAG D6 ||\\nV:3 name="Bass" '
... 'snm="b" clef=bass\\nD3 D3 | D6 | D3 D3 | D6 ||')
>>> ah = abcFormat.ABCHandler()
>>> junk = ah.process(abcStr)
>>> ah.definesMeasures()
True
>>> abcStr = 'M:6/8\\nL:1/8\\nK:G\\nB3 A3 G6 B3 A3 G6'
>>> ah = abcFormat.ABCHandler()
>>> junk = ah.process(abcStr)
>>> ah.definesMeasures()
False
'''
if not self.tokens:
raise ABCHandlerException('must process tokens before calling split')
count = 0
for i in range(len(self.tokens)):
token = self.tokens[i]
if isinstance(token, ABCBar):
# must define at least 2 regular barlines
# this leave out cases where only double bars are given
if token.isRegular():
count += 1
# forcing the inclusion of two measures to count
if count >= 2:
return True
return False
[docs]
def splitByVoice(self) -> list[ABCHandler]:
# noinspection PyShadowingNames
'''
Given a processed token list, look for voices. If voices exist,
split into parts: common metadata, then next voice, next voice, etc.
Each part is returned as an ABCHandler instance.
>>> abcStr = ('M:6/8\\nL:1/8\\nK:G\\nV:1 name="Whistle" '
... 'snm="wh"\\nB3 A3 | G6 | B3 A3 | G6 ||\\nV:2 name="violin" '
... 'snm="v"\\nBdB AcA | GAG D3 | BdB AcA | GAG D6 ||\\nV:3 name="Bass" '
... 'snm="b" clef=bass\\nD3 D3 | D6 | D3 D3 | D6 ||')
>>> ah = abcFormat.ABCHandler()
>>> ah.process(abcStr)
>>> tokenColls = ah.splitByVoice()
>>> tokenColls[0]
<music21.abcFormat.ABCHandler object at 0x...>
Common headers are first
>>> [token.src for token in tokenColls[0].tokens]
['M:6/8', 'L:1/8', 'K:G']
Then each voice
>>> [token.src for token in tokenColls[1].tokens]
['V:1 name="Whistle" snm="wh"',
'B3', 'A3', '|', 'G6', '|', 'B3', 'A3', '|', 'G6', '||']
>>> [token.src for token in tokenColls[2].tokens]
['V:2 name="violin" snm="v"',
'B', 'd', 'B', 'A', 'c', 'A', '|',
'G', 'A', 'G', 'D3', '|',
'B', 'd', 'B', 'A', 'c', 'A', '|',
'G', 'A', 'G', 'D6', '||']
>>> [token.src for token in tokenColls[3].tokens]
['V:3 name="Bass" snm="b" clef=bass',
'D3', 'D3', '|', 'D6', '|',
'D3', 'D3', '|', 'D6', '||']
Then later the metadata can be merged at the start of each voice:
>>> mergedTokens = tokenColls[0] + tokenColls[1]
>>> mergedTokens
<music21.abcFormat.ABCHandler object at 0x...>
>>> [token.src for token in mergedTokens.tokens]
['M:6/8', 'L:1/8', 'K:G', 'V:1 name="Whistle" snm="wh"',
'B3', 'A3', '|', 'G6', '|',
'B3', 'A3', '|', 'G6', '||']
'''
# TODO: this procedure should also be responsible for
# breaking the passage into voice/lyric pairs
if not self.tokens:
raise ABCHandlerException('must process tokens before calling split')
voiceCount = 0
pos = []
for i in range(len(self.tokens)):
token = self.tokens[i]
if isinstance(token, ABCMetadata):
if token.isVoice():
# if first char is a number
# can be V:3 name="Bass" snm="b" clef=bass
if token.data and token.data[0].isdigit():
pos.append(i) # store position
voiceCount += 1
abcHandlers = []
# no voices, or definition of one voice, or use of V: field for
# something else
if voiceCount <= 1:
ah = self.__class__() # just making a copy
ah.tokens = self.tokens
abcHandlers.append(ah)
# two or more voices
else:
# collect start and end pairs of split
pairs = []
pairs.append([0, pos[0]])
i = pos[0]
for x in range(1, len(pos)):
j = pos[x]
pairs.append([i, j])
i = j
# add last
pairs.append([i, len(self)])
for x, y in pairs:
ah = self.__class__()
ah.tokens = self.tokens[x:y]
abcHandlers.append(ah)
return abcHandlers
@staticmethod
def _buildMeasureBoundaryIndices(
positionList: list[int],
lastValidIndex: int
) -> list[list[int]]:
'''
Staticmethod
Given a list of indices of a list marking the position of
each barline or implied barline, and the last valid index,
return a list of two-element lists, each indicating
the start and end positions of a measure.
Here's an easy case that makes this method look worthless:
>>> AH = abcFormat.ABCHandler
>>> AH._buildMeasureBoundaryIndices([8, 12, 16], 20)
[[0, 8], [8, 12], [12, 16], [16, 20]]
But in this case, we need to see that 12 and 13 don't represent different measures but
probably represent an end and new barline (repeat bar), etc.
>>> AH._buildMeasureBoundaryIndices([8, 12, 13, 16], 20)
[[0, 8], [8, 12], [13, 16], [16, 20]]
Here 115 is both the last barline and the last index, so there is no [115, 115] entry.
>>> bi = [9, 10, 16, 23, 29, 36, 42, 49, 56, 61, 62, 64, 70, 77, 84, 90, 96, 103, 110, 115]
>>> AH._buildMeasureBoundaryIndices(bi, 115)
[[0, 9], [10, 16], [16, 23], [23, 29], [29, 36], [36, 42], [42, 49], [49, 56], [56, 61],
[62, 64], [64, 70], [70, 77], [77, 84], [84, 90], [90, 96],
[96, 103], [103, 110], [110, 115]]
'''
# collect start and end pairs of split
pairs = []
# first chunk is metadata, as first token is probably not a bar
pairs.append([0, positionList[0]])
i = positionList[0] # get first bar position stored
# iterate through every other bar position (already have first)
for x in range(1, len(positionList)):
j = positionList[x]
if j == i + 1: # a span of one is skipped
i = j
continue
pairs.append([i, j])
i = j # the end becomes the new start
# add last valid index
if i != lastValidIndex:
pairs.append([i, lastValidIndex])
# environLocal.printDebug(['splitByMeasure(); pairs pre filter', pairs])
return pairs
[docs]
def splitByMeasure(self) -> list[ABCHandlerBar]:
'''
Divide a token list by Measures, also
defining start and end bars of each Measure.
If a component does not have notes, leave
as an empty bar. This is often done with leading metadata.
Returns a list of ABCHandlerBar instances.
The first usually defines only Metadata.
TODO: Test and examples
'''
if not self.tokens:
raise ABCHandlerException('must process tokens before calling split')
abcBarHandlers = []
barIndices = self.tokensToBarIndices()
# barCount = 0 # not used
# noteCount = 0 # not used
# environLocal.printDebug(['splitByMeasure(); raw bar positions', barIndices])
measureIndices = self._buildMeasureBoundaryIndices(barIndices, len(self) - 1)
# for x, y in pairs:
# environLocal.printDebug(['boundary indices:', x, y])
# environLocal.printDebug([' values at x, y', self.tokens[x], self.tokens[y]])
# iterate through start and end pairs
for x, y in measureIndices:
ah = ABCHandlerBar()
# this will get the first to last
# shave of tokens if not needed
xClip = x
yClip = y
# check if first is a bar; if so, assign and remove
tokenAtX = self.tokens[x]
if isinstance(tokenAtX, ABCBar):
lbCandidate: ABCBar = tokenAtX
# if we get an end repeat, probably already assigned this
# in the last measure, so skip
# environLocal.printDebug(f'''reading pairs, got token: {lbCandidate}
# {lbCandidate.barType=}
# {lbCandidate.repeatForm=}''')
# skip end repeats assigned (improperly) to the left
if (lbCandidate.barType == 'repeat'
and lbCandidate.repeatForm == 'end'):
pass
else: # assign
ah.leftBarToken = lbCandidate
# environLocal.printDebug(['splitByMeasure(); assigning left bar token',
# lbCandidate])
# always trim if we have a bar
xClip = x + 1
# ah.tokens = ah.tokens[1:] # remove first, as not done above
# if x boundary is metadata, do not include it (as it is likely in the previous
# measure) unless it is at the beginning.
elif x != 0 and isinstance(self.tokens[x], ABCMetadata):
xClip = x + 1
else:
# if we find a note in the x-clip position, it is likely a pickup the
# first note after metadata. this we keep, b/c it
# should be part of this branch
pass
if y >= len(self):
yTestIndex = len(self)
else:
yTestIndex = y
tokenAtYTestIndex = self.tokens[yTestIndex]
if isinstance(tokenAtYTestIndex, ABCBar):
rbCandidate: ABCBar = tokenAtYTestIndex
# if a start repeat, save it to be placed as a left barline
if not (rbCandidate.barType == 'repeat'
and rbCandidate.repeatForm == 'start'):
# environLocal.printDebug(['splitByMeasure(); assigning right bar token',
# lbCandidate])
ah.rightBarToken = rbCandidate
# always trim if we have a bar
# ah.tokens = ah.tokens[:-1] # remove last
yClip = y - 1
# if y boundary is metadata, include it
elif isinstance(self.tokens[yTestIndex], ABCMetadata):
pass # no change
# if y position is a note/chord, and this is the last index,
# we must have included it
elif not (isinstance(self.tokens[yTestIndex], (ABCNote, ABCChord))
and yTestIndex == len(self.tokens) - 1):
# if we find a note in the yClip position, it is likely
# a pickup, the first note after metadata. we do not include this
yClip = yTestIndex - 1
# environLocal.printDebug(['clip boundaries: x,y', xClip, yClip])
# boundaries are inclusive; need to add one here
ah.tokens = self.tokens[xClip:yClip + 1]
# after bar assign, if no bars are known, reject
if not ah:
continue
abcBarHandlers.append(ah)
# for sub in abcBarHandlers:
# environLocal.printDebug(['concluded splitByMeasure:', sub,
# 'leftBarToken', sub.leftBarToken, 'rightBarToken', sub.rightBarToken,
# 'len(sub)', len(sub), 'sub.hasNotes()', sub.hasNotes()])
# for token in sub.tokens:
# print('\t', token)
return abcBarHandlers
[docs]
def tokensToBarIndices(self) -> list[int]:
'''
Return a list of indices indicating which tokens in self.tokens are
bar lines or the last piece of metadata before a note or chord.
'''
barIndices = []
tNext = None
for i, token in enumerate(self.tokens):
try:
tNext = self.tokens[i + 1]
except IndexError:
tNext = None
# either we get a bar, or we just complete metadata and
# possibly encounter a note (a pickup)
if isinstance(token, ABCBar): # or (barCount == 0 and noteCount > 0):
# environLocal.printDebug(['splitByMeasure()', 'found bar', token])
barIndices.append(i) # store position
# barCount += 1 # not used
# case of end of metadata and start of notes in a pickup
# tag the last metadata as the end
elif (isinstance(token, ABCMetadata)
and tNext is not None
and isinstance(tNext, (ABCNote, ABCChord))):
barIndices.append(i) # store position
return barIndices
[docs]
def hasNotes(self) -> bool:
'''
If tokens are processed, return True if ABCNote or
ABCChord classes are defined.
>>> abcStr = 'M:6/8\\nL:1/8\\nK:G\\n'
>>> ah1 = abcFormat.ABCHandler()
>>> junk = ah1.process(abcStr)
>>> ah1.hasNotes()
False
>>> abcStr = 'M:6/8\\nL:1/8\\nK:G\\nc1D2'
>>> ah2 = abcFormat.ABCHandler()
>>> junk = ah2.process(abcStr)
>>> ah2.hasNotes()
True
'''
if not self.tokens:
raise ABCHandlerException('must process tokens before calling')
count = 0
for token in self.tokens:
if isinstance(token, (ABCNote, ABCChord)):
count += 1
# environLocal.printDebug(['hasNotes', count])
if count > 0:
return True
else:
return False
[docs]
def getTitle(self) -> str|None:
'''
Get the first title tag. Used for testing.
Requires tokens to have been processed.
'''
if not self.tokens:
raise ABCHandlerException('must process tokens before calling split')
for token in self.tokens:
if isinstance(token, ABCMetadata):
if token.isTitle():
return token.data
return None
[docs]
class ABCHandlerBar(ABCHandler):
'''
A Handler specialized for storing bars. All left
and right bars are collected and assigned to their respective attributes.
'''
# divide elements of a character stream into objects and handle
# store in a list, and pass global information to components
def __init__(self) -> None:
# tokens are ABC objects in a linear stream
super().__init__()
self.leftBarToken: ABCBar|None = None
self.rightBarToken: ABCBar|None = None
def __add__(self, other: ABCHandler) -> ABCHandlerBar:
ah = self.__class__() # will get the same class type
ah.tokens = self.tokens + other.tokens
# get defined tokens
for barAttr in ('leftBarToken', 'rightBarToken'):
bOld = getattr(self, barAttr)
bNew = getattr(other, barAttr)
if bNew is None and bOld is None:
pass # nothing to do
elif bNew is not None and bOld is None: # get new
setattr(ah, barAttr, bNew)
elif bNew is None and bOld is not None: # get old
setattr(ah, barAttr, bOld)
else:
# if both ar the same, assign one
if bOld.src == bNew.src:
setattr(ah, barAttr, bNew)
else:
# might resolve this by ignoring standard bars and favoring
# repeats or styled bars
environLocal.printDebug(['cannot handle two non-None bars yet: got bNew, bOld',
bNew, bOld])
# raise ABCHandlerException('cannot handle two non-None bars yet')
setattr(ah, barAttr, bNew)
return ah
# ------------------------------------------------------------------------------
[docs]
class ABCFile(prebase.ProtoM21Object):
'''
ABC File or String access
The abcVersion attribution optionally specifies the (major, minor, patch)
version of ABC to process-- e.g., (2, 1, 0).
If not set, default ABC 1.3 parsing is performed.
'''
def __init__(self, abcVersion: tuple[int, int, int] = defaults.abcVersionDefault) -> None:
self.abcVersion: tuple[int, int, int] = abcVersion
self.file: t.IO|None = None
self.filename: str|pathlib.Path = ''
[docs]
def open(self, filename: str|pathlib.Path) -> None:
'''
Open a file for reading
'''
# try:
self.file = io.open(filename, encoding='utf-8') # pylint: disable=consider-using-with
# except
# self.file = io.open(filename, encoding='latin-1')
self.filename = filename
[docs]
def openFileLike(self, fileLike: t.IO) -> None:
'''
Assign a file-like object, such as those provided by
StringIO, as an open file object.
>>> from io import StringIO
>>> fileLikeOpen = StringIO()
'''
self.file = fileLike # already 'open'
def _reprInternal(self) -> str:
'''
Return the filename if one is set, otherwise a placeholder for an
open file-like object, otherwise the empty string.
>>> abcFile = abcFormat.ABCFile()
>>> abcFile
<music21.abcFormat.ABCFile>
>>> abcFile.filename = 'tune.abc'
>>> abcFile
<music21.abcFormat.ABCFile tune.abc>
A file-like object with no filename shows a generic placeholder:
>>> from io import StringIO
>>> abcFile2 = abcFormat.ABCFile()
>>> abcFile2.openFileLike(StringIO('X:1'))
>>> abcFile2
<music21.abcFormat.ABCFile <filelike-object>>
'''
if self.filename:
return str(self.filename)
if self.file is not None:
return '<filelike-object>'
return ''
[docs]
def close(self) -> None:
if self.file is not None:
self.file.close()
[docs]
def read(self, number: int|None = None) -> ABCHandler:
'''
Read a file. Note that this calls readstr,
which processes all tokens.
If `number` is given, a work number will be extracted if possible.
'''
file = self.file
if file is None:
raise ABCFileException('cannot read; no file or file-like object is open')
return self.readstr(file.read(), number)
[docs]
def readstr(self, strSrc: str, number: int|None = None) -> ABCHandler:
'''
Read a string and process all Tokens.
Returns an ABCHandler instance.
'''
if number is not None:
# will raise exception if number cannot be found
strSrc = self.extractReferenceNumber(strSrc, number)
handler = ABCHandler(abcVersion=self.abcVersion)
if self.abcVersion == defaults.abcVersionDefault:
handler.parseHeaderForVersionInformation(strSrc[:100])
# return the handler instance
handler.process(strSrc)
return handler
# ------------------------------------------------------------------------------
class Test(unittest.TestCase):
def testCopyAndDeepcopy(self):
from music21.test.commonTest import testCopyAll
testCopyAll(self, globals())
def testTokenization(self):
from music21.abcFormat import testFiles
for (tf, countTokens, noteTokens, chordTokens) in [
(testFiles.fyrareprisarn, 241, 152, 0),
(testFiles.mysteryReel, 192, 153, 0),
(testFiles.aleIsDear, 291, 206, 32),
(testFiles.testPrimitive, 100, 75, 2),
(testFiles.williamAndNancy, 127, 93, 0),
(testFiles.morrisonsJig, 178, 137, 0),
]:
handler = ABCHandler()
handler.tokenize(tf)
tokens = handler.tokens # get private for testing
self.assertEqual(len(tokens), countTokens)
countNotes = 0
countChords = 0
for o in tokens:
if isinstance(o, ABCChord):
countChords += 1
elif isinstance(o, ABCNote):
countNotes += 1
self.assertEqual(countNotes, noteTokens)
self.assertEqual(countChords, chordTokens)
def testRe(self):
src = 'A: this is a test'
post = reMetadataTag.match(src).end()
self.assertEqual(src[:post], 'A:')
self.assertEqual(src[post:], ' this is a test')
src = 'Q: this is a test % and a following comment'
post = reMetadataTag.match(src).end()
self.assertEqual(src[:post], 'Q:')
# chord symbol matches
src = 'd|"G"e2d B2d|"C"gfe "D7"d2d|"G"e2d B2d|"A7""C"gfe "D7""D"d2c|'
post = reChordSymbol.findall(src)
self.assertEqual(post, ['"G"', '"C"', '"D7"', '"G"', '"A7"',
'"C"', '"D7"', '"D"'])
# get index of last match of many
i = list(reChordSymbol.finditer(src))[-1].end()
src = '=d2'
self.assertEqual(rePitchName.findall(src)[0], 'd')
src = 'A3/2'
self.assertEqual(rePitchName.findall(src)[0], 'A')
def testTokenProcessMetadata(self):
from music21.abcFormat import testFiles
# noinspection SpellCheckingInspection
for (tf, titleEncoded, meterEncoded, keyEncoded) in [
(testFiles.fyrareprisarn, 'Fyrareprisarn', '3/4', 'F'),
(testFiles.mysteryReel, 'Mystery Reel', 'C|', 'G'),
(testFiles.aleIsDear, 'Ale is Dear, The', '4/4', 'D', ),
(testFiles.kitchGirl, 'Kitchen Girl', '4/4', 'D'),
(testFiles.williamAndNancy, 'William and Nancy', '6/8', 'G'),
]:
handler = ABCHandler()
handler.tokenize(tf)
handler.tokenProcess()
tokens = handler.tokens # get private for testing
for token in tokens:
if isinstance(token, ABCMetadata):
if token.tag == 'T':
self.assertEqual(token.data, titleEncoded)
elif token.tag == 'M':
self.assertEqual(token.data, meterEncoded)
elif token.tag == 'K':
self.assertEqual(token.data, keyEncoded)
def testTokenProcess(self):
from music21.abcFormat import testFiles
for tf in [
testFiles.fyrareprisarn,
testFiles.mysteryReel,
testFiles.aleIsDear,
testFiles.testPrimitive,
testFiles.kitchGirl,
testFiles.williamAndNancy,
]:
handler = ABCHandler()
handler.tokenize(tf)
handler.tokenProcess()
def testNoteParse(self):
from music21 import key
an = ABCNote()
# with a key signature, matching steps are assumed altered
an.activeKeySignature = key.KeySignature(3)
self.assertEqual(an.getPitchName('c'), ('C#5', False))
an.activeKeySignature = None
self.assertEqual(an.getPitchName('c'), ('C5', None))
self.assertEqual(an.getPitchName('^c'), ('C#5', True))
an.activeKeySignature = key.KeySignature(-3)
self.assertEqual(an.getPitchName('B'), ('B-4', False))
an.activeKeySignature = None
self.assertEqual(an.getPitchName('B'), ('B4', None))
self.assertEqual(an.getPitchName('_B'), ('B-4', True))
def testSplitByMeasure(self):
from music21.abcFormat import testFiles
ah = ABCHandler()
ah.process(testFiles.hectorTheHero)
ahm = ah.splitByMeasure()
for i, left, right in [
(0, None, None), # metadata
(2, '|:', '|'),
(3, '|', '|'),
(-2, '[1', ':|'),
(-1, '[2', '|'),
]:
if left is None:
self.assertEqual(ahm[i].leftBarToken, None)
else:
self.assertEqual(ahm[i].leftBarToken.src, left)
if right is None:
self.assertEqual(ahm[i].rightBarToken, None)
else:
self.assertEqual(ahm[i].rightBarToken.src, right)
# for ahSub in ah.splitByMeasure():
# environLocal.printDebug(['split by measure:', ahSub.tokens])
# environLocal.printDebug(['leftBar:', ahSub.leftBarToken,
# 'rightBar:', ahSub.rightBarToken, '\n'])
ah = ABCHandler()
ah.process(testFiles.theBeggerBoy)
ahm = ah.splitByMeasure()
for i, left, right in [
(0, None, None), # metadata
(1, None, '|'),
(-1, '||', None), # trailing lyric metadata
]:
if left is None:
self.assertEqual(ahm[i].leftBarToken, None)
else:
self.assertEqual(ahm[i].leftBarToken.src, left)
if right is None:
self.assertEqual(ahm[i].rightBarToken, None)
else:
self.assertEqual(ahm[i].rightBarToken.src, right)
# test a simple string with no bars
ah = ABCHandler()
ah.process('M:6/8\nL:1/8\nK:G\nc1D2')
ahm = ah.splitByMeasure()
for i, left, right in [
(0, None, None), # metadata
(-1, None, None), # note data, but no bars
]:
if left is None:
self.assertEqual(ahm[i].leftBarToken, None)
else:
self.assertEqual(ahm[i].leftBarToken.src, left)
if right is None:
self.assertEqual(ahm[i].rightBarToken, None)
else:
self.assertEqual(ahm[i].rightBarToken.src, right)
def testMergeLeadingMetaData(self):
from music21.abcFormat import testFiles
# a case of leading and trailing metadata
ah = ABCHandler()
ah.process(testFiles.theBeggerBoy)
ahm = ah.splitByMeasure()
self.assertEqual(len(ahm), 14)
mergedHandlers = mergeLeadingMetaData(ahm)
# after merging, one less handler as leading metadata is merged
self.assertEqual(len(mergedHandlers), 13)
# the last handler is all trailing metadata
self.assertTrue(mergedHandlers[0].hasNotes())
self.assertFalse(mergedHandlers[-1].hasNotes())
self.assertTrue(mergedHandlers[-2].hasNotes())
# these are all ABCHandlerBar instances with bars defined
self.assertEqual(mergedHandlers[-2].rightBarToken.src, '||')
# a case of only leading metadata
ah = ABCHandler()
ah.process(testFiles.theAleWifesDaughter)
ahm = ah.splitByMeasure()
self.assertEqual(len(ahm), 10)
mergedHandlers = mergeLeadingMetaData(ahm)
# after merging, one less handler as leading metadata is merged
self.assertEqual(len(mergedHandlers), 10)
# all handlers have notes
self.assertTrue(mergedHandlers[0].hasNotes())
self.assertTrue(mergedHandlers[-1].hasNotes())
self.assertTrue(mergedHandlers[-2].hasNotes())
# these are all ABCHandlerBar instances with bars defined
self.assertEqual(mergedHandlers[-1].rightBarToken.src, '|]')
# test a simple string with no bars
ah = ABCHandler()
ah.process('M:6/8\nL:1/8\nK:G\nc1D2')
ahm = ah.splitByMeasure()
# split by measure divides metadata
self.assertEqual(len(ahm), 2)
mergedHandlers = mergeLeadingMetaData(ahm)
# after merging, metadata is merged back
self.assertEqual(len(mergedHandlers), 1)
# and it has notes
self.assertTrue(mergedHandlers[0].hasNotes())
def testSplitByReferenceNumber(self):
from music21.abcFormat import testFiles
# a case of leading and trailing metadata
ah = ABCHandler()
ah.process(testFiles.theBeggerBoy)
ahs = ah.splitByReferenceNumber()
self.assertEqual(len(ahs), 1)
self.assertEqual(list(ahs.keys()), [5])
self.assertEqual(len(ahs[5]), 88) # tokens
self.assertEqual(ahs[5].tokens[0].src, 'X:5') # first is retained
# noinspection SpellCheckingInspection
self.assertEqual(ahs[5].getTitle(), 'The Begger Boy') # tokens
ah = ABCHandler()
ah.process(testFiles.testPrimitivePolyphonic) # has no reference num
self.assertEqual(len(ah), 47) # tokens
ahs = ah.splitByReferenceNumber()
self.assertEqual(len(ahs), 1)
self.assertEqual(list(ahs.keys()), [None])
self.assertEqual(ahs[None].tokens[0].src, 'M:6/8') # first is retained
self.assertEqual(len(ahs[None]), 47) # tokens
ah = ABCHandler()
ah.process(testFiles.valentineJigg) # has no reference num
self.assertEqual(len(ah), 244) # total tokens
ahs = ah.splitByReferenceNumber()
self.assertEqual(len(ahs), 3)
self.assertEqual(sorted(list(ahs.keys())), [166, 167, 168])
self.assertEqual(ahs[168].tokens[0].src, 'X:168') # first is retained
self.assertEqual(ahs[168].getTitle(), '168 The Castle Gate (HJ)')
self.assertEqual(len(ahs[168]), 89) # tokens
self.assertEqual(ahs[166].tokens[0].src, 'X:166') # first is retained
# noinspection SpellCheckingInspection
self.assertEqual(ahs[166].getTitle(), '166 Valentine Jigg (Pe)')
self.assertEqual(len(ahs[166]), 67) # tokens
self.assertEqual(ahs[167].tokens[0].src, 'X:167') # first is retained
self.assertEqual(ahs[167].getTitle(), '167 The Dublin Jig (HJ)')
self.assertEqual(len(ahs[167]), 88) # tokens
def testExtractReferenceNumber(self):
from music21 import corpus
fp = corpus.getWork('essenFolksong/test0')
af = ABCFile()
af.open(fp)
ah = af.read(5) # returns a parsed handler
af.close()
self.assertEqual(len(ah), 74)
af = ABCFile()
af.open(fp)
ah = af.read(7) # returns a parsed handler
af.close()
self.assertEqual(len(ah), 84)
fp = corpus.getWork('essenFolksong/han1')
af = ABCFile()
af.open(fp)
ah = af.read(339) # returns a parsed handler
af.close()
self.assertEqual(len(ah), 101)
def testSlurs(self):
from music21.abcFormat import testFiles
ah = ABCHandler()
ah.process(testFiles.slurTest)
self.assertEqual(len(ah), 70) # number of tokens
def testTies(self):
from music21.abcFormat import testFiles
ah = ABCHandler()
ah.process(testFiles.tieTest)
self.assertEqual(len(ah), 73) # number of tokens
def testCresc(self):
from music21.abcFormat import testFiles
ah = ABCHandler()
ah.process(testFiles.crescTest)
self.assertEqual(len(ah), 75)
tokens = ah.tokens
i = 0
for token in tokens:
if isinstance(token, ABCCrescStart):
i += 1
self.assertEqual(i, 1)
def testDim(self):
from music21.abcFormat import testFiles
ah = ABCHandler()
ah.process(testFiles.dimTest)
self.assertEqual(len(ah), 75)
tokens = ah.tokens
i = 0
for token in tokens:
if isinstance(token, ABCDimStart):
i += 1
self.assertEqual(i, 1)
def testStaccato(self):
from music21.abcFormat import testFiles
ah = ABCHandler()
ah.process(testFiles.staccTest)
self.assertEqual(len(ah), 80)
def testBow(self):
from music21.abcFormat import testFiles
ah = ABCHandler()
ah.process(testFiles.bowTest)
self.assertEqual(len(ah), 83)
tokens = ah.tokens
upBows = 0
downBows = 0
for token in tokens:
if isinstance(token, ABCUpbow):
upBows += 1
elif isinstance(token, ABCDownbow):
downBows += 1
self.assertEqual(upBows, 2)
self.assertEqual(downBows, 1)
def testAcc(self):
from music21.abcFormat import testFiles
from music21 import abcFormat
ah = abcFormat.ABCHandler()
ah.process(testFiles.accTest)
# noinspection SpellCheckingInspection
tokensCorrect = '''<music21.abcFormat.ABCMetadata 'X: 979'>
<music21.abcFormat.ABCMetadata 'T: Staccato test, plus accents and tenuto marks'>
<music21.abcFormat.ABCMetadata 'M: 2/4'>
<music21.abcFormat.ABCMetadata 'L: 1/16'>
<music21.abcFormat.ABCMetadata 'K: Edor'>
<music21.abcFormat.ABCNote 'B,2'>
<music21.abcFormat.ABCBar '|'>
<music21.abcFormat.ABCDimStart '!'>
<music21.abcFormat.ABCStaccato '.'>
<music21.abcFormat.ABCNote 'E'>
<music21.abcFormat.ABCNote '^D'>
<music21.abcFormat.ABCStaccato '.'>
<music21.abcFormat.ABCNote 'E'>
<music21.abcFormat.ABCTie '-'>
<music21.abcFormat.ABCNote 'E'>
<music21.abcFormat.ABCParenStop '!'>
<music21.abcFormat.ABCSlurStart '('>
<music21.abcFormat.ABCTuplet '(3'>
<music21.abcFormat.ABCStaccato '.'>
<music21.abcFormat.ABCNote 'G'>
<music21.abcFormat.ABCStaccato '.'>
<music21.abcFormat.ABCNote 'F'>
<music21.abcFormat.ABCStaccato '.'>
<music21.abcFormat.ABCAccent 'K'>
<music21.abcFormat.ABCNote 'G'>
<music21.abcFormat.ABCParenStop ')'>
<music21.abcFormat.ABCNote 'B'>
<music21.abcFormat.ABCNote 'A'>
<music21.abcFormat.ABCParenStop ')'>
<music21.abcFormat.ABCBar '|'>
<music21.abcFormat.ABCNote 'E'>
<music21.abcFormat.ABCNote '^D'>
<music21.abcFormat.ABCTenuto 'M'>
<music21.abcFormat.ABCNote 'E'>
<music21.abcFormat.ABCNote 'F'>
<music21.abcFormat.ABCTuplet '(3'>
<music21.abcFormat.ABCSlurStart '('>
<music21.abcFormat.ABCNote 'G'>
<music21.abcFormat.ABCTie '-'>
<music21.abcFormat.ABCNote 'G'>
<music21.abcFormat.ABCNote 'G'>
<music21.abcFormat.ABCParenStop ')'>
<music21.abcFormat.ABCParenStop ')'>
<music21.abcFormat.ABCNote 'B'>
<music21.abcFormat.ABCStraccent 'k'>
<music21.abcFormat.ABCTenuto 'M'>
<music21.abcFormat.ABCNote 'A'>
<music21.abcFormat.ABCBar '|'>
<music21.abcFormat.ABCSlurStart '('>
<music21.abcFormat.ABCNote 'E'>
<music21.abcFormat.ABCSlurStart '('>
<music21.abcFormat.ABCNote '^D'>
<music21.abcFormat.ABCNote 'E'>
<music21.abcFormat.ABCParenStop ')'>
<music21.abcFormat.ABCNote 'F'>
<music21.abcFormat.ABCParenStop ')'>
<music21.abcFormat.ABCTuplet '(3'>
<music21.abcFormat.ABCSlurStart '('>
<music21.abcFormat.ABCStraccent 'k'>
<music21.abcFormat.ABCNote 'G'>
<music21.abcFormat.ABCAccent 'K'>
<music21.abcFormat.ABCNote 'F'>
<music21.abcFormat.ABCParenStop ')'>
<music21.abcFormat.ABCNote 'G'>
<music21.abcFormat.ABCParenStop ')'>
<music21.abcFormat.ABCNote 'A'>
<music21.abcFormat.ABCTie '-'>
<music21.abcFormat.ABCNote 'A'>
<music21.abcFormat.ABCBar '|'>
<music21.abcFormat.ABCSlurStart '('>
<music21.abcFormat.ABCNote 'E'>
<music21.abcFormat.ABCNote '^D'>
<music21.abcFormat.ABCNote 'E'>
<music21.abcFormat.ABCNote 'F'>
<music21.abcFormat.ABCTuplet '(3'>
<music21.abcFormat.ABCSlurStart '('>
<music21.abcFormat.ABCNote 'G'>
<music21.abcFormat.ABCNote 'F'>
<music21.abcFormat.ABCNote 'G'>
<music21.abcFormat.ABCParenStop ')'>
<music21.abcFormat.ABCParenStop ')'>
<music21.abcFormat.ABCParenStop ')'>
<music21.abcFormat.ABCNote 'B'>
<music21.abcFormat.ABCNote 'A'>
<music21.abcFormat.ABCBar '|'>
<music21.abcFormat.ABCNote 'G6'>
'''.splitlines()
tokensReceived = [str(x) for x in ah.tokens]
self.assertEqual(tokensCorrect, tokensReceived)
self.assertEqual(len(ah), 86)
tokens = ah.tokens
i = 0
j = 0
k = 0
for token in tokens:
if isinstance(token, abcFormat.ABCAccent):
i += 1
elif isinstance(token, abcFormat.ABCStraccent):
j += 1
elif isinstance(token, abcFormat.ABCTenuto):
k += 1
self.assertEqual(i, 2)
self.assertEqual(j, 2)
self.assertEqual(k, 2)
def testGrace(self):
from music21.abcFormat import testFiles
ah = ABCHandler()
ah.process(testFiles.graceTest)
self.assertEqual(len(ah), 85)
def testGuineaPig(self):
from music21.abcFormat import testFiles
ah = ABCHandler()
ah.process(testFiles.guineapigTest)
self.assertEqual(len(ah), 105)
# ------------------------------------------------------------------------------
# define presented order in documentation
_DOC_ORDER = [ABCFile, ABCHandler, ABCHandlerBar]
if __name__ == '__main__':
# sys.arg test options will be used in mainTest()
import music21
music21.mainTest(Test)