5829a4e631ad984dcd7466f006a8ccf72c4f1f2e
[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.globals import BACKGROUND_LAYER
24 from minwii.globals import CURSOR_LAYER
25 from minwii.globals import PLAYING_MODES_DICT
26
27 from cursors import WarpingCursor
28 from column import Column
29
30 class PlayingScreenBase(pygame.sprite.LayeredDirty, EventHandlerMixin) :
31
32 def __init__(self, synth, distinctNotes=[], displayNotes=True) :
33 """
34 distinctNotes : notes disctinctes présentes dans la chanson
35 triées du plus grave au plus aigu.
36 """
37 super(PlayingScreenBase, self).__init__()
38 self.synth = synth
39 self.distinctNotes = distinctNotes
40 self.displayNotes = displayNotes
41 self.keyboardLength = 0
42 self.keyboardRects = []
43 self.cursor = None
44 self._initRects()
45 self.columns = {}
46 self._initColumns()
47 self._running = False
48 self.draw(pygame.display.get_surface())
49 self._initCursor()
50
51 self.kinectRgb = kinect.RGB()
52 self.kinectRgbSur = pygame.Surface((640, 480))
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 EventDispatcher.dispatchEvents()
102 dirty = self.draw(pygame.display.get_surface())
103 pygame.display.update(dirty)
104
105 self.kinectRgb.update()
106 rgbImg = self.kinectRgb.capture()
107 self.kinectRgbSur.blit(rgbImg, (0, 0))
108 screen = pygame.display.get_surface()
109 screen.blit(pygame.transform.flip(self.kinectRgbSur, True, False), (0, 0))
110 pygame.display.flip()
111
112 clock.tick(FRAMERATE)
113
114 def stop(self) :
115 self._running = False
116 self.synth.system_reset()
117 pygame.mouse.set_visible(True)
118 self.cursor._stopBlink()
119
120 @event_handler(pygame.KEYDOWN)
121 def handleKeyDown(self, event) :
122 if event.key in (pygame.K_q, pygame.K_ESCAPE) or \
123 event.unicode == u'q' :
124 self.stop()
125
126 @event_handler(pygame.MOUSEBUTTONDOWN)
127 def onMouseDown(self, event) :
128 # TODO à cleaner : on vire le dernier élément
129 # parce qu'il s'agit du curseur
130 for col in reversed(self.sprites()[:-1]) :
131 if col.rect.collidepoint(*event.pos):
132 self.raiseColDown(col, event)
133 break
134
135 @event_handler(pygame.MOUSEBUTTONUP)
136 def onMouseUp(self, event) :
137 for col in reversed(self.sprites()[:-1]) :
138 if col.rect.collidepoint(*event.pos) :
139 self.raiseColUp(col, event)
140 break
141
142 @event_handler(pygame.MOUSEMOTION)
143 def onMouseMove(self, event) :
144 for col in reversed(self.sprites()[:-1]) :
145 if col.rect.collidepoint(*event.pos) :
146 self.raiseColOver(col, event)
147 break
148
149 def raiseColDown(self, col, mouseEvent) :
150 evt = pygame.event.Event(events.COLDOWN, column=col, pos=mouseEvent.pos)
151 pygame.event.post(evt)
152
153 def raiseColUp(self, col, mouseEvent) :
154 evt = pygame.event.Event(events.COLUP, column=col, pos=mouseEvent.pos)
155 pygame.event.post(evt)
156
157 def raiseColOver(self, col, mouseEvent) :
158 evt = pygame.event.Event(events.COLOVER, column=col, pos=mouseEvent.pos, mouseEvent=mouseEvent)
159 pygame.event.post(evt)
160
161 def getVelocity(self, pos) :
162 vel = (float(self.dispWidth) - pos[1]) / self.dispWidth
163 vel = int(vel * (MIDI_VELOCITY_RANGE[1] - MIDI_VELOCITY_RANGE[0])) + MIDI_VELOCITY_RANGE[0]
164 return vel
165
166 def getPan(self, index) :
167 pan = float(index) / (self.keyboardLength -1)
168 pan = int(pan * (MIDI_PAN_RANGE[1] - MIDI_PAN_RANGE[0])) + MIDI_PAN_RANGE[0]
169 return pan
170
171 def playnote(self, col, pos, vel=None) :
172 pan = self.getPan(col.index)
173 self.synth.cc(0, 10, pan)
174 vel = vel or self.getVelocity(pos)
175 self.synth.noteon(0, col.tone.midi, vel)
176
177 class PlayingScreen(PlayingScreenBase) :
178 "fenêtre de jeu pour improvisation"
179
180 scale = [55, 57, 59, 60, 62, 64, 65, 67, 69, 71, 72]
181
182 def __init__(self, synth, displayNotes=True) :
183 distinctNotes = []
184 self.currentColumn = None
185 for midi in self.scale :
186 tone = Tone(midi)
187 distinctNotes.append(tone)
188
189 super(PlayingScreen, self).__init__(synth, distinctNotes, displayNotes=displayNotes)
190
191 @event_handler(events.COLDOWN)
192 def noteon(self, event) :
193 col = event.column
194 col.update(True)
195 self.currentColumn = col
196 self.playnote(col, event.pos)
197
198 @event_handler(events.COLUP)
199 def noteoff(self, event) :
200 if self.currentColumn :
201 self.currentColumn.update(False)
202 self.synth.noteoff(0, self.currentColumn.tone.midi)
203
204
205 class SongPlayingScreen(PlayingScreenBase) :
206
207 def __init__(self, synth, song, mode=PLAYING_MODES_DICT['NORMAL'], displayNotes=True, tempoTrim=0) :
208 super(SongPlayingScreen, self).__init__(synth, song.distinctNotes, displayNotes=displayNotes)
209 self.song = song
210 self.quarterNoteDuration = song.quarterNoteDuration
211 self.tempoTrim = tempoTrim
212 self.currentColumn = None
213 self.noteIterator = self.song.iterNotes()
214 self.displayNext()
215 self._plugListeners(mode)
216
217 def _plugListeners(self, mode) :
218 "initialisation des gestionnaires d'événements en fonction du mode"
219
220 if mode == PLAYING_MODES_DICT['BEGINNER'] :
221 EventDispatcher.addEventListener(events.COLOVER, self.handleBeginnerColumnOver)
222
223 elif mode == PLAYING_MODES_DICT['EASY'] :
224 EventDispatcher.addEventListener(events.COLDOWN, self.handleEasyColumnDown)
225 EventDispatcher.addEventListener(events.COLOVER, self.handleEasyColumnOver)
226
227 elif mode == PLAYING_MODES_DICT['NORMAL'] :
228 EventDispatcher.addEventListener(events.COLOVER, self.handleNormalColumnOver)
229 EventDispatcher.addEventListener(events.COLDOWN, self.handleColumnDown)
230 EventDispatcher.addEventListener(events.COLUP, self.handleColumnUp)
231
232 elif mode == PLAYING_MODES_DICT['ADVANCED'] :
233 EventDispatcher.addEventListener(events.COLDOWN, self.handleColumnDown)
234 EventDispatcher.addEventListener(events.COLUP, self.handleColumnUp)
235
236 elif mode == PLAYING_MODES_DICT['EXPERT'] :
237 EventDispatcher.addEventListener(events.COLDOWN, self.handleExpertColumnDown)
238 EventDispatcher.addEventListener(events.COLUP, self.handleExpertColumnUp)
239
240
241 # --- HID listeners ---
242 def handleBeginnerColumnOver(self, event) :
243 col = event.column
244 if col.state and not self.currentNotePlayed :
245 self.playnote(col, event.pos)
246 self.setNoteTimeout()
247 self.currentNotePlayed = True
248
249 def handleEasyColumnOver(self, event) :
250 col = event.column
251 if col.state and \
252 self.cursor.pressed and \
253 not self.currentNotePlayed :
254 self.playnote(col, event.pos)
255 self.setNoteTimeout()
256 self.currentNotePlayed = True
257
258
259 def handleNormalColumnOver(self, event) :
260 col = event.column
261 if col.state and \
262 self.cursor.pressed and \
263 not self.currentNotePlayed :
264 self.playnote(col, event.pos)
265 self.currentNotePlayed = True
266
267 def handleColumnDown(self, event) :
268 col = event.column
269 if col.state:
270 self.playnote(col, event.pos)
271 self.currentNotePlayed = True
272
273 def handleEasyColumnDown(self, event) :
274 col = event.column
275 if col.state and \
276 not self.currentNotePlayed :
277 self.playnote(col, event.pos)
278 self.setNoteTimeout()
279 self.currentNotePlayed = True
280
281
282 def handleExpertColumnDown(self, event) :
283 col = event.column
284 if col.state :
285 self.playnote(col, event.pos)
286 self.currentNotePlayed = True
287 else :
288 vel = self.getVelocity(event.pos) * MIDI_VELOCITY_WRONG_NOTE_ATTN
289 vel = int(vel)
290 self.playnote(col, event.pos, vel=vel)
291 self.alternateColumn = col
292 self.currentNotePlayed = False
293
294 def handleColumnUp(self, event) :
295 if self.currentNotePlayed :
296 self.synth.noteoff(0, self.currentColumn.tone.midi)
297 self.displayNext()
298
299 def handleExpertColumnUp(self, event) :
300 if self.currentNotePlayed :
301 self.synth.noteoff(0, self.currentColumn.tone.midi)
302 self.displayNext()
303 else :
304 self.synth.noteoff(0, self.alternateColumn.tone.midi)
305
306 # --- End HID listeners ---
307
308
309 def displayNext(self, event=None) :
310 if self.currentColumn:
311 self.currentColumn.update(False)
312 try :
313 note, verseIndex = self.noteIterator.next()
314 except StopIteration :
315 self.noteIterator = self.song.iterNotes()
316 note, verseIndex = self.noteIterator.next()
317 eventLogger.info(pygame.event.Event(events.SONGEND))
318 try :
319 syllabus = note.lyrics[verseIndex].syllabus()
320 except IndexError :
321 syllabus = u'…'
322
323 column = self.columns[note.midi]
324 column.update(True, syllabus)
325 self.currentColumn = column
326 self.currentNote = note
327 self.currentNotePlayed = False
328
329 @event_handler(events.NOTEEND)
330 def clearTimeOutAndDisplayNext(self, evt) :
331 pygame.time.set_timer(evt.type, 0)
332 self.synth.noteoff(0, self.currentNote.midi)
333 self.displayNext()
334
335 def setNoteTimeout(self) :
336 delay = self.currentNote.duration * self.quarterNoteDuration
337 delay = delay + delay * self.tempoTrim
338 delay = int(delay)
339 if delay < 1 :
340 delay = 1 # durée minimale, car 0 désactiverait le timer.
341 pygame.time.set_timer(events.NOTEEND, delay)
342
343 def tempoTrimUp(self, step=0.1) :
344 self.tempoTrim = round(self.tempoTrim - step, 1)
345
346 def tempoTrimDown(self, step=0.1) :
347 self.tempoTrim = round(self.tempoTrim + step, 1)
348
349 def stop(self) :
350 pygame.time.set_timer(events.NOTEEND, 0)
351 super(SongPlayingScreen, self).stop()
352
353