X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=scripts%2Fmidi2ly.py;h=79351bbfa2c1817b8fde62871471b18afff34bce;hb=1d3beed53413315ec6c0553b003b8c5776f8c38f;hp=b233d98a7581ac2ed2a2ab67a7c03369b6e46486;hpb=7bb304912f9b9752591abc4216d764a99012b90a;p=lilypond.git diff --git a/scripts/midi2ly.py b/scripts/midi2ly.py index b233d98a75..79351bbfa2 100644 --- a/scripts/midi2ly.py +++ b/scripts/midi2ly.py @@ -4,7 +4,7 @@ # This file is part of LilyPond, the GNU music typesetter. # -# Copyright (C) 1998--2011 Han-Wen Nienhuys +# Copyright (C) 1998--2014 Han-Wen Nienhuys # Jan Nieuwenhuizen # # LilyPond is free software: you can redistribute it and/or modify @@ -23,15 +23,6 @@ ''' TODO: - * test on weird and unquantised midi input (lily-devel) - * 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 os @@ -56,7 +47,7 @@ global_options = None clocks_per_1 = 1536 clocks_per_4 = 0 -time = 0 +time = None reference_note = 0 start_quant_clocks = 0 @@ -87,7 +78,7 @@ def warranty (): %s %s -''' % ( _ ('Copyright (c) %s by') % '1998--2011', +''' % ( _ ('Copyright (c) %s by') % '1998--2014', '\n '.join (authors), _ ('Distributed under terms of the GNU General Public License.'), _ ('It comes with NO WARRANTY.'))) @@ -103,7 +94,7 @@ def error (s): raise Exception (_ ("Exiting... ")) def debug (s): - if 0: + if global_options.debug: progress ("debug: " + s) def system (cmd, ignore_error = 0): @@ -120,8 +111,6 @@ 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): @@ -181,7 +170,11 @@ class Note: n = self.names[(self.pitch) % 12] a = self.alterations[(self.pitch) % 12] - if a and global_options.key.flats: + 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 @@ -213,7 +206,6 @@ class Note: o = self.pitch / 12 - 4 - key = global_options.key if key.minor: # as -> gis if (key.sharps == 0 and key.flats == 0 @@ -398,6 +390,7 @@ class 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) @@ -405,40 +398,172 @@ class Text: or d.compare (reference_note.duration)): s = s + Duration (self.clocks).dump () s = s + ' ' - else: + 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') -def split_track (track): - chs = {} - for i in range (16): - chs[i] = [] + # 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) - for e in track: + 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: - c = data[0] & 0x0f + channel = data[0] & 0x0f e = (e[0], tuple ([data[0] & 0xf0] + data[1:])) - chs[c].append (e) + track.add (e, channel) 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) - t = unthread_notes (events) - if len (t): - threads.append (t) - return threads + track.add (e) + return track def quantise_clocks (clocks, quant): q = int (clocks / quant) * quant @@ -473,108 +598,6 @@ def end_note (pitches, notes, t, e): 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)): - 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 - 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_options.key = k - - elif (e[1][1] == midi.LYRIC - or (global_options.text_lyrics 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 global_options.verbose: - sys.stderr.write ("SKIP: %s\n" % `e`) - pass - else: - if global_options.verbose: - 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 - def unthread_notes (channel): threads = [] while channel: @@ -637,7 +660,6 @@ def dump_chord (ch): 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 @@ -649,7 +671,7 @@ def dump_bar_line (last_bar_t, t, bar_count): bar_count = bar_count + (t - last_bar_t) / bar_t if t - last_bar_t == bar_t: - s = '|\n %% %d\n ' % bar_count + s = '\n | %% %(bar_count)d\n ' % locals () last_bar_t = t else: # urg, this will barf at meter changes @@ -660,14 +682,12 @@ def dump_bar_line (last_bar_t, t, bar_count): def dump_voice (thread, skip): global reference_note, time - - global_options.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) + ref = Note (0, 4*12, 0) if not reference_note: - reference_note = Note (0, 4*12, 0) + reference_note = ref + else: + ref.duration = reference_note.duration + reference_note = ref last_e = None chs = [] ch = [] @@ -743,29 +763,57 @@ def get_track_name (i): def get_channel_name (i): return 'channel' + number2ascii (i) -def get_voice_name (i): - if True: #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) - clef = guess_clef (track) + + 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 - v = 0 for voice in channel: voice_name = get_voice_name (v) - v += 1 voice_id = track_name + channel_name + voice_name item = voice_first_item (voice) if item and item.__class__ == Note: - skip = 's' + skip = 'r' + if global_options.skip: + skip = 's' s += '%(voice_id)s = ' % locals () if not global_options.absolute_pitches: s += '\\relative c ' @@ -776,8 +824,19 @@ def dump_track (track, n): 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 () @@ -785,19 +844,22 @@ def dump_track (track, n): s += clef.dump () + '\n' c = 0 + vv = 0 for channel in track: + v = 0 channel_name = get_channel_name (c) c += 1 - v = 0 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_name)s \\%(voice_id)s\n' % locals () + s += ' \\context %(context)s = %(voice_context_name)s \\%(voice_id)s\n' % locals () s += '>>\n\n' return s @@ -823,30 +885,49 @@ def track_first_item (track): return first return None -def guess_clef (track): +def track_average_pitch (track): i = 0 - p = 0 - for voice in track: - for thread in voice: - for event in thread: + 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 = 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) + 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 @@ -854,6 +935,7 @@ def convert_midi (in_file, out_file): 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 @@ -868,35 +950,86 @@ def convert_midi (in_file, out_file): if global_options.verbose: print 'allowed tuplet clocks:', allowed_tuplet_clocks - tracks = [] - for t in midi_dump[1]: - global_options.key = Key (0, 0, 0) - tracks.append (split_track (t)) + 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 = '' - s = tag + '\n\\version "2.7.38"\n\n' - for i in range (len (tracks)): - s = s + dump_track (tracks[i], i) + 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" + } +} +''' - s = s + '\n\\score {\n <<\n' + 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 t in tracks: + for i, staff in enumerate (staves): track_name = get_track_name (i) - item = track_first_item (t) - - if item and item.__class__ == Note: - s += ' \\context Staff=%(track_name)s \\%(track_name)s\n' % locals () + 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: - s += ' \\context Lyrics=%(track_name)s \\%(track_name)s\n' % locals () - - i += 1 - s = s + ' >>\n}\n' + context = 'Lyrics' + if context: + s += ' \\context %(context)s=%(staff_name)s \\%(track_name)s\n' % locals () + + s = s + ''' >> + \layout {} + \midi {} +} +''' - progress (_ ("%s output to `%s'...") % ('LY', out_file)) + if not global_options.quiet: + progress (_ ("%s output to `%s'...") % ('LY', out_file)) if out_file == '-': handle = sys.stdout @@ -918,22 +1051,36 @@ def get_option_parser (): 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')) + 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='0'), + 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', @@ -941,15 +1088,13 @@ def get_option_parser (): help=_ ('allow tuplet durations DUR*NUM/DEN'), default=[]) p.add_option ('-V', '--verbose', help=_ ('be verbose'), - action='store_true' - ), + 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', - ), + action='store_true',) p.add_option ('-x', '--text-lyrics', help=_ ('treat every text as a lyric'), action='store_true') @@ -970,6 +1115,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: '), @@ -979,10 +1128,7 @@ def do_options (): if options.duration_quant: options.duration_quant = int (options.duration_quant) - if options.warranty: - warranty () - sys.exit (0) - if 1: + if options.key: (alterations, minor) = map (int, (options.key + ':0').split (':'))[0:2] sharps = 0 flats = 0 @@ -990,7 +1136,6 @@ def do_options (): sharps = alterations else: flats = - alterations - options.key = Key (sharps, flats, minor) if options.start_quant: @@ -1014,30 +1159,33 @@ def do_options (): def main (): files = do_options () + exts = ['.midi', '.mid', '.MID'] for f in files: g = f - g = strip_extension (g, '.midi') - g = strip_extension (g, '.mid') - g = strip_extension (g, '.MID') - (outdir, outbase) = ('','') + 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 = os.path.join (outdir, outbase + '-midi.ly') - elif global_options.output[-1] == os.sep: + 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) - os.path.join (outdir, outbase + '-gen.ly') + o = os.path.join (outdir, outbase + '-midi.ly') else: o = global_options.output (outdir, outbase) = os.path.split (o) - if outdir != '.' and outdir != '': - try: - os.mkdir (outdir, 0777) - except OSError: - pass + if outdir and outdir != '.' and not os.path.exists (outdir): + os.mkdir (outdir, 0777) convert_midi (f, o)