Gestion explicite de KeyboardInterrupt pour être sûr de quitter le jeu sur un ^C.
[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 types import StringTypes
10 from widgets.playingscreen import PlayingScreenBase
11 from eventutils import EventDispatcher
12 from events import eventCodes
13 from synth import Synth
14 from musicxml import musicXml2Song
15 import pygame
16 from backwardsfilereader import BackwardsReader
17
18 SUPPORTED_FILE_HEADER = 'ENV winwii log format version : 1.0'
19
20 def inplaceread(m) :
21 def readinplace(self, *args, **kw) :
22 pos = self.logfile.tell()
23 self.logfile.seek(0)
24 ret = m(self, *args, **kw)
25 self.logfile.seek(pos)
26 return ret
27 return readinplace
28
29 class LogFileReader(object) :
30 """
31 classe utilitaire pour l'accès aux données d'un fichier de log MinWii.
32 """
33
34 def __init__(self, logfile, mode='r') :
35 """ logfile : chemin d'accès au fichier de log MinWii.
36 le format supporté est actuellement la version 1.0 uniquement.
37 """
38 if isinstance(logfile, StringTypes) :
39 self.logfile = open(logfile, mode)
40 else :
41 self.logfile = logfile
42
43 firstline = self.next()
44 assert firstline == SUPPORTED_FILE_HEADER
45
46
47 @inplaceread
48 def getSongFile(self) :
49 "retourne le chemin d'accès au fichier musicxml de la chanson"
50 for l in self :
51 if l.startswith('APP chanson :') :
52 break
53 songfile = l.split(':', 1)[1].strip()
54 return songfile
55
56 @inplaceread
57 def getSoundFontFile(self) :
58 "retourne le chemin d'accès au fichier de la soundfont (*.sf2)"
59 for l in self :
60 if l.startswith('ENV soundfont :') :
61 break
62 soundFontFile = l.split(':', 1)[1].strip()
63 return soundFontFile
64
65 @inplaceread
66 def getBank(self) :
67 "retourne le paramètre bank du synthétiseur (entier)"
68 for l in self :
69 if l.startswith('APP bank :') :
70 break
71 bank = l.split(':', 1)[1].strip()
72 return int(bank)
73
74 @inplaceread
75 def getPreset(self) :
76 "retourne le paramètre preset du synthétiseur (entier)"
77 for l in self :
78 if l.startswith('APP preset :') :
79 break
80 preset = l.split(':', 1)[1].strip()
81 return int(preset)
82
83 @inplaceread
84 def getScreenResolution(self) :
85 "retourne la résolution écran (tuple de deux entiers)"
86 for l in self :
87 if l.startswith('ENV résolution écran :') :
88 break
89 screenResolution = eval(l.split(':', 1)[1].strip())
90 return screenResolution
91
92 @inplaceread
93 def getMode(self) :
94 "retourne le niveau de difficulté"
95 for l in self :
96 if l.startswith('APP mode :') :
97 break
98
99 mode = l.split(':', 1)[1].strip()
100 return mode
101
102 @inplaceread
103 def getHID(self) :
104 "retourne l'interface homme-machine utilisée"
105 for l in self :
106 if l.startswith('APP HID :') :
107 break
108
109 mode = l.split(':', 1)[1].strip()
110 return mode
111
112 @inplaceread
113 def getFirstEventTicks(self) :
114 "retourne le timecode du premier événement (entier)"
115 for l in self :
116 if l.startswith('EVT ') :
117 break
118 firstTicks = int(l.split(None, 2)[1])
119 return firstTicks
120
121 @inplaceread
122 def getLastEventTicks(self) :
123 "retourne le timecode du dernier événement (entier)"
124 for l in self.getBackwardLineIterator() :
125 if l.startswith('EVT ') :
126 break
127 else :
128 return None
129
130 lastTicks = int(l.split(None, 2)[1])
131 return lastTicks
132
133 def __del__(self) :
134 self.close()
135
136 def __iter__(self) :
137 return self
138
139 def next(self) :
140 line = self.logfile.next().strip()
141 return line
142
143 def getEventsIterator(self) :
144 """ Retourne un itérateur sur les événements.
145 Chaque itération retourne un tuple de 3 éléments :
146 (timecode, nom_événement, données) avec le typage :
147 (entier, chaîne, chaîne)
148 """
149 self.logfile.seek(0)
150 while True :
151 try :
152 l = self.next()
153 except StopIteration :
154 break
155
156 if not l.startswith('EVT ') :
157 continue
158 try :
159 ticks, eventName, message = l.split(None, 3)[1:]
160 ticks = int(ticks)
161 yield ticks, eventName, message
162 except ValueError :
163 ticks, eventName = l.split(None, 3)[1:]
164 ticks = int(ticks)
165 yield ticks, eventName, ''
166
167 def getBackwardLineIterator(self) :
168 br = BackwardsReader(self.logfile, BLKSIZE=128)
169 line = br.readline()
170 while line :
171 yield line.strip()
172 line = br.readline()
173
174 @inplaceread
175 def getMetadata(self) :
176 metadata = {}
177 self.next() # skip identification line.
178 line = self.next()
179 while line.startswith('METADATA ') :
180 line = line.split(None, 1)[1]
181 name, value = [v.strip() for v in line.split(':', 1)]
182 metadata[name] = value
183 line = self.next()
184 return metadata
185
186 def setMetadata(self, metadata) :
187 f = self.logfile
188 f.seek(0)
189 before = f.readline()
190 line = f.readline()
191 while line.startswith('METADATA ') :
192 line = f.readline()
193 after = line + f.read()
194
195 lines = []
196 for name, value in metadata :
197 lines.append('METADATA %s : %s' % (name, value.encode('utf-8')))
198 metadata = '\n'.join(lines)
199 f.seek(0)
200 f.write(before)
201 f.write(metadata)
202 f.write(after)
203
204 def close(self) :
205 self.logfile.close()
206
207
208
209 class LogFilePlayer(PlayingScreenBase) :
210 """
211 ré-exécution d'une chanson sur la base de son fichier de log.
212 """
213
214 def __init__(self, logfile) :
215 lfr = self.lfr = LogFileReader(logfile)
216 songFile = lfr.getSongFile()
217 soundFontFile = lfr.getSoundFontFile()
218 sfPath = lfr.getSoundFontFile()
219 bank = lfr.getBank()
220 preset = lfr.getPreset()
221 synth = Synth(sfPath=sfPath)
222 synth.program_select(0, bank, preset)
223 self.song = musicXml2Song(songFile)
224 screenResolution = lfr.getScreenResolution()
225
226 pygame.display.set_mode(screenResolution)
227
228 super(LogFilePlayer, self).__init__(synth, self.song.distinctNotes)
229
230 def run(self):
231 self._running = True
232 clock = pygame.time.Clock()
233 pygame.display.flip()
234 pygame.mouse.set_visible(False)
235
236 previousTicks = self.lfr.getFirstEventTicks()
237 eIter = self.lfr.getEventsIterator()
238
239 for ticks, eventName, message in eIter :
240 t0 = pygame.time.get_ticks()
241 if eventName == 'COLSTATECHANGE' :
242 parts = message.split(None, 4)
243 if len(parts) == 4 :
244 parts.append('')
245 index, state, midi, name, syllabus = parts
246 index = int(index)
247 midi = int(midi)
248 state = state == 'True'
249 col = self.columns[midi]
250 col.update(state, syllabus=syllabus.decode('utf-8'))
251
252 elif eventName == 'NOTEON':
253 chan, key, vel = [int(v) for v in message.split(None, 2)]
254 self.synth.noteon(chan, key, vel)
255
256 elif eventName == 'NOTEOFF':
257 chan, key = [int(v) for v in message.split(None, 1)]
258 self.synth.noteoff(chan, key)
259
260 elif eventName.startswith('COL') :
261 pos = [int(n) for n in message.split(None, 4)[-1].strip('()').split(',')]
262 self.cursor.setPosition(pos)
263
264
265 pygame.event.clear()
266
267 dirty = self.draw(pygame.display.get_surface())
268 pygame.display.update(dirty)
269 execTime = pygame.time.get_ticks() - t0
270
271 delay = ticks - previousTicks - execTime
272 if delay > 0 :
273 pygame.time.wait(delay)
274
275 previousTicks = ticks
276
277 self.stop()
278