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