d7e136fa3e782874bd29b99173a6e152626ecb6a
[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 statlib import stats
13 from datetime import timedelta
14
15 DEFAULT_STATS = ('geometricmean',
16 'harmonicmean',
17 'mean',
18 'median',
19 'medianscore',
20 #'mode',
21 'moment',
22 'variation',
23 'skew',
24 'kurtosis',
25 #'itemfreq',
26 #'histogram',
27 #'cumfreq',
28 #'relfreq',
29 )
30
31 def statsresults(m) :
32 def computeList(self):
33 l = m(self)
34 ret = {}
35 for name in DEFAULT_STATS :
36 ret[name] = getattr(stats, name)(l)
37 return ret
38 return computeList
39
40 class LogFileAnalyser(LogFileReader) :
41
42 POSSIBLE_ANALYSES = {'BEGINNER' : ('songDuration',
43 'playingDuration',
44 'noteEndNoteOnLatency',
45 'realisationRate')
46 ,'EASY' : ('songDuration',
47 'playingDuration',
48 'noteEndNoteOnLatency',
49 'realisationRate',
50 'missCount')
51 ,'NORMAL' : ('songDuration',
52 'playingDuration',
53 'realisationRate',
54 'missCount')
55 ,'ADVANCED' : ('songDuration',
56 'playingDuration',
57 'realisationRate',
58 'missCount')
59 ,'EXPERT' : ('songDuration',
60 'playingDuration',
61 'realisationRate',
62 'missCount')
63 }
64
65 def analyse(self) :
66 results = []
67
68 try :
69 self.mode = mode = self.getMode()
70 results.append(('Mode de jeu', mode))
71 for name in self.POSSIBLE_ANALYSES[mode] :
72 meth = getattr(self, name)
73 results.append((meth.__doc__, meth()))
74 except :
75 raise
76
77 return results
78
79 def _toTimeDelta(self, milliseconds) :
80 duration = milliseconds / 1000.
81 duration = int(round(duration, 0))
82 return str(timedelta(seconds=duration))
83
84 def playingDuration(self) :
85 'Temps de jeu'
86 #retourne la durée écoulée entre le premier et de dernier message
87 #de type événement : correspond à la durée d'interprétation.
88
89 last = self.getLastEventTicks()
90 first = self.getFirstEventTicks()
91 return self._toTimeDelta(last - first)
92
93
94 def songDuration(self) :
95 'Durée de référence de la chanson'
96 #retourne la durée de référence de la chanson
97 #en prenant en compte le tempo présent dans la transcription
98 #et en effectuant toutes les répétitions des couplets / refrains.
99
100 songFile = self.getSongFile()
101 song = musicXml2Song(songFile)
102 duration = 0
103 for note, verseIndex in song.iterNotes() :
104 duration = duration + note.duration
105 duration = duration * song.quarterNoteDuration # en milisecondes
106 return self._toTimeDelta(duration)
107
108 @statsresults
109 def noteEndNoteOnLatency(self) :
110 'Réactivité'
111 eIter = self.getEventsIterator()
112 latencies = []
113 lastnoteEndT = 0
114
115 for ticks, eventName, message in eIter :
116 if eventName == 'NOTEEND':
117 lastnoteEndT = ticks
118 if eventName == 'NOTEON' and lastnoteEndT :
119 latencies.append(ticks - lastnoteEndT)
120
121 return latencies
122
123 def noteOnCount(self) :
124 "retourne le nombre d'événements NOTEON"
125
126 eIter = self.getEventsIterator()
127 cpt = 0
128
129 for ticks, eventName, message in eIter :
130 if eventName == 'NOTEON' :
131 cpt = cpt + 1
132
133 return cpt
134
135 def realisationRate(self) :
136 'Taux de réalisation'
137 #taux de réalisation en nombre de note
138 #peut être supérieur à 100 % car la chanson
139 #boucle à l'infini.
140
141 songFile = self.getSongFile()
142 song = musicXml2Song(songFile)
143 songNoteCpt = 0
144 for note, verseIndex in song.iterNotes() :
145 songNoteCpt = songNoteCpt + 1
146
147 return round(self.noteOnCount() / float(songNoteCpt) * 100, 1)
148
149 def missCount(self) :
150 "Nombre d'erreurs"
151 eIter = self.getEventsIterator()
152 miss = 0
153 if self.mode in ('EASY', 'NORMAL') :
154 catchColUp = False
155 for ticks, eventName, message in eIter :
156 if eventName == 'COLDOWN' :
157 colState = message.split(None, 2)[1]
158 colState = colState == 'True'
159 if colState :
160 catchColUp = False
161 continue
162 else :
163 catchColUp = True
164 elif eventName == 'NOTEON' :
165 catchColUp = False
166 elif eventName == 'COLUP' and catchColUp :
167 miss = miss + 1
168 else :
169 for ticks, eventName, message in eIter :
170 if eventName == 'COLDOWN' :
171 colState = message.split(None, 2)[1]
172 colState = colState == 'True'
173 if not colState :
174 miss = miss + 1
175
176 return miss
177
178
179
180
181
182 def main() :
183 from optparse import OptionParser
184 usage = "%prog logfile"
185 op = OptionParser(usage)
186 options, args = op.parse_args()
187 if len(args) != 1 :
188 op.error("incorrect number of arguments")
189
190
191 lfa = LogFileAnalyser(args[0])
192 pprint(lfa.analyse())
193
194 if __name__ == "__main__" :
195 from os.path import realpath, sep
196 import sys
197 minwiipath = realpath(__file__).split(sep)
198 minwiipath = minwiipath[:-2]
199 minwiipath = sep.join(minwiipath)
200 sys.path.insert(1, minwiipath)
201 main()