diff --git a/scripts/midi2ly.py b/scripts/midi2ly.py index c57c788..040dec5 100644 --- a/scripts/midi2ly.py +++ b/scripts/midi2ly.py @@ -11,7 +11,7 @@ ''' TODO: * test on weird and unquantised midi input (lily-devel) - * update doc and manpage + * update doc and manpage and translations * simply insert clef changes whenever too many ledger lines [to avoid tex capacity exceeded] @@ -19,6 +19,12 @@ TODO: * 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 + + * split notes that cross barlines, use ties + * better handling of polyphony in one staff. + * make barcheck and barcount work even after meter changes + * split midifile format-0 files into separate tracks ?? + * test and debug using more, complex example MIDI files ''' import os @@ -50,7 +56,6 @@ start_quant_clocks = 0 duration_quant_clocks = 0 allowed_tuplet_clocks = [] - ################################################################ @@ -285,7 +290,7 @@ class Time: def dump (self): global time time = self - return '\n ' + '\\time %d/%d ' % (self.num, self.den) + '\n ' + return '\n ' + '\\time %d/%d ' % (self.num, self.den) + ' ' class Tempo: def __init__ (self, seconds_per_1): @@ -299,7 +304,7 @@ class Tempo: return 4 * 60 / self.seconds_per_1 def dump (self): - return '\n ' + '\\tempo 4 = %d ' % (self.bpm()) + '\n ' + return '\n ' + '\\tempo 4 = %d ' % (self.bpm()) + '\n ' class Clef: clefs = ('"bass_8"', 'bass', 'violin', '"violin^8"') @@ -310,7 +315,7 @@ class Clef: return 'Clef(%s)' % self.clefs[self.type] def dump (self): - return '\n \\clef %s\n ' % self.clefs[self.type] + return ' \\clef %s\n' % self.clefs[self.type] class Key: key_sharps = ('c', 'g', 'd', 'a', 'e', 'b', 'fis') @@ -360,8 +365,10 @@ class Key: else: s = s + ' \\major' - return '\n\n ' + s + '\n ' - + if use_mykey == 0: + return '\n ' + s + ' ' + else: + return '' class Text: text_types = ( @@ -388,6 +395,8 @@ class Text: or d.compare (reference_note.duration): s = s + Duration (self.clocks).dump () s = s + ' ' + elif self.type == midi.SEQUENCE_TRACK_NAME: + s = '\\set Staff.instrumentName="%s"' % self.text else: s = '\n % [' + self.text_types[self.type] + '] ' + self.text + '\n ' return s @@ -459,14 +468,13 @@ def end_note (pitches, notes, t, e): 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) @@ -497,19 +505,28 @@ def events_on_channel (channel): seconds_per_1 = us_per_4 * 4 / 1e6 events.append ((t, Tempo (seconds_per_1))) elif e[1][1] == midi.TIME_SIGNATURE: + global num, den (num, dur, clocks4, count32) = map (ord, e[1][2]) den = 2 ** dur + if global_options.mytime: + num = int(global_options.mytime[:global_options.mytime.index('/')]) + den = int(global_options.mytime[global_options.mytime.index('/')+1:]) 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) + if mysharps == myflats: + k = Key (sharps, flats, minor) + else: + k = Key (mysharps, myflats, myminor) + events.append ((t, k)) # ugh, must set key while parsing @@ -639,11 +656,12 @@ def dump_bar_line (last_bar_t, t, bar_count): def dump_channel (thread, skip): global reference_note, time - global_options.key = Key (0, 0, 0) - time = Time (4, 4) + time = Time (num, den) + # 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 = [] @@ -667,7 +685,8 @@ def dump_channel (thread, skip): last_bar_t = 0 bar_count = 1 - lines = [''] + lines = ['\mytime '] + for ch in chs: t = ch[0] @@ -682,6 +701,7 @@ def dump_channel (thread, skip): (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]) @@ -715,7 +735,11 @@ def dump_track (channels, n): item = thread_first_item (channels[i]) if item and item.__class__ == Note: - skip = 's' + if global_options.skip: + skip = 's' + else: + skip = 'r' + s = s + '%s = ' % (track + channel) if not global_options.absolute_pitches: s = s + '\\relative c ' @@ -723,8 +747,9 @@ def dump_track (channels, n): skip = '" "' s = s + '%s = \\lyricmode ' % (track + channel) else: - skip = '\\skip ' + skip = ' \\skip ' s = s + '%s = ' % (track + channel) + s = s + '{\n' s = s + ' ' + dump_channel (channels[i][0], skip) s = s + '}\n\n' @@ -732,7 +757,7 @@ def dump_track (channels, n): s = s + '%s = <<\n' % track if clef.type != 2: - s = s + clef.dump () + '\n' + s = s + clef.dump () for i in range (len (channels)): channel = channel_name (i) @@ -781,12 +806,31 @@ def guess_clef (track): else: return Clef (2) +def mykey (sharps, flats, minor): + ks = [ 'g' , 'd' , 'a' , 'e' , 'b' , 'fis' , 'cis', 'gis', 'dis' ] + kf = [ 'd', 'g', 'c' , 'f' , 'bes' , 'es' , 'as' , 'des' , 'ges' , 'ces' ] + if sharps > 0: + mykey = ks [ sharps - 1 + 3*minor ] + elif flats > 0: + mykey = kf [ 2 + flats - 3*minor ] + else: + if minor == 1: + mykey = 'a' + else: + mykey = 'c' + + if minor == 1: + mykey = mykey + ' \\minor' + else: + mykey = mykey + ' \\major' + return mykey def convert_midi (in_file, out_file): global clocks_per_1, clocks_per_4, key global start_quant_clocks - global duration_quant_clocks + global duration_quant_clocks global allowed_tuplet_clocks + global mysharps, myflats, myminor, use_mykey str = open (in_file).read () midi_dump = midi.parse (str) @@ -804,33 +848,63 @@ def convert_midi (in_file, out_file): for (dur, num, den) in global_options.allowed_tuplets: allowed_tuplet_clocks.append (clocks_per_1 / den) + if global_options.key.sharps + global_options.key.flats + global_options.key.minor !=0: + mysharps = global_options.key.sharps + myflats = global_options.key.flats + myminor = global_options.key.minor + use_mykey = 1 + else: + mysharps = global_options.key.sharps + myflats = global_options.key.flats + myminor = global_options.key.minor + use_mykey = 0 + tracks = [] for t in midi_dump[1]: - global_options.key = Key (0, 0, 0) tracks.append (split_track (t)) - tag = '%% Lily was here -- automatically converted by %s from %s' % ( program_name, in_file) + tag = 'Automatically converted by %s (Lilypond v.%s) from %s' % ( program_name, program_version, in_file) + + s = '\\version "%s"\n\n' % ( program_version ) + + s = s + '\\header {\n' + s = s + ' title = "%s"\n' % ( in_file[:in_file.index('.')] ) + s = s + ' tagline = "%s"\n' % ( tag ) + s = s + '}\n\n' + s = s + 'mytime = {\n' + if global_options.mytime: + s = s + ' \\time ' + global_options.mytime + '\n' + + if global_options.mypartial: + s = s + ' \\partial ' + global_options.mypartial + '\n' - s = '' - s = tag + '\n\\version "2.7.18"\n\n' + if use_mykey == 1: + s = s + ' \\key ' + mykey(mysharps, myflats, myminor) + '\n' + + s = s + '}\n' + for i in range (len (tracks)): s = s + dump_track (tracks[i], i) s = s + '\n\\score {\n <<\n' - i = 0 + A = 0 for t in tracks: track = track_name (i) item = track_first_item (t) if item and item.__class__ == Note: - s = s + ' \\context Staff=%s \\%s\n' % (track, track) + if not ( A == 1 and track == 'trackB' ) : + s = s + ' \\context Staff=%s \\%s\n' % (track, track) + elif track == 'trackA' and not global_options.mytime: + s = s + ' \\context Staff=trackA << \\trackA \\\\ \\trackB >>\n' + A = 1 elif item and item.__class__ == Text: s = s + ' \\context Lyrics=%s \\%s\n' % (track, track) i += 1 - s = s + ' >>\n}\n' + s = s + ' >>\n \\layout{}\n \\midi{}\n}\n' progress (_ ("%s output to `%s'...") % ('LY', out_file)) @@ -853,40 +927,56 @@ def get_option_parser (): help=_ ("print absolute pitches")) p.add_option ('-d', '--duration-quant', metavar= _("DUR"), - help=_ ("quantise note durations on DUR")) + help=_ ("quantise note durations on DUR"), + default='16') 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('-k', '--key', help=_ ("set key: ALT=+sharps|-flats; MINOR=1"), - metavar=_ ("ALT[:MINOR]"), - default='0'), + action="help", + help=_ ("show this help and exit")) + p.add_option('-k', '--key', + help=_ ("set key: ALT=+sharps|-flats; MINOR=1"), + action = 'store', + dest = 'mykey', + metavar=_ ("ALT[:MINOR]"), + default = '0') p.add_option ('-o', '--output', help=_ ("write output to FILE"), metavar=_("FILE"), action='store') - p.add_option ('-s', '--start-quant',help= _ ("quantise note starts on DUR"), - metavar=_ ("DUR")) - p.add_option ('-t', '--allow-tuplet', + p.add_option ('-P', '--partial', + metavar =_ ("DUR"), + action = "store", + dest = "mypartial", + help =_ ("insert global \"\\partial DUR\" upbeat")) + p.add_option('-s', '--start-quant', + help= _ ("quantise note starts on DUR"), + default = '16', + metavar= _ ("DUR")) + p.add_option ('--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"), + help= _ ("allow tuplet durations DUR*NUM/DEN"), default=[]) + p.add_option ('-T', '--time', + metavar =_ ("NUM/DEN"), + action = "store", + dest = "mytime", + help =_ ("insert global \"\\time NUM/DEN\" signature")) p.add_option ('-V', '--verbose', help=_ ("be verbose"), - action='store_true' - ), - p.version = "midi2ly (LilyPond) @TOPLEVEL_VERSION@" + action='store_true') + p.version = "midi2ly (LilyPond) %s" % ( program_version ) p.add_option("--version", - action="version", - help=_ ("show version number and exit")) + action="version", + help=_ ("show version number and exit")) p.add_option ('-w', '--warranty', help=_ ("show warranty and copyright"), - action='store_true', - ), + 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 @@ -904,6 +994,10 @@ 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: "), @@ -913,11 +1007,8 @@ def do_options (): if options.duration_quant: options.duration_quant = int (options.duration_quant) - if options.warranty: - warranty () - sys.exit (0) if 1: - (alterations, minor) = map (int, (options.key + ':0').split (':'))[0:2] + (alterations, minor) = map (int, (options.mykey + ':0').split (':'))[0:2] sharps = 0 flats = 0 if alterations >= 0: @@ -927,10 +1018,9 @@ def do_options (): options.key = Key (sharps, flats, minor) - if options.start_quant: options.start_quant = int (options.start_quant) - + options.allowed_tuplets = [map (int, a.replace ('/','*').split ('*')) for a in options.allowed_tuplets] @@ -944,9 +1034,8 @@ def main(): for f in files: g = f - g = strip_extension (g, '.midi') - g = strip_extension (g, '.mid') - g = strip_extension (g, '.MID') + for ext in [ '.midi','.mid','.MID' ]: + g = strip_extension (g, ext) (outdir, outbase) = ('','') if not global_options.output: