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