X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=scripts%2Fmidi2ly.py;h=6aa331ab8e2e434b222b8908906af9a90e0ed889;hb=e07bf64b7d8f7522367e035738fa9af433ea51b0;hp=afe5c5af3550b3c8a4f106acebc28df91d17ad52;hpb=7a2766a80beb907df0b291584c1a84b6b4fe4c4f;p=lilypond.git diff --git a/scripts/midi2ly.py b/scripts/midi2ly.py index afe5c5af35..6aa331ab8e 100644 --- a/scripts/midi2ly.py +++ b/scripts/midi2ly.py @@ -56,7 +56,7 @@ global_options = None clocks_per_1 = 1536 clocks_per_4 = 0 -time = 0 +time = None reference_note = 0 start_quant_clocks = 0 @@ -102,6 +102,10 @@ 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): return ly.system (cmd, ignore_error=ignore_error) @@ -177,7 +181,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 @@ -209,7 +217,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 @@ -251,7 +258,7 @@ class Note: s = chr ((self.notename + 2) % 7 + ord ('a')) return 'Note(%s %s)' % (s, self.duration.dump ()) - def dump (self, dump_dur = 1): + 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] @@ -272,9 +279,9 @@ class Note: elif commas < 0: s = s + "," * -commas - ## FIXME: compile fix --jcn - if (dump_dur and (global_options.explicit_durations - or self.duration.compare (reference_note.duration))): + if ((dump_dur + and self.duration.compare (reference_note.duration)) + or global_options.explicit_durations): s = s + self.duration.dump () reference_note = self @@ -401,6 +408,12 @@ class Text: or d.compare (reference_note.duration)): s = s + Duration (self.clocks).dump () s = s + ' ' + elif (self.text + and self.type == midi.SEQUENCE_TRACK_NAME + and not self.text == 'control track'): + text = self.text.replace ('(MIDI)', '').strip () + if text: + s = '\n \\set Staff.instrumentName = "%(text)s"\n ' % locals () else: s = '\n % [' + self.text_types[self.type] + '] ' + self.text + '\n ' return s @@ -424,19 +437,19 @@ def split_track (track): else: chs[0].append (e) - for i in range (16): - if chs[i] == []: - del chs[i] - threads = [] + i = 0 for v in chs.values (): + i += 1 + if not v: + continue + debug ('channel: %d\n' % i) events = events_on_channel (v) - thread = unthread_notes (events) - if len (thread): - threads.append (thread) + t = unthread_notes (events) + if len (t): + threads.append (t) return threads - def quantise_clocks (clocks, quant): q = int (clocks / quant) * quant if q != clocks: @@ -486,11 +499,17 @@ def events_on_channel (channel): 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 @@ -523,6 +542,9 @@ def events_on_channel (channel): 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 events.append ((t, k)) # ugh, must set key while parsing @@ -623,10 +645,10 @@ def dump_chord (ch): elif len (notes) > 1: global reference_note s = s + '<' - s = s + notes[0].dump (dump_dur = 0) + s = s + notes[0].dump (dump_dur=False) r = reference_note for i in notes[1:]: - s = s + i.dump (dump_dur = 0 ) + s = s + i.dump (dump_dur=False) s = s + '>' s = s + notes[0].duration.dump () + ' ' @@ -649,15 +671,18 @@ def dump_bar_line (last_bar_t, t, bar_count): return (s, last_bar_t, bar_count) -def dump_channel (thread, skip): +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) - reference_note = Note (0, 4*12, 0) + 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 = [] @@ -727,100 +752,160 @@ def number2ascii (i): i = (i - m)/26 return s -def track_name (i): +def get_track_name (i): return 'track' + number2ascii (i) -def channel_name (i): +def get_channel_name (i): return 'channel' + number2ascii (i) -def dump_track (channels, n): +def get_voice_name (i): + if True: #i: + return 'voice' + number2ascii (i) + return '' + +def get_voice_layout (average_pitch): + d = {} + for i in range (len (average_pitch)): + d[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] + layout[v] = n + return layout + +def dump_track (track, 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 global_options.absolute_pitches: - 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 + 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 + v = 0 + for channel in track: + 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 = '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 v and global_options.key: + s += global_options.key.dump () + if average_pitch[v+1] and voices > 1: + s += ' \\voice' + get_voice_layout (average_pitch[1:])[v] + '\n' + s += ' ' + dump_voice (voice, skip) + s += '}\n\n' + v += 1 + + s += '%(track_name)s = <<\n' % locals () 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' + s += clef.dump () + '\n' + + c = 0 + v = 0 + for channel in track: + channel_name = get_channel_name (c) + c += 1 + 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) + context = 'Voice' + if item and item.__class__ == Text: + context = 'Lyrics' + s += ' \\context %(context)s = %(voice_name)s \\%(voice_id)s\n' % locals () + 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 +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 - return event[1] +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: - first = thread_first_item (thread) + for channel in track: + first = channel_first_item (channel) if first: return first return None -def guess_clef (track): +def track_average_pitch (track): i = 0 - p = 0 - for thread in track: - for chord in thread: - for event in chord: + 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) 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 () - midi_dump = midi.parse (str) + 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 @@ -837,29 +922,54 @@ def convert_midi (in_file, out_file): 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) - s = '' - s = tag + '\n\\version "2.7.38"\n\n' + s = tag + s += r''' +\version "2.7.38" +''' + + s += r''' +\layout { + \context { + \Voice + \remove "Note_heads_engraver" + \consists "Completion_heads_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 in range (len (tracks)): s = s + dump_track (tracks[i], i) - s = s + '\n\\score {\n <<\n' + s += '\n\\score {\n <<\n' i = 0 for t in tracks: - track = track_name (i) + track_name = get_track_name (i) item = track_first_item (t) - - if item and item.__class__ == Note: - s = s + ' \\context Staff=%s \\%s\n' % (track, track) + staff_name = track_name + context = None + if not i and not item and len (tracks) > 1: + # control track + staff_name = get_track_name (1) + context = 'Staff' + elif (item and item.__class__ == Note): + context = 'Staff' elif item and item.__class__ == Text: - s = s + ' \\context Lyrics=%s \\%s\n' % (track, track) - + context = 'Lyrics' + if context: + s += ' \\context %(context)s=%(staff_name)s \\%(track_name)s\n' % locals () i += 1 s = s + ' >>\n}\n' @@ -885,15 +995,23 @@ 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')) + 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'), + default=None), p.add_option ('-o', '--output', help=_ ('write output to FILE'), metavar=_ ('FILE'), action='store') @@ -937,6 +1055,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: '), @@ -946,10 +1068,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 @@ -957,7 +1076,6 @@ def do_options (): sharps = alterations else: flats = - alterations - options.key = Key (sharps, flats, minor) if options.start_quant: