Une jolie fleur complète.
[minwii.git] / src / minwii / loganalyse.py
1 # -*- coding: utf-8 -*-
2 """
3 Module d'analyse des fichiers de log minwii.
4
5 $Id$
6 $URL$
7 """
8
9 from minwii.logfilereader import LogFileReader
10 from pprint import pprint
11 from minwii.musicxml import musicXml2Song
12 from minwii.globals import PLAYING_MODES
13 from statlib import stats
14 from datetime import timedelta
15 from xml.etree import ElementTree
16 import os.path
17
18 PLAYING_MODES = dict(PLAYING_MODES)
19
20 DEFAULT_STATS = (#'geometricmean',
21 ('harmonicmean', 'Moyenne harmonique'),
22 ('mean', 'Moyenne '),
23 ('median', 'Médiane'),
24 #'medianscore',
25 #'mode',
26 #'moment',
27 ('variation', 'Variation'),
28 #'skew',
29 ('kurtosis', 'Kurtosis'),
30 #'itemfreq',
31 #'histogram',
32 #'cumfreq',
33 #'relfreq',
34 )
35
36 def statsresults(m) :
37 def computeList(self):
38 l = m(self)
39 results = []
40 for name, label in DEFAULT_STATS :
41 results.append('%s : %s' % (label, getattr(stats, name)(l)))
42 return '\n'.join(results)
43 computeList.__name__ = m.__name__
44 computeList.__doc__ = m.__doc__
45 return computeList
46
47 def timebased(m) :
48 m.timebased = True
49 return m
50
51 class LogFileAnalyser(LogFileReader) :
52
53 POSSIBLE_ANALYSES = {'BEGINNER' : ('songDuration',
54 'playingDuration',
55 'noteEndNoteOnLatency',
56 'realisationRate')
57
58 ,'EASY' : ('songDuration',
59 'playingDuration',
60 'noteEndNoteOnLatency',
61 'realisationRate',
62 'missCount',
63 'getMissPerTimeFrame')
64
65 ,'NORMAL' : ('songDuration',
66 'playingDuration',
67 'realisationRate',
68 'missCount',
69 'getMissPerTimeFrame')
70
71 ,'ADVANCED' : ('songDuration',
72 'playingDuration',
73 'realisationRate',
74 'missCount',
75 'getMissPerTimeFrame')
76
77 ,'EXPERT' : ('songDuration',
78 'playingDuration',
79 'realisationRate',
80 'missCount',
81 'getMissPerTimeFrame')
82 }
83
84 def analyse(self) :
85 results = []
86
87 try :
88 self.mode = mode = self.getMode()
89 results.append(('Mode de jeu', PLAYING_MODES.get(mode, mode), False))
90
91 self.songTitle = LogFileAnalyser.getSongTitle(self.getSongFile())
92 results.append(('Chanson', self.songTitle, False))
93
94 for name in self.POSSIBLE_ANALYSES[mode] :
95 meth = getattr(self, name)
96 results.append( (meth.__doc__, meth(), getattr(meth, 'timebased', False)) )
97 except :
98 raise
99
100 return results
101
102 @staticmethod
103 def getSongTitle(file) :
104 if os.path.exists(file) :
105 it = ElementTree.iterparse(file, ['start', 'end'])
106 creditFound = False
107
108 for evt, el in it :
109 if el.tag == 'credit' :
110 creditFound = True
111 if el.tag == 'credit-words' and creditFound:
112 return el.text
113 if el.tag == 'part-list' :
114 # plus de chance de trouver un titre
115 return os.path.basename(file)
116 else :
117 return os.path.basename(file)
118
119 def _toTimeDelta(self, milliseconds) :
120 duration = milliseconds / 1000.
121 duration = int(round(duration, 0))
122 return str(timedelta(seconds=duration))
123
124 def playingDuration(self) :
125 'Temps de jeu'
126 #retourne la durée écoulée entre le premier et de dernier message
127 #de type événement : correspond à la durée d'interprétation.
128
129 last = self.getLastEventTicks()
130 first = self.getFirstEventTicks()
131 return self._toTimeDelta(last - first)
132
133
134 def songDuration(self) :
135 'Durée de référence de la chanson'
136 #retourne la durée de référence de la chanson
137 #en prenant en compte le tempo présent dans la transcription
138 #et en effectuant toutes les répétitions des couplets / refrains.
139
140 songFile = self.getSongFile()
141 song = musicXml2Song(songFile)
142 duration = 0
143 for note, verseIndex in song.iterNotes() :
144 duration = duration + note.duration
145 duration = duration * song.quarterNoteDuration # en milisecondes
146 return self._toTimeDelta(duration)
147
148 @statsresults
149 def noteEndNoteOnLatency(self) :
150 'Réactivité'
151 eIter = self.getEventsIterator()
152 latencies = []
153 lastnoteEndT = 0
154
155 for ticks, eventName, message in eIter :
156 if eventName == 'NOTEEND':
157 lastnoteEndT = ticks
158 if eventName == 'NOTEON' and lastnoteEndT :
159 latencies.append(ticks - lastnoteEndT)
160
161 return latencies
162
163 def noteOnCount(self) :
164 "retourne le nombre d'événements NOTEON"
165
166 eIter = self.getEventsIterator()
167 cpt = 0
168
169 for ticks, eventName, message in eIter :
170 if eventName == 'NOTEON' :
171 cpt = cpt + 1
172
173 return cpt
174
175 def realisationRate(self) :
176 'Taux de réalisation'
177 #taux de réalisation en nombre de note
178 #peut être supérieur à 100 % car la chanson
179 #boucle à l'infini.
180
181 songFile = self.getSongFile()
182 song = musicXml2Song(songFile)
183 songNoteCpt = 0
184 for note, verseIndex in song.iterNotes() :
185 songNoteCpt = songNoteCpt + 1
186
187 return round(self.noteOnCount() / float(songNoteCpt) * 100, 1)
188
189 def missCount(self) :
190 "Nombre d'erreurs"
191 eIter = self.getEventsIterator()
192 miss = 0
193 if self.mode in ('EASY', 'NORMAL') :
194 catchColUp = False
195 for ticks, eventName, message in eIter :
196 if eventName == 'COLDOWN' :
197 colState = message.split(None, 2)[1]
198 colState = colState == 'True'
199 if colState :
200 catchColUp = False
201 continue
202 else :
203 catchColUp = True
204 elif eventName == 'NOTEON' :
205 catchColUp = False
206 elif eventName == 'COLUP' and catchColUp :
207 miss = miss + 1
208 else :
209 for ticks, eventName, message in eIter :
210 if eventName == 'COLDOWN' :
211 colState = message.split(None, 2)[1]
212 colState = colState == 'True'
213 if not colState :
214 miss = miss + 1
215
216 return miss
217
218 @timebased
219 def getMissPerTimeFrame(self, timeFrame=10000) :
220 "Nombre d'erreurs en fonction du temps"
221 eIter = self.getEventsIterator()
222 firstTicks = self.getFirstEventTicks()
223 frames = [0]
224
225 if self.mode in ('EASY', 'NORMAL') :
226 catchColUp = False
227 for ticks, eventName, message in eIter :
228 if ticks - firstTicks > timeFrame :
229 firstTicks = ticks
230 frames.append(0)
231
232 if eventName == 'COLDOWN' :
233 colState = message.split(None, 2)[1]
234 colState = colState == 'True'
235 if colState :
236 catchColUp = False
237 continue
238 else :
239 catchColUp = True
240 elif eventName == 'NOTEON' :
241 catchColUp = False
242 elif eventName == 'COLUP' and catchColUp :
243 frames[-1] = frames[-1] + 1
244 else :
245 for ticks, eventName, message in eIter :
246 if ticks - firstTicks > timeFrame :
247 firstTicks = ticks
248 frames.append(0)
249
250 if eventName == 'COLDOWN' :
251 colState = message.split(None, 2)[1]
252 colState = colState == 'True'
253 if not colState :
254 frames[-1] = frames[-1] + 1
255
256 return frames
257
258
259
260
261
262 def main() :
263 from optparse import OptionParser
264 usage = "%prog logfile"
265 op = OptionParser(usage)
266 options, args = op.parse_args()
267 if len(args) != 1 :
268 op.error("incorrect number of arguments")
269
270
271 lfa = LogFileAnalyser(args[0])
272 pprint(lfa.analyse())
273
274 if __name__ == "__main__" :
275 from os.path import realpath, sep
276 import sys
277 minwiipath = realpath(__file__).split(sep)
278 minwiipath = minwiipath[:-2]
279 minwiipath = sep.join(minwiipath)
280 sys.path.insert(1, minwiipath)
281 main()