ab8c819583045932d361a9f1241985b47eb74673
[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 class LogFileAnalyser(LogFileReader) :
48
49 POSSIBLE_ANALYSES = {'BEGINNER' : ('songDuration',
50 'playingDuration',
51 'noteEndNoteOnLatency',
52 'realisationRate')
53 ,'EASY' : ('songDuration',
54 'playingDuration',
55 'noteEndNoteOnLatency',
56 'realisationRate',
57 'missCount')
58 ,'NORMAL' : ('songDuration',
59 'playingDuration',
60 'realisationRate',
61 'missCount')
62 ,'ADVANCED' : ('songDuration',
63 'playingDuration',
64 'realisationRate',
65 'missCount')
66 ,'EXPERT' : ('songDuration',
67 'playingDuration',
68 'realisationRate',
69 'missCount')
70 }
71
72 def analyse(self) :
73 results = []
74
75 try :
76 self.mode = mode = self.getMode()
77 results.append(('Mode de jeu', PLAYING_MODES.get(mode, mode)))
78
79 self.songTitle = LogFileAnalyser.getSongTitle(self.getSongFile())
80 results.append(('Chanson', self.songTitle))
81
82 for name in self.POSSIBLE_ANALYSES[mode] :
83 meth = getattr(self, name)
84 results.append((meth.__doc__, meth()))
85 except :
86 raise
87
88 return results
89
90 @staticmethod
91 def getSongTitle(file) :
92 if os.path.exists(file) :
93 it = ElementTree.iterparse(file, ['start', 'end'])
94 creditFound = False
95
96 for evt, el in it :
97 if el.tag == 'credit' :
98 creditFound = True
99 if el.tag == 'credit-words' and creditFound:
100 return el.text
101 if el.tag == 'part-list' :
102 # plus de chance de trouver un titre
103 return os.path.basename(file)
104 else :
105 return os.path.basename(file)
106
107 def _toTimeDelta(self, milliseconds) :
108 duration = milliseconds / 1000.
109 duration = int(round(duration, 0))
110 return str(timedelta(seconds=duration))
111
112 def playingDuration(self) :
113 'Temps de jeu'
114 #retourne la durée écoulée entre le premier et de dernier message
115 #de type événement : correspond à la durée d'interprétation.
116
117 last = self.getLastEventTicks()
118 first = self.getFirstEventTicks()
119 return self._toTimeDelta(last - first)
120
121
122 def songDuration(self) :
123 'Durée de référence de la chanson'
124 #retourne la durée de référence de la chanson
125 #en prenant en compte le tempo présent dans la transcription
126 #et en effectuant toutes les répétitions des couplets / refrains.
127
128 songFile = self.getSongFile()
129 song = musicXml2Song(songFile)
130 duration = 0
131 for note, verseIndex in song.iterNotes() :
132 duration = duration + note.duration
133 duration = duration * song.quarterNoteDuration # en milisecondes
134 return self._toTimeDelta(duration)
135
136 @statsresults
137 def noteEndNoteOnLatency(self) :
138 'Réactivité'
139 eIter = self.getEventsIterator()
140 latencies = []
141 lastnoteEndT = 0
142
143 for ticks, eventName, message in eIter :
144 if eventName == 'NOTEEND':
145 lastnoteEndT = ticks
146 if eventName == 'NOTEON' and lastnoteEndT :
147 latencies.append(ticks - lastnoteEndT)
148
149 return latencies
150
151 def noteOnCount(self) :
152 "retourne le nombre d'événements NOTEON"
153
154 eIter = self.getEventsIterator()
155 cpt = 0
156
157 for ticks, eventName, message in eIter :
158 if eventName == 'NOTEON' :
159 cpt = cpt + 1
160
161 return cpt
162
163 def realisationRate(self) :
164 'Taux de réalisation'
165 #taux de réalisation en nombre de note
166 #peut être supérieur à 100 % car la chanson
167 #boucle à l'infini.
168
169 songFile = self.getSongFile()
170 song = musicXml2Song(songFile)
171 songNoteCpt = 0
172 for note, verseIndex in song.iterNotes() :
173 songNoteCpt = songNoteCpt + 1
174
175 return round(self.noteOnCount() / float(songNoteCpt) * 100, 1)
176
177 def missCount(self) :
178 "Nombre d'erreurs"
179 eIter = self.getEventsIterator()
180 miss = 0
181 if self.mode in ('EASY', 'NORMAL') :
182 catchColUp = False
183 for ticks, eventName, message in eIter :
184 if eventName == 'COLDOWN' :
185 colState = message.split(None, 2)[1]
186 colState = colState == 'True'
187 if colState :
188 catchColUp = False
189 continue
190 else :
191 catchColUp = True
192 elif eventName == 'NOTEON' :
193 catchColUp = False
194 elif eventName == 'COLUP' and catchColUp :
195 miss = miss + 1
196 else :
197 for ticks, eventName, message in eIter :
198 if eventName == 'COLDOWN' :
199 colState = message.split(None, 2)[1]
200 colState = colState == 'True'
201 if not colState :
202 miss = miss + 1
203
204 return miss
205
206
207
208
209
210 def main() :
211 from optparse import OptionParser
212 usage = "%prog logfile"
213 op = OptionParser(usage)
214 options, args = op.parse_args()
215 if len(args) != 1 :
216 op.error("incorrect number of arguments")
217
218
219 lfa = LogFileAnalyser(args[0])
220 pprint(lfa.analyse())
221
222 if __name__ == "__main__" :
223 from os.path import realpath, sep
224 import sys
225 minwiipath = realpath(__file__).split(sep)
226 minwiipath = minwiipath[:-2]
227 minwiipath = sep.join(minwiipath)
228 sys.path.insert(1, minwiipath)
229 main()