X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=scripts%2Fmidi2ly.py;h=9c490a5f2caf95bd737fee5046878054f69ab364;hb=HEAD;hp=2d7c22c37552a6b7def8274663fce6c3673794f3;hpb=c0748184ac78681dfca4c2116b6aa4dcb41adf9a;p=lilypond.git diff --git a/scripts/midi2ly.py b/scripts/midi2ly.py index 2d7c22c375..9c490a5f2c 100644 --- a/scripts/midi2ly.py +++ b/scripts/midi2ly.py @@ -1,1111 +1,1210 @@ -#!@PYTHON@ +#!@TARGET_PYTHON@ # # midi2ly.py -- LilyPond midi import script -# -# source file of the GNU LilyPond music typesetter + +# This file is part of LilyPond, the GNU music typesetter. +# +# Copyright (C) 1998--2015 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. # -# (c) 1998--2005 Han-Wen Nienhuys -# Jan Nieuwenhuizen +# You should have received a copy of the GNU General Public License +# along with LilyPond. If not, see . ''' TODO: - * test on weird and unquantised midi input (lily-devel) - * drop c++ midi2ly - * update doc and manpage - - * simply insert clef changes whenever too many ledger lines - [to avoid tex capacity exceeded] - * do not ever quant skips - * better lyrics handling - * [see if it is feasible to] move ly-classes to library for use in - other converters, while leaving midi specific stuff here ''' -import getopt import os -import string import sys +""" +@relocate-preamble@ +""" -################################################################ -# Users of python modules should include this snippet. -# -# This soon to be removed for: import lilypond.lilylib as ly -libdir = '@local_lilypond_libdir@' -if not os.path.isdir (libdir): - libdir = '@lilypond_libdir@' -sys.path.insert (0, os.path.join (libdir, 'python')) - - -################################################################ - -import midi - +import lilylib as ly +global _;_=ly._ ################################################################ -################ CONSTANTS +## CONSTANTS -output_name = '' LINE_BELL = 60 -scale_steps = [0,2,4,5,7,9,11] +scale_steps = [0, 2, 4, 5, 7, 9, 11] +global_options = None clocks_per_1 = 1536 clocks_per_4 = 0 -key = 0 -time = 0 + +time = None reference_note = 0 -start_quant = 0 start_quant_clocks = 0 -duration_quant = 0 + duration_quant_clocks = 0 -allowed_tuplets = [] allowed_tuplet_clocks = [] -absolute_p = 0 -explicit_durations_p = 0 -text_lyrics_p = 0 - - +bar_max = 0 ################################################################ -localedir = '@localedir@' -try: - import gettext - gettext.bindtextdomain ('lilypond', localedir) - gettext.textdomain ('lilypond') - _ = gettext.gettext -except: - def _ (s): - return s program_name = sys.argv[0] program_version = '@TOPLEVEL_VERSION@' -errorport = sys.stderr -verbose_p = 0 - -# temp_dir = os.path.join (original_dir, '%s.dir' % program_name) -# original_dir = os.getcwd () -# keep_temp_dir_p = 0 - - -help_summary = _ ("Convert MIDI to LilyPond source.") - -option_definitions = [ - ('', 'a', 'absolute-pitches', _ ("print absolute pitches")), - (_ ("DUR"), 'd', 'duration-quant', _ ("quantise note durations on DUR")), - ('', 'e', 'explicit-durations', _ ("print explicit durations")), - ('', 'h', 'help', _ ("print this help")), - (_ ("ALT[:MINOR]"), 'k', 'key', _ ("set key: ALT=+sharps|-flats; MINOR=1")), - (_ ("FILE"), 'o', 'output', _ ("write output to FILE")), - (_ ("DUR"), 's', 'start-quant', _ ("quantise note starts on DUR")), - (_ ("DUR*NUM/DEN"), 't', 'allow-tuplet', _ ("allow tuplet durations DUR*NUM/DEN")), - ('', 'V', 'verbose', _ ("be verbose")), - ('', 'v', 'version', _ ("print version number")), - ('', 'w', 'warranty', _ ("show warranty and copyright")), - ('', 'x', 'text-lyrics', _ ("treat every text as a lyric")), - ] - -################################################################ -# lilylib.py -- options and stuff -# -# source file of the GNU LilyPond music typesetter - -import os - -try: - import gettext - gettext.bindtextdomain ('lilypond', localedir) - gettext.textdomain ('lilypond') - _ = gettext.gettext -except: - def _ (s): - return s +authors = ('Jan Nieuwenhuizen ', + 'Han-Wen Nienhuys ') -if program_version == '@' + 'TOPLEVEL_VERSION' + '@': - program_version = '1.5.17' +errorport = sys.stderr def identify (): - sys.stdout.write ('%s (GNU LilyPond) %s\n' % (program_name, program_version)) + sys.stdout.write ('%s (GNU LilyPond) %s\n' % (program_name, program_version)) def warranty (): - identify () - sys.stdout.write ('\n') - sys.stdout.write (_ ('Copyright (c) %s by') % ' 2001--2005') - sys.stdout.write ('\n') - sys.stdout.write (' Han-Wen Nienhuys') - sys.stdout.write (' Jan Nieuwenhuizen') - sys.stdout.write ('\n\n') - sys.stdout.write (_ ("Distributed under terms of the GNU General Public License.")) - sys.stdout.write (_ ("It comes with NO WARRANTY.")) - sys.stdout.write ('\n') + identify () + ly.encoded_write (sys.stdout, ''' +%s + + %s + +%s +%s +''' % ( _ ('Copyright (c) %s by') % '1998--2015', + '\n '.join (authors), + _ ('Distributed under terms of the GNU General Public License.'), + _ ('It comes with NO WARRANTY.'))) def progress (s): - errorport.write (s + '\n') + ly.encoded_write (errorport, s + '\n') def warning (s): - progress (_ ("warning: ") + s) - -def error (s): - + progress (_ ("warning: ") + s) - '''Report the error S. Exit by raising an exception. Please - do not abuse by trying to catch this error. If you do not want - a stack trace, write to the output directly. - - RETURN VALUE - - None - - ''' - - progress (_ ("error: ") + s) - raise _ ("Exiting ... ") - -def getopt_args (opts): - '''Construct arguments (LONG, SHORT) for getopt from list of options.''' - short = '' - long = [] - for o in opts: - if o[1]: - short = short + o[1] - if o[0]: - short = short + ':' - if o[2]: - l = o[2] - if o[0]: - l = l + '=' - long.append (l) - return (short, long) - -def option_help_str (o): - '''Transform one option description (4-tuple ) into neatly formatted string''' - sh = ' ' - if o[1]: - sh = '-%s' % o[1] - - sep = ' ' - if o[1] and o[2]: - sep = ', ' - - long = '' - if o[2]: - long= '--%s' % o[2] - - arg = '' - if o[0]: - if o[2]: - arg = '=' - arg = arg + o[0] - return ' ' + sh + sep + long + arg - - -def options_help_str (opts): - '''Convert a list of options into a neatly formatted string''' - w = 0 - strs =[] - helps = [] - - for o in opts: - s = option_help_str (o) - strs.append ((s, o[3])) - if len (s) > w: - w = len (s) - - str = '' - for s in strs: - str = str + '%s%s%s\n' % (s[0], ' ' * (w - len(s[0]) + 3), s[1]) - return str - -def help (): - ls = [(_ ("Usage: %s [OPTIONS]... FILE") % program_name), - ('\n\n'), - (help_summary), - ('\n\n'), - (_ ("Options:")), - ('\n'), - (options_help_str (option_definitions)), - ('\n\n'), - (_ ("Report bugs to %s.") % 'bug-lilypond@gnu.org'), - ('\n')] - map (sys.stdout.write, ls) - -def setup_temp (): - """ - Create a temporary directory, and return its name. - """ - global temp_dir - if not keep_temp_dir_p: - temp_dir = tempfile.mktemp (program_name) - try: - os.mkdir (temp_dir, 0777) - except OSError: - pass - - return temp_dir +def error (s): + progress (_ ("error: ") + s) + raise Exception (_ ("Exiting... ")) +def debug (s): + if global_options.debug: + progress ("debug: " + s) def system (cmd, ignore_error = 0): - """Run CMD. If IGNORE_ERROR is set, don't complain when CMD returns non zero. - - RETURN VALUE - - Exit status of CMD - """ - - if verbose_p: - progress (_ ("Invoking `%s\'") % cmd) - st = os.system (cmd) - if st: - name = re.match ('[ \t]*([^ \t]*)', cmd).group (1) - msg = name + ': ' + _ ("command exited with value %d") % st - if ignore_error: - warning (msg + ' ' + _ ("(ignored)") + ' ') - else: - error (msg) - - return st - - -def cleanup_temp (): - if not keep_temp_dir_p: - if verbose_p: - progress (_ ("Cleaning %s...") % temp_dir) - shutil.rmtree (temp_dir) - + return ly.system (cmd, ignore_error=ignore_error) def strip_extension (f, ext): - (p, e) = os.path.splitext (f) - if e == ext: - e = '' - return p + e - -################################################################ -# END Library -################################################################ - - + (p, e) = os.path.splitext (f) + if e == ext: + e = '' + return p + e class Duration: - allowed_durs = (1, 2, 4, 8, 16, 32, 64, 128) - def __init__ (self, clocks): - self.clocks = clocks - if clocks <= 0: - self.clocks = duration_quant_clocks - (self.dur, self.num, self.den) = self.dur_num_den (clocks) - - def dur_num_den (self, clocks): - for i in range (len (allowed_tuplet_clocks)): - if clocks == allowed_tuplet_clocks[i]: - return allowed_tuplets[i] - - dur = 0; num = 1; den = 1; - g = gcd (clocks, clocks_per_1) - if g: - (dur, num) = (clocks_per_1 / g, clocks / g) - if not dur in self.allowed_durs: - dur = 4; num = clocks; den = clocks_per_4 - return (dur, num, den) - - def dump (self): - if self.den == 1: - if self.num == 1: - s = '%d' % self.dur - elif self.num == 3 and self.dur != 1: - s = '%d.' % (self.dur / 2) - else: - s = '%d*%d' % (self.dur, self.num) - else: - s = '%d*%d/%d' % (self.dur, self.num, self.den) - - global reference_note - reference_note.duration = self - - return s - - def compare (self, other): - return self.clocks - other.clocks + allowed_durs = (1, 2, 4, 8, 16, 32, 64, 128) + def __init__ (self, clocks): + self.clocks = clocks + (self.dur, self.num, self.den) = self.dur_num_den (clocks) + + def dur_num_den (self, clocks): + for i in range (len (allowed_tuplet_clocks)): + if clocks == allowed_tuplet_clocks[i]: + return global_options.allowed_tuplets[i] + + dur = 0; num = 1; den = 1; + g = gcd (clocks, clocks_per_1) + if g: + (dur, num) = (clocks_per_1 / g, clocks / g) + if not dur in self.allowed_durs: + dur = 4; num = clocks; den = clocks_per_4 + return (dur, num, den) + + def dump (self): + if self.den == 1: + if self.num == 1: + s = '%d' % self.dur + elif self.num == 3 and self.dur != 1: + s = '%d.' % (self.dur / 2) + else: + s = '%d*%d' % (self.dur, self.num) + else: + s = '%d*%d/%d' % (self.dur, self.num, self.den) + + global reference_note + reference_note.duration = self + + return s + + def compare (self, other): + return self.clocks - other.clocks def sign (x): - if x >= 0: - return 1 - else: - return -1 + if x >= 0: + return 1 + else: + return -1 class Note: - names = (0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6) - alterations = (0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0) - alteration_names = ('eses', 'es', '', 'is' , 'isis') - - def __init__ (self, clocks, pitch, velocity): - self.pitch = pitch - self.velocity = velocity - # hmm - self.clocks = clocks - self.duration = Duration (clocks) - (self.octave, self.notename, self.alteration) = self.o_n_a () - - def o_n_a (self): - # major scale: do-do - # minor scale: la-la (= + 5) ''' - - n = self.names[(self.pitch) % 12] - a = self.alterations[(self.pitch) % 12] - - if a and key.flats: - a = - self.alterations[(self.pitch) % 12] - n = (n - a) % 7 - - # By tradition, all scales now consist of a sequence - # of 7 notes each with a distinct name, from amongst - # a b c d e f g. But, minor scales have a wide - # second interval at the top - the 'leading note' is - # sharped. (Why? it just works that way! Anything - # else doesn't sound as good and isn't as flexible at - # saying things. In medieval times, scales only had 6 - # notes to avoid this problem - the hexachords.) - - # So, the d minor scale is d e f g a b-flat c-sharp d - # - using d-flat for the leading note would skip the - # name c and duplicate the name d. Why isn't c-sharp - # put in the key signature? Tradition. (It's also - # supposedly based on the Pythagorean theory of the - # cycle of fifths, but that really only applies to - # major scales...) Anyway, g minor is g a b-flat c d - # e-flat f-sharp g, and all the other flat minor keys - # end up with a natural leading note. And there you - # have it. - - # John Sankey - # - # Let's also do a-minor: a b c d e f gis a - # - # --jcn - - o = self.pitch / 12 - 4 - - if key.minor: - # as -> gis - if key.sharps == 0 and key.flats == 0 \ - and n == 5 and a == -1: - n = 4; a = 1 - # des -> cis - elif key.flats == 1 and n == 1 and a == -1: - n = 0; a = 1 - # ges -> fis - elif key.flats == 2 and n == 4 and a == -1: - n = 3; a = 1 - # g -> fisis - elif key.sharps == 5 and n == 4 and a == 0: - n = 3; a = 2 - # d -> cisis - elif key.sharps == 6 and n == 1 and a == 0: - n = 0; a = 2 - # a -> gisis - elif key.sharps == 7 and n == 5 and a == 0: - n = 4; a = 2 - - # b -> ces - if key.flats >= 6 and n == 6 and a == 0: - n = 0; a = -1; o = o + 1 - # e -> fes - if key.flats >= 7 and n == 2 and a == 0: - n = 3; a = -1 - - # f -> eis - if key.sharps >= 3 and n == 3 and a == 0: - n = 2; a = 1 - # c -> bis - if key.sharps >= 4 and n == 0 and a == 0: - n = 6; a = 1; o = o - 1 - - return (o, n, a) - - def dump (self, dump_dur = 1): - global reference_note - s = chr ((self.notename + 2) % 7 + ord ('a')) - s = s + self.alteration_names[self.alteration + 2] - if absolute_p: - commas = self.octave - else: - delta = self.pitch - reference_note.pitch - commas = sign (delta) * (abs (delta) / 12) - if ((sign (delta) \ - * (self.notename - reference_note.notename) + 7) \ - % 7 >= 4) \ - or ((self.notename == reference_note.notename) \ - and (abs (delta) > 4) and (abs (delta) < 12)): - commas = commas + sign (delta) - - if commas > 0: - s = s + "'" * commas - elif commas < 0: - s = s + "," * -commas - - ## FIXME: compile fix --jcn - if dump_dur and (explicit_durations_p \ - or Duration.compare (self.duration, - reference_note.duration)): - s = s + self.duration.dump () - - reference_note = self - - # TODO: move space - return s + ' ' + names = (0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6) + alterations = (0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0) + alteration_names = ('eses', 'es', '', 'is' , 'isis') + def __init__ (self, clocks, pitch, velocity): + self.pitch = pitch + self.velocity = velocity + # hmm + self.clocks = clocks + self.duration = Duration (clocks) + (self.octave, self.notename, self.alteration) = self.o_n_a () + + def o_n_a (self): + # major scale: do-do + # minor scale: la-la (= + 5) ''' + + n = self.names[(self.pitch) % 12] + a = self.alterations[(self.pitch) % 12] + + key = global_options.key + if not key: + key = Key (0, 0, 0) + + if a and key.flats: + a = - self.alterations[(self.pitch) % 12] + n = (n - a) % 7 + + # By tradition, all scales now consist of a sequence + # of 7 notes each with a distinct name, from amongst + # a b c d e f g. But, minor scales have a wide + # second interval at the top - the 'leading note' is + # sharped. (Why? it just works that way! Anything + # else doesn't sound as good and isn't as flexible at + # saying things. In medieval times, scales only had 6 + # notes to avoid this problem - the hexachords.) + + # So, the d minor scale is d e f g a b-flat c-sharp d + # - using d-flat for the leading note would skip the + # name c and duplicate the name d. Why isn't c-sharp + # put in the key signature? Tradition. (It's also + # supposedly based on the Pythagorean theory of the + # cycle of fifths, but that really only applies to + # major scales...) Anyway, g minor is g a b-flat c d + # e-flat f-sharp g, and all the other flat minor keys + # end up with a natural leading note. And there you + # have it. + + # John Sankey + # + # Let's also do a-minor: a b c d e f gis a + # + # --jcn + + o = self.pitch / 12 - 4 + + if key.minor: + # as -> gis + if (key.sharps == 0 and key.flats == 0 + and n == 5 and a == -1): + n = 4; a = 1 + # des -> cis + elif key.flats == 1 and n == 1 and a == -1: + n = 0; a = 1 + # ges -> fis + elif key.flats == 2 and n == 4 and a == -1: + n = 3; a = 1 + # g -> fisis + elif key.sharps == 5 and n == 4 and a == 0: + n = 3; a = 2 + # d -> cisis + elif key.sharps == 6 and n == 1 and a == 0: + n = 0; a = 2 + # a -> gisis + elif key.sharps == 7 and n == 5 and a == 0: + n = 4; a = 2 + + # b -> ces + if key.flats >= 6 and n == 6 and a == 0: + n = 0; a = -1; o = o + 1 + # e -> fes + if key.flats >= 7 and n == 2 and a == 0: + n = 3; a = -1 + + # f -> eis + if key.sharps >= 3 and n == 3 and a == 0: + n = 2; a = 1 + # c -> bis + if key.sharps >= 4 and n == 0 and a == 0: + n = 6; a = 1; o = o - 1 + + return (o, n, a) + + def __repr__ (self): + s = chr ((self.notename + 2) % 7 + ord ('a')) + return 'Note(%s %s)' % (s, self.duration.dump ()) + + def dump (self, dump_dur=True): + global reference_note + s = chr ((self.notename + 2) % 7 + ord ('a')) + s = s + self.alteration_names[self.alteration + 2] + if global_options.absolute_pitches: + commas = self.octave + else: + delta = self.pitch - reference_note.pitch + commas = sign (delta) * (abs (delta) / 12) + if (((sign (delta) + * (self.notename - reference_note.notename) + 7) + % 7 >= 4) + or ((self.notename == reference_note.notename) + and (abs (delta) > 4) and (abs (delta) < 12))): + commas = commas + sign (delta) + + if commas > 0: + s = s + "'" * commas + elif commas < 0: + s = s + "," * -commas + + if (dump_dur + and (self.duration.compare (reference_note.duration) + or global_options.explicit_durations)): + s = s + self.duration.dump () + + # Chords need to handle their reference duration themselves + + reference_note = self + + # TODO: move space + return s + ' ' class Time: - def __init__ (self, num, den): - self.clocks = 0 - self.num = num - self.den = den - - def bar_clocks (self): - return clocks_per_1 * self.num / self.den - - def dump (self): - global time - time = self - return '\n ' + '\\time %d/%d ' % (self.num, self.den) + '\n ' + def __init__ (self, num, den): + self.clocks = 0 + self.num = num + self.den = den + + def bar_clocks (self): + return clocks_per_1 * self.num / self.den + + def __repr__ (self): + return 'Time(%d/%d)' % (self.num, self.den) + + def dump (self): + global time + time = self + return '\n ' + '\\time %d/%d ' % (self.num, self.den) + '\n ' class Tempo: - def __init__ (self, seconds_per_1): - self.clocks = 0 - self.seconds_per_1 = seconds_per_1 + def __init__ (self, seconds_per_1): + self.clocks = 0 + self.seconds_per_1 = seconds_per_1 - def dump (self): - return '\n ' + '\\tempo 4 = %d ' % (4 * 60 / self.seconds_per_1) + '\n ' + def __repr__ (self): + return 'Tempo(%d)' % self.bpm () + + def bpm (self): + return 4 * 60 / self.seconds_per_1 + + def dump (self): + return '\n ' + '\\tempo 4 = %d ' % (self.bpm ()) + '\n ' class Clef: - clefs = ('"bass_8"', 'bass', 'violin', '"violin^8"') - def __init__ (self, type): - self.type = type - - def dump (self): - return '\n \\clef %s\n ' % self.clefs[self.type] + clefs = ('"bass_8"', 'bass', 'violin', '"violin^8"') + def __init__ (self, type): + self.type = type + + def __repr__ (self): + return 'Clef(%s)' % self.clefs[self.type] + + def dump (self): + return '\n \\clef %s\n ' % self.clefs[self.type] class Key: - key_sharps = ('c', 'g', 'd', 'a', 'e', 'b', 'fis') - key_flats = ('BUG', 'f', 'bes', 'es', 'as', 'des', 'ges') - - def __init__ (self, sharps, flats, minor): - self.clocks = 0 - self.flats = flats - self.sharps = sharps - self.minor = minor - - def dump (self): - global key - key = self - - s = '' - if self.sharps and self.flats: - pass - else: - if self.flats: - k = (ord ('cfbeadg'[self.flats % 7]) - ord ('a') - 2 -2 * self.minor + 7) % 7 - else: - k = (ord ('cgdaebf'[self.sharps % 7]) - ord ('a') - 2 -2 * self.minor + 7) % 7 - - if not self.minor: - name = chr ((k + 2) % 7 + ord ('a')) - else: - name = chr ((k + 2) % 7 + ord ('a')) - - # fis cis gis dis ais eis bis - sharps = (2, 4, 6, 1, 3, 5, 7) - # bes es as des ges ces fes - flats = (6, 4, 2, 7, 5, 3, 1) - a = 0 - if self.flats: - if flats[k] <= self.flats: - a = -1 - else: - if sharps[k] <= self.sharps: - a = 1 - - if a: - name = name + Note.alteration_names[a + 2] - - s = '\\key ' + name - if self.minor: - s = s + ' \\minor' - else: - s = s + ' \\major' - - return '\n\n ' + s + '\n ' + key_sharps = ('c', 'g', 'd', 'a', 'e', 'b', 'fis') + key_flats = ('BUG', 'f', 'bes', 'es', 'as', 'des', 'ges') + + def __init__ (self, sharps, flats, minor): + self.clocks = 0 + self.flats = flats + self.sharps = sharps + self.minor = minor + + def dump (self): + global_options.key = self + + s = '' + if self.sharps and self.flats: + pass + else: + if self.flats: + k = (ord ('cfbeadg'[self.flats % 7]) - ord ('a') - 2 -2 * self.minor + 7) % 7 + else: + k = (ord ('cgdaebf'[self.sharps % 7]) - ord ('a') - 2 -2 * self.minor + 7) % 7 + + if not self.minor: + name = chr ((k + 2) % 7 + ord ('a')) + else: + name = chr ((k + 2) % 7 + ord ('a')) + + # fis cis gis dis ais eis bis + sharps = (2, 4, 6, 1, 3, 5, 7) + # bes es as des ges ces fes + flats = (6, 4, 2, 7, 5, 3, 1) + a = 0 + if self.flats: + if flats[k] <= self.flats: + a = -1 + else: + if sharps[k] <= self.sharps: + a = 1 + + if a: + name = name + Note.alteration_names[a + 2] + + s = '\\key ' + name + if self.minor: + s = s + ' \\minor' + else: + s = s + ' \\major' + + return '\n\n ' + s + '\n ' class Text: - text_types = ( - 'SEQUENCE_NUMBER', - 'TEXT_EVENT', - 'COPYRIGHT_NOTICE', - 'SEQUENCE_TRACK_NAME', - 'INSTRUMENT_NAME', - 'LYRIC', - 'MARKER', - 'CUE_POINT',) - - def __init__ (self, type, text): - self.clocks = 0 - self.type = type - self.text = text - - def dump (self): - # urg, we should be sure that we're in a lyrics staff - if self.type == midi.LYRIC: - s = '"%s"' % self.text - d = Duration (self.clocks) - if explicit_durations_p \ - or Duration.compare (d, - reference_note.duration): - s = s + Duration (self.clocks).dump () - s = s + ' ' - else: - s = '\n % [' + self.text_types[self.type] + '] ' + self.text + '\n ' - return s - - -def split_track (track): - chs = {} - for i in range(16): - chs[i] = [] - - for e in track: - data = list (e[1]) - if data[0] > 0x7f and data[0] < 0xf0: - c = data[0] & 0x0f - e = (e[0], tuple ([data[0] & 0xf0] + data[1:])) - chs[c].append (e) - else: - chs[0].append (e) - - for i in range (16): - if chs[i] == []: - del chs[i] - - threads = [] - for v in chs.values (): - events = events_on_channel (v) - thread = unthread_notes (events) - if len (thread): - threads.append (thread) - return threads - + text_types = ( + 'SEQUENCE_NUMBER', + 'TEXT_EVENT', + 'COPYRIGHT_NOTICE', + 'SEQUENCE_TRACK_NAME', + 'INSTRUMENT_NAME', + 'LYRIC', + 'MARKER', + 'CUE_POINT', + 'PROGRAM_NAME', + 'DEVICE_NAME', ) + + @staticmethod + def _text_only(chr): + if ((' ' <= chr <= '~') or chr in ['\n','\r']): + return chr + else: + return '~' + + def __init__ (self, type, text): + self.clocks = 0 + self.type = type + self.text =''.join(map(self._text_only, text)) + + def dump (self): + # urg, we should be sure that we're in a lyrics staff + s = '' + if self.type == midi.LYRIC: + s = '"%s"' % self.text + d = Duration (self.clocks) + if (global_options.explicit_durations + or d.compare (reference_note.duration)): + s = s + Duration (self.clocks).dump () + s = s + ' ' + elif (self.text.strip () + and self.type == midi.SEQUENCE_TRACK_NAME + and not self.text == 'control track' + and not self.track.lyrics_p_): + text = self.text.replace ('(MIDI)', '').strip () + if text: + s = '\n \\set Staff.instrumentName = "%(text)s"\n ' % locals () + elif self.text.strip (): + s = '\n % [' + self.text_types[self.type] + '] ' + self.text + '\n ' + return s + + def __repr__ (self): + return 'Text(%d=%s)' % (self.type, self.text) + +def get_voice (channel, music): + debug ('channel: ' + str (channel) + '\n') + return unthread_notes (music) + +class Channel: + def __init__ (self, number): + self.number = number + self.events = [] + self.music = None + def add (self, event): + self.events.append (event) + def get_voice (self): + if not self.music: + self.music = self.parse () + return get_voice (self.number, self.music) + def parse (self): + pitches = {} + notes = [] + music = [] + last_lyric = 0 + last_time = 0 + for e in self.events: + t = e[0] + + if start_quant_clocks: + t = quantise_clocks (t, start_quant_clocks) + + if (e[1][0] == midi.NOTE_OFF + or (e[1][0] == midi.NOTE_ON and e[1][2] == 0)): + debug ('%d: NOTE OFF: %s' % (t, e[1][1])) + if not e[1][2]: + debug (' ...treated as OFF') + end_note (pitches, notes, t, e[1][1]) + + elif e[1][0] == midi.NOTE_ON: + if not pitches.has_key (e[1][1]): + debug ('%d: NOTE ON: %s' % (t, e[1][1])) + pitches[e[1][1]] = (t, e[1][2]) + else: + debug ('...ignored') + + # all include ALL_NOTES_OFF + elif (e[1][0] >= midi.ALL_SOUND_OFF + and e[1][0] <= midi.POLY_MODE_ON): + for i in pitches: + end_note (pitches, notes, t, i) + + elif e[1][0] == midi.META_EVENT: + if e[1][1] == midi.END_OF_TRACK: + for i in pitches: + end_note (pitches, notes, t, i) + break + + elif e[1][1] == midi.SET_TEMPO: + (u0, u1, u2) = map (ord, e[1][2]) + us_per_4 = u2 + 256 * (u1 + 256 * u0) + seconds_per_1 = us_per_4 * 4 / 1e6 + music.append ((t, Tempo (seconds_per_1))) + elif e[1][1] == midi.TIME_SIGNATURE: + (num, dur, clocks4, count32) = map (ord, e[1][2]) + den = 2 ** dur + music.append ((t, Time (num, den))) + elif e[1][1] == midi.KEY_SIGNATURE: + (alterations, minor) = map (ord, e[1][2]) + sharps = 0 + flats = 0 + if alterations < 127: + sharps = alterations + else: + flats = 256 - alterations + + k = Key (sharps, flats, minor) + if not t and global_options.key: + # At t == 0, a set --key overrides us + k = global_options.key + music.append ((t, k)) + + # ugh, must set key while parsing + # because Note init uses key + # Better do Note.calc () at dump time? + global_options.key = k + + elif (e[1][1] == midi.LYRIC + or (global_options.text_lyrics + and e[1][1] == midi.TEXT_EVENT)): + self.lyrics_p_ = True + if last_lyric: + last_lyric.clocks = t - last_time + music.append ((last_time, last_lyric)) + last_time = t + last_lyric = Text (midi.LYRIC, e[1][2]) + + elif (e[1][1] >= midi.SEQUENCE_NUMBER + and e[1][1] <= midi.CUE_POINT): + text = Text (e[1][1], e[1][2]) + text.track = self + music.append ((t, text)) + if (text.type == midi.SEQUENCE_TRACK_NAME): + self.name = text.text + else: + if global_options.verbose: + sys.stderr.write ("SKIP: %s\n" % `e`) + else: + if global_options.verbose: + sys.stderr.write ("SKIP: %s\n" % `e`) + + if last_lyric: + # last_lyric.clocks = t - last_time + # hmm + last_lyric.clocks = clocks_per_4 + music.append ((last_time, last_lyric)) + last_lyric = 0 + + i = 0 + while len (notes): + if i < len (music) and notes[0][0] >= music[i][0]: + i = i + 1 + else: + music.insert (i, notes[0]) + del notes[0] + return music + +class Track (Channel): + def __init__ (self): + Channel.__init__ (self, None) + self.name = None + self.channels = {} + self.lyrics_p_ = False + def _add (self, event): + self.events.append (event) + def add (self, event, channel=None): + if channel == None: + self._add (event) + else: + self.channels[channel] = self.channels.get (channel, Channel (channel)) + self.channels[channel].add (event) + def get_voices (self): + return ([self.get_voice ()] + + [self.channels[k].get_voice () + for k in sorted (self.channels.keys ())]) + +def create_track (events): + track = Track () + for e in events: + data = list (e[1]) + if data[0] > 0x7f and data[0] < 0xf0: + channel = data[0] & 0x0f + e = (e[0], tuple ([data[0] & 0xf0] + data[1:])) + track.add (e, channel) + else: + track.add (e) + return track def quantise_clocks (clocks, quant): - q = int (clocks / quant) * quant - if q != clocks: - for tquant in allowed_tuplet_clocks: - if int (clocks / tquant) * tquant == clocks: - return clocks - if 2 * (clocks - q) > quant: - q = q + quant - return q + q = int (clocks / quant) * quant + if q != clocks: + for tquant in allowed_tuplet_clocks: + if int (clocks / tquant) * tquant == clocks: + return clocks + if 2 * (clocks - q) > quant: + q = q + quant + return q def end_note (pitches, notes, t, e): - try: - (lt, vel) = pitches[e] - del pitches[e] - - i = len (notes) - 1 - while i > 0: - if notes[i][0] > lt: - i = i -1 - else: - break - d = t - lt - if duration_quant_clocks: - d = quantise_clocks (d, duration_quant_clocks) - if not d: - d = duration_quant_clocks - - notes.insert (i + 1, - (lt, Note (d, e, vel))) - - except KeyError: - pass - -def events_on_channel (channel): - pitches = {} - - notes = [] - events = [] - last_lyric = 0 - last_time = 0 - for e in channel: - t = e[0] - - if start_quant_clocks: - t = quantise_clocks (t, start_quant_clocks) - - - if e[1][0] == midi.NOTE_OFF \ - or (e[1][0] == midi.NOTE_ON and e[1][2] == 0): - end_note (pitches, notes, t, e[1][1]) - - elif e[1][0] == midi.NOTE_ON: - if not pitches.has_key (e[1][1]): - pitches[e[1][1]] = (t, e[1][2]) - - # all include ALL_NOTES_OFF - elif e[1][0] >= midi.ALL_SOUND_OFF \ - and e[1][0] <= midi.POLY_MODE_ON: - for i in pitches.keys (): - end_note (pitches, notes, t, i) - - elif e[1][0] == midi.META_EVENT: - if e[1][1] == midi.END_OF_TRACK: - for i in pitches.keys (): - end_note (pitches, notes, t, i) - break - - elif e[1][1] == midi.SET_TEMPO: - (u0, u1, u2) = map (ord, e[1][2]) - us_per_4 = u2 + 256 * (u1 + 256 * u0) - seconds_per_1 = us_per_4 * 4 / 1e6 - events.append ((t, Tempo (seconds_per_1))) - elif e[1][1] == midi.TIME_SIGNATURE: - (num, dur, clocks4, count32) = map (ord, e[1][2]) - den = 2 ** dur - events.append ((t, Time (num, den))) - elif e[1][1] == midi.KEY_SIGNATURE: - (alterations, minor) = map (ord, e[1][2]) - sharps = 0 - flats = 0 - if alterations < 127: - sharps = alterations - else: - flats = 256 - alterations - - k = Key (sharps, flats, minor) - events.append ((t, k)) - - # ugh, must set key while parsing - # because Note init uses key - # Better do Note.calc () at dump time? - global key - key = k - - elif e[1][1] == midi.LYRIC \ - or (text_lyrics_p and e[1][1] == midi.TEXT_EVENT): - if last_lyric: - last_lyric.clocks = t - last_time - events.append ((last_time, last_lyric)) - last_time = t - last_lyric = Text (midi.LYRIC, e[1][2]) - - elif e[1][1] >= midi.SEQUENCE_NUMBER \ - and e[1][1] <= midi.CUE_POINT: - events.append ((t, Text (e[1][1], e[1][2]))) - else: - if verbose_p: - sys.stderr.write ("SKIP: %s\n" % `e`) - pass - else: - if verbose_p: - sys.stderr.write ("SKIP: %s\n" % `e`) - pass - - if last_lyric: - # last_lyric.clocks = t - last_time - # hmm - last_lyric.clocks = clocks_per_4 - events.append ((last_time, last_lyric)) - last_lyric = 0 - - i = 0 - while len (notes): - if i < len (events) and notes[0][0] >= events[i][0]: - i = i + 1 - else: - events.insert (i, notes[0]) - del notes[0] - return events + try: + (lt, vel) = pitches[e] + del pitches[e] + + i = len (notes) - 1 + while i > 0: + if notes[i][0] > lt: + i = i -1 + else: + break + d = t - lt + if duration_quant_clocks: + d = quantise_clocks (d, duration_quant_clocks) + if not d: + d = duration_quant_clocks + + notes.insert (i + 1, + (lt, Note (d, e, vel))) + + except KeyError: + pass def unthread_notes (channel): - threads = [] - while channel: - thread = [] - end_busy_t = 0 - start_busy_t = 0 - todo = [] - for e in channel: - t = e[0] - if e[1].__class__ == Note \ - and ((t == start_busy_t \ - and e[1].clocks + t == end_busy_t) \ - or t >= end_busy_t): - thread.append (e) - start_busy_t = t - end_busy_t = t + e[1].clocks - elif e[1].__class__ == Time \ - or e[1].__class__ == Key \ - or e[1].__class__ == Text \ - or e[1].__class__ == Tempo: - thread.append (e) - else: - todo.append (e) - threads.append (thread) - channel = todo - - return threads + threads = [] + while channel: + thread = [] + end_busy_t = 0 + start_busy_t = 0 + todo = [] + for e in channel: + t = e[0] + if (e[1].__class__ == Note + and ((t == start_busy_t + and e[1].clocks + t == end_busy_t) + or t >= end_busy_t)): + thread.append (e) + start_busy_t = t + end_busy_t = t + e[1].clocks + elif (e[1].__class__ == Time + or e[1].__class__ == Key + or e[1].__class__ == Text + or e[1].__class__ == Tempo): + thread.append (e) + else: + todo.append (e) + threads.append (thread) + channel = todo + + return threads def gcd (a,b): - if b == 0: - return a - c = a - while c: - c = a % b - a = b - b = c - return a - + if b == 0: + return a + c = a + while c: + c = a % b + a = b + b = c + return a + def dump_skip (skip, clocks): - return skip + Duration (clocks).dump () + ' ' + return skip + Duration (clocks).dump () + ' ' -def dump (self): - return self.dump () +def dump (d): + return d.dump () def dump_chord (ch): - s = '' - notes = [] - for i in ch: - if i.__class__ == Note: - notes.append (i) - else: - s = s + i.dump () - if len (notes) == 1: - s = s + dump (notes[0]) - elif len (notes) > 1: - global reference_note - s = s + '<' - s = s + notes[0].dump (dump_dur = 0) - r = reference_note - for i in notes[1:]: - s = s + i.dump (dump_dur = 0 ) - s = s + '>' - - s = s + notes[0].duration.dump() + ' ' - reference_note = r - return s + s = '' + notes = [] + for i in ch: + if i.__class__ == Note: + notes.append (i) + else: + s = s + i.dump () + if len (notes) == 1: + s = s + dump (notes[0]) + elif len (notes) > 1: + global reference_note + reference_dur = reference_note.duration + s = s + '<' + s = s + notes[0].dump (dump_dur=False) + r = reference_note + for i in notes[1:]: + s = s + i.dump (dump_dur=False) + s = s + '>' + if (r.duration.compare (reference_dur) + or global_options.explicit_durations): + s = s + r.duration.dump () + s = s + ' ' + reference_note = r + return s def dump_bar_line (last_bar_t, t, bar_count): - s = '' - bar_t = time.bar_clocks () - if t - last_bar_t >= bar_t: - bar_count = bar_count + (t - last_bar_t) / bar_t - - if t - last_bar_t == bar_t: - s = '|\n %% %d\n ' % bar_count - last_bar_t = t - else: - # urg, this will barf at meter changes - last_bar_t = last_bar_t + (t - last_bar_t) / bar_t * bar_t - - return (s, last_bar_t, bar_count) - - -def dump_channel (thread, skip): - global key, reference_note, time - - key = Key (0, 0, 0) - time = Time (4, 4) - # urg LilyPond doesn't start at c4, but - # remembers from previous tracks! - # reference_note = Note (clocks_per_4, 4*12, 0) - reference_note = Note (0, 4*12, 0) - last_e = None - chs = [] - ch = [] - - for e in thread: - if last_e and last_e[0] == e[0]: - ch.append (e[1]) - else: - if ch: - chs.append ((last_e[0], ch)) - - ch = [e[1]] - - last_e = e - - if ch: - chs.append ((last_e[0], ch)) - t = 0 - last_t = 0 - last_bar_t = 0 - bar_count = 1 - - lines = [''] - for ch in chs: - t = ch[0] - - i = string.rfind (lines[-1], '\n') + 1 - if len (lines[-1][i:]) > LINE_BELL: - lines.append ('') - - if t - last_t > 0: - lines[-1] = lines[-1] + dump_skip (skip, t-last_t) - elif t - last_t < 0: - errorport.write ('BUG: time skew') - - (s, last_bar_t, bar_count) = dump_bar_line (last_bar_t, - t, bar_count) - lines[-1] = lines[-1] + s - - lines[-1] = lines[-1] + dump_chord (ch[1]) - - clocks = 0 - for i in ch[1]: - if i.clocks > clocks: - clocks = i.clocks - - last_t = t + clocks - - (s, last_bar_t, bar_count) = dump_bar_line (last_bar_t, - last_t, bar_count) - lines[-1] = lines[-1] + s - - return string.join (lines, '\n ') + '\n' - -def track_name (i): - return 'track%c' % (i + ord ('A')) - -def channel_name (i): - return 'channel%c' % (i + ord ('A')) - -def dump_track (channels, n): - s = '\n' - track = track_name (n) - clef = guess_clef (channels) - - for i in range (len (channels)): - channel = channel_name (i) - item = thread_first_item (channels[i]) - - if item and item.__class__ == Note: - skip = 's' - s = s + '%s = ' % (track + channel) - if not absolute_p: - s = s + '\\relative c ' - elif item and item.__class__ == Text: - skip = '" "' - s = s + '%s = \\lyricmode ' % (track + channel) - else: - skip = '\\skip ' - s = s + '%s = ' % (track + channel) - s = s + '{\n' - s = s + ' ' + dump_channel (channels[i][0], skip) - s = s + '}\n\n' - - s = s + '%s = <<\n' % track - - if clef.type != 2: - s = s + clef.dump () + '\n' - - for i in range (len (channels)): - channel = channel_name (i) - item = thread_first_item (channels[i]) - if item and item.__class__ == Text: - s = s + ' \\context Lyrics = %s \\%s\n' % (channel, - track + channel) - else: - s = s + ' \\context Voice = %s \\%s\n' % (channel, - track + channel) - s = s + '>>\n\n' - return s - -def thread_first_item (thread): - for chord in thread: - for event in chord: - if event[1].__class__ == Note \ - or (event[1].__class__ == Text \ - and event[1].type == midi.LYRIC): - return event[1] - return 0 + s = '' + bar_t = time.bar_clocks () + if t - last_bar_t >= bar_t: + bar_count = bar_count + (t - last_bar_t) / bar_t + + if t - last_bar_t == bar_t: + s = '\n | %% %(bar_count)d\n ' % locals () + last_bar_t = t + else: + # urg, this will barf at meter changes + last_bar_t = last_bar_t + (t - last_bar_t) / bar_t * bar_t + + return (s, last_bar_t, bar_count) + + +def dump_voice (thread, skip): + global reference_note, time + ref = Note (0, 4*12, 0) + if not reference_note: + reference_note = ref + else: + ref.duration = reference_note.duration + reference_note = ref + last_e = None + chs = [] + ch = [] + + for e in thread: + if last_e and last_e[0] == e[0]: + ch.append (e[1]) + else: + if ch: + chs.append ((last_e[0], ch)) + + ch = [e[1]] + + last_e = e + + if ch: + chs.append ((last_e[0], ch)) + t = 0 + last_t = 0 + last_bar_t = 0 + bar_count = 1 + + lines = [''] + for ch in chs: + t = ch[0] + + i = lines[-1].rfind ('\n') + 1 + if len (lines[-1][i:]) > LINE_BELL: + lines.append ('') + + if t - last_t > 0: + d = t - last_t + if bar_max and t > time.bar_clocks () * bar_max: + d = time.bar_clocks () * bar_max - last_t + lines[-1] = lines[-1] + dump_skip (skip, d) + elif t - last_t < 0: + errorport.write ('BUG: time skew') + + (s, last_bar_t, bar_count) = dump_bar_line (last_bar_t, + t, bar_count) + + if bar_max and bar_count > bar_max: + break + + lines[-1] = lines[-1] + s + lines[-1] = lines[-1] + dump_chord (ch[1]) + + clocks = 0 + for i in ch[1]: + if i.clocks > clocks: + clocks = i.clocks + + last_t = t + clocks + + (s, last_bar_t, bar_count) = dump_bar_line (last_bar_t, + last_t, bar_count) + lines[-1] = lines[-1] + s + + return '\n '.join (lines) + '\n' + +def number2ascii (i): + s = '' + i += 1 + while i > 0: + m = (i - 1) % 26 + s = '%c' % (m + ord ('A')) + s + i = (i - m)/26 + return s + +def get_track_name (i): + return 'track' + number2ascii (i) + +def get_channel_name (i): + return 'channel' + number2ascii (i) + +def get_voice_name (i, zero_too_p=False): + if i or zero_too_p: + return 'voice' + number2ascii (i) + return '' + +def lst_append (lst, x): + lst.append (x) + return lst + +def get_voice_layout (average_pitch): + d = {} + for i in range (len (average_pitch)): + d[average_pitch[i]] = lst_append (d.get (average_pitch[i], []), i) + s = list (reversed (sorted (average_pitch))) + non_empty = len (filter (lambda x: x, s)) + names = ['One', 'Two'] + if non_empty > 2: + names = ['One', 'Three', 'Four', 'Two'] + layout = map (lambda x: '', range (len (average_pitch))) + for i, n in zip (s, names): + if i: + v = d[i] + if type (v) == list: + d[i] = v[1:] + v = v[0] + layout[v] = n + return layout + +def dump_track (track, n): + s = '\n' + track_name = get_track_name (n) + + average_pitch = track_average_pitch (track) + voices = len (filter (lambda x: x, average_pitch[1:])) + clef = get_best_clef (average_pitch[0]) + + c = 0 + vv = 0 + for channel in track: + v = 0 + channel_name = get_channel_name (c) + c += 1 + for voice in channel: + voice_name = get_voice_name (v) + voice_id = track_name + channel_name + voice_name + item = voice_first_item (voice) + + if item and item.__class__ == Note: + skip = 'r' + if global_options.skip: + skip = 's' + s += '%(voice_id)s = ' % locals () + if not global_options.absolute_pitches: + s += '\\relative c ' + elif item and item.__class__ == Text: + skip = '" "' + s += '%(voice_id)s = \\lyricmode ' % locals () + else: + skip = '\\skip ' + s += '%(voice_id)s = ' % locals () + s += '{\n' + if not n and not vv and global_options.key: + s += global_options.key.dump () + if average_pitch[vv+1] and voices > 1: + vl = get_voice_layout (average_pitch[1:])[vv] + if vl: + s += ' \\voice' + vl + '\n' + else: + if not global_options.quiet: + warning (_ ('found more than 5 voices on a staff, expect bad output')) + s += ' ' + dump_voice (voice, skip) + s += '}\n\n' + v += 1 + vv += 1 + + s += '%(track_name)s = <<\n' % locals () + + if clef.type != 2: + s += clef.dump () + '\n' + + c = 0 + vv = 0 + for channel in track: + v = 0 + channel_name = get_channel_name (c) + c += 1 + for voice in channel: + voice_context_name = get_voice_name (vv, zero_too_p=True) + voice_name = get_voice_name (v) + v += 1 + vv += 1 + voice_id = track_name + channel_name + voice_name + item = voice_first_item (voice) + context = 'Voice' + if item and item.__class__ == Text: + context = 'Lyrics' + s += ' \\context %(context)s = %(voice_context_name)s \\%(voice_id)s\n' % locals () + s += '>>\n\n' + return s + +def voice_first_item (voice): + for event in voice: + if (event[1].__class__ == Note + or (event[1].__class__ == Text + and event[1].type == midi.LYRIC)): + return event[1] + return None + +def channel_first_item (channel): + for voice in channel: + first = voice_first_item (voice) + if first: + return first + return None def track_first_item (track): - for thread in track: - return thread_first_item (thread) - -def guess_clef (track): - i = 0 - p = 0 - for thread in track: - for chord in thread: - for event in chord: - if event[1].__class__ == Note: - i = i + 1 - p = p + event[1].pitch - if i and p / i <= 3*12: - return Clef (0) - elif i and p / i <= 5*12: - return Clef (1) - elif i and p / i >= 7*12: - return Clef (3) - else: - return Clef (2) - - -def convert_midi (f, o): - global clocks_per_1, clocks_per_4, key - - str = open (f).read () - midi_dump = midi.parse (str) - - clocks_per_1 = midi_dump[0][1] - clocks_per_4 = clocks_per_1 / 4 - - global start_quant, start_quant_clocks - if start_quant: - start_quant_clocks = clocks_per_1 / start_quant - - global duration_quant, duration_quant_clocks - if duration_quant: - duration_quant_clocks = clocks_per_1 / duration_quant - - global allowed_tuplet_clocks - allowed_tuplet_clocks = [] - for (dur, num, den) in allowed_tuplets: - allowed_tuplet_clocks.append (clocks_per_1 * num / (dur * den)) - - tracks = [] - for t in midi_dump[1]: - key = Key (0, 0, 0) - tracks.append (split_track (t)) - - tag = '%% Lily was here -- automatically converted by %s from %s' % ( program_name, f) - - - s = '' - s = tag + '\n\\version "2.3.25"\n\n' - for i in range (len (tracks)): - s = s + dump_track (tracks[i], i) - - s = s + '\n\\score {\n <<\n' - for i in range (len (tracks)): - track = track_name (i) - item = track_first_item (tracks[i]) - if item and item.__class__ == Note: - s = s + ' \\context Staff=%s \\%s\n' % (track, track) - elif item and item.__class__ == Text: - s = s + ' \\context Lyrics=%s \\%s\n' % (track, track) - s = s + ' >>\n}\n' - - progress (_ ("%s output to `%s'...") % ('LY', o)) - - if o == '-': - h = sys.stdout - else: - h = open (o, 'w') - - h.write (s) - h.close () - - -(sh, long) = getopt_args (option_definitions) -try: - (options, files) = getopt.getopt(sys.argv[1:], sh, long) -except getopt.error, s: - errorport.write ('\n') - errorport.write (_ ("error: ") + _ ("getopt says: `%s\'" % s)) - errorport.write ('\n') - errorport.write ('\n') - help () - sys.exit (2) - -for opt in options: - o = opt[0] - a = opt[1] - - if 0: - pass - elif o == '--help' or o == '-h': - help () - errorport.write ('\n') - errorport.write (_ ("Example:")) - errorport.write (r''' - midi2ly --key=-2:1 --duration-quant=32 \ - --allow-tuplet=4*2/3 --allow-tuplet=2*4/3 foo.midi -''') - sys.exit (0) - elif o == '--output' or o == '-o': - output_name = a - elif o == '--verbose' or o == '-V': - verbose_p = 1 - elif o == '--version' or o == '-v': - identify () - sys.exit (0) - elif o == '--warranty' or o == '-w': - status = system ('lilypond -w', ignore_error = 1) - if status: - warranty () - sys.exit (0) - - - elif o == '--absolute-pitches' or o == '-a': - absolute_p = 1 - elif o == '--duration-quant' or o == '-d': - duration_quant = string.atoi (a) - elif o == '--explicit-durations' or o == '-e': - explicit_durations_p = 1 - elif o == '--key' or o == '-k': - (alterations, minor) = map (string.atoi, string.split (a + ':0', ':'))[0:2] - sharps = 0 - flats = 0 - if alterations >= 0: - sharps = alterations - else: - flats = - alterations - key = Key (sharps, flats, minor) - elif o == '--start-quant' or o == '-s': - start_quant = string.atoi (a) - elif o == '--allow-tuplet' or o == '-t': - a = string.replace (a, '/', '*') - tuplet = map (string.atoi, string.split (a, '*')) - allowed_tuplets.append (tuplet) - # lots of midi files use plain text for lyric events - elif o == '--text-lyrics' or o == '-x': - text_lyrics_p = 1 - - -if not files or files[0] == '-': - - # FIXME: read from stdin when files[0] = '-' - help () - errorport.write (program_name + ":" + _ ("error: ") + _ ("no files specified on command line.") + '\n') - sys.exit (2) - - -for f in files: - g = f - g = strip_extension (g, '.midi') - g = strip_extension (g, '.mid') - g = strip_extension (g, '.MID') - (outdir, outbase) = ('','') - - if not output_name: - outdir = '.' - outbase = os.path.basename (g) - o = os.path.join (outdir, outbase + '-midi.ly') - elif output_name[-1] == os.sep: - outdir = output_name - outbase = os.path.basename (g) - os.path.join (outdir, outbase + '-gen.ly') - else: - o = output_name - (outdir, outbase) = os.path.split (o) - - if outdir != '.' and outdir != '': - try: - os.mkdir (outdir, 0777) - except OSError: - pass - - convert_midi (f, o) + for channel in track: + first = channel_first_item (channel) + if first: + return first + return None + +def track_average_pitch (track): + i = 0 + p = [0] + v = 1 + for channel in track: + for voice in channel: + c = 0 + p.append (0) + for event in voice: + if event[1].__class__ == Note: + i += 1 + c += 1 + p[v] += event[1].pitch + if c: + p[0] += p[v] + p[v] = p[v] / c + v += 1 + if i: + p[0] = p[0] / i + return p + +def get_best_clef (average_pitch): + if average_pitch: + if average_pitch <= 3*12: + return Clef (0) + elif average_pitch <= 5*12: + return Clef (1) + elif average_pitch >= 7*12: + return Clef (3) + return Clef (2) + +class Staff: + def __init__ (self, track): + self.voices = track.get_voices () + def dump (self, i): + return dump_track (self.voices, i) + +def convert_midi (in_file, out_file): + global midi + import midi + + global clocks_per_1, clocks_per_4, key + global start_quant_clocks + global duration_quant_clocks + global allowed_tuplet_clocks + global time + + str = open (in_file, 'rb').read () + clocks_max = bar_max * clocks_per_1 * 2 + midi_dump = midi.parse (str, clocks_max) + + clocks_per_1 = midi_dump[0][1] + clocks_per_4 = clocks_per_1 / 4 + time = Time (4, 4) + + if global_options.start_quant: + start_quant_clocks = clocks_per_1 / global_options.start_quant + + if global_options.duration_quant: + duration_quant_clocks = clocks_per_1 / global_options.duration_quant + + allowed_tuplet_clocks = [] + for (dur, num, den) in global_options.allowed_tuplets: + allowed_tuplet_clocks.append (clocks_per_1 / dur * num / den) + + if global_options.verbose: + print 'allowed tuplet clocks:', allowed_tuplet_clocks + + tracks = [create_track (t) for t in midi_dump[1]] + # urg, parse all global track events, such as Key first + # this fixes key in different voice/staff problem + for t in tracks: + t.music = t.parse () + prev = None + staves = [] + for t in tracks: + voices = t.get_voices () + if ((t.name and prev and prev.name) + and t.name.split (':')[0] == prev.name.split (':')[0]): + # staves[-1].voices += voices + # all global track events first + staves[-1].voices = ([staves[-1].voices[0]] + + [voices[0]] + + staves[-1].voices[1:] + + voices[1:]) + else: + staves.append (Staff (t)) + prev = t + + tag = '%% Lily was here -- automatically converted by %s from %s' % ( program_name, in_file) + + + s = tag + s += r''' +\version "2.14.0" +''' + + s += r''' +\layout { + \context { + \Voice + \remove "Note_heads_engraver" + \consists "Completion_heads_engraver" + \remove "Rest_engraver" + \consists "Completion_rest_engraver" + } +} +''' + for i in global_options.include_header: + s += '\n%% included from %(i)s\n' % locals () + s += open (i).read () + if s[-1] != '\n': + s += '\n' + s += '% end\n' + + for i, t in enumerate (staves): + s += t.dump (i) + + s += '\n\\score {\n <<\n' + + control_track = False + i = 0 + for i, staff in enumerate (staves): + track_name = get_track_name (i) + item = track_first_item (staff.voices) + staff_name = track_name + context = None + if not i and not item and len (staves) > 1: + control_track = track_name + continue + elif (item and item.__class__ == Note): + context = 'Staff' + if control_track: + s += ' \\context %(context)s=%(staff_name)s \\%(control_track)s\n' % locals () + elif item and item.__class__ == Text: + context = 'Lyrics' + if context: + s += ' \\context %(context)s=%(staff_name)s \\%(track_name)s\n' % locals () + + s = s + ''' >> + \layout {} + \midi {} +} +''' + + if not global_options.quiet: + progress (_ ("%s output to `%s'...") % ('LY', out_file)) + + if out_file == '-': + handle = sys.stdout + else: + handle = open (out_file, 'w') + + handle.write (s) + handle.close () + + +def get_option_parser (): + p = ly.get_option_parser (usage=_ ("%s [OPTION]... FILE") % 'midi2ly', + description=_ ("Convert %s to LilyPond input.\n") % 'MIDI', + add_help_option=False) + + p.add_option ('-a', '--absolute-pitches', + action='store_true', + help=_ ('print absolute pitches')) + p.add_option ('-d', '--duration-quant', + metavar=_ ('DUR'), + help=_ ('quantise note durations on DUR')) + p.add_option ('-D', '--debug', + action='store_true', + help=_ ('debug printing')) + p.add_option ('-e', '--explicit-durations', + action='store_true', + help=_ ('print explicit durations')) + p.add_option('-h', '--help', + action='help', + help=_ ('show this help and exit')) + p.add_option('-i', '--include-header', + help=_ ('prepend FILE to output'), + action='append', + default=[], + metavar=_ ('FILE')) + p.add_option('-k', '--key', help=_ ('set key: ALT=+sharps|-flats; MINOR=1'), + metavar=_ ('ALT[:MINOR]'), + default=None), + p.add_option ('-o', '--output', help=_ ('write output to FILE'), + metavar=_ ('FILE'), + action='store') + p.add_option ('-p', '--preview', help=_ ('preview of first 4 bars'), + action='store_true') + p.add_option ('-q', '--quiet', + action="store_true", + help=_ ("suppress progress messages and warnings about excess voices")) + p.add_option ('-s', '--start-quant',help= _ ('quantise note starts on DUR'), + metavar=_ ('DUR')) + p.add_option ('-S', '--skip', + action = "store_true", + help =_ ("use s instead of r for rests")) + p.add_option ('-t', '--allow-tuplet', + metavar=_ ('DUR*NUM/DEN'), + action = 'append', + dest='allowed_tuplets', + help=_ ('allow tuplet durations DUR*NUM/DEN'), + default=[]) + p.add_option ('-V', '--verbose', help=_ ('be verbose'), + action='store_true') + p.version = 'midi2ly (LilyPond) @TOPLEVEL_VERSION@' + p.add_option ('--version', + action='version', + help=_ ('show version number and exit')) + p.add_option ('-w', '--warranty', help=_ ('show warranty and copyright'), + action='store_true',) + p.add_option ('-x', '--text-lyrics', help=_ ('treat every text as a lyric'), + action='store_true') + + p.add_option_group (ly.display_encode (_ ('Examples')), + description = r''' + $ midi2ly --key=-2:1 --duration-quant=32 --allow-tuplet=4*2/3 --allow-tuplet=2*4/3 foo.midi +''') + p.add_option_group ('', + description=( + _ ('Report bugs via %s') + % 'http://post.gmane.org/post.php' + '?group=gmane.comp.gnu.lilypond.bugs') + '\n') + return p + + + +def do_options (): + opt_parser = get_option_parser () + (options, args) = opt_parser.parse_args () + + if options.warranty: + warranty () + sys.exit (0) + + if not args or args[0] == '-': + opt_parser.print_help () + ly.stderr_write ('\n%s: %s %s\n' % (program_name, _ ('error: '), + _ ('no files specified on command line.'))) + sys.exit (2) + + if options.duration_quant: + options.duration_quant = int (options.duration_quant) + + if options.key: + (alterations, minor) = map (int, (options.key + ':0').split (':'))[0:2] + sharps = 0 + flats = 0 + if alterations >= 0: + sharps = alterations + else: + flats = - alterations + options.key = Key (sharps, flats, minor) + + if options.start_quant: + options.start_quant = int (options.start_quant) + + global bar_max + if options.preview: + bar_max = 4 + + options.allowed_tuplets = [map (int, a.replace ('/','*').split ('*')) + for a in options.allowed_tuplets] + + if options.verbose: + sys.stderr.write ('Allowed tuplets: %s\n' % `options.allowed_tuplets`) + + global global_options + global_options = options + + return args + +def main (): + files = do_options () + + exts = ['.midi', '.mid', '.MID'] + for f in files: + g = f + for e in exts: + g = strip_extension (g, e) + if not os.path.exists (f): + for e in exts: + n = g + e + if os.path.exists (n): + f = n + break + + if not global_options.output: + outdir = '.' + outbase = os.path.basename (g) + o = outbase + '-midi.ly' + elif (global_options.output[-1] == os.sep + or os.path.isdir (global_options.output)): + outdir = global_options.output + outbase = os.path.basename (g) + o = os.path.join (outdir, outbase + '-midi.ly') + else: + o = global_options.output + (outdir, outbase) = os.path.split (o) + + if outdir and outdir != '.' and not os.path.exists (outdir): + os.mkdir (outdir, 0777) + + convert_midi (f, o) + +if __name__ == '__main__': + main ()