Gestion explicite de KeyboardInterrupt pour être sûr de quitter le jeu sur un ^C.
[minwii.git] / src / minwii / widgets / playingscreen.py
1 # -*- coding: utf-8 -*-
2 """
3 Écran de jeu MinWii :
4 bandes arc-en-ciel représentant un clavier.
5
6 $Id$
7 $URL$
8 """
9 import pygame
10 import types
11
12 import kinect.pygamedisplay as kinect
13
14 import minwii.events as events
15 from minwii.log import eventLogger
16 from minwii.eventutils import event_handler, EventDispatcher, EventHandlerMixin
17 from minwii.musicxml import Tone
18 from minwii.config import FRAMERATE
19 from minwii.config import FIRST_HUE
20 from minwii.config import MIDI_VELOCITY_RANGE
21 from minwii.config import MIDI_PAN_RANGE
22 from minwii.config import MIDI_VELOCITY_WRONG_NOTE_ATTN
23 from minwii.config import SCREEN_RESOLUTION
24 from minwii.globals import BACKGROUND_LAYER
25 from minwii.globals import CURSOR_LAYER
26 from minwii.globals import PLAYING_MODES_DICT
27
28 from cursors import WarpingCursor
29 from column import Column
30
31 class PlayingScreenBase(pygame.sprite.LayeredDirty, EventHandlerMixin) :
32
33 def __init__(self, synth, distinctNotes=[], displayNotes=True) :
34 """
35 distinctNotes : notes disctinctes présentes dans la chanson
36 triées du plus grave au plus aigu.
37 """
38 super(PlayingScreenBase, self).__init__()
39 self.synth = synth
40 self.distinctNotes = distinctNotes
41 self.displayNotes = displayNotes
42 self.keyboardLength = 0
43 self.keyboardRects = []
44 self.cursor = None
45 self._initRects()
46 self.columns = {}
47 self._initColumns()
48 self._running = False
49 self.kinectRgb = kinect.RGBSprite(alpha=128, size=SCREEN_RESOLUTION)
50 self.add(self.kinectRgb, layer=CURSOR_LAYER)
51 self._initCursor()
52
53
54 def _initRects(self) :
55 """ création des espaces réservés pour
56 afficher les colonnes.
57 """
58 self.keyboardLength = len(self.distinctNotes)
59
60 screen = pygame.display.get_surface()
61
62 self.dispWidth = dispWidth = screen.get_width()
63 self.dispHeight = dispHeight = screen.get_height()
64
65 columnWidth = int(round(float(dispWidth) / self.keyboardLength))
66
67 rects = []
68 for i in range(self.keyboardLength - 1) :
69 upperLeftCorner = (i*columnWidth, 0)
70 rect = pygame.Rect(upperLeftCorner, (columnWidth, dispHeight))
71 rects.append(rect)
72
73 # la dernière colonne à la largeur du reste
74 upperLeftCorner = ((i+1) * columnWidth, 0)
75 rect = pygame.Rect(upperLeftCorner, (dispWidth - (self.keyboardLength - 1) * columnWidth , dispHeight))
76 rects.append(rect)
77
78 self.keyboardRects = rects
79
80 def _initColumns(self) :
81
82 hueStep = FIRST_HUE / (self.keyboardLength - 1)
83 for i, rect in enumerate(self.keyboardRects) :
84 hue = FIRST_HUE - hueStep * i
85 tone = self.distinctNotes[i]
86 c = Column(self, i, hue, rect, tone, displayNote=self.displayNotes)
87 self.add(c, layer=BACKGROUND_LAYER)
88 self.columns[tone.midi] = c
89
90
91 def _initCursor(self) :
92 self.cursor = WarpingCursor(blinkMode=True)
93 self.add(self.cursor, layer=CURSOR_LAYER)
94
95 def run(self):
96 self._running = True
97 clock = pygame.time.Clock()
98 pygame.display.flip()
99 pygame.mouse.set_visible(False)
100 while self._running :
101 try :
102 EventDispatcher.dispatchEvents()
103 self.kinectRgb.update()
104 dirty = self.draw(pygame.display.get_surface())
105 pygame.display.update(dirty)
106 clock.tick(FRAMERATE)
107 except KeyboardInterrupt :
108 self.stop()
109 raise
110
111 def stop(self) :
112 self._running = False
113 self.synth.system_reset()
114 pygame.mouse.set_visible(True)
115 self.cursor._stopBlink()
116
117 @event_handler(pygame.KEYDOWN)
118 def handleKeyDown(self, event) :
119 if event.key in (pygame.K_q, pygame.K_ESCAPE) or \
120 event.unicode == u'q' :
121 self.stop()
122
123 @event_handler(pygame.MOUSEBUTTONDOWN)
124 def onMouseDown(self, event) :
125 # TODO à cleaner : on vire le dernier élément
126 # parce qu'il s'agit du curseur
127 for col in reversed(self.sprites()[:-1]) :
128 if col.rect.collidepoint(*event.pos):
129 self.raiseColDown(col, event)
130 break
131
132 @event_handler(pygame.MOUSEBUTTONUP)
133 def onMouseUp(self, event) :
134 for col in reversed(self.sprites()[:-1]) :
135 if col.rect.collidepoint(*event.pos) :
136 self.raiseColUp(col, event)
137 break
138
139 @event_handler(pygame.MOUSEMOTION)
140 def onMouseMove(self, event) :
141 for col in reversed(self.sprites()[:-1]) :
142 if col.rect.collidepoint(*event.pos) :
143 self.raiseColOver(col, event)
144 break
145
146 def raiseColDown(self, col, mouseEvent) :
147 evt = pygame.event.Event(events.COLDOWN, column=col, pos=mouseEvent.pos)
148 pygame.event.post(evt)
149
150 def raiseColUp(self, col, mouseEvent) :
151 evt = pygame.event.Event(events.COLUP, column=col, pos=mouseEvent.pos)
152 pygame.event.post(evt)
153
154 def raiseColOver(self, col, mouseEvent) :
155 evt = pygame.event.Event(events.COLOVER, column=col, pos=mouseEvent.pos, mouseEvent=mouseEvent)
156 pygame.event.post(evt)
157
158 def getVelocity(self, pos) :
159 vel = (float(self.dispWidth) - pos[1]) / self.dispWidth
160 vel = int(vel * (MIDI_VELOCITY_RANGE[1] - MIDI_VELOCITY_RANGE[0])) + MIDI_VELOCITY_RANGE[0]
161 return vel
162
163 def getPan(self, index) :
164 pan = float(index) / (self.keyboardLength -1)
165 pan = int(pan * (MIDI_PAN_RANGE[1] - MIDI_PAN_RANGE[0])) + MIDI_PAN_RANGE[0]
166 return pan
167
168 def playnote(self, col, pos, vel=None) :
169 pan = self.getPan(col.index)
170 self.synth.cc(0, 10, pan)
171 vel = vel or self.getVelocity(pos)
172 self.synth.noteon(0, col.tone.midi, vel)
173
174 class PlayingScreen(PlayingScreenBase) :
175 "fenêtre de jeu pour improvisation"
176
177 scale = [55, 57, 59, 60, 62, 64, 65, 67, 69, 71, 72]
178
179 def __init__(self, synth, displayNotes=True) :
180 distinctNotes = []
181 self.currentColumn = None
182 for midi in self.scale :
183 tone = Tone(midi)
184 distinctNotes.append(tone)
185
186 super(PlayingScreen, self).__init__(synth, distinctNotes, displayNotes=displayNotes)
187
188 @event_handler(events.COLDOWN)
189 def noteon(self, event) :
190 col = event.column
191 col.update(True)
192 self.currentColumn = col
193 self.playnote(col, event.pos)
194
195 @event_handler(events.COLUP)
196 def noteoff(self, event) :
197 if self.currentColumn :
198 self.currentColumn.update(False)
199 self.synth.noteoff(0, self.currentColumn.tone.midi)
200
201
202 class SongPlayingScreen(PlayingScreenBase) :
203
204 def __init__(self, synth, song, mode=PLAYING_MODES_DICT['NORMAL'], displayNotes=True, tempoTrim=0) :
205 super(SongPlayingScreen, self).__init__(synth, song.distinctNotes, displayNotes=displayNotes)
206 self.song = song
207 self.quarterNoteDuration = song.quarterNoteDuration
208 self.tempoTrim = tempoTrim
209 self.currentColumn = None
210 self.noteIterator = self.song.iterNotes()
211 self.displayNext()
212 self._plugListeners(mode)
213
214 def _plugListeners(self, mode) :
215 "initialisation des gestionnaires d'événements en fonction du mode"
216
217 if mode == PLAYING_MODES_DICT['BEGINNER'] :
218 EventDispatcher.addEventListener(events.COLOVER, self.handleBeginnerColumnOver)
219
220 elif mode == PLAYING_MODES_DICT['EASY'] :
221 EventDispatcher.addEventListener(events.COLDOWN, self.handleEasyColumnDown)
222 EventDispatcher.addEventListener(events.COLOVER, self.handleEasyColumnOver)
223
224 elif mode == PLAYING_MODES_DICT['NORMAL'] :
225 EventDispatcher.addEventListener(events.COLOVER, self.handleNormalColumnOver)
226 EventDispatcher.addEventListener(events.COLDOWN, self.handleColumnDown)
227 EventDispatcher.addEventListener(events.COLUP, self.handleColumnUp)
228
229 elif mode == PLAYING_MODES_DICT['ADVANCED'] :
230 EventDispatcher.addEventListener(events.COLDOWN, self.handleColumnDown)
231 EventDispatcher.addEventListener(events.COLUP, self.handleColumnUp)
232
233 elif mode == PLAYING_MODES_DICT['EXPERT'] :
234 EventDispatcher.addEventListener(events.COLDOWN, self.handleExpertColumnDown)
235 EventDispatcher.addEventListener(events.COLUP, self.handleExpertColumnUp)
236
237
238 # --- HID listeners ---
239 def handleBeginnerColumnOver(self, event) :
240 col = event.column
241 if col.state and not self.currentNotePlayed :
242 self.playnote(col, event.pos)
243 self.setNoteTimeout()
244 self.currentNotePlayed = True
245
246 def handleEasyColumnOver(self, event) :
247 col = event.column
248 if col.state and \
249 self.cursor.pressed and \
250 not self.currentNotePlayed :
251 self.playnote(col, event.pos)
252 self.setNoteTimeout()
253 self.currentNotePlayed = True
254
255
256 def handleNormalColumnOver(self, event) :
257 col = event.column
258 if col.state and \
259 self.cursor.pressed and \
260 not self.currentNotePlayed :
261 self.playnote(col, event.pos)
262 self.currentNotePlayed = True
263
264 def handleColumnDown(self, event) :
265 col = event.column
266 if col.state:
267 self.playnote(col, event.pos)
268 self.currentNotePlayed = True
269
270 def handleEasyColumnDown(self, event) :
271 col = event.column
272 if col.state and \
273 not self.currentNotePlayed :
274 self.playnote(col, event.pos)
275 self.setNoteTimeout()
276 self.currentNotePlayed = True
277
278
279 def handleExpertColumnDown(self, event) :
280 col = event.column
281 if col.state :
282 self.playnote(col, event.pos)
283 self.currentNotePlayed = True
284 else :
285 vel = self.getVelocity(event.pos) * MIDI_VELOCITY_WRONG_NOTE_ATTN
286 vel = int(vel)
287 self.playnote(col, event.pos, vel=vel)
288 self.alternateColumn = col
289 self.currentNotePlayed = False
290
291 def handleColumnUp(self, event) :
292 if self.currentNotePlayed :
293 self.synth.noteoff(0, self.currentColumn.tone.midi)
294 self.displayNext()
295
296 def handleExpertColumnUp(self, event) :
297 if self.currentNotePlayed :
298 self.synth.noteoff(0, self.currentColumn.tone.midi)
299 self.displayNext()
300 else :
301 self.synth.noteoff(0, self.alternateColumn.tone.midi)
302
303 # --- End HID listeners ---
304
305
306 def displayNext(self, event=None) :
307 if self.currentColumn:
308 self.currentColumn.update(False)
309 try :
310 note, verseIndex = self.noteIterator.next()
311 except StopIteration :
312 self.noteIterator = self.song.iterNotes()
313 note, verseIndex = self.noteIterator.next()
314 eventLogger.info(pygame.event.Event(events.SONGEND))
315 try :
316 syllabus = note.lyrics[verseIndex].syllabus()
317 except IndexError :
318 syllabus = u'…'
319
320 column = self.columns[note.midi]
321 column.update(True, syllabus)
322 self.currentColumn = column
323 self.currentNote = note
324 self.currentNotePlayed = False
325
326 @event_handler(events.NOTEEND)
327 def clearTimeOutAndDisplayNext(self, evt) :
328 pygame.time.set_timer(evt.type, 0)
329 self.synth.noteoff(0, self.currentNote.midi)
330 self.displayNext()
331
332 def setNoteTimeout(self) :
333 delay = self.currentNote.duration * self.quarterNoteDuration
334 delay = delay + delay * self.tempoTrim
335 delay = int(delay)
336 if delay < 1 :
337 delay = 1 # durée minimale, car 0 désactiverait le timer.
338 pygame.time.set_timer(events.NOTEEND, delay)
339
340 def tempoTrimUp(self, step=0.1) :
341 self.tempoTrim = round(self.tempoTrim - step, 1)
342
343 def tempoTrimDown(self, step=0.1) :
344 self.tempoTrim = round(self.tempoTrim + step, 1)
345
346 def stop(self) :
347 pygame.time.set_timer(events.NOTEEND, 0)
348 super(SongPlayingScreen, self).stop()
349
350