k-l-lambda's picture
added node-addon-lilypond
f65fe85
#!@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 <[email protected]>
# Jan Nieuwenhuizen <[email protected]>
#
# 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 <http://www.gnu.org/licenses/>.
#
# ...
#
# (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 <[email protected]>, Laura Conrad
<[email protected]>, Roy Rankin <Roy.Rankin@@alcatel.com.au>.
"""
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')
% '[email protected]') + '\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')