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