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