Correction des liaisons entre les syllabes.
[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 class LogFileReader(object) :
19 """
20 classe utilitaire pour l'accès aux données d'un fichier de log MinWii.
21 """
22
23 def __init__(self, logfile) :
24 """ logfile : chemin d'accès au fichier de log MinWii.
25 le format supporté est actuellement la version 1.0 uniquement.
26 """
27 if isinstance(logfile, str) :
28 self.logfile = open(logfile, 'r')
29 else :
30 self.logfile = logfile
31
32 firstline = self.next()
33 assert firstline == SUPPORTED_FILE_HEADER
34
35
36 def getSongFile(self) :
37 "retourne le chemin d'accès au fichier musicxml de la chanson"
38 f = self.logfile
39 pos = f.tell()
40
41 f.seek(0)
42 for l in self :
43 if l.startswith('APP chanson :') :
44 break
45 songfile = l.split(':', 1)[1].strip()
46 f.seek(pos)
47 return songfile
48
49 def getSoundFontFile(self) :
50 "retourne le chemin d'accès au fichier de la soundfont (*.sf2)"
51 f = self.logfile
52 pos = f.tell()
53 f.seek(0)
54 for l in self :
55 if l.startswith('ENV soundfont :') :
56 break
57 soundFontFile = l.split(':', 1)[1].strip()
58 f.seek(pos)
59 return soundFontFile
60
61 def getBank(self) :
62 "retourne le paramètre bank du synthétiseur (entier)"
63 f = self.logfile
64 pos = f.tell()
65 f.seek(0)
66 for l in self :
67 if l.startswith('APP bank :') :
68 break
69 f.seek(pos)
70 bank = l.split(':', 1)[1].strip()
71 return int(bank)
72
73 def getPreset(self) :
74 "retourne le paramètre preset du synthétiseur (entier)"
75 f = self.logfile
76 pos = f.tell()
77 f.seek(0)
78 for l in self :
79 if l.startswith('APP preset :') :
80 break
81 f.seek(pos)
82 preset = l.split(':', 1)[1].strip()
83 return int(preset)
84
85 def getScreenResolution(self) :
86 "retourne la résolution écran (tuple de deux entiers)"
87 f = self.logfile
88 pos = f.tell()
89 f.seek(0)
90 for l in self :
91 if l.startswith('ENV résolution écran :') :
92 break
93 screenResolution = eval(l.split(':', 1)[1].strip())
94 f.seek(pos)
95 return screenResolution
96
97 def getFirstEventTicks(self) :
98 "retourne le timecode du premier événement (entier)"
99 f = self.logfile
100 pos = f.tell()
101 f.seek(0)
102 for l in self :
103 if l.startswith('EVT ') :
104 break
105 firstTicks = int(l.split(None, 2)[1])
106 f.seek(pos)
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