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