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