X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=scripts%2Fabc2ly.py;h=e9f842cc23d5e8077343553a7722df0b9fac454c;hb=6d27cf98eed19d753bde354104766a507bb75703;hp=af8cbf877c8b810b10e0b4813c2416e22859dfc9;hpb=736bfdaea194aade5d20d9f749f009c96d41b953;p=lilypond.git diff --git a/scripts/abc2ly.py b/scripts/abc2ly.py index af8cbf877c..e9f842cc23 100644 --- a/scripts/abc2ly.py +++ b/scripts/abc2ly.py @@ -1,5 +1,5 @@ #!@PYTHON@ - +# -*- coding: utf-8 -*- # once upon a rainy monday afternoon. # # ... @@ -34,6 +34,13 @@ # \breve and \longa supported. # M:none doesn't crash lily. +# Enhancements (Guy Gascoigne-Piggford) +# +# Add support for maintaining ABC's notion of beaming, this is selectable +# from the command line with a -b or --beam option. +# Fixd a problem where on cygwin empty lines weren't being correctly identifed +# and so were complaining, but still generating the correct output. + # Limitations # # Multiple tunes in single file not supported @@ -49,15 +56,17 @@ #TODO: +# +# * lilylib +# * GNU style messages: warning:FILE:LINE: +# * l10n +# +# Convert to new chord styles. +# # UNDEF -> None - - - -program_name = 'abc2ly' -version = '@TOPLEVEL_VERSION@' -if version == '@' + 'TOPLEVEL_VERSION' + '@': - version = '(unknown version)' # uGUHGUHGHGUGH +# + import __main__ import getopt import sys @@ -65,10 +74,38 @@ import re import string import os +program_name = sys.argv[0] + + +datadir = '@local_lilypond_datadir@' +if not os.path.isdir (datadir): + datadir = '@lilypond_datadir@' + +sys.path.insert (0, os.path.join (datadir, 'python')) + +if os.environ.has_key ('LILYPONDPREFIX'): + datadir = os.environ['LILYPONDPREFIX'] + while datadir[-1] == os.sep: + datadir= datadir[:-1] + + datadir = os.path.join (datadir, "share/lilypond/current/") +sys.path.insert (0, os.path.join (datadir, 'python')) + +# dynamic relocation, for GUB binaries. +bindir = os.path.split (sys.argv[0])[0] +for p in ['share', 'lib']: + datadir = os.path.abspath (bindir + '/../%s/lilypond/current/python/' % p) + sys.path.insert (0, os.path.join (datadir)) + +import lilylib as ly +global _;_=ly._ + +version = '@TOPLEVEL_VERSION@' +if version == '@' + 'TOPLEVEL_VERSION' + '@': + version = '(unknown version)' # uGUHGUHGHGUGH UNDEF = 255 state = UNDEF -strict = 0 voice_idx_dict = {} header = {} header['footnotes'] = '' @@ -87,77 +124,77 @@ nobarlines = 0 global_key = [0] * 7 # UGH names = ["One", "Two", "Three"] DIGITS='0123456789' -alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZ" HSPACE=' \t' midi_specs = '' def error (msg): sys.stderr.write (msg) - if strict: + if global_options.strict: sys.exit (1) + +def alphabet (i): + return chr (i + ord('A')) def check_clef(s): - if not s: - return '' - if re.match('-8va', s) or re.match('treble8', s): - # treble8 is used by abctab2ps; -8va is used by barfly, - # and by my patch to abc2ps. If there's ever a standard - # about this we'll support that. - s = s[4:] - state.base_octave = -1 - voices_append("\\clef \"G_8\"\n") - elif re.match('^treble', s): - s = s[6:] - if re.match ('^-8', s): - s = s[2:] - state.base_octave = -2 - voices_append("\\clef \"G_8\"\n") - else: - state.base_octave = 0 - voices_append("\\clef treble\n") - elif re.match('^alto', s): - s = s[4:] - state.base_octave = -1 - voices_append ("\\clef alto\n" ) - elif re.match('^bass',s ): - s = s[4:] - state.base_octave = -2 - voices_append ("\\clef bass\n" ) - return s + if not s: + return '' + if re.match('-8va', s) or re.match('treble8', s): + # treble8 is used by abctab2ps; -8va is used by barfly, + # and by my patch to abc2ps. If there's ever a standard + # about this we'll support that. + s = s[4:] + state.base_octave = -1 + voices_append("\\clef \"G_8\"\n") + elif re.match('^treble', s): + s = s[6:] + if re.match ('^-8', s): + s = s[2:] + state.base_octave = -2 + voices_append("\\clef \"G_8\"\n") + else: + state.base_octave = 0 + voices_append("\\clef treble\n") + elif re.match('^alto', s): + s = s[4:] + state.base_octave = -1 + voices_append ("\\clef alto\n" ) + elif re.match('^bass',s ): + s = s[4:] + state.base_octave = -2 + voices_append ("\\clef bass\n" ) + return s def select_voice (name, rol): if not voice_idx_dict.has_key (name): - state_list.append(Parser_state()) - voices.append ('') - slyrics.append ([]) - voice_idx_dict[name] = len (voices) -1 + state_list.append(Parser_state()) + voices.append ('') + slyrics.append ([]) + voice_idx_dict[name] = len (voices) -1 __main__.current_voice_idx = voice_idx_dict[name] __main__.state = state_list[current_voice_idx] while rol != '': - m = re.match ('^([^ \t=]*)=(.*)$', rol) # find keywork - if m: - keyword = m.group(1) - rol = m.group (2) - a = re.match ('^("[^"]*"|[^ \t]*) *(.*)$', rol) - if a: - value = a.group (1) - rol = a.group ( 2) - if keyword == 'clef': - check_clef(value) - elif keyword == "name": - value = re.sub ('\\\\','\\\\\\\\', value) - voices_append ("\\property Staff.instrument = %s\n" % value ) - __main__.part_names = 1 - elif keyword == "sname" or keyword == "snm": - voices_append ("\\property Staff.instr = %s\n" % value ) - - else: - break - - return - + m = re.match ('^([^ \t=]*)=(.*)$', rol) # find keywork + if m: + keyword = m.group(1) + rol = m.group (2) + a = re.match ('^("[^"]*"|[^ \t]*) *(.*)$', rol) + if a: + value = a.group (1) + rol = a.group ( 2) + if keyword == 'clef': + check_clef(value) + elif keyword == "name": + value = re.sub ('\\\\','\\\\\\\\', value) + ## < 2.2 + voices_append ("\\set Staff.instrument = %s\n" % value ) + + __main__.part_names = 1 + elif keyword == "sname" or keyword == "snm": + voices_append ("\\set Staff.instr = %s\n" % value ) + else: + break def dump_header (outf,hdr): outf.write ('\\header {\n') @@ -170,14 +207,18 @@ def dump_header (outf,hdr): def dump_lyrics (outf): if (len(lyrics)): - outf.write("\n\\score\n{\n \\context Lyrics\n <\n") + outf.write("\n\\score\n{\n \\lyrics\n <<\n") for i in range (len (lyrics)): outf.write ( lyrics [i]) outf.write ("\n") - outf.write(" >\n \\paper{}\n}\n") + outf.write(" >>\n \\layout{}\n}\n") def dump_default_bar (outf): - outf.write ("\n\\property Score.defaultBarType=\"empty\"\n") + """ + Nowadays abc2ly outputs explicits barlines (?) + """ + ## < 2.2 + outf.write ("\n\\set Score.defaultBarType = \"empty\"\n") def dump_slyrics (outf): @@ -185,12 +226,12 @@ def dump_slyrics (outf): ks.sort () for k in ks: if re.match('[1-9]', k): - m = alphabet[string.atoi(k)] + m = alphabet(string.atoi(k)) else: m = k for i in range (len(slyrics[voice_idx_dict[k]])): - l=alphabet[i] - outf.write ("\nwords%sV%s = \\lyrics {" % (m, l)) + l= alphabet(i) + outf.write ("\nwords%sV%s = \lyricmode {" % (m, l)) outf.write ("\n" + slyrics [voice_idx_dict[k]][i]) outf.write ("\n}") @@ -200,10 +241,10 @@ def dump_voices (outf): ks.sort () for k in ks: if re.match ('[1-9]', k): - m = alphabet[string.atoi(k)] + m = alphabet(string.atoi(k)) else: m = k - outf.write ("\nvoice%s = \\notes {" % m) + outf.write ("\nvoice%s = {" % m) dump_default_bar(outf) if repeat_state[voice_idx_dict[k]]: outf.write("\n\\repeat volta 2 {") @@ -233,43 +274,40 @@ def try_parse_q(a): sys.stderr.write("abc2ly: Warning, unable to parse Q specification: %s\n" % a) def dump_score (outf): - outf.write (r"""\score{ - \notes < + outf.write (r""" + +\score{ + << """) - ks = voice_idx_dict.keys (); + ks = voice_idx_dict.keys (); ks.sort () for k in ks: if re.match('[1-9]', k): - m = alphabet[string.atoi(k)] + m = alphabet (string.atoi(k)) else: m = k if k == 'default' and len (voice_idx_dict) > 1: break - if len ( slyrics [voice_idx_dict[k]] ): - outf.write ("\n \\addlyrics") outf.write ("\n\t\\context Staff=\"%s\"\n\t{\n" %k ) if k != 'default': outf.write ("\t \\voicedefault\n") outf.write ("\t \\voice%s " % m) outf.write ("\n\t}\n") - if len ( slyrics [voice_idx_dict[k]] ): - outf.write ("\n\t\\context Lyrics=\"%s\" \n\t<\t" % k) + + l = ord( 'A' ) + for lyrics in slyrics [voice_idx_dict[k]]: + outf.write ("\n\t\\addlyrics { \n") if re.match('[1-9]',k): - m = alphabet[string.atoi(k)] + m = alphabet (string.atoi(k)) else: m = k - for i in range (len(slyrics[voice_idx_dict[k]])): - l=alphabet[i] - outf.write("\n\t { \\words%sV%s }" % ( m, l) ) - outf.write ( "\n\t>\n" ) - outf.write ("\n >") - outf.write ("\n\t\\paper {\n") - if part_names: - outf.write ("\t \\translator \n\t {\n") - outf.write ("\t\t\\StaffContext\n") -# outf.write ("\t\t\\consists Staff_margin_engraver\n") - outf.write ("\t }\n") + + outf.write ( " \\words%sV%s } " % ( m, chr (l)) ) + l += 1 + + outf.write ("\n >>") + outf.write ("\n\t\\layout {\n") outf.write ("\t}\n\t\\midi {%s}\n}\n" % midi_specs) @@ -382,8 +420,11 @@ key_lookup = { # abc to lilypond key mode names } def lily_key (k): + orig = "" + k + # UGR k = string.lower (k) key = k[0] + #UGH k = k[1:] if k and k[0] == '#': key = key + 'is' @@ -395,24 +436,27 @@ def lily_key (k): return '%s \\major' % key type = k[0:3] - if key_lookup.has_key(type): - return("%s \\%s" % ( key, key_lookup[type])) - sys.stderr.write("Unknown key type `%s' ignored\n" % type) - return key - -def shift_key (note, acc , shift): - s = semitone_pitch((note, acc)) - s = (s + shift + 12) % 12 - if s <= 4: - n = s / 2 - a = s % 2 - else: - n = (s + 1) / 2 - a = (s + 1) % 2 - if a: - n = n + 1 - a = -1 - return (n,a) + if not key_lookup.has_key (type): + #ugh, use lilylib, say WARNING:FILE:LINE: + sys.stderr.write ("abc2ly:warning:") + sys.stderr.write ("ignoring unknown key: `%s'" % orig) + sys.stderr.write ('\n') + return 0 + return ("%s \\%s" % ( key, key_lookup[type])) + +def shift_key (note, acc, shift): + s = semitone_pitch((note, acc)) + s = (s + shift + 12) % 12 + if s <= 4: + n = s / 2 + a = s % 2 + else: + n = (s + 1) / 2 + a = (s + 1) % 2 + if a: + n = n + 1 + a = -1 + return (n,a) key_shift = { # semitone shifts for key mode names 'm' : 3, @@ -501,6 +545,7 @@ def try_parse_tuplet_begin (str, state): def try_parse_group_end (str, state): if str and str[0] in HSPACE: str = str[1:] + close_beam_state(state) return str def header_append (key, a): @@ -521,13 +566,31 @@ def stuff_append (stuff, idx, a): else: stuff [idx] = wordwrap(a, stuff[idx]) - +# ignore wordwrap since we are adding to the previous word +def stuff_append_back(stuff, idx, a): + if not stuff: + stuff.append (a) + else: + point = len(stuff[idx])-1 + while stuff[idx][point] is ' ': + point = point - 1 + point = point +1 + stuff[idx] = stuff[idx][:point] + a + stuff[idx][point:] def voices_append(a): if current_voice_idx < 0: select_voice ('default', '') stuff_append (voices, current_voice_idx, a) +# word wrap really makes it hard to bind beams to the end of notes since it +# pushes out whitespace on every call. The _back functions do an append +# prior to the last space, effectively tagging whatever they are given +# onto the last note +def voices_append_back(a): + if current_voice_idx < 0: + select_voice ('default', '') + stuff_append_back(voices, current_voice_idx, a) + def repeat_prepend(): global repeat_state if current_voice_idx < 0: @@ -537,9 +600,9 @@ def repeat_prepend(): def lyrics_append(a): - a = re.sub ( '#', '\\#', a) # latex does not like naked #'s - a = re.sub ( '"', '\\"', a) # latex does not like naked "'s - a = '\t{ \\lyrics "' + a + '" }\n' + a = re.sub ('#', '\\#', a) # latex does not like naked #'s + a = re.sub ('"', '\\"', a) # latex does not like naked "'s + a = '\t{ "' + a + '" }\n' stuff_append (lyrics, current_lyric_idx, a) # break lyrics to words and put "'s around words containing numbers and '"'s @@ -590,7 +653,10 @@ def try_parse_header_line (ln, state): if header.has_key('title'): if a: if len(header['title']): - header['title'] = header['title'] + '\\\\\\\\' + a + # the non-ascii character + # in the string below is a + # punctuation dash. (TeX ---) + header['title'] = header['title'] + ' — ' + a else: header['subtitle'] = a else: @@ -599,12 +665,12 @@ def try_parse_header_line (ln, state): if a == 'C': if not state.common_time: state.common_time = 1 - voices_append ("\\property Staff.TimeSignature \\override #\'style = #'C\n") + voices_append (" \\override Staff.TimeSignature #\'style = #'C\n") a = '4/4' if a == 'C|': if not state.common_time: state.common_time = 1 - voices_append ("\\property Staff.TimeSignature \\override #\'style = #'C\n") + voices_append ("\\override Staff.TimeSignature #\'style = #'C\n") a = '2/2' if not length_specified: set_default_len_from_time_sig (a) @@ -626,12 +692,16 @@ def try_parse_header_line (ln, state): else: key_info = m.group(1) clef_info = m.group(2) - __main__.global_key = compute_key (key_info)# ugh. - voices_append ('\\key %s' % lily_key(key_info)) + __main__.global_key = compute_key (key_info) + k = lily_key (key_info) + if k: + voices_append ('\\key %s' % k) check_clef(clef_info) else: - __main__.global_key = compute_key (a)# ugh. - voices_append ('\\key %s \\major' % lily_key(a)) + __main__.global_key = compute_key (a) + k = lily_key (a) + if k: + voices_append ('\\key %s \\major' % k) if g == 'N': # Notes header ['footnotes'] = header['footnotes'] + '\\\\\\\\' + a if g == 'O': # Origin @@ -738,6 +808,7 @@ class Parser_state: self.plus_chord = 0 self.base_octave = 0 self.common_time = 0 + self.parsing_beam = 0 @@ -874,6 +945,12 @@ def clear_bar_acc(state): del state.in_acc[k] +# if we are parsing a beam, close it off +def close_beam_state(state): + if state.parsing_beam and global_options.beams: + state.parsing_beam = 0 + voices_append_back( ']' ) + # WAT IS ABC EEN ONTZETTENDE PROGRAMMEERPOEP ! def try_parse_note (str, parser_state): @@ -895,7 +972,7 @@ def try_parse_note (str, parser_state): if c == '_': acc = -1 - octave = parser_state.base_octave + octave = parser_state.base_octave if str[0] in "ABCDEFG": str = string.lower (str[0]) + str[1:] octave = octave - 1 @@ -924,7 +1001,6 @@ def try_parse_note (str, parser_state): (str, num,den,current_dots) = parse_duration (str, parser_state) - if re.match('[ \t]*\)', str): str = string.lstrip (str) @@ -934,9 +1010,6 @@ def try_parse_note (str, parser_state): str = str[1:] - if slur_end: - voices_append ('%s' % ')' *slur_end ) - bar_acc = get_bar_acc(notename, octave, parser_state) pit = pitch_to_lilypond_name(notename, acc, bar_acc, global_key[notename]) oct = octave_to_lilypond_quotes (octave) @@ -960,14 +1033,23 @@ def try_parse_note (str, parser_state): if not parser_state.parsing_tuplet: voices_append ("}") if slur_begin: - voices_append ('%s' % '(' * slur_begin ) - - + voices_append ('-(' * slur_begin ) + if slur_end: + voices_append ('-)' *slur_end ) + + if global_options.beams and \ + str[0] in '^=_ABCDEFGabcdefg' and \ + not parser_state.parsing_beam and \ + not parser_state.parsing_tuplet: + parser_state.parsing_beam = 1 + voices_append_back( '[' ) + return str -def junk_space (str): - while str and str[0] in '\t\n ': +def junk_space (str,state): + while str and str[0] in '\t\n\r ': str = str[1:] + close_beam_state(state) return str @@ -988,7 +1070,8 @@ def try_parse_guitar_chord (str, state): if str: str = str[1:] gc = re.sub('#', '\\#', gc) # escape '#'s - state.next_articulation = ("%c\"%s\"" % (position ,gc)) + state.next_articulation + state.next_articulation = ("%c\"%s\"" % (position, gc)) \ + + state.next_articulation return str def try_parse_escape (str): @@ -1090,6 +1173,7 @@ def try_parse_bar (str,state): state.next_bar = '|\n' str = str[1:] clear_bar_acc(state) + close_beam_state(state) if bs <> None or state.next_bar != '': if state.parsing_tuplet: @@ -1098,6 +1182,7 @@ def try_parse_bar (str,state): if bs <> None: clear_bar_acc(state) + close_beam_state(state) voices_append (bs) if do_curly != '': voices_append("} }") @@ -1126,24 +1211,24 @@ def try_parse_chord_delims (str, state): if state.next_bar: voices_append(state.next_bar) state.next_bar = '' - voices_append ('<') + voices_append ('<<') if str[:1] == '+': str = str[1:] if state.plus_chord: - voices_append ('>') + voices_append ('>>') state.plus_chord = 0 else: if state.next_bar: voices_append(state.next_bar) state.next_bar = '' - voices_append ('<') + voices_append ('<<') state.plus_chord = 1 ch = '' if str[:1] ==']': str = str[1:] - ch = '>' + ch = '>>' end = 0 while str[:1] ==')': @@ -1200,12 +1285,15 @@ def try_parse_comment (str): #write other kinds of appending if we ever need them. return str +lineno = 0 happy_count = 100 def parse_file (fn): f = open (fn) ls = f.readlines () + ls = map (lambda x: re.sub ("\r$", '', x), ls) select_voice('default', '') + global lineno lineno = 0 sys.stderr.write ("Line ... ") sys.stderr.flush () @@ -1243,7 +1331,7 @@ def parse_file (fn): ln = try_parse_tuplet_begin (ln, state) ln = try_parse_group_end (ln, state) ln = try_parse_grace_delims (ln, state) - ln = junk_space (ln) + ln = junk_space (ln, state) if ln: error ("%s: %d: Huh? Don't understand\n" % (fn, lineno)) @@ -1255,52 +1343,35 @@ def parse_file (fn): def identify(): sys.stderr.write ("%s from LilyPond %s\n" % (program_name, version)) -def help (): - print r""" -Convert ABC to lilypond. - -Usage: abc2ly [OPTIONS]... ABC-FILE - -Options: - -h, --help this help - -o, --output=FILE set output filename to FILE - -v, --version version information - -s, --strict be strict about succes. - -This program converts ABC music files (see -http://www.gre.ac.uk/~c.walshaw/abc2mtex/abc.txt) To LilyPond input. - - -Report bugs to bug-gnu-music@gnu.org - +authors = """ Written by Han-Wen Nienhuys , Laura Conrad -, Roy Rankin +, Roy Rankin . """ def print_version (): print r"""abc2ly (GNU lilypond) %s""" % version +def get_option_parser (): + p = ly.get_option_parser (usage='abc2ly [OPTIONS] FILE', + version="abc2ly (LilyPond) @TOPLEVEL_VERSION@", + description=_('''This program converts ABC music files (see +http://www.gre.ac.uk/~c.walshaw/abc2mtex/abc.txt) to LilyPond input.''')) + + p.add_option ('-o', '--output', metavar='FILE',help=_("set output filename to FILE"), + action='store') + p.add_option ('-s', '--strict', help=_("be strict about succes"), + action='store_true') + p.add_option ('-b', '--beams', help=_("preserve ABC's notion of beams")) + p.add_option_group ('bugs', + description='''Report bugs via http://post.gmane.org/post.php''' + '''?group=gmane.comp.gnu.lilypond.bugs\n''') + + return p -(options, files) = getopt.getopt (sys.argv[1:], 'vo:hs', ['help','version', 'output=', 'strict']) -out_filename = '' +option_parser = get_option_parser() +(global_options, files) = option_parser.parse_args() -for opt in options: - o = opt[0] - a = opt[1] - if o== '--help' or o == '-h': - help () - sys.exit (0) - elif o == '--version' or o == '-v': - print_version () - sys.exit(0) - elif o == '--strict' or o == '-s': - strict = 1 - elif o == '--output' or o == '-o': - out_filename = a - else: - print o - raise getopt.error identify() @@ -1312,13 +1383,17 @@ for f in files: sys.stderr.write ('Parsing `%s\'...\n' % f) parse_file (f) - if not out_filename: - out_filename = os.path.basename (os.path.splitext (f)[0]) + ".ly" - sys.stderr.write ('lilypond output to: `%s\'...' % out_filename) - outf = open (out_filename, 'w') + if not global_options.output: + global_options.output = os.path.basename (os.path.splitext (f)[0]) + ".ly" + sys.stderr.write ('lilypond output to: `%s\'...' % global_options.output) + outf = open (global_options.output, 'w') + +# don't substitute @VERSION@. We want this to reflect +# the last version that was verified to work. + outf.write ('\\version "2.5.20"\n') # dump_global (outf) - dump_header (outf ,header) + dump_header (outf, header) dump_slyrics (outf) dump_voices (outf) dump_score (outf)