a06e831d601f595e01837097b1cdf6c9700d75c6
[minwii.git] / src / gui / SongPlayingScreen.py
1 '''
2 Created on 23 juil. 2009
3
4 @author: Samuel Benveniste
5 '''
6 from math import floor, ceil
7 import pygame
8 import sys
9 import colorsys
10 import constants
11 from gradients import gradients
12 from logging.PickleableEvent import PickleableEvent
13
14
15 class SongPlayingScreen:
16 '''
17 The screen on which the game is played
18
19 wiimotes:
20 The wiimotes used in this session
21 window:
22 The main display window
23 screen:
24 The main display surface
25 clock:
26 The clock used to animate the screen
27 savedScreen:
28 The background that is painted every time
29 playerScreen:
30 The buffer for painting everything before bliting
31 width:
32 The width of the window in pixels
33 height:
34 The height of the window in pixels
35 extendScale :
36 True if the scale is G to C instead of C to C
37 cascade:
38 True if crossing from note to note with a button pressed triggers a new note
39 scaleSize:
40 The size of the scale used
41 cursorPositions:
42 The positions of the cursors on the screen, in pixels
43 '''
44
45
46
47 def __init__(self, instrumentChoice, song, cascade=False, extendedScale=False, easyMode = False, alwaysDown = False, eventLog = None, replay = None, defaultInstrumentChannel = 16, defaultNote = 60):
48 '''
49 Constructor
50 '''
51 self.songDurations = []
52 self.totalDuration = None
53 self.clicks = [0]
54 self.clicksIn = [0]
55 self.clicksPerMinute = [0]
56 self.clicksInPerMinute = [0]
57 self.meanTimeBetweenNotes = []
58 self.firstClick = None
59 self.firstClickIn = None
60
61 self.blinkLength = 200
62 self.minimalVelocity = 90
63 self.shortScaleSize = 8
64 self.longScaleSize = 11
65 if not extendedScale:
66 self.offset = self.longScaleSize - self.shortScaleSize
67 else:
68 self.offset = 0
69 self.borderSize = 5
70 self.highlightedNote = 0
71 self.highlightedNoteNumber = 0
72 self.syllabus = None
73 self.savedHighlightedNote = 0
74 self.alwaysDown = alwaysDown
75 self.nextLevel = None
76
77 self.wiimotes = instrumentChoice.wiimotes
78 self.activeWiimotes = instrumentChoice.activeWiimotes
79 self.window = instrumentChoice.window
80 self.screen = instrumentChoice.screen
81 self.blitOrigin = instrumentChoice.blitOrigin
82 self.clock = instrumentChoice.clock
83 self.width = instrumentChoice.width
84 self.height = instrumentChoice.height
85 self.cursorPositions = instrumentChoice.cursorPositions
86 self.savedScreen = instrumentChoice.savedScreen
87 self.playerScreen = instrumentChoice.playerScreen
88 self.extendedScale = extendedScale
89 self.cascade = cascade
90 self.joys = instrumentChoice.joys
91 self.portOffset = instrumentChoice.portOffset
92 if eventLog == None :
93 self.eventLog = instrumentChoice.eventLog
94 else :
95 self.eventLog = eventLog
96 self.cursorPositions = instrumentChoice.cursorPositions
97 self.song = song
98 self.songIterator = self.song.getSongIterator()
99 self.midiNoteNumbers = self.song.scale
100 if replay == None :
101 self.replay = instrumentChoice.replay
102 else :
103 self.replay = replay
104 self.quarterNoteLength = song.quarterNoteLength
105 self.cascadeLockLengthMultiplier = 1
106 self.nextCascadeLockLengthMultiplier = 1
107 self.cascadeLockLength = self.quarterNoteLength * self.cascadeLockLengthMultiplier
108
109 self.defaultInstrumentChannel = defaultInstrumentChannel
110 self.defaultNote = defaultNote
111
112 self.done = False
113 self.backToInstrumentChoice = False
114 self.easyMode = easyMode
115
116 #Initializes the highlightedNote and highlightedNoteNumber etc...
117 self.moveToNextNote()
118 self.cascadeLockLengthMultiplier = self.nextCascadeLockLengthMultiplier
119
120 self.blinkOn = False
121 self.savedBlinkOn = False
122 ##Will prevent the song to move on if two consecutive notes are identical and the buttons have not been released in between the two
123 ##i.e. it guarantees that there will be an attack between two identical consecutive notes
124 self.highlightIsFree = True
125
126 self.noteRects = []
127 self.boundingRect = None
128 self.notes = []
129
130 self.buttonDown = []
131 self.velocityLock = []
132
133 self._blinkOffset = 0
134 self._cascadeLockTimer = 0
135 self.cascadeIsFree = True
136
137 self.font = pygame.font.Font(None,80)
138 self.renderedNoteNames = [self.font.render(constants.noteNumberToName(note),False,(0,0,0)) for note in self.midiNoteNumbers]
139
140 self.drawBackground()
141 self.initializeWiimotes()
142
143 self.songStartTime = self.eventLog.getCurrentTime()
144
145 #The main loop
146 while not self.done :
147
148 #Clear the cursors from the screen
149 if self.hasChanged():
150 self.drawBackground()
151 self.playerScreen.blit(self.savedScreen, (0, 0))
152
153 # Limit frame speed to 50 FPS
154 #
155 timePassed = self.clock.tick(10000)
156
157 self._blinkOffset += timePassed
158 if (self.buttonDown or self.alwaysDown) and not self.cascadeIsFree :
159 self._cascadeLockTimer += timePassed
160 if self._cascadeLockTimer > self.cascadeLockLengthMultiplier*self.quarterNoteLength :
161 self.cascadeIsFree = True
162 self.cascadeLockLengthMultiplier = self.nextCascadeLockLengthMultiplier
163
164
165 if self._blinkOffset > self.blinkLength:
166 self._blinkOffset -= self.blinkLength
167 self.blinkOn = not self.blinkOn
168
169 if self.replay:
170 self.eventLog.update(timePassed)
171 pickledEventsToPost = self.eventLog.getPickledEvents()
172 for pickledEvent in pickledEventsToPost:
173 pygame.event.post(pickledEvent.event)
174
175 events = pygame.event.get()
176
177 if not self.replay:
178 pickledEvents = [PickleableEvent(event.type,event.dict) for event in events]
179 if pickledEvents != [] :
180 self.eventLog.appendEventGroup(pickledEvents)
181
182 for event in events:
183 self.input(event)
184
185 for i in range(len(self.wiimotes)):
186 if self.activeWiimotes[i]:
187 self.wiimotes[i].cursor.update(timePassed, self.cursorPositions[i])
188 if self.buttonDown[i] or self.alwaysDown:
189 self.wiimotes[i].cursor.flash()
190 self.wiimotes[i].cursor.blit(self.playerScreen)
191
192 self.screen.blit(self.playerScreen, (0,0))
193
194 pygame.display.flip()
195
196 for i in range(len(self.wiimotes)):
197 if self.activeWiimotes[i]:
198 self.wiimotes[i].stopNoteByNoteNumber(self.midiNoteNumbers[self.notes[i]])
199 if self.replay :
200 self.totalDuration = self.eventLog.getCurrentTime()
201
202 def drawBackground(self):
203 self.savedScreen.fill((255,255,255))
204
205 if self.extendedScale :
206 self.scaleSize = self.longScaleSize
207 else:
208 self.scaleSize = self.shortScaleSize
209
210 self.noteRects = [pygame.Rect(i * self.width / self.scaleSize+self.blitOrigin[0], self.blitOrigin[1], self.width / self.scaleSize + 1, self.height+1) for i in range(self.scaleSize)]
211 #inflate last noteRect to cover the far right pixels
212 self.noteRects[-1].width = self.noteRects[-1].width + 1
213
214 self.noteRects[self.highlightedNote-self.offset].inflate_ip(self.noteRects[self.highlightedNote-self.offset].width*2,0)
215
216 #create bounding rect
217 self.boundingRect = self.noteRects[0].unionall(self.noteRects)
218
219 self.renderedNoteNames = [self.font.render(constants.noteNumberToName(note),False,(0,0,0)) for note in self.midiNoteNumbers]
220
221 #fill the rectangles with a color gradient
222 #We start with blue
223 startingHue = 0.66666666666666663
224
225 for rectNumber in range(self.scaleSize):
226 colorRatio = float(rectNumber) / (self.scaleSize - 1)
227 #hue will go from 0.6666... (blue) to 0 (red) as colorRation goes up
228 hue = startingHue * (1 - colorRatio)
229 if rectNumber + self.offset != self.highlightedNote:
230 #The color of the bottom of the rectangle in hls coordinates
231 bottomColorHls = (hue, 0.1, 1)
232 #The color of the top of the rectangle in hls coordinates
233 topColorHls = (hue, 0.1, 1)
234
235 #convert to rgb ranging from 0 to 255
236 bottomColorRgb = [floor(255 * i) for i in colorsys.hls_to_rgb(*bottomColorHls)]
237 topColorRgb = [floor(255 * i) for i in colorsys.hls_to_rgb(*topColorHls)]
238 #add transparency
239 bottomColorRgb.append(255)
240 topColorRgb.append(255)
241 #convert to tuple
242 bottomColorRgb = tuple(bottomColorRgb)
243 topColorRgb = tuple(topColorRgb)
244
245 self.savedScreen.blit(gradients.vertical(self.noteRects[rectNumber].size, topColorRgb, bottomColorRgb), self.noteRects[rectNumber])
246
247 noteNameBlitPoint = (self.noteRects[rectNumber].left+(self.noteRects[rectNumber].width-self.renderedNoteNames[rectNumber+self.offset].get_width())/2,
248 self.noteRects[rectNumber].bottom-self.renderedNoteNames[rectNumber+self.offset].get_height())
249
250 self.savedScreen.blit(self.renderedNoteNames[rectNumber+self.offset], noteNameBlitPoint)
251
252 pygame.draw.rect(self.savedScreen, pygame.Color(0, 0, 0, 255), self.noteRects[rectNumber], 2)
253
254 colorRatio = float(self.highlightedNote-self.offset) / (self.scaleSize - 1)
255 #hue will go from 0.6666... (blue) to 0 (red) as colorRation goes up
256 hue = startingHue * (1 - colorRatio)
257 #The color of the bottom of the rectangle in hls coordinates
258 bottomColorHls = (hue, 0.6, 1)
259 #The color of the top of the rectangle in hls coordinates
260 topColorHls = (hue, 0.9, 1)
261
262 #convert to rgb ranging from 0 to 255
263 bottomColorRgb = [floor(255 * i) for i in colorsys.hls_to_rgb(*bottomColorHls)]
264 topColorRgb = [floor(255 * i) for i in colorsys.hls_to_rgb(*topColorHls)]
265 #add transparency
266 bottomColorRgb.append(255)
267 topColorRgb.append(255)
268 #convert to tuple
269 bottomColorRgb = tuple(bottomColorRgb)
270 topColorRgb = tuple(topColorRgb)
271
272 self.savedScreen.blit(gradients.vertical(self.noteRects[self.highlightedNote-self.offset].size, topColorRgb, bottomColorRgb), self.noteRects[self.highlightedNote-self.offset])
273
274 noteNameBlitPoint = (self.noteRects[self.highlightedNote-self.offset].left+(self.noteRects[self.highlightedNote-self.offset].width-self.renderedNoteNames[self.highlightedNote].get_width())/2,
275 self.noteRects[self.highlightedNote-self.offset].bottom-self.renderedNoteNames[self.highlightedNote].get_height())
276
277 self.savedScreen.blit(self.renderedNoteNames[self.highlightedNote], noteNameBlitPoint)
278
279 if self.syllabus :
280 renderedSyllabus = self.font.render(self.syllabus,False,(0,0,0))
281
282 syllabusBlitPoint = (self.noteRects[self.highlightedNote-self.offset].left+(self.noteRects[self.highlightedNote-self.offset].width-renderedSyllabus.get_width())/2,
283 self.noteRects[self.highlightedNote-self.offset].centery-renderedSyllabus.get_height()/2)
284
285 self.savedScreen.blit(renderedSyllabus, syllabusBlitPoint)
286
287 pygame.draw.rect(self.savedScreen, pygame.Color(0, 0, 0, 255), self.noteRects[self.highlightedNote-self.offset], 2)
288
289 if self.song != None and self.blinkOn:
290 borderSize = self.borderSize
291 pygame.draw.rect(self.savedScreen, pygame.Color(0, 0, 0, 0), self.noteRects[self.highlightedNote-self.offset].inflate(borderSize/2,borderSize/2), borderSize)
292
293 def initializeWiimotes(self):
294 for loop in self.wiimotes:
295 if loop.port == None :
296 loop.port = pygame.midi.Output(loop.portNumber)
297 self.notes.append(0)
298 self.buttonDown.append(False)
299 self.velocityLock.append(False)
300
301 def updateCursorPositionFromJoy(self, joyEvent):
302 joyName = pygame.joystick.Joystick(joyEvent.joy).get_name()
303 correctedJoyId = constants.joyNames.index(joyName)
304 if correctedJoyId < len(self.cursorPositions):
305 if joyEvent.axis == 0 :
306 self.cursorPositions[correctedJoyId] = (int((joyEvent.value + 1) / 2 * self.screen.get_width()), self.cursorPositions[correctedJoyId][1])
307 if joyEvent.axis == 1 :
308 self.cursorPositions[correctedJoyId] = (self.cursorPositions[correctedJoyId][0], int((joyEvent.value + 1) / 2 * self.screen.get_height()))
309
310 def heightToVelocity(self, pos, controllerNumber):
311 if self.song != None:
312 if self.boundingRect.collidepoint(pos) and (self.highlightedNote == self.notes[controllerNumber] or self.velocityLock[controllerNumber]):
313 velocity = int(floor((1 - (float(pos[1])-self.blitOrigin[1]) / self.height) * (127-self.minimalVelocity))+self.minimalVelocity)
314 else :
315 if self.easyMode:
316 velocity = None
317 else:
318 velocity = 60
319 else:
320 if self.boundingRect.collidepoint(pos):
321 velocity = int(floor((1 - (float(pos[1])-self.blitOrigin[1]) / self.height) * (127-self.minimalVelocity))+self.minimalVelocity)
322 else :
323 velocity = self.minimalVelocity
324 return(velocity)
325
326 def widthToNote(self, pos):
327 nn = 0
328 try :
329 if self.noteRects[self.highlightedNote-self.offset].collidepoint(pos) :
330 return self.highlightedNote
331 else :
332 while self.noteRects[nn].collidepoint(pos) == False:
333 nn = nn + 1
334 return(nn + self.offset)
335 except(IndexError):
336 return(None)
337
338 def logClick(self):
339 self.clicks[-1] += 1
340 if self.firstClick == None :
341 self.firstClick = self.eventLog.getCurrentTime()
342 minute = int(floor((self.eventLog.getCurrentTime()-self.songStartTime)/60000))
343 if minute > len(self.clicksPerMinute)-1:
344 self.clicksPerMinute.append(0)
345 self.clicksPerMinute[-1] += 1
346
347 def logClickIn(self):
348 self.clicksIn[-1] += 1
349 if self.clicksIn[-1] > len(self.song.notes)-1 :
350 self.clicksIn.append(0)
351 self.clicks.append(0)
352 self.songDurations.append(self.eventLog.getCurrentTime())
353 if self.firstClickIn == None :
354 self.firstClickIn = self.eventLog.getCurrentTime()
355 minute = int(floor((self.eventLog.getCurrentTime()-self.songStartTime)/60000))
356 if minute > len(self.clicksInPerMinute)-1:
357 self.clicksInPerMinute.append(0)
358 self.clicksInPerMinute[-1]+=1
359
360 def input(self, event):
361
362 if event.type == pygame.QUIT:
363 for loop in self.wiimotes:
364 del loop.port
365 pygame.midi.quit()
366 sys.exit(0)
367
368 if event.type == pygame.KEYDOWN:
369 if event.key == pygame.K_q:
370 self.nextLevel = None
371 self.done = True
372
373 if event.key == pygame.K_i:
374 self.backToInstrumentChoice = True
375 self.done = True
376
377 if event.key == pygame.K_w:
378 self.nextLevel = 0
379 self.done = True
380
381 if event.key == pygame.K_e:
382 self.nextLevel = 1
383 self.done = True
384
385 if event.key == pygame.K_r:
386 self.nextLevel = 2
387 self.done = True
388
389 if event.key == pygame.K_t:
390 self.nextLevel = 3
391 self.done = True
392
393 if event.type == pygame.JOYAXISMOTION:
394
395 joyName = pygame.joystick.Joystick(event.joy).get_name()
396 correctedJoyId = constants.joyNames.index(joyName)
397 if self.activeWiimotes[correctedJoyId]:
398 self.updateCursorPositionFromJoy(event)
399 wiimote = self.wiimotes[correctedJoyId]
400 pos = self.cursorPositions[correctedJoyId]
401
402 if (self.buttonDown[correctedJoyId] or self.alwaysDown):
403 if self.notes[correctedJoyId] != None:
404 velocity = self.heightToVelocity(pos, correctedJoyId)
405 if velocity != None :
406 CCHexCode = wiimote.getCCHexCode()
407 wiimote.port.write_short(CCHexCode, 07, velocity)
408 if self.cascade and self.cascadeIsFree :
409 n = self.widthToNote(pos)
410 if self.highlightedNote == n:
411 wiimote.stopNoteByNoteNumber(self.savedMidiNoteNumbers[self.notes[correctedJoyId]])
412 self.notes[correctedJoyId] = n
413 velocity = self.heightToVelocity(pos, correctedJoyId)
414 self.velocityLock[correctedJoyId] = True
415 wiimote.playNoteByNoteNumber(self.midiNoteNumbers[self.notes[correctedJoyId]],velocity)
416 self.moveToNextNote()
417 self._cascadeLockTimer = 0
418 self.cascadeIsFree = False
419
420 if event.type == pygame.JOYBUTTONDOWN :
421
422 joyName = pygame.joystick.Joystick(event.joy).get_name()
423 correctedJoyId = constants.joyNames.index(joyName)
424 if self.activeWiimotes[correctedJoyId]:
425 wiimote = self.wiimotes[correctedJoyId]
426 pos = self.cursorPositions[correctedJoyId]
427 self.wiimotes[correctedJoyId].cursor.flash()
428 if self.replay:
429 self.logClick()
430
431 if not (self.buttonDown[correctedJoyId] or self.alwaysDown):
432 n = self.widthToNote(pos)
433 if self.highlightedNote == n:
434 self._cascadeLockTimer = 0
435 self.cascadeIsFree = False
436 if self.easyMode:
437 wiimote.stopNoteByNoteNumber(self.savedMidiNoteNumbers[self.notes[correctedJoyId]])
438 self.notes[correctedJoyId] = n
439 velocity = self.heightToVelocity(pos, correctedJoyId)
440 self.velocityLock[correctedJoyId] = True
441 wiimote.playNoteByNoteNumber(self.midiNoteNumbers[self.notes[correctedJoyId]],velocity)
442 if self.replay :
443 self.logClickIn()
444 self.moveToNextNote()
445 else :
446 if not self.easyMode :
447 self._cascadeLockTimer = 0
448 self.cascadeIsFree = False
449 self.notes[correctedJoyId] = n
450 velocity = self.heightToVelocity(pos, correctedJoyId)
451 if velocity != None and self.notes[correctedJoyId] != None :
452 wiimote.playNoteByNoteNumber(self.midiNoteNumbers[self.notes[correctedJoyId]],velocity)
453 self.buttonDown[correctedJoyId] = True
454
455 if event.type == pygame.JOYBUTTONUP:
456 joyName = pygame.joystick.Joystick(event.joy).get_name()
457 correctedJoyId = constants.joyNames.index(joyName)
458 if self.activeWiimotes[correctedJoyId]:
459 self.buttonDown[correctedJoyId] = False
460 wiimote = self.wiimotes[correctedJoyId]
461 if not self.easyMode:
462 wiimote.stopNoteByNoteNumber(self.savedMidiNoteNumbers[self.notes[correctedJoyId]])
463 self.velocityLock[correctedJoyId] = False
464
465 if event.type == pygame.MOUSEMOTION:
466
467 self.updateCursorPositionFromMouse(event)
468
469 correctedJoyId = 0
470 while not self.activeWiimotes[correctedJoyId] :
471 correctedJoyId += 1
472 wiimote = self.wiimotes[correctedJoyId]
473 pos = self.cursorPositions[correctedJoyId]
474
475 if (self.buttonDown[correctedJoyId] or self.alwaysDown):
476 self.wiimotes[correctedJoyId].cursor.flash()
477 if self.notes[correctedJoyId] != None:
478 velocity = self.heightToVelocity(pos, correctedJoyId)
479 if velocity != None :
480 CCHexCode = wiimote.getCCHexCode()
481 wiimote.port.write_short(CCHexCode, 07, velocity)
482 if self.cascade and self.cascadeIsFree :
483 n = self.widthToNote(pos)
484 if self.highlightedNote == n:
485 wiimote.stopNoteByNoteNumber(self.savedMidiNoteNumbers[self.notes[correctedJoyId]])
486 self.notes[correctedJoyId] = n
487 velocity = self.heightToVelocity(pos, correctedJoyId)
488 self.velocityLock[correctedJoyId] = True
489 wiimote.playNoteByNoteNumber(self.midiNoteNumbers[self.notes[correctedJoyId]],velocity)
490 self.moveToNextNote()
491 self._cascadeLockTimer = 0
492 self.cascadeIsFree = False
493
494 if event.type == pygame.MOUSEBUTTONDOWN:
495
496 if event.button == 1:
497 correctedJoyId = 0
498 while not self.activeWiimotes[correctedJoyId] :
499 correctedJoyId += 1
500 wiimote = self.wiimotes[correctedJoyId]
501 pos = self.cursorPositions[correctedJoyId]
502 self.wiimotes[correctedJoyId].cursor.flash()
503 if self.replay:
504 self.logClick()
505
506 if not (self.buttonDown[correctedJoyId] or self.alwaysDown):
507 n = self.widthToNote(pos)
508 if self.highlightedNote == n:
509 self._cascadeLockTimer = 0
510 self.cascadeIsFree = False
511 if self.easyMode:
512 wiimote.stopNoteByNoteNumber(self.savedMidiNoteNumbers[self.notes[correctedJoyId]])
513 self.notes[correctedJoyId] = n
514 velocity = self.heightToVelocity(pos, correctedJoyId)
515 self.velocityLock[correctedJoyId] = True
516 wiimote.playNoteByNoteNumber(self.midiNoteNumbers[self.notes[correctedJoyId]],velocity)
517 if self.replay :
518 self.logClickIn()
519 self.moveToNextNote()
520 else :
521 if not self.easyMode :
522 self._cascadeLockTimer = 0
523 self.cascadeIsFree = False
524 self.notes[correctedJoyId] = n
525 velocity = self.heightToVelocity(pos, correctedJoyId)
526 if velocity != None and self.notes[correctedJoyId] != None :
527 wiimote.playNoteByNoteNumber(self.midiNoteNumbers[self.notes[correctedJoyId]],velocity)
528 self.buttonDown[correctedJoyId] = True
529
530 if event.button == 2:
531
532 self.done = True
533
534 if event.type == pygame.MOUSEBUTTONUP:
535 if event.button == 1 :
536 correctedJoyId = 0
537 while not self.activeWiimotes[correctedJoyId] :
538 correctedJoyId += 1
539 wiimote = self.wiimotes[correctedJoyId]
540 self.buttonDown[correctedJoyId] = False
541 if not self.easyMode:
542 if self.notes[correctedJoyId] != None :
543 wiimote.stopNoteByNoteNumber(self.savedMidiNoteNumbers[self.notes[correctedJoyId]])
544 self.velocityLock[correctedJoyId] = False
545
546 def hasChanged(self):
547 changed = False
548 if self.song != None:
549 if self.blinkOn != self.savedBlinkOn or self.highlightedNote != self.savedHighlightedNote:
550 self.savedBlinkOn = self.blinkOn
551 self.savedHighlightedNote = self.highlightedNote
552 changed = True
553 return(changed)
554
555 def updateCursorPositionFromMouse(self, mouseEvent):
556 correctedJoyId = 0
557 while not self.activeWiimotes[correctedJoyId] :
558 correctedJoyId += 1
559 self.cursorPositions[correctedJoyId] = mouseEvent.pos
560
561 def moveToNextNote(self):
562 self.savedMidiNoteNumbers = self.midiNoteNumbers[:]
563 self.highlightedNote, self.highlightedNoteNumber, self.syllabus, self.nextCascadeLockLengthMultiplier = self.songIterator.next()
564 self.midiNoteNumbers[self.highlightedNote] = self.highlightedNoteNumber