bugfix.
[minwii.git] / src / app / 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 from colorsys import hls_to_rgb
11 from gradients import gradients
12 from cursors import WarpingCursor
13 import events
14 from eventutils import event_handler, EventDispatcher, EventHandlerMixin
15 from math import floor
16 import types
17 from musicxml import Tone
18
19 from config import FRAMERATE
20 from config import BORDER
21 from config import FIRST_HUE
22 from config import OFF_LUMINANCE
23 from config import OFF_SATURATION
24 from config import ON_TOP_LUMINANCE
25 from config import ON_BOTTOM_LUMINANCE
26 from config import ON_SATURATION
27 from config import ON_COLUMN_OVERSIZING
28 from config import ON_COLUMN_ALPHA
29 from config import FONT
30 from config import FONT_COLOR
31 from config import DEFAULT_MIDI_VELOCITY
32
33 BACKGROUND_LAYER = 0
34 FOREGROUND_LAYER = 1
35 CURSOR_LAYER = 2
36 PLAYING_MODES = {'EASY':0
37 ,'NORMAL':1
38 ,'ADVANCED':2
39 ,'EXPERT':3}
40
41 class _PlayingScreenBase(pygame.sprite.LayeredDirty, EventHandlerMixin) :
42
43 def __init__(self, synth, distinctNotes=[]) :
44 """
45 distinctNotes : notes disctinctes présentes dans la chanson
46 triées du plus grave au plus aigu.
47 """
48 super(_PlayingScreenBase, self).__init__()
49 self.synth = synth
50 self.distinctNotes = distinctNotes
51 self.keyboardLength = 0
52 self.keyboardRects = []
53 self.cursor = None
54 self._initRects()
55 self.columns = {}
56 self._initColumns()
57 self._running = False
58 self.draw(pygame.display.get_surface())
59 self._initCursor()
60
61
62 def _initRects(self) :
63 """ création des espaces réservés pour
64 afficher les colonnes.
65 """
66 #ambitus = self.distinctNotes[-1].midi - self.distinctNotes[0].midi
67 #if ambitus <= 12 :
68 # self.keyboardLength = 8
69 #else :
70 # self.keyboardLength = 11
71 self.keyboardLength = len(self.distinctNotes)
72
73 screen = pygame.display.get_surface()
74
75 # taille de la zone d'affichage utile (bordure autour)
76 dispWidth = screen.get_width() - 2 * BORDER
77 dispHeight = screen.get_height() - 2 * BORDER
78
79 columnWidth = int(round(float(dispWidth) / self.keyboardLength))
80
81 rects = []
82 for i in range(self.keyboardLength) :
83 upperLeftCorner = (i*columnWidth + BORDER, BORDER)
84 rect = pygame.Rect(upperLeftCorner, (columnWidth, dispHeight))
85 rects.append(rect)
86
87 self.keyboardRects = rects
88
89 def _initColumns(self) :
90
91 hueStep = FIRST_HUE / (self.keyboardLength - 1)
92 for i, rect in enumerate(self.keyboardRects) :
93 hue = FIRST_HUE - hueStep * i
94 tone = self.distinctNotes[i]
95 c = Column(self, hue, rect, tone)
96 self.add(c, layer=BACKGROUND_LAYER)
97 self.columns[tone.midi] = c
98
99
100 def _initCursor(self) :
101 self.cursor = WarpingCursor(blinkMode=True)
102 self.add(self.cursor, layer=CURSOR_LAYER)
103
104 def run(self):
105 self._running = True
106 clock = pygame.time.Clock()
107 pygame.display.flip()
108 pygame.mouse.set_visible(False)
109 while self._running :
110 EventDispatcher.dispatchEvents()
111 dirty = self.draw(pygame.display.get_surface())
112 pygame.display.update(dirty)
113 clock.tick(FRAMERATE)
114
115 pygame.mouse.set_visible(True)
116 self.cursor._stopBlink()
117
118 @event_handler(pygame.KEYDOWN)
119 def handleKeyDown(self, event) :
120 if event.key == pygame.K_q:
121 self._running = False
122
123
124 class PlayingScreen(_PlayingScreenBase) :
125 "fenêtre de jeu pour improvisation"
126
127 scale = [55, 57, 59, 60, 62, 64, 65, 67, 69, 71, 72]
128
129 def __init__(self, synth) :
130 distinctNotes = []
131 for midi in self.scale :
132 tone = Tone(midi)
133 distinctNotes.append(tone)
134
135 super(PlayingScreen, self).__init__(synth, distinctNotes)
136
137 @event_handler(events.NOTEON)
138 def noteon(self, evt) :
139 tone = evt.tone
140 self.synth.noteon(0, tone.midi, DEFAULT_MIDI_VELOCITY)
141
142 @event_handler(events.NOTEOFF)
143 def noteoff(self, evt) :
144 tone = evt.tone
145 self.synth.noteoff(0, tone.midi)
146
147
148 class SongPlayingScreen(_PlayingScreenBase) :
149
150 def __init__(self, synth, song, mode=PLAYING_MODES['NORMAL']) :
151 super(SongPlayingScreen, self).__init__(synth, song.distinctNotes)
152 self.song = song
153 self.noteIterator = self.song.iterNotes()
154 self.play()
155
156 def play(self) :
157 note, verseIndex = self.noteIterator.next()
158 syllabus = note.lyrics[verseIndex].syllabus()
159 column = self.columns[note.midi]
160 column.update(True, syllabus)
161
162
163 @event_handler(events.NOTEON)
164 def noteon(self, evt) :
165 tone = evt.tone
166 self.synth.noteon(0, tone.midi, DEFAULT_MIDI_VELOCITY)
167
168 @event_handler(events.NOTEOFF)
169 def noteoff(self, evt) :
170 tone = evt.tone
171 self.synth.noteoff(0, tone.midi)
172
173
174
175 class Column(pygame.sprite.DirtySprite, EventHandlerMixin) :
176
177 def __init__(self, group, hue, rect, tone) :
178 pygame.sprite.DirtySprite.__init__(self, group)
179 self.state = False
180
181 # nom de l'intonation
182 self.tone = tone
183 toneName = FONT.render(tone.nom, True, (0,0,0))
184
185 # état off : surface unie et nom de l'intonation
186 sur = pygame.surface.Surface(rect.size)
187 rgba = hls_to_rgba_8bits(hue, OFF_LUMINANCE, OFF_SATURATION)
188 sur.fill(rgba)
189 w, h = rect.w, rect.h
190 tw, th, = toneName.get_size()
191 toneRect = pygame.Rect(((w - tw) / 2, h - th), (tw, th))
192 sur.blit(toneName, toneRect)
193 self.surOff = sur
194 self.rectOff = rect
195
196
197 # état on : surface dégradée avec nom de la note avec largeur agrandie
198 topRgba = hls_to_rgba_8bits(hue, ON_TOP_LUMINANCE, ON_SATURATION, ON_COLUMN_ALPHA)
199 bottomRgba = hls_to_rgba_8bits(hue, ON_BOTTOM_LUMINANCE, ON_SATURATION, ON_COLUMN_ALPHA)
200 onWidth = rect.width * ON_COLUMN_OVERSIZING
201 onLeft = rect.centerx - onWidth / 2
202 rectOn = pygame.Rect((onLeft, 0),
203 (onWidth, rect.height))
204 self.surOn = gradients.vertical(rectOn.size, topRgba, bottomRgba)
205 w, h = rectOn.w, rectOn.h
206 toneRect = pygame.Rect(((w - tw) / 2, h - th), (tw, th))
207 self.surOn.blit(toneName, toneRect)
208 self.rectOn = rectOn
209
210 self.image = self.surOff
211 self.rect = rect
212 #EventDispatcher.addEventListener(pygame.MOUSEBUTTONDOWN, self.onMouseDown)
213 #EventDispatcher.addEventListener(pygame.MOUSEBUTTONUP, self.onMouseUp)
214
215 def update(self, state, syllabus='') :
216 group = self.groups()[0]
217 if state == self.state :
218 # no changes
219 return
220 if state :
221 group.change_layer(self, FOREGROUND_LAYER)
222 sur = self.surOn
223 if syllabus :
224 sur = sur.copy()
225 renderedSyl = FONT.render(syllabus, True, (0,0,0))
226 sw, sh, = renderedSyl.get_size()
227 w, h = self.rectOn.w, self.rectOn.h
228 sylRect = pygame.Rect(((w - sw) / 2, (h - sh) / 2), (sw, sh))
229 sur.blit(renderedSyl, sylRect)
230
231 self.image = sur
232 self.rect = self.rectOn
233 else :
234 group.change_layer(self, BACKGROUND_LAYER)
235 self.image = self.surOff
236 self.rect = self.rectOff
237 self.state = state
238 self.dirty = 1
239
240 @event_handler(pygame.MOUSEBUTTONDOWN)
241 def onMouseDown(self, event) :
242 if self.rect.collidepoint(*event.pos) :
243 self.update(True)
244 self.raiseNoteOn()
245
246 @event_handler(pygame.MOUSEBUTTONUP)
247 def onMouseUp(self, event) :
248 self.update(False)
249 self.raiseNoteOff()
250
251 def raiseNoteOn(self) :
252 evt = pygame.event.Event(events.NOTEON, tone=self.tone)
253 pygame.event.post(evt)
254
255 def raiseNoteOff(self) :
256 evt = pygame.event.Event(events.NOTEOFF, tone=self.tone)
257 pygame.event.post(evt)
258
259
260
261 def hls_to_rgba_8bits(h, l, s, a=1) :
262 #convert to rgb ranging from 0 to 255
263 rgba = [floor(255 * i) for i in hls_to_rgb(h, l, s) + (a,)]
264 return tuple(rgba)