]> git.donarmstrong.com Git - lilypond.git/blob - scripts/midi2ly.py
ec72a6a07a61c5b1a00f737dd2c8229decc22f42
[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 def split_track (track):
11         chs = {}
12         for i in range(16):
13                 chs[i] = []
14                 
15         for e in track:
16                 data = list (e[1])
17                 if data[0] > 0x7f and data[0] < 0xf0:
18                         c = data[0] & 0x0f
19                         e = (e[0], tuple ([data[0] & 0xf0] + data[1:]))
20                         chs[c].append (e)
21                 else:
22                         chs[0].append (e)
23
24         for i in range (16):
25                 if chs[i] == []:
26                         del chs[i]
27
28         whatarewes = []
29         for v in chs.values ():
30                 ns = notes_on_channel (v)
31                 dinges = unthread_notes (ns)
32                 if len (dinges):
33                         whatarewes.append (dinges)
34         return whatarewes
35
36
37 class Note:
38         def __init__ (self, duration, pitch, velocity):
39                 self.velocity = velocity
40                 self.pitch = pitch
41                 self.duration = duration
42
43         def duration_compare (a, b):
44                 if a.duration < b.duration:
45                         return -1
46                 elif a.duration > b.duration:
47                         return 1
48                 else:
49                         return 0
50
51         def dump (self):
52                 return dump_note (self)
53
54 class Time:
55         def __init__ (self, t, num, den):
56                 self.duration = t
57                 self.num = num
58                 self.den = den
59                 
60         def dump (self):
61                 return dump_skip (self.duration) + '\\time %d/%d ' % (self.num, self.den)
62
63 class Key:
64         key_sharps = ('c', 'g', 'd', 'a', 'e', 'b', 'fis')
65         key_flats = ('BUG', 'f', 'bes', 'es', 'as', 'des', 'ges')
66
67         def __init__ (self, t, sharps, flats, minor):
68                 self.duration = t
69                 self.flats = flats
70                 self.sharps = sharps
71                 self.minor = minor
72                 
73         def dump (self):
74                 if self.sharps and self.flats:
75                         s = '\\keysignature %s ' % 'TODO'
76                 elif self.sharps:
77                         s = '\\notes\\key %s \major' % key_sharps[self.sharps]
78                 elif self.flats:
79                         s = '\\notes\\key %s \major' % key_flats[self.flats]
80                 return dump_skip (self.duration) + s
81
82
83 class Text:
84         def __init__ (self, text):
85                 self.text = text
86                 self.duration = 0
87                 
88         def dump (self):
89                 return dump_text (self)
90
91 def notes_on_channel (channel):
92         pitches = {}
93
94         nch = []
95         for e in channel:
96                 t = e[0]
97
98                 if e[1][0] == midi.NOTE_ON:
99                         if not pitches.has_key (e[1][1]):
100                                 pitches[e[1][1]] = (t, e[1][2])
101                 elif e[1][0] == midi.NOTE_OFF:
102                         try:
103                                 (lt, vel) = pitches[e[1][1]]
104                                 del pitches[e[1][1]]
105                                 
106                                 nch.append ((t, Note (t-lt, e[1][1], vel)))
107                                 
108                         except KeyError:
109                                 pass
110                 elif e[1][0] == midi.META_EVENT:
111                         if e[1][1] == midi.TIME_SIGNATURE:
112                                 (num, den, clocks4, count32) = map (ord, e[1][2])
113                                 nch.append ((t, Time (t, num, den)))
114                         elif e[1][1] == midi.KEY_SIGNATURE:
115                                 (accidentals, minor) = map (ord, e[1][2])
116                                 sharps = 0
117                                 flats = 0
118                                 if accidentals < 127:
119                                         sharps = accidentals
120                                 else:
121                                         flats = 256 - accidentals
122                                 nch.append ((t, Key (t, sharps, flats, minor)))
123                         elif e[1][1] == midi.TEXT_EVENT:
124                                 nch.append ((t, Text (e[1][2])))
125                         else:
126                                 sys.stderr.write ("SKIP: %s\n" % `e`)
127                                 pass
128                 else:
129                         sys.stderr.write ("SKIP: %s\n" % `e`)
130                         pass
131         
132         return nch
133
134 def unthread_notes (channel):
135         threads = []
136         while channel:
137                 thread = []
138                 end_busy_t = 0
139                 start_busy_t = 0
140                 todo = []
141                 for e in channel:
142                         t = e[0]
143                         if e[1].__class__ == Note and ((t == start_busy_t and e[1].duration + t == end_busy_t) \
144                             or t >= end_busy_t):
145                                 thread.append (e)
146                                 start_busy_t = t
147                                 end_busy_t = t + e[1].duration
148                         elif e[1].__class__ == Time or e[1].__class__ == Key or e[1].__class__ == Text:
149                                 thread.append (e)
150                         else:
151                                 todo.append (e)
152                 threads.append (thread)
153                 channel = todo
154
155         return threads
156
157 def gcd (a,b):
158         if b == 0:
159                 return a
160         c = a
161         while c: 
162                 c = a % b
163                 a = b
164                 b = c
165         return a
166         
167 def dump_skip (dt):
168         return 's' + dump_duration (dt)
169
170 def dump_duration (dur):
171         g = gcd (dur, 384)
172         s = '4'
173         (p,q) = (dur / g, 384 / g)
174         if (p == 1 and q == 1) :
175                 pass
176         else:
177                 if p <> 1:      
178                         s = s + '*%d'% p
179                 if q <> 1:
180                         s = s + '*%d'% q
181         return s
182         
183 def dump_note (note):
184         p = note.pitch
185         oct = p / 12
186         step = p % 12
187
188         i = 0
189         while  i < len (scale_steps):
190                 if scale_steps[i] > step:
191                         break
192                 i = i+1
193                 
194         i = i-1
195         str = chr ((i + 2)  % 7 + ord ('a'))
196         if scale_steps[i] <> step:
197                 str = str + 'is'
198
199         return ' %s' % str + dump_duration (note.duration)
200
201 def dump (self):
202         return self.dump ()
203
204 def dump_text (text):
205         return '\n  % ' + text.text + '\n  '
206
207 def dump_chord (ch):
208         s = ''
209         if len(ch) == 1:
210                 s = dump (ch[0])
211         else:
212                 s = '<' + string.join (map (dump, ch)) + '>'
213         return s
214
215 # thread?
216 def dump_channel (thread):
217         last_e = None
218         chs = []
219         ch = []
220
221         for e in thread:
222                 if last_e and last_e[0] == e[0]:
223                         ch.append (e[1])
224                 else:
225                         if ch:
226                                 chs.append ((last_e[0], ch))
227                                 
228                         ch = [e[1]]
229                         
230                 last_e = e
231
232         if ch:
233                 chs.append ((last_e[0], ch))
234         t = 0
235         last_t = 0
236
237         lines = ['']
238         for ch in chs:
239                 if len (lines[-1]) > LINE_BELL:
240                         lines.append ('')
241                         
242                 t = ch[0]
243                 if t - last_t:
244                         lines[-1] = lines[-1] + dump_skip (t-last_t)
245                         
246                 lines[-1] = lines[-1] + dump_chord (ch[1])
247
248                 last_t = t + ch[1][0].duration
249
250         return string.join (lines, '\n  ') + '\n'
251
252 def dump_notes (channel):
253         on_hold = []
254         s = ''
255         for e in channel:
256                 if e[0] <> last_t:
257                         s = s + dump_chord (on_hold)
258                         on_hold = []
259                         last_t = e[0]
260
261                 on_hold.append (e)
262         return s
263
264 def track_name (i):
265         return 'track%c' % (i + ord ('A'))
266
267 def channel_name (i):
268         return 'channel%c' % (i + ord ('A'))
269
270 def dump_track (channels, n):
271         s = '\n'
272         track = track_name (n)
273         for i in range (len (channels)):
274                 channel = channel_name (i)
275                 s = s + '%s = \\notes {\n' % (track + channel)
276                 s = s + '  ' + dump_channel (channels[i][0])
277                 s = s + '}\n\n'
278
279         s = s + '%s = <\n' % track
280
281         for i in range (len (channels)):
282                 channel = channel_name (i)
283                 s = s + '  \\context Voice = %s \\%s\n' % (channel, track + channel)
284         s = s + '>\n\n'
285         return s
286                         
287         
288 def convert_midi (f):
289         str = open (f).read ()
290         midi_dump = midi.parse (str)
291
292         tracks = []
293         for t in midi_dump[1]:
294                 tracks.append (split_track (t))
295
296         s = ''
297         for i in range (len (tracks)):
298                 s = s + dump_track (tracks[i], i)
299
300         s = s + '\n\\score {\n  <\n'
301         for i in range (len (tracks)):
302                 track = track_name (i)
303                 s = s + '    \\context Staff=%s \\%s\n' % (track, track)
304         s = s + '  >\n}\n'
305         
306         sys.stdout.write (s)
307                 
308
309 for f in sys.argv[1:]:
310         convert_midi (f)