2 Created on 23 juil. 2009
4 @author: Samuel Benveniste
6 from math
import floor
, ceil
11 from gradients
import gradients
12 from logging
.PickleableEvent
import PickleableEvent
13 from logging
.EventLog
import EventLog
16 class SongFamiliarizer
:
18 The screen on which the game is played
21 The wiimotes used in this session
23 The main display window
25 The main display surface
27 The clock used to animate the screen
29 The background that is painted every time
31 The buffer for painting everything before bliting
33 The width of the window in pixels
35 The height of the window in pixels
37 True if the scale is G to C instead of C to C
39 True if crossing from note to note with a button pressed triggers a new note
41 The size of the scale used
43 The positions of the cursors on the screen, in pixels
48 def __init__(self
, wiimotes
, window
, screen
, clock
, joys
, portOffset
, song
, activeWiimotes
, cascade
=False, extendedScale
=False, easyMode
= False, replay
= False, eventLog
= None, defaultInstrumentChannel
= 16, defaultNote
= 60):
52 self
.firstClickTime
= None
53 self
.firstClickInTime
= None
59 self
.font
= pygame
.font
.Font(None,60)
60 self
.congratulations
= ["Bien !","Tres Bien !","Bravo !","Excellent !","Felicitations !"]
61 self
.renderedCongratulations
= [self
.font
.render(congratulation
,False,(0,0,0)) for congratulation
in self
.congratulations
]
62 self
.congratulationCount
= None
63 self
.isCongratulating
= False
64 self
.congratulationTimer
= 0
65 self
.congratulationLength
= 2000
66 self
.congratulationPos
= None
68 self
.blinkLength
= 200
69 self
.minimalVelocity
= 90
70 self
.shortScaleSize
= 8
71 self
.longScaleSize
= 11
73 self
.offset
= self
.longScaleSize
- self
.shortScaleSize
77 self
.highlightedNote
= 0
78 self
.highlightedNoteNumber
= 0
80 self
.savedHighlightedNote
= 0
84 self
.wiimotes
= wiimotes
85 self
.activeWiimotes
= activeWiimotes
88 self
.width
= int(floor(screen
.get_width()*self
.scaleFactor
))
89 self
.height
= int(floor(screen
.get_height()*self
.scaleFactor
))
90 self
.blitOrigin
= ((self
.screen
.get_width()-self
.width
)/2,(self
.screen
.get_height()-self
.height
)/2)
93 self
.cursorPositions
= []
94 self
.savedScreen
= pygame
.Surface(self
.screen
.get_size())
95 self
.savedScreen
.fill((255,255,255))
96 self
.playerScreen
= pygame
.Surface(self
.savedScreen
.get_size())
97 self
.playerScreen
.blit(self
.savedScreen
, (0, 0))
98 self
.extendedScale
= extendedScale
99 self
.cascade
= cascade
100 self
.portOffset
=portOffset
101 self
.eventLog
= eventLog
103 self
.songIterator
= self
.song
.getSongIterator()
104 self
.midiNoteNumbers
= self
.song
.scale
106 self
.quarterNoteLength
= 800
107 self
.cascadeLockLengthMultiplier
= 1
108 self
.cascadeLockLength
= self
.quarterNoteLength
* self
.cascadeLockLengthMultiplier
110 self
.defaultInstrumentChannel
= defaultInstrumentChannel
111 self
.defaultNote
= defaultNote
114 self
.backToInstrumentChoice
= False
115 self
.easyMode
= easyMode
118 self
.eventLog
= EventLog()
121 self
.eventLog
= eventLog
124 #Initializes the highlightedNote and highlightedNoteNumber etc...
125 self
.moveToNextNote()
128 self
.savedBlinkOn
= False
129 ##Will prevent the song to move on if two consecutive notes are identical and the buttons have not been released in between the two
130 ##i.e. it guarantees that there will be an attack between two identical consecutive notes
131 self
.highlightIsFree
= True
134 self
.boundingRect
= None
138 self
.velocityLock
= []
140 self
._blinkOffset
= 0
141 self
._cascadeLockTimer
= 0
142 self
.cascadeIsFree
= True
144 self
.font
= pygame
.font
.Font(None,50)
145 self
.renderedNoteNames
= [self
.font
.render(constants
.noteNumberToName(note
),False,(0,0,0)) for note
in self
.midiNoteNumbers
]
147 self
.drawBackground()
148 self
.initializeWiimotes()
150 events
= pygame
.event
.get()
153 while not self
.done
:
155 #Clear the cursors from the screen
156 if self
.hasChanged():
157 self
.drawBackground()
158 self
.playerScreen
.blit(self
.savedScreen
, (0, 0))
160 # Limit frame speed to 50 FPS
162 timePassed
= self
.clock
.tick(10000)
164 self
._blinkOffset
+= timePassed
165 if self
.buttonDown
and not self
.cascadeIsFree
:
166 self
._cascadeLockTimer
+= timePassed
167 if self
._cascadeLockTimer
> self
.cascadeLockLength
:
168 self
.cascadeIsFree
= True
171 if self
._blinkOffset
> self
.blinkLength
:
172 self
._blinkOffset
-= self
.blinkLength
173 self
.blinkOn
= not self
.blinkOn
176 self
.eventLog
.update(timePassed
)
177 pickledEventsToPost
= self
.eventLog
.getPickledEvents()
178 for pickledEvent
in pickledEventsToPost
:
179 pygame
.event
.post(pickledEvent
.event
)
181 events
= pygame
.event
.get()
184 pickledEvents
= [PickleableEvent(event
.type,event
.dict) for event
in events
]
185 if pickledEvents
!= [] :
186 self
.eventLog
.appendEventGroup(pickledEvents
)
191 if self
.isCongratulating
:
192 self
.congratulationTimer
+= timePassed
193 if self
.congratulationTimer
< self
.congratulationLength
:
194 self
.blitCongratulation()
196 self
.isCongratulating
= False
198 for i
in range(len(self
.wiimotes
)):
199 if self
.activeWiimotes
[i
]:
200 self
.wiimotes
[i
].cursor
.update(timePassed
, self
.cursorPositions
[i
])
201 if self
.buttonDown
[i
] :
202 self
.wiimotes
[i
].cursor
.flash()
203 self
.wiimotes
[i
].cursor
.blit(self
.playerScreen
)
205 self
.screen
.blit(self
.playerScreen
, (0,0))
207 pygame
.display
.flip()
209 for i
in range(len(self
.wiimotes
)):
210 if self
.activeWiimotes
[i
]:
211 self
.wiimotes
[i
].stopNoteByNoteNumber(self
.midiNoteNumbers
[self
.notes
[i
]])
213 self
.duration
= self
.eventLog
.getCurrentTime()
215 def drawBackground(self
):
216 self
.savedScreen
.fill((255,255,255))
218 if self
.extendedScale
:
219 self
.scaleSize
= self
.longScaleSize
221 self
.scaleSize
= self
.shortScaleSize
223 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
)]
224 #inflate last noteRect to cover the far right pixels
225 self
.noteRects
[-1].width
= self
.noteRects
[-1].width
+ 1
227 self
.noteRects
[self
.highlightedNote
-self
.offset
].inflate_ip(self
.noteRects
[self
.highlightedNote
-self
.offset
].width
*2,0)
229 #create bounding rect
230 self
.boundingRect
= self
.noteRects
[0].unionall(self
.noteRects
)
232 self
.renderedNoteNames
= [self
.font
.render(constants
.noteNumberToName(note
),False,(0,0,0)) for note
in self
.midiNoteNumbers
]
234 #fill the rectangles with a color gradient
236 startingHue
= 0.66666666666666663
238 # for rectNumber in range(self.scaleSize):
239 # colorRatio = float(rectNumber) / (self.scaleSize - 1)
240 # #hue will go from 0.6666... (blue) to 0 (red) as colorRation goes up
241 # hue = startingHue * (1 - colorRatio)
242 # if rectNumber + self.offset != self.highlightedNote:
243 # #The color of the bottom of the rectangle in hls coordinates
244 # bottomColorHls = (hue, 0.1, 1)
245 # #The color of the top of the rectangle in hls coordinates
246 # topColorHls = (hue, 0.1, 1)
248 # #convert to rgb ranging from 0 to 255
249 # bottomColorRgb = [floor(255 * i) for i in colorsys.hls_to_rgb(*bottomColorHls)]
250 # topColorRgb = [floor(255 * i) for i in colorsys.hls_to_rgb(*topColorHls)]
252 # bottomColorRgb.append(255)
253 # topColorRgb.append(255)
255 # bottomColorRgb = tuple(bottomColorRgb)
256 # topColorRgb = tuple(topColorRgb)
258 # self.savedScreen.blit(gradients.vertical(self.noteRects[rectNumber].size, topColorRgb, bottomColorRgb), self.noteRects[rectNumber])
260 # noteNameBlitPoint = (self.noteRects[rectNumber].left+(self.noteRects[rectNumber].width-self.renderedNoteNames[rectNumber].get_width())/2,
261 # self.noteRects[rectNumber].bottom-self.renderedNoteNames[rectNumber].get_height())
263 # self.savedScreen.blit(self.renderedNoteNames[rectNumber], noteNameBlitPoint)
265 # pygame.draw.rect(self.savedScreen, pygame.Color(0, 0, 0, 255), self.noteRects[rectNumber], 2)
267 colorRatio
= float(self
.highlightedNote
-self
.offset
) / (self
.scaleSize
- 1)
268 #hue will go from 0.6666... (blue) to 0 (red) as colorRation goes up
269 hue
= startingHue
* (1 - colorRatio
)
270 #The color of the bottom of the rectangle in hls coordinates
271 bottomColorHls
= (hue
, 0.6, 1)
272 #The color of the top of the rectangle in hls coordinates
273 topColorHls
= (hue
, 0.9, 1)
275 #convert to rgb ranging from 0 to 255
276 bottomColorRgb
= [floor(255 * i
) for i
in colorsys
.hls_to_rgb(*bottomColorHls
)]
277 topColorRgb
= [floor(255 * i
) for i
in colorsys
.hls_to_rgb(*topColorHls
)]
279 bottomColorRgb
.append(255)
280 topColorRgb
.append(255)
282 bottomColorRgb
= tuple(bottomColorRgb
)
283 topColorRgb
= tuple(topColorRgb
)
285 self
.savedScreen
.blit(gradients
.vertical(self
.noteRects
[self
.highlightedNote
-self
.offset
].size
, topColorRgb
, bottomColorRgb
), self
.noteRects
[self
.highlightedNote
-self
.offset
])
287 # noteNameBlitPoint = (self.noteRects[self.highlightedNote-self.offset].left+(self.noteRects[self.highlightedNote-self.offset].width-self.renderedNoteNames[self.highlightedNote-self.offset].get_width())/2,
288 # self.noteRects[self.highlightedNote-self.offset].bottom-self.renderedNoteNames[self.highlightedNote-self.offset].get_height())
290 # self.savedScreen.blit(self.renderedNoteNames[self.highlightedNote-self.offset], noteNameBlitPoint)
293 # renderedSyllabus = self.font.render(self.syllabus,False,(0,0,0))
295 # syllabusBlitPoint = (self.noteRects[self.highlightedNote-self.offset].left+(self.noteRects[self.highlightedNote-self.offset].width-renderedSyllabus.get_width())/2,
296 # self.noteRects[self.highlightedNote-self.offset].centery-renderedSyllabus.get_height()/2)
298 # self.savedScreen.blit(renderedSyllabus, syllabusBlitPoint)
300 pygame
.draw
.rect(self
.savedScreen
, pygame
.Color(0, 0, 0, 255), self
.noteRects
[self
.highlightedNote
-self
.offset
], 2)
302 # if self.song != None and self.blinkOn:
303 # borderSize = self.borderSize
304 # pygame.draw.rect(self.savedScreen, pygame.Color(0, 0, 0, 0), self.noteRects[self.highlightedNote-self.offset].inflate(borderSize/2,borderSize/2), borderSize)
306 def initializeWiimotes(self
):
307 for loop
in self
.wiimotes
:
308 if loop
.port
== None :
309 loop
.port
= pygame
.midi
.Output(loop
.portNumber
)
311 self
.cursorPositions
.append(loop
.cursor
.centerPosition
)
312 self
.buttonDown
.append(False)
313 self
.velocityLock
.append(False)
315 def updateCursorPositionFromJoy(self
, joyEvent
):
316 joyName
= pygame
.joystick
.Joystick(joyEvent
.joy
).get_name()
317 correctedJoyId
= constants
.joyNames
.index(joyName
)
318 if correctedJoyId
< len(self
.cursorPositions
):
319 if joyEvent
.axis
== 0 :
320 self
.cursorPositions
[correctedJoyId
] = (int((joyEvent
.value
+ 1) / 2 * self
.screen
.get_width()), self
.cursorPositions
[correctedJoyId
][1])
321 if joyEvent
.axis
== 1 :
322 self
.cursorPositions
[correctedJoyId
] = (self
.cursorPositions
[correctedJoyId
][0], int((joyEvent
.value
+ 1) / 2 * self
.screen
.get_height()))
324 def heightToVelocity(self
, pos
, controllerNumber
):
325 if self
.song
!= None:
326 if self
.boundingRect
.collidepoint(pos
) and (self
.highlightedNote
== self
.notes
[controllerNumber
] or self
.velocityLock
[controllerNumber
]):
327 velocity
= int(floor((1 - (float(pos
[1])-self
.blitOrigin
[1]) / self
.height
) * (127-self
.minimalVelocity
))+self
.minimalVelocity
)
334 if self
.boundingRect
.collidepoint(pos
):
335 velocity
= int(floor((1 - (float(pos
[1])-self
.blitOrigin
[1]) / self
.height
) * (127-self
.minimalVelocity
))+self
.minimalVelocity
)
337 velocity
= self
.minimalVelocity
340 def widthToNote(self
, pos
):
343 if self
.noteRects
[self
.highlightedNote
-self
.offset
].collidepoint(pos
) :
344 return self
.highlightedNote
346 while self
.noteRects
[nn
].collidepoint(pos
) == False:
348 return(nn
+ self
.offset
)
352 def congratulate(self
,targetRect
,posy
):
353 if self
.congratulationCount
!= None :
354 if self
.congratulationCount
< len(self
.congratulations
)-1:
355 self
.congratulationCount
+= 1
357 self
.congratulationCount
= 0
358 self
.congratulationTimer
= 0
359 self
.congratulationPos
= (targetRect
.left
+(targetRect
.width
-self
.renderedCongratulations
[self
.congratulationCount
].get_width())/2,posy
)
360 self
.isCongratulating
= True
362 def resetCongratulation(self
):
363 self
.congratulationCount
= None
364 self
.congratulationPos
= None
365 self
.isCongratulating
= False
367 def blitCongratulation(self
):
368 self
.playerScreen
.blit(self
.renderedCongratulations
[self
.congratulationCount
],self
.congratulationPos
)
370 def input(self
, event
):
372 if event
.type == pygame
.QUIT
:
373 for loop
in self
.wiimotes
:
378 if event
.type == pygame
.KEYDOWN
:
379 if event
.key
== pygame
.K_q
:
380 self
.nextLevel
= None
383 if event
.key
== pygame
.K_w
:
387 if event
.key
== pygame
.K_e
:
391 if event
.key
== pygame
.K_r
:
395 if event
.key
== pygame
.K_t
:
399 if event
.type == pygame
.JOYAXISMOTION
:
401 joyName
= pygame
.joystick
.Joystick(event
.joy
).get_name()
402 correctedJoyId
= constants
.joyNames
.index(joyName
)
403 if self
.activeWiimotes
[correctedJoyId
]:
404 self
.updateCursorPositionFromJoy(event
)
405 wiimote
= self
.wiimotes
[correctedJoyId
]
406 pos
= self
.cursorPositions
[correctedJoyId
]
408 if self
.buttonDown
[correctedJoyId
]:
409 if self
.notes
[correctedJoyId
] != None:
410 velocity
= self
.heightToVelocity(pos
, correctedJoyId
)
411 if velocity
!= None :
412 CCHexCode
= wiimote
.getCCHexCode()
413 wiimote
.port
.write_short(CCHexCode
, 07, velocity
)
414 if self
.cascade
and self
.cascadeIsFree
:
415 n
= self
.widthToNote(pos
)
416 if self
.highlightedNote
== n
:
417 wiimote
.stopNoteByNoteNumber(self
.savedMidiNoteNumbers
[self
.notes
[correctedJoyId
]])
418 self
.notes
[correctedJoyId
] = n
419 velocity
= self
.heightToVelocity(pos
, correctedJoyId
)
420 self
.velocityLock
[correctedJoyId
] = True
421 wiimote
.playNoteByNoteNumber(self
.midiNoteNumbers
[self
.notes
[correctedJoyId
]],velocity
)
422 self
.moveToNextNote()
423 self
._cascadeLockTimer
= 0
424 self
.cascadeIsFree
= False
426 if event
.type == pygame
.JOYBUTTONDOWN
:
428 joyName
= pygame
.joystick
.Joystick(event
.joy
).get_name()
429 correctedJoyId
= constants
.joyNames
.index(joyName
)
430 if self
.activeWiimotes
[correctedJoyId
]:
431 wiimote
= self
.wiimotes
[correctedJoyId
]
432 pos
= self
.cursorPositions
[correctedJoyId
]
433 self
.wiimotes
[correctedJoyId
].cursor
.flash()
436 if self
.firstClickTime
== None :
437 self
.firstClickTime
= self
.eventLog
.getCurrentTime()
439 if not self
.buttonDown
[correctedJoyId
]:
440 n
= self
.widthToNote(pos
)
441 if self
.highlightedNote
== n
:
442 self
._cascadeLockTimer
= 0
443 self
.cascadeIsFree
= False
445 wiimote
.stopNoteByNoteNumber(self
.savedMidiNoteNumbers
[self
.notes
[correctedJoyId
]])
446 self
.notes
[correctedJoyId
] = n
447 velocity
= self
.heightToVelocity(pos
, correctedJoyId
)
448 self
.velocityLock
[correctedJoyId
] = True
449 wiimote
.playNoteByNoteNumber(self
.midiNoteNumbers
[self
.notes
[correctedJoyId
]],velocity
)
450 self
.congratulate(self
.noteRects
[self
.notes
[correctedJoyId
]],pos
[1])
453 if self
.firstClickInTime
== None :
454 self
.firstClickInTime
= self
.eventLog
.getCurrentTime()
456 self
.moveToNextNote()
458 self
.resetCongratulation()
459 if not self
.easyMode
:
460 self
._cascadeLockTimer
= 0
461 self
.cascadeIsFree
= False
462 self
.notes
[correctedJoyId
] = n
463 velocity
= self
.heightToVelocity(pos
, correctedJoyId
)
464 if velocity
!= None :
465 wiimote
.playNoteByNoteNumber(self
.midiNoteNumbers
[self
.notes
[correctedJoyId
]],velocity
)
466 self
.buttonDown
[correctedJoyId
] = True
468 if event
.type == pygame
.JOYBUTTONUP
:
469 joyName
= pygame
.joystick
.Joystick(event
.joy
).get_name()
470 correctedJoyId
= constants
.joyNames
.index(joyName
)
471 if self
.activeWiimotes
[correctedJoyId
]:
472 self
.buttonDown
[correctedJoyId
] = False
473 wiimote
= self
.wiimotes
[correctedJoyId
]
474 if not self
.easyMode
:
475 wiimote
.stopNoteByNoteNumber(self
.savedMidiNoteNumbers
[self
.notes
[correctedJoyId
]])
476 self
.velocityLock
[correctedJoyId
] = False
478 if event
.type == pygame
.MOUSEMOTION
:
480 self
.updateCursorPositionFromMouse(event
)
483 while not self
.activeWiimotes
[correctedJoyId
] :
485 wiimote
= self
.wiimotes
[correctedJoyId
]
486 pos
= self
.cursorPositions
[correctedJoyId
]
488 if self
.buttonDown
[correctedJoyId
]:
489 self
.wiimotes
[correctedJoyId
].cursor
.flash()
490 if self
.notes
[correctedJoyId
] != None:
491 velocity
= self
.heightToVelocity(pos
, correctedJoyId
)
492 if velocity
!= None :
493 CCHexCode
= wiimote
.getCCHexCode()
494 wiimote
.port
.write_short(CCHexCode
, 07, velocity
)
495 if self
.cascade
and self
.cascadeIsFree
:
496 n
= self
.widthToNote(pos
)
497 if self
.highlightedNote
== n
:
498 wiimote
.stopNoteByNoteNumber(self
.savedMidiNoteNumbers
[self
.notes
[correctedJoyId
]])
499 self
.notes
[correctedJoyId
] = n
500 velocity
= self
.heightToVelocity(pos
, correctedJoyId
)
501 self
.velocityLock
[correctedJoyId
] = True
502 wiimote
.playNoteByNoteNumber(self
.midiNoteNumbers
[self
.notes
[correctedJoyId
]],velocity
)
503 self
.moveToNextNote()
504 self
._cascadeLockTimer
= 0
505 self
.cascadeIsFree
= False
507 if event
.type == pygame
.MOUSEBUTTONDOWN
:
509 if event
.button
== 1:
511 while not self
.activeWiimotes
[correctedJoyId
] :
513 wiimote
= self
.wiimotes
[correctedJoyId
]
514 pos
= self
.cursorPositions
[correctedJoyId
]
515 self
.wiimotes
[correctedJoyId
].cursor
.flash()
518 if self
.firstClickTime
== None :
519 self
.firstClickTime
= self
.eventLog
.getCurrentTime()
521 if not self
.buttonDown
[correctedJoyId
]:
522 n
= self
.widthToNote(pos
)
523 if self
.highlightedNote
== n
:
524 self
._cascadeLockTimer
= 0
525 self
.cascadeIsFree
= False
527 wiimote
.stopNoteByNoteNumber(self
.savedMidiNoteNumbers
[self
.notes
[correctedJoyId
]])
528 self
.notes
[correctedJoyId
] = n
529 velocity
= self
.heightToVelocity(pos
, correctedJoyId
)
530 self
.velocityLock
[correctedJoyId
] = True
531 wiimote
.playNoteByNoteNumber(self
.midiNoteNumbers
[self
.notes
[correctedJoyId
]],velocity
)
532 self
.congratulate(self
.noteRects
[self
.notes
[correctedJoyId
]],pos
[1])
535 if self
.firstClickInTime
== None :
536 self
.firstClickInTime
= self
.eventLog
.getCurrentTime()
538 self
.moveToNextNote()
540 self
.resetCongratulation()
541 if not self
.easyMode
:
542 self
._cascadeLockTimer
= 0
543 self
.cascadeIsFree
= False
544 self
.notes
[correctedJoyId
] = n
545 velocity
= self
.heightToVelocity(pos
, correctedJoyId
)
546 if velocity
!= None :
547 wiimote
.playNoteByNoteNumber(self
.midiNoteNumbers
[self
.notes
[correctedJoyId
]],velocity
)
548 self
.buttonDown
[correctedJoyId
] = True
550 if event
.button
== 2:
554 if event
.type == pygame
.MOUSEBUTTONUP
:
555 if event
.button
== 1 :
557 while not self
.activeWiimotes
[correctedJoyId
] :
559 wiimote
= self
.wiimotes
[correctedJoyId
]
560 self
.buttonDown
[correctedJoyId
] = False
561 if not self
.easyMode
:
562 wiimote
.stopNoteByNoteNumber(self
.savedMidiNoteNumbers
[self
.notes
[correctedJoyId
]])
563 self
.velocityLock
[correctedJoyId
] = False
565 def hasChanged(self
):
567 if self
.song
!= None:
568 if self
.blinkOn
!= self
.savedBlinkOn
or self
.highlightedNote
!= self
.savedHighlightedNote
:
569 self
.savedBlinkOn
= self
.blinkOn
570 self
.savedHighlightedNote
= self
.highlightedNote
574 def updateCursorPositionFromMouse(self
, mouseEvent
):
576 while not self
.activeWiimotes
[correctedJoyId
] :
578 self
.cursorPositions
[correctedJoyId
] = mouseEvent
.pos
580 def moveToNextNote(self
):
581 self
.savedMidiNoteNumbers
= self
.midiNoteNumbers
[:]
582 self
.highlightedNote
, self
.highlightedNoteNumber
, self
.syllabus
, self
.cascadeLockLengthMultiplier
= self
.songIterator
.next()
583 self
.midiNoteNumbers
[self
.highlightedNote
] = self
.highlightedNoteNumber