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