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