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