]> CRI, Mines Paris - PSL - minwii.git/blobdiff - src/minwii/logfilereader.py
renommage de app en minwii (ça va tout péter…).
[minwii.git] / src / minwii / logfilereader.py
diff --git a/src/minwii/logfilereader.py b/src/minwii/logfilereader.py
new file mode 100755 (executable)
index 0000000..64711c1
--- /dev/null
@@ -0,0 +1,214 @@
+# -*- coding: utf-8 -*-
+"""
+Module de lecture des fichiers de log minwii
+
+$Id$
+$URL$
+"""
+
+from widgets.playingscreen import PlayingScreenBase
+from eventutils import EventDispatcher
+from events import eventCodes
+from synth import Synth
+from musicxml import musicXml2Song
+import pygame
+
+SUPPORTED_FILE_HEADER = 'ENV winwii log format version : 1.0'
+
+class LogFileReader(object) :
+    """
+    classe utilitaire pour l'accès aux données d'un fichier de log MinWii.
+    """
+    
+    def __init__(self, logfile) :
+        """ logfile : chemin d'accès au fichier de log MinWii.
+            le format supporté est actuellement la version 1.0 uniquement.
+        """
+        if isinstance(logfile, str) :
+            self.logfile = open(logfile, 'r')
+        else :
+            self.logfile = logfile
+        
+        firstline = self.next()
+        assert firstline == SUPPORTED_FILE_HEADER
+        
+    
+    def getSongFile(self) :
+        "retourne le chemin d'accès au fichier musicxml de la chanson"
+        f = self.logfile
+        pos = f.tell()
+
+        f.seek(0)
+        for l in self :
+            if l.startswith('APP chanson :') :
+                break
+        songfile = l.split(':', 1)[1].strip()
+        f.seek(pos)
+        return songfile
+    
+    def getSoundFontFile(self) :
+        "retourne le chemin d'accès au fichier de la soundfont (*.sf2)"
+        f = self.logfile
+        pos = f.tell()
+        f.seek(0)
+        for l in self :
+            if l.startswith('ENV soundfont :') :
+                break
+        soundFontFile = l.split(':', 1)[1].strip()
+        f.seek(pos)
+        return soundFontFile
+
+    def getBank(self) :
+        "retourne le paramètre bank du synthétiseur (entier)"
+        f = self.logfile
+        pos = f.tell()
+        f.seek(0)
+        for l in self :
+            if l.startswith('APP bank :') :
+                break
+        f.seek(pos)
+        bank = l.split(':', 1)[1].strip()
+        return int(bank)
+    
+    def getPreset(self) :
+        "retourne le paramètre preset du synthétiseur (entier)"
+        f = self.logfile
+        pos = f.tell()
+        f.seek(0)
+        for l in self :
+            if l.startswith('APP preset :') :
+                break
+        f.seek(pos)
+        preset = l.split(':', 1)[1].strip()
+        return int(preset)
+
+    def getScreenResolution(self) :
+        "retourne la résolution écran (tuple de deux entiers)"
+        f = self.logfile
+        pos = f.tell()
+        f.seek(0)
+        for l in self :
+            if l.startswith('ENV résolution écran :') :
+                break
+        screenResolution = eval(l.split(':', 1)[1].strip())
+        f.seek(pos)
+        return screenResolution
+    
+    def getFirstEventTicks(self) :
+        "retourne le timecode du premier événement (entier)"
+        f = self.logfile
+        pos = f.tell()
+        f.seek(0)
+        for l in self :
+            if l.startswith('EVT ') :
+                break
+        firstTicks = int(l.split(None, 2)[1])
+        f.seek(pos)
+        return firstTicks
+    
+    def __del__(self) :
+        self.logfile.close()
+    
+    def __iter__(self) :
+        return self
+    
+    def next(self) :
+        line = self.logfile.next().strip()
+        return line
+    
+    def getEventsIterator(self) :
+        """ Retourne un itérateur sur les événements.
+            Chaque itération retourne un tuple de 3 éléments :
+            (timecode, nom_événement, données) avec le typage :
+            (entier,   chaîne,        chaîne)
+        """
+        self.logfile.seek(0)
+        while True :
+            try :
+                l = self.next()
+            except StopIteration :
+                break
+            
+            if not l.startswith('EVT ') :
+                continue
+            try :
+                ticks, eventName, message = l.split(None, 3)[1:]
+                ticks = int(ticks)
+                yield ticks, eventName, message
+            except ValueError :
+                ticks, eventName = l.split(None, 3)[1:]
+                ticks = int(ticks)
+                yield ticks, eventName, ''
+                
+
+class LogFilePlayer(PlayingScreenBase) :
+    """
+    ré-exécution d'une chanson sur la base de son fichier de log.
+    """
+
+    def __init__(self, logfile) :
+        lfr = self.lfr = LogFileReader(logfile)
+        songFile = lfr.getSongFile()
+        soundFontFile = lfr.getSoundFontFile()
+        sfPath = lfr.getSoundFontFile()
+        bank = lfr.getBank()
+        preset = lfr.getPreset()
+        synth = Synth(sfPath=sfPath)
+        synth.program_select(0, bank, preset)
+        self.song = musicXml2Song(songFile)
+        screenResolution = lfr.getScreenResolution()
+        
+        pygame.display.set_mode(screenResolution)
+        
+        super(LogFilePlayer, self).__init__(synth, self.song.distinctNotes)
+    
+    def run(self):
+        self._running = True
+        clock = pygame.time.Clock()
+        pygame.display.flip()
+        pygame.mouse.set_visible(False)
+        
+        previousTicks = self.lfr.getFirstEventTicks()
+        eIter = self.lfr.getEventsIterator()
+
+        for ticks, eventName, message in eIter :
+            t0 = pygame.time.get_ticks()
+            if eventName == 'COLSTATECHANGE' :
+                parts = message.split(None, 4)
+                if len(parts) == 4 :
+                    parts.append('')
+                index, state, midi, name, syllabus = parts
+                index = int(index)
+                midi = int(midi)
+                state = state == 'True'
+                col = self.columns[midi]
+                col.update(state, syllabus=syllabus.decode('utf-8'))
+
+            elif eventName == 'NOTEON':
+                chan, key, vel = [int(v) for v in message.split(None, 2)]
+                self.synth.noteon(chan, key, vel)
+
+            elif eventName == 'NOTEOFF':
+                chan, key = [int(v) for v in message.split(None, 1)]
+                self.synth.noteoff(chan, key)
+            
+            elif eventName.startswith('COL') :
+                pos = [int(n) for n in message.split(None, 4)[-1].strip('()').split(',')]
+                self.cursor.setPosition(pos)
+            
+                
+            pygame.event.clear()
+
+            dirty = self.draw(pygame.display.get_surface())
+            pygame.display.update(dirty)
+            execTime = pygame.time.get_ticks() - t0
+            
+            delay = ticks - previousTicks - execTime
+            if delay > 0 :
+                pygame.time.wait(delay)
+            
+            previousTicks = ticks
+        
+        self.stop()
+        
+    
\ No newline at end of file