X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=scripts%2Fmidi2ly.py;h=a59cadcbd64bfdfec6f5cd891924101c880a545a;hb=bea1783a6b70e41c45f85136146cdd7fa773fb8e;hp=1b3723e2984e139205c396b77f692dce7092c0de;hpb=7b3a9b122d7ef1eb70fdabaac466b7b0c23a1df2;p=lilypond.git diff --git a/scripts/midi2ly.py b/scripts/midi2ly.py index 1b3723e298..a59cadcbd6 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 @@ -394,6 +401,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) @@ -401,7 +409,13 @@ 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'): + 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 @@ -424,19 +438,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 +500,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 +543,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 +646,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 () + ' ' @@ -640,7 +663,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 @@ -649,15 +672,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 +753,167 @@ 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, zero_too_p=False): + if i or zero_too_p: + 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 + 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: + s += ' \\voice' + get_voice_layout (average_pitch[1:])[vv] + '\n' + s += ' ' + dump_voice (voice, skip) + s += '}\n\n' + v += 1 + vv += 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 + 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 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 @@ -830,33 +923,63 @@ def convert_midi (in_file, out_file): allowed_tuplet_clocks = [] for (dur, num, den) in global_options.allowed_tuplets: - allowed_tuplet_clocks.append (clocks_per_1 / den) + allowed_tuplet_clocks.append (clocks_per_1 / dur * num / den) + + 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)) 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.13.53" +''' + + 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 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' @@ -882,15 +1005,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') @@ -898,6 +1029,9 @@ def get_option_parser (): action='store_true') 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', @@ -934,6 +1068,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: '), @@ -943,10 +1081,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 @@ -954,7 +1089,6 @@ def do_options (): sharps = alterations else: flats = - alterations - options.key = Key (sharps, flats, minor) if options.start_quant: @@ -967,6 +1101,9 @@ def do_options (): 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 @@ -975,30 +1112,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)