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