Ajout script d'export des chansons en musicxml vers .smwi (qq trucs en dur).
[minwii.git] / src / app / musicxml.py
1 # -*- coding: utf-8 -*-
2 """
3 conversion d'un fichier musicxml en objet song minwii.
4
5 $Id$
6 $URL$
7 """
8 import sys
9 from types import StringTypes
10 from xml.dom.minidom import parse
11 from optparse import OptionParser
12 from itertools import cycle
13 #from Song import Song
14
15 # Do4 <=> midi 60
16 OCTAVE_REF = 4
17 DIATO_SCALE = {'C' : 60,
18 'D' : 62,
19 'E' : 64,
20 'F' : 65,
21 'G' : 67,
22 'A' : 69,
23 'B' : 71}
24
25 CHROM_SCALE = { 0 : ('C', 0),
26 1 : ('C', 1),
27 2 : ('D', 0),
28 3 : ('E', -1),
29 4 : ('E', 0),
30 5 : ('F', 0),
31 6 : ('F', 1),
32 7 : ('G', 0),
33 8 : ('G', 1),
34 9 : ('A', 0),
35 10 : ('B', -1),
36 11 : ('B', 0)}
37
38
39 FR_NOTES = {'C' : u'Do',
40 'D' : u'Ré',
41 'E' : u'Mi',
42 'F' : u'Fa',
43 'G' : u'Sol',
44 'A' : u'La',
45 'B' : u'Si'}
46
47 _marker = []
48
49 class Part(object) :
50
51 def __init__(self, node, autoDetectChorus=True) :
52 self.node = node
53 self.notes = []
54 self.repeats = []
55 self.distinctNotes = []
56 self._parseMusic()
57 self.verses = [[]]
58 self.chorus = []
59 if autoDetectChorus :
60 self._findChorus()
61 self._findVersesLoops()
62
63 def _parseMusic(self) :
64 divisions = 0
65 previous = None
66 distinctNotesDict = {}
67
68 for measureNode in self.node.getElementsByTagName('measure') :
69 measureNotes = []
70
71 # iteration sur les notes
72 # divisions de la noire
73 divisions = int(_getNodeValue(measureNode, 'attributes/divisions', divisions))
74 for noteNode in measureNode.getElementsByTagName('note') :
75 note = Note(noteNode, divisions, previous)
76 if (not note.isRest) and (not note.tiedStop) :
77 measureNotes.append(note)
78 if previous :
79 previous.next = note
80 elif note.tiedStop :
81 assert previous.tiedStart
82 previous.addDuration(note)
83 continue
84 else :
85 previous.addDuration(note)
86 continue
87 previous = note
88
89 self.notes.extend(measureNotes)
90
91 for note in measureNotes :
92 if not distinctNotesDict.has_key(note.midi) :
93 distinctNotesDict[note.midi] = True
94 self.distinctNotes.append(note)
95
96 # barres de reprises
97 try :
98 barlineNode = measureNode.getElementsByTagName('barline')[0]
99 except IndexError :
100 continue
101
102 barline = Barline(barlineNode, measureNotes)
103 if barline.repeat :
104 self.repeats.append(barline)
105
106 self.distinctNotes.sort(lambda a, b : cmp(a.midi, b.midi))
107
108
109 def _findChorus(self):
110 """ le refrain correspond aux notes pour lesquelles
111 il n'existe q'une seule syllable attachée.
112 """
113 start = stop = None
114 for i, note in enumerate(self.notes) :
115 ll = len(note.lyrics)
116 if start is None and ll == 1 :
117 start = i
118 elif start is not None and ll > 1 :
119 stop = i
120 break
121 if not (start or stop) :
122 self.chorus = []
123 else :
124 self.chorus = self.notes[start:stop]
125
126 def _findVersesLoops(self) :
127 "recherche des couplets / boucles"
128 verse = self.verses[0]
129 for note in self.notes[:-1] :
130 verse.append(note)
131 ll = len(note.lyrics)
132 nll = len(note.next.lyrics)
133 if ll != nll :
134 verse = []
135 self.verses.append(verse)
136 verse.append(self.notes[-1])
137
138
139 def iterNotes(self, indefinitely=True) :
140 "exécution de la chanson avec l'alternance couplets / refrains"
141 print 'indefinitely', indefinitely
142 if indefinitely == False :
143 iterable = self.verses
144 else :
145 iterable = cycle(self.verses)
146 for verse in iterable :
147 print "---partie---"
148 repeats = len(verse[0].lyrics)
149 if repeats > 1 :
150 for i in range(repeats) :
151 # couplet
152 print "---couplet%d---" % i
153 for note in verse :
154 yield note, i
155 # refrain
156 print "---refrain---"
157 for note in self.chorus :
158 yield note, 0
159 else :
160 for note in verse :
161 yield note, 0
162
163 def pprint(self) :
164 for note, verseIndex in self.iterNotes(indefinitely=False) :
165 print note, note.lyrics[verseIndex]
166
167
168 def assignNotesFromMidiNoteNumbers(self):
169 # TODO faire le mapping bande hauteur midi
170 for i in range(len(self.midiNoteNumbers)):
171 noteInExtendedScale = 0
172 while self.midiNoteNumbers[i] > self.scale[noteInExtendedScale] and noteInExtendedScale < len(self.scale)-1:
173 noteInExtendedScale += 1
174 if self.midiNoteNumbers[i]<self.scale[noteInExtendedScale]:
175 noteInExtendedScale -= 1
176 self.notes.append(noteInExtendedScale)
177
178
179 class Barline(object) :
180
181 def __init__(self, node, measureNotes) :
182 self.node = node
183 location = self.location = node.getAttribute('location') or 'right'
184 try :
185 repeatN = node.getElementsByTagName('repeat')[0]
186 repeat = {'direction' : repeatN.getAttribute('direction'),
187 'times' : int(repeatN.getAttribute('times') or 1)}
188 if location == 'left' :
189 repeat['note'] = measureNotes[0]
190 elif location == 'right' :
191 repeat['note'] = measureNotes[-1]
192 else :
193 raise ValueError(location)
194 self.repeat = repeat
195 except IndexError :
196 self.repeat = None
197
198 def __str__(self) :
199 if self.repeat :
200 if self.location == 'left' :
201 return '|:'
202 elif self.location == 'right' :
203 return ':|'
204 return '|'
205
206 __repr__ = __str__
207
208
209 class Tone(object) :
210
211 @staticmethod
212 def midi_to_step_alter_octave(midi):
213 stepIndex = midi % 12
214 step, alter = CHROM_SCALE[stepIndex]
215 octave = midi / 12 - 1
216 return step, alter, octave
217
218
219 def __init__(self, *args) :
220 if len(args) == 3 :
221 self.step, self.alter, self.octave = args
222 elif len(args) == 1 :
223 midi = args[0]
224 self.step, self.alter, self.octave = Tone.midi_to_step_alter_octave(midi)
225
226 @property
227 def midi(self) :
228 mid = DIATO_SCALE[self.step]
229 mid = mid + (self.octave - OCTAVE_REF) * 12
230 mid = mid + self.alter
231 return mid
232
233
234 @property
235 def name(self) :
236 name = '%s%d' % (self.step, self.octave)
237 if self.alter < 0 :
238 alterext = 'b'
239 else :
240 alterext = '#'
241 name = '%s%s' % (name, abs(self.alter) * alterext)
242 return name
243
244 @property
245 def nom(self) :
246 name = FR_NOTES[self.step]
247 if self.alter < 0 :
248 alterext = 'b'
249 else :
250 alterext = '#'
251 name = '%s%s' % (name, abs(self.alter) * alterext)
252 return name
253
254
255
256 class Note(Tone) :
257 scale = [55, 57, 59, 60, 62, 64, 65, 67, 69, 71, 72]
258
259 def __init__(self, node, divisions, previous) :
260 self.node = node
261 self.isRest = False
262 self.tiedStart = False
263 self.tiedStop = False
264
265 tieds = _getElementsByPath(node, 'notations/tied', [])
266 for tied in tieds :
267 if tied.getAttribute('type') == 'start' :
268 self.tiedStart = True
269 elif tied.getAttribute('type') == 'stop' :
270 self.tiedStop = True
271
272 self.step = _getNodeValue(node, 'pitch/step', None)
273 if self.step is not None :
274 self.octave = int(_getNodeValue(node, 'pitch/octave'))
275 self.alter = int(_getNodeValue(node, 'pitch/alter', 0))
276 elif self.node.getElementsByTagName('rest') :
277 self.isRest = True
278 else :
279 NotImplementedError(self.node.toxml('utf-8'))
280
281 self._duration = float(_getNodeValue(node, 'duration'))
282 self.lyrics = []
283 for ly in node.getElementsByTagName('lyric') :
284 self.lyrics.append(Lyric(ly))
285
286 self.divisions = divisions
287 self.previous = previous
288 self.next = None
289
290 def __str__(self) :
291 return (u'%5s %2s %2d %4s' % (self.nom, self.name, self.midi, round(self.duration, 2))).encode('utf-8')
292
293 def __repr__(self) :
294 return self.name.encode('utf-8')
295
296 def addDuration(self, note) :
297 self._duration = self.duration + note.duration
298 self.divisions = 1
299
300 @property
301 def duration(self) :
302 return self._duration / self.divisions
303
304 @property
305 def column(self):
306 return self.scale.index(self.midi)
307
308
309 class Lyric(object) :
310
311 _syllabicModifiers = {
312 'single' : u'%s',
313 'begin' : u'%s -',
314 'middle' : u'- %s -',
315 'end' : u'- %s'
316 }
317
318 def __init__(self, node) :
319 self.node = node
320 self.syllabic = _getNodeValue(node, 'syllabic', 'single')
321 self.text = _getNodeValue(node, 'text')
322
323 def syllabus(self):
324 text = self._syllabicModifiers[self.syllabic] % self.text
325 return text
326
327 def __str__(self) :
328 return self.syllabus().encode('utf-8')
329 __repr__ = __str__
330
331
332
333
334 def _getNodeValue(node, path, default=_marker) :
335 try :
336 for name in path.split('/') :
337 node = node.getElementsByTagName(name)[0]
338 return node.firstChild.nodeValue
339 except :
340 if default is _marker :
341 raise
342 else :
343 return default
344
345 def _getElementsByPath(node, path, default=_marker) :
346 try :
347 parts = path.split('/')
348 for name in parts[:-1] :
349 node = node.getElementsByTagName(name)[0]
350 return node.getElementsByTagName(parts[-1])
351 except IndexError :
352 if default is _marker :
353 raise
354 else :
355 return default
356
357 def musicXml2Song(input, partIndex=0, autoDetectChorus=True, printNotes=False) :
358 if isinstance(input, StringTypes) :
359 input = open(input, 'r')
360
361 d = parse(input)
362 doc = d.documentElement
363
364 # TODO conversion préalable score-timewise -> score-partwise
365 assert doc.nodeName == u'score-partwise'
366
367 parts = doc.getElementsByTagName('part')
368 leadPart = parts[partIndex]
369
370 part = Part(leadPart, autoDetectChorus=autoDetectChorus)
371
372 if printNotes :
373 part.pprint()
374
375 return part
376
377
378
379 def main() :
380 usage = "%prog musicXmlFile.xml [options]"
381 op = OptionParser(usage)
382 op.add_option("-i", "--part-index", dest="partIndex"
383 , default = 0
384 , help = "Index de la partie qui contient le champ.")
385
386 op.add_option("-p", '--print', dest='printNotes'
387 , action="store_true"
388 , default = False
389 , help = "Affiche les notes sur la sortie standard (debug)")
390
391 op.add_option("-c", '--no-chorus', dest='autoDetectChorus'
392 , action="store_false"
393 , default = True
394 , help = "désactive la détection du refrain")
395
396
397 options, args = op.parse_args()
398
399 if len(args) != 1 :
400 raise SystemExit(op.format_help())
401
402 musicXml2Song(args[0],
403 partIndex=options.partIndex,
404 autoDetectChorus=options.autoDetectChorus,
405 printNotes=options.printNotes)
406
407
408 if __name__ == '__main__' :
409 sys.exit(main())