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
15 class SongPlayingScreen
:
17 The screen on which the game is played
20 The wiimotes used in this session
22 The main display window
24 The main display surface
26 The clock used to animate the screen
28 The background that is painted every time
30 The buffer for painting everything before bliting
32 The width of the window in pixels
34 The height of the window in pixels
36 True if the scale is G to C instead of C to C
38 True if crossing from note to note with a button pressed triggers a new note
40 The size of the scale used
42 The positions of the cursors on the screen, in pixels
47 def __init__(self
, instrumentChoice
, song
, cascade
=False, extendedScale
=False, easyMode
= False, alwaysDown
= False, eventLog
= None, replay
= None, defaultInstrumentChannel
= 16, defaultNote
= 60):
51 self
.songDurations
= []
52 self
.totalDuration
= None
55 self
.clicksPerMinute
= [0]
56 self
.clicksInPerMinute
= [0]
57 self
.meanTimeBetweenNotes
= []
58 self
.firstClick
= None
59 self
.firstClickIn
= None
61 self
.blinkLength
= 200
62 self
.minimalVelocity
= 90
63 self
.shortScaleSize
= 8
64 self
.longScaleSize
= 11
66 self
.offset
= self
.longScaleSize
- self
.shortScaleSize
70 self
.highlightedNote
= 0
71 self
.highlightedNoteNumber
= 0
73 self
.savedHighlightedNote
= 0
74 self
.alwaysDown
= alwaysDown
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
93 self
.eventLog
= instrumentChoice
.eventLog
95 self
.eventLog
= eventLog
96 self
.cursorPositions
= instrumentChoice
.cursorPositions
98 self
.songIterator
= self
.song
.getSongIterator()
99 self
.midiNoteNumbers
= self
.song
.scale
101 self
.replay
= instrumentChoice
.replay
104 self
.quarterNoteLength
= song
.quarterNoteLength
105 self
.cascadeLockLengthMultiplier
= 1
106 self
.nextCascadeLockLengthMultiplier
= 1
107 self
.cascadeLockLength
= self
.quarterNoteLength
* self
.cascadeLockLengthMultiplier
109 self
.defaultInstrumentChannel
= defaultInstrumentChannel
110 self
.defaultNote
= defaultNote
113 self
.backToInstrumentChoice
= False
114 self
.easyMode
= easyMode
116 #Initializes the highlightedNote and highlightedNoteNumber etc...
117 self
.moveToNextNote()
118 self
.cascadeLockLengthMultiplier
= self
.nextCascadeLockLengthMultiplier
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
127 self
.boundingRect
= None
131 self
.velocityLock
= []
133 self
._blinkOffset
= 0
134 self
._cascadeLockTimer
= 0
135 self
.cascadeIsFree
= True
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
]
140 self
.drawBackground()
141 self
.initializeWiimotes()
143 self
.songStartTime
= self
.eventLog
.getCurrentTime()
146 while not self
.done
:
148 #Clear the cursors from the screen
149 if self
.hasChanged():
150 self
.drawBackground()
151 self
.playerScreen
.blit(self
.savedScreen
, (0, 0))
153 # Limit frame speed to 50 FPS
155 timePassed
= self
.clock
.tick(10000)
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
165 if self
._blinkOffset
> self
.blinkLength
:
166 self
._blinkOffset
-= self
.blinkLength
167 self
.blinkOn
= not self
.blinkOn
170 self
.eventLog
.update(timePassed
)
171 pickledEventsToPost
= self
.eventLog
.getPickledEvents()
172 for pickledEvent
in pickledEventsToPost
:
173 pygame
.event
.post(pickledEvent
.event
)
175 events
= pygame
.event
.get()
178 pickledEvents
= [PickleableEvent(event
.type,event
.dict) for event
in events
]
179 if pickledEvents
!= [] :
180 self
.eventLog
.appendEventGroup(pickledEvents
)
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
)
192 self
.screen
.blit(self
.playerScreen
, (0,0))
194 pygame
.display
.flip()
196 for i
in range(len(self
.wiimotes
)):
197 if self
.activeWiimotes
[i
]:
198 self
.wiimotes
[i
].stopNoteByNoteNumber(self
.midiNoteNumbers
[self
.notes
[i
]])
200 self
.totalDuration
= self
.eventLog
.getCurrentTime()
202 def drawBackground(self
):
203 self
.savedScreen
.fill((255,255,255))
205 if self
.extendedScale
:
206 self
.scaleSize
= self
.longScaleSize
208 self
.scaleSize
= self
.shortScaleSize
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
214 self
.noteRects
[self
.highlightedNote
-self
.offset
].inflate_ip(self
.noteRects
[self
.highlightedNote
-self
.offset
].width
*2,0)
216 #create bounding rect
217 self
.boundingRect
= self
.noteRects
[0].unionall(self
.noteRects
)
219 self
.renderedNoteNames
= [self
.font
.render(constants
.noteNumberToName(note
),False,(0,0,0)) for note
in self
.midiNoteNumbers
]
221 #fill the rectangles with a color gradient
223 startingHue
= 0.66666666666666663
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)
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
)]
239 bottomColorRgb
.append(255)
240 topColorRgb
.append(255)
242 bottomColorRgb
= tuple(bottomColorRgb
)
243 topColorRgb
= tuple(topColorRgb
)
245 self
.savedScreen
.blit(gradients
.vertical(self
.noteRects
[rectNumber
].size
, topColorRgb
, bottomColorRgb
), self
.noteRects
[rectNumber
])
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())
250 self
.savedScreen
.blit(self
.renderedNoteNames
[rectNumber
+self
.offset
], noteNameBlitPoint
)
252 pygame
.draw
.rect(self
.savedScreen
, pygame
.Color(0, 0, 0, 255), self
.noteRects
[rectNumber
], 2)
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)
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
)]
266 bottomColorRgb
.append(255)
267 topColorRgb
.append(255)
269 bottomColorRgb
= tuple(bottomColorRgb
)
270 topColorRgb
= tuple(topColorRgb
)
272 self
.savedScreen
.blit(gradients
.vertical(self
.noteRects
[self
.highlightedNote
-self
.offset
].size
, topColorRgb
, bottomColorRgb
), self
.noteRects
[self
.highlightedNote
-self
.offset
])
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())
277 self
.savedScreen
.blit(self
.renderedNoteNames
[self
.highlightedNote
], noteNameBlitPoint
)
280 renderedSyllabus
= self
.font
.render(self
.syllabus
,False,(0,0,0))
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)
285 self
.savedScreen
.blit(renderedSyllabus
, syllabusBlitPoint
)
287 pygame
.draw
.rect(self
.savedScreen
, pygame
.Color(0, 0, 0, 255), self
.noteRects
[self
.highlightedNote
-self
.offset
], 2)
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
)
293 def initializeWiimotes(self
):
294 for loop
in self
.wiimotes
:
295 if loop
.port
== None :
296 loop
.port
= pygame
.midi
.Output(loop
.portNumber
)
298 self
.buttonDown
.append(False)
299 self
.velocityLock
.append(False)
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()))
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
)
320 if self
.boundingRect
.collidepoint(pos
):
321 velocity
= int(floor((1 - (float(pos
[1])-self
.blitOrigin
[1]) / self
.height
) * (127-self
.minimalVelocity
))+self
.minimalVelocity
)
323 velocity
= self
.minimalVelocity
326 def widthToNote(self
, pos
):
329 if self
.noteRects
[self
.highlightedNote
-self
.offset
].collidepoint(pos
) :
330 return self
.highlightedNote
332 while self
.noteRects
[nn
].collidepoint(pos
) == False:
334 return(nn
+ self
.offset
)
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
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
360 def input(self
, event
):
362 if event
.type == pygame
.QUIT
:
363 for loop
in self
.wiimotes
:
368 if event
.type == pygame
.KEYDOWN
:
369 if event
.key
== pygame
.K_q
:
370 self
.nextLevel
= None
373 if event
.key
== pygame
.K_i
:
374 self
.backToInstrumentChoice
= True
377 if event
.key
== pygame
.K_w
:
381 if event
.key
== pygame
.K_e
:
385 if event
.key
== pygame
.K_r
:
389 if event
.key
== pygame
.K_t
:
393 if event
.type == pygame
.JOYAXISMOTION
:
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
]
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
420 if event
.type == pygame
.JOYBUTTONDOWN
:
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()
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
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
)
444 self
.moveToNextNote()
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
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
465 if event
.type == pygame
.MOUSEMOTION
:
467 self
.updateCursorPositionFromMouse(event
)
470 while not self
.activeWiimotes
[correctedJoyId
] :
472 wiimote
= self
.wiimotes
[correctedJoyId
]
473 pos
= self
.cursorPositions
[correctedJoyId
]
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
494 if event
.type == pygame
.MOUSEBUTTONDOWN
:
496 if event
.button
== 1:
498 while not self
.activeWiimotes
[correctedJoyId
] :
500 wiimote
= self
.wiimotes
[correctedJoyId
]
501 pos
= self
.cursorPositions
[correctedJoyId
]
502 self
.wiimotes
[correctedJoyId
].cursor
.flash()
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
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
)
519 self
.moveToNextNote()
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
530 if event
.button
== 2:
534 if event
.type == pygame
.MOUSEBUTTONUP
:
535 if event
.button
== 1 :
537 while not self
.activeWiimotes
[correctedJoyId
] :
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
546 def hasChanged(self
):
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
555 def updateCursorPositionFromMouse(self
, mouseEvent
):
557 while not self
.activeWiimotes
[correctedJoyId
] :
559 self
.cursorPositions
[correctedJoyId
] = mouseEvent
.pos
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