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