#!@TARGET_PYTHON@ # -*- coding: utf-8 -*- # once upon a rainy monday afternoon. # This file is part of LilyPond, the GNU music typesetter. # # Copyright (C) 1999--2020 Han-Wen Nienhuys # Jan Nieuwenhuizen # # LilyPond 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 3 of the License, or # (at your option) any later version. # # LilyPond 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. # # You should have received a copy of the GNU General Public License # along with LilyPond. If not, see . # # ... # # (not finished.) # ABC standard v1.6: http://abcnotation.com/ # # Enhancements (Roy R. Rankin) # # Header section moved to top of lilypond file # handle treble, treble-8, alto, and bass clef # Handle voices (V: headers) with clef and part names, multiple voices # Handle w: lyrics with multiple verses # Handle key mode names for minor, major, phrygian, ionian, locrian, aeolian, # mixolydian, lydian, dorian # Handle part names from V: header # Tuplets handling fixed up # Lines starting with |: not discarded as header lines # Multiple T: and C: header entries handled # Accidental maintained until next bar check # Silent rests supported # articulations fermata, upbow, downbow, ltoe, accent, tenuto supported # Chord strings([-^]"string") can contain a '#' # Header fields enclosed by [] in notes string processed # W: words output after tune as abc2ps does it (they failed before) # Enhancements (Laura Conrad) # # Barring now preserved between ABC and lilypond # the default placement for text in abc is above the staff. # %%LY now supported. # \breve and \longa supported. # M:none doesn't crash lily. # lilypond '--' supported. # 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 # Blank T: header lines should write score and open a new score # Not all header fields supported # ABC line breaks are ignored # Block comments generate error and are ignored # Postscript commands are ignored # lyrics not resynchronized by line breaks (lyrics must fully match notes) # %%LY slyrics can't be directly before a w: line. # ??? # TODO: # # * coding style # * lilylib # * GNU style messages: warning:FILE:LINE: # * l10n # # Convert to new chord styles. # # UNDEF -> None # import __main__ import getopt import gettext import os import re import sys program_name = sys.argv[0] """ @relocate-preamble@ """ # Load translation and install _() into Python's builtins namespace. gettext.install('lilypond', '@localedir@') import lilylib as ly version = '@TOPLEVEL_VERSION@' if version == '@' + 'TOPLEVEL_VERSION' + '@': version = '(unknown version)' # uGUHGUHGHGUGH UNDEF = 255 state = UNDEF voice_idx_dict = {} header = {} header['footnotes'] = '' lyrics = [] slyrics = [] voices = [] state_list = [] repeat_state = [0] * 8 current_voice_idx = -1 current_lyric_idx = -1 lyric_idx = -1 part_names = 0 default_len = 8 length_specified = 0 nobarlines = 0 global_key = [0] * 7 # UGH names = ["One", "Two", "Three"] DIGITS = '0123456789' HSPACE = ' \t' midi_specs = '' def error(msg): sys.stderr.write(msg) if global_options.strict: sys.exit(1) def alphabet(i): return chr(i + ord('A')) def check_clef(s): # the number gives the base_octave clefs = [("treble", "treble", 0), ("treble1", "french", 0), ("bass3", "varbaritone", 0), ("bass", "bass", 0), ("alto4", "tenor", 0), ("alto2", "mezzosoprano", 0), ("alto1", "soprano", 0), ("alto", "alto", 0), ("perc", "percussion", 0)] modifier = [("-8va", "_8", -1), ("-8", "_8", -1), (r"\+8", "^8", +1), ("8", "_8", -1)] if not s: return '' clef = None octave = 0 for c in clefs: m = re.match('^'+c[0], s) if m: (clef, octave) = (c[1], c[2]) s = s[m.end():] break if not clef: return s mod = "" for md in modifier: m = re.match('^'+md[0], s) if m: mod = md[1] octave += md[2] s = s[m.end():] break state.base_octave = octave voices_append("\\clef \""+clef+mod+"\"\n") return s def select_voice(name, rol): if name not in voice_idx_dict: 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) # < 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') ks = sorted(hdr.keys()) for k in ks: hdr[k] = re.sub('"', '\\"', hdr[k]) outf.write('\t%s = "%s"\n' % (k, hdr[k])) outf.write('}') def dump_lyrics(outf): if lyrics: outf.write("\n\\markup \\column {\n") for i in range(len(lyrics)): outf.write(lyrics[i]) outf.write("\n") outf.write("}\n") def dump_default_bar(outf): """ Nowadays abc2ly outputs explicits barlines (?) """ # < 2.2 outf.write("\n\\set Score.defaultBarType = \"\"\n") def dump_slyrics(outf): ks = sorted(voice_idx_dict.keys()) for k in ks: if re.match('[1-9]', k): m = alphabet(int(k)) else: m = k for i in range(len(slyrics[voice_idx_dict[k]])): l = alphabet(i) outf.write("\nwords%sV%s = \\lyricmode {" % (m, l)) outf.write("\n" + slyrics[voice_idx_dict[k]][i]) outf.write("\n}") def dump_voices(outf): global doing_alternative, in_repeat ks = sorted(voice_idx_dict.keys()) for k in ks: if re.match('[1-9]', k): m = alphabet(int(k)) else: m = k outf.write("\nvoice%s = {" % m) dump_default_bar(outf) if repeat_state[voice_idx_dict[k]]: outf.write("\n\\repeat volta 2 {") outf.write("\n" + voices[voice_idx_dict[k]]) if not using_old: if doing_alternative[voice_idx_dict[k]]: outf.write("}") if in_repeat[voice_idx_dict[k]]: outf.write("}") outf.write("\n}") def try_parse_q(a): # assume that Q takes the form "Q:'opt. description' 1/4=120" # There are other possibilities, but they are deprecated r = re.compile(r'^(.*) *([0-9]+) */ *([0-9]+) *=* *([0-9]+)\s*') m = r.match(a) if m: descr = m.group(1) # possibly empty numerator = int(m.group(2)) denominator = int(m.group(3)) tempo = m.group(4) dur = duration_to_lilypond_duration((numerator, denominator), 1, 0) voices_append("\\tempo " + descr + " " + dur + "=" + tempo + "\n") else: # Parsing of numeric tempi, as these are fairly # common. The spec says the number is a "beat" so using # a quarter note as the standard time numericQ = re.compile('[0-9]+') m = numericQ.match(a) if m: voices_append("\\tempo 4=" + m.group(0)) else: sys.stderr.write( "abc2ly: Warning, unable to parse Q specification: %s\n" % a) def dump_score(outf): outf.write(r""" \score{ << """) ks = sorted(voice_idx_dict.keys()) for k in ks: if re.match('[1-9]', k): m = alphabet(int(k)) else: m = k if k == 'default' and len(voice_idx_dict) > 1: break 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") 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(int(k)) else: m = k 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) def set_default_length(s): global length_specified m = re.search('1/([0-9]+)', s) if m: __main__.default_len = int(m.group(1)) length_specified = 1 def set_default_len_from_time_sig(s): m = re.search('([0-9]+)/([0-9]+)', s) if m: n = int(m.group(1)) d = int(m.group(2)) if (n * 1.0)/(d * 1.0) < 0.75: __main__.default_len = 16 else: __main__.default_len = 8 def gulp_file(f): try: i = open(f, encoding="utf8") i.seek(0, 2) n = i.tell() i.seek(0, 0) except FileNotFoundError: sys.stderr.write("cannot open file: `%s'\n" % f) return '' s = i.read(n) if len(s) <= 0: sys.stderr.write("gulped empty file: `%s'\n" % f) i.close() return s # pitch manipulation. Tuples are (name, alteration). # 0 is (central) C. Alteration -1 is a flat, Alteration +1 is a sharp # pitch in semitones. def semitone_pitch(tup): p = 0 t = tup[0] p = p + 12 * (t // 7) t = t % 7 if t > 2: p = p - 1 p = p + t * 2 + tup[1] return p def fifth_above_pitch(tup): (n, a) = (tup[0] + 4, tup[1]) difference = 7 - (semitone_pitch((n, a)) - semitone_pitch(tup)) a = a + difference return (n, a) def sharp_keys(): p = (0, 0) l = [] k = 0 while True: l.append(p) (t, a) = fifth_above_pitch(p) if semitone_pitch((t, a)) % 12 == 0: break p = (t % 7, a) return l def flat_keys(): p = (0, 0) l = [] k = 0 while True: l.append(p) (t, a) = quart_above_pitch(p) if semitone_pitch((t, a)) % 12 == 0: break p = (t % 7, a) return l def quart_above_pitch(tup): (n, a) = (tup[0] + 3, tup[1]) difference = 5 - (semitone_pitch((n, a)) - semitone_pitch(tup)) a = a + difference return (n, a) key_lookup = { # abc to lilypond key mode names 'm': 'minor', 'min': 'minor', 'maj': 'major', 'major': 'major', 'phr': 'phrygian', 'ion': 'ionian', 'loc': 'locrian', 'aeo': 'aeolian', 'mix': 'mixolydian', 'mixolydian': 'mixolydian', 'lyd': 'lydian', 'dor': 'dorian', 'dorian': 'dorian' } def lily_key(k): if k == 'none': return orig = "" + k # UGR k = k.lower() key = k[0] # UGH k = k[1:] if k and k[0] == '#': key = key + 'is' k = k[1:] elif k and k[0] == 'b': key = key + 'es' k = k[1:] if not k: return '%s \\major' % key type = k[0:3] if type not in key_lookup: # 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, 'min': 3, 'minor': 3, 'maj': 0, 'major': 0, 'phr': -4, 'phrygian': -4, 'ion': 0, 'ionian': 0, 'loc': 1, 'locrian': 1, 'aeo': 3, 'aeolian': 3, 'mix': 5, 'mixolydian': 5, 'lyd': -5, 'lydian': -5, 'dor': -2, 'dorian': -2 } def compute_key(k): k = k.lower() intkey = (ord(k[0]) - ord('a') + 5) % 7 intkeyacc = 0 k = k[1:] if k and k[0] == 'b': intkeyacc = -1 k = k[1:] elif k and k[0] == '#': intkeyacc = 1 k = k[1:] k = k[0:3] if k and k in key_shift: (intkey, intkeyacc) = shift_key(intkey, intkeyacc, key_shift[k]) keytup = (intkey, intkeyacc) sharp_key_seq = sharp_keys() flat_key_seq = flat_keys() accseq = None accsign = 0 if keytup in sharp_key_seq: accsign = 1 key_count = sharp_key_seq.index(keytup) accseq = [(4*x - 1) % 7 for x in range(1, key_count + 1)] elif keytup in flat_key_seq: accsign = -1 key_count = flat_key_seq.index(keytup) accseq = [(3*x + 3) % 7 for x in range(1, key_count + 1)] else: error("Huh?") raise Exception("Huh") key_table = [0] * 7 for a in accseq: key_table[a] = key_table[a] + accsign 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(s, state): if re.match(r'\([2-9]', s): dig = s[1] s = s[2:] prev_tuplet_state = state.parsing_tuplet state.parsing_tuplet = int(dig[0]) if prev_tuplet_state: voices_append("}") voices_append("\\times %s {" % tup_lookup[dig]) return s def try_parse_group_end(s, state): if s and s[0] in HSPACE: s = s[1:] close_beam_state(state) return s def header_append(key, a): s = '' if key in header: s = header[key] + "\n" header[key] = s + a def wordwrap(a, v): linelen = len(v) - v.rfind('\n') if linelen + len(a) > 80: v = v + '\n' return v + a + ' ' def stuff_append(stuff, idx, a): if not stuff: stuff.append(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] == ' ': 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: select_voice('default', '') if not using_old: repeat_state[current_voice_idx] = 't' 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 = ' \\line { "' + a + '" }\n' stuff_append(lyrics, current_lyric_idx, a) # break lyrics to words and put "'s around words containing numbers and '"'s def fix_lyric(s): ret = '' while s != '': m = re.match('[ \t]*([^ \t]*)[ \t]*(.*$)', s) if m: word = m.group(1) s = m.group(2) word = re.sub('"', '\\"', word) # escape " if re.match(r'.*[0-9"\(]', word): word = re.sub('_', ' ', word) # _ causes probs inside "" ret = ret + '\"' + word + '\" ' else: ret = ret + word + ' ' else: return ret return ret def slyrics_append(a): a = re.sub('_', ' _ ', a) # _ to ' _ ' # split words with "-" unless was originally "--" a = re.sub('([^-])-([^-])', '\\1- \\2', a) a = re.sub('\\\\- ', '-', a) # unless \- a = re.sub('~', '_', a) # ~ to space('_') a = re.sub(r'\*', '_ ', a) # * to to space a = re.sub('#', '\\#', a) # latex does not like naked #'s if re.match(r'.*[0-9"\(]', a): # put numbers and " and ( into quoted string a = fix_lyric(a) a = re.sub('$', ' ', a) # insure space between lines __main__.lyric_idx = lyric_idx + 1 if len(slyrics[current_voice_idx]) <= lyric_idx: slyrics[current_voice_idx].append(a) else: v = slyrics[current_voice_idx][lyric_idx] slyrics[current_voice_idx][lyric_idx] = wordwrap( a, slyrics[current_voice_idx][lyric_idx]) def try_parse_header_line(ln, state): global length_specified m = re.match('^([A-Za-z]): *(.*)$', ln) if m: g = m.group(1) a = m.group(2) if g == 'T': # title a = re.sub('[ \t]*$', '', a) # strip trailing blanks if 'title' in header: if a: if len(header['title']): # the non-ascii character # in the string below is a # punctuation dash. (TeX ---) header['title'] = header['title'] + ' — ' + a else: header['subtitle'] = a else: header['title'] = a if g == 'M': # Meter if a == 'C': if not state.common_time: state.common_time = 1 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( "\\override Staff.TimeSignature #'style = #'C\n") a = '2/2' if not length_specified: set_default_len_from_time_sig(a) else: length_specified = 0 if not a == 'none': voices_append('\\time %s' % a) state.next_bar = '' if g == 'K': # KEY a = check_clef(a) if a: # separate clef info m = re.match('^([^ \t]*) *([^ ]*)( *)(.*)$', a) if m: # there may or may not be a space # between the key letter and the mode # convert the mode to lower-case before comparing mode = m.group(2)[0:3].lower() if mode in key_lookup: # use the full mode, not only the first three letters key_info = m.group(1) + m.group(2).lower() clef_info = a[m.start(4):] else: key_info = m.group(1) clef_info = a[m.start(2):] __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) 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 header['origin'] = a if g == 'X': # Reference Number header['crossRefNumber'] = a if g == 'A': # Area header['area'] = a if g == 'H': # History header_append('history', a) if g == 'B': # Book header['book'] = a if g == 'C': # Composer if 'composer' in header: if a: header['composer'] = header['composer'] + '\\\\\\\\' + a else: header['composer'] = a if g == 'S': header['subtitle'] = a if g == 'L': # Default note length set_default_length(ln) if g == 'V': # Voice voice = re.sub(' .*$', '', a) rest = re.sub('^[^ \t]* *', '', a) if state.next_bar: voices_append(state.next_bar) state.next_bar = '' select_voice(voice, rest) if g == 'W': # Words lyrics_append(a) if g == 'w': # vocals slyrics_append(a) if g == 'Q': # tempo try_parse_q(a) if g == 'R': # Rhythm (e.g. jig, reel, hornpipe) header['meter'] = a if g == 'Z': # Transcription (e.g. Steve Mansfield 1/2/2000) header['transcription'] = a return '' return ln # we use in this order specified accidental, active accidental for bar, # active accidental for key def pitch_to_lilypond_name(name, acc, bar_acc, key): s = '' if acc == UNDEF: if not nobarlines: acc = bar_acc if acc == UNDEF: acc = key if acc == -1: s = 'es' elif acc == 1: s = 'is' if name > 4: name = name - 7 return chr(name + ord('c')) + s def octave_to_lilypond_quotes(o): o = o + 2 s = '' if o < 0: o = -o s = ',' else: s = '\'' return s * o def parse_num(s): durstr = '' while s and s[0] in DIGITS: durstr = durstr + s[0] s = s[1:] n = None if durstr: n = int(durstr) return (s, n) def duration_to_lilypond_duration(multiply_tup, defaultlen, dots): base = 1 # (num / den) / defaultlen < 1/base while base * multiply_tup[0] < multiply_tup[1]: base = base * 2 if base == 1: if (multiply_tup[0] / multiply_tup[1]) == 2: base = '\\breve' if (multiply_tup[0] / multiply_tup[1]) == 3: base = '\\breve' dots = 1 if (multiply_tup[0] / multiply_tup[1]) == 4: base = '\\longa' return '%s%s' % (base, '.' * dots) class Parser_state: def __init__(self): self.in_acc = {} self.next_articulation = '' self.next_bar = '' self.next_dots = 0 self.next_den = 1 self.parsing_tuplet = 0 self.plus_chord = 0 self.base_octave = 0 self.common_time = 0 self.parsing_beam = 0 # return (str, num,den,dots) def parse_duration(s, parser_state): num = 0 den = parser_state.next_den parser_state.next_den = 1 (s, num) = parse_num(s) if not num: num = 1 if len(s): if s[0] == '/': if len(s[0]): while s[:1] == '/': s = s[1:] d = 2 if s[0] in DIGITS: (s, d) = parse_num(s) den = den * d den = den * default_len current_dots = parser_state.next_dots parser_state.next_dots = 0 if re.match('[ \t]*[<>]', s): while s[0] in HSPACE: s = s[1:] while s[0] == '>': s = s[1:] current_dots = current_dots + 1 parser_state.next_den = parser_state.next_den * 2 while s[0] == '<': s = s[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 (s, num, den, current_dots) def try_parse_rest(s, parser_state): if not s or s[0] != 'z' and s[0] != 'x': return s __main__.lyric_idx = -1 if parser_state.next_bar: voices_append(parser_state.next_bar) parser_state.next_bar = '' if s[0] == 'z': rest = 'r' else: rest = 's' s = s[1:] (s, num, den, d) = parse_duration(s, parser_state) voices_append( '%s%s' % (rest, duration_to_lilypond_duration((num, den), default_len, d))) if parser_state.next_articulation: voices_append(parser_state.next_articulation) parser_state.next_articulation = '' return s artic_tbl = { '.': '-.', 'T': '^\\trill', 'H': '^\\fermata', 'u': '^\\upbow', 'K': '^\\ltoe', 'k': '^\\accent', 'M': '^\\tenuto', '~': '^"~" ', 'J': '', # ignore slide 'R': '', # ignore roll 'S': '^\\segno', 'O': '^\\coda', 'v': '^\\downbow' } def try_parse_articulation(s, state): while s and s[:1] in artic_tbl: state.next_articulation = state.next_articulation + artic_tbl[s[:1]] if not artic_tbl[s[:1]]: sys.stderr.write("Warning: ignoring `%s'\n" % s[:1]) s = s[1:] # s7m2 input doesn't care about spaces if re.match(r'[ \t]*\(', s): s = s.lstrip() slur_begin = 0 while s[:1] == '(' and s[1] not in DIGITS: slur_begin = slur_begin + 1 state.next_articulation = state.next_articulation + '(' s = s[1:] return s # # remember accidental for rest of bar # def set_bar_acc(note, octave, acc, state): if acc == UNDEF: return n_oct = note + octave * 7 state.in_acc[n_oct] = acc # get accidental set in this bar or UNDEF if not set def get_bar_acc(note, octave, state): n_oct = note + octave * 7 if n_oct in state.in_acc: return state.in_acc[n_oct] else: return UNDEF def clear_bar_acc(state): state.in_acc = {} # 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(s, parser_state): mud = '' slur_begin = 0 if not s: return s articulation = '' acc = UNDEF if s[0] in '^=_': c = s[0] s = s[1:] if c == '^': acc = 1 if c == '=': acc = 0 if c == '_': acc = -1 octave = parser_state.base_octave if s[0] in "ABCDEFG": s = s[0].lower() + s[1:] octave = octave - 1 notename = 0 if s[0] in "abcdefg": notename = (ord(s[0]) - ord('a') + 5) % 7 s = s[1:] else: return s # failed; not a note! __main__.lyric_idx = -1 if parser_state.next_bar: voices_append(parser_state.next_bar) parser_state.next_bar = '' while s[0] == ',': octave = octave - 1 s = s[1:] while s[0] == '\'': octave = octave + 1 s = s[1:] (s, num, den, current_dots) = parse_duration(s, parser_state) if re.match(r'[ \t]*\)', s): s = s.lstrip() slur_end = 0 while s[:1] == ')': slur_end = slur_end + 1 s = s[1:] 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) if acc != UNDEF and (acc == global_key[notename] or acc == bar_acc): mod = '!' else: mod = '' voices_append("%s%s%s%s" % (pit, oct, mod, duration_to_lilypond_duration((num, den), default_len, current_dots))) set_bar_acc(notename, octave, acc, parser_state) if parser_state.next_articulation: articulation = articulation + parser_state.next_articulation parser_state.next_articulation = '' voices_append(articulation) if slur_begin: voices_append('-(' * slur_begin) if slur_end: voices_append('-)' * slur_end) if parser_state.parsing_tuplet: parser_state.parsing_tuplet = parser_state.parsing_tuplet - 1 if not parser_state.parsing_tuplet: voices_append("}") if global_options.beams and \ s[0] in '^=_ABCDEFGabcdefg' and \ not parser_state.parsing_beam and \ not parser_state.parsing_tuplet: parser_state.parsing_beam = 1 voices_append_back('[') return s def junk_space(s, state): while s and s[0] in '\t\n\r ': s = s[1:] close_beam_state(state) return s def try_parse_guitar_chord(s, state): if s[:1] == '"': s = s[1:] gc = '' if s[0] == '_' or (s[0] == '^'): position = s[0] s = s[1:] else: position = '^' while s and s[0] != '"': gc = gc + s[0] s = s[1:] if s: s = s[1:] gc = re.sub('#', '\\#', gc) # escape '#'s state.next_articulation = ("%c\"%s\"" % (position, gc)) \ + state.next_articulation return s def try_parse_escape(s): if not s or s[0] != '\\': return s s = s[1:] if s[:1] == 'K': key_table = compute_key() return s # # |] thin-thick double bar line # || thin-thin double bar line # [| thick-thin double bar line # :| left repeat # |: right repeat # :: left-right repeat # |1 volta 1 # |2 volta 2 old_bar_dict = { '|]': '|.', '||': '||', '[|': '||', ':|': ':|.', '|:': '|:', '::': ':|.|:', '|1': '|', '|2': '|', ':|2': ':|.', '|': '|' } bar_dict = { '|]': '\\bar "|."', '||': '\\bar "||"', '[|': '\\bar "||"', ':|': '}', '|:': '\\repeat volta 2 {', '::': '} \\repeat volta 2 {', '|1': '} \\alternative{{', '|2': '} {', ':|2': '} {', '|': '\\bar "|"' } warn_about = ['|:', '::', ':|', '|1', ':|2', '|2'] alternative_opener = ['|1', '|2', ':|2'] repeat_ender = ['::', ':|'] repeat_opener = ['::', '|:'] in_repeat = [''] * 8 doing_alternative = [''] * 8 using_old = '' def try_parse_bar(string, state): global in_repeat, doing_alternative, using_old do_curly = '' bs = None if current_voice_idx < 0: select_voice('default', '') # first try the longer one for trylen in [3, 2, 1]: if string[:trylen] and string[:trylen] in bar_dict: s = string[:trylen] if using_old: bs = "\\bar \"%s\"" % old_bar_dict[s] else: bs = "%s" % bar_dict[s] string = string[trylen:] if s in alternative_opener: if not in_repeat[current_voice_idx]: using_old = 't' bs = "\\bar \"%s\"" % old_bar_dict[s] else: doing_alternative[current_voice_idx] = 't' if s in repeat_ender: if not in_repeat[current_voice_idx]: sys.stderr.write( "Warning: inserting repeat to beginning of notes.\n") repeat_prepend() in_repeat[current_voice_idx] = '' else: if doing_alternative[current_voice_idx]: do_curly = 't' if using_old: bs = "\\bar \"%s\"" % old_bar_dict[s] else: bs = bar_dict[s] doing_alternative[current_voice_idx] = '' in_repeat[current_voice_idx] = '' if s in repeat_opener: in_repeat[current_voice_idx] = 't' if using_old: bs = "\\bar \"%s\"" % old_bar_dict[s] else: bs = bar_dict[s] break if string[:1] == '|': state.next_bar = '|\n' string = string[1:] clear_bar_acc(state) close_beam_state(state) if string[:1] == '}': close_beam_state(state) if bs is not None or state.next_bar != '': if state.parsing_tuplet: state.parsing_tuplet = 0 voices_append('} ') if bs is not None: clear_bar_acc(state) close_beam_state(state) voices_append(bs) if do_curly != '': voices_append("} ") do_curly = '' return string def try_parse_tie(s): if s[:1] == '-': s = s[1:] voices_append(' ~ ') return s def bracket_escape(s, state): m = re.match(r'^([^\]]*)] *(.*)$', s) if m: cmd = m.group(1) s = m.group(2) try_parse_header_line(cmd, state) return s def try_parse_chord_delims(s, state): if s[:1] == '[': s = s[1:] if re.match('[A-Z]:', s): # bracket escape return bracket_escape(s, state) if state.next_bar: voices_append(state.next_bar) state.next_bar = '' voices_append('<<') if s[:1] == '+': s = s[1:] if state.plus_chord: voices_append('>>') state.plus_chord = 0 else: if state.next_bar: voices_append(state.next_bar) state.next_bar = '' voices_append('<<') state.plus_chord = 1 ch = '' if s[:1] == ']': s = s[1:] ch = '>>' end = 0 while s[:1] == ')': end = end + 1 s = s[1:] voices_append("\\spanrequest \\stop \"slur\"" * end) voices_append(ch) return s def try_parse_grace_delims(s, state): if s[:1] == '{': if state.next_bar: voices_append(state.next_bar) state.next_bar = '' s = s[1:] voices_append('\\grace { ') if s[:1] == '}': s = s[1:] voices_append('}') return s def try_parse_comment(s): global nobarlines if s[0] == '%': if s[0:5] == '%MIDI': # the nobarlines option is necessary for an abc to lilypond translator for # exactly the same reason abc2midi needs it: abc requires the user to enter # the note that will be printed, and MIDI and lilypond expect entry of the # pitch that will be played. # # In standard 19th century musical notation, the algorithm for translating # between printed note and pitch involves using the barlines to determine # the scope of the accidentals. # # Since ABC is frequently used for music in styles that do not use this # convention, such as most music written before 1700, or ethnic music in # non-western scales, it is necessary to be able to tell a translator that # the barlines should not affect its interpretation of the pitch. if 'nobarlines' in s: nobarlines = 1 elif s[0:3] == '%LY': p = s.find('voices') if p > -1: voices_append(s[p+7:]) voices_append("\n") p = s.find('slyrics') if p > -1: slyrics_append(s[p+8:]) # write other kinds of appending if we ever need them. return s lineno = 0 happy_count = 100 def parse_file(fn): f = open(fn, encoding='utf8') ls = f.readlines() ls = [re.sub("\r$", '', x) for x in ls] select_voice('default', '') global lineno lineno = 0 if not global_options.quiet: sys.stderr.write("Line ... ") sys.stderr.flush() __main__.state = state_list[current_voice_idx] for ln in ls: lineno = lineno + 1 if not lineno % happy_count: sys.stderr.write('[%d]' % lineno) sys.stderr.flush() m = re.match('^([^%]*)%(.*)$', ln) # add comments to current voice if m: if m.group(2): try_parse_comment(m.group(2)) voices_append('%% %s\n' % m.group(2)) ln = m.group(1) orig_ln = ln ln = junk_space(ln, state) ln = try_parse_header_line(ln, state) # 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, state) 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, state) ln = junk_space(ln, state) if ln: error("%s: %d: Huh? Don't understand\n" % (fn, lineno)) left = orig_ln[0:-len(ln)] sys.stderr.write(left + '\n') sys.stderr.write(' ' * len(left) + ln + '\n') def identify(): if not global_options.quiet: sys.stderr.write("%s from LilyPond %s\n" % (program_name, version)) authors = """ Written by Han-Wen Nienhuys , Laura Conrad , Roy Rankin . """ def print_version(): print(r"""abc2ly (GNU lilypond) %s""" % version) def get_option_parser(): p = ly.get_option_parser(usage=_("%s [OPTION]... FILE") % 'abc2ly', description=_('''abc2ly converts ABC music files (see %s) to LilyPond input. ''') % 'http://abcnotation.com/abc2mtex/abc.txt', add_help_option=False) p.version = "abc2ly (LilyPond) @TOPLEVEL_VERSION@" p.add_option("--version", action="version", help=_("show version number and exit")) p.add_option("-h", "--help", action="help", help=_("show this help and exit")) p.add_option("-o", "--output", metavar='FILE', action="store", help=_("write output to FILE")) p.add_option("-s", "--strict", action="store_true", help=_("be strict about success")) p.add_option('-b', '--beams', action="store_true", help=_("preserve ABC's notion of beams")) p.add_option('-q', '--quiet', action="store_true", help=_("suppress progress messages")) p.add_option_group('', description=( _('Report bugs via %s') % 'bug-lilypond@gnu.org') + '\n') return p option_parser = get_option_parser() (global_options, files) = option_parser.parse_args() identify() header['tagline'] = 'Lily was here %s -- automatically converted from ABC' % version for f in files: if f == '-': f = '' if not global_options.quiet: sys.stderr.write('Parsing `%s\'...\n' % f) parse_file(f) if not global_options.output: global_options.output = os.path.basename( os.path.splitext(f)[0]) + ".ly" if not global_options.quiet: sys.stderr.write('lilypond output to: `%s\'...' % global_options.output) outf = open(global_options.output, 'w', encoding='utf8') # don't substitute @VERSION@. We want this to reflect # the last version that was verified to work. outf.write('\\version "2.7.40"\n') # dump_global (outf) dump_header(outf, header) dump_slyrics(outf) dump_voices(outf) dump_score(outf) dump_lyrics(outf) if not global_options.quiet: sys.stderr.write('\n')