Ajout du mode improvisation.
[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 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 else :
118 return None
119
120 lastTicks = int(l.split(None, 2)[1])
121 return lastTicks
122
123 def __del__(self) :
124 self.close()
125
126 def __iter__(self) :
127 return self
128
129 def next(self) :
130 line = self.logfile.next().strip()
131 return line
132
133 def getEventsIterator(self) :
134 """ Retourne un itérateur sur les événements.
135 Chaque itération retourne un tuple de 3 éléments :
136 (timecode, nom_événement, données) avec le typage :
137 (entier, chaîne, chaîne)
138 """
139 self.logfile.seek(0)
140 while True :
141 try :
142 l = self.next()
143 except StopIteration :
144 break
145
146 if not l.startswith('EVT ') :
147 continue
148 try :
149 ticks, eventName, message = l.split(None, 3)[1:]
150 ticks = int(ticks)
151 yield ticks, eventName, message
152 except ValueError :
153 ticks, eventName = l.split(None, 3)[1:]
154 ticks = int(ticks)
155 yield ticks, eventName, ''
156
157 def getBackwardLineIterator(self) :
158 br = BackwardsReader(self.logfile, BLKSIZE=128)
159 line = br.readline()
160 while line :
161 yield line.strip()
162 line = br.readline()
163
164 @inplaceread
165 def getMetadata(self) :
166 metadata = {}
167 self.next() # skip identification line.
168 line = self.next()
169 while line.startswith('METADATA ') :
170 line = line.split(None, 1)[1]
171 name, value = [v.strip() for v in line.split(':', 1)]
172 metadata[name] = value
173 line = self.next()
174 return metadata
175
176 def setMetadata(self, metadata) :
177 f = self.logfile
178 f.seek(0)
179 before = f.readline()
180 line = f.readline()
181 while line.startswith('METADATA ') :
182 line = f.readline()
183 after = line + f.read()
184
185 lines = []
186 for name, value in metadata :
187 lines.append('METADATA %s : %s' % (name, value.encode('utf-8')))
188 metadata = '\n'.join(lines)
189 f.seek(0)
190 f.write(before)
191 f.write(metadata)
192 f.write(after)
193
194 def close(self) :
195 self.logfile.close()
196
197
198
199 class LogFilePlayer(PlayingScreenBase) :
200 """
201 ré-exécution d'une chanson sur la base de son fichier de log.
202 """
203
204 def __init__(self, logfile) :
205 lfr = self.lfr = LogFileReader(logfile)
206 songFile = lfr.getSongFile()
207 soundFontFile = lfr.getSoundFontFile()
208 sfPath = lfr.getSoundFontFile()
209 bank = lfr.getBank()
210 preset = lfr.getPreset()
211 synth = Synth(sfPath=sfPath)
212 synth.program_select(0, bank, preset)
213 self.song = musicXml2Song(songFile)
214 screenResolution = lfr.getScreenResolution()
215
216 pygame.display.set_mode(screenResolution)
217
218 super(LogFilePlayer, self).__init__(synth, self.song.distinctNotes)
219
220 def run(self):
221 self._running = True
222 clock = pygame.time.Clock()
223 pygame.display.flip()
224 pygame.mouse.set_visible(False)
225
226 previousTicks = self.lfr.getFirstEventTicks()
227 eIter = self.lfr.getEventsIterator()
228
229 for ticks, eventName, message in eIter :
230 t0 = pygame.time.get_ticks()
231 if eventName == 'COLSTATECHANGE' :
232 parts = message.split(None, 4)
233 if len(parts) == 4 :
234 parts.append('')
235 index, state, midi, name, syllabus = parts
236 index = int(index)
237 midi = int(midi)
238 state = state == 'True'
239 col = self.columns[midi]
240 col.update(state, syllabus=syllabus.decode('utf-8'))
241
242 elif eventName == 'NOTEON':
243 chan, key, vel = [int(v) for v in message.split(None, 2)]
244 self.synth.noteon(chan, key, vel)
245
246 elif eventName == 'NOTEOFF':
247 chan, key = [int(v) for v in message.split(None, 1)]
248 self.synth.noteoff(chan, key)
249
250 elif eventName.startswith('COL') :
251 pos = [int(n) for n in message.split(None, 4)[-1].strip('()').split(',')]
252 self.cursor.setPosition(pos)
253
254
255 pygame.event.clear()
256
257 dirty = self.draw(pygame.display.get_surface())
258 pygame.display.update(dirty)
259 execTime = pygame.time.get_ticks() - t0
260
261 delay = ticks - previousTicks - execTime
262 if delay > 0 :
263 pygame.time.wait(delay)
264
265 previousTicks = ticks
266
267 self.stop()
268