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