diff --git a/abc2xml/Changes.txt b/abc2xml/Changes.txt deleted file mode 100644 index 62adff3..0000000 --- a/abc2xml/Changes.txt +++ /dev/null @@ -1,261 +0,0 @@ -Version 58 -- grammer for I:score instruction replaces ad hoc parsing. -- voice merging, part grouping and grand staffs are derived by transformation of the parse tree of I:score. -- also move clef redefinitions after [I:staff +/-n]. -- avoid a false beam break when [I:staff +/-n] is used within a beamed note group. -- absolute ABC staff numbers in [I:staff n] are mapped to MusicXML grand staff numbers (when possible). -- added translation of the [+-^_]15 octave clefs. -- no default channel/panning/volume in elements. - -Version 57 -- grand staff implemented. -- in an [I:score] instruction recognize a braced voice group as a grand staff when the opening brace is followed by an asterix ('{*' no space allowed). -- also recognize a normal braced voice group as a grand staff when precisely one of the braced voices has a name. -- the name of a staff or part is taken from the first of its voices (in ABC source order) for which a name is defined. -- within a grand staff notes and directions preceded by [I:staff +/-n] are moved n staffs upwards or downwards. -- accept voice ids with a leading asterix in I:score instructions (for the time being the asterix is discarded, however) - -Version 56 -- the element should come before the element in a note. (previously caused syntax warning with FinaleNotepad) -- translates the _8, ^8 variants of the clefs in addition to the +8, -8. Also translates octave= argument of V: and K: -- part names are left empty when not explicitly given using V:id name="" (previously id was used in that case) -- transpose= was only honoured when a typed clef= definition was present, now is works in any V: or K: field. -- a tempo definition in the ABC header (Q:) is treated as an inline tempo ([Q:]) for the first voice. -- repeated occurrences of [I:MIDI] in a voice are translated into a MusicXML instrument changes for that voice. - Neither Finale nor Musescore recognize the relevant MusicXML elements ( and yet. -- new cue=on/off attribute for K: and V: definitions, to mark sections of cue notes in a voice. - Neither Finale nor Musescore handle cue size for directions, yet (). -- normally whole measure rests are discarded when merging staffs. Use option -r to keep these rests in the merged results. - -Version 55 -- replaced info message text: "leeg" with "empty voice", when skipping an empty voice. -- avoided exception when ABC voice has only one measure containing only a volta number -- do not output an xml-backup element when it has zero duration - -Version 54 -- allow gracenotes within chords -- discard unbalanced chord brackets (caused a syntax error previously) -- chords containing only rests caused an exception -- ABC-code like: B2 1 caused an exception, now interpreted as: B2/1 - -Version 53 -- tie to a chord with a single note caused an exception. -- a first line with a single measure and only a left bar line caused a syntax error. -- a first line with a single measure with only chords and no bar lines caused a syntax error. -- option -z or --mxl writes an additional compressed xml file with extention .mxl -- better treatment of barlines with spaces interspersed. -- dot after note right before barline caused syntax error (now only warning and skip) - -Version 52 -- accept and translate chords in a grace sequence (caused an exception previously) -- corrected bug when parsing %%score without space between voice number and left bracket, like %%staves 1(2 3) - -Version 51 -- treat abc file without any X: as a single tune and prepend X:1 -- recognize alto4 as alternative notation for tenor clef - -Version 50 -- corrected translation of the clef octave change notation (i.e. +8, -8 suffix) - -Version 49 -- ties between different pitches are converted to slurs (previously the tie in 'A-B A' would extend -to the second A, now there will be a slur between A and B). -- do not add accidentals to tied notes even when present in ABC code. -- use consistent voice numbering in XML when merging ABC-voices with overlays. - -Version 48 -- M: field with more than one slash caused an exception. -- limit shortest note durations to 1/64 -- skip redundant closing slurs (caused exception previously) -- check instrument id when generating MIDI elements (caused exception when errors in %%score) -- issue warning message when illegal user defined characters are used (X,Y,Z,x,y,z) -- use correct xml-syntax for credit elements (was wrong in version 47) -- translate explicit alterations for non traditional keys -(non traditional key signatures not supported by MuseScore, but note alterations are correct) -- skip more illegal symbols in stead of issuing parse error - -Version 47 -- translate middle= in clefs -- translate transpose= in clefs (also %%MIDI transpose is recognised) -- translate subname= in V: fields (name= was already translated) -- translate "%%MIDI program" and "%%MIDI channel" (multiple instruments per staff supported) -- an abc file with multiple tunes can be converted to a set of musicxml files. -- new command line option "-m SKIP NUM" to set the range of tunes to be converted for large abc files. -- leading dot was skipped when parsing a float in page format settings (.5in -> 5in) -- accept [r:] syntax for remarks (caused syntax error previously) -- relax syntax for user defined symbols (allowed character set was needlessly restricted) -- all abc meta-info fields are translated to credit tags (with type) - -Version 46 -- warn when unit length is a single integer and assume L:1/8 (caused exception previously) -- translate tune with only header fields (caused exception previously) -- translate tunes where first voice definition appears in the middle of the body (caused exception previously) -- skip illegal characters in chords and issue warning (caused syntax error previously) -- skip illegal U: field in body and issue warning (caused syntax error previously) -- more illegal characters between elements are skipped but with less specific warnings. -- line endings with only were not handled properly -- add check for text without any abc code in it (caused exception previously) -- conversion of pObj to string caused exception when it contained non latin-1 characters (e.g. in warning messages) - -Version 45 -- ignore old style continuation when next line is w: -- replace illegal durations by the nearest smaller legal one (caused exception previously) -- when multiple stave mappings keep only first one and issue warning -- accept start of next tune (X:) when not preceeded by an empty line (caused syntax error previously) -- warn when unit length is no fraction of integers and assume L:1/8 (caused exception previously) -- raise meaningful exception when the tune is empty (e.g. empty line after K:) -- broken rhythm involving a rest was not translated - -Version 44 -- translate volta brackets with quoted text (in addition to numbers and number ranges) -- when error in M: field assume 4/4. (caused exception previously) -- allow voice names with underscore -- bug in parsing %%score when no space between two stave groups, like: (v1 v2)(v3 v4) -- corrected merging of voices of unequal length (caused exception previously) -- user defined symbols with '()<>.' chars were not accepted -- when p was a user defined symbol also !p! was erroneously seen as used defined -- skip random text before tune starts (caused syntax error previously) - -Version 43 -- more non standard bar lines recognized (e.g. :]) -- accept X: fields when the tune number is followed by non-numeric chars. -- allow complex meter (M:2+2+3/8) -- warn for illegal L: fields (like L:1/6) and use 1/8 instead -- accept and skip E: field -- wedge close was not recognized when written with old '+' notation (bug was in fixSlurs) -- remove * and ** at end of line (probably old notation for right adjustment) -- accept and ignore a Q:-field with only text -- replace "Expected end of text at .." message from parser by "Syntax error at .." - -Version 42 -- translate tempo from Q: field -- translate -8 in key or voice field (temporary fix) -- accept empty text annotations -- in addition to !<(! also accept !crescendo(! and the same for diminuendo -- ignore line continuation after information field -- in lyrics treat isolated tildes as note skips -- encode parse error message in utf-8 and when that fails in latin-1 -- limit length of misplaced symbol message to 80 chars -- put text between K: and first V: into the header (was skipped previously) -- bug in %%score when no spaces around '|'-character -- added "o" as alternative for dim ("Edim7" = "Eo7") -- in chord symbols "+" and "-" were not always translated correctly -- tuple abbreviation (n:: and (n::m were not translated correctly -- dotted barlines translated (: .|) - -Version 41 -- translate (multi-)measure rests: Z, X -- discard misplaced broken symbols (no more exceptions) -- discard misplaced octave suffices (warning, no parse error) -- discard misplaced header fields (warning, no parse error after K:) -- show location of misplaced symbols -- only parse header if it is not empty (-> better error messages) -- accept score/stave-directives and page formatting also in body - -Version 40 -- when reading a file with multiple tunes only translate the first one -- command line option added for scale, page size and margins -- all %% directives are changed into I: fields -- translate scale, page size and margins from ABC directives, but command line overrides - -Version 39 -- accept and discard rests in chords -- accept and discard accidentals before rests -- spacer is treated as a normal note but discarded -- accept chords with only one note -- invisible rests (x) are translated as invisible in xml too. - -Version 38 -- also recognise ma and mi in chord symbols (e.g. Cma7, Cmi7) -- replace tildes in lyrics by spaces -- accept syllabi starting with a tilde -- accept space after V: 1 - -Version 37 -- accidental elements are written whenever an explicit accidentals are present in ABC (for finale, mscore does not need them) -- tuplet notation elements are written to mark begin and end of tuplet (for finale, mscore doet not need them) -- normal-type elements are written when tuplets are irregular -- issue understandable message when metre is not recognized -- take (compound) metre into account when translating tuplet shorthands (as described in ABC 2.1) -- do not add beaming-elements into rest-elements (still break beam when long rest in beamgroup). - -Version 36 -- volta-stop elements were somtimes (recognised at and) put into a left-bar element, which -is not valid MusicXML. These elements are now put into the right-bar of the previous measure. -- accept volta's without barline: "abc [1 def" is read as: "abc |[1 def" -- accept volta's after redundant left barline: "abc |\n|[1 def" is read as "abc |\n[1 def" -- changed document order of repeat and ending elements (MusicXML order requirement) -- xml output to stdout is also encoded in utf-8 (was erroneously done in latin-1) -- prevent finale detecting the string "xml" in the -tag (finale quirk) -- only issue a -tag when abc code really uses explicit linebreaks - -Version 35 -- recognise and translate (nested) braces and brackets in both %%staves and %%score -- translate more keys (maj, min, mix, dor, phr, lyd, loc and none) -- recognise and skip [I:...] in body -- invisible bar lines translated -- silently skip slur end when there is no corresponding slur begin -- also accept user defined symbols delimited by + -- limit length of syntax error message (in a way compatible with pyparsing internal administration) -- add tag, because Finale seems to expect it. - -Version 34 -- removed copy() from 2 parse expressions because it did not appear to work on Linux -- recognize, warn and skip misplaced accidentals and ties -- bug in old style continuation when there was nothing left to continue with. -- limit syntax error output to 80 chars, with error in the middle. - -Version 33 -- added !coda!, !segno!, !fine!, !dacoda!, !dalsegno! etc. -- move coda and segno when just before a barline to next measure -- added several ornaments and articulations. -- recognize (and skip) '<' and '>' position markers for text expressions -- fall back to text expression for unrecognized chord symbols. -- recognize (and skip) alternative chord symbols between brackets -- interpret expressions like "ABC" as text expression and not as misspelled chord symbol. -- beam detection improved (grammar) - -Version 32 -- grammar for both linebreaks ! and $ without ambiguities! -- remove commandline option for linebreak (also I:linebreak not needed anymore) -- accept both + and ! for deco's -- accept (and skip) empty fields -- accept ']' in all header fields -- strip leading and trailing spaces from header fields (T: title -> T:title) -- also translate inline fields before a left barline -- recognise volta after forward repeat -- translate dashes to comma's in volta ([1-3 -> [1,3) -- recognise slurs after broken rhythm symbols i.e. replace (ab>>)c -> (ab)>>c -- skip P: fields -- allow lines with a single measure without barline -- combine ~-syllabi (multiple syllabi under one note) -- skip W: lyrics because no musicXML equivalent -- translate longa and breve - -Version 31 -- bug in treatment of double-repeat when it is at the end of a voice-lyrics block -- added declaration because Finale seems to need it. -- added identification/encoding element so people can see what software made the file - -Version 30 -- voice overlays -- merging voices rewritten: lyrics renumbering, measurewise merging -- linebreak after left-bar is output directly (i.e. not postponed to the next measure) - -Version 29 -- implementation of beaming -- insert line breaks at start of next measure -- keep only one line break per measure when mapping multiple voices -- renumber slurs when clash occurs during voice mapping -- syntax error message is encoded in latin-1 -- bug in recognition of presence of open tie - -Version 28 -- all chord symbols were erroneously recognized as text annotations (wrong syntax priority) -- Recognize (and skip) redundant left side bars -- don't stumble on a double repeat (::) when it occurs at the and of a voice/lyrics block -- better error message when header fields appear after the first K: -- output of non latin-1 strings on stderr uses repr() - -Version 27 -- Initial release diff --git a/abc2xml/Readme.txt b/abc2xml/Readme.txt deleted file mode 100644 index b8e0bb0..0000000 --- a/abc2xml/Readme.txt +++ /dev/null @@ -1,76 +0,0 @@ ----- abc2xml ---- - -abc2xml is a command line utility that translates ABC notation into MusicXML. - -In principle all elements from ABC are translated, but some translations are only partially -implemented. Translated are: -- multiple lyric lines per voice -- mapping voices to staves, brackets and braces (%%score or %%staves) -- voice overlays (only single &-overlays, no multiple &&-overlays yet) -- dynamics, slurs, several decorations (ties between different voices are not possible in MusicXML -and are changed to slurs when present in ABC. Also (illegal) ties between different pitches are converted -to slurs. In both cases a warning message is issued) -- grace notes, tuplets -- keys (maj, min, mix, dor, phr, lyd, loc and none), meter, tempo -- clefs (only most common clef names, tranposition= and middle= are supported) -- jazz chord symbols and text annotations -- beaming. Only one beam per abc-beam group is translated at the moment, which is -sufficient for MuseScore. In musicXML every beam should be explicitly notated, so a 32th -note should start 3 beams. -- scale, page size and page margins are recognized as either %% or I: directive. The scale value is -the distance between two stafflines in millimeters. The other values are also in millimeters unless -they are followed by a unit (cm,in,pt). -- %%MIDI program and %%MIDI channel (or I:MIDI ..) are translated when used in a current voice -(i.e. after a V: definition). The instrument of a voice cannot be changed in the middle of a tune. -If one does so, only the last %%MIDI will be used for the whole voice. (and the earlier settings are -discarded). %%MIDI transpose is translated and has the same effect as transpose= in the clef, i.e. -only play back is transposed, not notation. In %%MIDI program=num, the number should be between 0 -and 127. Also in %%MIDI channel=num, the number is starts from zero. The midi translation supports -mapping multiple voices whith different instruments to one stave. (i.e. the resulting xml part will -have multiple instruments). This feature, though present in MusicXML is not supported by MuseScore, -nor by Finale Notepad. These programs only allow one instrument per stave. -- multiple tunes within one abc file can be converted to a set of xml files, one file per tune. - -In conjunction with xml2abc the translation from xml -> abc -> xml works for all examples -in the set from MakeMusic. The translated examples produce reasonable score when typeset with MuseScore. - ----- Usage: ---- - -When you have Python installed: -> python abc2xml.py [-h] [-m SKIP NUM] [-o DIR] [-p PFMT] [-z MODE] file1 [file2 ...] - -When you use the Win32 executable: -> abc2xml.exe [-h] [-m SKIP NUM] [-o DIR] [-p PFMT] [-z MODE] file1 [file2 ...] - -Translates all .abc files in the file list to MusicXML. Output goes to stdout unless the -o option -is given. Wildcards in file names are expanded. -Option -h prints help message with explanation of the options -Option -m skip num skips skip tunes and then reads at most num tunes. -Can be used when abc files contain multiple tunes (tune books) to select only a subset of the tunes. -The default skips nothing (skip=0) and reads 1 tune (num=1). -Option -o dir translates every .abc file to a separate .xml file with the same name -into directory dir. For example, -o. puts all xml files into the same directory where -the input files reside. -Option -p fmt sets the page format of the ouput. fmt should be a string with 7 float -values sepatated by comma's without any spaces. The values are: space, page-height, -width, and -page margin left, -right, -top, -bottom. space defines the scale of the whole score and equals the -distance between two staff lines in mm. When the -p option is omitted the values default to A4 with -10mm margins and space=1.764. All commandline values are in millimeters. -Option -z mode or --mxl mode writes compressed xml files with extention .mxl. -If mode is a or add both .xml and .mxl files will be written. If mode is r or replace only .mxl -files are written. - ----- Download ---- - -The python script: abc2xml.py-58.zip -http://wim.vree.org/svgParse/abc2xml.py-58.zip - -Stand alone win32 executable: abc2xml.exe-58.zip -http://wim.vree.org/svgParse/abc2xml.exe-58.zip - ----- ABC Syntax ---- - -ABC is recognized by a high level parser that reads the ABC syntax in a notation close to (E)BNF. -The drawback of this approach is that many dialects of ABC will cause sytax errors. -In addition, the high level parser implementation in pure python (pyparsing.py) is slow. -The pyparsing library is included (as a single python file) in abc2xml.py-58.zip \ No newline at end of file diff --git a/abc2xml/abc2xml.py b/abc2xml/abc2xml.py index c4bfc32..3f3784b 100644 --- a/abc2xml/abc2xml.py +++ b/abc2xml/abc2xml.py @@ -1,1747 +1,2239 @@ -# coding=latin-1 -''' -Copyright (C) 2012: Willem G. Vree -Contributions: Nils Liberg, Nicolas Froment, Norman Schmidt, Reinier Maliepaard, Martin Tarenskeen - -This program is free software; you can redistribute it and/or modify it under the terms of the -GNU General Public License as published by the Free Software Foundation; either version 2 of -the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -See the GNU General Public License for more details. . -''' - -from pyparsing import Word, OneOrMore, Optional, Literal, NotAny, MatchFirst -from pyparsing import Group, oneOf, Suppress, ZeroOrMore, Combine, FollowedBy -from pyparsing import srange, CharsNotIn, StringEnd, LineEnd, White, Regex -from pyparsing import nums, alphas, alphanums, ParseException, Forward -try: import xml.etree.cElementTree as E -except: import xml.etree.ElementTree as E -import types, sys, os, re, datetime - -VERSION = 58 - -def info (s, warn=1): - x = (warn and '-- ' or '') + s - try: sys.stderr.write (x + '\n') - except: sys.stderr.write (repr (x) + '\n') - -def abc_grammar (): # header, voice and lyrics grammar for ABC - b1 = Word (u"-,'<>\u2019#", exact=1) # catch misplaced chars in chords - - #----------------------------------------------------------------- - # ABC header (fld_text elements are matched later with reg. epr's) - #----------------------------------------------------------------- - - number = Word (nums).setParseAction (lambda t: int (t[0])) - field_str = Regex (r'(?:\\.|[^]\\])*') # match anything until end of field, skip escaped \] - field_str.setParseAction (lambda t: t[0].strip ()) # and strip spacing - - userdef_symbol = Word (srange ('[H-Wh-w~]'), exact=1) - fieldId = oneOf ('K L M Q P I T C O A Z N G H R B D F S E r') # info fields - X_field = Literal ('X') + Suppress (':') + number + field_str - U_field = Literal ('U') + Suppress (':') + userdef_symbol + Suppress ('=') + field_str - V_field = Literal ('V') + Suppress (':') + Word (alphanums + '_') + field_str - inf_fld = fieldId + Suppress (':') + field_str - ifield = Suppress ('[') + (X_field | U_field | V_field | inf_fld) + Suppress (']') - abc_header = OneOrMore (ifield) + StringEnd () - - #--------------------------------------------------------------------------------- - # I:score with recursive part groups and {* grand staff marker - #--------------------------------------------------------------------------------- - - voiceId = Suppress (Optional ('*')) + Word (alphanums + '_') - voice_gr = Suppress ('(') + OneOrMore (voiceId | Suppress ('|')) + Suppress (')') - simple_part = voiceId | voice_gr | Suppress ('|') - grand_staff = oneOf ('{* {') + OneOrMore (simple_part) + Suppress ('}') - part = Forward () - part_seq = OneOrMore (part | Suppress ('|')) - brace_gr = Suppress ('{') + part_seq + Suppress ('}') - bracket_gr = Suppress ('[') + part_seq + Suppress ('\]') # closing brackets are escaped by splitHeaderVoices - part << MatchFirst (simple_part | grand_staff | brace_gr | bracket_gr | Suppress ('|')) - abc_scoredef = Suppress (oneOf ('staves score')) + OneOrMore (part) - - #--------------------------------------------------------------------------------- - # ABC voice (not white space sensitive, beams detected in note/rest parse actions) - #--------------------------------------------------------------------------------- - - inline_field = Suppress ('[') + (inf_fld | U_field | V_field) + Suppress (']') - - note_length = Optional (number, 1) + Group (ZeroOrMore ('/')) + Optional (number, 2) - octaveHigh = OneOrMore ("'").setParseAction (lambda t: len(t)) - octaveLow = OneOrMore (',').setParseAction (lambda t: -len(t)) - octave = octaveHigh | octaveLow - - basenote = oneOf ('C D E F G A B c d e f g a b y') # includes spacer for parse efficiency - accidental = oneOf ('^^ __ ^ _ =') - rest_sym = oneOf ('x X z Z') - slur_beg = oneOf ('( .(') + ~Word (nums) # no tuplet_start - slur_ends = OneOrMore (oneOf (') .)')) - - long_decoration = Combine (oneOf ('! +') + CharsNotIn ('!+ \n') + oneOf ('! +')) - staccato = Literal ('.') + ~Literal ('|') # avoid dotted barline - decoration = staccato | userdef_symbol | long_decoration | slur_beg - decorations = OneOrMore (decoration) - staff_decos = decorations + ~oneOf (': | [|] []') - - tie = oneOf ('.- -') - rest = Optional (accidental) + rest_sym + note_length - pitch = Optional (accidental) + basenote + Optional (octave, 0) - note = pitch + note_length + Optional (tie) + Optional (slur_ends) - chord_note = Optional (decorations) + pitch + note_length + Optional (tie) + Optional (slur_ends) - chord_notes = OneOrMore (chord_note | rest | b1) - grace_notes = Forward () - chord = Suppress ('[') + OneOrMore (chord_notes | grace_notes) + Suppress (']') + note_length + Optional (tie) + Optional (slur_ends) - stem = note | chord | rest - - broken = Combine (OneOrMore ('<') | OneOrMore ('>')) - - tuplet_num = Suppress ('(') + number - tuplet_into = Suppress (':') + Optional (number, 0) - tuplet_notes = Suppress (':') + Optional (number, 0) - tuplet_start = tuplet_num + Optional (tuplet_into + Optional (tuplet_notes)) - - acciaccatura = Literal ('/') - grace_stem = Optional (decorations) + stem - grace_notes << Group (Suppress ('{') + Optional (acciaccatura) + OneOrMore (grace_stem) + Suppress ('}')) - - text_expression = Optional (oneOf ('^ _ < > @'), '^') + Optional (CharsNotIn ('"'), "") - chord_accidental = oneOf ('# b =') - triad = oneOf ('ma Maj maj M mi min m aug dim o + -') - seventh = oneOf ('7 ma7 Maj7 M7 maj7 mi7 m7 dim7 o7 -7 aug7 +7 m7b5 mi7b5') - sixth = oneOf ('6 ma6 M6 m6 mi6') - ninth = oneOf ('9 ma9 M9 maj9 Maj9 mi9 m9') - elevn = oneOf ('11 ma11 M11 maj11 Maj11 mi m11') - suspended = oneOf ('sus sus2 sus4') - chord_degree = Combine (Optional (chord_accidental) + oneOf ('2 4 5 6 7 9 11 13')) - chord_kind = Optional (seventh | sixth | ninth | elevn | triad, '_') + Optional (suspended) - chord_root = oneOf ('C D E F G A B') + Optional (chord_accidental) - chord_bass = oneOf ('C D E F G A B') + Optional (chord_accidental) # needs a different parse action - chordsym = chord_root + chord_kind + ZeroOrMore (chord_degree) + Optional (Suppress ('/') + chord_bass) - chord_sym = chordsym + Optional (Literal ('(') + CharsNotIn (')') + Literal (')')).suppress () - chord_or_text = Suppress ('"') + (chord_sym ^ text_expression) + Suppress ('"') - - volta_nums = Optional ('[').suppress () + Combine (Word (nums) + ZeroOrMore (oneOf (', -') + Word (nums))) - volta_text = Literal ('[').suppress () + Regex (r'"[^"]+"') - volta = volta_nums | volta_text - invisible_barline = oneOf ('[|] []') - dashed_barline = oneOf (': .|') - double_rep = Literal (':') + FollowedBy (':') # otherwise ambiguity with dashed barline - voice_overlay = Combine (OneOrMore ('&')) - bare_volta = FollowedBy (Literal ('[') + Word (nums)) # no barline, but volta follows (volta is parsed in next measure) - bar_left = (oneOf ('[|: |: [: :') + Optional (volta)) | Optional ('|').suppress () + volta | oneOf ('| [|') - bars = ZeroOrMore (':') + ZeroOrMore ('[') + OneOrMore (oneOf ('| ]')) - bar_right = Optional (decorations) + (invisible_barline | double_rep | Combine (bars) | dashed_barline | voice_overlay | bare_volta) - - errors = ~bar_right + Optional (Word (' \n')) + CharsNotIn (':&|', exact=1) - linebreak = Literal ('$') | ~decorations + Literal ('!') # no need for I:linebreak !!! - element = inline_field | broken | staff_decos | stem | chord_or_text | grace_notes | tuplet_start | linebreak | errors - measure = Group (ZeroOrMore (inline_field) + Optional (bar_left) + ZeroOrMore (element) + bar_right + Optional (linebreak)) - noBarMeasure = Group (ZeroOrMore (inline_field) + Optional (bar_left) + OneOrMore (element) + Optional (linebreak)) - abc_voice = ZeroOrMore (measure) + Optional (noBarMeasure | Group (bar_left)) + ZeroOrMore (inline_field).suppress () + StringEnd () - - #---------------------------------------- - # ABC lyric lines (white space sensitive) - #---------------------------------------- - - skip_note = oneOf ('* - ~') - extend_note = Literal ('_') - measure_end = Literal ('|') - syl_chars = CharsNotIn ('*~-_| \t\n') - white = Word (' \t') - syllable = Combine (Optional ('~') + syl_chars + ZeroOrMore (Literal ('~') + syl_chars)) + Optional ('-') - lyr_elem = (syllable | skip_note | extend_note | measure_end) + Optional (white).suppress () - lyr_head = (Literal ('w:') + Optional (white)).suppress () - lyr_line = Group (lyr_head + ZeroOrMore (lyr_elem) + LineEnd ().suppress ()) - - #---------------------------------------------------------------- - # Parse actions to convert all relevant results into an abstract - # syntax tree where all tree nodes are instances of pObj - #---------------------------------------------------------------- - - ifield.setParseAction (lambda t: pObj ('field', t)) - grand_staff.setParseAction (lambda t: pObj ('grand', t, 1)) # 1 = keep ordered list of results - brace_gr.setParseAction (lambda t: pObj ('bracegr', t, 1)) - bracket_gr.setParseAction (lambda t: pObj ('bracketgr', t, 1)) - voice_gr.setParseAction (lambda t: pObj ('voicegr', t, 1)) - voiceId.setParseAction (lambda t: pObj ('vid', t, 1)) - abc_scoredef.setParseAction (lambda t: pObj ('score', t, 1)) - note_length.setParseAction (lambda t: pObj ('dur', (t[0], (t[2] << len (t[1])) >> 1))) - chordsym.setParseAction (lambda t: pObj ('chordsym', t)) - chord_root.setParseAction (lambda t: pObj ('root', t)) - chord_kind.setParseAction (lambda t: pObj ('kind', t)) - chord_degree.setParseAction (lambda t: pObj ('degree', t)) - chord_bass.setParseAction (lambda t: pObj ('bass', t)) - text_expression.setParseAction (lambda t: pObj ('text', t)) - inline_field.setParseAction (lambda t: pObj ('inline', t)) - grace_notes.setParseAction (doGrace) # (lambda t: pObj ('grace', t)) - acciaccatura.setParseAction (lambda t: pObj ('accia', t)) - note.setParseAction (noteActn) - chord_note.setParseAction (noteActn) - rest.setParseAction (restActn) - decorations.setParseAction (lambda t: pObj ('deco', t)) - slur_ends.setParseAction (lambda t: pObj ('slurs', t)) - chord.setParseAction (lambda t: pObj ('chord', t)) - tie.setParseAction (lambda t: pObj ('tie', t)) - pitch.setParseAction (lambda t: pObj ('pitch', t)) - bare_volta.setParseAction (lambda t: ['|']) # return barline that user forgot - dashed_barline.setParseAction (lambda t: ['.|']) - bar_right.setParseAction (lambda t: pObj ('rbar', t)) - bar_left.setParseAction (lambda t: pObj ('lbar', t)) - broken.setParseAction (lambda t: pObj ('broken', t)) - tuplet_start.setParseAction (lambda t: pObj ('tup', t)) - linebreak.setParseAction (lambda t: pObj ('linebrk', t)) - measure.setParseAction (doMaat) - noBarMeasure.setParseAction (doMaat) - syllable.setParseAction (lambda t: pObj ('syl', t)) - skip_note.setParseAction (lambda t: pObj ('skip', t)) - extend_note.setParseAction (lambda t: pObj ('ext', t)) - measure_end.setParseAction (lambda t: pObj ('sbar', t)) - b1.setParseAction (errorWarn) - errors.setParseAction (errorWarn) - lyr_block = OneOrMore (lyr_line).leaveWhitespace () # after leaveWhiteSpace no more parse actions can be set!! - - return abc_header, abc_voice, lyr_block, abc_scoredef - -class pObj (object): # every relevant parse result is converted into a pObj - def __init__ (s, name, t, seq=0): # t = list of nested parse results - s.name = name # name uniqueliy identifies this pObj - rest = [] # collect parse results that are not a pObj - attrs = {} # new attributes - for x in t: # nested pObj's become attributes of this pObj - if type (x) == pObj: - attrs [x.name] = attrs.get (x.name, []) + [x] - else: - rest.append (x) # collect non-pObj's (mostly literals) - for name, xs in attrs.items (): - if len (xs) == 1: xs = xs[0] # only list if more then one pObj - setattr (s, name, xs) # create the new attributes - s.t = rest # all nested non-pObj's (mostly literals) - s.objs = seq and t or [] # for nested ordered (lyric) pObj's - - def __repr__ (s): # make a nice string representation of a pObj - r = [] - for nm in dir (s): - if nm.startswith ('_'): continue # skip build in attributes - elif nm == 'name': continue # redundant - else: - x = getattr (s, nm) - if not x: continue # s.t may be empty (list of non-pObj's) - if type (x) == types.ListType: r.extend (x) - else: r.append (x) - xs = [] - for x in r: # recursively call __repr__ and convert all strings to latin-1 - if isinstance (x, types.StringTypes): - try: xs.append (x.encode ('latin-1')) - except: xs.append (repr (x)) # string -> no recursion - else: xs.append (repr (x)) # pObj -> recursive call - return '(' + s.name + ' ' +','.join (xs) + ')' - -global prevloc # global to remember previous match position of a note/rest -prevloc = 0 -def detectBeamBreak (line, loc, t): - global prevloc # location in string 'line' of previous note match - xs = line[prevloc:loc+1] # string between previous and current note match - xs = xs.lstrip () # first note match starts on a space! - prevloc = loc # location in string 'line' of current note match - b = pObj ('bbrk', [' ' in xs]) # space somewhere between two notes -> beambreak - t.insert (0, b) # insert beambreak as a nested parse result - -def noteActn (line, loc, t): # detect beambreak between previous and current note/rest - if 'y' in t[0].t: return [] # discard spacer - detectBeamBreak (line, loc, t) # adds beambreak to parse result t as side effect - return pObj ('note', t) - -def restActn (line, loc, t): # detect beambreak between previous and current note/rest - detectBeamBreak (line, loc, t) # adds beambreak to parse result t as side effect - return pObj ('rest', t) - -def errorWarn (line, loc, t): # warning for misplaced symbols and skip them - info ('**misplaced symbol: %s' % t[0], warn=0) - lineCopy = line [:] - if loc > 40: - lineCopy = line [loc - 40: loc + 40] - loc = 40 - info (lineCopy.replace ('\n', ' '), warn=0) - info (loc * '-' + '^', warn=0) - return [] - -#------------------------------------------------------------- -# transformations of a measure (called by parse action doMaat) -#------------------------------------------------------------- - -def simplify (a, b): # divide a and b by their greatest common divisor - x, y = a, b - while b: a, b = b, a % b - return x / a, y / a - -def doBroken (prev, brk, x): - if not prev: info ('error in broken rhythm: %s' % x); return # no changes - nom1, den1 = prev.dur.t # duration of first note/chord - nom2, den2 = x.dur.t # duration of second note/chord - if brk == '>': - nom1, den1 = simplify (3 * nom1, 2 * den1) - nom2, den2 = simplify (1 * nom2, 2 * den2) - elif brk == '<': - nom1, den1 = simplify (1 * nom1, 2 * den1) - nom2, den2 = simplify (3 * nom2, 2 * den2) - elif brk == '>>': - nom1, den1 = simplify (7 * nom1, 4 * den1) - nom2, den2 = simplify (1 * nom2, 4 * den2) - elif brk == '<<': - nom1, den1 = simplify (1 * nom1, 4 * den1) - nom2, den2 = simplify (7 * nom2, 4 * den2) - else: return # give up - prev.dur.t = nom1, den1 # change duration of previous note/chord - x.dur.t = nom2, den2 # and current note/chord - -def convertBroken (t): # convert broken rhythms to normal note durations - prev = None # the last note/chord before the broken symbol - brk = '' # the broken symbol - remove = [] # indexes to broken symbols (to be deleted) in measure - for i, x in enumerate (t): # scan all elements in measure - if x.name == 'note' or x.name == 'chord' or x.name == 'rest': - if brk: # a broken symbol was encountered before - doBroken (prev, brk, x) # change duration previous note/chord/rest and current one - brk = '' - else: - prev = x # remember the last note/chord/rest - elif x.name == 'broken': - brk = x.t[0] # remember the broken symbol (=string) - remove.insert (0, i) # and its index, highest index first - for i in remove: del t[i] # delete broken symbols from high to low - -def convertChord (t): # convert chord to sequence of notes in musicXml-style - ins = [] - for i, x in enumerate (t): - if x.name == 'chord': - if hasattr (x, 'rest') and not hasattr (x, 'note'): # chords containing only rests - if type (x.rest) == types.ListType: x.rest = x.rest[0] # more rests == one rest - ins.insert (0, (i, [x.rest])) # just output a single rest, no chord - continue - num1, den1 = x.dur.t # chord duration - tie = getattr (x, 'tie', None) # chord tie - slurs = getattr (x, 'slurs', []) # slur endings - deco = getattr (x, 'deco', []) # chord decorations - if type (x.note) != types.ListType: x.note = [x.note] # when chord has only one note ... - for j, nt in enumerate (x.note): # all notes of the chord - num2, den2 = nt.dur.t # note duration * chord duration - nt.dur.t = simplify (num1 * num2, den1 * den2) - if tie: nt.tie = tie # tie on all chord notes - if j == 0 and deco: nt.deco = deco # decorations only on first chord note - if j == 0 and slurs: nt.slurs = slurs # slur endings only on first chord note - if j > 0: nt.chord = pObj ('chord', [1]) # label all but first as chord notes - else: # remember all pitches of the chord in the first note - pitches = [n.pitch for n in x.note] # to implement conversion of erroneous ties to slurs - nt.pitches = pObj ('pitches', pitches) - ins.insert (0, (i, x.note)) # high index first - for i, notes in ins: # insert from high to low - for nt in reversed (notes): - t.insert (i+1, nt) # insert chord notes after chord - del t[i] # remove chord itself - -def doMaat (t): # t is a Group() result -> the measure is in t[0] - convertBroken (t[0]) # remove all broken rhythms and convert to normal durations - convertChord (t[0]) # replace chords by note sequences in musicXML style - -def doGrace (t): # t is a Group() result -> the grace sequence is in t[0] - convertChord (t[0]) # a grace sequence may have chords - for nt in t[0]: # flag all notes within the grace sequence - if nt.name == 'note': nt.grace = 1 # set grace attribute - return t[0] # ungroup the parse result -#-------------------- -# musicXML generation -#---------------------------------- - -def compChordTab (): # avoid some typing work: returns mapping constant {ABC chordsyms -> musicXML kind} - maj, min, aug, dim, dom, ch7, ch6, ch9, ch11, hd = 'major minor augmented diminished dominant -seventh -sixth -ninth -11th half-diminished'.split () - triad = zip ('ma Maj maj M mi min m aug dim o + -'.split (), [maj, maj, maj, maj, min, min, min, aug, dim, dim, aug, min]) - seventh = zip ('7 ma7 Maj7 M7 maj7 mi7 m7 dim7 o7 -7 aug7 +7 m7b5 mi7b5'.split (), - [dom, maj+ch7, maj+ch7, maj+ch7, maj+ch7, min+ch7, min+ch7, dim+ch7, dim+ch7, min+ch7, aug+ch7, aug+ch7, hd, hd]) - sixth = zip ('6 ma6 M6 mi6 m6'.split (), [maj+ch6, maj+ch6, maj+ch6, min+ch6, min+ch6]) - ninth = zip ('9 ma9 M9 maj9 Maj9 mi9 m9'.split (), [dom+ch9, maj+ch9, maj+ch9, maj+ch9, maj+ch9, min+ch9, min+ch9]) - elevn = zip ('11 ma11 M11 maj11 Maj11 mi11 m11'.split (), [dom+ch11, maj+ch11, maj+ch11, maj+ch11, maj+ch11, min+ch11, min+ch11]) - return dict (triad + seventh + sixth + ninth + elevn) - -def addElem (parent, child, level): - indent = 2 - chldrn = parent.getchildren () - if chldrn: - chldrn[-1].tail += indent * ' ' - else: - parent.text = '\n' + level * indent * ' ' - parent.append (child) - child.tail = '\n' + (level-1) * indent * ' ' - -def addElemT (parent, tag, text, level): - e = E.Element (tag) - e.text = text - addElem (parent, e, level) - -def mkTmod (tmnum, tmden, lev): - tmod = E.Element ('time-modification') - addElemT (tmod, 'actual-notes', str (tmnum), lev + 1) - addElemT (tmod, 'normal-notes', str (tmden), lev + 1) - return tmod - -def addDirection (parent, elem, lev, gstaff, subelms=[], placement='below', cue_on=0): - dir = E.Element ('direction', placement=placement) - addElem (parent, dir, lev) - typ = E.Element ('direction-type') - addElem (dir, typ, lev + 1) - addElem (typ, elem, lev + 2) - for subel in subelms: addElem (elem, subel, lev + 3) - if cue_on: addElem (dir, E.Element ('level', size='cue'), lev + 1) - if gstaff: addElemT (dir, 'staff', str (gstaff), lev + 1) - return dir - -def removeElems (root_elem, parent_str, elem_str): - for p in root_elem.findall (parent_str): - e = p.find (elem_str) - if e != None: p.remove (e) - -def alignLyr (vce, lyrs): - empty_el = pObj ('leeg', '*') - for k, lyr in enumerate (lyrs): # lyr = one full line of lyrics - i = 0 # syl counter - for msre in vce: # reiterate the voice block for each lyrics line - for elem in msre: - if elem.name == 'note' and not (hasattr (elem, 'chord') or hasattr (elem, 'grace')): - if i >= len (lyr): lr = empty_el - else: lr = lyr [i] - elem.objs.append (lr) - if lr.name != 'sbar': i += 1 - if i < len (lyr) and lyr[i].name == 'sbar': i += 1 - return vce - -slur_move = re.compile (r'(?<][<>]?)(\)+)') # (? I: - x2 = r1.sub ('', x) # remove comment - while x2.endswith ('*'): x2 = x2[:-1] # remove old syntax for right adjusting - if not x2: continue # empty line - if x2[:2] == 'W:': continue # skip W: lyrics - if x2[:2] == 'w:' and xs[-1][-1] == '\\': - xs[-1] = xs[-1][:-1] # ignore line continuation before lyrics line - ro = r2.match (x2) - if ro: # field -> inline_field, escape all ']' - if x2[-1] == '\\': x2 = x2[:-1] # ignore continuation after field line - x2 = '[' + x2.replace (']',r'\]') + ']' - if x2[:2] == '+:': # new style continuation - xs[-1] += x2[2:] - elif xs and xs[-1][-1] == '\\': # old style continuation - xs[-1] = xs[-1][:-1] + x2 - else: # skip lines (except I:) until first X: - if x.startswith ('X:'): - if nx == 1: break # second tune starts without an empty line !! - nx = 1 # start of first tune - if nx == 1 or x.startswith ('I:'): - xs.append (x2) - if xs and xs[-1][-1] == '\\': # nothing left to continue with, remove last continuation - xs[-1] = xs[-1][:-1] - - r1 = re.compile (r'\[[A-Z]:(\\.|[^]\\])*\]') # inline field with escaped ']' - r2 = re.compile (r'\[K:') # start of K: field - r3 = re.compile (r'\[V:|\[I:MIDI') # start of V: field or midi field - fields, voices, b = [], [], 0 - for i, x in enumerate (xs): - n = len (r1.sub ('', x)) # remove all inline fields - if n > 0: b = 1; break # real abc present -> end of header - if r2.search (x): # start of K: field - fields.append (x) - i += 1; b = 1 - break # first K: field -> end of header - if r3.search (x): # start of V: field - voices.append (x) - else: - fields.append (x) - if b: voices += xs[i:] - else: voices += [] # tune has only header fields - header = '\n'.join (fields) - abctext = '\n'.join (voices) - - xs = abctext.split ('[V:') - if len (xs) == 1: abctext = '[V:1]' + abctext # abc has no voice defs at all - elif r1.sub ('', xs[0]).strip (): # remove inline fields from starting text, if any - abctext = '[V:1]' + abctext # abc with voices has no V: at start - - r1 = re.compile (r'\[V:\s*(\S*)[ \]]') # get voice id from V: field (skip spaces betwee V: and ID) - vmap = {} # {voice id -> [voice abc string]} - vorder = {} # mark document order of voices - xs = re.split (r'(\[V:[^]]*\])', abctext) # split on every V-field (V-fields included in split result list) - if len (xs) == 1: raise (Exception ('bugs ...')) - else: - header += xs[0] # xs[0] = text between K: and first V:, normally empty, but we put it in the header - i = 1 - while i < len (xs): # xs = ['', V-field, voice abc, V-field, voice abc, ...] - vce, abc = xs[i:i+2] - id = r1.search (vce).group (1) # get voice ID from V-field - vmap[id] = vmap.get (id, []) + [vce, abc] # collect abc-text for each voice id (include V-fields) - if id not in vorder: vorder [id] = i # store document order of first occurrence of voice id - i += 2 - voices = [] - ixs = sorted ([(i, id) for id, i in vorder.items ()]) # restore document order of voices - for i, id in ixs: - voice = ''.join (vmap [id]) # all abc of one voice - xs = re.split (r'((?:\nw:[^\n]*)+)', voice) # split voice into voice-lyrics blocks - if len (xs) == 1: # no lyrics - voice = fixSlurs (xs[0]) # put slurs right after the notes - vce_lyr = [[voice, '']] - else: - if xs[-1].strip () != '': xs.append ('w:') # last block had no lyrics - vce_lyr = [] # [[voice, lyrics],[],...] list of voice-lyrics blocks - for k in range (0, len (xs) - 1, 2): - voice, lyrics = xs [k:k+2] - voice = fixSlurs (voice) # put slurs right after the notes - vce_lyr.append ((voice, lyrics)) - voices.append ((id, vce_lyr)) - return header, voices - -def mergeMeasure (m1, m2, slur_offset, voice_offset, is_grand=0): - slurs = m2.findall ('note/notations/slur') - for slr in slurs: - slrnum = int (slr.get ('number')) + slur_offset - slr.set ('number', str (slrnum)) # make unique slurnums in m2 - vs = m2.findall ('note/voice') # set all voice number elements in m2 - for v in vs: v.text = str (voice_offset + int (v.text)) - ls = m1.findall ('note/lyric') # all lyric elements in m1 - lnum_max = max ([int (l.get ('number')) for l in ls] + [0]) # highest lyric number in m1 - ls = m2.findall ('note/lyric') # update lyric elements in m2 - for el in ls: - n = int (el.get ('number')) - el.set ('number', str (n + lnum_max)) - ns = m1.findall ('note') # determine the total duration of m1, subtract all backups - dur1 = sum (int (n.find ('duration').text) for n in ns - if n.find ('grace') == None and n.find ('chord') == None) - dur1 -= sum (int (b.text) for b in m1.findall ('backup/duration')) - nns, es = 0, [] # nns = number of real notes in m2 - for e in m2.getchildren (): # scan all elements of m2 - if e.tag == 'attributes': - if not is_grand: continue # no attribute merging for normal voices - else: nns += 1 # but we do merge (clef) attributes for a grand staff - if e.tag == 'print': continue - if e.tag == 'note' and (mxm.gmwr or e.find ('rest') == None): nns += 1 - es.append (e) # buffer elements to be merged - if nns > 0: # only merge if m2 contains any real notes - if dur1 > 0: # only insert backup if duration of m1 > 0 - b = E.Element ('backup') - addElem (m1, b, level=3) - addElemT (b, 'duration', str (dur1), level=4) - for e in es: addElem (m1, e, level=3) # merge buffered elements of m2 - -def mergePartList (parts, is_grand=0): # merge parts, make grand staff when is_grand true - - def delAttrs (part): # for the time being we only keep clef attributes - xs = [(m, e) for m in part.findall ('measure') for e in m.findall ('attributes')] - for m, e in xs: - for c in e.getchildren (): - if c.tag == 'clef': continue # keep clef attribute - e.remove (c) # delete all other attrinutes for higher staff numbers - if len (e.getchildren ()) == 0: m.remove (e) # remove empty attributes element - - p1 = parts[0] - for p2 in parts[1:]: - if is_grand: delAttrs (p2) # delete all attributes except clef - for i in range (len (p1) + 1, len (p2) + 1): # second part longer than first one - maat = E.Element ('measure', number = str(i)) # append empty measures - addElem (p1, maat, 2) - slurs = p1.findall ('measure/note/notations/slur') # find highest slur num in first part - slur_max = max ([int (slr.get ('number')) for slr in slurs] + [0]) - vs = p1.findall ('measure/note/voice') # all voice number elements in first part - vnum_max = max ([int (v.text) for v in vs] + [0]) # highest voice number in first part - for im, m2 in enumerate (p2.findall ('measure')): # merge all measures of p2 into p1 - mergeMeasure (p1[im], m2, slur_max, vnum_max, is_grand) # may change slur numbers in p1 - return p1 - -def mergeParts (parts, vids, staves, is_grand=0): - if not staves: return parts, vids # no voice mapping - partsnew, vidsnew = [], [] - for voice_ids in staves: - pixs = [] - for vid in voice_ids: - if vid in vids: pixs.append (vids.index (vid)) - else: info ('score partname %s does not exist' % vid) - if pixs: - xparts = [parts[pix] for pix in pixs] - if len (xparts) > 1: mergedpart = mergePartList (xparts, is_grand) - else: mergedpart = xparts [0] - partsnew.append (mergedpart) - vidsnew.append (vids [pixs[0]]) - return partsnew, vidsnew - -def mergePartMeasure (part, msre, ovrlaynum): # merge msre into last measure of part, only for overlays - slurs = part.findall ('measure/note/notations/slur') # find highest slur num in part - slur_max = max ([int (slr.get ('number')) for slr in slurs] + [0]) - last_msre = part.getchildren ()[-1] # last measure in part - mergeMeasure (last_msre, msre, slur_max, ovrlaynum) # voice offset = s.overlayVNum - -def setFristVoiceNameFromGroup (vids, vdefs): # vids = [vid], vdef = {vid -> (name, subname, voicedef)} - vids = [v for v in vids if v in vdefs] # only consider defined voices - if not vids: return vdefs - vid0 = vids [0] # first vid of the group - _, _, vdef0 = vdefs [vid0] # keep de voice definition (vdef0) when renaming vid0 - for vid in vids: - nm, snm, vdef = vdefs [vid] - if nm: # first non empty name encountered will become - vdefs [vid0] = nm, snm, vdef0 # name of merged group == name of first voice in group (vid0) - break - return vdefs - -def mkGrand (p, vdefs): # transform parse subtree into list needed for s.grands - xs = [] - for i, x in enumerate (p.objs): # changing p.objs [i] alters the tree. changing x has no effect on the tree. - if type (x) == pObj: - us = mkGrand (x, vdefs) # first get transformation results of current pObj - if x.name == 'grand': # x.objs contains ordered list of nested parse results within x - vids = [y.objs[0] for y in x.objs[1:]] # the voice ids in the grand staff - nms = [vdefs [u][0] for u in vids if u in vdefs] # the names of those voices - accept = sum ([1 for nm in nms if nm]) == 1 # accept as grand staff when only one of the voices has a name - if accept or us[0] == '{*': - xs.append (us[1:]) # append voice ids as a list (discard first item '{' or '{*') - vdefs = setFristVoiceNameFromGroup (vids, vdefs) - p.objs [i] = x.objs[1] # replace voices by first one in the grand group (this modifies the parse tree) - else: - xs.extend (us[1:]) # extend current result with all voice ids of rejected grand staff - else: xs.extend (us) # extend current result with transformed pObj - else: xs.append (p.t[0]) # append the non pObj (== voice id string) - return xs - -def mkStaves (p, vdefs): # transform parse tree into list needed for s.staves - xs = [] - for i, x in enumerate (p.objs): # structure and comments identical to mkGrand - if type (x) == pObj: - us = mkStaves (x, vdefs) - if x.name == 'voicegr': - xs.append (us) - vids = [y.objs[0] for y in x.objs] - vdefs = setFristVoiceNameFromGroup (vids, vdefs) - p.objs [i] = x.objs[0] - else: - xs.extend (us) - else: - if p.t[0] not in '{*': xs.append (p.t[0]) - return xs - -def mkGroups (p): # transform parse tree into list needed for s.groups - xs = [] - for x in p.objs: - if type (x) == pObj: - if x.name == 'vid': xs.extend (mkGroups (x)) - elif x.name == 'bracketgr': xs.extend (['['] + mkGroups (x) + [']']) - elif x.name == 'bracegr': xs.extend (['{'] + mkGroups (x) + ['}']) - else: xs.extend (mkGroups (x) + ['}']) # x.name == 'grand' == rejected grand staff - else: - xs.append (p.t[0]) - return xs - -class MusicXml: - typeMap = {1:'long', 2:'breve', 4:'whole', 8:'half', 16:'quarter', 32:'eighth', 64:'16th', 128:'32nd', 256:'64th'} - dynaMap = {'p':1,'pp':1,'ppp':1,'f':1,'ff':1,'fff':1,'mp':1,'mf':1,'sfz':1} - wedgeMap = {'>(':1, '>)':1, '<(':1,'<)':1,'crescendo(':1,'crescendo)':1,'diminuendo(':1,'diminuendo)':1} - artMap = {'.':'staccato','>':'accent','accent':'accent','wedge':'staccatissimo','tenuto':'tenuto'} - ornMap = {'trill':'trill-mark','T':'trill-mark','turn':'turn','uppermordent':'inverted-mordent','lowermordent':'mordent', - 'pralltriller':'inverted-mordent','mordent':'mordent','turn':'turn','invertedturn':'inverted-turn'} - tecMap = {'upbow':'up-bow', 'downbow':'down-bow'} - capoMap = {'fine':('Fine','fine','yes'), 'D.S.':('D.S.','dalsegno','segno'), 'D.C.':('D.C.','dacapo','yes'),'dacapo':('D.C.','dacapo','yes'), - 'dacoda':('To Coda','tocoda','coda'), 'coda':('coda','coda','coda'), 'segno':('segno','segno','segno')} - sharpness = ['Fb', 'Cb','Gb','Db','Ab','Eb','Bb','F','C','G','D','A', 'E', 'B', 'F#','C#','G#','D#','A#','E#','B#'] - offTab = {'maj':8, 'm':11, 'min':11, 'mix':9, 'dor':10, 'phr':12, 'lyd':7, 'loc':13} - modTab = {'maj':'major', 'm':'minor', 'min':'minor', 'mix':'mixolydian', 'dor':'dorian', 'phr':'phrygian', 'lyd':'lydian', 'loc':'locrian'} - clefMap = { 'alto1':('C','1'), 'alto2':('C','2'), 'alto':('C','3'), 'alto4':('C','4'), 'tenor':('C','4'), - 'bass3':('F','3'), 'bass':('F','4'), 'treble':('G','2'), 'perc':('percussion',''), 'none':('','')} - clefLineMap = {'B':'treble', 'G':'alto1', 'E':'alto2', 'C':'alto', 'A':'tenor', 'F':'bass3', 'D':'bass'} - alterTab = {'=':'0', '_':'-1', '__':'-2', '^':'1', '^^':'2'} - accTab = {'=':'natural', '_':'flat', '__':'flat-flat', '^':'sharp', '^^':'sharp-sharp'} - chordTab = compChordTab () - uSyms = {'~':'roll', 'H':'fermata','L':'>','M':'lowermordent','O':'coda', - 'P':'uppermordent','S':'segno','T':'trill','u':'upbow','v':'downbow'} - pageFmtDef = [1.764,297,210,10,10,10,10] # the MuseScore page formatting defaults for A4 - creditTab = {'O':'origin', 'A':'area', 'Z':'transcription', 'N':'notes', 'G':'group', 'H':'history', 'R':'rhythm', - 'B':'book', 'D':'discography', 'F':'fileurl', 'S':'source'} - - def __init__ (s): - s.pageFmtCmd = [] # set by command line option -p - s.gmwr = 0 # set by command line option -r - s.reset () - def reset (s): - s.divisions = 120 # xml duration of 1/4 note - s.ties = {} # {abc pitch tuple -> alteration} for all open ties - s.slurstack = [] # stack of open slur numbers - s.slurbeg = 0 # number of slurs to start (when slurs are detected at element-level) - s.tmnum = 0 # time modification, numerator - s.tmden = 0 # time modification, denominator - s.ntup = 0 # number of tuplet notes remaining - s.tupnts = [] # all tuplet modifiers with corresp. durations: [(duration, modifier), ...] - s.irrtup = 0 # 1 if an irregular tuplet - s.ntype = '' # the normal-type of a tuplet (== duration type of a normal tuplet note) - s.unitL = (1, 8) # default unit length - s.unitLcur = (1, 8) # unit length of current voice - s.keyAlts = {} # alterations implied by key - s.msreAlts = {} # temporarily alterations - s.curVolta = '' # open volta bracket - s.slurstack = [] # stack of open slur numbers - s.title = '' # title of music - s.creator = {} # {creator-type -> creator string} - s.credits = {} # {credit-type -> string} - s.lyrdash = {} # {lyric number -> 1 if dash between syllables} - s.usrSyms = s.uSyms # user defined symbols - s.prevNote = None # xml element of previous beamed note to correct beams (start, continue) - s.grcbbrk = False # remember any bbrk in a grace sequence - s.linebrk = 0 # 1 if next measure should start with a line break - s.bardecos = [] # barline decorations (coda, segno) that go into the next measure (MuseScore deficiency!) - s.nextdecos = [] # decorations for the next note - s.prevmsre = None # the previous measure - s.supports_tag = 0 # issue supports-tag in xml file when abc uses explicit linebreaks - s.staveDefs = [] # collected %%staves or %%score instructions from score - s.staves = [] # staves = [[voice names to be merged into one stave]] - s.groups = [] # list of merged part names with interspersed {[ and }] - s.grands = [] # [[vid1, vid2, ..], ...] voiceIds to be merged in a grand staff - s.gStaffNums = {} # map each voice id in a grand staff to a staff number - s.gNstaves = {} # map each voice id in a grand staff to total number of staves - s.pageFmtAbc = [] # formatting from abc directives - s.mdur = (4,4) # duration of one measure - s.gtrans = 0 # octave transposition (by clef) - s.midprg = ['', ''] # MIDI channel nr, program nr for the current part - s.vid = '' # abc voice id for the current part - s.gcue_on = 0 # insert tag in each note - - def mkPitch (s, acc, note, oct, lev): - nUp = note.upper () - octnum = (4 if nUp == note else 5) + int (oct) + s.gtrans - pitch = E.Element ('pitch') - addElemT (pitch, 'step', nUp, lev + 1) - alter = '' - if (note, oct) in s.ties: - tied_alter, _, vnum = s.ties [(note,oct)] # vnum = overlay voice number when tie started - if vnum == s.overlayVnum: alter = tied_alter # tied note in the same overlay -> same alteration - elif acc: - s.msreAlts [(nUp, octnum)] = s.alterTab [acc] - alter = s.alterTab [acc] # explicit notated alteration - elif (nUp, octnum) in s.msreAlts: alter = s.msreAlts [(nUp, octnum)] # temporary alteration - elif nUp in s.keyAlts: alter = s.keyAlts [nUp] # alteration implied by the key - if alter: addElemT (pitch, 'alter', alter, lev + 1) - addElemT (pitch, 'octave', str (octnum), lev + 1) - return pitch, alter - - def mkNote (s, n, lev): - nnum, nden = n.dur.t # abc dutation of note - if nden == 0: nden = 1 # occurs with illegal ABC like: "A2 1". Now interpreted as A2/1 - num, den = simplify (nnum * s.unitLcur[0], nden * s.unitLcur[1]) # normalised with unit length - if den > 64: # limit denominator to 64 - num = int (round (64 * float (num) / den)) # scale note to num/64 - num, den = simplify (max ([num, 1]), 64) # smallest num == 1 - info ('duration too small: rounded to %d/%d' % (num, den)) - if n.name == 'rest' and ('Z' in n.t or 'X' in n.t): - num, den = s.mdur # duration of one measure - dvs = (4 * s.divisions * num) / den # divisions is xml-duration of 1/4 - rdvs = dvs # real duration (will be 0 for chord/grace) - num, den = simplify (num, den * 4) # scale by 1/4 for s.typeMap - ndot = 0 - if num == 3: ndot = 1; den = den / 2 # look for dotted notes - if num == 7: ndot = 2; den = den / 4 - nt = E.Element ('note') - if getattr (n, 'grace', ''): # a grace note (and possibly a chord note) - grace = E.Element ('grace') - if s.acciatura: grace.set ('slash', 'yes'); s.acciatura = 0 - addElem (nt, grace, lev + 1) - dvs = rdvs = 0 # no (real) duration for a grace note - if den <= 16: den = 32 # not longer than 1/8 for a grace note - if s.gcue_on: # insert cue tag - cue = E.Element ('cue') - addElem (nt, cue, lev + 1) - if getattr (n, 'chord', ''): # a chord note - chord = E.Element ('chord') - addElem (nt, chord, lev + 1) - rdvs = 0 # chord notes no real duration - if rdvs and s.ntup >= 0: s.ntup -= 1 # count tuplet notes only on non-chord, non grace notes (rdvs > 0) - if den not in s.typeMap: # take the nearest smaller legal duration - info ('illegal duration %d/%d' % (nnum, nden)) - den = min (x for x in s.typeMap.keys () if x > den) - xmltype = str (s.typeMap [den]) # xml needs the note type in addition to duration - acc, step, oct = '', 'C', '0' # abc-notated pitch elements (accidental, pitch step, octave) - alter = '' # xml alteration - if n.name == 'rest': - if 'x' in n.t or 'X' in n.t: nt.set ('print-object', 'no') - rest = E.Element ('rest') - addElem (nt, rest, lev + 1) - else: - p = n.pitch.t # get pitch elements from parsed tokens - if len (p) == 3: acc, step, oct = p - else: step, oct = p - pitch, alter = s.mkPitch (acc, step, oct, lev + 1) - addElem (nt, pitch, lev + 1) - if s.ntup >= 0: # modify duration for tuplet notes - dvs = dvs * s.tmden / s.tmnum - if dvs: addElemT (nt, 'duration', str (dvs), lev + 1) # skip when dvs == 0, requirement of musicXML - inst = E.Element ('instrument', id='I-'+s.vid) # instrument id for midi - if s.midprg != ['', '']: addElem (nt, inst, lev + 1) # only add when %%midi was present - addElemT (nt, 'voice', '1', lev + 1) # default voice, for merging later - addElemT (nt, 'type', xmltype, lev + 1) # add note type - for i in range (ndot): # add dots - dot = E.Element ('dot') - addElem (nt, dot, lev + 1) - ptup = (step, oct) # pitch tuple without alteration to check for ties - tstop = ptup in s.ties and s.ties[ptup][2] == s.overlayVnum # open tie on this pitch tuple in this overlay - if acc and not tstop: addElemT (nt, 'accidental', s.accTab [acc], lev + 1) # only add accidental if note not tied - tupnotation = '' # start/stop notation element for tuplets - if s.ntup >= 0: # add time modification element for tuplet notes - tmod = mkTmod (s.tmnum, s.tmden, lev + 1) - addElem (nt, tmod, lev + 1) - if s.ntup > 0 and not s.tupnts: tupnotation = 'start' - s.tupnts.append ((rdvs, tmod)) # remember all tuplet modifiers with corresp. durations - if s.ntup == 0: # last tuplet note (and possible chord notes there after) - if rdvs: tupnotation = 'stop' # only insert notation in the real note (rdvs > 0) - s.cmpNormType (rdvs, lev + 1) # compute and/or add normal-type elements (-> s.ntype) - gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice - if gstaff: addElemT (nt, 'staff', str (gstaff), lev + 1) - s.doBeams (n, nt, den, lev + 1) - s.doNotations (n, ptup, alter, tupnotation, tstop, nt, lev + 1) - if n.objs: s.doLyr (n, nt, lev + 1) - return nt - - def cmpNormType (s, rdvs, lev): # compute the normal-type of a tuplet (only needed for Finale) - if rdvs: # the last real tuplet note (chord notes can still follow afterwards with rdvs == 0) - durs = [dur for dur, tmod in s.tupnts if dur > 0] - ndur = sum (durs) / s.tmnum # duration of the normal type - s.irrtup = any ((dur != ndur) for dur in durs) # irregular tuplet - tix = 16 * s.divisions / ndur # index in typeMap of normal-type duration - if tix in s.typeMap: - s.ntype = str (s.typeMap [tix]) # the normal-type - else: s.irrtup = 0 # give up, no normal type possible - if s.irrtup: # only add normal-type for irregular tuplets - for dur, tmod in s.tupnts: # add normal-type to all modifiers - addElemT (tmod, 'normal-type', s.ntype, lev + 1) - s.tupnts = [] # reset the tuplet buffer - - def doNotations (s, n, ptup, alter, tupnotation, tstop, nt, lev): - slurs = getattr (n, 'slurs', 0) # slur ends - pts = getattr (n, 'pitches', []) # all chord notes available in the first note - if pts: # make list of pitches in chord: [(pitch, octave), ..] - if type (pts.pitch) == pObj: pts = [pts.pitch] # chord with one note - else: pts = [tuple (p.t[-2:]) for p in pts.pitch] # normal chord - for pt, (tie_alter, nts, vnum) in s.ties.items (): # scan all open ties and delete illegal ones - if vnum != s.overlayVnum: continue # tie belongs to different overlay - if pts and pt in pts: continue # pitch tuple of tie exists in chord - if getattr (n, 'chord', 0): continue # skip chord notes - if pt == ptup: continue # skip correct single note tie - if getattr (n, 'grace', 0): continue # skip grace notes - info ('tie between different pitches: %s%s converted to slur' % pt) - del s.ties [pt] # remove the note from pending ties - e = [t for t in nts.findall ('tied') if t.get ('type') == 'start'][0] # get the tie start element - e.tag = 'slur' # convert tie into slur - slurnum = len (s.slurstack) + 1 - s.slurstack.append (slurnum) - e.set ('number', str (slurnum)) - if slurs: slurs.t.append (')') # close slur on this note - else: slurs = pObj ('slurs', [')']) - tstart = getattr (n, 'tie', 0) # start a new tie - decos = s.nextdecos # decorations encountered so far - ndeco = getattr (n, 'deco', 0) # possible decorations of notes of a chord - if ndeco: # add decorations, translate used defined symbols - decos += [s.usrSyms.get (d, d).strip ('!+') for d in ndeco.t] - s.nextdecos = [] - if not (tstop or tstart or decos or slurs or s.slurbeg or tupnotation): return nt - nots = E.Element ('notations') # notation element needed - if tupnotation: # add tuplet type - tup = E.Element ('tuplet', type=tupnotation) - if tupnotation == 'start': tup.set ('bracket', 'yes') - addElem (nots, tup, lev + 1) - if tstop: # stop tie - del s.ties[ptup] # remove flag - tie = E.Element ('tied', type='stop') - addElem (nots, tie, lev + 1) - if tstart: # start a tie - s.ties[ptup] = (alter, nots, s.overlayVnum) # remember pitch tuple to stop tie and apply same alteration - tie = E.Element ('tied', type='start') - addElem (nots, tie, lev + 1) - if decos: # look for slurs and decorations - arts = [] # collect articulations - for d in decos: # do all slurs and decos - if d == '(': s.slurbeg += 1; continue # slurs made in while loop at the end - elif d == 'fermata' or d == 'H': - ntn = E.Element ('fermata', type='upright') - elif d == 'arpeggio': - ntn = E.Element ('arpeggiate', number='1') - else: arts.append (d); continue - addElem (nots, ntn, lev + 1) - if arts: # do only note articulations and collect staff annotations in xmldecos - rest = s.doArticulations (nots, arts, lev + 1) - if rest: info ('unhandled note decorations: %s' % rest) - while s.slurbeg > 0: - s.slurbeg -= 1 - slurnum = len (s.slurstack) + 1 - s.slurstack.append (slurnum) - ntn = E.Element ('slur', number='%d' % slurnum, type='start') - addElem (nots, ntn, lev + 1) - if slurs: # these are only slur endings - for d in slurs.t: - if not s.slurstack: break # no more open slurs - slurnum = s.slurstack.pop () - slur = E.Element ('slur', number='%d' % slurnum, type='stop') - addElem (nots, slur, lev + 1) - if nots.getchildren() != []: # only add notations if not empty - addElem (nt, nots, lev) - - def doArticulations (s, nots, arts, lev): - decos = [] - for a in arts: - if a in s.artMap: - art = E.Element ('articulations') - addElem (nots, art, lev) - addElem (art, E.Element (s.artMap[a]), lev + 1) - elif a in s.ornMap: - orn = E.Element ('ornaments') - addElem (nots, orn, lev) - addElem (orn, E.Element (s.ornMap[a]), lev + 1) - elif a in s.tecMap: - tec = E.Element ('technical') - addElem (nots, tec, lev) - addElem (tec, E.Element (s.tecMap[a]), lev + 1) - else: decos.append (a) # return staff annotations - return decos - - def doLyr (s, n, nt, lev): - for i, lyrobj in enumerate (n.objs): - if lyrobj.name != 'syl': continue - dash = len (lyrobj.t) == 2 - if dash: - if i in s.lyrdash: type = 'middle' - else: type = 'begin'; s.lyrdash [i] = 1 - else: - if i in s.lyrdash: type = 'end'; del s.lyrdash [i] - else: type = 'single' - lyrel = E.Element ('lyric', number = str (i + 1)) - addElem (nt, lyrel, lev) - addElemT (lyrel, 'syllabic', type, lev + 1) - addElemT (lyrel, 'text', lyrobj.t[0].replace ('~',' '), lev + 1) - - def doBeams (s, n, nt, den, lev): - if hasattr (n, 'chord') or hasattr (n, 'grace'): - s.grcbbrk = s.grcbbrk or n.bbrk.t[0] # remember if there was any bbrk in or before a grace sequence - return - bbrk = s.grcbbrk or n.bbrk.t[0] or den < 32 - s.grcbbrk = False - if not s.prevNote: pbm = None - else: pbm = s.prevNote.find ('beam') - bm = E.Element ('beam', number='1') - bm.text = 'begin' - if pbm != None: - if bbrk: - if pbm.text == 'begin': - s.prevNote.remove (pbm) - elif pbm.text == 'continue': - pbm.text = 'end' - s.prevNote = None - else: bm.text = 'continue' - if den >= 32 and n.name != 'rest': - addElem (nt, bm, lev) - s.prevNote = nt - - def stopBeams (s): - if not s.prevNote: return - pbm = s.prevNote.find ('beam') - if pbm != None: - if pbm.text == 'begin': - s.prevNote.remove (pbm) - elif pbm.text == 'continue': - pbm.text = 'end' - s.prevNote = None - - def staffDecos (s, decos, maat, lev, bardecos=0): - gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice - for d in decos: - d = s.usrSyms.get (d, d).strip ('!+') # try to replace user defined symbol - if d in s.dynaMap: - dynel = E.Element ('dynamics') - addDirection (maat, dynel, lev, gstaff, [E.Element (d)], 'below', s.gcue_on) - elif d in s.wedgeMap: # wedge - if ')' in d: type = 'stop' - else: type = 'crescendo' if '<' in d or 'crescendo' in d else 'diminuendo' - addDirection (maat, E.Element ('wedge', type=type), lev, gstaff) - elif d in ['coda', 'segno']: - if bardecos: s.bardecos.append (d) # postpone to begin next measure - else: - text, attr, val = s.capoMap [d] - dir = addDirection (maat, E.Element (text), lev, gstaff, placement='above') - sound = E.Element ('sound'); sound.set (attr, val) - addElem (dir, sound, lev + 1) - elif d in s.capoMap: - text, attr, val = s.capoMap [d] - words = E.Element ('words'); words.text = text - dir = addDirection (maat, words, lev, gstaff, placement='above') - sound = E.Element ('sound'); sound.set (attr, val) - addElem (dir, sound, lev + 1) - elif d == '(': s.slurbeg += 1 # start slur on next note - else: s.nextdecos.append (d) # keep annotation for the next note - - def doFields (s, maat, fieldmap, lev): - def doClef (): - clef, gtrans = 0, 0 - clefn = re.search (r'alto1|alto2|alto4|alto|tenor|bass3|bass|treble|perc|none', field) - clefm = re.search (r"(?:^m=| m=|middle=)([A-Ga-g])([,']*)", field) - trans_oct2 = re.search (r'octave=([-+]\d)', field) - trans = re.search (r'(?:^t=| t=|transpose=)(-?[\d]+)', field) - trans_oct = re.search (r'([+-^_])(8|15)', field) - cue_onoff = re.search (r'cue=(on|off)', field) - if clefn: - clef = clefn.group () - if clefm: - note, octstr = clefm.groups () - nUp = note.upper () - octnum = (4 if nUp == note else 5) + (len (octstr) if "'" in octstr else -len (octstr)) - gtrans = (3 if nUp in 'AFD' else 4) - octnum - if clef not in ['perc', 'none']: clef = s.clefLineMap [nUp] - if clef: - s.gtrans = gtrans # only change global tranposition when a clef is really defined - sign, line = s.clefMap [clef] - if not sign: return - c = E.Element ('clef') - gstaff = s.gStaffNums.get (s.vid, 0) # the current staff number - if gstaff: c.set ('number', str (gstaff)) # only add staff number when defined - addElemT (c, 'sign', sign, lev + 2) - if line: addElemT (c, 'line', line, lev + 2) - if trans_oct: - n = trans_oct.group (1) in '-_' and -1 or 1 - if trans_oct.group (2) == '15': n *= 2 # 8 => 1 octave, 15 => 2 octaves - addElemT (c, 'clef-octave-change', str (n), lev + 2) # transpose print out - if trans_oct.group (1) in '+-': s.gtrans += n # also transpose all pitches with one octave - if trans_oct2: - n = int (trans_oct2.group (1)) - s.gtrans += n - atts.append ((7, c)) - if trans != None: # add transposition in semitones - e = E.Element ('transpose') - addElemT (e, 'chromatic', str (trans.group (1)), lev + 3) - atts.append ((9, e)) - if cue_onoff: s.gcue_on = cue_onoff.group (1) == 'on' - atts = [] # collect xml attribute elements [(order-number, xml-element), ..] - for ftype, field in fieldmap.items (): - if not field: # skip empty fields - continue - if ftype == 'Div': # not an abc field, but handled as if - d = E.Element ('divisions') - d.text = field - atts.append ((1, d)) - elif ftype == 'gstaff': # make grand staff - e = E.Element ('staves') - e.text = str (field) - atts.append ((4, e)) - elif ftype == 'M': - if field == 'none': continue - if field == 'C': field = '4/4' - elif field == 'C|': field = '2/2' - t = E.Element ('time') - if '/' not in field: - info ('M:%s not recognized, 4/4 assumed' % field) - field = '4/4' - beats, btype = field.split ('/')[:2] - try: s.mdur = simplify (eval (beats), int (btype)) # measure duration for Z and X rests (eval allows M:2+3/4) - except: - info ('error in M:%s, 4/4 assumed' % field) - s.mdur = (4,4) - beats, btype = '4','4' - addElemT (t, 'beats', beats, lev + 2) - addElemT (t, 'beat-type', btype, lev + 2) - atts.append ((3, t)) - elif ftype == 'K': - accs = ['F','C','G','D','A','E','B'] # == s.sharpness [7:14] - mode = '' - key = re.match (r'\s*([A-G][#b]?)\s*([a-zA-Z]*)', field) - alts = re.search (r'\s((\s?[=^_][A-Ga-g])+)', ' ' + field) # avoid matching middle=G and m=G - if key: - key, mode = key.groups () - mode = mode.lower ()[:3] # only first three chars, no case - if mode not in s.offTab: mode = 'maj' - fifths = s.sharpness.index (key) - s.offTab [mode] - if fifths >= 0: s.keyAlts = dict (zip (accs[:fifths], fifths * ['1'])) - else: s.keyAlts = dict (zip (accs[fifths:], -fifths * ['-1'])) - elif field.startswith ('none') or field == '': # the default key - fifths = 0 - mode = 'maj' - if alts: - alts = re.findall (r'[=^_][A-Ga-g]', alts.group(1)) # list of explicit alterations - alts = [(x[1], s.alterTab [x[0]]) for x in alts] # [step, alter] - for step, alter in alts: # correct permanent alterations for this key - s.keyAlts [step.upper ()] = alter - k = E.Element ('key') - koctave = [] - lowerCaseSteps = [step.upper () for step, alter in alts if step.islower ()] - for step, alter in s.keyAlts.items (): - if alter == '0': # skip neutrals - del s.keyAlts [step.upper ()] # otherwise you get neutral signs on normal notes - continue - addElemT (k, 'key-step', step.upper (), lev + 2) - addElemT (k, 'key-alter', alter, lev + 2) - koctave.append ('5' if step in lowerCaseSteps else '4') - if koctave: # only key signature if not empty - for oct in koctave: - e = E.Element ('key-octave', number=oct) - addElem (k, e, lev + 2) - atts.append ((2, k)) - elif mode: - k = E.Element ('key') - addElemT (k, 'fifths', str (fifths), lev + 2) - addElemT (k, 'mode', s.modTab [mode], lev + 2) - atts.append ((2, k)) - doClef () - elif ftype == 'L': - s.unitLcur = map (int, field.split ('/')) - if len (s.unitLcur) == 1 or s.unitLcur[1] not in s.typeMap: - info ('L:%s is not allowed, 1/8 assumed' % field) - s.unitLcur = 1,8 - elif ftype == 'V': - doClef () - elif ftype == 'I': - xs = s.doField_I (ftype, field) - if xs and len (xs) == 1: # when "%%MIDI transpose" matched insert 'attribute/transpose/chromatic' - e = E.Element ('transpose') - addElemT (e, 'chromatic', xs[0], lev + 2) # xs[0] == signed number string given after transpose - atts.append ((9, e)) - if xs and len (xs) == 2: # repeated occurrence of [I:MIDI] -> instrument change - midchan, midprog = xs - snd = E.Element ('sound') - mi = E.Element ('midi-instrument', id='I-' + s.vid) - addElem (maat, snd, lev) - addElem (snd, mi, lev + 1) - if midchan: addElemT (mi, 'midi-channel', midchan, lev + 2) - if midprog: addElemT (mi, 'midi-program', str (int (midprog) + 1), lev + 2) # compatible with abc2midi - - elif ftype == 'Q': - s.doTempo (maat, field, lev) - elif ftype in 'TCOAZNGHRBDFSU': - info ('**illegal header field in body: %s, content: %s' % (ftype, field)) - else: - info ('unhandled field: %s, content: %s' % (ftype, field)) - - if atts: - att = E.Element ('attributes') # insert sub elements in the order required by musicXML - addElem (maat, att, lev) - for _, att_elem in sorted (atts): # ordering ! - addElem (att, att_elem, lev + 1) - - def doTempo (s, maat, field, lev): - gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice - t = re.search (r'(\d)/(\d\d?)\s*=\s*([.\d]+)|([.\d]+)', field) - if not t: return - try: - if t.group (4): - num, den, upm = 1, s.unitLcur[1] , float (t.group (4)) - else: - num, den, upm = int (t.group (1)), int (t.group (2)), float (t.group (3)) - except: return # float or int conversion failure - if num != 1: info ('in Q: numerator > 1 in %d/%d not supported' % (num, den)) - qpm = 4. * num * upm / den - metro = E.Element ('metronome') - u = E.Element ('beat-unit'); u.text = s.typeMap [4 * den] - pm = E.Element ('per-minute'); pm.text = '%.2f' % upm - dir = addDirection (maat, metro, lev, gstaff, [u, pm], placement='above') - sound = E.Element ('sound'); sound.set ('tempo', '%.2f' % qpm) - addElem (dir, sound, lev + 1) - - def mkBarline (s, maat, loc, lev, style='', dir='', ending=''): - b = E.Element ('barline', location=loc) - if style: - addElemT (b, 'bar-style', style, lev + 1) - if s.curVolta: # first stop a current volta - end = E.Element ('ending', number=s.curVolta, type='stop') - s.curVolta = '' - if loc == 'left': # stop should always go to a right barline - bp = E.Element ('barline', location='right') - addElem (bp, end, lev + 1) - addElem (s.prevmsre, bp, lev) # prevmsre has no right barline! (ending would have stopped there) - else: - addElem (b, end, lev + 1) - if ending: - ending = ending.replace ('-',',') # MusicXML only accepts comma's - endtxt = '' - if ending.startswith ('"'): # ending is a quoted string - endtxt = ending.strip ('"') - ending = '33' # any number that is not likely to occur elsewhere - end = E.Element ('ending', number=ending, type='start') - if endtxt: end.text = endtxt # text appears in score in stead of number attribute - addElem (b, end, lev + 1) - s.curVolta = ending - if dir: - r = E.Element ('repeat', direction=dir) - addElem (b, r, lev + 1) - addElem (maat, b, lev) - - def doChordSym (s, maat, sym, lev): - alterMap = {'#':'1','=':'0','b':'-1'} - rnt = sym.root.t - chord = E.Element ('harmony') - addElem (maat, chord, lev) - root = E.Element ('root') - addElem (chord, root, lev + 1) - addElemT (root, 'root-step', rnt[0], lev + 2) - if len (rnt) == 2: addElemT (root, 'root-alter', alterMap [rnt[1]], lev + 2) - kind = s.chordTab.get (sym.kind.t[0], 'major') - addElemT (chord, 'kind', kind, lev + 1) - degs = getattr (sym, 'degree', '') - if degs: - if type (degs) != types.ListType: degs = [degs] - for deg in degs: - deg = deg.t[0] - if deg[0] == '#': alter = '1'; deg = deg[1:] - elif deg[0] == 'b': alter = '-1'; deg = deg[1:] - else: alter = '0'; deg = deg - degree = E.Element ('degree') - addElem (chord, degree, lev + 1) - addElemT (degree, 'degree-value', deg, lev + 2) - addElemT (degree, 'degree-alter', alter, lev + 2) - addElemT (degree, 'degree-type', 'add', lev + 2) - - def mkMeasure (s, i, t, lev, fieldmap={}): - s.msreAlts = {} - s.ntup = -1 - s.acciatura = 0 # next grace element gets acciatura attribute - overlay = 0 - maat = E.Element ('measure', number = str(i)) - if fieldmap: s.doFields (maat, fieldmap, lev + 1) - if s.linebrk: # there was a line break in the previous measure - e = E.Element ('print') - e.set ('new-system', 'yes') - addElem (maat, e, lev + 1) - s.linebrk = 0 - if s.bardecos: # output coda and segno attached to the previous right barline - s.staffDecos (s.bardecos, maat, lev + 1) - s.bardecos = [] - for it, x in enumerate (t): - if x.name == 'note' or x.name == 'rest': - note = s.mkNote (x, lev + 1) - addElem (maat, note, lev + 1) - elif x.name == 'lbar': - bar = x.t[0] - if bar == '|': pass # skip redundant bar - elif ':' in bar: # forward repeat - volta = x.t[1] if len (x.t) == 2 else '' - s.mkBarline (maat, 'left', lev + 1, style='heavy-light', dir='forward', ending=volta) - else: # bar must be a volta number - s.mkBarline (maat, 'left', lev + 1, ending=bar) - elif x.name == 'rbar': - if hasattr (x, 'deco'): # MuseScore does not support this -> emergency solution - s.staffDecos (x.deco.t, maat, lev + 1, bardecos=1) # coda, segno -> next measure - bar = x.t[0] - if bar == '.|': - s.mkBarline (maat, 'right', lev + 1, style='dotted') - elif ':' in bar: # backward repeat - s.mkBarline (maat, 'right', lev + 1, style='light-heavy', dir='backward') - elif bar == '||': - s.mkBarline (maat, 'right', lev + 1, style='light-light') - elif bar == '[|]' or bar == '[]': - s.mkBarline (maat, 'right', lev + 1, style='none') - elif '[' in bar or ']' in bar: - s.mkBarline (maat, 'right', lev + 1, style='light-heavy') - elif bar[0] == '&': overlay = 1 - elif x.name == 'tup': - if len (x.t) == 3: n, into, nts = x.t - else: n, into, nts = x.t[0], 0, 0 - if into == 0: into = 3 if n in [2,4,8] else 2 - if nts == 0: nts = n - s.tmnum, s.tmden, s.ntup = n, into, nts - elif x.name == 'deco': - s.staffDecos (x.t, maat, lev + 1) # output staff decos, postpone note decos to next note - elif x.name == 'text': - pos, text = x.t[:2] - place = 'above' if pos == '^' else 'below' - words = E.Element ('words') - words.text = text - gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice - addDirection (maat, words, lev + 1, gstaff, placement=place) - elif x.name == 'inline': - fieldtype, fieldval = x.t[:2] - s.doFields (maat, {fieldtype:fieldval}, lev + 1) - elif x.name == 'accia': s.acciatura = 1 - elif x.name == 'linebrk': - s.supports_tag = 1 - if it > 0 and t[it -1].name == 'lbar': # we are at start of measure - e = E.Element ('print') # output linebreak now - e.set ('new-system', 'yes') - addElem (maat, e, lev + 1) - else: - s.linebrk = 1 # output linebreak at start of next measure - elif x.name == 'chordsym': - s.doChordSym (maat, x, lev + 1) - s.stopBeams () - s.prevmsre = maat - return maat, overlay - - def mkPart (s, maten, id, lev, attrs, nstaves): - s.slurstack = [] - s.unitLcur = s.unitL # set the default unit length at begin of each voice - s.curVolta = '' - s.lyrdash = {} - s.linebrk = 0 - s.midprg = ['', ''] # MIDI channel nr, program nr for the current part - s.gcue_on = 0 # reset cue note marker for each new voice - s.gtrans = 0 # reset octave transposition (by clef) - part = E.Element ('part', id=id) - s.overlayVnum = 0 # overlay voice number to relate ties that extend from one overlayed measure to the next - gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice - attrs_cpy = attrs.copy () # don't change attrs itself in next line - if gstaff == 1: attrs_cpy ['gstaff'] = nstaves # make a grand staff - msre, overlay = s.mkMeasure (1, maten[0], lev + 1, attrs_cpy) - addElem (part, msre, lev + 1) - for i, maat in enumerate (maten[1:]): - s.overlayVnum = s.overlayVnum + 1 if overlay else 0 - msre, next_overlay = s.mkMeasure (i+2, maat, lev + 1) - if overlay: mergePartMeasure (part, msre, s.overlayVnum) - else: addElem (part, msre, lev + 1) - overlay = next_overlay - return part - - def mkScorePart (s, id, vids_p, partAttr, lev): - naam, subnm, midprg = partAttr [id] - sp = E.Element ('score-part', id='P'+id) - nm = E.Element ('part-name') - nm.text = naam - addElem (sp, nm, lev + 1) - snm = E.Element ('part-abbreviation') - snm.text = subnm - if subnm: addElem (sp, snm, lev + 1) # only add if subname was given - if s.staves: instr_vids = [vids for vids in s.staves if vids[0] == id][0] - else: instr_vids = [id] - inst = [] - for id in instr_vids: - if id not in partAttr: continue # error in %%score -> instr_vids may have non existing id's - naam, subnm, midprg = partAttr [id] - midchan, midprog = midprg - if not midchan and not midprog: continue # only add if program nr or channel was given - si = E.Element ('score-instrument', id='I-'+id) - addElemT (si, 'instrument-name', naam, lev + 2) - mi = E.Element ('midi-instrument', id='I-'+id) - if midchan: addElemT (mi, 'midi-channel', midchan, lev + 2) - if midprog: addElemT (mi, 'midi-program', str (int (midprog) + 1), lev + 2) # compatible with abc2midi - inst.append ((si, mi)) - for si, mi in inst: addElem (sp, si, lev + 1) - for si, mi in inst: addElem (sp, mi, lev + 1) - return sp, len (inst) - - def mkPartlist (s, vids, partAttr, lev): - def addPartGroup (sym, num): - pg = E.Element ('part-group', number=str (num), type='start') - addElem (partlist, pg, lev + 1) - addElemT (pg, 'group-symbol', sym, lev + 2) - addElemT (pg, 'group-barline', 'yes', lev + 2) - partlist = E.Element ('part-list') - g_num = 0 # xml group number - nInstrs = [] # number of instruments in each part - for g in (s.groups or vids): # brace/bracket or abc_voice_id - if g == '[': g_num += 1; addPartGroup ('bracket', g_num) - elif g == '{': g_num += 1; addPartGroup ('brace', g_num) - elif g in '}]': - pg = E.Element ('part-group', number=str (g_num), type='stop') - addElem (partlist, pg, lev + 1) - g_num -= 1 - else: # g = abc_voice_id - if g not in vids: continue # error in %%score - sp, nInst = s.mkScorePart (g, vids, partAttr, lev + 1) - addElem (partlist, sp, lev + 1) - nInstrs.append (nInst) - return partlist, nInstrs - - def doField_I (s, type, x): - def readPfmt (x, n): # read ABC page formatting constant - if not s.pageFmtAbc: s.pageFmtAbc = s.pageFmtDef # set the default values on first change - ro = re.search (r'[^.\d]*([\d.]+)\s*(cm|in|pt)?', x) # float followed by unit - if ro: - x, unit = ro.groups () # unit == None when not present - u = {'cm':10., 'in':25.4, 'pt':25.4/72} [unit] if unit else 1. - s.pageFmtAbc [n] = float (x) * u # convert ABC values to millimeters - else: info ('error in page format: %s' % x) - - if x.startswith ('score') or x.startswith ('staves'): - s.staveDefs += [x] # collect all voice mappings - elif x.startswith ('staffwidth'): info ('skipped I-field: %s' % x) - elif x.startswith ('staff'): # set new staff number of the current voice - r1 = re.search (r'staff *([+-]?)(\d)', x) - if r1: - sign = r1.group (1) - num = int (r1.group (2)) - gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice - if sign: # relative staff number - num = (sign == '-') and gstaff - num or gstaff + num - else: # absolute abc staff number - try: vabc = s.staves [num - 1][0] # vid of (first voice of) abc-staff num - except: vabc = 0; info ('abc staff %s does not exist' % num) - num = s.gStaffNumsOrg.get (vabc, 0) # xml staff number of abc-staff num - if gstaff and num > 0 and num <= s.gNstaves [s.vid]: - s.gStaffNums [s.vid] = num - else: info ('could not relocate to staff: %s' % r1.group ()) - else: info ('not a valid staff redirection: %s' % x) - elif x.startswith ('scale'): readPfmt (x, 0) - elif x.startswith ('pageheight'): readPfmt (x, 1) - elif x.startswith ('pagewidth'): readPfmt (x, 2) - elif x.startswith ('leftmargin'): readPfmt (x, 3) - elif x.startswith ('rightmargin'): readPfmt (x, 4) - elif x.startswith ('topmargin'): readPfmt (x, 5) - elif x.startswith ('botmargin'): readPfmt (x, 6) - elif x.startswith ('MIDI'): - r1 = re.search (r'program *(\d*) +(\d+)', x) - r2 = re.search (r'channel\D*(\d+)', x) - if r1: ch, prg = r1.groups () # channel nr or '', program nr - if r2: ch, prg = r2.group (1), '' # channel nr only - if r1 or r2: - if s.midprg[1] == '': # no instrument defined yet - s.midprg[1] = prg - if ch: s.midprg[0] = ch - elif ch and s.midprg[0] == '': # no channel defined yet - s.midprg[0] = ch - else: # repeated midi def -> insert instument change - return [ch, prg] - r = re.search (r'transpose[^-\d]*(-?\d+)', x) - if r: return [r.group (1)] - else: info ('skipped I-field: %s' % x) - - def parseStaveDef (s, vdefs): - if not s.staveDefs: return vdefs - for x in s.staveDefs [1:]: info ('%%%%%s dropped, multiple stave mappings not supported' % x) - x = s.staveDefs [0] # only the first %%score is honoured - score = abc_scoredef.parseString (x) [0] - f = lambda x: type (x) == types.UnicodeType and [x] or x - s.staves = map (f, mkStaves (score, vdefs)) - s.grands = map (f, mkGrand (score, vdefs)) - s.groups = mkGroups (score) - vce_groups = [vids for vids in s.staves if len (vids) > 1] # all voice groups - d = {} # for each voice group: map first voice id -> all merged voice ids - for vgr in vce_groups: d [vgr[0]] = vgr - for gstaff in s.grands: # for all grand staves - if len (gstaff) == 1: continue # skip single parts - for v, stf_num in zip (gstaff, range (1, len (gstaff) + 1)): - for vx in d.get (v, [v]): # allocate staff numbers - s.gStaffNums [vx] = stf_num # to all constituant voices - s.gNstaves [vx] = len (gstaff) # also remember total number of staves - s.gStaffNumsOrg = s.gStaffNums.copy () # keep original allocation for abc -> xml staff map - return vdefs - - def voiceNamesAndMaps (s, ps): # get voice names and mappings - vdefs = {} - for vid, vcedef, vce in ps: # vcedef == emtpy of first pObj == voice definition - pname, psubnm = '', '' # part name and abbreviation - if not vcedef: # simple abc without voice definitions - vdefs [vid] = pname, psubnm, '' - else: # abc with voice definitions - if vid != vcedef.t[1]: info ('voice ids unequal: %s (reg-ex) != %s (grammar)' % (vid, vcedef.t[1])) - rn = re.search (r'(?:name|nm)="([^"]*)"', vcedef.t[2]) - if rn: pname = rn.group (1) - rn = re.search (r'(?:subname|snm|sname)="([^"]*)"', vcedef.t[2]) - if rn: psubnm = rn.group (1) - vdefs [vid] = pname, psubnm, vcedef.t[2] - xs = [pObj.t[1] for maat in vce for pObj in maat if pObj.name == 'inline'] # all inline statements in vce - s.staveDefs += [x for x in xs if x.startswith ('score') or x.startswith ('staves')] # filter %%score and %%staves - return vdefs - - def doHeaderField (s, fld, attrmap): - type, value = fld.t[:2] - if not value: # skip empty field - return - if type == 'M': - attrmap [type] = value - elif type == 'L': - try: s.unitL = map (int, fld.t[1].split ('/')) - except: - info ('illegal unit length:%s, 1/8 assumed' % fld.t[1]) - s.unitL = 1,8 - if len (s.unitL) == 1 or s.unitL[1] not in s.typeMap: - info ('L:%s is not allowed, 1/8 assumed' % fld.t[1]) - s.unitL = 1,8 - elif type == 'K': - attrmap[type] = value - elif type == 'T': - if s.title: s.title = s.title + '\n' + value - else: s.title = value - elif type == 'C': - s.creator ['composer'] = s.creator.get ('composer', '') + value - elif type == 'Z': - s.creator ['lyricist'] = s.creator.get ('lyricist', '') + value - elif type == 'U': - sym = fld.t[2].strip ('!+') - s.usrSyms [value] = sym - elif type == 'I': - s.doField_I (type, value) - elif type == 'Q': - attrmap[type] = value - elif type in s.creditTab: s.credits [s.creditTab [type]] = value - else: - info ('skipped header: %s' % fld) - - def mkIdentification (s, score, lev): - if s.title: - addElemT (score, 'movement-title', s.title, lev + 1) - ident = E.Element ('identification') - addElem (score, ident, lev + 1) - if s.creator: - for ctype, cname in s.creator.items (): - c = E.Element ('creator', type=ctype) - c.text = cname - addElem (ident, c, lev + 2) - encoding = E.Element ('encoding') - addElem (ident, encoding, lev + 2) - encoder = E.Element ('encoder') - encoder.text = 'abc2xml version %d' % VERSION - addElem (encoding, encoder, lev + 3) - if s.supports_tag: # avoids interference of auto-flowing and explicit linebreaks - suports = E.Element ('supports', attribute="new-system", element="print", type="yes", value="yes") - addElem (encoding, suports, lev + 3) - encodingDate = E.Element ('encoding-date') - encodingDate.text = str (datetime.date.today ()) - addElem (encoding, encodingDate, lev + 3) - - def mkDefaults (s, score, lev): - if s.pageFmtCmd: s.pageFmtAbc = s.pageFmtCmd - if not s.pageFmtAbc: return # do not output the defaults if none is desired - space, h, w, l, r, t, b = s.pageFmtAbc - mils = 4 * space # staff height in millimeters - scale = 40. / mils # tenth's per millimeter - dflts = E.Element ('defaults') - addElem (score, dflts, lev) - scaling = E.Element ('scaling') - addElem (dflts, scaling, lev + 1) - addElemT (scaling, 'millimeters', '%g' % mils, lev + 2) - addElemT (scaling, 'tenths', '40', lev + 2) - layout = E.Element ('page-layout') - addElem (dflts, layout, lev + 1) - addElemT (layout, 'page-height', '%g' % (h * scale), lev + 2) - addElemT (layout, 'page-width', '%g' % (w * scale), lev + 2) - margins = E.Element ('page-margins', type='both') - addElem (layout, margins, lev + 2) - addElemT (margins, 'left-margin', '%g' % (l * scale), lev + 3) - addElemT (margins, 'right-margin', '%g' % (r * scale), lev + 3) - addElemT (margins, 'top-margin', '%g' % (t * scale), lev + 3) - addElemT (margins, 'bottom-margin', '%g' % (b * scale), lev + 3) - - def mkCredits (s, score, lev): - if not s.credits: return - for ctype, ctext in s.credits.items (): - credit = E.Element ('credit', page='1') - addElemT (credit, 'credit-type', ctype, lev + 2) - addElemT (credit, 'credit-words', ctext, lev + 2) - addElem (score, credit, lev) - - def parse (s, abc_string): - abctext = abc_string if type (abc_string) == types.UnicodeType else decodeInput (abc_string) - abctext = abctext.replace ('[I:staff ','[I:staff') # avoid false beam breaks - s.reset () - header, voices = splitHeaderVoices (abctext) - ps = [] - try: - hs = abc_header.parseString (header) if header else '' - for id, vce_lyr in voices: # vce_lyr = [voice-block] where voice-block = (measures, corresponding lyric lines) - vcelyr = [] # list of measures where measure = list of elements (see syntax) - prevLeftBar = None # previous voice ended with a left-bar symbol (double repeat) - for voice, lyr in vce_lyr: - vce = abc_voice.parseString (voice).asList () - if not vce: # empty voice, insert an inline field that will be rejected - vce = [[pObj ('inline', ['I', 'empty voice'])]] - if prevLeftBar: - vce[0].insert (0, prevLeftBar) # insert at begin of first measure - prevLeftBar = None - if vce[-1] and vce[-1][-1].name == 'lbar': # last measure ends with an lbar - prevLeftBar = vce[-1][-1] - if len (vce) > 1: # vce should not become empty (-> exception when taking vcelyr [0][0]) - del vce[-1] # lbar was the only element in measure vce[-1] - lyr = lyr.strip () # strip leading \n (because we split on '\nw:...') - if lyr: # no lyrics for this measures-lyrics block - lyr = lyr_block.parseString (lyr).asList () - xs = alignLyr (vce, lyr) # put all syllables into corresponding notes - else: xs = vce - vcelyr += xs - elem1 = vcelyr [0][0] # the first element of the first measure - if elem1.name == 'inline'and elem1.t[0] == 'V': # is a voice definition - voicedef = elem1 - del vcelyr [0][0] # do not read voicedef twice - else: - voicedef = '' - ps.append ((id, voicedef, vcelyr)) - except ParseException, err: - if err.loc > 40: # limit length of error message, compatible with markInputline - err.pstr = err.pstr [err.loc - 40: err.loc + 40] - err.loc = 40 - xs = err.line[err.col-1:] - try: info (err.line.encode ('utf-8'), warn=0) # err.line is a unicode string!! - except: info (err.line.encode ('latin-1'), warn=0) - info ((err.col-1) * '-' + '^', warn=0) - if re.search (r'\[U:[XYZxyz]', xs): - info ('Error: illegal user defined symbol: %s' % xs[1:], warn=0) - elif re.search (r'\[[OAPZNGHRBDFSXTCIU]:', xs): - info ('Error: header-only field %s appears after K:' % xs[1:], warn=0) - else: - info ('Syntax error at column %d' % err.col, warn=0) - raise err - - s.unitL = (1, 8) - s.title = '' - s.creator = {} # {creator type -> name string} - s.credits = {} # {credit type -> string} - score = E.Element ('score-partwise') - attrmap = {'Div': str (s.divisions), 'K':'C treble', 'M':'4/4'} - for res in hs: - if res.name == 'field': - s.doHeaderField (res, attrmap) - else: - info ('unexpected header item: %s' % res) - - vdefs = s.voiceNamesAndMaps (ps) - vdefs = s.parseStaveDef (vdefs) - - lev = 0 - vids, parts, partAttr = [], [], {} - for vid, _, vce in ps: # voice id, voice parse tree - pname, psubnm, voicedef = vdefs [vid] # part name - attrmap ['V'] = voicedef # abc text of first voice definition (after V:vid) or empty - pid = 'P%s' % vid # let part id start with an alpha - s.vid = vid # avoid parameter passing, needed in mkNote for instrument id - part = s.mkPart (vce, pid, lev + 1, attrmap, s.gNstaves.get (vid, 0)) - if 'Q' in attrmap: del attrmap ['Q'] # header tempo only in first part - parts.append (part) - vids.append (vid) - partAttr [vid] = (pname, psubnm, s.midprg) - parts, vidsnew = mergeParts (parts, vids, s.staves) # merge parts into staves as indicated by %%score - parts, _ = mergeParts (parts, vidsnew, s.grands, 1) # merge grand staves - - s.mkIdentification (score, lev) - s.mkDefaults (score, lev + 1) - s.mkCredits (score, lev) - - partlist, nInstrs = s.mkPartlist (vids, partAttr, lev + 1) - addElem (score, partlist, lev + 1) - for ip, part in enumerate (parts): - if nInstrs [ip] < 2: # no instrument tag needed for one- or no-instrument parts - removeElems (part, 'measure/note', 'instrument') - addElem (score, part, lev + 1) - - return score - -def decodeInput (data_string): - try: enc = 'utf-8'; unicode_string = data_string.decode (enc) - except: - try: enc = 'latin-1'; unicode_string = data_string.decode (enc) - except: raise Exception ('data not encoded in utf-8 nor in latin-1') - info ('decoded from %s' % enc) - return unicode_string - -xmlVersion = "" -def fixDoctype (elem, enc): - xs = E.tostring (elem, encoding=enc) - ys = xs.split ('\n') - if enc == 'utf-8': ys.insert (0, xmlVersion) # crooked logic of ElementTree lib - ys.insert (1, '') - return '\n'.join (ys) - -def xml2mxl (pad, fnm, data): # write xml data to compressed .mxl file - from zipfile import ZipFile, ZIP_DEFLATED - fnmext = fnm + '.xml' # file name with extension, relative to the root within the archive - outfile = os.path.join (pad, fnm + '.mxl') - meta = '%s\n\n' % xmlVersion - meta += '\n' % fnmext - meta += '' - f = ZipFile (outfile, 'w', ZIP_DEFLATED) - f.writestr ('META-INF/container.xml', meta) - f.writestr (fnmext, data) - f.close () - info ('%s written' % outfile, warn=0) - -def convert (pad, fnm, abc_string, mxl): - # these globals should be initialised (as in the __main__ secion) before calling convert - global mxm # optimisation 1: keep instance of MusicXml - global abc_header, abc_voice, lyr_block, abc_scoredef # optimisation 2: keep computed grammars - score = mxm.parse (abc_string) - if pad: - data = fixDoctype (score, 'utf-8') - if not mxl or mxl in ['a', 'add']: - outfnm = os.path.join (pad, fnm + '.xml') - outfile = file (outfnm, 'wb') - outfile.write (data) - outfile.close () - info ('%s written' % outfnm, warn=0) - if mxl: xml2mxl (pad, fnm, data) # also write a compressed version - else: - outfile = sys.stdout - outfile.write (fixDoctype (score, 'utf-8')) - outfile.write ('\n') - -#---------------- -# Main Program -#---------------- -if __name__ == '__main__': - from optparse import OptionParser - from glob import glob - import time - global mxm # keep instance of MusicXml - global abc_header, abc_voice, lyr_block, abc_scoredef # keep computed grammars - mxm = MusicXml () - - parser = OptionParser (usage='%prog [-h] [-r] [-m SKIP NUM] [-o DIR] [-p PFMT] [-z MODE] [ ...]', version='version %d' % VERSION) - parser.add_option ("-o", action="store", help="store xml files in DIR", default='', metavar='DIR') - parser.add_option ("-m", action="store", help="skip SKIP tunes, then read at most NUM tunes", nargs=2, type='int', default=(0,1), metavar='SKIP NUM') - parser.add_option ("-p", action="store", help="page formatting in PFMT", default='', metavar='PFMT') - parser.add_option ("-z", "--mxl", dest="mxl", help="store as compressed mxl, MODE = a(dd) or r(eplace)", default='', metavar='MODE') - parser.add_option ("-r", action="store_true", help="show whole measure rests in merged staffs", default=False) - options, args = parser.parse_args () - if len (args) == 0: parser.error ('no input file given') - pad = options.o - if options.mxl and options.mxl not in ['a','add', 'r', 'replace']: - parser.error ('MODE should be a(dd) or r(eplace), not: %s' % options.mxl) - if pad: - if not os.path.exists (pad): os.mkdir (pad) - if not os.path.isdir (pad): parser.error ('%s is not a directory' % pad) - if options.p: # set page formatting values - try: # space, page-height, -width, margin-left, -right, -top, -bottom - mxm.pageFmtCmd = map (float, options.p.split (',')) - if len (mxm.pageFmtCmd) != 7: raise Exception ('-p needs 7 values') - except Exception, err: parser.error (err) - mxm.gmwr = options.r # ugly: needs to be globally accessable - - abc_header, abc_voice, lyr_block, abc_scoredef = abc_grammar () # compute grammar only once per file set - fnmext_list = [] - for i in args: fnmext_list += glob (i) - if not fnmext_list: parser.error ('none of the input files exist') - t_start = time.time () - for X, fnmext in enumerate (fnmext_list): - fnm, ext = os.path.splitext (fnmext) - if ext.lower () not in ('.abc'): - info ('skipped input file %s, it should have extension .abc' % fnmext) - continue - if os.path.isdir (fnmext): - info ('skipped directory %s. Only files are accepted' % fnmext) - continue - - fobj = open (fnmext, 'rb') - encoded_data = fobj.read () - fobj.close () - fragments = encoded_data.split ('X:') - preamble = fragments [0] # tunes can be preceeded by formatting instructions - tunes = fragments[1:] - if not tunes and preamble: tunes, preamble = ['1\n' + preamble], '' # tune without X: - skip, num = options.m # skip tunes, then read at most num tunes - numtunes = min ([len (tunes), num]) # number of tunes to be converted - for itune, tune in enumerate (tunes): - if itune < skip: continue - if itune >= skip + num: break - tune = preamble + 'X:' + tune # restore preamble before each tune - fnmNum = '%s%02d' % (fnm, itune + 1) if numtunes > 1 else fnm - try: # convert string abctext -> file pad/fnmNum.xml - convert (pad, fnmNum, tune, options.mxl) - except ParseException, err: pass # output already printed - except Exception, err: info ('an exception occurred.\n%s' % err) - info ('done in %.2f secs' % (time.time () - t_start)) +#!/usr/bin/env python +# coding=latin-1 +''' +Copyright (C) 2012-2018: Willem G. Vree +Contributions: Nils Liberg, Nicolas Froment, Norman Schmidt, Reinier Maliepaard, Martin Tarenskeen, + Paul Villiger, Alexander Scheutzow, Herbert Schneider, David Randolph, Michael Strasser + +This program is free software; you can redistribute it and/or modify it under the terms of the +Lesser GNU General Public License as published by the Free Software Foundation; + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the Lesser GNU General Public License for more details. . +''' + +from functools import reduce +from pyparsing import Word, OneOrMore, Optional, Literal, NotAny, MatchFirst +from pyparsing import Group, oneOf, Suppress, ZeroOrMore, Combine, FollowedBy +from pyparsing import srange, CharsNotIn, StringEnd, LineEnd, White, Regex +from pyparsing import nums, alphas, alphanums, ParseException, Forward +try: import xml.etree.cElementTree as E +except: import xml.etree.ElementTree as E +import types, sys, os, re, datetime + +VERSION = 238 + +python3 = sys.version_info[0] > 2 +lmap = lambda f, xs: list (map (f, xs)) # eager map for python 3 +if python3: + int_type = int + list_type = list + str_type = str + uni_type = str + stdin = sys.stdin.buffer # read binary! +else: + int_type = types.IntType + list_type = types.ListType + str_type = types.StringTypes + uni_type = types.UnicodeType + stdin = sys.stdin + +def info (s, warn=1): + x = (warn and '-- ' or '') + s + try: sys.stderr.write (x + '\n') + except: sys.stderr.write (repr (x) + '\n') + +def abc_grammar (): # header, voice and lyrics grammar for ABC + #----------------------------------------------------------------- + # expressions that catch and skip some syntax errors (see corresponding parse expressions) + #----------------------------------------------------------------- + b1 = Word (u"-,'<>\u2019#", exact=1) # catch misplaced chars in chords + b2 = Regex ('[^H-Wh-w~=]*') # same in user defined symbol definition + b3 = Regex ('[^=]*') # same, second part + + #----------------------------------------------------------------- + # ABC header (field_str elements are matched later with reg. epr's) + #----------------------------------------------------------------- + + number = Word (nums).setParseAction (lambda t: int (t[0])) + field_str = Regex (r'[^]]*') # match anything until end of field + field_str.setParseAction (lambda t: t[0].strip ()) # and strip spacing + + userdef_symbol = Word (srange ('[H-Wh-w~]'), exact=1) + fieldId = oneOf ('K L M Q P I T C O A Z N G H R B D F S E r Y') # info fields + X_field = Literal ('X') + Suppress (':') + field_str + U_field = Literal ('U') + Suppress (':') + b2 + Optional (userdef_symbol, 'H') + b3 + Suppress ('=') + field_str + V_field = Literal ('V') + Suppress (':') + Word (alphanums + '_') + field_str + inf_fld = fieldId + Suppress (':') + field_str + ifield = Suppress ('[') + (X_field | U_field | V_field | inf_fld) + Suppress (']') + abc_header = OneOrMore (ifield) + StringEnd () + + #--------------------------------------------------------------------------------- + # I:score with recursive part groups and {* grand staff marker + #--------------------------------------------------------------------------------- + + voiceId = Suppress (Optional ('*')) + Word (alphanums + '_') + voice_gr = Suppress ('(') + OneOrMore (voiceId | Suppress ('|')) + Suppress (')') + simple_part = voiceId | voice_gr | Suppress ('|') + grand_staff = oneOf ('{* {') + OneOrMore (simple_part) + Suppress ('}') + part = Forward () + part_seq = OneOrMore (part | Suppress ('|')) + brace_gr = Suppress ('{') + part_seq + Suppress ('}') + bracket_gr = Suppress ('[') + part_seq + Suppress (']') + part <<= MatchFirst (simple_part | grand_staff | brace_gr | bracket_gr | Suppress ('|')) + abc_scoredef = Suppress (oneOf ('staves score')) + OneOrMore (part) + + #---------------------------------------- + # ABC lyric lines (white space sensitive) + #---------------------------------------- + + skip_note = oneOf ('* - ~') + extend_note = Literal ('_') + measure_end = Literal ('|') + syl_str = CharsNotIn ('*~-_| \t\n\\]') + syl_chars = Combine (OneOrMore (syl_str | Regex (r'\\.'))) + white = Word (' \t') + syllable = Combine (Optional ('~') + syl_chars + ZeroOrMore (Literal ('~') + syl_chars)) + Optional ('-') + lyr_elem = (syllable | skip_note | extend_note | measure_end) + Optional (white).suppress () + lyr_line = Optional (white).suppress () + ZeroOrMore (lyr_elem) + + syllable.setParseAction (lambda t: pObj ('syl', t)) + skip_note.setParseAction (lambda t: pObj ('skip', t)) + extend_note.setParseAction (lambda t: pObj ('ext', t)) + measure_end.setParseAction (lambda t: pObj ('sbar', t)) + lyr_line_wsp = lyr_line.leaveWhitespace () # parse actions must be set before calling leaveWhitespace + + #--------------------------------------------------------------------------------- + # ABC voice (not white space sensitive, beams detected in note/rest parse actions) + #--------------------------------------------------------------------------------- + + inline_field = Suppress ('[') + (inf_fld | U_field | V_field) + Suppress (']') + lyr_fld = Suppress ('[') + Suppress ('w') + Suppress (':') + lyr_line_wsp + Suppress (']') # lyric line + lyr_blk = OneOrMore (lyr_fld) # verses + fld_or_lyr = inline_field | lyr_blk # inline field or block of lyric verses + + note_length = Optional (number, 1) + Group (ZeroOrMore ('/')) + Optional (number, 2) + octaveHigh = OneOrMore ("'").setParseAction (lambda t: len(t)) + octaveLow = OneOrMore (',').setParseAction (lambda t: -len(t)) + octave = octaveHigh | octaveLow + + basenote = oneOf ('C D E F G A B c d e f g a b y') # includes spacer for parse efficiency + accidental = oneOf ('^^ __ ^ _ =') + rest_sym = oneOf ('x X z Z') + slur_beg = oneOf ("( (, (' .( .(, .('") + ~Word (nums) # no tuplet_start + slur_ends = OneOrMore (oneOf (') .)')) + + long_decoration = Combine (oneOf ('! +') + CharsNotIn ('!+ \n') + oneOf ('! +')) + staccato = Literal ('.') + ~Literal ('|') # avoid dotted barline + pizzicato = Literal ('!+!') # special case: plus sign is old style deco marker + decoration = slur_beg | staccato | userdef_symbol | long_decoration | pizzicato + decorations = OneOrMore (decoration) + + tie = oneOf ('.- -') + rest = Optional (accidental) + rest_sym + note_length + pitch = Optional (accidental) + basenote + Optional (octave, 0) + note = pitch + note_length + Optional (tie) + Optional (slur_ends) + dec_note = Optional (decorations) + pitch + note_length + Optional (tie) + Optional (slur_ends) + chord_note = dec_note | rest | b1 + grace_notes = Forward () + chord = Suppress ('[') + OneOrMore (chord_note | grace_notes) + Suppress (']') + note_length + Optional (tie) + Optional (slur_ends) + stem = note | chord | rest + + broken = Combine (OneOrMore ('<') | OneOrMore ('>')) + + tuplet_num = Suppress ('(') + number + tuplet_into = Suppress (':') + Optional (number, 0) + tuplet_notes = Suppress (':') + Optional (number, 0) + tuplet_start = tuplet_num + Optional (tuplet_into + Optional (tuplet_notes)) + + acciaccatura = Literal ('/') + grace_stem = Optional (decorations) + stem + grace_notes <<= Group (Suppress ('{') + Optional (acciaccatura) + OneOrMore (grace_stem) + Suppress ('}')) + + text_expression = Optional (oneOf ('^ _ < > @'), '^') + Optional (CharsNotIn ('"'), "") + chord_accidental = oneOf ('# b =') + triad = oneOf ('ma Maj maj M mi min m aug dim o + -') + seventh = oneOf ('7 ma7 Maj7 M7 maj7 mi7 min7 m7 dim7 o7 -7 aug7 +7 m7b5 mi7b5') + sixth = oneOf ('6 ma6 M6 mi6 min6 m6') + ninth = oneOf ('9 ma9 M9 maj9 Maj9 mi9 min9 m9') + elevn = oneOf ('11 ma11 M11 maj11 Maj11 mi11 min11 m11') + thirt = oneOf ('13 ma13 M13 maj13 Maj13 mi13 min13 m13') + suspended = oneOf ('sus sus2 sus4') + chord_degree = Combine (Optional (chord_accidental) + oneOf ('2 4 5 6 7 9 11 13')) + chord_kind = Optional (seventh | sixth | ninth | elevn | thirt | triad) + Optional (suspended) + chord_root = oneOf ('C D E F G A B') + Optional (chord_accidental) + chord_bass = oneOf ('C D E F G A B') + Optional (chord_accidental) # needs a different parse action + chordsym = chord_root + chord_kind + ZeroOrMore (chord_degree) + Optional (Suppress ('/') + chord_bass) + chord_sym = chordsym + Optional (Literal ('(') + CharsNotIn (')') + Literal (')')).suppress () + chord_or_text = Suppress ('"') + (chord_sym ^ text_expression) + Suppress ('"') + + volta_nums = Optional ('[').suppress () + Combine (Word (nums) + ZeroOrMore (oneOf (', -') + Word (nums))) + volta_text = Literal ('[').suppress () + Regex (r'"[^"]+"') + volta = volta_nums | volta_text + invisible_barline = oneOf ('[|] []') + dashed_barline = oneOf (': .|') + double_rep = Literal (':') + FollowedBy (':') # otherwise ambiguity with dashed barline + voice_overlay = Combine (OneOrMore ('&')) + bare_volta = FollowedBy (Literal ('[') + Word (nums)) # no barline, but volta follows (volta is parsed in next measure) + bar_left = (oneOf ('[|: |: [: :') + Optional (volta)) | Optional ('|').suppress () + volta | oneOf ('| [|') + bars = ZeroOrMore (':') + ZeroOrMore ('[') + OneOrMore (oneOf ('| ]')) + bar_right = invisible_barline | double_rep | Combine (bars) | dashed_barline | voice_overlay | bare_volta + + errors = ~bar_right + Optional (Word (' \n')) + CharsNotIn (':&|', exact=1) + linebreak = Literal ('$') | ~decorations + Literal ('!') # no need for I:linebreak !!! + element = fld_or_lyr | broken | decorations | stem | chord_or_text | grace_notes | tuplet_start | linebreak | errors + measure = Group (ZeroOrMore (inline_field) + Optional (bar_left) + ZeroOrMore (element) + bar_right + Optional (linebreak) + Optional (lyr_blk)) + noBarMeasure = Group (ZeroOrMore (inline_field) + Optional (bar_left) + OneOrMore (element) + Optional (linebreak) + Optional (lyr_blk)) + abc_voice = ZeroOrMore (measure) + Optional (noBarMeasure | Group (bar_left)) + ZeroOrMore (inline_field).suppress () + StringEnd () + + #---------------------------------------- + # I:percmap note [step] [midi] [note-head] + #---------------------------------------- + + white2 = (white | StringEnd ()).suppress () + w3 = Optional (white2) + percid = Word (alphanums + '-') + step = basenote + Optional (octave, 0) + pitchg = Group (Optional (accidental, '') + step + FollowedBy (white2)) + stepg = Group (step + FollowedBy (white2)) | Literal ('*') + midi = (Literal ('*') | number | pitchg | percid) + nhd = Optional (Combine (percid + Optional ('+')), '') + perc_wsp = Literal ('percmap') + w3 + pitchg + w3 + Optional (stepg, '*') + w3 + Optional (midi, '*') + w3 + nhd + abc_percmap = perc_wsp.leaveWhitespace () + + #---------------------------------------------------------------- + # Parse actions to convert all relevant results into an abstract + # syntax tree where all tree nodes are instances of pObj + #---------------------------------------------------------------- + + ifield.setParseAction (lambda t: pObj ('field', t)) + grand_staff.setParseAction (lambda t: pObj ('grand', t, 1)) # 1 = keep ordered list of results + brace_gr.setParseAction (lambda t: pObj ('bracegr', t, 1)) + bracket_gr.setParseAction (lambda t: pObj ('bracketgr', t, 1)) + voice_gr.setParseAction (lambda t: pObj ('voicegr', t, 1)) + voiceId.setParseAction (lambda t: pObj ('vid', t, 1)) + abc_scoredef.setParseAction (lambda t: pObj ('score', t, 1)) + note_length.setParseAction (lambda t: pObj ('dur', (t[0], (t[2] << len (t[1])) >> 1))) + chordsym.setParseAction (lambda t: pObj ('chordsym', t)) + chord_root.setParseAction (lambda t: pObj ('root', t)) + chord_kind.setParseAction (lambda t: pObj ('kind', t)) + chord_degree.setParseAction (lambda t: pObj ('degree', t)) + chord_bass.setParseAction (lambda t: pObj ('bass', t)) + text_expression.setParseAction (lambda t: pObj ('text', t)) + inline_field.setParseAction (lambda t: pObj ('inline', t)) + lyr_fld.setParseAction (lambda t: pObj ('lyr_fld', t, 1)) + lyr_blk.setParseAction (lambda t: pObj ('lyr_blk', t, 1)) # 1 = keep ordered list of lyric lines + grace_notes.setParseAction (doGrace) + acciaccatura.setParseAction (lambda t: pObj ('accia', t)) + note.setParseAction (noteActn) + rest.setParseAction (restActn) + decorations.setParseAction (lambda t: pObj ('deco', t)) + pizzicato.setParseAction (lambda t: ['!plus!']) # translate !+! + slur_ends.setParseAction (lambda t: pObj ('slurs', t)) + chord.setParseAction (lambda t: pObj ('chord', t, 1)) + dec_note.setParseAction (noteActn) + tie.setParseAction (lambda t: pObj ('tie', t)) + pitch.setParseAction (lambda t: pObj ('pitch', t)) + bare_volta.setParseAction (lambda t: ['|']) # return barline that user forgot + dashed_barline.setParseAction (lambda t: ['.|']) + bar_right.setParseAction (lambda t: pObj ('rbar', t)) + bar_left.setParseAction (lambda t: pObj ('lbar', t)) + broken.setParseAction (lambda t: pObj ('broken', t)) + tuplet_start.setParseAction (lambda t: pObj ('tup', t)) + linebreak.setParseAction (lambda t: pObj ('linebrk', t)) + measure.setParseAction (doMaat) + noBarMeasure.setParseAction (doMaat) + b1.setParseAction (errorWarn) + b2.setParseAction (errorWarn) + b3.setParseAction (errorWarn) + errors.setParseAction (errorWarn) + + return abc_header, abc_voice, abc_scoredef, abc_percmap + +class pObj (object): # every relevant parse result is converted into a pObj + def __init__ (s, name, t, seq=0): # t = list of nested parse results + s.name = name # name uniqueliy identifies this pObj + rest = [] # collect parse results that are not a pObj + attrs = {} # new attributes + for x in t: # nested pObj's become attributes of this pObj + if type (x) == pObj: + attrs [x.name] = attrs.get (x.name, []) + [x] + else: + rest.append (x) # collect non-pObj's (mostly literals) + for name, xs in attrs.items (): + if len (xs) == 1: xs = xs[0] # only list if more then one pObj + setattr (s, name, xs) # create the new attributes + s.t = rest # all nested non-pObj's (mostly literals) + s.objs = seq and t or [] # for nested ordered (lyric) pObj's + + def __repr__ (s): # make a nice string representation of a pObj + r = [] + for nm in dir (s): + if nm.startswith ('_'): continue # skip build in attributes + elif nm == 'name': continue # redundant + else: + x = getattr (s, nm) + if not x: continue # s.t may be empty (list of non-pObj's) + if type (x) == list_type: r.extend (x) + else: r.append (x) + xs = [] + for x in r: # recursively call __repr__ and convert all strings to latin-1 + if isinstance (x, str_type): xs.append (x) # string -> no recursion + else: xs.append (repr (x)) # pObj -> recursive call + return '(' + s.name + ' ' +','.join (xs) + ')' + +global prevloc # global to remember previous match position of a note/rest +prevloc = 0 +def detectBeamBreak (line, loc, t): + global prevloc # location in string 'line' of previous note match + xs = line[prevloc:loc+1] # string between previous and current note match + xs = xs.lstrip () # first note match starts on a space! + prevloc = loc # location in string 'line' of current note match + b = pObj ('bbrk', [' ' in xs]) # space somewhere between two notes -> beambreak + t.insert (0, b) # insert beambreak as a nested parse result + +def noteActn (line, loc, t): # detect beambreak between previous and current note/rest + if 'y' in t[0].t: return [] # discard spacer + detectBeamBreak (line, loc, t) # adds beambreak to parse result t as side effect + return pObj ('note', t) + +def restActn (line, loc, t): # detect beambreak between previous and current note/rest + detectBeamBreak (line, loc, t) # adds beambreak to parse result t as side effect + return pObj ('rest', t) + +def errorWarn (line, loc, t): # warning for misplaced symbols and skip them + if not t[0]: return [] # only warn if catched string not empty + info ('**misplaced symbol: %s' % t[0], warn=0) + lineCopy = line [:] + if loc > 40: + lineCopy = line [loc - 40: loc + 40] + loc = 40 + info (lineCopy.replace ('\n', ' '), warn=0) + info (loc * '-' + '^', warn=0) + return [] + +#------------------------------------------------------------- +# transformations of a measure (called by parse action doMaat) +#------------------------------------------------------------- + +def simplify (a, b): # divide a and b by their greatest common divisor + x, y = a, b + while b: a, b = b, a % b + return x // a, y // a + +def doBroken (prev, brk, x): + if not prev: info ('error in broken rhythm: %s' % x); return # no changes + nom1, den1 = prev.dur.t # duration of first note/chord + nom2, den2 = x.dur.t # duration of second note/chord + if brk == '>': + nom1, den1 = simplify (3 * nom1, 2 * den1) + nom2, den2 = simplify (1 * nom2, 2 * den2) + elif brk == '<': + nom1, den1 = simplify (1 * nom1, 2 * den1) + nom2, den2 = simplify (3 * nom2, 2 * den2) + elif brk == '>>': + nom1, den1 = simplify (7 * nom1, 4 * den1) + nom2, den2 = simplify (1 * nom2, 4 * den2) + elif brk == '<<': + nom1, den1 = simplify (1 * nom1, 4 * den1) + nom2, den2 = simplify (7 * nom2, 4 * den2) + else: return # give up + prev.dur.t = nom1, den1 # change duration of previous note/chord + x.dur.t = nom2, den2 # and current note/chord + +def convertBroken (t): # convert broken rhythms to normal note durations + prev = None # the last note/chord before the broken symbol + brk = '' # the broken symbol + remove = [] # indexes to broken symbols (to be deleted) in measure + for i, x in enumerate (t): # scan all elements in measure + if x.name == 'note' or x.name == 'chord' or x.name == 'rest': + if brk: # a broken symbol was encountered before + doBroken (prev, brk, x) # change duration previous note/chord/rest and current one + brk = '' + else: + prev = x # remember the last note/chord/rest + elif x.name == 'broken': + brk = x.t[0] # remember the broken symbol (=string) + remove.insert (0, i) # and its index, highest index first + for i in remove: del t[i] # delete broken symbols from high to low + +def ptc2midi (n): # convert parsed pitch attribute to a midi number + pt = getattr (n, 'pitch', '') + if pt: + p = pt.t + if len (p) == 3: acc, step, oct = p + else: acc = ''; step, oct = p + nUp = step.upper () + oct = (4 if nUp == step else 5) + int (oct) + midi = oct * 12 + [0,2,4,5,7,9,11]['CDEFGAB'.index (nUp)] + {'^':1,'_':-1}.get (acc, 0) + 12 + else: midi = 130 # all non pitch objects first + return midi + +def convertChord (t): # convert chord to sequence of notes in musicXml-style + ins = [] + for i, x in enumerate (t): + if x.name == 'chord': + if hasattr (x, 'rest') and not hasattr (x, 'note'): # chords containing only rests + if type (x.rest) == list_type: x.rest = x.rest[0] # more rests == one rest + ins.insert (0, (i, [x.rest])) # just output a single rest, no chord + continue + num1, den1 = x.dur.t # chord duration + tie = getattr (x, 'tie', None) # chord tie + slurs = getattr (x, 'slurs', []) # slur endings + if type (x.note) != list_type: x.note = [x.note] # when chord has only one note ... + elms = []; j = 0 # sort chord notes, highest first + nss = sorted (x.objs, key = ptc2midi, reverse=1) if mxm.orderChords else x.objs + for nt in nss: # all chord elements (note | decorations | rest | grace note) + if nt.name == 'note': + num2, den2 = nt.dur.t # note duration * chord duration + nt.dur.t = simplify (num1 * num2, den1 * den2) + if tie: nt.tie = tie # tie on all chord notes + if j == 0 and slurs: nt.slurs = slurs # slur endings only on first chord note + if j > 0: nt.chord = pObj ('chord', [1]) # label all but first as chord notes + else: # remember all pitches of the chord in the first note + pitches = [n.pitch for n in x.note] # to implement conversion of erroneous ties to slurs + nt.pitches = pObj ('pitches', pitches) + j += 1 + if nt.name not in ['dur','tie','slurs','rest']: elms.append (nt) + ins.insert (0, (i, elms)) # chord position, [note|decotation|grace note] + for i, notes in ins: # insert from high to low + for nt in reversed (notes): + t.insert (i+1, nt) # insert chord notes after chord + del t[i] # remove chord itself + +def doMaat (t): # t is a Group() result -> the measure is in t[0] + convertBroken (t[0]) # remove all broken rhythms and convert to normal durations + convertChord (t[0]) # replace chords by note sequences in musicXML style + +def doGrace (t): # t is a Group() result -> the grace sequence is in t[0] + convertChord (t[0]) # a grace sequence may have chords + for nt in t[0]: # flag all notes within the grace sequence + if nt.name == 'note': nt.grace = 1 # set grace attribute + return t[0] # ungroup the parse result +#-------------------- +# musicXML generation +#---------------------------------- + +def compChordTab (): # avoid some typing work: returns mapping constant {ABC chordsyms -> musicXML kind} + maj, min, aug, dim, dom, ch7, ch6, ch9, ch11, ch13, hd = 'major minor augmented diminished dominant -seventh -sixth -ninth -11th -13th half-diminished'.split () + triad = zip ('ma Maj maj M mi min m aug dim o + -'.split (), [maj, maj, maj, maj, min, min, min, aug, dim, dim, aug, min]) + seventh = zip ('7 ma7 Maj7 M7 maj7 mi7 min7 m7 dim7 o7 -7 aug7 +7 m7b5 mi7b5'.split (), + [dom, maj+ch7, maj+ch7, maj+ch7, maj+ch7, min+ch7, min+ch7, min+ch7, dim+ch7, dim+ch7, min+ch7, aug+ch7, aug+ch7, hd, hd]) + sixth = zip ('6 ma6 M6 mi6 min6 m6'.split (), [maj+ch6, maj+ch6, maj+ch6, min+ch6, min+ch6, min+ch6]) + ninth = zip ('9 ma9 M9 maj9 Maj9 mi9 min9 m9'.split (), [dom+ch9, maj+ch9, maj+ch9, maj+ch9, maj+ch9, min+ch9, min+ch9, min+ch9]) + elevn = zip ('11 ma11 M11 maj11 Maj11 mi11 min11 m11'.split (), [dom+ch11, maj+ch11, maj+ch11, maj+ch11, maj+ch11, min+ch11, min+ch11, min+ch11]) + thirt = zip ('13 ma13 M13 maj13 Maj13 mi13 min13 m13'.split (), [dom+ch13, maj+ch13, maj+ch13, maj+ch13, maj+ch13, min+ch13, min+ch13, min+ch13]) + sus = zip ('sus sus4 sus2'.split (), ['suspended-fourth', 'suspended-fourth', 'suspended-second']) + return dict (list (triad) + list (seventh) + list (sixth) + list (ninth) + list (elevn) + list (thirt) + list (sus)) + +def addElem (parent, child, level): + indent = 2 + chldrn = list (parent) + if chldrn: + chldrn[-1].tail += indent * ' ' + else: + parent.text = '\n' + level * indent * ' ' + parent.append (child) + child.tail = '\n' + (level-1) * indent * ' ' + +def addElemT (parent, tag, text, level): + e = E.Element (tag) + e.text = text + addElem (parent, e, level) + return e + +def mkTmod (tmnum, tmden, lev): + tmod = E.Element ('time-modification') + addElemT (tmod, 'actual-notes', str (tmnum), lev + 1) + addElemT (tmod, 'normal-notes', str (tmden), lev + 1) + return tmod + +def addDirection (parent, elems, lev, gstaff, subelms=[], placement='below', cue_on=0): + dir = E.Element ('direction', placement=placement) + addElem (parent, dir, lev) + if type (elems) != list_type: elems = [(elems, subelms)] # ugly hack to provide for multiple direction types + for elem, subelms in elems: # add direction types + typ = E.Element ('direction-type') + addElem (dir, typ, lev + 1) + addElem (typ, elem, lev + 2) + for subel in subelms: addElem (elem, subel, lev + 3) + if cue_on: addElem (dir, E.Element ('level', size='cue'), lev + 1) + if gstaff: addElemT (dir, 'staff', str (gstaff), lev + 1) + return dir + +def removeElems (root_elem, parent_str, elem_str): + for p in root_elem.findall (parent_str): + e = p.find (elem_str) + if e != None: p.remove (e) + +def alignLyr (vce, lyrs): + empty_el = pObj ('leeg', '*') + for k, lyr in enumerate (lyrs): # lyr = one full line of lyrics + i = 0 # syl counter + for elem in vce: # reiterate the voice block for each lyrics line + if elem.name == 'note' and not (hasattr (elem, 'chord') or hasattr (elem, 'grace')): + if i >= len (lyr): lr = empty_el + else: lr = lyr [i] + lr.t[0] = lr.t[0].replace ('%5d',']') + elem.objs.append (lr) + if lr.name != 'sbar': i += 1 + if elem.name == 'rbar' and i < len (lyr) and lyr[i].name == 'sbar': i += 1 + return vce + +slur_move = re.compile (r'(?<][<>]?)(\)+)') # (? I: + x2 = r1.sub ('', x) # remove comment + while x2.endswith ('*') and not (x2.startswith ('w:') or x2.startswith ('+:') or 'percmap' in x2): + x2 = x2[:-1] # remove old syntax for right adjusting + if not x2: continue # empty line + if x2[:2] == 'W:': + field = x2 [2:].strip () + ftype = mxm.metaMap.get ('W', 'W') # respect the (user defined --meta) mapping of various ABC fields to XML meta data types + c = mxm.metadata.get (ftype, '') + mxm.metadata [ftype] = c + '\n' + field if c else field # concatenate multiple info fields with new line as separator + continue # skip W: lyrics + if x2[:2] == '+:': # field continuation + fln += x2[2:] + continue + ro = r2.match (x2) # single field on a line + if ro: # field -> inline_field, escape all ']' + if fcont: # old style \-info-continuation active + fcont = x2 [-1] == '\\' # possible further \-info-continuation + fln += re.sub (r'^.:(.*?)\\*$', r'\1', x2) # add continuation, remove .: and \ + continue + if fln: mln += escField (fln) + if x2.startswith ('['): x2 = x2.strip ('[]') + fcont = x2 [-1] == '\\' # first encounter of old style \-info-continuation + fln = x2.rstrip ('\\') # remove continuation from field and inline brackets + continue + if nx == 1: # x2 is a new music line + fcont = 0 # stop \-continuations (-> only adjacent \-info-continuations are joined) + if fln: + mln += escField (fln) + fln = '' + if mcont: + mcont = x2 [-1] == '\\' + mln += x2.rstrip ('\\') + else: + if mln: xs.append (mln); mln = '' + mcont = x2 [-1] == '\\' + mln = x2.rstrip ('\\') + if not mcont: xs.append (mln); mln = '' + if fln: mln += escField (fln) + if mln: xs.append (mln) + + hs = re.split (r'(\[K:[^]]*\])', xs [0]) # look for end of header K: + if len (hs) == 1: header = hs[0]; xs [0] = '' # no K: present + else: header = hs [0] + hs [1]; xs [0] = ''.join (hs[2:]) # h[1] is the first K: + abctext = '\n'.join (xs) # the rest is body text + hfs, vfs = [], [] + for x in header[1:-1].split (']['): + if x[0] == 'V': vfs.append (x) # filter voice- and midi-definitions + elif x[:6] == 'I:MIDI': vfs.append (x) # from the header to vfs + elif x[:9] == 'I:percmap': vfs.append (x) # and also percmap + else: hfs.append (x) # all other fields stay in header + header = '[' + ']['.join (hfs) + ']' # restore the header + abctext = ('[' + ']['.join (vfs) + ']' if vfs else '') + abctext # prepend voice/midi from header before abctext + + xs = abctext.split ('[V:') + if len (xs) == 1: abctext = '[V:1]' + abctext # abc has no voice defs at all + elif re.sub (r'\[[A-Z]:[^]]*\]', '', xs[0]).strip (): # remove inline fields from starting text, if any + abctext = '[V:1]' + abctext # abc with voices has no V: at start + + r1 = re.compile (r'\[V:\s*(\S*)[ \]]') # get voice id from V: field (skip spaces betwee V: and ID) + vmap = {} # {voice id -> [voice abc string]} + vorder = {} # mark document order of voices + xs = re.split (r'(\[V:[^]]*\])', abctext) # split on every V-field (V-fields included in split result list) + if len (xs) == 1: raise ValueError ('bugs ...') + else: + pm = re.findall (r'\[P:.\]', xs[0]) # all P:-marks after K: but before first V: + if pm: xs[2] = ''.join (pm) + xs[2] # prepend P:-marks to the text of the first voice + header += re.sub (r'\[P:.\]', '', xs[0]) # clear all P:-marks from text between K: and first V: and put text in the header + i = 1 + while i < len (xs): # xs = ['', V-field, voice abc, V-field, voice abc, ...] + vce, abc = xs[i:i+2] + id = r1.search (vce).group (1) # get voice ID from V-field + if not id: id, vce = '1', '[V:1]' # voice def has no ID + vmap[id] = vmap.get (id, []) + [vce, abc] # collect abc-text for each voice id (include V-fields) + if id not in vorder: vorder [id] = i # store document order of first occurrence of voice id + i += 2 + voices = [] + ixs = sorted ([(i, id) for id, i in vorder.items ()]) # restore document order of voices + for i, id in ixs: + voice = ''.join (vmap [id]) # all abc of one voice + voice = fixSlurs (voice) # put slurs right after the notes + voices.append ((id, voice)) + return header, voices + +def mergeMeasure (m1, m2, slur_offset, voice_offset, rOpt, is_grand=0, is_overlay=0): + slurs = m2.findall ('note/notations/slur') + for slr in slurs: + slrnum = int (slr.get ('number')) + slur_offset + slr.set ('number', str (slrnum)) # make unique slurnums in m2 + vs = m2.findall ('note/voice') # set all voice number elements in m2 + for v in vs: v.text = str (voice_offset + int (v.text)) + ls = m1.findall ('note/lyric') # all lyric elements in m1 + lnum_max = max ([int (l.get ('number')) for l in ls] + [0]) # highest lyric number in m1 + ls = m2.findall ('note/lyric') # update lyric elements in m2 + for el in ls: + n = int (el.get ('number')) + el.set ('number', str (n + lnum_max)) + ns = m1.findall ('note') # determine the total duration of m1, subtract all backups + dur1 = sum (int (n.find ('duration').text) for n in ns + if n.find ('grace') == None and n.find ('chord') == None) + dur1 -= sum (int (b.text) for b in m1.findall ('backup/duration')) + repbar, nns, es = 0, 0, [] # nns = number of real notes in m2 + for e in list (m2): # scan all elements of m2 + if e.tag == 'attributes': + if not is_grand: continue # no attribute merging for normal voices + else: nns += 1 # but we do merge (clef) attributes for a grand staff + if e.tag == 'print': continue + if e.tag == 'note' and (rOpt or e.find ('rest') == None): nns += 1 + if e.tag == 'barline' and e.find ('repeat') != None: repbar = e; + es.append (e) # buffer elements to be merged + if nns > 0: # only merge if m2 contains any real notes + if dur1 > 0: # only insert backup if duration of m1 > 0 + b = E.Element ('backup') + addElem (m1, b, level=3) + addElemT (b, 'duration', str (dur1), level=4) + for e in es: addElem (m1, e, level=3) # merge buffered elements of m2 + elif is_overlay and repbar: addElem (m1, repbar, level=3) # merge repeat in empty overlay + +def mergePartList (parts, rOpt, is_grand=0): # merge parts, make grand staff when is_grand true + + def delAttrs (part): # for the time being we only keep clef attributes + xs = [(m, e) for m in part.findall ('measure') for e in m.findall ('attributes')] + for m, e in xs: + for c in list (e): + if c.tag == 'clef': continue # keep clef attribute + if c.tag == 'staff-details': continue # keep staff-details attribute + e.remove (c) # delete all other attrinutes for higher staff numbers + if len (list (e)) == 0: m.remove (e) # remove empty attributes element + + p1 = parts[0] + for p2 in parts[1:]: + if is_grand: delAttrs (p2) # delete all attributes except clef + for i in range (len (p1) + 1, len (p2) + 1): # second part longer than first one + maat = E.Element ('measure', number = str(i)) # append empty measures + addElem (p1, maat, 2) + slurs = p1.findall ('measure/note/notations/slur') # find highest slur num in first part + slur_max = max ([int (slr.get ('number')) for slr in slurs] + [0]) + vs = p1.findall ('measure/note/voice') # all voice number elements in first part + vnum_max = max ([int (v.text) for v in vs] + [0]) # highest voice number in first part + for im, m2 in enumerate (p2.findall ('measure')): # merge all measures of p2 into p1 + mergeMeasure (p1[im], m2, slur_max, vnum_max, rOpt, is_grand) # may change slur numbers in p1 + return p1 + +def mergeParts (parts, vids, staves, rOpt, is_grand=0): + if not staves: return parts, vids # no voice mapping + partsnew, vidsnew = [], [] + for voice_ids in staves: + pixs = [] + for vid in voice_ids: + if vid in vids: pixs.append (vids.index (vid)) + else: info ('score partname %s does not exist' % vid) + if pixs: + xparts = [parts[pix] for pix in pixs] + if len (xparts) > 1: mergedpart = mergePartList (xparts, rOpt, is_grand) + else: mergedpart = xparts [0] + partsnew.append (mergedpart) + vidsnew.append (vids [pixs[0]]) + return partsnew, vidsnew + +def mergePartMeasure (part, msre, ovrlaynum, rOpt): # merge msre into last measure of part, only for overlays + slur_offset = 0; # slur numbers determined by the slurstack size (as in a single voice) + last_msre = list (part)[-1] # last measure in part + mergeMeasure (last_msre, msre, slur_offset, ovrlaynum, rOpt, is_overlay=1) # voice offset = s.overlayVNum + +def pushSlur (boogStapel, stem): + if stem not in boogStapel: boogStapel [stem] = [] # initialize slurstack for stem + boognum = sum (map (len, boogStapel.values ())) + 1 # number of open slurs in all (overlay) voices + boogStapel [stem].append (boognum) + return boognum + +def setFristVoiceNameFromGroup (vids, vdefs): # vids = [vid], vdef = {vid -> (name, subname, voicedef)} + vids = [v for v in vids if v in vdefs] # only consider defined voices + if not vids: return vdefs + vid0 = vids [0] # first vid of the group + _, _, vdef0 = vdefs [vid0] # keep de voice definition (vdef0) when renaming vid0 + for vid in vids: + nm, snm, vdef = vdefs [vid] + if nm: # first non empty name encountered will become + vdefs [vid0] = nm, snm, vdef0 # name of merged group == name of first voice in group (vid0) + break + return vdefs + +def mkGrand (p, vdefs): # transform parse subtree into list needed for s.grands + xs = [] + for i, x in enumerate (p.objs): # changing p.objs [i] alters the tree. changing x has no effect on the tree. + if type (x) == pObj: + us = mkGrand (x, vdefs) # first get transformation results of current pObj + if x.name == 'grand': # x.objs contains ordered list of nested parse results within x + vids = [y.objs[0] for y in x.objs[1:]] # the voice ids in the grand staff + nms = [vdefs [u][0] for u in vids if u in vdefs] # the names of those voices + accept = sum ([1 for nm in nms if nm]) == 1 # accept as grand staff when only one of the voices has a name + if accept or us[0] == '{*': + xs.append (us[1:]) # append voice ids as a list (discard first item '{' or '{*') + vdefs = setFristVoiceNameFromGroup (vids, vdefs) + p.objs [i] = x.objs[1] # replace voices by first one in the grand group (this modifies the parse tree) + else: + xs.extend (us[1:]) # extend current result with all voice ids of rejected grand staff + else: xs.extend (us) # extend current result with transformed pObj + else: xs.append (p.t[0]) # append the non pObj (== voice id string) + return xs + +def mkStaves (p, vdefs): # transform parse tree into list needed for s.staves + xs = [] + for i, x in enumerate (p.objs): # structure and comments identical to mkGrand + if type (x) == pObj: + us = mkStaves (x, vdefs) + if x.name == 'voicegr': + xs.append (us) + vids = [y.objs[0] for y in x.objs] + vdefs = setFristVoiceNameFromGroup (vids, vdefs) + p.objs [i] = x.objs[0] + else: + xs.extend (us) + else: + if p.t[0] not in '{*': xs.append (p.t[0]) + return xs + +def mkGroups (p): # transform parse tree into list needed for s.groups + xs = [] + for x in p.objs: + if type (x) == pObj: + if x.name == 'vid': xs.extend (mkGroups (x)) + elif x.name == 'bracketgr': xs.extend (['['] + mkGroups (x) + [']']) + elif x.name == 'bracegr': xs.extend (['{'] + mkGroups (x) + ['}']) + else: xs.extend (mkGroups (x) + ['}']) # x.name == 'grand' == rejected grand staff + else: + xs.append (p.t[0]) + return xs + +def stepTrans (step, soct, clef): # [A-G] (1...8) + if clef.startswith ('bass'): + nm7 = 'C,D,E,F,G,A,B'.split (',') + n = 14 + nm7.index (step) - 12 # two octaves extra to avoid negative numbers + step, soct = nm7 [n % 7], soct + n // 7 - 2 # subtract two octaves again + return step, soct + +def reduceMids (parts, vidsnew, midiInst): # remove redundant instruments from a part + for pid, part in zip (vidsnew, parts): + mids, repls, has_perc = {}, {}, 0 + for ipid, ivid, ch, prg, vol, pan in sorted (list (midiInst.values ())): + if ipid != pid: continue # only instruments from part pid + if ch == '10': has_perc = 1; continue # only consider non percussion instruments + instId, inst = 'I%s-%s' % (ipid, ivid), (ch, prg) + if inst in mids: # midi instrument already defined in this part + repls [instId] = mids [inst] # remember to replace instId by inst (see below) + del midiInst [instId] # instId is redundant + else: mids [inst] = instId # collect unique instruments in this part + if len (mids) < 2 and not has_perc: # only one instrument used -> no instrument tags needed in notes + removeElems (part, 'measure/note', 'instrument') # no instrument tag needed for one- or no-instrument parts + else: + for e in part.findall ('measure/note/instrument'): + id = e.get ('id') # replace all redundant instrument Id's + if id in repls: e.set ('id', repls [id]) + +class stringAlloc: + def __init__ (s): + s.snaarVrij = [] # [[(t1, t2) ...] for each string ] + s.snaarIx = [] # index in snaarVrij for each string + s.curstaff = -1 # staff being allocated + def beginZoek (s): # reset snaarIx at start of each voice + s.snaarIx = [] + for i in range (len (s.snaarVrij)): s.snaarIx.append (0) + def setlines (s, stflines, stfnum): + if stfnum != s.curstaff: # initialize for new staff + s.curstaff = stfnum + s.snaarVrij = [] + for i in range (stflines): s.snaarVrij.append ([]) + s.beginZoek () + def isVrij (s, snaar, t1, t2): # see if string snaar is free between t1 and t2 + xs = s.snaarVrij [snaar] + for i in range (s.snaarIx [snaar], len (xs)): + tb, te = xs [i] + if t1 >= te: continue # te_prev < t1 <= te + if t1 >= tb: s.snaarIx [snaar] = i; return 0 # tb <= t1 < te + if t2 > tb: s.snaarIx [snaar] = i; return 0 # t1 < tb < t2 + s.snaarIx [snaar] = i; # remember position for next call + xs.insert (i, (t1,t2)) # te_prev < t1 < t2 < tb + return 1 + xs.append ((t1,t2)) + s.snaarIx [snaar] = len (xs) - 1 + return 1 + def bezet (s, snaar, t1, t2): # force allocation of note (t1,t2) on string snaar + xs = s.snaarVrij [snaar] + for i, (tb, te) in enumerate (xs): + if t1 >= te: continue # te_prev < t1 <= te + xs.insert (i, (t1, t2)) + return + xs.append ((t1,t2)) + +class MusicXml: + typeMap = {1:'long', 2:'breve', 4:'whole', 8:'half', 16:'quarter', 32:'eighth', 64:'16th', 128:'32nd', 256:'64th'} + dynaMap = {'p':1,'pp':1,'ppp':1,'pppp':1,'f':1,'ff':1,'fff':1,'ffff':1,'mp':1,'mf':1,'sfz':1} + tempoMap = {'larghissimo':40, 'moderato':104, 'adagissimo':44, 'allegretto':112, 'lentissimo':48, 'allegro':120, 'largo':56, + 'vivace':168, 'adagio':59, 'vivo':180, 'lento':62, 'presto':192, 'larghetto':66, 'allegrissimo':208, 'adagietto':76, + 'vivacissimo':220, 'andante':88, 'prestissimo':240, 'andantino':96} + wedgeMap = {'>(':1, '>)':1, '<(':1,'<)':1,'crescendo(':1,'crescendo)':1,'diminuendo(':1,'diminuendo)':1} + artMap = {'.':'staccato','>':'accent','accent':'accent','wedge':'staccatissimo','tenuto':'tenuto', + 'breath':'breath-mark','marcato':'strong-accent','^':'strong-accent','slide':'scoop'} + ornMap = {'trill':'trill-mark','T':'trill-mark','turn':'turn','uppermordent':'inverted-mordent','lowermordent':'mordent', + 'pralltriller':'inverted-mordent','mordent':'mordent','turn':'turn','invertedturn':'inverted-turn'} + tecMap = {'upbow':'up-bow', 'downbow':'down-bow', 'plus':'stopped','open':'open-string','snap':'snap-pizzicato', + 'thumb':'thumb-position'} + capoMap = {'fine':('Fine','fine','yes'), 'D.S.':('D.S.','dalsegno','segno'), 'D.C.':('D.C.','dacapo','yes'),'dacapo':('D.C.','dacapo','yes'), + 'dacoda':('To Coda','tocoda','coda'), 'coda':('coda','coda','coda'), 'segno':('segno','segno','segno')} + sharpness = ['Fb', 'Cb','Gb','Db','Ab','Eb','Bb','F','C','G','D','A', 'E', 'B', 'F#','C#','G#','D#','A#','E#','B#'] + offTab = {'maj':8, 'm':11, 'min':11, 'mix':9, 'dor':10, 'phr':12, 'lyd':7, 'loc':13} + modTab = {'maj':'major', 'm':'minor', 'min':'minor', 'mix':'mixolydian', 'dor':'dorian', 'phr':'phrygian', 'lyd':'lydian', 'loc':'locrian'} + clefMap = { 'alto1':('C','1'), 'alto2':('C','2'), 'alto':('C','3'), 'alto4':('C','4'), 'tenor':('C','4'), + 'bass3':('F','3'), 'bass':('F','4'), 'treble':('G','2'), 'perc':('percussion',''), 'none':('',''), 'tab':('TAB','5')} + clefLineMap = {'B':'treble', 'G':'alto1', 'E':'alto2', 'C':'alto', 'A':'tenor', 'F':'bass3', 'D':'bass'} + alterTab = {'=':'0', '_':'-1', '__':'-2', '^':'1', '^^':'2'} + accTab = {'=':'natural', '_':'flat', '__':'flat-flat', '^':'sharp', '^^':'sharp-sharp'} + chordTab = compChordTab () + uSyms = {'~':'roll', 'H':'fermata','L':'>','M':'lowermordent','O':'coda', + 'P':'uppermordent','S':'segno','T':'trill','u':'upbow','v':'downbow'} + pageFmtDef = [0.75,297,210,18,18,10,10] # the abcm2ps page formatting defaults for A4 + metaTab = {'O':'origin', 'A':'area', 'Z':'transcription', 'N':'notes', 'G':'group', 'H':'history', 'R':'rhythm', + 'B':'book', 'D':'discography', 'F':'fileurl', 'S':'source', 'P':'partmap', 'W':'lyrics'} + metaMap = {'C':'composer'} # mapping of composer is fixed + metaTypes = {'composer':1,'lyricist':1,'poet':1,'arranger':1,'translator':1, 'rights':1} # valid MusicXML meta data types + tuningDef = 'E2,A2,D3,G3,B3,E4'.split (',') # default string tuning (guitar) + + def __init__ (s): + s.pageFmtCmd = [] # set by command line option -p + s.reset () + def reset (s, fOpt=False): + s.divisions = 2520 # xml duration of 1/4 note, 2^3 * 3^2 * 5 * 7 => 5,7,9 tuplets + s.ties = {} # {abc pitch tuple -> alteration} for all open ties + s.slurstack = {} # stack of open slur numbers per (overlay) voice + s.slurbeg = [] # type of slurs to start (when slurs are detected at element-level) + s.tmnum = 0 # time modification, numerator + s.tmden = 0 # time modification, denominator + s.ntup = 0 # number of tuplet notes remaining + s.trem = 0 # number of bars for tremolo + s.intrem = 0 # mark tremolo sequence (for duration doubling) + s.tupnts = [] # all tuplet modifiers with corresp. durations: [(duration, modifier), ...] + s.irrtup = 0 # 1 if an irregular tuplet + s.ntype = '' # the normal-type of a tuplet (== duration type of a normal tuplet note) + s.unitL = (1, 8) # default unit length + s.unitLcur = (1, 8) # unit length of current voice + s.keyAlts = {} # alterations implied by key + s.msreAlts = {} # temporarily alterations + s.curVolta = '' # open volta bracket + s.title = '' # title of music + s.creator = {} # {creator-type -> creator string} + s.metadata = {} # {metadata-type -> string} + s.lyrdash = {} # {lyric number -> 1 if dash between syllables} + s.usrSyms = s.uSyms # user defined symbols + s.prevNote = None # xml element of previous beamed note to correct beams (start, continue) + s.prevLyric = {} # xml element of previous lyric to add/correct extend type (start, continue) + s.grcbbrk = False # remember any bbrk in a grace sequence + s.linebrk = 0 # 1 if next measure should start with a line break + s.nextdecos = [] # decorations for the next note + s.prevmsre = None # the previous measure + s.supports_tag = 0 # issue supports-tag in xml file when abc uses explicit linebreaks + s.staveDefs = [] # collected %%staves or %%score instructions from score + s.staves = [] # staves = [[voice names to be merged into one stave]] + s.groups = [] # list of merged part names with interspersed {[ and }] + s.grands = [] # [[vid1, vid2, ..], ...] voiceIds to be merged in a grand staff + s.gStaffNums = {} # map each voice id in a grand staff to a staff number + s.gNstaves = {} # map each voice id in a grand staff to total number of staves + s.pageFmtAbc = [] # formatting from abc directives + s.mdur = (4,4) # duration of one measure + s.gtrans = 0 # octave transposition (by clef) + s.midprg = ['', '', '', ''] # MIDI channel nr, program nr, volume, panning for the current part + s.vid = '' # abc voice id for the current voice + s.pid = '' # xml part id for the current voice + s.gcue_on = 0 # insert tag in each note + s.percVoice = 0 # 1 if percussion enabled + s.percMap = {} # (part-id, abc_pitch, xml-octave) -> (abc staff step, midi note number, xml notehead) + s.pMapFound = 0 # at least one I:percmap has been found + s.vcepid = {} # voice_id -> part_id + s.midiInst = {} # inst_id -> (part_id, voice_id, channel, midi_number), remember instruments used + s.capo = 0 # fret position of the capodastro + s.tunmid = [] # midi numbers of strings + s.tunTup = [] # ordered midi numbers of strings [(midi_num, string_num), ...] (midi_num from high to low) + s.fOpt = fOpt # force string/fret allocations for tab staves + s.orderChords = 0 # order notes in a chord + s.chordDecos = {} # decos that should be distributed to all chord notes for xml + ch10 = 'acoustic-bass-drum,35;bass-drum-1,36;side-stick,37;acoustic-snare,38;hand-clap,39;electric-snare,40;low-floor-tom,41;closed-hi-hat,42;high-floor-tom,43;pedal-hi-hat,44;low-tom,45;open-hi-hat,46;low-mid-tom,47;hi-mid-tom,48;crash-cymbal-1,49;high-tom,50;ride-cymbal-1,51;chinese-cymbal,52;ride-bell,53;tambourine,54;splash-cymbal,55;cowbell,56;crash-cymbal-2,57;vibraslap,58;ride-cymbal-2,59;hi-bongo,60;low-bongo,61;mute-hi-conga,62;open-hi-conga,63;low-conga,64;high-timbale,65;low-timbale,66;high-agogo,67;low-agogo,68;cabasa,69;maracas,70;short-whistle,71;long-whistle,72;short-guiro,73;long-guiro,74;claves,75;hi-wood-block,76;low-wood-block,77;mute-cuica,78;open-cuica,79;mute-triangle,80;open-triangle,81' + s.percsnd = [x.split (',') for x in ch10.split (';')] # {name -> midi number} of standard channel 10 sound names + s.gTime = (0,0) # (XML begin time, XML end time) in divisions + s.tabStaff = '' # == pid (part ID) for a tab staff + + def mkPitch (s, acc, note, oct, lev): + if s.percVoice: # percussion map switched off by perc=off (see doClef) + octq = int (oct) + s.gtrans # honour the octave= transposition when querying percmap + tup = s.percMap.get ((s.pid, acc+note, octq), s.percMap.get (('', acc+note, octq), 0)) + if tup: step, soct, midi, notehead = tup + else: step, soct = note, octq + octnum = (4 if step.upper() == step else 5) + int (soct) + if not tup: # add percussion map for unmapped notes in this part + midi = str (octnum * 12 + [0,2,4,5,7,9,11]['CDEFGAB'.index (step.upper())] + {'^':1,'_':-1}.get (acc, 0) + 12) + notehead = {'^':'x', '_':'circle-x'}.get (acc, 'normal') + if s.pMapFound: info ('no I:percmap for: %s%s in part %s, voice %s' % (acc+note, -oct*',' if oct<0 else oct*"'", s.pid, s.vid)) + s.percMap [(s.pid, acc+note, octq)] = (note, octq, midi, notehead) + else: # correct step value for clef + step, octnum = stepTrans (step.upper (), octnum, s.curClef) + pitch = E.Element ('unpitched') + addElemT (pitch, 'display-step', step.upper (), lev + 1) + addElemT (pitch, 'display-octave', str (octnum), lev + 1) + return pitch, '', midi, notehead + nUp = note.upper () + octnum = (4 if nUp == note else 5) + int (oct) + s.gtrans + pitch = E.Element ('pitch') + addElemT (pitch, 'step', nUp, lev + 1) + alter = '' + if (note, oct) in s.ties: + tied_alter, _, vnum, _ = s.ties [(note,oct)] # vnum = overlay voice number when tie started + if vnum == s.overlayVnum: alter = tied_alter # tied note in the same overlay -> same alteration + elif acc: + s.msreAlts [(nUp, octnum)] = s.alterTab [acc] + alter = s.alterTab [acc] # explicit notated alteration + elif (nUp, octnum) in s.msreAlts: alter = s.msreAlts [(nUp, octnum)] # temporary alteration + elif nUp in s.keyAlts: alter = s.keyAlts [nUp] # alteration implied by the key + if alter: addElemT (pitch, 'alter', alter, lev + 1) + addElemT (pitch, 'octave', str (octnum), lev + 1) + return pitch, alter, '', '' + + def getNoteDecos (s, n): + decos = s.nextdecos # decorations encountered so far + ndeco = getattr (n, 'deco', 0) # possible decorations of notes of a chord + if ndeco: # add decorations, translate used defined symbols + decos += [s.usrSyms.get (d, d).strip ('!+') for d in ndeco.t] + s.nextdecos = [] + if s.tabStaff == s.pid and s.fOpt and n.name != 'rest': # force fret/string allocation if explicit string decoration is missing + if [d for d in decos if d in '0123456789'] == []: decos.append ('0') + return decos + + def mkNote (s, n, lev): + isgrace = getattr (n, 'grace', '') + ischord = getattr (n, 'chord', '') + if s.ntup >= 0 and not isgrace and not ischord: + s.ntup -= 1 # count tuplet notes only on non-chord, non grace notes + if s.ntup == -1 and s.trem <= 0: + s.intrem = 0 # tremolo pair ends at first note that is not a new tremolo pair (s.trem > 0) + nnum, nden = n.dur.t # abc dutation of note + if s.intrem: nnum += nnum # double duration of tremolo duplets + if nden == 0: nden = 1 # occurs with illegal ABC like: "A2 1". Now interpreted as A2/1 + num, den = simplify (nnum * s.unitLcur[0], nden * s.unitLcur[1]) # normalised with unit length + if den > 64: # limit denominator to 64 + num = int (round (64 * float (num) / den)) # scale note to num/64 + num, den = simplify (max ([num, 1]), 64) # smallest num == 1 + info ('duration too small: rounded to %d/%d' % (num, den)) + if n.name == 'rest' and ('Z' in n.t or 'X' in n.t): + num, den = s.mdur # duration of one measure + noMsrRest = not (n.name == 'rest' and (num, den) == s.mdur) # not a measure rest + dvs = (4 * s.divisions * num) // den # divisions is xml-duration of 1/4 + rdvs = dvs # real duration (will be 0 for chord/grace) + num, den = simplify (num, den * 4) # scale by 1/4 for s.typeMap + ndot = 0 + if num == 3 and noMsrRest: ndot = 1; den = den // 2 # look for dotted notes + if num == 7 and noMsrRest: ndot = 2; den = den // 4 + nt = E.Element ('note') + if isgrace: # a grace note (and possibly a chord note) + grace = E.Element ('grace') + if s.acciatura: grace.set ('slash', 'yes'); s.acciatura = 0 + addElem (nt, grace, lev + 1) + dvs = rdvs = 0 # no (real) duration for a grace note + if den <= 16: den = 32 # not longer than 1/8 for a grace note + if s.gcue_on: # insert cue tag + cue = E.Element ('cue') + addElem (nt, cue, lev + 1) + if ischord: # a chord note + chord = E.Element ('chord') + addElem (nt, chord, lev + 1) + rdvs = 0 # chord notes no real duration + if den not in s.typeMap: # take the nearest smaller legal duration + info ('illegal duration %d/%d' % (nnum, nden)) + den = min (x for x in s.typeMap.keys () if x > den) + xmltype = str (s.typeMap [den]) # xml needs the note type in addition to duration + acc, step, oct = '', 'C', '0' # abc-notated pitch elements (accidental, pitch step, octave) + alter, midi, notehead = '', '', '' # xml alteration + if n.name == 'rest': + if 'x' in n.t or 'X' in n.t: nt.set ('print-object', 'no') + rest = E.Element ('rest') + if not noMsrRest: rest.set ('measure', 'yes') + addElem (nt, rest, lev + 1) + else: + p = n.pitch.t # get pitch elements from parsed tokens + if len (p) == 3: acc, step, oct = p + else: step, oct = p + pitch, alter, midi, notehead = s.mkPitch (acc, step, oct, lev + 1) + if midi: acc = '' # erase accidental for percussion notes + addElem (nt, pitch, lev + 1) + if s.ntup >= 0: # modify duration for tuplet notes + dvs = dvs * s.tmden // s.tmnum + if dvs: + addElemT (nt, 'duration', str (dvs), lev + 1) # skip when dvs == 0, requirement of musicXML + if not ischord: s.gTime = s.gTime [1], s.gTime [1] + dvs + ptup = (step, oct) # pitch tuple without alteration to check for ties + tstop = ptup in s.ties and s.ties[ptup][2] == s.overlayVnum # open tie on this pitch tuple in this overlay + if tstop: + tie = E.Element ('tie', type='stop') + addElem (nt, tie, lev + 1) + if getattr (n, 'tie', 0): + tie = E.Element ('tie', type='start') + addElem (nt, tie, lev + 1) + if (s.midprg != ['', '', '', ''] or midi) and n.name != 'rest': # only add when %%midi was present or percussion + instId = 'I%s-%s' % (s.pid, 'X' + midi if midi else s.vid) + chan, midi = ('10', midi) if midi else s.midprg [:2] + inst = E.Element ('instrument', id=instId) # instrument id for midi + addElem (nt, inst, lev + 1) + if instId not in s.midiInst: s.midiInst [instId] = (s.pid, s.vid, chan, midi, s.midprg [2], s.midprg [3]) # for instrument list in mkScorePart + addElemT (nt, 'voice', '1', lev + 1) # default voice, for merging later + if noMsrRest: addElemT (nt, 'type', xmltype, lev + 1) # add note type if not a measure rest + for i in range (ndot): # add dots + dot = E.Element ('dot') + addElem (nt, dot, lev + 1) + decos = s.getNoteDecos (n) # get decorations for this note + if acc and not tstop: # only add accidental if note not tied + e = E.Element ('accidental') + if 'courtesy' in decos: + e.set ('parentheses', 'yes') + decos.remove ('courtesy') + e.text = s.accTab [acc] + addElem (nt, e, lev + 1) + tupnotation = '' # start/stop notation element for tuplets + if s.ntup >= 0: # add time modification element for tuplet notes + tmod = mkTmod (s.tmnum, s.tmden, lev + 1) + addElem (nt, tmod, lev + 1) + if s.ntup > 0 and not s.tupnts: tupnotation = 'start' + s.tupnts.append ((rdvs, tmod)) # remember all tuplet modifiers with corresp. durations + if s.ntup == 0: # last tuplet note (and possible chord notes there after) + if rdvs: tupnotation = 'stop' # only insert notation in the real note (rdvs > 0) + s.cmpNormType (rdvs, lev + 1) # compute and/or add normal-type elements (-> s.ntype) + hasStem = 1 + if not ischord: s.chordDecos = {} # clear on non chord note + if 'stemless' in decos or (s.nostems and n.name != 'rest') or 'stemless' in s.chordDecos: + hasStem = 0 + addElemT (nt, 'stem', 'none', lev + 1) + if 'stemless' in decos: decos.remove ('stemless') # do not handle in doNotations + if hasattr (n, 'pitches'): s.chordDecos ['stemless'] = 1 # set on first chord note + if notehead: + nh = addElemT (nt, 'notehead', re.sub (r'[+-]$', '', notehead), lev + 1) + if notehead[-1] in '+-': nh.set ('filled', 'yes' if notehead[-1] == '+' else 'no') + gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice + if gstaff: addElemT (nt, 'staff', str (gstaff), lev + 1) + if hasStem: s.doBeams (n, nt, den, lev + 1) # no stems -> no beams in a tab staff + s.doNotations (n, decos, ptup, alter, tupnotation, tstop, nt, lev + 1) + if n.objs: s.doLyr (n, nt, lev + 1) + else: s.prevLyric = {} # clear on note without lyrics + return nt + + def cmpNormType (s, rdvs, lev): # compute the normal-type of a tuplet (only needed for Finale) + if rdvs: # the last real tuplet note (chord notes can still follow afterwards with rdvs == 0) + durs = [dur for dur, tmod in s.tupnts if dur > 0] + ndur = sum (durs) // s.tmnum # duration of the normal type + s.irrtup = any ((dur != ndur) for dur in durs) # irregular tuplet + tix = 16 * s.divisions // ndur # index in typeMap of normal-type duration + if tix in s.typeMap: + s.ntype = str (s.typeMap [tix]) # the normal-type + else: s.irrtup = 0 # give up, no normal type possible + if s.irrtup: # only add normal-type for irregular tuplets + for dur, tmod in s.tupnts: # add normal-type to all modifiers + addElemT (tmod, 'normal-type', s.ntype, lev + 1) + s.tupnts = [] # reset the tuplet buffer + + def doNotations (s, n, decos, ptup, alter, tupnotation, tstop, nt, lev): + slurs = getattr (n, 'slurs', 0) # slur ends + pts = getattr (n, 'pitches', []) # all chord notes available in the first note + ov = s.overlayVnum # current overlay voice number (0 for the main voice) + if pts: # make list of pitches in chord: [(pitch, octave), ..] + if type (pts.pitch) == pObj: pts = [pts.pitch] # chord with one note + else: pts = [tuple (p.t[-2:]) for p in pts.pitch] # normal chord + for pt, (tie_alter, nts, vnum, ntelm) in sorted (list (s.ties.items ())): # scan all open ties and delete illegal ones + if vnum != s.overlayVnum: continue # tie belongs to different overlay + if pts and pt in pts: continue # pitch tuple of tie exists in chord + if getattr (n, 'chord', 0): continue # skip chord notes + if pt == ptup: continue # skip correct single note tie + if getattr (n, 'grace', 0): continue # skip grace notes + info ('tie between different pitches: %s%s converted to slur' % pt) + del s.ties [pt] # remove the note from pending ties + e = [t for t in ntelm.findall ('tie') if t.get ('type') == 'start'][0] # get the tie start element + ntelm.remove (e) # delete start tie element + e = [t for t in nts.findall ('tied') if t.get ('type') == 'start'][0] # get the tied start element + e.tag = 'slur' # convert tie into slur + slurnum = pushSlur (s.slurstack, ov) + e.set ('number', str (slurnum)) + if slurs: slurs.t.append (')') # close slur on this note + else: slurs = pObj ('slurs', [')']) + tstart = getattr (n, 'tie', 0) # start a new tie + if not (tstop or tstart or decos or slurs or s.slurbeg or tupnotation or s.trem): return nt + nots = E.Element ('notations') # notation element needed + if s.trem: # +/- => tuple tremolo sequence / single note tremolo + if s.trem < 0: tupnotation = 'single'; s.trem = -s.trem + if not tupnotation: return # only add notation at first or last note of a tremolo sequence + orn = E.Element ('ornaments') + trm = E.Element ('tremolo', type=tupnotation) # type = start, stop or single + trm.text = str (s.trem) # the number of bars in a tremolo note + addElem (nots, orn, lev + 1) + addElem (orn, trm, lev + 2) + if tupnotation == 'stop' or tupnotation == 'single': s.trem = 0 + elif tupnotation: # add tuplet type + tup = E.Element ('tuplet', type=tupnotation) + if tupnotation == 'start': tup.set ('bracket', 'yes') + addElem (nots, tup, lev + 1) + if tstop: # stop tie + del s.ties[ptup] # remove flag + tie = E.Element ('tied', type='stop') + addElem (nots, tie, lev + 1) + if tstart: # start a tie + s.ties[ptup] = (alter, nots, s.overlayVnum, nt) # remember pitch tuple to stop tie and apply same alteration + tie = E.Element ('tied', type='start') + if tstart.t[0] == '.-': tie.set ('line-type', 'dotted') + addElem (nots, tie, lev + 1) + if decos: # look for slurs and decorations + slurMap = { '(':1, '.(':1, '(,':1, "('":1, '.(,':1, ".('":1 } + arts = [] # collect articulations + for d in decos: # do all slurs and decos + if d in slurMap: s.slurbeg.append (d); continue # slurs made in while loop at the end + elif d == 'fermata' or d == 'H': + ntn = E.Element ('fermata', type='upright') + elif d == 'arpeggio': + ntn = E.Element ('arpeggiate', number='1') + elif d in ['~(', '~)']: + if d[1] == '(': tp = 'start'; s.glisnum += 1; gn = s.glisnum + else: tp = 'stop'; gn = s.glisnum; s.glisnum -= 1 + if s.glisnum < 0: s.glisnum = 0; continue # stop without previous start + ntn = E.Element ('glissando', {'line-type':'wavy', 'number':'%d' % gn, 'type':tp}) + elif d in ['-(', '-)']: + if d[1] == '(': tp = 'start'; s.slidenum += 1; gn = s.slidenum + else: tp = 'stop'; gn = s.slidenum; s.slidenum -= 1 + if s.slidenum < 0: s.slidenum = 0; continue # stop without previous start + ntn = E.Element ('slide', {'line-type':'solid', 'number':'%d' % gn, 'type':tp}) + else: arts.append (d); continue + addElem (nots, ntn, lev + 1) + if arts: # do only note articulations and collect staff annotations in xmldecos + rest = s.doArticulations (nt, nots, arts, lev + 1) + if rest: info ('unhandled note decorations: %s' % rest) + if slurs: # these are only slur endings + for d in slurs.t: # slurs to be closed on this note + if not s.slurstack.get (ov, 0): break # no more open old slurs for this (overlay) voice + slurnum = s.slurstack [ov].pop () + slur = E.Element ('slur', number='%d' % slurnum, type='stop') + addElem (nots, slur, lev + 1) + while s.slurbeg: # create slurs beginning on this note + stp = s.slurbeg.pop (0) + slurnum = pushSlur (s.slurstack, ov) + ntn = E.Element ('slur', number='%d' % slurnum, type='start') + if '.' in stp: ntn.set ('line-type', 'dotted') + if ',' in stp: ntn.set ('placement', 'below') + if "'" in stp: ntn.set ('placement', 'above') + addElem (nots, ntn, lev + 1) + if list (nots) != []: # only add notations if not empty + addElem (nt, nots, lev) + + def doArticulations (s, nt, nots, arts, lev): + decos = [] + for a in arts: + if a in s.artMap: + art = E.Element ('articulations') + addElem (nots, art, lev) + addElem (art, E.Element (s.artMap[a]), lev + 1) + elif a in s.ornMap: + orn = E.Element ('ornaments') + addElem (nots, orn, lev) + addElem (orn, E.Element (s.ornMap[a]), lev + 1) + elif a in ['trill(','trill)']: + orn = E.Element ('ornaments') + addElem (nots, orn, lev) + type = 'start' if a.endswith ('(') else 'stop' + if type == 'start': addElem (orn, E.Element ('trill-mark'), lev + 1) + addElem (orn, E.Element ('wavy-line', type=type), lev + 1) + elif a in s.tecMap: + tec = E.Element ('technical') + addElem (nots, tec, lev) + addElem (tec, E.Element (s.tecMap[a]), lev + 1) + elif a in '0123456': + tec = E.Element ('technical') + addElem (nots, tec, lev) + if s.tabStaff == s.pid: # current voice belongs to a tabStaff + alt = int (nt.findtext ('pitch/alter') or 0) # find midi number of current note + step = nt.findtext ('pitch/step') + oct = int (nt.findtext ('pitch/octave')) + midi = oct * 12 + [0,2,4,5,7,9,11]['CDEFGAB'.index (step)] + alt + 12 + if a == '0': # no string annotation: find one + firstFit = '' + for smid, istr in s.tunTup: # midi numbers of open strings from high to low + if midi >= smid: # highest open string where this note can be played + isvrij = s.strAlloc.isVrij (istr - 1, s.gTime [0], s.gTime [1]) + a = str (istr) # string number + if not firstFit: firstFit = a + if isvrij: break + if not isvrij: # no free string, take the first fit (lowest fret) + a = firstFit + s.strAlloc.bezet (int (a) - 1, s.gTime [0], s.gTime [1]) + else: # force annotated string number + s.strAlloc.bezet (int (a) - 1, s.gTime [0], s.gTime [1]) + bmidi = s.tunmid [int (a) - 1] # midi number of allocated string (with capodastro) + fret = midi - bmidi # fret position (respecting capodastro) + if fret < 25 and fret >= 0: + addElemT (tec, 'fret', str (fret), lev + 1) + else: + altp = 'b' if alt == -1 else '#' if alt == 1 else '' + info ('fret %d out of range, note %s%d on string %s' % (fret, step+altp, oct, a)) + addElemT (tec, 'string', a, lev + 1) + else: + addElemT (tec, 'fingering', a, lev + 1) + else: decos.append (a) # return staff annotations + return decos + + def doLyr (s, n, nt, lev): + for i, lyrobj in enumerate (n.objs): + lyrel = E.Element ('lyric', number = str (i + 1)) + if lyrobj.name == 'syl': + dash = len (lyrobj.t) == 2 + if dash: + if i in s.lyrdash: type = 'middle' + else: type = 'begin'; s.lyrdash [i] = 1 + else: + if i in s.lyrdash: type = 'end'; del s.lyrdash [i] + else: type = 'single' + addElemT (lyrel, 'syllabic', type, lev + 1) + txt = lyrobj.t[0] # the syllabe + txt = re.sub (r'(? continue + pext.set ('type', 'continue') + ext = E.Element ('extend', type = 'stop') # always stop on current extend + addElem (lyrel, ext, lev + 1) + elif lyrobj.name == 'ext': info ('lyric extend error'); continue + else: continue # skip other lyric elements or errors + addElem (nt, lyrel, lev) + s.prevLyric [i] = lyrel # for extension (melisma) on the next note + + def doBeams (s, n, nt, den, lev): + if hasattr (n, 'chord') or hasattr (n, 'grace'): + s.grcbbrk = s.grcbbrk or n.bbrk.t[0] # remember if there was any bbrk in or before a grace sequence + return + bbrk = s.grcbbrk or n.bbrk.t[0] or den < 32 + s.grcbbrk = False + if not s.prevNote: pbm = None + else: pbm = s.prevNote.find ('beam') + bm = E.Element ('beam', number='1') + bm.text = 'begin' + if pbm != None: + if bbrk: + if pbm.text == 'begin': + s.prevNote.remove (pbm) + elif pbm.text == 'continue': + pbm.text = 'end' + s.prevNote = None + else: bm.text = 'continue' + if den >= 32 and n.name != 'rest': + addElem (nt, bm, lev) + s.prevNote = nt + + def stopBeams (s): + if not s.prevNote: return + pbm = s.prevNote.find ('beam') + if pbm != None: + if pbm.text == 'begin': + s.prevNote.remove (pbm) + elif pbm.text == 'continue': + pbm.text = 'end' + s.prevNote = None + + def staffDecos (s, decos, maat, lev): + gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice + for d in decos: + d = s.usrSyms.get (d, d).strip ('!+') # try to replace user defined symbol + if d in s.dynaMap: + dynel = E.Element ('dynamics') + addDirection (maat, dynel, lev, gstaff, [E.Element (d)], 'below', s.gcue_on) + elif d in s.wedgeMap: # wedge + if ')' in d: type = 'stop' + else: type = 'crescendo' if '<' in d or 'crescendo' in d else 'diminuendo' + addDirection (maat, E.Element ('wedge', type=type), lev, gstaff) + elif d.startswith ('8v'): + if 'a' in d: type, plce = 'down', 'above' + else: type, plce = 'up', 'below' + if ')' in d: type = 'stop' + addDirection (maat, E.Element ('octave-shift', type=type, size='8'), lev, gstaff, placement=plce) + elif d in (['ped','ped-up']): + type = 'stop' if d.endswith ('up') else 'start' + addDirection (maat, E.Element ('pedal', type=type), lev, gstaff) + elif d in ['coda', 'segno']: + text, attr, val = s.capoMap [d] + dir = addDirection (maat, E.Element (text), lev, gstaff, placement='above') + sound = E.Element ('sound'); sound.set (attr, val) + addElem (dir, sound, lev + 1) + elif d in s.capoMap: + text, attr, val = s.capoMap [d] + words = E.Element ('words'); words.text = text + dir = addDirection (maat, words, lev, gstaff, placement='above') + sound = E.Element ('sound'); sound.set (attr, val) + addElem (dir, sound, lev + 1) + elif d == '(' or d == '.(': s.slurbeg.append (d) # start slur on next note + elif d in ['/-','//-','///-','////-']: # duplet tremolo sequence + s.tmnum, s.tmden, s.ntup, s.trem, s.intrem = 2, 1, 2, len (d) - 1, 1 + elif d in ['/','//','///']: s.trem = - len (d) # single note tremolo + else: s.nextdecos.append (d) # keep annotation for the next note + + def doFields (s, maat, fieldmap, lev): + def instDir (midelm, midnum, dirtxt): + instId = 'I%s-%s' % (s.pid, s.vid) + words = E.Element ('words'); words.text = dirtxt % midnum + snd = E.Element ('sound') + mi = E.Element ('midi-instrument', id=instId) + dir = addDirection (maat, words, lev, gstaff, placement='above') + addElem (dir, snd, lev + 1) + addElem (snd, mi, lev + 2) + addElemT (mi, midelm, midnum, lev + 3) + def addTrans (n): + e = E.Element ('transpose') + addElemT (e, 'chromatic', n, lev + 2) # n == signed number string given after transpose + atts.append ((9, e)) + def doClef (field): + if re.search (r'perc|map', field): # percussion clef or new style perc=on or map=perc + r = re.search (r'(perc|map)\s*=\s*(\S*)', field) + s.percVoice = 0 if r and r.group (2) not in ['on','true','perc'] else 1 + field = re.sub (r'(perc|map)\s*=\s*(\S*)', '', field) # erase the perc= for proper clef matching + clef, gtrans = 0, 0 + clefn = re.search (r'alto1|alto2|alto4|alto|tenor|bass3|bass|treble|perc|none|tab', field) + clefm = re.search (r"(?:^m=| m=|middle=)([A-Ga-g])([,']*)", field) + trans_oct2 = re.search (r'octave=([-+]?\d)', field) + trans = re.search (r'(?:^t=| t=|transpose=)(-?[\d]+)', field) + trans_oct = re.search (r'([+-^_])(8|15)', field) + cue_onoff = re.search (r'cue=(on|off)', field) + strings = re.search (r"strings=(\S+)", field) + stafflines = re.search (r'stafflines=\s*(\d)', field) + capo = re.search (r'capo=(\d+)', field) + if clefn: + clef = clefn.group () + if clefm: + note, octstr = clefm.groups () + nUp = note.upper () + octnum = (4 if nUp == note else 5) + (len (octstr) if "'" in octstr else -len (octstr)) + gtrans = (3 if nUp in 'AFD' else 4) - octnum + if clef not in ['perc', 'none']: clef = s.clefLineMap [nUp] + if clef: + s.gtrans = gtrans # only change global tranposition when a clef is really defined + if clef != 'none': s.curClef = clef # keep track of current abc clef (for percmap) + sign, line = s.clefMap [clef] + if not sign: return + c = E.Element ('clef') + if gstaff: c.set ('number', str (gstaff)) # only add staff number when defined + addElemT (c, 'sign', sign, lev + 2) + if line: addElemT (c, 'line', line, lev + 2) + if trans_oct: + n = trans_oct.group (1) in '-_' and -1 or 1 + if trans_oct.group (2) == '15': n *= 2 # 8 => 1 octave, 15 => 2 octaves + addElemT (c, 'clef-octave-change', str (n), lev + 2) # transpose print out + if trans_oct.group (1) in '+-': s.gtrans += n # also transpose all pitches with one octave + atts.append ((7, c)) + if trans_oct2: # octave= can also be in a K: field + n = int (trans_oct2.group (1)) + s.gtrans = gtrans + n + if trans != None: # add transposition in semitones + e = E.Element ('transpose') + addElemT (e, 'chromatic', str (trans.group (1)), lev + 3) + atts.append ((9, e)) + if cue_onoff: s.gcue_on = cue_onoff.group (1) == 'on' + nlines = 0 + if clef == 'tab': + s.tabStaff = s.pid + if capo: s.capo = int (capo.group (1)) + if strings: s.tuning = strings.group (1).split (',') + s.tunmid = [int (boct) * 12 + [0,2,4,5,7,9,11]['CDEFGAB'.index (bstep)] + 12 + s.capo for bstep, boct in s.tuning] + s.tunTup = sorted (zip (s.tunmid, range (len (s.tunmid), 0, -1)), reverse=1) + s.tunmid.reverse () + nlines = str (len (s.tuning)) + s.strAlloc.setlines (len (s.tuning), s.pid) + s.nostems = 'nostems' in field # tab clef without stems + s.diafret = 'diafret' in field # tab with diatonic fretting + if stafflines or nlines: + e = E.Element ('staff-details') + if gstaff: e.set ('number', str (gstaff)) # only add staff number when defined + if not nlines: nlines = stafflines.group (1) + addElemT (e, 'staff-lines', nlines, lev + 2) + if clef == 'tab': + for line, t in enumerate (s.tuning): + st = E.Element ('staff-tuning', line=str(line+1)) + addElemT (st, 'tuning-step', t[0], lev + 3) + addElemT (st, 'tuning-octave', t[1], lev + 3) + addElem (e, st, lev + 2) + if s.capo: addElemT (e, 'capo', str (s.capo), lev + 2) + atts.append ((8, e)) + s.diafret = 0 # chromatic fretting is default + atts = [] # collect xml attribute elements [(order-number, xml-element), ..] + gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice + for ftype, field in fieldmap.items (): + if not field: # skip empty fields + continue + if ftype == 'Div': # not an abc field, but handled as if + d = E.Element ('divisions') + d.text = field + atts.append ((1, d)) + elif ftype == 'gstaff': # make grand staff + e = E.Element ('staves') + e.text = str (field) + atts.append ((4, e)) + elif ftype == 'M': + if field == 'none': continue + if field == 'C': field = '4/4' + elif field == 'C|': field = '2/2' + t = E.Element ('time') + if '/' not in field: + info ('M:%s not recognized, 4/4 assumed' % field) + field = '4/4' + beats, btype = field.split ('/')[:2] + try: s.mdur = simplify (eval (beats), int (btype)) # measure duration for Z and X rests (eval allows M:2+3/4) + except: + info ('error in M:%s, 4/4 assumed' % field) + s.mdur = (4,4) + beats, btype = '4','4' + addElemT (t, 'beats', beats, lev + 2) + addElemT (t, 'beat-type', btype, lev + 2) + atts.append ((3, t)) + elif ftype == 'K': + accs = ['F','C','G','D','A','E','B'] # == s.sharpness [7:14] + mode = '' + key = re.match (r'\s*([A-G][#b]?)\s*([a-zA-Z]*)', field) + alts = re.search (r'\s((\s?[=^_][A-Ga-g])+)', ' ' + field) # avoid matching middle=G and m=G + if key: + key, mode = key.groups () + mode = mode.lower ()[:3] # only first three chars, no case + if mode not in s.offTab: mode = 'maj' + fifths = s.sharpness.index (key) - s.offTab [mode] + if fifths >= 0: s.keyAlts = dict (zip (accs[:fifths], fifths * ['1'])) + else: s.keyAlts = dict (zip (accs[fifths:], -fifths * ['-1'])) + elif field.startswith ('none') or field == '': # the default key + fifths = 0 + mode = 'maj' + if alts: + alts = re.findall (r'[=^_][A-Ga-g]', alts.group(1)) # list of explicit alterations + alts = [(x[1], s.alterTab [x[0]]) for x in alts] # [step, alter] + for step, alter in alts: # correct permanent alterations for this key + s.keyAlts [step.upper ()] = alter + k = E.Element ('key') + koctave = [] + lowerCaseSteps = [step.upper () for step, alter in alts if step.islower ()] + for step, alter in sorted (list (s.keyAlts.items ())): + if alter == '0': # skip neutrals + del s.keyAlts [step.upper ()] # otherwise you get neutral signs on normal notes + continue + addElemT (k, 'key-step', step.upper (), lev + 2) + addElemT (k, 'key-alter', alter, lev + 2) + koctave.append ('5' if step in lowerCaseSteps else '4') + if koctave: # only key signature if not empty + for oct in koctave: + e = E.Element ('key-octave', number=oct) + addElem (k, e, lev + 2) + atts.append ((2, k)) + elif mode: + k = E.Element ('key') + addElemT (k, 'fifths', str (fifths), lev + 2) + addElemT (k, 'mode', s.modTab [mode], lev + 2) + atts.append ((2, k)) + doClef (field) + elif ftype == 'L': + try: s.unitLcur = lmap (int, field.split ('/')) + except: s.unitLcur = (1,8) + if len (s.unitLcur) == 1 or s.unitLcur[1] not in s.typeMap: + info ('L:%s is not allowed, 1/8 assumed' % field) + s.unitLcur = 1,8 + elif ftype == 'V': + doClef (field) + elif ftype == 'I': + s.doField_I (ftype, field, instDir, addTrans) + elif ftype == 'Q': + s.doTempo (maat, field, lev) + elif ftype == 'P': # ad hoc translation of P: into a staff text direction + words = E.Element ('rehearsal') + words.set ('font-weight', 'bold') + words.text = field + addDirection (maat, words, lev, gstaff, placement='above') + elif ftype in 'TCOAZNGHRBDFSU': + info ('**illegal header field in body: %s, content: %s' % (ftype, field)) + else: + info ('unhandled field: %s, content: %s' % (ftype, field)) + + if atts: + att = E.Element ('attributes') # insert sub elements in the order required by musicXML + addElem (maat, att, lev) + for _, att_elem in sorted (atts, key=lambda x: x[0]): # ordering ! + addElem (att, att_elem, lev + 1) + if s.diafret: + other = E.Element ('other-direction'); other.text = 'diatonic fretting' + addDirection (maat, other, lev, 0) + + def doTempo (s, maat, field, lev): + gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice + t = re.search (r'(\d)/(\d\d?)\s*=\s*(\d[.\d]*)|(\d[.\d]*)', field) + rtxt = re.search (r'"([^"]*)"', field) # look for text in Q: field + if not t and not rtxt: return + elems = [] # [(element, sub-elements)] will be added as direction-types + if rtxt: + num, den, upm = 1, 4, s.tempoMap.get (rtxt.group (1).lower ().strip (), 120) + words = E.Element ('words'); words.text = rtxt.group (1) + elems.append ((words, [])) + if t: + try: + if t.group (4): num, den, upm = 1, s.unitLcur[1] , float (t.group (4)) # old syntax Q:120 + else: num, den, upm = int (t.group (1)), int (t.group (2)), float (t.group (3)) + except: info ('conversion error: %s' % field); return + num, den = simplify (num, den); + dotted, den_not = (1, den // 2) if num == 3 else (0, den) + metro = E.Element ('metronome') + u = E.Element ('beat-unit'); u.text = s.typeMap.get (4 * den_not, 'quarter') + pm = E.Element ('per-minute'); pm.text = ('%.2f' % upm).rstrip ('0').rstrip ('.') + subelms = [u, E.Element ('beat-unit-dot'), pm] if dotted else [u, pm] + elems.append ((metro, subelms)) + dir = addDirection (maat, elems, lev, gstaff, [], placement='above') + if num != 1 and num != 3: info ('in Q: numerator in %d/%d not supported' % (num, den)) + qpm = 4. * num * upm / den + sound = E.Element ('sound'); sound.set ('tempo', '%.2f' % qpm) + addElem (dir, sound, lev + 1) + + def mkBarline (s, maat, loc, lev, style='', dir='', ending=''): + b = E.Element ('barline', location=loc) + if style: + addElemT (b, 'bar-style', style, lev + 1) + if s.curVolta: # first stop a current volta + end = E.Element ('ending', number=s.curVolta, type='stop') + s.curVolta = '' + if loc == 'left': # stop should always go to a right barline + bp = E.Element ('barline', location='right') + addElem (bp, end, lev + 1) + addElem (s.prevmsre, bp, lev) # prevmsre has no right barline! (ending would have stopped there) + else: + addElem (b, end, lev + 1) + if ending: + ending = ending.replace ('-',',') # MusicXML only accepts comma's + endtxt = '' + if ending.startswith ('"'): # ending is a quoted string + endtxt = ending.strip ('"') + ending = '33' # any number that is not likely to occur elsewhere + end = E.Element ('ending', number=ending, type='start') + if endtxt: end.text = endtxt # text appears in score in stead of number attribute + addElem (b, end, lev + 1) + s.curVolta = ending + if dir: + r = E.Element ('repeat', direction=dir) + addElem (b, r, lev + 1) + addElem (maat, b, lev) + + def doChordSym (s, maat, sym, lev): + alterMap = {'#':'1','=':'0','b':'-1'} + rnt = sym.root.t + chord = E.Element ('harmony') + addElem (maat, chord, lev) + root = E.Element ('root') + addElem (chord, root, lev + 1) + addElemT (root, 'root-step', rnt[0], lev + 2) + if len (rnt) == 2: addElemT (root, 'root-alter', alterMap [rnt[1]], lev + 2) + kind = s.chordTab.get (sym.kind.t[0], 'major') if sym.kind.t else 'major' + addElemT (chord, 'kind', kind, lev + 1) + if hasattr (sym, 'bass'): + bnt = sym.bass.t + bass = E.Element ('bass') + addElem (chord, bass, lev + 1) + addElemT (bass, 'bass-step', bnt[0], lev + 2) + if len (bnt) == 2: addElemT (bass, 'bass-alter', alterMap [bnt[1]], lev + 2) + degs = getattr (sym, 'degree', '') + if degs: + if type (degs) != list_type: degs = [degs] + for deg in degs: + deg = deg.t[0] + if deg[0] == '#': alter = '1'; deg = deg[1:] + elif deg[0] == 'b': alter = '-1'; deg = deg[1:] + else: alter = '0'; deg = deg + degree = E.Element ('degree') + addElem (chord, degree, lev + 1) + addElemT (degree, 'degree-value', deg, lev + 2) + addElemT (degree, 'degree-alter', alter, lev + 2) + addElemT (degree, 'degree-type', 'add', lev + 2) + + def mkMeasure (s, i, t, lev, fieldmap={}): + s.msreAlts = {} + s.ntup, s.trem, s.intrem = -1, 0, 0 + s.acciatura = 0 # next grace element gets acciatura attribute + overlay = 0 + maat = E.Element ('measure', number = str(i)) + if fieldmap: s.doFields (maat, fieldmap, lev + 1) + if s.linebrk: # there was a line break in the previous measure + e = E.Element ('print') + e.set ('new-system', 'yes') + addElem (maat, e, lev + 1) + s.linebrk = 0 + for it, x in enumerate (t): + if x.name == 'note' or x.name == 'rest': + if x.dur.t[0] == 0: # a leading zero was used for stemmless in abcm2ps, we only support !stemless! + x.dur.t = tuple ([1, x.dur.t[1]]) + note = s.mkNote (x, lev + 1) + addElem (maat, note, lev + 1) + elif x.name == 'lbar': + bar = x.t[0] + if bar == '|' or bar == '[|': pass # skip redundant bar + elif ':' in bar: # forward repeat + volta = x.t[1] if len (x.t) == 2 else '' + s.mkBarline (maat, 'left', lev + 1, style='heavy-light', dir='forward', ending=volta) + else: # bar must be a volta number + s.mkBarline (maat, 'left', lev + 1, ending=bar) + elif x.name == 'rbar': + bar = x.t[0] + if bar == '.|': + s.mkBarline (maat, 'right', lev + 1, style='dotted') + elif ':' in bar: # backward repeat + s.mkBarline (maat, 'right', lev + 1, style='light-heavy', dir='backward') + elif bar == '||': + s.mkBarline (maat, 'right', lev + 1, style='light-light') + elif bar == '[|]' or bar == '[]': + s.mkBarline (maat, 'right', lev + 1, style='none') + elif '[' in bar or ']' in bar: + s.mkBarline (maat, 'right', lev + 1, style='light-heavy') + elif bar[0] == '&': overlay = 1 + elif x.name == 'tup': + if len (x.t) == 3: n, into, nts = x.t + elif len (x.t) == 2: n, into, nts = x.t + [0] + else: n, into, nts = x.t[0], 0, 0 + if into == 0: into = 3 if n in [2,4,8] else 2 + if nts == 0: nts = n + s.tmnum, s.tmden, s.ntup = n, into, nts + elif x.name == 'deco': + s.staffDecos (x.t, maat, lev + 1) # output staff decos, postpone note decos to next note + elif x.name == 'text': + pos, text = x.t[:2] + place = 'above' if pos == '^' else 'below' + words = E.Element ('words') + words.text = text + gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice + addDirection (maat, words, lev + 1, gstaff, placement=place) + elif x.name == 'inline': + fieldtype, fieldval = x.t[0], ' '.join (x.t[1:]) + s.doFields (maat, {fieldtype:fieldval}, lev + 1) + elif x.name == 'accia': s.acciatura = 1 + elif x.name == 'linebrk': + s.supports_tag = 1 + if it > 0 and t[it -1].name == 'lbar': # we are at start of measure + e = E.Element ('print') # output linebreak now + e.set ('new-system', 'yes') + addElem (maat, e, lev + 1) + else: + s.linebrk = 1 # output linebreak at start of next measure + elif x.name == 'chordsym': + s.doChordSym (maat, x, lev + 1) + s.stopBeams () + s.prevmsre = maat + return maat, overlay + + def mkPart (s, maten, id, lev, attrs, nstaves, rOpt): + s.slurstack = {} + s.glisnum = 0; # xml number attribute for glissandos + s.slidenum = 0; # xml number attribute for slides + s.unitLcur = s.unitL # set the default unit length at begin of each voice + s.curVolta = '' + s.lyrdash = {} + s.linebrk = 0 + s.midprg = ['', '', '', ''] # MIDI channel nr, program nr, volume, panning for the current part + s.gcue_on = 0 # reset cue note marker for each new voice + s.gtrans = 0 # reset octave transposition (by clef) + s.percVoice = 0 # 1 if percussion clef encountered + s.curClef = '' # current abc clef (for percmap) + s.nostems = 0 # for the tab clef + s.tuning = s.tuningDef # reset string tuning to default + part = E.Element ('part', id=id) + s.overlayVnum = 0 # overlay voice number to relate ties that extend from one overlayed measure to the next + gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice + attrs_cpy = attrs.copy () # don't change attrs itself in next line + if gstaff == 1: attrs_cpy ['gstaff'] = nstaves # make a grand staff + if 'perc' in attrs_cpy.get ('V', ''): del attrs_cpy ['K'] # remove key from percussion voice + msre, overlay = s.mkMeasure (1, maten[0], lev + 1, attrs_cpy) + addElem (part, msre, lev + 1) + for i, maat in enumerate (maten[1:]): + s.overlayVnum = s.overlayVnum + 1 if overlay else 0 + msre, next_overlay = s.mkMeasure (i+2, maat, lev + 1) + if overlay: mergePartMeasure (part, msre, s.overlayVnum, rOpt) + else: addElem (part, msre, lev + 1) + overlay = next_overlay + return part + + def mkScorePart (s, id, vids_p, partAttr, lev): + def mkInst (instId, vid, midchan, midprog, midnot, vol, pan, lev): + si = E.Element ('score-instrument', id=instId) + pnm = partAttr.get (vid, [''])[0] # part name if present + addElemT (si, 'instrument-name', pnm or 'dummy', lev + 2) # MuseScore needs a name + mi = E.Element ('midi-instrument', id=instId) + if midchan: addElemT (mi, 'midi-channel', midchan, lev + 2) + if midprog: addElemT (mi, 'midi-program', str (int (midprog) + 1), lev + 2) # compatible with abc2midi + if midnot: addElemT (mi, 'midi-unpitched', str (int (midnot) + 1), lev + 2) + if vol: addElemT (mi, 'volume', '%.2f' % (int (vol) / 1.27), lev + 2) + if pan: addElemT (mi, 'pan', '%.2f' % (int (pan) / 127. * 180 - 90), lev + 2) + return (si, mi) + naam, subnm, midprg = partAttr [id] + sp = E.Element ('score-part', id='P'+id) + nm = E.Element ('part-name') + nm.text = naam + addElem (sp, nm, lev + 1) + snm = E.Element ('part-abbreviation') + snm.text = subnm + if subnm: addElem (sp, snm, lev + 1) # only add if subname was given + inst = [] + for instId, (pid, vid, chan, midprg, vol, pan) in sorted (s.midiInst.items ()): + midprg, midnot = ('0', midprg) if chan == '10' else (midprg, '') + if pid == id: inst.append (mkInst (instId, vid, chan, midprg, midnot, vol, pan, lev)) + for si, mi in inst: addElem (sp, si, lev + 1) + for si, mi in inst: addElem (sp, mi, lev + 1) + return sp + + def mkPartlist (s, vids, partAttr, lev): + def addPartGroup (sym, num): + pg = E.Element ('part-group', number=str (num), type='start') + addElem (partlist, pg, lev + 1) + addElemT (pg, 'group-symbol', sym, lev + 2) + addElemT (pg, 'group-barline', 'yes', lev + 2) + partlist = E.Element ('part-list') + g_num = 0 # xml group number + for g in (s.groups or vids): # brace/bracket or abc_voice_id + if g == '[': g_num += 1; addPartGroup ('bracket', g_num) + elif g == '{': g_num += 1; addPartGroup ('brace', g_num) + elif g in '}]': + pg = E.Element ('part-group', number=str (g_num), type='stop') + addElem (partlist, pg, lev + 1) + g_num -= 1 + else: # g = abc_voice_id + if g not in vids: continue # error in %%score + sp = s.mkScorePart (g, vids, partAttr, lev + 1) + addElem (partlist, sp, lev + 1) + return partlist + + def doField_I (s, type, x, instDir, addTrans): + def instChange (midchan, midprog): # instDir -> doFields + if midchan and midchan != s.midprg [0]: instDir ('midi-channel', midchan, 'chan: %s') + if midprog and midprog != s.midprg [1]: instDir ('midi-program', str (int (midprog) + 1), 'prog: %s') + def readPfmt (x, n): # read ABC page formatting constant + if not s.pageFmtAbc: s.pageFmtAbc = s.pageFmtDef # set the default values on first change + ro = re.search (r'[^.\d]*([\d.]+)\s*(cm|in|pt)?', x) # float followed by unit + if ro: + x, unit = ro.groups () # unit == None when not present + u = {'cm':10., 'in':25.4, 'pt':25.4/72} [unit] if unit else 1. + s.pageFmtAbc [n] = float (x) * u # convert ABC values to millimeters + else: info ('error in page format: %s' % x) + def readPercMap (x): # parse I:percmap + def getMidNum (sndnm): # find midi number of GM drum sound name + pnms = sndnm.split ('-') # sound name parts (from I:percmap) + ps = s.percsnd [:] # copy of the instruments + _f = lambda ip, xs, pnm: ip < len (xs) and xs[ip].find (pnm) > -1 # part xs[ip] and pnm match + for ip, pnm in enumerate (pnms): # match all percmap sound name parts + ps = [(nm, mnum) for nm, mnum in ps if _f (ip, nm.split ('-'), pnm) ] # filter instruments + if len (ps) <= 1: break # no match or one instrument left + if len (ps) == 0: info ('drum sound: %s not found' % sndnm); return '38' + return ps [0][1] # midi number of (first) instrument found + def midiVal (acc, step, oct): # abc note -> midi note number + oct = (4 if step.upper() == step else 5) + int (oct) + return oct * 12 + [0,2,4,5,7,9,11]['CDEFGAB'.index (step.upper())] + {'^':1,'_':-1,'=':0}.get (acc, 0) + 12 + p0, p1, p2, p3, p4 = abc_percmap.parseString (x).asList () # percmap, abc-note, display-step, midi, note-head + acc, astep, aoct = p1 + nstep, noct = (astep, aoct) if p2 == '*' else p2 + if p3 == '*': midi = str (midiVal (acc, astep, aoct)) + elif isinstance (p3, list_type): midi = str (midiVal (p3[0], p3[1], p3[2])) + elif isinstance (p3, int_type): midi = str (p3) + else: midi = getMidNum (p3.lower ()) + head = re.sub (r'(.)-([^x])', r'\1 \2', p4) # convert abc note head names to xml + s.percMap [(s.pid, acc + astep, aoct)] = (nstep, noct, midi, head) + if x.startswith ('score') or x.startswith ('staves'): + s.staveDefs += [x] # collect all voice mappings + elif x.startswith ('staffwidth'): info ('skipped I-field: %s' % x) + elif x.startswith ('staff'): # set new staff number of the current voice + r1 = re.search (r'staff *([+-]?)(\d)', x) + if r1: + sign = r1.group (1) + num = int (r1.group (2)) + gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice + if sign: # relative staff number + num = (sign == '-') and gstaff - num or gstaff + num + else: # absolute abc staff number + try: vabc = s.staves [num - 1][0] # vid of (first voice of) abc-staff num + except: vabc = 0; info ('abc staff %s does not exist' % num) + num = s.gStaffNumsOrg.get (vabc, 0) # xml staff number of abc-staff num + if gstaff and num > 0 and num <= s.gNstaves [s.vid]: + s.gStaffNums [s.vid] = num + else: info ('could not relocate to staff: %s' % r1.group ()) + else: info ('not a valid staff redirection: %s' % x) + elif x.startswith ('scale'): readPfmt (x, 0) + elif x.startswith ('pageheight'): readPfmt (x, 1) + elif x.startswith ('pagewidth'): readPfmt (x, 2) + elif x.startswith ('leftmargin'): readPfmt (x, 3) + elif x.startswith ('rightmargin'): readPfmt (x, 4) + elif x.startswith ('topmargin'): readPfmt (x, 5) + elif x.startswith ('botmargin'): readPfmt (x, 6) + elif x.startswith ('MIDI') or x.startswith ('midi'): + r1 = re.search (r'program *(\d*) +(\d+)', x) + r2 = re.search (r'channel *(\d+)', x) + r3 = re.search (r"drummap\s+([_=^]*)([A-Ga-g])([,']*)\s+(\d+)", x) + r4 = re.search (r'control *(\d+) +(\d+)', x) + ch_nw, prg_nw, vol_nw, pan_nw = '', '', '', '' + if r1: ch_nw, prg_nw = r1.groups () # channel nr or '', program nr + if r2: ch_nw = r2.group (1) # channel nr only + if r4: + cnum, cval = r4.groups () # controller number, controller value + if cnum == '7': vol_nw = cval + if cnum == '10': pan_nw = cval + if r1 or r2 or r4: + ch = ch_nw or s.midprg [0] + prg = prg_nw or s.midprg [1] + vol = vol_nw or s.midprg [2] + pan = pan_nw or s.midprg [3] + instId = 'I%s-%s' % (s.pid, s.vid) # only look for real instruments, no percussion + if instId in s.midiInst: instChange (ch, prg) # instChance -> doFields + s.midprg = [ch, prg, vol, pan] # mknote: new instrument -> s.midiInst + if r3: # translate drummap to percmap + acc, step, oct, midi = r3.groups () + oct = -len (oct) if ',' in x else len (oct) + notehead = 'x' if acc == '^' else 'circle-x' if acc == '_' else 'normal' + s.percMap [(s.pid, acc + step, oct)] = (step, oct, midi, notehead) + r = re.search (r'transpose[^-\d]*(-?\d+)', x) + if r: addTrans (r.group (1)) # addTrans -> doFields + elif x.startswith ('percmap'): readPercMap (x); s.pMapFound = 1 + else: info ('skipped I-field: %s' % x) + + def parseStaveDef (s, vdefs): + for vid in vdefs: s.vcepid [vid] = vid # default: each voice becomes an xml part + if not s.staveDefs: return vdefs + for x in s.staveDefs [1:]: info ('%%%%%s dropped, multiple stave mappings not supported' % x) + x = s.staveDefs [0] # only the first %%score is honoured + score = abc_scoredef.parseString (x) [0] + f = lambda x: type (x) == uni_type and [x] or x + s.staves = lmap (f, mkStaves (score, vdefs)) # [[vid] for each staff] + s.grands = lmap (f, mkGrand (score, vdefs)) # [staff-id], staff-id == [vid][0] + s.groups = mkGroups (score) + vce_groups = [vids for vids in s.staves if len (vids) > 1] # all voice groups + d = {} # for each voice group: map first voice id -> all merged voice ids + for vgr in vce_groups: d [vgr[0]] = vgr + for gstaff in s.grands: # for all grand staves + if len (gstaff) == 1: continue # skip single parts + for v, stf_num in zip (gstaff, range (1, len (gstaff) + 1)): + for vx in d.get (v, [v]): # allocate staff numbers + s.gStaffNums [vx] = stf_num # to all constituant voices + s.gNstaves [vx] = len (gstaff) # also remember total number of staves + s.gStaffNumsOrg = s.gStaffNums.copy () # keep original allocation for abc -> xml staff map + for xmlpart in s.grands: + pid = xmlpart [0] # part id == first staff id == first voice id + vces = [v for stf in xmlpart for v in d.get (stf, [stf])] + for v in vces: s.vcepid [v] = pid + return vdefs + + def voiceNamesAndMaps (s, ps): # get voice names and mappings + vdefs = {} + for vid, vcedef, vce in ps: # vcedef == emtpy or first pObj == voice definition + pname, psubnm = '', '' # part name and abbreviation + if not vcedef: # simple abc without voice definitions + vdefs [vid] = pname, psubnm, '' + else: # abc with voice definitions + if vid != vcedef.t[1]: info ('voice ids unequal: %s (reg-ex) != %s (grammar)' % (vid, vcedef.t[1])) + rn = re.search (r'(?:name|nm)="([^"]*)"', vcedef.t[2]) + if rn: pname = rn.group (1) + rn = re.search (r'(?:subname|snm|sname)="([^"]*)"', vcedef.t[2]) + if rn: psubnm = rn.group (1) + vcedef.t[2] = vcedef.t[2].replace ('"%s"' % pname, '""').replace ('"%s"' % psubnm, '""') # clear voice name to avoid false clef matches later on + vdefs [vid] = pname, psubnm, vcedef.t[2] + xs = [pObj.t[1] for maat in vce for pObj in maat if pObj.name == 'inline'] # all inline statements in vce + s.staveDefs += [x.replace ('%5d',']') for x in xs if x.startswith ('score') or x.startswith ('staves')] # filter %%score and %%staves + return vdefs + + def doHeaderField (s, fld, attrmap): + type, value = fld.t[0], fld.t[1].replace ('%5d',']') # restore closing brackets (see splitHeaderVoices) + if not value: # skip empty field + return + if type == 'M': + attrmap [type] = value + elif type == 'L': + try: s.unitL = lmap (int, fld.t[1].split ('/')) + except: + info ('illegal unit length:%s, 1/8 assumed' % fld.t[1]) + s.unitL = 1,8 + if len (s.unitL) == 1 or s.unitL[1] not in s.typeMap: + info ('L:%s is not allowed, 1/8 assumed' % fld.t[1]) + s.unitL = 1,8 + elif type == 'K': + attrmap[type] = value + elif type == 'T': + s.title = s.title + '\n' + value if s.title else value + elif type == 'U': + sym = fld.t[2].strip ('!+') + s.usrSyms [value] = sym + elif type == 'I': + s.doField_I (type, value, lambda x,y,z:0, lambda x:0) + elif type == 'Q': + attrmap[type] = value + elif type in 'CRZNOAGHBDFSP': # part maps are treated as meta data + type = s.metaMap.get (type, type) # respect the (user defined --meta) mapping of various ABC fields to XML meta data types + c = s.metadata.get (type, '') + s.metadata [type] = c + '\n' + value if c else value # concatenate multiple info fields with new line as separator + else: + info ('skipped header: %s' % fld) + + def mkIdentification (s, score, lev): + if s.title: + xs = s.title.split ('\n') # the first T: line goes to work-title + ys = '\n'.join (xs [1:]) # place subsequent T: lines into work-number + w = E.Element ('work') + addElem (score, w, lev + 1) + if ys: addElemT (w, 'work-number', ys, lev + 2) + addElemT (w, 'work-title', xs[0], lev + 2) + ident = E.Element ('identification') + addElem (score, ident, lev + 1) + for mtype, mval in s.metadata.items (): + if mtype in s.metaTypes and mtype != 'rights': # all metaTypes are MusicXML creator types + c = E.Element ('creator', type=mtype) + c.text = mval + addElem (ident, c, lev + 2) + if 'rights' in s.metadata: + c = addElemT (ident, 'rights', s.metadata ['rights'], lev + 2) + encoding = E.Element ('encoding') + addElem (ident, encoding, lev + 2) + encoder = E.Element ('encoder') + encoder.text = 'abc2xml version %d' % VERSION + addElem (encoding, encoder, lev + 3) + if s.supports_tag: # avoids interference of auto-flowing and explicit linebreaks + suports = E.Element ('supports', attribute="new-system", element="print", type="yes", value="yes") + addElem (encoding, suports, lev + 3) + encodingDate = E.Element ('encoding-date') + encodingDate.text = str (datetime.date.today ()) + addElem (encoding, encodingDate, lev + 3) + s.addMeta (ident, lev + 2) + + def mkDefaults (s, score, lev): + if s.pageFmtCmd: s.pageFmtAbc = s.pageFmtCmd + if not s.pageFmtAbc: return # do not output the defaults if none is desired + abcScale, h, w, l, r, t, b = s.pageFmtAbc + space = abcScale * 2.117 # 2.117 = 6pt = space between staff lines for scale = 1.0 in abcm2ps + mils = 4 * space # staff height in millimeters + scale = 40. / mils # tenth's per millimeter + dflts = E.Element ('defaults') + addElem (score, dflts, lev) + scaling = E.Element ('scaling') + addElem (dflts, scaling, lev + 1) + addElemT (scaling, 'millimeters', '%g' % mils, lev + 2) + addElemT (scaling, 'tenths', '40', lev + 2) + layout = E.Element ('page-layout') + addElem (dflts, layout, lev + 1) + addElemT (layout, 'page-height', '%g' % (h * scale), lev + 2) + addElemT (layout, 'page-width', '%g' % (w * scale), lev + 2) + margins = E.Element ('page-margins', type='both') + addElem (layout, margins, lev + 2) + addElemT (margins, 'left-margin', '%g' % (l * scale), lev + 3) + addElemT (margins, 'right-margin', '%g' % (r * scale), lev + 3) + addElemT (margins, 'top-margin', '%g' % (t * scale), lev + 3) + addElemT (margins, 'bottom-margin', '%g' % (b * scale), lev + 3) + + def addMeta (s, parent, lev): + misc = E.Element ('miscellaneous') + mf = 0 + for mtype, mval in sorted (s.metadata.items ()): + if mtype == 'S': + addElemT (parent, 'source', mval, lev) + elif mtype in s.metaTypes: continue # mapped meta data has already been output (in creator elements) + else: + mf = E.Element ('miscellaneous-field', name=s.metaTab [mtype]) + mf.text = mval + addElem (misc, mf, lev + 1) + if mf != 0: addElem (parent, misc, lev) + + def parse (s, abc_string, rOpt=False, bOpt=False, fOpt=False): + abctext = abc_string.replace ('[I:staff ','[I:staff') # avoid false beam breaks + s.reset (fOpt) + header, voices = splitHeaderVoices (abctext) + ps = [] + try: + lbrk_insert = 0 if re.search (r'I:linebreak\s*([!$]|none)|I:continueall\s*(1|true)', header) else bOpt + hs = abc_header.parseString (header) if header else '' + for id, voice in voices: + if lbrk_insert: # insert linebreak at EOL + r1 = re.compile (r'\[[wA-Z]:[^]]*\]') # inline field + has_abc = lambda x: r1.sub ('', x).strip () # empty if line only contains inline fields + voice = '\n'.join ([balk.rstrip ('$!') + '$' if has_abc (balk) else balk for balk in voice.splitlines ()]) + prevLeftBar = None # previous voice ended with a left-bar symbol (double repeat) + s.orderChords = s.fOpt and ('tab' in voice [:200] or [x for x in hs if x.t[0] == 'K' and 'tab' in x.t[1]]) + vce = abc_voice.parseString (voice).asList () + lyr_notes = [] # remember notes between lyric blocks + for m in vce: # all measures + for e in m: # all abc-elements + if e.name == 'lyr_blk': # -> e.objs is list of lyric lines + lyr = [line.objs for line in e.objs] # line.objs is listof syllables + alignLyr (lyr_notes, lyr) # put all syllables into corresponding notes + lyr_notes = [] + else: + lyr_notes.append (e) + if not vce: # empty voice, insert an inline field that will be rejected + vce = [[pObj ('inline', ['I', 'empty voice'])]] + if prevLeftBar: + vce[0].insert (0, prevLeftBar) # insert at begin of first measure + prevLeftBar = None + if vce[-1] and vce[-1][-1].name == 'lbar': # last measure ends with an lbar + prevLeftBar = vce[-1][-1] + if len (vce) > 1: # vce should not become empty (-> exception when taking vcelyr [0][0]) + del vce[-1] # lbar was the only element in measure vce[-1] + vcelyr = vce + elem1 = vcelyr [0][0] # the first element of the first measure + if elem1.name == 'inline'and elem1.t[0] == 'V': # is a voice definition + voicedef = elem1 + del vcelyr [0][0] # do not read voicedef twice + else: + voicedef = '' + ps.append ((id, voicedef, vcelyr)) + except ParseException as err: + if err.loc > 40: # limit length of error message, compatible with markInputline + err.pstr = err.pstr [err.loc - 40: err.loc + 40] + err.loc = 40 + xs = err.line[err.col-1:] + info (err.line, warn=0) + info ((err.col-1) * '-' + '^', warn=0) + if re.search (r'\[U:', xs): + info ('Error: illegal user defined symbol: %s' % xs[1:], warn=0) + elif re.search (r'\[[OAPZNGHRBDFSXTCIU]:', xs): + info ('Error: header-only field %s appears after K:' % xs[1:], warn=0) + else: + info ('Syntax error at column %d' % err.col, warn=0) + raise + + score = E.Element ('score-partwise') + attrmap = {'Div': str (s.divisions), 'K':'C treble', 'M':'4/4'} + for res in hs: + if res.name == 'field': + s.doHeaderField (res, attrmap) + else: + info ('unexpected header item: %s' % res) + + vdefs = s.voiceNamesAndMaps (ps) + vdefs = s.parseStaveDef (vdefs) + + lev = 0 + vids, parts, partAttr = [], [], {} + s.strAlloc = stringAlloc () + for vid, _, vce in ps: # voice id, voice parse tree + pname, psubnm, voicedef = vdefs [vid] # part name + attrmap ['V'] = voicedef # abc text of first voice definition (after V:vid) or empty + pid = 'P%s' % vid # let part id start with an alpha + s.vid = vid # avoid parameter passing, needed in mkNote for instrument id + s.pid = s.vcepid [s.vid] # xml part-id for the current voice + s.gTime = (0, 0) # reset time + s.strAlloc.beginZoek () # reset search index + part = s.mkPart (vce, pid, lev + 1, attrmap, s.gNstaves.get (vid, 0), rOpt) + if 'Q' in attrmap: del attrmap ['Q'] # header tempo only in first part + parts.append (part) + vids.append (vid) + partAttr [vid] = (pname, psubnm, s.midprg) + if s.midprg != ['', '', '', ''] and not s.percVoice: # when a part has only rests + instId = 'I%s-%s' % (s.pid, s.vid) + if instId not in s.midiInst: s.midiInst [instId] = (s.pid, s.vid, s.midprg [0], s.midprg [1], s.midprg [2], s.midprg [3]) + parts, vidsnew = mergeParts (parts, vids, s.staves, rOpt) # merge parts into staves as indicated by %%score + parts, vidsnew = mergeParts (parts, vidsnew, s.grands, rOpt, 1) # merge grand staves + reduceMids (parts, vidsnew, s.midiInst) + + s.mkIdentification (score, lev) + s.mkDefaults (score, lev + 1) + + partlist = s.mkPartlist (vids, partAttr, lev + 1) + addElem (score, partlist, lev + 1) + for ip, part in enumerate (parts): addElem (score, part, lev + 1) + + return score + + +def decodeInput (data_string): + try: enc = 'utf-8'; unicode_string = data_string.decode (enc) + except: + try: enc = 'latin-1'; unicode_string = data_string.decode (enc) + except: raise ValueError ('data not encoded in utf-8 nor in latin-1') + info ('decoded from %s' % enc) + return unicode_string + +def ggd (a, b): # greatest common divisor + return a if b == 0 else ggd (b, a % b) + +xmlVersion = "" +def fixDoctype (elem): + if python3: xs = E.tostring (elem, encoding='unicode') # writing to file will auto-encode to utf-8 + else: xs = E.tostring (elem, encoding='utf-8') # keep the string utf-8 encoded for writing to file + ys = xs.split ('\n') + ys.insert (0, xmlVersion) # crooked logic of ElementTree lib + ys.insert (1, '') + return '\n'.join (ys) + +def xml2mxl (pad, fnm, data): # write xml data to compressed .mxl file + from zipfile import ZipFile, ZIP_DEFLATED + fnmext = fnm + '.xml' # file name with extension, relative to the root within the archive + outfile = os.path.join (pad, fnm + '.mxl') + meta = '%s\n\n' % xmlVersion + meta += '\n' % fnmext + meta += '' + f = ZipFile (outfile, 'w', ZIP_DEFLATED) + f.writestr ('META-INF/container.xml', meta) + f.writestr (fnmext, data) + f.close () + info ('%s written' % outfile, warn=0) + +def convert (pad, fnm, abc_string, mxl, rOpt=False, tOpt=False, bOpt=False, fOpt=False): # not used, backwards compatibility + score = mxm.parse (abc_string, rOpt, bOpt, fOpt) + writefile (pad, fnm, '', score, mxl, tOpt) + +def writefile (pad, fnm, fnmNum, xmldoc, mxlOpt, tOpt=False): + ipad, ifnm = os.path.split (fnm) # base name of input path is + if tOpt: + x = xmldoc.findtext ('work/work-title', 'no_title') + ifnm = x.replace (',','_').replace ("'",'_').replace ('?','_') + else: + ifnm += fnmNum + xmlstr = fixDoctype (xmldoc) + if pad: + if not mxlOpt or mxlOpt in ['a', 'add']: + outfnm = os.path.join (pad, ifnm + '.xml') # joined with path from -o option + outfile = open (outfnm, 'w') + outfile.write (xmlstr) + outfile.close () + info ('%s written' % outfnm, warn=0) + if mxlOpt: xml2mxl (pad, ifnm, xmlstr) # also write a compressed version + else: + outfile = sys.stdout + outfile.write (xmlstr) + outfile.write ('\n') + +def readfile (fnmext, errmsg='read error: '): + try: + if fnmext == '-.abc': fobj = stdin # see python2/3 differences + else: fobj = open (fnmext, 'rb') + encoded_data = fobj.read () + fobj.close () + return encoded_data if type (encoded_data) == uni_type else decodeInput (encoded_data) + except Exception as e: + info (errmsg + repr (e) + ' ' + fnmext) + return None + +def expand_abc_include (abctxt): + ys = [] + for x in abctxt.splitlines (): + if x.startswith ('%%abc-include') or x.startswith ('I:abc-include'): + x = readfile (x[13:].strip (), 'include error: ') + if x != None: ys.append (x) + return '\n'.join (ys) + +abc_header, abc_voice, abc_scoredef, abc_percmap = abc_grammar () # compute grammars only once +mxm = MusicXml () # same for instance of MusicXml + +def getXmlScores (abc_string, skip=0, num=1, rOpt=False, bOpt=False, fOpt=False): # not used, backwards compatibility + return [fixDoctype (xml_doc) for xml_doc in + getXmlDocs (abc_string, skip=0, num=1, rOpt=False, bOpt=False, fOpt=False)] + +def getXmlDocs (abc_string, skip=0, num=1, rOpt=False, bOpt=False, fOpt=False): # added by David Randolph + xml_docs = [] + abctext = expand_abc_include (abc_string) + fragments = re.split ('^\s*X:', abctext, flags=re.M) + preamble = fragments [0] # tunes can be preceeded by formatting instructions + tunes = fragments[1:] + if not tunes and preamble: tunes, preamble = ['1\n' + preamble], '' # tune without X: + for itune, tune in enumerate (tunes): + if itune < skip: continue # skip tunes, then read at most num tunes + if itune >= skip + num: break + tune = preamble + 'X:' + tune # restore preamble before each tune + try: # convert string abctext -> file pad/fnmNum.xml + score = mxm.parse (tune, rOpt, bOpt, fOpt) + ds = list (score.iter ('duration')) # need to iterate twice + ss = [int (d.text) for d in ds] + deler = reduce (ggd, ss + [21]) # greatest common divisor of all durations + for i, d in enumerate (ds): d.text = str (ss [i] // deler) + for d in score.iter ('divisions'): d.text = str (int (d.text) // deler) + xml_docs.append (score) + except ParseException: + pass # output already printed + except Exception as err: + info ('an exception occurred.\n%s' % err) + return xml_docs + +#---------------- +# Main Program +#---------------- +if __name__ == '__main__': + from optparse import OptionParser + from glob import glob + import time + + parser = OptionParser (usage='%prog [-h] [-r] [-t] [-b] [-m SKIP NUM] [-o DIR] [-p PFMT] [-z MODE] [--meta MAP] [ ...]', version='version %d' % VERSION) + parser.add_option ("-o", action="store", help="store xml files in DIR", default='', metavar='DIR') + parser.add_option ("-m", action="store", help="skip SKIP (0) tunes, then read at most NUM (1) tunes", nargs=2, type='int', default=(0,1), metavar='SKIP NUM') + parser.add_option ("-p", action="store", help="pageformat PFMT (mm) = scale (0.75), pageheight (297), pagewidth (210), leftmargin (18), rightmargin (18), topmargin (10), botmargin (10)", default='', metavar='PFMT') + parser.add_option ("-z", "--mxl", dest="mxl", help="store as compressed mxl, MODE = a(dd) or r(eplace)", default='', metavar='MODE') + parser.add_option ("-r", action="store_true", help="show whole measure rests in merged staffs", default=False) + parser.add_option ("-t", action="store_true", help="use tune title as file name", default=False) + parser.add_option ("-b", action="store_true", help="line break at EOL", default=False) + parser.add_option ("--meta", action="store", help="map infofields to XML metadata, MAP = R:poet,Z:lyricist,N:...", default='', metavar='MAP') + parser.add_option ("-f", action="store_true", help="force string/fret allocations for tab staves", default=False) + options, args = parser.parse_args () + if len (args) == 0: parser.error ('no input file given') + pad = options.o + if options.mxl and options.mxl not in ['a','add', 'r', 'replace']: + parser.error ('MODE should be a(dd) or r(eplace), not: %s' % options.mxl) + if pad: + if not os.path.exists (pad): os.mkdir (pad) + if not os.path.isdir (pad): parser.error ('%s is not a directory' % pad) + if options.p: # set page formatting values + try: # space, page-height, -width, margin-left, -right, -top, -bottom + mxm.pageFmtCmd = lmap (float, options.p.split (',')) + if len (mxm.pageFmtCmd) != 7: raise ValueError ('-p needs 7 values') + except Exception as err: parser.error (err) + for x in options.meta.split (','): + if not x: continue + try: field, tag = x.split (':') + except: parser.error ('--meta: %s cannot be split on colon' % x) + if field not in 'OAZNGHRBDFSPW': parser.error ('--meta: field %s is no valid ABC field' % field) + if tag not in mxm.metaTypes: parser.error ('--meta: tag %s is no valid XML creator type' % tag) + mxm.metaMap [field] = tag + fnmext_list = [] + for i in args: + if i == '-': fnmext_list.append ('-.abc') # represents standard input + else: fnmext_list += glob (i) + if not fnmext_list: parser.error ('none of the input files exist') + t_start = time.time () + for fnmext in fnmext_list: + fnm, ext = os.path.splitext (fnmext) + if ext.lower () not in ('.abc'): + info ('skipped input file %s, it should have extension .abc' % fnmext) + continue + if os.path.isdir (fnmext): + info ('skipped directory %s. Only files are accepted' % fnmext) + continue + abctext = readfile (fnmext) + skip, num = options.m + xml_docs = getXmlDocs (abctext, skip, num, options.r, options.b, options.f) + for itune, xmldoc in enumerate (xml_docs): + fnmNum = '%02d' % (itune + 1) if len (xml_docs) > 1 else '' + writefile (pad, fnm, fnmNum, xmldoc, options.mxl, options.t) + info ('done in %.2f secs' % (time.time () - t_start)) diff --git a/abc2xml/pyparsing.py b/abc2xml/pyparsing.py index 9be97dc..51b9bcc 100644 --- a/abc2xml/pyparsing.py +++ b/abc2xml/pyparsing.py @@ -1,6 +1,6 @@ # module pyparsing.py # -# Copyright (c) 2003-2011 Paul T. McGuire +# Copyright (c) 2003-2013 Paul T. McGuire # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -21,7 +21,6 @@ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # -#from __future__ import generators __doc__ = \ """ @@ -40,7 +39,7 @@ Here is a program to parse "Hello, World!" (or any greeting of the form C{"", greet.parseString( hello ) + print (hello, "->", greet.parseString( hello )) The program outputs the following:: @@ -58,8 +57,8 @@ The pyparsing module handles some of the problems that are typically vexing when - embedded comments """ -__version__ = "1.5.6" -__versionTime__ = "26 June 2011 10:53" +__version__ = "2.0.1" +__versionTime__ = "16 July 2013 22:22" __author__ = "Paul McGuire " import string @@ -69,6 +68,7 @@ import sys import warnings import re import sre_constants +import collections #~ sys.stderr.write( "testing pyparsing module, version %s, %s\n" % (__version__,__versionTime__ ) ) __all__ = [ @@ -81,32 +81,29 @@ __all__ = [ 'White', 'Word', 'WordEnd', 'WordStart', 'ZeroOrMore', 'alphanums', 'alphas', 'alphas8bit', 'anyCloseTag', 'anyOpenTag', 'cStyleComment', 'col', 'commaSeparatedList', 'commonHTMLEntity', 'countedArray', 'cppStyleComment', 'dblQuotedString', -'dblSlashComment', 'delimitedList', 'dictOf', 'downcaseTokens', 'empty', 'getTokensEndLoc', 'hexnums', +'dblSlashComment', 'delimitedList', 'dictOf', 'downcaseTokens', 'empty', 'hexnums', 'htmlComment', 'javaStyleComment', 'keepOriginalText', 'line', 'lineEnd', 'lineStart', 'lineno', 'makeHTMLTags', 'makeXMLTags', 'matchOnlyAtCol', 'matchPreviousExpr', 'matchPreviousLiteral', 'nestedExpr', 'nullDebugAction', 'nums', 'oneOf', 'opAssoc', 'operatorPrecedence', 'printables', 'punc8bit', 'pythonStyleComment', 'quotedString', 'removeQuotes', 'replaceHTMLEntity', 'replaceWith', 'restOfLine', 'sglQuotedString', 'srange', 'stringEnd', 'stringStart', 'traceParseAction', 'unicodeString', 'upcaseTokens', 'withAttribute', -'indentedBlock', 'originalTextFor', +'indentedBlock', 'originalTextFor', 'ungroup', 'infixNotation', ] -""" -Detect if we are running version 3.X and make appropriate changes -Robert A. Clark -""" -_PY3K = sys.version_info[0] > 2 -if _PY3K: +PY_3 = sys.version.startswith('3') +if PY_3: _MAX_INT = sys.maxsize basestring = str unichr = chr _ustr = str - alphas = string.ascii_lowercase + string.ascii_uppercase + + # build list of single arg builtins, that can be used as parse actions + singleArgBuiltins = [sum, len, sorted, reversed, list, tuple, set, any, all, min, max] + else: _MAX_INT = sys.maxint range = xrange - set = lambda s : dict( [(c,0) for c in s] ) - alphas = string.lowercase + string.uppercase def _ustr(obj): """Drop-in replacement for str(obj) that tries to be Unicode friendly. It first tries @@ -134,24 +131,23 @@ else: # Replace unprintables with question marks? #return unicode(obj).encode(sys.getdefaultencoding(), 'replace') # ... - - alphas = string.lowercase + string.uppercase -# build list of single arg builtins, tolerant of Python version, that can be used as parse actions -singleArgBuiltins = [] -import __builtin__ -for fname in "sum len enumerate sorted reversed list tuple set any all".split(): - try: - singleArgBuiltins.append(getattr(__builtin__,fname)) - except AttributeError: - continue + # build list of single arg builtins, tolerant of Python version, that can be used as parse actions + singleArgBuiltins = [] + import __builtin__ + for fname in "sum len sorted reversed list tuple set any all min max".split(): + try: + singleArgBuiltins.append(getattr(__builtin__,fname)) + except AttributeError: + continue + def _xml_escape(data): """Escape &, <, >, ", ', etc. in a string of data.""" # ampersand must be replaced first from_symbols = '&><"\'' - to_symbols = ['&'+s+';' for s in "amp gt lt quot apos".split()] + to_symbols = ('&'+s+';' for s in "amp gt lt quot apos".split()) for from_,to_ in zip(from_symbols, to_symbols): data = data.replace(from_, to_) return data @@ -159,11 +155,12 @@ def _xml_escape(data): class _Constants(object): pass -nums = string.digits +alphas = string.ascii_lowercase + string.ascii_uppercase +nums = "0123456789" hexnums = nums + "ABCDEFabcdef" alphanums = alphas + nums _bslash = chr(92) -printables = "".join( [ c for c in string.printable if c not in string.whitespace ] ) +printables = "".join(c for c in string.printable if c not in string.whitespace) class ParseBaseException(Exception): """base exception class for all parsing runtime exceptions""" @@ -206,12 +203,12 @@ class ParseBaseException(Exception): line_str = self.line line_column = self.column - 1 if markerString: - line_str = "".join( [line_str[:line_column], - markerString, line_str[line_column:]]) + line_str = "".join(line_str[:line_column], + markerString, line_str[line_column:]) return line_str.strip() def __dir__(self): return "loc msg pstr parserElement lineno col line " \ - "markInputLine __str__ __repr__".split() + "markInputline __str__ __repr__".split() class ParseException(ParseBaseException): """exception thrown when parse expressions don't match class; @@ -228,8 +225,8 @@ class ParseFatalException(ParseBaseException): pass class ParseSyntaxException(ParseFatalException): - """just like C{ParseFatalException}, but thrown internally when an - C{ErrorStop} ('-' operator) indicates that parsing is to stop immediately because + """just like C{L{ParseFatalException}}, but thrown internally when an + C{L{ErrorStop}} ('-' operator) indicates that parsing is to stop immediately because an unbacktrackable syntax error has been found""" def __init__(self, pe): super(ParseSyntaxException, self).__init__( @@ -444,16 +441,13 @@ class ParseResults(object): return "(%s, %s)" % ( repr( self.__toklist ), repr( self.__tokdict ) ) def __str__( self ): - out = "[" - sep = "" + out = [] for i in self.__toklist: if isinstance(i, ParseResults): - out += sep + _ustr(i) + out.append(_ustr(i)) else: - out += sep + repr(i) - sep = ", " - out += "]" - return out + out.append(repr(i)) + return '[' + ', '.join(out) + ']' def _asStringList( self, sep='' ): out = [] @@ -493,8 +487,8 @@ class ParseResults(object): """Returns the parse results as XML. Tags are created for tokens and lists that have defined results names.""" nl = "\n" out = [] - namedItems = dict( [ (v[1],k) for (k,vlist) in self.__tokdict.items() - for v in vlist ] ) + namedItems = dict((v[1],k) for (k,vlist) in self.__tokdict.items() + for v in vlist) nextLevelIndent = indent + " " # collapse out indents if formatting is not desired @@ -616,7 +610,13 @@ class ParseResults(object): self.__parent = None def __dir__(self): - return dir(super(ParseResults,self)) + self.keys() + return dir(super(ParseResults,self)) + list(self.keys()) + +if hasattr (collections, 'MutableMapping'): + collections.MutableMapping.register(ParseResults) +else: + from collections.abc import MutableMapping + MutableMapping.register (ParseResults) def col (loc,strg): """Returns current column within a string, counting newlines as line separators. @@ -624,7 +624,7 @@ def col (loc,strg): Note: the default parsing behavior is to expand tabs in the input string before starting the parsing process. See L{I{ParserElement.parseString}} for more information - on parsing strings containing s, and suggested methods to maintain a + on parsing strings containing C{}s, and suggested methods to maintain a consistent view of the parsed string, the parse location, and line and column positions within the parsed string. """ @@ -636,7 +636,7 @@ def lineno(loc,strg): Note: the default parsing behavior is to expand tabs in the input string before starting the parsing process. See L{I{ParserElement.parseString}} for more information - on parsing strings containing s, and suggested methods to maintain a + on parsing strings containing C{}s, and suggested methods to maintain a consistent view of the parsed string, the parse location, and line and column positions within the parsed string. """ @@ -653,7 +653,7 @@ def line( loc, strg ): return strg[lastCR+1:] def _defaultStartDebugAction( instring, loc, expr ): - print ("Match " + _ustr(expr) + " at loc " + _ustr(loc) + "(%d,%d)" % ( lineno(loc,instring), col(loc,instring) )) + print (("Match " + _ustr(expr) + " at loc " + _ustr(loc) + "(%d,%d)" % ( lineno(loc,instring), col(loc,instring) ))) def _defaultSuccessDebugAction( instring, startloc, endloc, expr, toks ): print ("Matched " + _ustr(expr) + " -> " + str(toks.asList())) @@ -665,35 +665,47 @@ def nullDebugAction(*args): """'Do-nothing' debug action, to suppress debugging output during parsing.""" pass +# Only works on Python 3.x - nonlocal is toxic to Python 2 installs +#~ 'decorator to trim function calls to match the arity of the target' +#~ def _trim_arity(func, maxargs=3): + #~ if func in singleArgBuiltins: + #~ return lambda s,l,t: func(t) + #~ limit = 0 + #~ foundArity = False + #~ def wrapper(*args): + #~ nonlocal limit,foundArity + #~ while 1: + #~ try: + #~ ret = func(*args[limit:]) + #~ foundArity = True + #~ return ret + #~ except TypeError: + #~ if limit == maxargs or foundArity: + #~ raise + #~ limit += 1 + #~ continue + #~ return wrapper + +# this version is Python 2.x-3.x cross-compatible 'decorator to trim function calls to match the arity of the target' -if not _PY3K: - def _trim_arity(func, maxargs=2): - limit = [0] - def wrapper(*args): - while 1: - try: - return func(*args[limit[0]:]) - except TypeError: - if limit[0] <= maxargs: - limit[0] += 1 - continue - raise - return wrapper -else: - def _trim_arity(func, maxargs=2): - limit = maxargs - def wrapper(*args): - #~ nonlocal limit - while 1: - try: - return func(*args[limit:]) - except TypeError: - if limit: - limit -= 1 - continue - raise - return wrapper - +def _trim_arity(func, maxargs=2): + if func in singleArgBuiltins: + return lambda s,l,t: func(t) + limit = [0] + foundArity = [False] + def wrapper(*args): + while 1: + try: + ret = func(*args[limit[0]:]) + foundArity[0] = True + return ret + except TypeError: + if limit[0] <= maxargs and not foundArity[0]: + limit[0] += 1 + continue + raise + return wrapper + class ParserElement(object): """Abstract base level parser element class.""" DEFAULT_WHITE_CHARS = " \n\t\r" @@ -705,6 +717,13 @@ class ParserElement(object): ParserElement.DEFAULT_WHITE_CHARS = chars setDefaultWhitespaceChars = staticmethod(setDefaultWhitespaceChars) + def inlineLiteralsUsing(cls): + """ + Set class to be used for inclusion of string literals into a parser. + """ + ParserElement.literalStringClass = cls + inlineLiteralsUsing = staticmethod(inlineLiteralsUsing) + def __init__( self, savelist=False ): self.parseAction = list() self.failAction = None @@ -789,14 +808,14 @@ class ParserElement(object): C{fn(loc,toks)}, C{fn(toks)}, or just C{fn()}, where: - s = the original string being parsed (see note below) - loc = the location of the matching substring - - toks = a list of the matched tokens, packaged as a ParseResults object + - toks = a list of the matched tokens, packaged as a C{L{ParseResults}} object If the functions in fns modify the tokens, they can return them as the return value from fn, and the modified list of tokens will replace the original. Otherwise, fn does not need to return any value. Note: the default parsing behavior is to expand tabs in the input string before starting the parsing process. See L{I{parseString}} for more information - on parsing strings containing s, and suggested methods to maintain a + on parsing strings containing C{}s, and suggested methods to maintain a consistent view of the parsed string, the parse location, and line and column positions within the parsed string. """ @@ -818,7 +837,7 @@ class ParserElement(object): - loc = location where expression match was attempted and failed - expr = the parse expression that failed - err = the exception thrown - The function returns no value. It may throw C{ParseFatalException} + The function returns no value. It may throw C{L{ParseFatalException}} if it is desired to stop parsing immediately.""" self.failAction = fn return self @@ -872,15 +891,11 @@ class ParserElement(object): loc,tokens = self.parseImpl( instring, preloc, doActions ) except IndexError: raise ParseException( instring, len(instring), self.errmsg, self ) - except ParseBaseException: + except ParseBaseException as err: #~ print ("Exception raised:", err) - err = None if self.debugActions[2]: - err = sys.exc_info()[1] self.debugActions[2]( instring, tokensStart, self, err ) if self.failAction: - if err is None: - err = sys.exc_info()[1] self.failAction( instring, tokensStart, self, err ) raise else: @@ -910,10 +925,9 @@ class ParserElement(object): self.resultsName, asList=self.saveAsList and isinstance(tokens,(ParseResults,list)), modal=self.modalResults ) - except ParseBaseException: + except ParseBaseException as err: #~ print "Exception raised in user parse action:", err if (self.debugActions[2] ): - err = sys.exc_info()[1] self.debugActions[2]( instring, tokensStart, self, err ) raise else: @@ -952,8 +966,8 @@ class ParserElement(object): value = self._parseNoCache( instring, loc, doActions, callPreParse ) ParserElement._exprArgCache[ lookup ] = (value[0],value[1].copy()) return value - except ParseBaseException: - pe = sys.exc_info()[1] + except ParseBaseException as pe: + pe.__traceback__ = None ParserElement._exprArgCache[ lookup ] = pe raise @@ -994,7 +1008,7 @@ class ParserElement(object): If you want the grammar to require that the entire input string be successfully parsed, then set C{parseAll} to True (equivalent to ending - the grammar with C{StringEnd()}). + the grammar with C{L{StringEnd()}}). Note: C{parseString} implicitly calls C{expandtabs()} on the input string, in order to report proper column numbers in parse actions. @@ -1023,12 +1037,11 @@ class ParserElement(object): loc = self.preParse( instring, loc ) se = Empty() + StringEnd() se._parse( instring, loc ) - except ParseBaseException: + except ParseBaseException as exc: if ParserElement.verbose_stacktrace: raise else: # catch and re-raise exception from here, clears out pyparsing internal stack trace - exc = sys.exc_info()[1] raise exc else: return tokens @@ -1076,16 +1089,15 @@ class ParserElement(object): loc = nextLoc else: loc = preloc+1 - except ParseBaseException: + except ParseBaseException as exc: if ParserElement.verbose_stacktrace: raise else: # catch and re-raise exception from here, clears out pyparsing internal stack trace - exc = sys.exc_info()[1] raise exc def transformString( self, instring ): - """Extension to C{scanString}, to modify matching text with modified tokens that may + """Extension to C{L{scanString}}, to modify matching text with modified tokens that may be returned from a parse action. To use C{transformString}, define a grammar and attach a parse action to it that modifies the returned token list. Invoking C{transformString()} on a target string will then scan for matches, @@ -1110,33 +1122,31 @@ class ParserElement(object): out.append(instring[lastE:]) out = [o for o in out if o] return "".join(map(_ustr,_flatten(out))) - except ParseBaseException: + except ParseBaseException as exc: if ParserElement.verbose_stacktrace: raise else: # catch and re-raise exception from here, clears out pyparsing internal stack trace - exc = sys.exc_info()[1] raise exc def searchString( self, instring, maxMatches=_MAX_INT ): - """Another extension to C{scanString}, simplifying the access to the tokens found + """Another extension to C{L{scanString}}, simplifying the access to the tokens found to match the given parse expression. May be called with optional C{maxMatches} argument, to clip searching after 'n' matches are found. """ try: return ParseResults([ t for t,s,e in self.scanString( instring, maxMatches ) ]) - except ParseBaseException: + except ParseBaseException as exc: if ParserElement.verbose_stacktrace: raise else: # catch and re-raise exception from here, clears out pyparsing internal stack trace - exc = sys.exc_info()[1] raise exc def __add__(self, other ): - """Implementation of + operator - returns And""" + """Implementation of + operator - returns C{L{And}}""" if isinstance( other, basestring ): - other = Literal( other ) + other = ParserElement.literalStringClass( other ) if not isinstance( other, ParserElement ): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), SyntaxWarning, stacklevel=2) @@ -1144,9 +1154,9 @@ class ParserElement(object): return And( [ self, other ] ) def __radd__(self, other ): - """Implementation of + operator when left operand is not a C{ParserElement}""" + """Implementation of + operator when left operand is not a C{L{ParserElement}}""" if isinstance( other, basestring ): - other = Literal( other ) + other = ParserElement.literalStringClass( other ) if not isinstance( other, ParserElement ): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), SyntaxWarning, stacklevel=2) @@ -1154,9 +1164,9 @@ class ParserElement(object): return other + self def __sub__(self, other): - """Implementation of - operator, returns C{And} with error stop""" + """Implementation of - operator, returns C{L{And}} with error stop""" if isinstance( other, basestring ): - other = Literal( other ) + other = ParserElement.literalStringClass( other ) if not isinstance( other, ParserElement ): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), SyntaxWarning, stacklevel=2) @@ -1164,9 +1174,9 @@ class ParserElement(object): return And( [ self, And._ErrorStop(), other ] ) def __rsub__(self, other ): - """Implementation of - operator when left operand is not a C{ParserElement}""" + """Implementation of - operator when left operand is not a C{L{ParserElement}}""" if isinstance( other, basestring ): - other = Literal( other ) + other = ParserElement.literalStringClass( other ) if not isinstance( other, ParserElement ): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), SyntaxWarning, stacklevel=2) @@ -1179,12 +1189,12 @@ class ParserElement(object): tuple, similar to C{{min,max}} multipliers in regular expressions. Tuples may also include C{None} as in: - C{expr*(n,None)} or C{expr*(n,)} is equivalent - to C{expr*n + ZeroOrMore(expr)} + to C{expr*n + L{ZeroOrMore}(expr)} (read as "at least n instances of C{expr}") - C{expr*(None,n)} is equivalent to C{expr*(0,n)} (read as "0 to n instances of C{expr}") - - C{expr*(None,None)} is equivalent to C{ZeroOrMore(expr)} - - C{expr*(1,None)} is equivalent to C{OneOrMore(expr)} + - C{expr*(None,None)} is equivalent to C{L{ZeroOrMore}(expr)} + - C{expr*(1,None)} is equivalent to C{L{OneOrMore}(expr)} Note that C{expr*(None,n)} does not raise an exception if more than n exprs exist in the input stream; that is, @@ -1245,9 +1255,9 @@ class ParserElement(object): return self.__mul__(other) def __or__(self, other ): - """Implementation of | operator - returns C{MatchFirst}""" + """Implementation of | operator - returns C{L{MatchFirst}}""" if isinstance( other, basestring ): - other = Literal( other ) + other = ParserElement.literalStringClass( other ) if not isinstance( other, ParserElement ): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), SyntaxWarning, stacklevel=2) @@ -1255,9 +1265,9 @@ class ParserElement(object): return MatchFirst( [ self, other ] ) def __ror__(self, other ): - """Implementation of | operator when left operand is not a C{ParserElement}""" + """Implementation of | operator when left operand is not a C{L{ParserElement}}""" if isinstance( other, basestring ): - other = Literal( other ) + other = ParserElement.literalStringClass( other ) if not isinstance( other, ParserElement ): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), SyntaxWarning, stacklevel=2) @@ -1265,9 +1275,9 @@ class ParserElement(object): return other | self def __xor__(self, other ): - """Implementation of ^ operator - returns C{Or}""" + """Implementation of ^ operator - returns C{L{Or}}""" if isinstance( other, basestring ): - other = Literal( other ) + other = ParserElement.literalStringClass( other ) if not isinstance( other, ParserElement ): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), SyntaxWarning, stacklevel=2) @@ -1275,9 +1285,9 @@ class ParserElement(object): return Or( [ self, other ] ) def __rxor__(self, other ): - """Implementation of ^ operator when left operand is not a C{ParserElement}""" + """Implementation of ^ operator when left operand is not a C{L{ParserElement}}""" if isinstance( other, basestring ): - other = Literal( other ) + other = ParserElement.literalStringClass( other ) if not isinstance( other, ParserElement ): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), SyntaxWarning, stacklevel=2) @@ -1285,9 +1295,9 @@ class ParserElement(object): return other ^ self def __and__(self, other ): - """Implementation of & operator - returns C{Each}""" + """Implementation of & operator - returns C{L{Each}}""" if isinstance( other, basestring ): - other = Literal( other ) + other = ParserElement.literalStringClass( other ) if not isinstance( other, ParserElement ): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), SyntaxWarning, stacklevel=2) @@ -1295,9 +1305,9 @@ class ParserElement(object): return Each( [ self, other ] ) def __rand__(self, other ): - """Implementation of & operator when left operand is not a C{ParserElement}""" + """Implementation of & operator when left operand is not a C{L{ParserElement}}""" if isinstance( other, basestring ): - other = Literal( other ) + other = ParserElement.literalStringClass( other ) if not isinstance( other, ParserElement ): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), SyntaxWarning, stacklevel=2) @@ -1305,11 +1315,11 @@ class ParserElement(object): return other & self def __invert__( self ): - """Implementation of ~ operator - returns C{NotAny}""" + """Implementation of ~ operator - returns C{L{NotAny}}""" return NotAny( self ) def __call__(self, name): - """Shortcut for C{setResultsName}, with C{listAllMatches=default}:: + """Shortcut for C{L{setResultsName}}, with C{listAllMatches=default}:: userdata = Word(alphas).setResultsName("name") + Word(nums+"-").setResultsName("socsecno") could be written as:: userdata = Word(alphas)("name") + Word(nums+"-")("socsecno") @@ -1403,25 +1413,17 @@ class ParserElement(object): try: file_contents = file_or_filename.read() except AttributeError: - f = open(file_or_filename, "rb") + f = open(file_or_filename, "r") file_contents = f.read() f.close() try: return self.parseString(file_contents, parseAll) - except ParseBaseException: - # catch and re-raise exception from here, clears out pyparsing internal stack trace - exc = sys.exc_info()[1] - raise exc - - def getException(self): - return ParseException("",0,self.errmsg,self) - - def __getattr__(self,aname): - if aname == "myException": - self.myException = ret = self.getException(); - return ret; - else: - raise AttributeError("no such attribute " + aname) + except ParseBaseException as exc: + if ParserElement.verbose_stacktrace: + raise + else: + # catch and re-raise exception from here, clears out pyparsing internal stack trace + raise exc def __eq__(self,other): if isinstance(other, ParserElement): @@ -1478,10 +1480,7 @@ class NoMatch(Token): self.errmsg = "Unmatchable token" def parseImpl( self, instring, loc, doActions=True ): - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) class Literal(Token): @@ -1509,16 +1508,13 @@ class Literal(Token): if (instring[loc] == self.firstMatchChar and (self.matchLen==1 or instring.startswith(self.match,loc)) ): return loc+self.matchLen, self.match - #~ raise ParseException( instring, loc, self.errmsg ) - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) _L = Literal +ParserElement.literalStringClass = Literal class Keyword(Token): """Token to exactly match a specified string as a keyword, that is, it must be - immediately followed by a non-keyword character. Compare with C{Literal}:: + immediately followed by a non-keyword character. Compare with C{L{Literal}}:: Literal("if") will match the leading C{'if'} in C{'ifAndOnlyIf'}. Keyword("if") will not; it will only match the leading C{'if'} in C{'if x=1'}, or C{'if(y==2)'} Accepts two optional constructor arguments in addition to the keyword string: @@ -1559,11 +1555,7 @@ class Keyword(Token): (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen] not in self.identChars) and (loc == 0 or instring[loc-1] not in self.identChars) ): return loc+self.matchLen, self.match - #~ raise ParseException( instring, loc, self.errmsg ) - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) def copy(self): c = super(Keyword,self).copy() @@ -1591,11 +1583,7 @@ class CaselessLiteral(Literal): def parseImpl( self, instring, loc, doActions=True ): if instring[ loc:loc+self.matchLen ].upper() == self.match: return loc+self.matchLen, self.returnString - #~ raise ParseException( instring, loc, self.errmsg ) - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) class CaselessKeyword(Keyword): def __init__( self, matchString, identChars=Keyword.DEFAULT_KEYWORD_CHARS ): @@ -1605,11 +1593,7 @@ class CaselessKeyword(Keyword): if ( (instring[ loc:loc+self.matchLen ].upper() == self.caselessmatch) and (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen].upper() not in self.identChars) ): return loc+self.matchLen, self.match - #~ raise ParseException( instring, loc, self.errmsg ) - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) class Word(Token): """Token for matching words composed of allowed character sets. @@ -1626,9 +1610,9 @@ class Word(Token): def __init__( self, initChars, bodyChars=None, min=1, max=0, exact=0, asKeyword=False, excludeChars=None ): super(Word,self).__init__() if excludeChars: - initChars = ''.join([c for c in initChars if c not in excludeChars]) + initChars = ''.join(c for c in initChars if c not in excludeChars) if bodyChars: - bodyChars = ''.join([c for c in bodyChars if c not in excludeChars]) + bodyChars = ''.join(c for c in bodyChars if c not in excludeChars) self.initCharsOrig = initChars self.initChars = set(initChars) if bodyChars : @@ -1681,20 +1665,14 @@ class Word(Token): if self.re: result = self.re.match(instring,loc) if not result: - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) loc = result.end() return loc, result.group() if not(instring[ loc ] in self.initChars): - #~ raise ParseException( instring, loc, self.errmsg ) - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) + start = loc loc += 1 instrlen = len(instring) @@ -1714,11 +1692,7 @@ class Word(Token): throwException = True if throwException: - #~ raise ParseException( instring, loc, self.errmsg ) - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) return loc, instring[start:loc] @@ -1787,10 +1761,7 @@ class Regex(Token): def parseImpl( self, instring, loc, doActions=True ): result = self.re.match(instring,loc) if not result: - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) loc = result.end() d = result.groupdict() @@ -1821,9 +1792,9 @@ class QuotedString(Token): - quoteChar - string of one or more characters defining the quote delimiting string - escChar - character to escape quotes, typically backslash (default=None) - escQuote - special quote sequence to escape an embedded quote string (such as SQL's "" to escape an embedded ") (default=None) - - multiline - boolean indicating whether quotes can span multiple lines (default=False) - - unquoteResults - boolean indicating whether the matched text should be unquoted (default=True) - - endQuoteChar - string of one or more characters defining the end of the quote delimited string (default=None => same as quoteChar) + - multiline - boolean indicating whether quotes can span multiple lines (default=C{False}) + - unquoteResults - boolean indicating whether the matched text should be unquoted (default=C{True}) + - endQuoteChar - string of one or more characters defining the end of the quote delimited string (default=C{None} => same as quoteChar) """ super(QuotedString,self).__init__() @@ -1864,9 +1835,9 @@ class QuotedString(Token): (escChar is not None and _escapeRegexRangeChars(escChar) or '') ) if len(self.endQuoteChar) > 1: self.pattern += ( - '|(?:' + ')|(?:'.join(["%s[^%s]" % (re.escape(self.endQuoteChar[:i]), + '|(?:' + ')|(?:'.join("%s[^%s]" % (re.escape(self.endQuoteChar[:i]), _escapeRegexRangeChars(self.endQuoteChar[i])) - for i in range(len(self.endQuoteChar)-1,0,-1)]) + ')' + for i in range(len(self.endQuoteChar)-1,0,-1)) + ')' ) if escQuote: self.pattern += (r'|(?:%s)' % re.escape(escQuote)) @@ -1892,10 +1863,7 @@ class QuotedString(Token): def parseImpl( self, instring, loc, doActions=True ): result = instring[loc] == self.firstQuoteChar and self.re.match(instring,loc) or None if not result: - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) loc = result.end() ret = result.group() @@ -1961,11 +1929,7 @@ class CharsNotIn(Token): def parseImpl( self, instring, loc, doActions=True ): if instring[loc] in self.notChars: - #~ raise ParseException( instring, loc, self.errmsg ) - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) start = loc loc += 1 @@ -1976,11 +1940,7 @@ class CharsNotIn(Token): loc += 1 if loc - start < self.minLen: - #~ raise ParseException( instring, loc, self.errmsg ) - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) return loc, instring[start:loc] @@ -2003,7 +1963,7 @@ class White(Token): by pyparsing grammars. This class is included when some whitespace structures are significant. Define with a string containing the whitespace characters to be matched; default is C{" \\t\\r\\n"}. Also takes optional C{min}, C{max}, and C{exact} arguments, - as defined for the C{Word} class.""" + as defined for the C{L{Word}} class.""" whiteStrs = { " " : "", "\t": "", @@ -2014,9 +1974,9 @@ class White(Token): def __init__(self, ws=" \t\r\n", min=1, max=0, exact=0): super(White,self).__init__() self.matchWhite = ws - self.setWhitespaceChars( "".join([c for c in self.whiteChars if c not in self.matchWhite]) ) + self.setWhitespaceChars( "".join(c for c in self.whiteChars if c not in self.matchWhite) ) #~ self.leaveWhitespace() - self.name = ("".join([White.whiteStrs[c] for c in self.matchWhite])) + self.name = ("".join(White.whiteStrs[c] for c in self.matchWhite)) self.mayReturnEmpty = True self.errmsg = "Expected " + self.name @@ -2033,11 +1993,7 @@ class White(Token): def parseImpl( self, instring, loc, doActions=True ): if not(instring[ loc ] in self.matchWhite): - #~ raise ParseException( instring, loc, self.errmsg ) - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) start = loc loc += 1 maxloc = start + self.maxLen @@ -2046,11 +2002,7 @@ class White(Token): loc += 1 if loc - start < self.minLen: - #~ raise ParseException( instring, loc, self.errmsg ) - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) return loc, instring[start:loc] @@ -2102,11 +2054,7 @@ class LineStart(_PositionToken): if not( loc==0 or (loc == self.preParse( instring, 0 )) or (instring[loc-1] == "\n") ): #col(loc, instring) != 1: - #~ raise ParseException( instring, loc, "Expected start of line" ) - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) return loc, [] class LineEnd(_PositionToken): @@ -2121,18 +2069,11 @@ class LineEnd(_PositionToken): if instring[loc] == "\n": return loc+1, "\n" else: - #~ raise ParseException( instring, loc, "Expected end of line" ) - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) elif loc == len(instring): return loc+1, [] else: - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) class StringStart(_PositionToken): """Matches if current position is at the beginning of the parse string""" @@ -2144,11 +2085,7 @@ class StringStart(_PositionToken): if loc != 0: # see if entire string up to here is just whitespace and ignoreables if loc != self.preParse( instring, 0 ): - #~ raise ParseException( instring, loc, "Expected start of text" ) - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) return loc, [] class StringEnd(_PositionToken): @@ -2159,20 +2096,13 @@ class StringEnd(_PositionToken): def parseImpl( self, instring, loc, doActions=True ): if loc < len(instring): - #~ raise ParseException( instring, loc, "Expected end of text" ) - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) elif loc == len(instring): return loc+1, [] elif loc > len(instring): return loc, [] else: - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) class WordStart(_PositionToken): """Matches if the current position is at the beginning of a Word, and @@ -2190,10 +2120,7 @@ class WordStart(_PositionToken): if loc != 0: if (instring[loc-1] in self.wordChars or instring[loc] not in self.wordChars): - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) return loc, [] class WordEnd(_PositionToken): @@ -2214,11 +2141,7 @@ class WordEnd(_PositionToken): if instrlen>0 and loc maxExcLoc: maxException = err maxExcLoc = err.loc @@ -2436,7 +2360,7 @@ class Or(ParseExpression): def __ixor__(self, other ): if isinstance( other, basestring ): - other = Literal( other ) + other = ParserElement.literalStringClass( other ) return self.append( other ) #Or( [ self, other ] ) def __str__( self ): @@ -2444,7 +2368,7 @@ class Or(ParseExpression): return self.name if self.strRepr is None: - self.strRepr = "{" + " ^ ".join( [ _ustr(e) for e in self.exprs ] ) + "}" + self.strRepr = "{" + " ^ ".join(_ustr(e) for e in self.exprs) + "}" return self.strRepr @@ -2477,7 +2401,7 @@ class MatchFirst(ParseExpression): try: ret = e._parse( instring, loc, doActions ) return ret - except ParseException, err: + except ParseException as err: if err.loc > maxExcLoc: maxException = err maxExcLoc = err.loc @@ -2495,7 +2419,7 @@ class MatchFirst(ParseExpression): def __ior__(self, other ): if isinstance( other, basestring ): - other = Literal( other ) + other = ParserElement.literalStringClass( other ) return self.append( other ) #MatchFirst( [ self, other ] ) def __str__( self ): @@ -2503,7 +2427,7 @@ class MatchFirst(ParseExpression): return self.name if self.strRepr is None: - self.strRepr = "{" + " | ".join( [ _ustr(e) for e in self.exprs ] ) + "}" + self.strRepr = "{" + " | ".join(_ustr(e) for e in self.exprs) + "}" return self.strRepr @@ -2562,7 +2486,7 @@ class Each(ParseExpression): keepMatching = False if tmpReqd: - missing = ", ".join( [ _ustr(e) for e in tmpReqd ] ) + missing = ", ".join(_ustr(e) for e in tmpReqd) raise ParseException(instring,loc,"Missing one or more required elements (%s)" % missing ) # add any unmatched Optionals, in case they have default values defined @@ -2591,7 +2515,7 @@ class Each(ParseExpression): return self.name if self.strRepr is None: - self.strRepr = "{" + " & ".join( [ _ustr(e) for e in self.exprs ] ) + "}" + self.strRepr = "{" + " & ".join(_ustr(e) for e in self.exprs) + "}" return self.strRepr @@ -2706,11 +2630,7 @@ class NotAny(ParseElementEnhance): except (ParseException,IndexError): pass else: - #~ raise ParseException(instring, loc, self.errmsg ) - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) return loc, [] def __str__( self ): @@ -2878,7 +2798,7 @@ class SkipTo(ParseElementEnhance): while 1: try: loc = self.ignoreExpr.tryParse(instring,loc) - # print "found ignoreExpr, advance to", loc + # print("found ignoreExpr, advance to", loc) except ParseBaseException: break expr._parse( instring, loc, doActions=False, callPreParse=False ) @@ -2898,10 +2818,7 @@ class SkipTo(ParseElementEnhance): raise else: loc += 1 - exc = self.myException - exc.loc = loc - exc.pstr = instring - raise exc + raise ParseException(instring, loc, self.errmsg, self) class Forward(ParseElementEnhance): """Forward declaration of an expression to be defined later - @@ -2916,13 +2833,14 @@ class Forward(ParseElementEnhance): thereby leaving b and c out as parseable alternatives. It is recommended that you explicitly group the values inserted into the C{Forward}:: fwdExpr << (a | b | c) + Converting to use the '<<=' operator instead will avoid this problem. """ def __init__( self, other=None ): super(Forward,self).__init__( other, savelist=False ) - def __lshift__( self, other ): + def __ilshift__( self, other ): if isinstance( other, basestring ): - other = Literal(other) + other = ParserElement.literalStringClass(other) self.expr = other self.mayReturnEmpty = other.mayReturnEmpty self.strRepr = None @@ -2932,8 +2850,14 @@ class Forward(ParseElementEnhance): self.skipWhitespace = self.expr.skipWhitespace self.saveAsList = self.expr.saveAsList self.ignoreExprs.extend(self.expr.ignoreExprs) + return self + + def __lshift__(self, other): + warnings.warn("Operator '<<' is deprecated, use '<<=' instead", + DeprecationWarning,stacklevel=2) + self <<= other return None - + def leaveWhitespace( self ): self.skipWhitespace = False return self @@ -2993,7 +2917,7 @@ class Upcase(TokenConverter): DeprecationWarning,stacklevel=2) def postParse( self, instring, loc, tokenlist ): - return list(map( string.upper, tokenlist )) + return list(map( str.upper, tokenlist )) class Combine(TokenConverter): @@ -3029,7 +2953,7 @@ class Combine(TokenConverter): return retToks class Group(TokenConverter): - """Converter to return the matched tokens as a list - useful for returning tokens of C{ZeroOrMore} and C{OneOrMore} expressions.""" + """Converter to return the matched tokens as a list - useful for returning tokens of C{L{ZeroOrMore}} and C{L{OneOrMore}} expressions.""" def __init__( self, expr ): super(Group,self).__init__( expr ) self.saveAsList = True @@ -3105,8 +3029,7 @@ def traceParseAction(f): sys.stderr.write( ">>entering %s(line: '%s', %d, %s)\n" % (thisFunc,line(l,s),l,t) ) try: ret = f(*paArgs) - except Exception: - exc = sys.exc_info()[1] + except Exception as exc: sys.stderr.write( "<", "|".join( [ _escapeRegexChars(sym) for sym in symbols] )) try: if len(symbols)==len("".join(symbols)): - return Regex( "[%s]" % "".join( [ _escapeRegexRangeChars(sym) for sym in symbols] ) ) + return Regex( "[%s]" % "".join(_escapeRegexRangeChars(sym) for sym in symbols) ) else: - return Regex( "|".join( [ re.escape(sym) for sym in symbols] ) ) + return Regex( "|".join(re.escape(sym) for sym in symbols) ) except: warnings.warn("Exception creating Regex for oneOf, building MatchFirst", SyntaxWarning, stacklevel=2) @@ -3284,7 +3207,7 @@ def oneOf( strs, caseless=False, useRegex=True ): def dictOf( key, value ): """Helper to easily and clearly define a dictionary by specifying the respective patterns - for the key and value. Takes care of defining the C{Dict}, C{ZeroOrMore}, and C{Group} tokens + for the key and value. Takes care of defining the C{L{Dict}}, C{L{ZeroOrMore}}, and C{L{Group}} tokens in the proper order. The key pattern can include delimiting markers or punctuation, as long as they are suppressed, thereby leaving the significant key text. The value pattern can include named results, so that the C{Dict} results can include named token @@ -3301,7 +3224,7 @@ def originalTextFor(expr, asString=True): string containing the original parsed text. If the optional C{asString} argument is passed as C{False}, then the return value is a - C{ParseResults} containing any results names that were originally matched, and a + C{L{ParseResults}} containing any results names that were originally matched, and a single token containing the original matched text from the input string. So if the expression passed to C{L{originalTextFor}} contains expressions with defined results names, you must set C{asString} to C{False} if you want to preserve those @@ -3334,14 +3257,13 @@ stringStart = StringStart().setName("stringStart") stringEnd = StringEnd().setName("stringEnd") _escapedPunc = Word( _bslash, r"\[]-*.$+^?()~ ", exact=2 ).setParseAction(lambda s,l,t:t[0][1]) -_printables_less_backslash = "".join([ c for c in printables if c not in r"\]" ]) -_escapedHexChar = Regex(r"\\0?[xX][0-9a-fA-F]+").setParseAction(lambda s,l,t:unichr(int(t[0][1:],16))) +_escapedHexChar = Regex(r"\\0?[xX][0-9a-fA-F]+").setParseAction(lambda s,l,t:unichr(int(t[0].lstrip(r'\0x'),16))) _escapedOctChar = Regex(r"\\0[0-7]+").setParseAction(lambda s,l,t:unichr(int(t[0][1:],8))) -_singleChar = _escapedPunc | _escapedHexChar | _escapedOctChar | Word(_printables_less_backslash,exact=1) +_singleChar = _escapedPunc | _escapedHexChar | _escapedOctChar | Word(printables, excludeChars=r'\]', exact=1) _charRange = Group(_singleChar + Suppress("-") + _singleChar) _reBracketExpr = Literal("[") + Optional("^").setResultsName("negate") + Group( OneOrMore( _charRange | _singleChar ) ).setResultsName("body") + "]" -_expanded = lambda p: (isinstance(p,ParseResults) and ''.join([ unichr(c) for c in range(ord(p[0]),ord(p[1])+1) ]) or p) +_expanded = lambda p: (isinstance(p,ParseResults) and ''.join(unichr(c) for c in range(ord(p[0]),ord(p[1])+1)) or p) def srange(s): r"""Helper to easily define string ranges for use in Word construction. Borrows @@ -3361,7 +3283,7 @@ def srange(s): any combination of the above ('aeiouy', 'a-zA-Z0-9_$', etc.) """ try: - return "".join([_expanded(part) for part in _reBracketExpr.parseString(s).body]) + return "".join(_expanded(part) for part in _reBracketExpr.parseString(s).body) except: return "" @@ -3376,7 +3298,7 @@ def matchOnlyAtCol(n): def replaceWith(replStr): """Helper method for common parse actions that simply return a literal value. Especially - useful when used with C{transformString()}. + useful when used with C{L{transformString}()}. """ def _replFunc(*args): return [replStr] @@ -3398,7 +3320,7 @@ def downcaseTokens(s,l,t): return [ tt.lower() for tt in map(_ustr,t) ] def keepOriginalText(s,startLoc,t): - """DEPRECATED - use new helper method C{originalTextFor}. + """DEPRECATED - use new helper method C{L{originalTextFor}}. Helper parse action to preserve original parsed text, overriding any nested parse actions.""" try: @@ -3440,7 +3362,7 @@ def _makeTags(tagStr, xml): Dict(ZeroOrMore(Group( tagAttrName + Suppress("=") + tagAttrValue ))) + \ Optional("/",default=[False]).setResultsName("empty").setParseAction(lambda s,l,t:t[0]=='/') + Suppress(">") else: - printablesLessRAbrack = "".join( [ c for c in printables if c not in ">" ] ) + printablesLessRAbrack = "".join(c for c in printables if c not in ">") tagAttrValue = quotedString.copy().setParseAction( removeQuotes ) | Word(printablesLessRAbrack) openTag = Suppress("<") + tagStr("tag") + \ Dict(ZeroOrMore(Group( tagAttrName.setParseAction(downcaseTokens) + \ @@ -3464,7 +3386,7 @@ def makeXMLTags(tagStr): def withAttribute(*args,**attrDict): """Helper to create a validating parse action to be used with start tags created - with C{makeXMLTags} or C{makeHTMLTags}. Use C{withAttribute} to qualify a starting tag + with C{L{makeXMLTags}} or C{L{makeHTMLTags}}. Use C{withAttribute} to qualify a starting tag with a required attribute value, to avoid false matches on common tags such as C{} or C{
}. @@ -3499,7 +3421,7 @@ opAssoc = _Constants() opAssoc.LEFT = object() opAssoc.RIGHT = object() -def operatorPrecedence( baseExpr, opList ): +def infixNotation( baseExpr, opList, lpar=Suppress('('), rpar=Suppress(')') ): """Helper method for constructing grammars of expressions made up of operators working in a precedence hierarchy. Operators may be unary or binary, left- or right-associative. Parse actions can also be attached @@ -3518,13 +3440,15 @@ def operatorPrecedence( baseExpr, opList ): be 1, 2, or 3) - rightLeftAssoc is the indicator whether the operator is right or left associative, using the pyparsing-defined - constants opAssoc.RIGHT and opAssoc.LEFT. + constants C{opAssoc.RIGHT} and C{opAssoc.LEFT}. - parseAction is the parse action to be associated with expressions matching this operator expression (the parse action tuple member may be omitted) + - lpar - expression for matching left-parentheses (default=Suppress('(')) + - rpar - expression for matching right-parentheses (default=Suppress(')')) """ ret = Forward() - lastExpr = baseExpr | ( Suppress('(') + ret + Suppress(')') ) + lastExpr = baseExpr | ( lpar + ret + rpar ) for i,operDef in enumerate(opList): opExpr,arity,rightLeftAssoc,pa = (operDef + (None,))[:4] if arity == 3: @@ -3569,6 +3493,7 @@ def operatorPrecedence( baseExpr, opList ): lastExpr = thisExpr ret << lastExpr return ret +operatorPrecedence = infixNotation dblQuotedString = Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\x[0-9a-fA-F]+)|(?:\\.))*"').setName("string enclosed in double quotes") sglQuotedString = Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\x[0-9a-fA-F]+)|(?:\\.))*'").setName("string enclosed in single quotes") @@ -3697,8 +3622,7 @@ cppStyleComment = Regex(r"/(?:\*(?:[^*]*\*+)+?/|/[^\n]*(?:\n[^\n]*)*?(?:(?") print (err.line) print (" "*(err.column-1) + "^")