exécution de la chanson avec alternance couplets / refrains.
[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 def __init__(self, node) :
142 self.node = node
143 self.syllabic = _getNodeValue(node, 'syllabic', 'single')
144 self.text = _getNodeValue(node, 'text')
145
146 def __str__(self) :
147 return self.text.encode('utf-8')
148 __repr__ = __str__
149
150
151
152
153 def _getNodeValue(node, path, default=_marker) :
154 try :
155 for name in path.split('/') :
156 node = node.getElementsByTagName(name)[0]
157 return node.firstChild.nodeValue
158 except :
159 if default is _marker :
160 raise
161 else :
162 return default
163
164 def musicXml2Song(input, output, partIndex=0, printNotes=False) :
165 if isinstance(input, StringTypes) :
166 input = open(input, 'r')
167
168 d = parse(input)
169 doc = d.documentElement
170
171 # TODO conversion préalable score-timewise -> score-partwise
172 assert doc.nodeName == u'score-partwise'
173
174 parts = doc.getElementsByTagName('part')
175 leadPart = parts[partIndex]
176
177 part = Part(leadPart)
178
179 if printNotes :
180 part.pprint()
181
182 # divisions de la noire
183 # divisions = 0
184 # midiNotes, durations, lyrics = [], [], []
185 #
186 # for measureNode in leadPart.getElementsByTagName('measure') :
187 # divisions = int(_getNodeValue(measureNode, 'attributes/divisions', divisions))
188 # for noteNode in measureNode.getElementsByTagName('note') :
189 # note = Note(noteNode, divisions)
190 # if printNotes :
191 # print note.name, note.midi, note.duration, note.lyric
192 # midiNotes.append(note.midi)
193 # durations.append(note.duration)
194 # lyrics.append(note.lyric)
195 #
196 # song = Song(None,
197 # midiNoteNumbers = midiNotes,
198 # noteLengths = durations,
199 # lyrics = lyrics,
200 # notesInExtendedScale=None)
201 # song.save(output)
202
203
204 def main() :
205 usage = "%prog musicXmlFile.xml outputSongFile.smwi [options]"
206 op = OptionParser(usage)
207 op.add_option("-i", "--part-index", dest="partIndex"
208 , default = 0
209 , help = "Index de la partie qui contient le champ.")
210 op.add_option("-p", '--print', dest='printNotes'
211 , action="store_true"
212 , default = False
213 , help = "Affiche les notes sur la sortie standard (debug)")
214
215 options, args = op.parse_args()
216
217 if len(args) != 2 :
218 raise SystemExit(op.format_help())
219
220 musicXml2Song(args[0], args[1], partIndex=options.partIndex, printNotes=options.printNotes)
221
222
223
224 if __name__ == '__main__' :
225 sys.exit(main())