b7f84a1308f863fa9b21b78ea079a9a89ff50a2e
[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) :
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, 'r')
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 getFirstEventTicks(self) :
104 "retourne le timecode du premier événement (entier)"
105 for l in self :
106 if l.startswith('EVT ') :
107 break
108 firstTicks = int(l.split(None, 2)[1])
109 return firstTicks
110
111 @inplaceread
112 def getLastEventTicks(self) :
113 "retourne le timecode du dernier événement (entier)"
114 for l in self.getBackwardLineIterator() :
115 if l.startswith('EVT ') :
116 break
117
118 lastTicks = int(l.split(None, 2)[1])
119 return lastTicks
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 def getBackwardLineIterator(self) :
156 br = BackwardsReader(self.logfile, BLKSIZE=128)
157 line = br.readline()
158 while line :
159 yield line.strip()
160 line = br.readline()
161
162
163
164 class LogFilePlayer(PlayingScreenBase) :
165 """
166 ré-exécution d'une chanson sur la base de son fichier de log.
167 """
168
169 def __init__(self, logfile) :
170 lfr = self.lfr = LogFileReader(logfile)
171 songFile = lfr.getSongFile()
172 soundFontFile = lfr.getSoundFontFile()
173 sfPath = lfr.getSoundFontFile()
174 bank = lfr.getBank()
175 preset = lfr.getPreset()
176 synth = Synth(sfPath=sfPath)
177 synth.program_select(0, bank, preset)
178 self.song = musicXml2Song(songFile)
179 screenResolution = lfr.getScreenResolution()
180
181 pygame.display.set_mode(screenResolution)
182
183 super(LogFilePlayer, self).__init__(synth, self.song.distinctNotes)
184
185 def run(self):
186 self._running = True
187 clock = pygame.time.Clock()
188 pygame.display.flip()
189 pygame.mouse.set_visible(False)
190
191 previousTicks = self.lfr.getFirstEventTicks()
192 eIter = self.lfr.getEventsIterator()
193
194 for ticks, eventName, message in eIter :
195 t0 = pygame.time.get_ticks()
196 if eventName == 'COLSTATECHANGE' :
197 parts = message.split(None, 4)
198 if len(parts) == 4 :
199 parts.append('')
200 index, state, midi, name, syllabus = parts
201 index = int(index)
202 midi = int(midi)
203 state = state == 'True'
204 col = self.columns[midi]
205 col.update(state, syllabus=syllabus.decode('utf-8'))
206
207 elif eventName == 'NOTEON':
208 chan, key, vel = [int(v) for v in message.split(None, 2)]
209 self.synth.noteon(chan, key, vel)
210
211 elif eventName == 'NOTEOFF':
212 chan, key = [int(v) for v in message.split(None, 1)]
213 self.synth.noteoff(chan, key)
214
215 elif eventName.startswith('COL') :
216 pos = [int(n) for n in message.split(None, 4)[-1].strip('()').split(',')]
217 self.cursor.setPosition(pos)
218
219
220 pygame.event.clear()
221
222 dirty = self.draw(pygame.display.get_surface())
223 pygame.display.update(dirty)
224 execTime = pygame.time.get_ticks() - t0
225
226 delay = ticks - previousTicks - execTime
227 if delay > 0 :
228 pygame.time.wait(delay)
229
230 previousTicks = ticks
231
232 self.stop()
233
234