retrait TODO.
[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 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, indefinitely=True) :
112 "exécution de la chanson avec l'alternance couplets / refrains"
113 print 'indefinitely', indefinitely
114 if indefinitely == False :
115 iterable = self.verses
116 else :
117 iterable = cycle(self.verses)
118 for verse in iterable :
119 print "---partie---"
120 repeats = len(verse[0].lyrics)
121 if repeats > 1 :
122 for i in range(repeats) :
123 # couplet
124 print "---couplet%d---" % i
125 for note in verse :
126 yield note, i
127 # refrain
128 print "---refrain---"
129 for note in self.chorus :
130 yield note, 0
131 else :
132 for note in verse :
133 yield note, 0
134
135 def pprint(self) :
136 for note, verseIndex in self.iterNotes(indefinitely=False) :
137 print note, note.lyrics[verseIndex]
138
139
140 def assignNotesFromMidiNoteNumbers(self):
141 # TODO faire le mapping bande hauteur midi
142 for i in range(len(self.midiNoteNumbers)):
143 noteInExtendedScale = 0
144 while self.midiNoteNumbers[i] > self.scale[noteInExtendedScale] and noteInExtendedScale < len(self.scale)-1:
145 noteInExtendedScale += 1
146 if self.midiNoteNumbers[i]<self.scale[noteInExtendedScale]:
147 noteInExtendedScale -= 1
148 self.notes.append(noteInExtendedScale)
149
150
151 class Barline(object) :
152
153 def __init__(self, node, measureNotes) :
154 self.node = node
155 location = self.location = node.getAttribute('location') or 'right'
156 try :
157 repeatN = node.getElementsByTagName('repeat')[0]
158 repeat = {'direction' : repeatN.getAttribute('direction'),
159 'times' : int(repeatN.getAttribute('times') or 1)}
160 if location == 'left' :
161 repeat['note'] = measureNotes[0]
162 elif location == 'right' :
163 repeat['note'] = measureNotes[-1]
164 else :
165 raise ValueError(location)
166 self.repeat = repeat
167 except IndexError :
168 self.repeat = None
169
170 def __str__(self) :
171 if self.repeat :
172 if self.location == 'left' :
173 return '|:'
174 elif self.location == 'right' :
175 return ':|'
176 return '|'
177
178 __repr__ = __str__
179
180
181 class Note(object) :
182 scale = [55, 57, 59, 60, 62, 64, 65, 67, 69, 71, 72]
183
184 def __init__(self, node, divisions, previous) :
185 self.node = node
186 self.isRest = False
187 self.step = _getNodeValue(node, 'pitch/step', None)
188 if self.step is not None :
189 self.octave = int(_getNodeValue(node, 'pitch/octave'))
190 self.alter = int(_getNodeValue(node, 'pitch/alter', 0))
191 elif self.node.getElementsByTagName('rest') :
192 self.isRest = True
193 else :
194 NotImplementedError(self.node.toxml('utf-8'))
195
196 self._duration = float(_getNodeValue(node, 'duration'))
197 self.lyrics = []
198 for ly in node.getElementsByTagName('lyric') :
199 self.lyrics.append(Lyric(ly))
200
201 self.divisions = divisions
202 self.previous = previous
203 self.next = None
204
205 def __str__(self) :
206 return (u'%5s %2s %2d %4s' % (self.nom, self.name, self.midi, round(self.duration, 2))).encode('utf-8')
207
208 def __repr__(self) :
209 return self.name.encode('utf-8')
210
211 def addDuration(self, note) :
212 self._duration = self.duration + note.duration
213 self.divisions = 1
214
215 @property
216 def midi(self) :
217 mid = DIATO_SCALE[self.step]
218 mid = mid + (self.octave - OCTAVE_REF) * 12
219 mid = mid + self.alter
220 return mid
221
222 @property
223 def duration(self) :
224 return self._duration / self.divisions
225
226 @property
227 def name(self) :
228 name = '%s%d' % (self.step, self.octave)
229 if self.alter < 0 :
230 alterext = 'b'
231 else :
232 alterext = '#'
233 name = '%s%s' % (name, abs(self.alter) * alterext)
234 return name
235
236 @property
237 def nom(self) :
238 name = FR_NOTES[self.step]
239 if self.alter < 0 :
240 alterext = 'b'
241 else :
242 alterext = '#'
243 name = '%s%s' % (name, abs(self.alter) * alterext)
244 return name
245
246 @property
247 def column(self):
248 return self.scale.index(self.midi)
249
250
251 class Lyric(object) :
252
253 _syllabicModifiers = {
254 'single' : '%s',
255 'begin' : '%s -',
256 'middle' : '- %s -',
257 'end' : '- %s'
258 }
259
260 def __init__(self, node) :
261 self.node = node
262 self.syllabic = _getNodeValue(node, 'syllabic', 'single')
263 self.text = _getNodeValue(node, 'text')
264
265 def syllabus(self, encoding='utf-8'):
266 text = self._syllabicModifiers[self.syllabic] % self.text
267 return text.encode(encoding)
268
269 def __str__(self) :
270 return self.syllabus()
271 __repr__ = __str__
272
273
274
275
276 def _getNodeValue(node, path, default=_marker) :
277 try :
278 for name in path.split('/') :
279 node = node.getElementsByTagName(name)[0]
280 return node.firstChild.nodeValue
281 except :
282 if default is _marker :
283 raise
284 else :
285 return default
286
287 def musicXml2Song(input, partIndex=0, printNotes=False) :
288 if isinstance(input, StringTypes) :
289 input = open(input, 'r')
290
291 d = parse(input)
292 doc = d.documentElement
293
294 # TODO conversion préalable score-timewise -> score-partwise
295 assert doc.nodeName == u'score-partwise'
296
297 parts = doc.getElementsByTagName('part')
298 leadPart = parts[partIndex]
299
300 part = Part(leadPart)
301
302 if printNotes :
303 part.pprint()
304
305 return part
306
307
308 def main() :
309 usage = "%prog musicXmlFile.xml [options]"
310 op = OptionParser(usage)
311 op.add_option("-i", "--part-index", dest="partIndex"
312 , default = 0
313 , help = "Index de la partie qui contient le champ.")
314 op.add_option("-p", '--print', dest='printNotes'
315 , action="store_true"
316 , default = False
317 , help = "Affiche les notes sur la sortie standard (debug)")
318
319 options, args = op.parse_args()
320
321 if len(args) != 1 :
322 raise SystemExit(op.format_help())
323
324 musicXml2Song(args[0], partIndex=options.partIndex, printNotes=options.printNotes)
325
326
327
328 if __name__ == '__main__' :
329 sys.exit(main())