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