]> git.donarmstrong.com Git - lilypond.git/blobdiff - scripts/abc2ly.py
release: 1.2.6
[lilypond.git] / scripts / abc2ly.py
index dd4bcf263766dec2bdb03eb79e49c928b403006f..7b075e727e637b8fd2ca0ee8418dde05377a8dbb 100644 (file)
@@ -8,25 +8,38 @@
 # ABC standard v1.6:  http://www.gre.ac.uk/~c.walshaw/abc2mtex/abc.txt
 # 
 
-program_name = 'abc-to-ly'
+
+program_name = 'abc2ly'
 version = '@TOPLEVEL_VERSION@'
+if version == '@' + 'TOPLEVEL_VERSION' + '@':
+       version = '1.2.6'               # uGUHGUHGHGUGH
+
 import __main__
 import getopt
 import sys
 import re
 import string
-try:
-       import mpz
-except:
-       sys.stderr.write ("This script needs Python 1.5.1\n")
-       sys.exit (1)
+import os
+
 
 
+voice_idx_dict = {}
 header = {}
 lyrics = []
 voices = []
-global_voice_stuff = []
-default_len = 4
+current_voice_idx = -1
+current_lyric_idx = -1
+
+def select_voice (name):
+       if not voice_idx_dict.has_key (name):
+               voices.append ('')              
+               voice_idx_dict[name] = len (voices) -1
+       __main__.current_voice_idx =  voice_idx_dict[name]
+       
+#      assert 0
+# current_voice_idx >= 0
+
+default_len = 8
 global_key = [0] * 7                   # UGH
 names = ["One", "Two", "Three"]
 DIGITS='0123456789'
@@ -54,54 +67,66 @@ class Rational:
                pass
        
 
-def dump_global ():
-       print ("global = \\notes{")
-       for i in global_voice_stuff:
-               print (i);
-       print ("}")
 
+def dump_header (outf,hdr):
+       outf.write ('\\header {')
+       ks = hdr.keys ()
+       ks.sort ()
+       for k in ks:
+               outf.write ('\n%s = "%s";\n'% (k,hdr[k]))
+       outf.write ('}')
 
-def dump_header (hdr):
-       print '\\header {'
-       for k in hdr.keys ():
-               print '%s = "%s";\n'% (k,hdr[k])
-       print '}'
-
-def dump_lyrics ():
+def dump_lyrics (outf):
        for i in range (len (lyrics)):
-               print ("verse%s = \\lyrics {" % names [i])
-               print (lyrics [i])
-               print ("}")
-
-def dump_voices ():
-       for i in range (len (voices)):
-               print ("voice%s = \\notes {" % names [i])
-               print (voices [i])
-               print ("}")
+               outf.write ("\nverse%s = \\lyrics {" % names [i])
+               outf.write ("\n" + lyrics [i])
+               outf.write ("\n}")
+
+def dump_voices (outf):
+       ks = voice_idx_dict.keys()
+       ks.sort ()
+       for k in ks:
+               outf.write ("\nvoice%s = \\notes {" % k)
+               outf.write ("\n" + voices [voice_idx_dict[k]])
+               outf.write ("\n}")
        
-def dump_score ():
-       print ("\\score{")
-       print ("        \\notes<")
-       print ("                \\global")
-       for i in range (len (voices)):
-               print ("        \\context Staff=%s \\voice%s" %
-                       (names [i], names [i]))
+def dump_score (outf):
+       outf.write (r"""\score{
+        \notes <
+""")
+
+       ks  = voice_idx_dict.keys ();
+       ks.sort ()
+       for k in  ks:
+               outf.write ("\n        \\context Staff=\"%s\" \\$voice%s " % (k,k))# ugh
        for i in range (len (lyrics)):
                j = i
                if j >= len (voices):
                        j = len (voices) - 1
-               print ("        \\context Lyrics=%s \\rhythm \\voice%s \\verse%s" % 
+               outf.write ("\n        \\context Lyrics=\"%s\" \\addlyrics \\$voice%s \\$verse%s " % 
                        (names [i], names [j], names [i]))
-       print ("    >")
-       dump_header (header)
-       #print "%%%s" % global_voice_stuff, 1
-       print ("}")
+       outf.write ("\n    >")
+       dump_header (outf ,header)
+       outf.write (r"""
+\paper {}
+\midi {}
+}""")
 
 def set_default_length (s):
        m =  re.search ('1/([0-9]+)', s)
        if m:
                __main__.default_len = string.atoi ( m.group (1))
 
+def set_default_len_from_time_sig (s):
+       m =  re.search ('([0-9]+)/([0-9]+)', s)
+       if m:
+               n = string.atoi (m.group (1))
+               d = string.atoi (m.group (2))
+               if (n * 1.0 )/(d * 1.0) <  0.75:
+                       default_len =  16
+               else:
+                       default_len = 8
+
 def gulp_file(f):
        try:
                i = open(f)
@@ -217,17 +242,20 @@ def compute_key (k):
        return key_table
 
 tup_lookup = {
+       '2' : '3/2',
        '3' : '2/3',
        '4' : '4/3',
        '5' : '4/5',
        '6' : '4/6',
+       '7' : '6/7',
+       '9' : '8/9',
        }
 
 
 def try_parse_tuplet_begin (str, state):
-       if str and str[0] in DIGITS:
-               dig = str[0]
-               str = str[1:]
+       if re.match ('\([0-9]', str):
+               dig = str[1]
+               str = str[2:]
                state.parsing_tuplet = 1
                
                voices_append ("\\times %s {" % tup_lookup[dig])
@@ -247,21 +275,30 @@ def header_append (key, a):
                s = header[key] + "\n"
        header [key] = s + a
 
-def lyrics_append (a):
-       i = len (lyrics) - 1
-       if i < 0:
-               i = 0
-       if len (lyrics) <= i:
-               lyrics.append ('')
-       lyrics [i] = lyrics [i] + a + "\n"
-
-def voices_append (a):
-       i = len (voices) - 1
-       if i < 0:
-               i = 0
-       if len (voices) <= i:
-               voices.append ('')
-       voices [i] = voices [i] + a + "\n"
+def stuff_append (stuff, idx, a):
+       if not stuff:
+               stuff.append ('')
+
+       v = stuff[idx]
+
+       #wordwrap
+       linelen = len (v) - string.rfind(v, '\n')
+       if linelen + len (a) > 80:
+               v = v + '\n'
+       v = v + a + ' '
+       stuff [idx] = v
+
+
+
+def voices_append(a):
+       if current_voice_idx < 0:
+               select_voice ('default')
+
+       stuff_append (voices, current_voice_idx, a)
+
+def lyrics_append(a):
+       stuff_append (lyrics, current_lyric_idx, a)
+
 
 def try_parse_header_line (ln):
        m = re.match ('^(.): *(.*)$', ln)
@@ -275,11 +312,13 @@ def try_parse_header_line (ln):
                if g == 'M':
                        if a == 'C':
                                a = '4/4'
-                       global_voice_stuff.append ('\\time %s;' % a)
+#                      global_voice_stuff.append ('\\time %s;' % a)
+                       set_default_len_from_time_sig (a)
+                       voices_append ('\\time %s;' % a)
                if g == 'K':
                        __main__.global_key  =compute_key (a)# ugh.
+                       voices_append ('\\key %s;' % a)
 
-                       global_voice_stuff.append ('\\key %s;' % a)
                if g == 'O': 
                        header ['origin'] = a
                if g == 'X': 
@@ -290,16 +329,23 @@ def try_parse_header_line (ln):
                        header_append ('history', a)
                if g == 'B':
                        header ['book'] = a
+               if g == 'C':
+                       header ['composer'] = a
                if g == 'S':
                        header ['subtitle'] = a
                if g == 'L':
                        set_default_length (ln)
+               if g == 'V':
+                       a = re.sub (' .*$', '', a)
+                       select_voice (a)
                if g == 'W':
                        if not len (a):
                                lyrics.append ('')
                        else:
                                lyrics_append (a);
-       return m
+
+               return ''
+       return ln
 
 def pitch_to_mudela_name (name, acc):
        s = ''
@@ -340,7 +386,7 @@ def duration_to_mudela_duration  (multiply_tup, defaultlen, dots):
        base = 1
 
        # (num /  den)  / defaultlen < 1/base
-       while base * multiply_tup[0] < defaultlen * multiply_tup[1]:
+       while base * multiply_tup[0] < multiply_tup[1]:
                base = base * 2
 
 
@@ -348,11 +394,101 @@ def duration_to_mudela_duration  (multiply_tup, defaultlen, dots):
 
 class Parser_state:
        def __init__ (self):
+               self.next_articulation = ''
                self.next_dots = 0
                self.next_den = 1
                self.parsing_tuplet = 0
 
 
+
+# return (str, num,den,dots) 
+def parse_duration (str, parser_state):
+       num = 0
+       den = parser_state.next_den
+       parser_state.next_den = 1
+
+       (str, num) = parse_num (str)
+       if not num:
+               num = 1
+       
+       if str[0] == '/':
+               while str[:1] == '/':
+                       str= str[1:]
+                       d = 2
+                       if str[0] in DIGITS:
+                               (str, d) =parse_num (str)
+
+                       den = den * d
+
+       den = den * default_len
+       
+       current_dots = parser_state.next_dots
+       parser_state.next_dots = 0
+
+       while str[0] == ' ':
+           str = str [1:]
+       
+       while str[0] == '>':
+               str = str [1:]
+               current_dots = current_dots + 1;
+               parser_state.next_den = parser_state.next_den * 2
+
+       while str[0] == '<':
+               str = str [1:]
+               den = den * 2
+               parser_state.next_dots = parser_state.next_dots + 1
+
+
+
+       try_dots = [3, 2, 1]
+       for d in try_dots:
+               f = 1 << d
+               multiplier = (2*f-1)
+               if num % multiplier == 0 and den % f == 0:
+                       num = num / multiplier
+                       den = den / f
+                       current_dots = current_dots + d
+               
+       return (str, num,den,current_dots)
+
+
+def try_parse_rest (str, parser_state):
+       if not str or str[0] <> 'z':
+               return str
+
+       str = str[1:]
+
+       (str, num,den,d) = parse_duration (str, parser_state)
+       voices_append ('r%s' % duration_to_mudela_duration ((num,den), default_len, d))
+
+       return str
+
+def try_parse_articulation (str, state):
+       
+       if str[:1] =='.':
+               state.next_articulation = state.next_articulation + '-.'
+               str = str[1:]
+
+       if str[:1] =='~':
+               state.next_articulation = state.next_articulation + '-\\trill'
+               str = str[1:]
+               
+       if str[:1] =='H':
+               state.next_articulation = state.next_articulation + '-\\fermata'
+               str = str[1:]
+
+       # s7m2 input doesnt care about spaces
+       if re.match('[ \t]*\(', str):
+               str = string.lstrip (str)
+
+       slur_begin =0
+       while str[:1] =='(' and str[1] not in DIGITS:
+               slur_begin = slur_begin + 1
+               state.next_articulation = state.next_articulation + '('
+               str = str[1:]
+
+       return str
+               
 # WAT IS ABC EEN ONTZETTENDE PROGRAMMEERPOEP  !
 def try_parse_note (str, parser_state):
        mud = ''
@@ -360,11 +496,8 @@ def try_parse_note (str, parser_state):
        slur_begin =0
        if not str:
                return str
-       
-       if  str[0] == '(':
-               slur_begin = 1
-               str = str[1:]
 
+       articulation =''
        acc = 0
        if str[0] in '^=_':
                c = str[0]
@@ -396,45 +529,31 @@ def try_parse_note (str, parser_state):
                 octave = octave + 1
                 str = str[1:]
 
-       num = 0
-       den = parser_state.next_den
-       parser_state.next_den = 1
-
-       (str, num) = parse_num (str)
-       if not num:
-               num = 1
-       
-       if str[0] == '/':
-               while str[0] == '/':
-                       str= str[1:]
-                       d = 2
-                       if str[0] in DIGITS:
-                               (str, d) =parse_num (str)
+       (str, num,den,current_dots) = parse_duration (str, parser_state)
 
-                       den = den * d
 
-       current_dots = parser_state.next_dots
-       parser_state.next_dots = 0
-       while str[0] == '>':
-               str = str [1:]
-               current_dots = current_dots + 1;
-               parser_state.next_den = parser_state.next_den * 2
+       if re.match('[ \t]*\)', str):
+               str = string.lstrip (str)
        
-       while str[0] == '<':
-               str = str [1:]
-               den = den * 2
-               parser_state.next_dots = parser_state.next_dots + 1
-       
-               
+       slur_end =0
+       while str[:1] ==')':
+               slur_end = slur_end + 1
+               str = str[1:]
+
        
+       if slur_end:
+               voices_append ('%s' % ')' *slur_end )
        voices_append ("%s%s%s" %
                (pitch_to_mudela_name (notename, acc + global_key[notename]),
                                        octave_to_mudela_quotes (octave),
                 duration_to_mudela_duration ((num,den), default_len, current_dots)))
-       slur_end =0
-       if str[0] == ')':
-               slur_begin = 1
-               str = str[1:]
+       if parser_state.next_articulation:
+               articulation = articulation + parser_state.next_articulation
+               parser_state.next_articulation = ''
+
+       voices_append (articulation)
+       if slur_begin:
+               voices_append ('%s' % '(' * slur_begin )
 
 
        return str
@@ -446,8 +565,8 @@ def junk_space (str):
        return str
 
 
-def try_parse_guitar_chord (str):
-       if str and str[0] == '"':
+def try_parse_guitar_chord (str, state):
+       if str[:1] =='"':
                str = str[1:]
                gc = ''
                while str and str[0] != '"':
@@ -457,8 +576,7 @@ def try_parse_guitar_chord (str):
                if str:
                        str = str[1:]
 
-               sys.stderr.write ("warning: ignoring guitar chord: %s\n" % gc)
-               
+               state.next_articulation = "-\"%s\"" % gc
        return str
 
 def try_parse_escape (str):
@@ -466,7 +584,7 @@ def try_parse_escape (str):
                return str
        
        str = str[1:]
-       if str and str[0] == 'K':
+       if str[:1] =='K':
                key_table = compute_key ()
 
        return str
@@ -478,102 +596,137 @@ def try_parse_escape (str):
 # :| left repeat
 # |: right repeat
 # :: left-right repeat
-#
+# |1 volta 1
+# |2 volta 2
+bar_dict = {
+'|]' : '|.',
+'||' : '||',
+'[|' : '||',
+':|' : ':|',
+'|:' : '|:',
+'::' : '::',
+'|1' : '|',
+'|2' : '|',
+':|2' : ':|'
+}
+
+
+warn_about = ['|:', '::', ':|', '|1', ':|2', '|2']
+
+def try_parse_bar (str,state):
+       bs = None
+
+       # first try the longer one
+       for trylen in [3,2]:
+               if str[:trylen] and bar_dict.has_key (str[:trylen]):
+                       s = str[:trylen]
+                       bs = "\\bar \"%s\";" % bar_dict[s]
+                       if s in warn_about:
+                               sys.stderr.write('Warning kludging for barline `%s\'\n' % s)
+                       str = str[trylen:]
+                       break
 
-def try_parse_bar (str):
-       if str and str[0] == '|':
-               bs = ''
+       if str[:1] == '|':
+               bs = '|\n'
                str = str[1:]
-               if str:
-                       if  str[0] == ']':
-                               bs = '|.'
-                       if str[0] == '|':
-                               bs = '||'
-                       if str[0] == '|:':
-                               sys.stderr.write ("warning: repeat kludge\n")
-                               bs = '|:'
-               if bs:
-                       voices_append ('\\bar "%s";' % bs)
-                       str = str[1:]
-
-       if str and str[:2] == '[|':
-               sys.stderr.write ("warning: thick-thin bar kludge\n")
-               voices_append ('\\bar "||";')
-               str = str[2:]
-
-       if str and str[:2] == ':|':
-               sys.stderr.write ("warning: repeat kludge\n")
-               voices_append ('\\bar ":|:";')
-               str = str[2:]
+       
+       if bs <> None:
+               if state.parsing_tuplet:
+                       state.parsing_tuplet =0
+                       voices_append ('} ')
+               
+               voices_append (bs)
 
-       if str and str[:2] == '::':
-               sys.stderr.write ("warning: repeat kludge\n")
-               voices_append ('\\bar ":|:";')
-               str = str[2:]
+       return str
 
+def try_parse_tie (str):
+       if str[:1] =='-':
+               str = str[1:]
+               voices_append (' ~ ')
        return str
-       
 
 def try_parse_chord_delims (str):
-       if str and str[0] == '[':
+       if str[:1] =='[':
                str = str[1:]
                voices_append ('<')
 
-       if str and str[0] == ']':
+       ch = ''
+       if str[:1] ==']':
+               str = str[1:]
+               ch = '>'
+
+       end = 0
+       while str[:1] ==')':
+               end = end + 1
                str = str[1:]
-               voices_append ('>')
 
+       
+       voices_append ("\\spanrequest \\stop \"slur\"" * end);
+       voices_append (ch)
        return str
 
-# urg, hairy to compute grace note hack using \times{}
 def try_parse_grace_delims (str):
-       if str and str[0] == '{':
+       if str[:1] =='{':
                str = str[1:]
                voices_append ('\\grace { ')
 
-       if str and str[0] == '}':
+       if str[:1] =='}':
                str = str[1:]
                voices_append ('}')
 
        return str
 
-# Try nibbling characters off until the line doesn't change.
-def try_parse_body_line (ln, state):
-       prev_ln = ''
-       while ln != prev_ln:
-               prev_ln = ln
-               ln = try_parse_chord_delims (ln)
-               ln = try_parse_note  (ln, state)
-               ln = try_parse_bar (ln)
-               ln = try_parse_escape (ln)
-               ln = try_parse_guitar_chord (ln)
-               ln = try_parse_tuplet_begin (ln, state)
-               ln = try_parse_group_end (ln, state)
-               ln = try_parse_grace_delims (ln)
-               ln = junk_space (ln)
-               
-       if ln:
-               sys.stderr.write ("Huh?  Don't understand `%s'\n" % ln)
-       
-
 
+happy_count = 100
 def parse_file (fn):
        f = open (fn)
        ls = f.readlines ()
 
-       head = 1
        state = Parser_state ()
-       for l in ls:
-               if re.match ('^[\t ]*(%.*)?$', l):
+       lineno = 0
+       sys.stderr.write ("Line ... ")
+       sys.stderr.flush ()
+       
+       for ln in ls:
+               lineno = lineno + 1
+
+               if not (lineno % happy_count):
+                       sys.stderr.write ('[%d]'% lineno)
+                       sys.stderr.flush ()
+               if re.match ('^[\t ]*(%.*)?$', ln):
                        continue
-               
-               if head:
-                       m = try_parse_header_line (l)
-                       if not m:
-                               head = 0
+               m = re.match  ('^(.*?)%(.*)$',ln)
+               if m:
+                       voices_append ('%% %s\n' % m.group(2))
+                       ln = m.group (1)
 
-               if not head:
-                       m = try_parse_body_line (l,state)
+               orig_ln = ln
+               
+               ln = try_parse_header_line (ln)
+
+               # Try nibbling characters off until the line doesn't change.
+               prev_ln = ''
+               while ln != prev_ln:
+                       prev_ln = ln
+                       ln = try_parse_chord_delims (ln)
+                       ln = try_parse_rest (ln, state)
+                       ln = try_parse_articulation (ln,state)
+                       ln = try_parse_note  (ln, state)
+                       ln = try_parse_bar (ln, state)
+                       ln = try_parse_tie (ln)
+                       ln = try_parse_escape (ln)
+                       ln = try_parse_guitar_chord (ln, state)
+                       ln = try_parse_tuplet_begin (ln, state)
+                       ln = try_parse_group_end (ln, state)
+                       ln = try_parse_grace_delims (ln)
+                       ln = junk_space (ln)
+
+               if ln:
+                       msg = "%s: %d: Huh?  Don't understand\n" % (fn, lineno)
+                       sys.stderr.write (msg)
+                       left = orig_ln[0:-len (ln)]
+                       sys.stderr.write (left + '\n')
+                       sys.stderr.write (' ' *  len (left) + ln + '\n')        
 
 
 def identify():
@@ -581,37 +734,58 @@ def identify():
 
 def help ():
        print r"""
-This is a disfunctional ABC to mudela convertor.  It only gulps input, and
-says huh when confused.  Go ahead and fix me.
+Convert ABC to Mudela.
 
-Usage: abc-2-ly INPUTFILE
+Usage: abc2ly [OPTION]... ABC-FILE
 
--h, --help   this help.
+Options:
+  -h, --help          this help
+  -o, --output=FILE   set output filename to FILE
+  -v, --version       version information
 """
 
+def print_version ():
+       print r"""abc2ly (GNU lilypond) %s""" % version
 
 
-identify()
-(options, files) = getopt.getopt (sys.argv[1:], 'h', ['help'])
+
+(options, files) = getopt.getopt (sys.argv[1:], 'vo:h', ['help','version', 'output='])
+out_filename = ''
 
 for opt in options:
        o = opt[0]
        a = opt[1]
        if o== '--help' or o == '-h':
                help ()
+               sys.exit (0)
+       if o == '--version' or o == '-v':
+               print_version ()
+               sys.exit(0)
+               
+       if o == '--output' or o == '-o':
+               out_filename = a
        else:
                print o
                raise getopt.error
 
+identify()
 
+header['tagline'] = 'Lily was here %s -- automatically converted from ABC' % version
 for f in files:
        if f == '-':
                f = ''
+
+       sys.stderr.write ('Parsing... [%s]\n' % f)
        parse_file (f)
 
-       dump_global ()
-       dump_lyrics ()
-       dump_voices ()
-       dump_score ()
-       
+       if not out_filename:
+               out_filename = os.path.basename (os.path.splitext (f)[0]) + ".ly"
+       sys.stderr.write ('Ly output to: %s...' % out_filename)
+       outf = open (out_filename, 'w')
+
+#      dump_global (outf)
+       dump_lyrics (outf)
+       dump_voices (outf)
+       dump_score (outf)
+       sys.stderr.write ('\n')