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