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