Ajout module (vide) d'analyse d'un fichier de log.
[minwii.git] / src / minwii / logfilereader.py
1 # -*- coding: utf-8 -*-
2 """
3 Module de lecture des fichiers de log minwii
4
5 $Id$
6 $URL$
7 """
8
9 from widgets.playingscreen import PlayingScreenBase
10 from eventutils import EventDispatcher
11 from events import eventCodes
12 from synth import Synth
13 from musicxml import musicXml2Song
14 import pygame
15
16 SUPPORTED_FILE_HEADER = 'ENV winwii log format version : 1.0'
17
18 class LogFileReader(object) :
19 """
20 classe utilitaire pour l'accès aux données d'un fichier de log MinWii.
21 """
22
23 def __init__(self, logfile) :
24 """ logfile : chemin d'accès au fichier de log MinWii.
25 le format supporté est actuellement la version 1.0 uniquement.
26 """
27 if isinstance(logfile, str) :
28 self.logfile = open(logfile, 'r')
29 else :
30 self.logfile = logfile
31
32 firstline = self.next()
33 assert firstline == SUPPORTED_FILE_HEADER
34
35
36 def getSongFile(self) :
37 "retourne le chemin d'accès au fichier musicxml de la chanson"
38 f = self.logfile
39 pos = f.tell()
40
41 f.seek(0)
42 for l in self :
43 if l.startswith('APP chanson :') :
44 break
45 songfile = l.split(':', 1)[1].strip()
46 f.seek(pos)
47 return songfile
48
49 def getSoundFontFile(self) :
50 "retourne le chemin d'accès au fichier de la soundfont (*.sf2)"
51 f = self.logfile
52 pos = f.tell()
53 f.seek(0)
54 for l in self :
55 if l.startswith('ENV soundfont :') :
56 break
57 soundFontFile = l.split(':', 1)[1].strip()
58 f.seek(pos)
59 return soundFontFile
60
61 def getBank(self) :
62 "retourne le paramètre bank du synthétiseur (entier)"
63 f = self.logfile
64 pos = f.tell()
65 f.seek(0)
66 for l in self :
67 if l.startswith('APP bank :') :
68 break
69 f.seek(pos)
70 bank = l.split(':', 1)[1].strip()
71 return int(bank)
72
73 def getPreset(self) :
74 "retourne le paramètre preset du synthétiseur (entier)"
75 f = self.logfile
76 pos = f.tell()
77 f.seek(0)
78 for l in self :
79 if l.startswith('APP preset :') :
80 break
81 f.seek(pos)
82 preset = l.split(':', 1)[1].strip()
83 return int(preset)
84
85 def getScreenResolution(self) :
86 "retourne la résolution écran (tuple de deux entiers)"
87 f = self.logfile
88 pos = f.tell()
89 f.seek(0)
90 for l in self :
91 if l.startswith('ENV résolution écran :') :
92 break
93 screenResolution = eval(l.split(':', 1)[1].strip())
94 f.seek(pos)
95 return screenResolution
96
97 def getMode(self) :
98 "retourne le niveau de difficulté"
99 f = self.logfile
100 pos = f.tell()
101 for l in self :
102 if l.startswith('APP mode :') :
103 break
104
105 mode = l.split(':', 1)[1].strip()
106 f.geek(pos)
107 return mode
108
109 def getFirstEventTicks(self) :
110 "retourne le timecode du premier événement (entier)"
111 f = self.logfile
112 pos = f.tell()
113 f.seek(0)
114 for l in self :
115 if l.startswith('EVT ') :
116 break
117 firstTicks = int(l.split(None, 2)[1])
118 f.seek(pos)
119 return firstTicks
120
121 def __del__(self) :
122 self.logfile.close()
123
124 def __iter__(self) :
125 return self
126
127 def next(self) :
128 line = self.logfile.next().strip()
129 return line
130
131 def getEventsIterator(self) :
132 """ Retourne un itérateur sur les événements.
133 Chaque itération retourne un tuple de 3 éléments :
134 (timecode, nom_événement, données) avec le typage :
135 (entier, chaîne, chaîne)
136 """
137 self.logfile.seek(0)
138 while True :
139 try :
140 l = self.next()
141 except StopIteration :
142 break
143
144 if not l.startswith('EVT ') :
145 continue
146 try :
147 ticks, eventName, message = l.split(None, 3)[1:]
148 ticks = int(ticks)
149 yield ticks, eventName, message
150 except ValueError :
151 ticks, eventName = l.split(None, 3)[1:]
152 ticks = int(ticks)
153 yield ticks, eventName, ''
154
155
156 class LogFilePlayer(PlayingScreenBase) :
157 """
158 ré-exécution d'une chanson sur la base de son fichier de log.
159 """
160
161 def __init__(self, logfile) :
162 lfr = self.lfr = LogFileReader(logfile)
163 songFile = lfr.getSongFile()
164 soundFontFile = lfr.getSoundFontFile()
165 sfPath = lfr.getSoundFontFile()
166 bank = lfr.getBank()
167 preset = lfr.getPreset()
168 synth = Synth(sfPath=sfPath)
169 synth.program_select(0, bank, preset)
170 self.song = musicXml2Song(songFile)
171 screenResolution = lfr.getScreenResolution()
172
173 pygame.display.set_mode(screenResolution)
174
175 super(LogFilePlayer, self).__init__(synth, self.song.distinctNotes)
176
177 def run(self):
178 self._running = True
179 clock = pygame.time.Clock()
180 pygame.display.flip()
181 pygame.mouse.set_visible(False)
182
183 previousTicks = self.lfr.getFirstEventTicks()
184 eIter = self.lfr.getEventsIterator()
185
186 for ticks, eventName, message in eIter :
187 t0 = pygame.time.get_ticks()
188 if eventName == 'COLSTATECHANGE' :
189 parts = message.split(None, 4)
190 if len(parts) == 4 :
191 parts.append('')
192 index, state, midi, name, syllabus = parts
193 index = int(index)
194 midi = int(midi)
195 state = state == 'True'
196 col = self.columns[midi]
197 col.update(state, syllabus=syllabus.decode('utf-8'))
198
199 elif eventName == 'NOTEON':
200 chan, key, vel = [int(v) for v in message.split(None, 2)]
201 self.synth.noteon(chan, key, vel)
202
203 elif eventName == 'NOTEOFF':
204 chan, key = [int(v) for v in message.split(None, 1)]
205 self.synth.noteoff(chan, key)
206
207 elif eventName.startswith('COL') :
208 pos = [int(n) for n in message.split(None, 4)[-1].strip('()').split(',')]
209 self.cursor.setPosition(pos)
210
211
212 pygame.event.clear()
213
214 dirty = self.draw(pygame.display.get_surface())
215 pygame.display.update(dirty)
216 execTime = pygame.time.get_ticks() - t0
217
218 delay = ticks - previousTicks - execTime
219 if delay > 0 :
220 pygame.time.wait(delay)
221
222 previousTicks = ticks
223
224 self.stop()
225
226