]> git.donarmstrong.com Git - lilypond.git/blob - scripts/midi2ly.py
9c5ff1845246baaec58738bfac037da15cfaf269
[lilypond.git] / scripts / midi2ly.py
1 #!@PYTHON@
2
3 import midi
4 import sys
5 import string
6
7 LINE_BELL = 60
8 scale_steps = [0,2,4,5,7,9,11]
9
10 clocks_per_1 = 1536
11
12
13 program_name = 'midi2ly.py [experimental]'
14
15 def split_track (track):
16         chs = {}
17         for i in range(16):
18                 chs[i] = []
19                 
20         for e in track:
21                 data = list (e[1])
22                 if data[0] > 0x7f and data[0] < 0xf0:
23                         c = data[0] & 0x0f
24                         e = (e[0], tuple ([data[0] & 0xf0] + data[1:]))
25                         chs[c].append (e)
26                 else:
27                         chs[0].append (e)
28
29         for i in range (16):
30                 if chs[i] == []:
31                         del chs[i]
32
33         threads = []
34         for v in chs.values ():
35                 events = events_on_channel (v)
36                 thread = unthread_notes (events)
37                 if len (thread):
38                         threads.append (thread)
39         return threads
40
41
42 class Note:
43         names = (0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6)
44         accidentals = (0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0)
45         acc_name = ('eses', 'es', 'BUG', 'is' , 'isis')
46         
47         def __init__ (self, clocks, pitch, velocity):
48                 self.velocity = velocity
49                 self.pitch = pitch
50                 self.clocks = clocks
51
52         def clocks_compare (a, b):
53                 if a.clocks < b.clocks:
54                         return -1
55                 elif a.clocks > b.clocks:
56                         return 1
57                 else:
58                         return 0
59
60         def dump (self):
61                 # major scale: do-do
62                 # minor scale: la-la  (= + 5) '''
63
64                 n = self.names[(self.pitch) % 12]
65                 a = self.accidentals[(self.pitch) % 12]
66
67                 if a and key.flats:
68                         a = - self.accidentals[(self.pitch) % 12]
69                         n = (n - a) % 7
70
71                 name = chr ((n + 2)  % 7 + ord ('a'))
72
73                 if a:
74                         name = name + self.acc_name[a + 2]
75
76                 #  By tradition, all scales now consist of a sequence
77                 #  of 7 notes each with a distinct name, from amongst
78                 #  a b c d e f g.  But, minor scales have a wide
79                 #  second interval at the top - the 'leading note' is
80                 #  sharped. (Why? it just works that way! Anything
81                 #  else doesn't sound as good and isn't as flexible at
82                 #  saying things. In medieval times, scales only had 6
83                 #  notes to avoid this problem - the hexachords.)
84
85                 #  So, the d minor scale is d e f g a b-flat c-sharp d
86                 #  - using d-flat for the leading note would skip the
87                 #  name c and duplicate the name d.  Why isn't c-sharp
88                 #  put in the key signature? Tradition. (It's also
89                 #  supposedly based on the Pythagorean theory of the
90                 #  cycle of fifths, but that really only applies to
91                 #  major scales...)  Anyway, g minor is g a b-flat c d
92                 #  e-flat f-sharp g, and all the other flat minor keys
93                 #  end up with a natural leading note. And there you
94                 #  have it.
95
96                 #  John Sankey <bf250@freenet.carleton.ca>
97                 #
98                 #  Let's also do a-minor: a b c d e f gis a
99                 #
100                 #  --jcn
101
102                 o = self.pitch / 12 - 4
103
104                 if key.minor:
105                         if key.sharps == 0 and key.flats == 0 and name == 'as':
106                                 name = 'gis'
107                         elif key.flats == 1 and name == 'des':
108                                 name = 'cis'
109                         elif key.flats == 2 and name == 'ges':
110                                 name = 'fis'
111                         elif key.sharps == 5 and name == 'g':
112                                 name = 'fisis'
113                         elif key.sharps == 6 and name == 'd':
114                                 name = 'cisis'
115                         elif key.sharps == 7 and name == 'a':
116                                 name = 'gisis'
117
118                 if key.flats >= 6 and name == 'b':
119                         name = 'ces'
120                         o = o + 1
121                 if key.flats >= 7 and name == 'e':
122                         name = 'fes'
123
124                 if key.sharps >= 3 and name == 'f':
125                         name = 'eis'
126                 if key.sharps >= 4 and name == 'c':
127                         name = 'bis'
128                         o = o - 1
129
130                 s = name
131                 
132                 if o > 0:
133                         s = s + "'" * o
134                 elif o < 0:
135                         s = s + "," * -o
136
137                 return s + dump_duration (self.clocks) + ' '
138
139
140 class Time:
141         def __init__ (self, num, den):
142                 self.clocks = 0
143                 self.num = num
144                 self.den = den
145
146         def dump (self):
147                 return '\n  ' + '\\time %d/%d ' % (self.num, self.den) + '\n  '
148
149 class Tempo:
150         def __init__ (self, seconds_per_1):
151                 self.clocks = 0
152                 self.seconds_per_1 = seconds_per_1
153
154         def dump (self):
155                 # return '\n  ' + '\\tempo 1 = %d ' % (60 / self.seconds_per_1) + '\n  '
156                 return '\n  ' + '\\tempo 4 = %d ' % (4 * 60 / self.seconds_per_1) + '\n  '
157
158 class Key:
159         key_sharps = ('c', 'g', 'd', 'a', 'e', 'b', 'fis')
160         key_flats = ('BUG', 'f', 'bes', 'es', 'as', 'des', 'ges')
161
162         def __init__ (self, sharps, flats, minor):
163                 self.clocks = 0
164                 self.flats = flats
165                 self.sharps = sharps
166                 self.minor = minor
167
168         def dump (self):
169                 global key
170                 key = self
171
172                 s = ''
173                 if self.sharps and self.flats:
174                         s = '\\keysignature %s ' % 'TODO'
175                 else:
176                         
177                         if self.flats:
178                                 k = (ord ('cfbeadg'[self.flats % 7]) - ord ('a') - 2 -2 * self.minor + 7) % 7
179                         else:
180                                 k = (ord ('cgdaebf'[self.sharps % 7]) - ord ('a') - 2 -2 * self.minor + 7) % 7
181   
182                         if not self.minor:
183                                 name = chr ((k + 2) % 7 + ord ('a'))
184                         else:
185                                 name = chr ((k + 2) % 7 + ord ('a'))
186
187                         # fis cis gis dis ais eis bis
188                         sharps = (2, 4, 6, 1, 3, 5, 7)
189                         # bes es as des ges ces fes
190                         flats = (6, 4, 2, 7, 5, 3, 1)
191                         a = 0
192                         if self.flats:
193                                 if flats[k] <= self.flats:
194                                         a = -1
195                         else:
196                                 if sharps[k] <= self.sharps:
197                                         a = 1
198
199                         if a:
200                                 name = name + Note.acc_name[a + 2]
201
202                         s = '\\key ' + name
203                         if self.minor:
204                                 s = s + ' \\minor'
205                         else:
206                                 s = s + ' \\major'
207
208                 return '\n\n  ' + s + '\n  '
209
210
211 # TODO: really handle lyrics
212 class Text:
213         text_types = (
214                 'SEQUENCE_NUMBER',
215                 'TEXT_EVENT',
216                 'COPYRIGHT_NOTICE',
217                 'SEQUENCE_TRACK_NAME',
218                 'INSTRUMENT_NAME',
219                 'LYRIC',
220                 'MARKER',
221                 'CUE_POINT',)
222         
223         def __init__ (self, type, text):
224                 self.clocks = 0
225                 self.type = type
226                 self.text = text
227
228         def dump (self):
229                 return '\n  % [' + self.text_types[self.type] + '] ' + self.text + '\n  '
230
231 def events_on_channel (channel):
232         pitches = {}
233
234         notes = []
235         events = []
236         for e in channel:
237                 t = e[0]
238
239                 if e[1][0] == midi.NOTE_ON:
240                         if not pitches.has_key (e[1][1]):
241                                 pitches[e[1][1]] = (t, e[1][2])
242                 elif e[1][0] == midi.NOTE_OFF:
243                         try:
244                                 (lt, vel) = pitches[e[1][1]]
245                                 del pitches[e[1][1]]
246
247                                 i = len (notes) - 1 
248                                 while i > 0:
249                                         if notes[i][0] > lt:
250                                                 i = i -1
251                                         else:
252                                                 break
253                                 notes.insert (i + 1,
254                                             (lt, Note (t-lt, e[1][1], vel)))
255
256                         except KeyError:
257                                 pass
258                 elif e[1][0] == midi.META_EVENT:
259                         if e[1][1] == midi.SET_TEMPO:
260                                 (u0, u1, u2) = map (ord, e[1][2])
261                                 us_per_4 = u2 + 256 * (u1 + 256 * u0)
262                                 seconds_per_1 = us_per_4 * 4 / 1e6
263                                 events.append ((t, Tempo (seconds_per_1)))
264                         elif e[1][1] == midi.TIME_SIGNATURE:
265                                 (num, dur, clocks4, count32) = map (ord, e[1][2])
266                                 den = 2 ** dur 
267                                 events.append ((t, Time (num, den)))
268                         elif e[1][1] == midi.KEY_SIGNATURE:
269                                 (accidentals, minor) = map (ord, e[1][2])
270                                 sharps = 0
271                                 flats = 0
272                                 if accidentals < 127:
273                                         sharps = accidentals
274                                 else:
275                                         flats = 256 - accidentals
276
277                                 events.append ((t, Key (sharps, flats, minor)))
278                         elif e[1][1] >= midi.SEQUENCE_NUMBER and e[1][1] <= midi.CUE_POINT:
279                                 events.append ((t, Text (e[1][1], e[1][2])))
280                         else:
281                                 sys.stderr.write ("SKIP: %s\n" % `e`)
282                                 pass
283                 else:
284                         sys.stderr.write ("SKIP: %s\n" % `e`)
285                         pass
286
287         i = 0
288         while len (notes):
289                 if i < len (events) and notes[0][0] >= events[i][0]:
290                         i = i + 1
291                 else:
292                         events.insert (i, notes[0])
293                         del notes[0]
294         return events
295
296 def unthread_notes (channel):
297         threads = []
298         while channel:
299                 thread = []
300                 end_busy_t = 0
301                 start_busy_t = 0
302                 todo = []
303                 for e in channel:
304                         t = e[0]
305                         if e[1].__class__ == Note and ((t == start_busy_t and e[1].clocks + t == end_busy_t) \
306                             or t >= end_busy_t):
307                                 thread.append (e)
308                                 start_busy_t = t
309                                 end_busy_t = t + e[1].clocks
310                         elif e[1].__class__ == Time or e[1].__class__ == Key or e[1].__class__ == Text or e[1].__class__ == Tempo:
311                                 thread.append (e)
312                         else:
313                                 todo.append (e)
314                 threads.append (thread)
315                 channel = todo
316
317         return threads
318
319 def gcd (a,b):
320         if b == 0:
321                 return a
322         c = a
323         while c: 
324                 c = a % b
325                 a = b
326                 b = c
327         return a
328         
329 def dump_skip (clocks):
330         return 's' + dump_duration (clocks) + ' '
331
332 def dump_duration (clocks):
333         g = gcd (clocks, clocks_per_1)
334         (d, n) = (clocks_per_1/ g, clocks / g)
335         if n == 1:
336                 s = '%d' % d
337         elif n == 3 and d != 1:
338                 s = '%d.' % (d / 2)
339         else:
340                 s = '%d*%d' % (d, n)
341         return s
342         
343 def dump (self):
344         return self.dump ()
345
346 def dump_chord (ch):
347         s = ''
348         notes = []
349         for i in ch:
350                 if i.__class__ == Note:
351                         notes.append (i)
352                 else:
353                         s = s + i.dump ()
354         if len (notes) == 1:
355                 s = s + dump (notes[0])
356         elif len (notes) > 1:
357                 s = s + '<' + string.join (map (dump, notes)) + '>'
358         return s
359
360 # thread?
361 def dump_channel (thread):
362         global key
363
364         key = Key (0, 0, 0)
365         last_e = None
366         chs = []
367         ch = []
368
369         for e in thread:
370                 if last_e and last_e[0] == e[0]:
371                         ch.append (e[1])
372                 else:
373                         if ch:
374                                 chs.append ((last_e[0], ch))
375                                 
376                         ch = [e[1]]
377                         
378                 last_e = e
379
380         if ch:
381                 chs.append ((last_e[0], ch))
382         t = 0
383         last_t = 0
384
385         lines = ['']
386         for ch in chs:
387                 i = string.rfind (lines[-1], '\n')
388                 if len (lines[-1][i:]) > LINE_BELL:
389                         lines.append ('')
390                         
391                 t = ch[0]
392                 if t - last_t:
393                         lines[-1] = lines[-1] + dump_skip (t-last_t)
394                         
395                 lines[-1] = lines[-1] + dump_chord (ch[1])
396
397                 clocks = 0
398                 for i in ch[1]:
399                         if i.clocks > clocks:
400                                 clocks = i.clocks
401                                 
402                 last_t = t + clocks
403
404         return string.join (lines, '\n  ') + '\n'
405
406 def track_name (i):
407         return 'track%c' % (i + ord ('A'))
408
409 def channel_name (i):
410         return 'channel%c' % (i + ord ('A'))
411
412 def dump_track (channels, n):
413         s = '\n'
414         track = track_name (n)
415         for i in range (len (channels)):
416                 channel = channel_name (i)
417                 s = s + '%s = \\notes {\n' % (track + channel)
418                 s = s + '  ' + dump_channel (channels[i][0])
419                 s = s + '}\n\n'
420
421         s = s + '%s = <\n' % track
422
423         for i in range (len (channels)):
424                 channel = channel_name (i)
425                 s = s + '  \\context Voice = %s \\%s\n' % (channel, track + channel)
426         s = s + '>\n\n'
427         return s
428                         
429         
430 def convert_midi (f):
431         global clocks_per_1
432
433         str = open (f).read ()
434         midi_dump = midi.parse (str)
435
436         clocks_per_1 = midi_dump[0][1]
437
438         tracks = []
439         for t in midi_dump[1]:
440                 tracks.append (split_track (t))
441
442         tag = '%% Lily was here -- automatically converted by %s from %s' % ( program_name, f)
443
444         s = ''
445         s = tag + '\n\n'
446         for i in range (len (tracks)):
447                 s = s + dump_track (tracks[i], i)
448
449         s = s + '\n\\score {\n  <\n'
450         for i in range (len (tracks)):
451                 track = track_name (i)
452                 s = s + '    \\context Staff=%s \\%s\n' % (track, track)
453         s = s + '  >\n}\n'
454         
455         sys.stdout.write (s)
456                 
457
458 for f in sys.argv[1:]:
459         convert_midi (f)