comparison visualclient2/midiclient.py @ 37:2db17c224664

added visclient2
author gyorgyf
date Fri, 24 Apr 2015 07:09:32 +1000
parents
children
comparison
equal deleted inserted replaced
36:caea7ec0c162 37:2db17c224664
1 #!/usr/bin/env python
2 # encoding: utf-8
3 """
4 visclient.py
5
6 Created by George Fazekas on 2012-06-17.
7 Copyright (c) 2012 . All rights reserved.
8 """
9
10 import sys,os,math,time,copy
11 import pygame as pg
12 from pygame.locals import *
13 import httplib as ht
14 from pygame import midi
15
16 import gradients
17 from gradients import genericFxyGradient
18
19 from threading import Thread
20 from random import random
21 import numpy as np
22
23 import colorsys as cs
24
25 NOTE_OFF_CH0 = [[[0xb0,0x7b,0],0]]
26
27 # 1 HONKY TONK 0x1 / 0x4 (5)
28 # 2 SITAR 0x9 / 0x0 (1)
29 # 3 Melancholia
30 # 4 Rock GITAR 0x3 / 0x2C (45)
31
32 IMAP = {1 : (0x1,0x4), 2: (0x9,0x0), 3:(0x8,0xb), 4:(0x3,0x2c) }
33
34
35 # from pytagcloud import create_tag_image, make_tags
36 # from pytagcloud.lang.counter import get_tag_counts
37
38 # YOUR_TEXT = "A tag cloud is a visual representation for text data, typically\
39 # used to depict keyword metadata on websites, or to visualize free form text."
40 #
41 # tags = make_tags(get_tag_counts(YOUR_TEXT), maxsize=120)
42 #
43 # create_tag_image(tags, 'cloud_large.png', size=(900, 600), fontname='Lobster')
44
45 scol = (0,255,0,255)
46 ecol = (0,0,0,255)
47
48 # X,Y=1140,900
49 # X,Y = 600,400
50 X,Y = 800,600
51
52 # Fullscreen resolution:
53 # XF,YF = 1280,900
54 # XF,YF = 1440,900
55 XF,YF = 1344,900
56 # display calibrated
57
58 # detect display resolution
59 import subprocess
60 screenres = subprocess.Popen('xrandr | grep "\*" | cut -d" " -f4',shell=True, stdout=subprocess.PIPE).communicate()[0]
61 screenres = map(lambda x: int(x.strip()), screenres.split('x'))
62 XF,YF = screenres
63 print "Screen resolution: ",XF,YF
64
65 NBLOBS = 18
66 BLOBSIZE = 25
67 G=110
68 FADE = 15
69 DIST = 0.15 # blob equivalence tolerance
70 FRAMERATE = 60
71
72 # Connection:
73 # IP = "127.0.0.1:8030"
74 # IP = "192.168.2.158:8030"
75 IP = "138.37.95.215"
76 HTTP_TIMEOUT = 3
77 SERVER_UPDATE_INTERVAL = 0.8
78
79
80 class Indicator(object):
81
82 off_color = pg.Color(110,0,0)
83 on_color = pg.Color(0,120,0)
84
85 def __init__(self,bg,pos):
86 self.visible = True
87 self.ison = True
88 self.x,self.y = pos
89 self.xs = int(self.x * X)
90 self.ys = int(Y - (self.y * Y))
91 self.c = self.off_color
92 self.size = 6
93 self.bg = bg
94
95 def reinit(self,bg):
96 self.bg = bg
97 self.xs = int(self.x * X)
98 self.ys = int(Y - (self.y * Y))
99
100 def draw(self):
101 if self.visible :
102 pg.draw.circle(self.bg, self.c, (self.xs,self.ys),self.size,0)
103
104 def toggle(self):
105 if self.ison == True :
106 self.off()
107 else :
108 self.on()
109 return self
110
111 def on(self):
112 self.c = self.on_color
113 self.ison = True
114 return self
115
116 def off(self):
117 self.c = self.off_color
118 self.ison = False
119 return self
120
121
122 class Blob(object):
123
124 def __init__(self,bg,x,y,color=(255,255,255),mood=None,fade=FADE):
125 # print x,y
126 self.x = x
127 self.y = y
128 self.quadrant = self.get_quadrant_number(x,y)
129 self.xs = x * X
130 self.ys = Y - (y * Y)
131 self.bg = bg
132 self.size = BLOBSIZE
133 self.time = time.time()
134 self.alpha = 255
135 self.c = color
136 self.count = 1
137 self.visible = True
138 self.FADE = fade
139 if mood and mood.color :
140 self.c = mood.color
141
142 def get_quadrant_number(self,x,y):
143 if x > 0.5 and y > 0.5 :
144 return 1
145 if x > 0.5 and y < 0.5 :
146 return 2
147 if x < 0.5 and y < 0.5 :
148 return 3
149 if x < 0.5 and y > 0.5 :
150 return 4
151
152
153 def __cmp__(self,other):
154 d = math.sqrt( math.pow((self.x-other.x),2) + math.pow((self.y-other.y),2) )
155 if d < DIST :
156 return 0
157 else :
158 return -1
159
160 def draw(self):
161 if not self.visible : return
162 d=int(self.size)
163 self.bg.blit(gradients.radial(self.size, (self.c[0],self.c[1],self.c[2],self.alpha), (0,0,0,self.alpha)), (self.xs-d,self.ys-d))
164 self.alpha = 255 - int(self.age()*self.FADE)
165 if self.alpha < 5 :
166 self.alpha = 1
167 self.visible = False
168
169 def age(self):
170 return time.time() - self.time
171
172 def increment(self,count):
173 self.time = time.time()
174 self.count = count
175 # self.size = int(BLOBSIZE * int(self.count/1.5))
176 self.to = int(BLOBSIZE * int(self.count/1.5))
177 self.start_animate()
178
179 def get_distance(self,x,y):
180 return math.sqrt( math.pow((self.x-x),2) + math.pow((self.y-y),2) )
181
182 def fade(self,fade):
183 if not fade : self.alpha = 255
184 self.FADE = fade
185
186 def reset_time(self):
187 self.time = time.time()
188
189 def start_animate(self):
190 self.thread = Thread(target = self.animate)
191 self.thread.daemon = True
192 self.thread.start()
193
194 def animate(self):
195 '''Animate the way bubbles are grown.'''
196 while self.size < self.to :
197 self.size += 1
198 time_inc = 20.0 / (pow(1.2, self.to-self.size) * 200.0)
199 time.sleep(0.02+time_inc)
200
201
202
203
204 class Mood():
205 def __init__(self,word,x,y):
206 self.word = word
207 self.x = float(x)
208 self.y = float(y)
209 self.color = []
210
211 def get_distance(self,x,y):
212 return math.sqrt( math.pow((self.x-x),2) + math.pow((self.y-y),2) )
213
214
215
216 class VisualClient(object):
217 '''Main visualisation client.'''
218
219 def __init__(self):
220 # self.conn = ht.HTTPConnection("192.168.2.184:8030")
221 # self.conn = ht.HTTPConnection("138.37.95.215")
222 self.s_age = 10
223 self.s_dist = DIST
224 self.s_ninp = 18
225
226 pg.init()
227
228 # fontObj = pg.font.Font("freesansbold.ttf",18)
229 white = ( 255, 255, 255)
230 black = ( 0,0,0)
231
232 self.fpsClock = pg.time.Clock()
233 self.screen = pg.display.set_mode((X, Y))
234 pg.display.set_caption('Mood Conductor')
235 self.bg = pg.Surface(self.screen.get_size())
236 self.bg = self.bg.convert()
237 self.bg.fill((0,0,0))
238 pg.display.set_gamma(100.0)
239
240
241 self.scol = (0,255,0,255)
242 self.ecol = (0,0,0,255)
243 coordstxt = "test"
244
245 self.blobs = []
246 self.moods = []
247 self.read_mood_data()
248
249 self.FADE = FADE
250
251 self.indicators = {
252 "conn":Indicator(self.bg,(0.98,0.02)),
253 "update":Indicator(self.bg,(0.96,0.02)),
254 "data":Indicator(self.bg,(0.94,0.02)),
255 "receive":Indicator(self.bg,(0.92,0.02))}
256
257 self.thread = None
258 self.running = False
259 self.fullscreen = False
260 self.init_reconnect = False
261
262 pg.midi.init()
263
264 print pg.midi.get_device_info(4)
265 self.midi_out = pg.midi.Output(4,0)
266 self.midi_out.set_instrument(6)
267
268 # self.midi_out.note_on(23,128,1)
269 # self.midi_out.write([[[0xc0,0,0],20000],[[0x90,60,100],20500]])
270
271 self.active_quadrant = 1
272 self.prev_quadrant = 1
273
274 pass
275
276
277 def read_mood_data(self):
278 '''Read the mood position and color information form csv file.'''
279 with open('moods.csv') as mf:
280 data = mf.readlines()[1:]
281 for line in data :
282 l = line.split(',')
283 mood = Mood(l[0],l[1],l[2])
284 self.moods.append(mood)
285 with open('colors.txt') as ff:
286 data = ff.readlines()[1:]
287 data = map(lambda x: x.split(','),data)
288 for mood in self.moods :
289 d = cd = sys.float_info.max
290 for colors in data :
291 d = mood.get_distance(float(colors[0]),float(colors[1]))
292 if d < cd :
293 cd = d
294 # mood.color = tuple(map(lambda x: int(pow(math.atan((float(x)/7.0)),12.5)),(colors[2],colors[3],colors[4])))
295 mood.color = self.set_color(tuple(map(lambda x: int(x),(colors[2],colors[3],colors[4]))))
296 return True
297
298 def set_color(self,color):
299 '''Move to HLS colour space and manipulate saturation there.'''
300 # TODO: ideally, we need a non-linear compressor of the lightness and saturation values
301 r,g,b = map(lambda x: (1.0*x/255.0), color)
302 h,l,s = cs.rgb_to_hls(r,g,b)
303 s = 1.0 #1.0 - (1.0 / pow(50.0,s))
304 l = 1.0 - (1.0 / pow(20.0,l)) #0.6
305 r,g,b = map(lambda x: int(x*255), cs.hls_to_rgb(h,l,s))
306 return r,g,b
307
308 def start_update_thread(self):
309 '''Start the thread that reads data from the server.'''
310 self.running = True
311 self.thread = Thread(target = self.update_thread)
312 self.thread.daemon = True
313 self.thread.start()
314
315 def stop_update_thread(self):
316 '''Stop the thread and allow some time fot the connections to close.'''
317 self.running = False
318 try :
319 self.thread.join(2)
320 except :
321 print "No update thread to join."
322
323 def update_thread(self):
324 '''The server update thread'''
325 while self.running :
326 try :
327 self.update()
328 # self.indicators["update"].visible = True
329 except Exception, e:
330 if str(e).strip() : print "Exception: ", str(e), type(e), len(str(e).strip())
331 self.indicators["conn"].off()
332 # self.indicators["update"].visible = False
333 time.sleep(SERVER_UPDATE_INTERVAL)
334
335
336 def update(self):
337 '''Update the blob list from the server. This should be in a thread.'''
338
339 # indicate connection health by toggling an indictor
340 self.indicators["update"].toggle()
341
342 # delete invisibles
343 for blob in self.blobs :
344 if not blob.visible :
345 self.blobs.remove(blob)
346
347 # get new coordinates from the server
348 self.conn.putrequest("GET","/moodconductor/result", skip_host=True)
349 self.conn.putheader("Host", "www.isophonics.net")
350 self.conn.endheaders()
351 res = self.conn.getresponse()
352 data = res.read()
353 data = eval(data)
354 if not data :
355 self.conn.close()
356 self.indicators["data"].toggle()
357 return False
358 for d in data :
359 # coordstxt = "x:%s y:%s c:%s" %d
360 x,y,count = d
361 self.add_blob(x,y,count)
362 self.indicators["receive"].toggle()
363 self.conn.close()
364 self.blobs = self.blobs[:NBLOBS]
365 self.compute_quadrant_weighting()
366 self.self_change_instrument()
367 return True
368
369 def compute_quadrant_weighting(self):
370 quadrant_dict = {1:[],2:[],3:[],4:[]}
371 # sort blobs into quadrants
372 for blob in self.blobs :
373 quadrant_dict[blob.quadrant].append(blob)
374 # get weight for each
375 quadrant_weights = []
376 for q,blob_list in quadrant_dict.iteritems() :
377 quadrant_weights.append(sum(map(lambda x: x.alpha * x.size,blob_list)))
378 self.active_quadrant = np.argmax(quadrant_weights) + 1
379 print self.active_quadrant
380 return self.active_quadrant
381
382 def self_change_instrument(self):
383 if self.active_quadrant != self.prev_quadrant :
384 self.prev_quadrant = self.active_quadrant
385 args = IMAP[self.active_quadrant]
386 self.send_midi_patch_change_GR20(*args)
387 print args
388
389 def add_blob(self,x,y,count=1):
390 '''Insert a blob to the list of blobs'''
391 # find mood correxponding to x,y
392 cmood = None
393 d = cd = sys.float_info.max
394 for mood in self.moods :
395 d = mood.get_distance(x,y)
396 if d < cd :
397 cd = d
398 cmood = mood
399 # create new blob or increase click count on existing one
400 new = Blob(self.bg,x,y,mood=cmood,fade=self.FADE)
401 if not new in self.blobs :
402 self.blobs.insert(0,new)
403 # self.send_midi()
404 elif count > self.blobs[self.blobs.index(new)].count:
405 self.blobs[self.blobs.index(new)].increment(count)
406 pass
407
408 def send_midi(self):
409 # self.midi_out.write([[[0xc0,0,0],20000],[[0x90,60,100],20500]])
410 self.midi_out.write([[[0x90,60,100],0],[[0x90,60,100],500]])
411
412 def send_midi_patch_change_GR20(self,bank,instrument):
413 '''PIANO = BANK 1, Patch 5.. bank starts from 1, patch starts from 0 so I have to substract one...'''
414 self.midi_out.write([[[0xB0,0x0,bank],0],[[0xC0,instrument],100]])
415 # Control change (B) followed by patch change (C):
416 # midi_out.write([[[0xB0,0x0,0x9],0],[[0xC0,0x0],100]])
417 # midi_out.write([[[0xB0,0x0,int(bank)],0],[[0xC0,int(instrument)-1],100]])
418 # 1 HONKY TONK 0x1 / 0x4 (5)
419 # 2 SITAR 0x9 / 0x0 (1)
420 # 3 Melancholia
421 # 4 Rock GITAR 0x3 / 0x2C (45)
422
423
424 def draw(self):
425 self.bg.fill((0,0,0))
426 # self.bg.blit(gradients.radial(19, self.scol, self.ecol), (rect_x,rect_y))
427 l = copy.copy(self.blobs)
428 l.reverse()
429 for blob in l :
430 blob.draw()
431
432 # axis
433 pg.draw.line(self.bg, (G,G,G), (int(X/2.0),0),(int(X/2.0),Y), 1)
434 pg.draw.line(self.bg, (G,G,G), (0,int(Y/2.0)),(X,int(Y/2.0)), 1)
435
436 # indicators
437 for i in self.indicators.itervalues() :
438 i.draw()
439
440 def read_keys(self):
441 '''Read keys'''
442 for event in pg.event.get() :
443 # Quit (event)
444 if event.type == QUIT:
445 self.midi_out.write(NOTE_OFF_CH0)
446 self.quit()
447 elif event.type == KEYDOWN :
448 # Post Quit: Esc, q
449 if event.key == K_ESCAPE or event.key == K_q :
450 pg.event.post(pg.event.Event(QUIT))
451 # Reset: Space
452 elif event.key == K_SPACE :
453 self.blobs = []
454 # Random : r
455 elif event.key == K_r :
456 self.add_blob(random(),random(),count=5)
457 # Connection : c
458 elif event.key == K_c :
459 self.init_reconnect = True
460 self.indicators["conn"].off()
461 # Fullscreen: f
462 elif event.key == K_f :
463 # pg.display.toggle_fullscreen()
464 self.toggle_screen_mode()
465 # Toggle fade: s
466 elif event.key == K_s :
467 if self.FADE :
468 print "fade off"
469 self.indicators["conn"].off()
470 self.FADE = 0
471 for blob in self.blobs :
472 blob.fade(0)
473 else:
474 print "fade on"
475 self.indicators["conn"].on()
476 self.FADE = 15
477 for blob in self.blobs :
478 blob.fade(15)
479 blob.reset_time()
480 # inc age
481 elif event.key == K_1 :
482 self.s_age += 1
483 self.update_server_config(self.s_age,self.s_dist,self.s_ninp)
484 # dec age
485 elif event.key == K_2 :
486 self.s_age -= 1
487 self.update_server_config(self.s_age,self.s_dist,self.s_ninp)
488 # inc dist
489 elif event.key == K_3 :
490 self.s_dist += 0.025
491 self.update_server_config(self.s_age,self.s_dist,self.s_ninp)
492 # dec dist
493 elif event.key == K_4 :
494 self.s_dist -= 0.025
495 if self.s_dist < 0.025 : self.s_dist = 0.025
496 self.update_server_config(self.s_age,self.s_dist,self.s_ninp)
497 # inc ninp
498 elif event.key == K_5 :
499 self.s_ninp += 1
500 self.update_server_config(self.s_age,self.s_dist,self.s_ninp)
501 # dec ninp
502 elif event.key == K_6 :
503 self.s_ninp -= 1
504 if self.s_ninp < 2 : self.s_ninp = 2
505 self.update_server_config(self.s_age,self.s_dist,self.s_ninp)
506
507 pass
508 pass
509
510 def toggle_screen_mode(self):
511 '''Go back and forth between full screen mode.'''
512 if self.fullscreen == False:
513 globals()['_X'] = globals()['X']
514 globals()['_Y'] = globals()['Y']
515 globals()['X'] = XF
516 globals()['Y'] = YF
517 self.screen = pg.display.set_mode((X, Y))
518 # self.screen = pg.display.set_mode((0,0),pg.FULLSCREEN)
519 self.fullscreen = True
520 self.bg = pg.Surface(self.screen.get_size())
521 self.bg = self.bg.convert()
522 self.bg.fill((0,0,0))
523 for i in self.indicators.itervalues() :
524 i.reinit(self.bg)
525 i.draw()
526 else :
527 globals()['X'] = globals()['_X']
528 globals()['Y'] = globals()['_Y']
529 self.screen = pg.display.set_mode((X, Y))
530 self.fullscreen = False
531 self.bg = pg.Surface(self.screen.get_size())
532 self.bg = self.bg.convert()
533 self.bg.fill((0,0,0))
534 for i in self.indicators.itervalues() :
535 i.reinit(self.bg)
536 i.draw()
537 pg.display.toggle_fullscreen()
538
539
540
541 def run(self):
542
543 # setup connection
544 self.connect()
545
546 # main loop
547 while True :
548 # pg.draw.circle(screen, pg.Color(255,0,0), (300,50),20,0)
549 # screen.blit(gradients.radial(99, scol, ecol), (401, 1))
550
551 self.read_keys()
552
553 # Draw
554 self.draw()
555
556 # update display
557 self.screen.blit(self.bg, (0, 0))
558 pg.display.flip()
559 self.fpsClock.tick(FRAMERATE)
560
561 if self.init_reconnect:
562 self.reconnect()
563
564 return True
565
566 def configure_server(self):
567 '''Send the server some configuration data.'''
568 # age = 10.0
569 # dist = DIST
570 # ninp = 18
571 self.update_server_config(self.s_age,self.s_dist,self.s_ninp)
572
573
574 def update_server_config(self,age,dist,ninp,retry = 3):
575 '''Send the server some configuration data.'''
576 try :
577 self.conn.putrequest("GET","/moodconductor/config?age=%(age)s&dist=%(dist)s&ninp=%(ninp)s" %locals(), skip_host=True)
578 self.conn.putheader("Host", "www.isophonics.net")
579 self.conn.endheaders()
580 res = self.conn.getresponse()
581 if not res.status == 200 :
582 print "Server response:", res.status, res.reason
583 self.indicators["conn"].off()
584 time.sleep(0.5)
585 self.conn.putrequest("GET","/moodconductor/getconf", skip_host=True)
586 self.conn.putheader("Host", "www.isophonics.net")
587 self.conn.endheaders()
588 res = self.conn.getresponse()
589 if not res.status == 200 :
590 print "Server response:", res.status, res.reason
591 self.indicators["conn"].off()
592 print "Server configuration:", res.read()
593 except:
594 time.sleep(2)
595 retry -= 1
596 self.update_server_config(age,dist,ninp,retry)
597
598
599 def connect(self,retry = 5):
600 '''Connect to the server and test connection.'''
601 if not retry :
602 print "Server unreachable"
603 pg.quit()
604 raise SystemExit
605 if retry < 5 :
606 time.sleep(3)
607 try :
608 self.conn = ht.HTTPConnection(IP,timeout=HTTP_TIMEOUT)
609 # self.start_update_thread()
610 self.indicators["conn"].on()
611 except :
612 self.indicators["conn"].off()
613 self.connect(retry = retry-1)
614
615 try:
616 self.conn.putrequest("GET","/moodconductor/index.html", skip_host=True)
617 self.conn.putheader("Host", "www.isophonics.net")
618 self.conn.endheaders()
619 res = self.conn.getresponse()
620 if res.status == 200 :
621 self.indicators["conn"].on()
622 else :
623 print "Server response:", res.status, res.reason
624 self.indicators["conn"].off()
625 if not hasattr(self,"noretry") and raw_input("Go offline? ") in ['y',''] :
626 return False
627 else :
628 self.noretry = None
629 self.connect(retry = retry-1)
630 except :
631 print "Exception while testing connection."
632 self.indicators["conn"].off()
633 # comment out in offline mode
634 if not hasattr(self,"noretry") and raw_input("Go offline? ") in ['y',''] :
635 return False
636 else :
637 self.noretry = None
638 self.connect(retry = retry-1)
639 self.configure_server()
640 self.start_update_thread()
641 return True
642
643
644
645 def reconnect(self):
646 '''Called when c is pressed.'''
647 self.init_reconnect = False
648 # self.indicators["conn"].off().draw()
649 # self.screen.blit(self.bg, (0, 0))
650 # pg.display.flip()
651
652 self.stop_update_thread()
653 time.sleep(1)
654 try :
655 self.conn.close()
656 except :
657 self.indicators["conn"].off()
658 try :
659 self.conn = ht.HTTPConnection(IP,timeout=HTTP_TIMEOUT)
660 self.conn.connect()
661 self.indicators["conn"].on()
662 print "Reconnected."
663 except :
664 self.indicators["conn"].off()
665 print "Error while reconnecting."
666 self.start_update_thread()
667
668
669 def quit(self):
670 print "Quitting.."
671 self.indicators["conn"].off()
672 self.stop_update_thread()
673 self.conn.close()
674 pg.quit()
675 sys.exit()
676
677
678
679
680
681 def main():
682
683 v = VisualClient()
684 v.run()
685
686
687 if __name__ == '__main__':
688 pass
689 main()
690