Ajout d'une option pour afficher / masquer les noms de notes en bas du clavier.
[minwii.git] / src / minwii / loganalyse.py
index 4a0460a..f3e5e32 100755 (executable)
@@ -5,3 +5,277 @@ Module d'analyse des fichiers de log minwii.
 $Id$
 $URL$
 """
 $Id$
 $URL$
 """
+
+from minwii.logfilereader import LogFileReader
+from pprint import pprint
+from minwii.musicxml import musicXml2Song
+from minwii.globals import PLAYING_MODES
+from statlib import stats
+from datetime import timedelta
+from xml.etree import ElementTree
+import os.path
+
+PLAYING_MODES = dict(PLAYING_MODES)
+
+DEFAULT_STATS = (#'geometricmean',
+                 ('harmonicmean', 'Moyenne harmonique'),
+                 ('mean', 'Moyenne  '),
+                 ('median', 'Médiane'),
+                 #'medianscore',
+                 #'mode',
+                 #'moment',
+                 ('variation', 'Variation'),
+                 #'skew',
+                 ('kurtosis', 'Kurtosis'),
+                 #'itemfreq',
+                 #'histogram',
+                 #'cumfreq',
+                 #'relfreq',
+                 )
+
+def statsresults(m) :
+    def computeList(self):
+        l = m(self)
+        results = []
+        for name, label in DEFAULT_STATS :
+            results.append('%s : %s' % (label, getattr(stats, name)(l)))
+        return '\n'.join(results)
+    computeList.__name__ = m.__name__
+    computeList.__doc__ = m.__doc__
+    return computeList
+
+def timebased(m) :
+    m.timebased = True
+    return m
+
+class LogFileAnalyser(LogFileReader) :
+
+    POSSIBLE_ANALYSES = {'BEGINNER' : ('songDuration',
+                                       'playingDuration',
+                                       'noteEndNoteOnLatency',
+                                       'realisationRate')
+                                       
+                        ,'EASY'     : ('songDuration',
+                                       'playingDuration',
+                                       'noteEndNoteOnLatency',
+                                       'realisationRate',
+                                       'missCount',
+                                       'getMissPerTimeFrame')
+                                       
+                        ,'NORMAL'   : ('songDuration',
+                                       'playingDuration',
+                                       'realisationRate',
+                                       'missCount',
+                                       'getMissPerTimeFrame')
+                                       
+                        ,'ADVANCED' : ('songDuration',
+                                       'playingDuration',
+                                       'realisationRate',
+                                       'missCount',
+                                       'getMissPerTimeFrame')
+                                       
+                        ,'EXPERT'   : ('songDuration',
+                                       'playingDuration',
+                                       'realisationRate',
+                                       'missCount',
+                                       'getMissPerTimeFrame')
+                        }
+    
+    def analyse(self) :
+        results = []
+        
+        try :
+            self.mode = mode = self.getMode()
+            results.append(('Mode de jeu', PLAYING_MODES.get(mode, mode), False))
+
+            self.songTitle = LogFileAnalyser.getSongTitle(self.getSongFile())
+            results.append(('Chanson', self.songTitle, False))
+
+            for name in self.POSSIBLE_ANALYSES[mode] :
+                meth = getattr(self, name)
+                results.append( (meth.__doc__, meth(), getattr(meth, 'timebased', False)) )
+        except :
+            raise
+        
+        return results
+    
+    @staticmethod
+    def getSongTitle(file) :
+        if os.path.exists(file) :
+            it = ElementTree.iterparse(file, ['start', 'end'])
+            creditFound = False
+
+            for evt, el in it :
+                if el.tag == 'credit' :
+                    creditFound = True
+                if el.tag == 'credit-words' and creditFound:
+                    return el.text
+                if el.tag == 'part-list' :
+                    # plus de chance de trouver un titre
+                    return os.path.basename(file)
+        else :
+            return os.path.basename(file)
+
+    def _toTimeDelta(self, milliseconds) :
+        duration = milliseconds / 1000.
+        duration = int(round(duration, 0))
+        return str(timedelta(seconds=duration))
+    
+    def playingDuration(self) :
+        'Temps de jeu'
+        #retourne la durée écoulée entre le premier et de dernier message
+        #de type événement : correspond à la durée d'interprétation.
+        
+        last = self.getLastEventTicks()
+        first = self.getFirstEventTicks()
+        return self._toTimeDelta(last - first)
+        
+    
+    def songDuration(self) :
+        'Durée de référence de la chanson'
+        #retourne la durée de référence de la chanson
+        #en prenant en compte le tempo présent dans la transcription
+        #et en effectuant toutes les répétitions des couplets / refrains.
+        
+        songFile = self.getSongFile()
+        song = musicXml2Song(songFile)
+        duration = 0
+        for note, verseIndex in song.iterNotes() :
+            duration = duration + note.duration
+        duration = duration * song.quarterNoteDuration # en milisecondes
+        return self._toTimeDelta(duration)
+    
+    @statsresults
+    def noteEndNoteOnLatency(self) :
+        'Réactivité'
+        eIter = self.getEventsIterator()
+        latencies = []
+        lastnoteEndT = 0
+        
+        for ticks, eventName, message in eIter :
+            if eventName == 'NOTEEND':
+                lastnoteEndT = ticks
+            if eventName == 'NOTEON' and lastnoteEndT :
+                latencies.append(ticks - lastnoteEndT)
+        
+        return latencies
+    
+    def noteOnCount(self) :
+        "retourne le nombre d'événements NOTEON"
+        
+        eIter = self.getEventsIterator()
+        cpt = 0        
+
+        for ticks, eventName, message in eIter :
+            if eventName == 'NOTEON' :
+                cpt = cpt + 1
+        
+        return cpt
+    
+    def realisationRate(self) :
+        'Taux de réalisation'
+        #taux de réalisation en nombre de note
+        #peut être supérieur à 100 % car la chanson
+        #boucle à l'infini.
+        
+        songFile = self.getSongFile()
+        song = musicXml2Song(songFile)
+        songNoteCpt = 0
+        for note, verseIndex in song.iterNotes() :
+            songNoteCpt = songNoteCpt + 1
+        
+        return round(self.noteOnCount() / float(songNoteCpt) * 100, 1)
+    
+    def missCount(self) :
+        "Nombre d'erreurs"
+        eIter = self.getEventsIterator()
+        miss = 0
+        if self.mode in ('EASY', 'NORMAL') :
+            catchColUp = False
+            for ticks, eventName, message in eIter :
+                if eventName == 'COLDOWN' :
+                    colState = message.split(None, 2)[1]
+                    colState = colState == 'True'
+                    if colState :
+                        catchColUp = False
+                        continue
+                    else :
+                        catchColUp = True
+                elif eventName == 'NOTEON' :
+                    catchColUp = False
+                elif eventName == 'COLUP' and catchColUp :
+                    miss = miss + 1
+        else :
+            for ticks, eventName, message in eIter :
+                if eventName == 'COLDOWN' :
+                    colState = message.split(None, 2)[1]
+                    colState = colState == 'True'
+                    if not colState :
+                        miss = miss + 1
+        
+        return miss
+    
+    @timebased
+    def getMissPerTimeFrame(self, timeFrame=10000) :
+        "Nombre d'erreurs en fonction du temps"
+        eIter = self.getEventsIterator()
+        firstTicks = self.getFirstEventTicks()
+        frames = [0]
+
+        if self.mode in ('EASY', 'NORMAL') :
+            catchColUp = False
+            for ticks, eventName, message in eIter :
+                if ticks - firstTicks > timeFrame :
+                    firstTicks = ticks
+                    frames.append(0)
+                    
+                if eventName == 'COLDOWN' :
+                    colState = message.split(None, 2)[1]
+                    colState = colState == 'True'
+                    if colState :
+                        catchColUp = False
+                        continue
+                    else :
+                        catchColUp = True
+                elif eventName == 'NOTEON' :
+                    catchColUp = False
+                elif eventName == 'COLUP' and catchColUp :
+                    frames[-1] = frames[-1] + 1
+        else :
+            for ticks, eventName, message in eIter :
+                if ticks - firstTicks > timeFrame :
+                    firstTicks = ticks
+                    frames.append(0)
+                
+                if eventName == 'COLDOWN' :
+                    colState = message.split(None, 2)[1]
+                    colState = colState == 'True'
+                    if not colState :
+                        frames[-1] = frames[-1] + 1
+        
+        return frames
+                
+        
+        
+        
+
+def main() :
+    from optparse import OptionParser
+    usage = "%prog logfile"
+    op = OptionParser(usage)
+    options, args = op.parse_args()
+    if len(args) != 1 :
+        op.error("incorrect number of arguments")
+
+
+    lfa = LogFileAnalyser(args[0])
+    pprint(lfa.analyse())
+
+if __name__ == "__main__" :
+    from os.path import realpath, sep
+    import sys
+    minwiipath = realpath(__file__).split(sep)
+    minwiipath = minwiipath[:-2]
+    minwiipath = sep.join(minwiipath)
+    sys.path.insert(1, minwiipath)
+    main()