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