+ 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=False)
+ r = reference_note
+ for i in notes[1:]:
+ s = s + i.dump (dump_dur=False)
+ s = s + '>'
+ s = s + notes[0].duration.dump () + ' '
+ 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 | %% %(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 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 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 ()