Mercurial > hg > mood-conductor
view visualclient/visclient.py @ 54:c0b34039917a tip
Server: added an exposed function to log the start time of a performance (for log-to-audio sync)
author | Mathieu Barthet <mathieu.barthet@eecs.qmul.ac.uk> |
---|---|
date | Wed, 14 Oct 2015 19:20:08 +0100 |
parents | ea2ec5f95ad8 |
children |
line wrap: on
line source
#!/usr/bin/env python # encoding: utf-8 """ visclient.py Created by George Fazekas on 2012-06-17. Copyright (c) 2012 . All rights reserved. """ import sys,os,math,time,copy import pygame as pg from pygame.locals import * from pygame import gfxdraw as gd import httplib as ht import gradients from gradients import genericFxyGradient from threading import Thread from random import random import colorsys as cs scol = (0,255,0,255) ecol = (0,0,0,255) # X,Y=1140,900 # X,Y = 600,400 X,Y = 800,600 # Fullscreen resolution: # XF,YF = 1280,900 # XF,YF = 1440,900 XF,YF = 1344,900 # display calibrated # detect display resolution import subprocess screenres = subprocess.Popen('xrandr | grep "\*" | cut -d" " -f4',shell=True, stdout=subprocess.PIPE).communicate()[0] screenres = map(lambda x: int(x.strip()), screenres.split('x')) XF,YF = screenres print "Screen resolution: ",XF,YF NBLOBS = 18 BLOBSIZE = 25 G=110 FADE = 15 DIST = 0.15 # blob equivalence tolerance FRAMERATE = 60 # Connection: # IP = "127.0.0.1:8030" # IP = "192.168.2.158:8030" IP = "138.37.95.215" HTTP_TIMEOUT = 3 SERVER_UPDATE_INTERVAL = 0.8 class Indicator(object): off_color = pg.Color(110,0,0) on_color = pg.Color(0,120,0) def __init__(self,bg,pos): self.visible = True self.ison = True self.x,self.y = pos self.xs = int(self.x * X) self.ys = int(Y - (self.y * Y)) self.c = self.off_color self.size = 6 self.bg = bg def reinit(self,bg): self.bg = bg self.xs = int(self.x * X) self.ys = int(Y - (self.y * Y)) def draw(self): if self.visible : # pg.draw.circle(self.bg, self.c, (self.xs,self.ys),self.size,0) # gd.aacircle(self.bg, self.xs, self.ys, self.size+1, self.c) gd.filled_circle(self.bg, self.xs, self.ys, self.size, self.c) gd.aacircle(self.bg, self.xs, self.ys, self.size, self.c) def toggle(self): if self.ison == True : self.off() else : self.on() return self def on(self): self.c = self.on_color self.ison = True return self def off(self): self.c = self.off_color self.ison = False return self class Blob(object): def __init__(self,bg,x,y,color=(255,255,255),mood=None,fade=FADE): self.x = x self.y = y self.xs = x * X self.ys = Y - (y * Y) self.bg = bg self.size = BLOBSIZE self.time = time.time() self.alpha = 255 self.c = color self.count = 1 self.visible = True self.FADE = fade if mood and mood.color : self.c = mood.color def __cmp__(self,other): d = math.sqrt( math.pow((self.x-other.x),2) + math.pow((self.y-other.y),2) ) if d < DIST : return 0 else : return -1 def draw(self): if not self.visible : return d=int(self.size) 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)) self.alpha = 255 - int(self.age()*self.FADE) if self.alpha < 5 : self.alpha = 1 self.visible = False def age(self): return time.time() - self.time def increment(self,count): self.time = time.time() self.count = count # self.size = int(BLOBSIZE * int(self.count/1.5)) self.to = int(BLOBSIZE * int(self.count/1.5)) self.start_animate() def get_distance(self,x,y): return math.sqrt( math.pow((self.x-x),2) + math.pow((self.y-y),2) ) def fade(self,fade): if not fade : self.alpha = 255 self.FADE = fade def reset_time(self): self.time = time.time() def start_animate(self): self.thread = Thread(target = self.animate) self.thread.daemon = True self.thread.start() def animate(self): '''Animate the way bubbles are grown.''' while self.size < self.to : self.size += 1 time_inc = 20.0 / (pow(1.2, self.to-self.size) * 200.0) time.sleep(0.02+time_inc) class Mood(): def __init__(self,word,x,y): self.word = word self.x = float(x) self.y = float(y) self.color = [] def get_distance(self,x,y): return math.sqrt( math.pow((self.x-x),2) + math.pow((self.y-y),2) ) class VisualClient(object): '''Main visualisation client.''' def __init__(self): # self.conn = ht.HTTPConnection("192.168.2.184:8030") # self.conn = ht.HTTPConnection("138.37.95.215") self.s_age = 10 self.s_dist = DIST self.s_ninp = 18 pg.init() # fontObj = pg.font.Font("freesansbold.ttf",18) white = ( 255, 255, 255) black = ( 0,0,0) self.fpsClock = pg.time.Clock() self.screen = pg.display.set_mode((X, Y)) pg.display.set_caption('Mood Conductor') self.bg = pg.Surface(self.screen.get_size()) self.bg = self.bg.convert() self.bg.fill((0,0,0)) pg.display.set_gamma(100.0) self.scol = (0,255,0,255) self.ecol = (0,0,0,255) coordstxt = "test" self.blobs = [] self.moods = [] self.read_mood_data() self.FADE = FADE self.indicators = { "conn":Indicator(self.bg,(0.98,0.02)), "update":Indicator(self.bg,(0.96,0.02)), "data":Indicator(self.bg,(0.94,0.02)), "receive":Indicator(self.bg,(0.92,0.02))} self.thread = None self.running = False self.fullscreen = False self.init_reconnect = False pass def read_mood_data(self): '''Read the mood position and color information form csv file.''' with open('moods.csv') as mf: data = mf.readlines()[1:] for line in data : l = line.split(',') mood = Mood(l[0],l[1],l[2]) self.moods.append(mood) with open('colors.txt') as ff: data = ff.readlines()[1:] data = map(lambda x: x.split(','),data) for mood in self.moods : d = cd = sys.float_info.max for colors in data : d = mood.get_distance(float(colors[0]),float(colors[1])) if d < cd : cd = d # mood.color = tuple(map(lambda x: int(pow(math.atan((float(x)/7.0)),12.5)),(colors[2],colors[3],colors[4]))) mood.color = self.set_color(tuple(map(lambda x: int(x),(colors[2],colors[3],colors[4])))) return True def set_color(self,color): '''Move to HLS colour space and manipulate saturation there.''' # TODO: ideally, we need a non-linear compressor of the lightness and saturation values r,g,b = map(lambda x: (1.0*x/255.0), color) h,l,s = cs.rgb_to_hls(r,g,b) s = 1.0 #1.0 - (1.0 / pow(50.0,s)) l = 1.0 - (1.0 / pow(20.0,l)) #0.6 r,g,b = map(lambda x: int(x*255), cs.hls_to_rgb(h,l,s)) return r,g,b def start_update_thread(self): '''Start the thread that reads data from the server.''' self.thread = Thread(target = self.update_thread) self.thread.daemon = True self.running = True self.thread.start() print "OK. Update thread started." def stop_update_thread(self): '''Stop the thread and allow some time fot the connections to close.''' self.running = False try : self.thread.join(2) except : print "No update thread to join." def update_thread(self): '''The server update thread''' print "Thread reporting..." while self.running : # self.update() try : self.update() # self.indicators["update"].visible = True except Exception, e: if str(e).strip() : print "Exception: ", str(e), type(e), len(str(e).strip()) self.indicators["conn"].off() # self.indicators["update"].visible = False time.sleep(SERVER_UPDATE_INTERVAL) def update(self): '''Update the blob list from the server. This should be in a thread.''' # indicate connection health by toggling an indictor self.indicators["update"].toggle() # delete invisibles for blob in self.blobs : if not blob.visible : self.blobs.remove(blob) # get new coordinates from the server self.conn.putrequest("GET","/moodconductor/result", skip_host=True) self.conn.putheader("Host", "www.isophonics.net") self.conn.endheaders() res = self.conn.getresponse() data = res.read() data = eval(data) if not data : self.conn.close() self.indicators["data"].toggle() return False for d in data : # coordstxt = "x:%s y:%s c:%s" %d x,y,count = d self.add_blob(x,y,count) self.indicators["receive"].toggle() self.conn.close() self.blobs = self.blobs[:NBLOBS] return True def add_blob(self,x,y,count=1): '''Insert a blob to the list of blobs''' # find mood correxponding to x,y cmood = None d = cd = sys.float_info.max for mood in self.moods : d = mood.get_distance(x,y) if d < cd : cd = d cmood = mood # create new blob or increase click count on existing one new = Blob(self.bg,x,y,mood=cmood,fade=self.FADE) if not new in self.blobs : self.blobs.insert(0,new) elif count > self.blobs[self.blobs.index(new)].count: self.blobs[self.blobs.index(new)].increment(count) pass def draw(self): self.bg.fill((0,0,0)) # self.bg.blit(gradients.radial(19, self.scol, self.ecol), (rect_x,rect_y)) l = copy.copy(self.blobs) l.reverse() for blob in l : blob.draw() # axis pg.draw.line(self.bg, (G,G,G), (int(X/2.0),0),(int(X/2.0),Y), 1) pg.draw.line(self.bg, (G,G,G), (0,int(Y/2.0)),(X,int(Y/2.0)), 1) # indicators for i in self.indicators.itervalues() : i.draw() def read_keys(self): '''Read keys''' for event in pg.event.get() : # Quit (event) if event.type == QUIT: self.quit() elif event.type == KEYDOWN : # Post Quit: Esc, q if event.key == K_ESCAPE or event.key == K_q : pg.event.post(pg.event.Event(QUIT)) # Reset: Space elif event.key == K_SPACE : self.blobs = [] # Random : r elif event.key == K_r : self.add_blob(random(),random(),count=5) # Connection : c elif event.key == K_c : self.init_reconnect = True self.indicators["conn"].off() # Fullscreen: f elif event.key == K_f : # pg.display.toggle_fullscreen() self.toggle_screen_mode() # Toggle fade: s elif event.key == K_s : if self.FADE : print "fade off" self.indicators["conn"].off() self.FADE = 0 for blob in self.blobs : blob.fade(0) else: print "fade on" self.indicators["conn"].on() self.FADE = 15 for blob in self.blobs : blob.fade(15) blob.reset_time() # inc age elif event.key == K_1 : self.s_age += 1 self.update_server_config(self.s_age,self.s_dist,self.s_ninp) # dec age elif event.key == K_2 : self.s_age -= 1 self.update_server_config(self.s_age,self.s_dist,self.s_ninp) # inc dist elif event.key == K_3 : self.s_dist += 0.025 self.update_server_config(self.s_age,self.s_dist,self.s_ninp) # dec dist elif event.key == K_4 : self.s_dist -= 0.025 if self.s_dist < 0.025 : self.s_dist = 0.025 self.update_server_config(self.s_age,self.s_dist,self.s_ninp) # inc ninp elif event.key == K_5 : self.s_ninp += 1 self.update_server_config(self.s_age,self.s_dist,self.s_ninp) # dec ninp elif event.key == K_6 : self.s_ninp -= 1 if self.s_ninp < 2 : self.s_ninp = 2 self.update_server_config(self.s_age,self.s_dist,self.s_ninp) pass pass def toggle_screen_mode(self): '''Go back and forth between full screen mode.''' if self.fullscreen == False: globals()['_X'] = globals()['X'] globals()['_Y'] = globals()['Y'] globals()['X'] = XF globals()['Y'] = YF self.screen = pg.display.set_mode((X, Y)) # self.screen = pg.display.set_mode((0,0),pg.FULLSCREEN) self.fullscreen = True self.bg = pg.Surface(self.screen.get_size()) self.bg = self.bg.convert() self.bg.fill((0,0,0)) for i in self.indicators.itervalues() : i.reinit(self.bg) i.draw() else : globals()['X'] = globals()['_X'] globals()['Y'] = globals()['_Y'] self.screen = pg.display.set_mode((X, Y)) self.fullscreen = False self.bg = pg.Surface(self.screen.get_size()) self.bg = self.bg.convert() self.bg.fill((0,0,0)) for i in self.indicators.itervalues() : i.reinit(self.bg) i.draw() pg.display.toggle_fullscreen() def run(self): # setup connection self.connect() # main loop while True : # pg.draw.circle(screen, pg.Color(255,0,0), (300,50),20,0) # screen.blit(gradients.radial(99, scol, ecol), (401, 1)) self.read_keys() # Draw self.draw() # update display self.screen.blit(self.bg, (0, 0)) pg.display.flip() self.fpsClock.tick(FRAMERATE) if self.init_reconnect: self.reconnect() return True def configure_server(self): '''Send the server some configuration data.''' # age = 10.0 # dist = DIST # ninp = 18 self.update_server_config(self.s_age,self.s_dist,self.s_ninp) def update_server_config(self,age,dist,ninp,retry = 3): '''Send the server some configuration data.''' try : print "Sending configuration data." self.conn.putrequest("GET","/moodconductor/config?age=%(age)s&dist=%(dist)s&ninp=%(ninp)s" %locals(), skip_host=True) self.conn.putheader("Host", "www.isophonics.net") self.conn.endheaders() res = self.conn.getresponse() res.read() if not res.status == 200 : print "Server response:", res.status, res.reason self.indicators["conn"].off() time.sleep(0.5) self.conn.putrequest("GET","/moodconductor/getconf", skip_host=True) self.conn.putheader("Host", "www.isophonics.net") self.conn.endheaders() res = self.conn.getresponse() if not res.status == 200 : print "Server response:", res.status, res.reason self.indicators["conn"].off() print "Server configuration:", res.read() except: print "Failed to send configuration data." time.sleep(2) retry -= 1 self.update_server_config(age,dist,ninp,retry) def connect(self,retry = 5): '''Connect to the server and test connection.''' if not retry : print "Server unreachable" pg.quit() raise SystemExit if retry < 5 : time.sleep(3) try : print "connecting to server..." self.conn = ht.HTTPConnection(IP,timeout=HTTP_TIMEOUT) self.indicators["conn"].on() except : self.indicators["conn"].off() self.connect(retry = retry-1) print "connection failed." try: print "Testing connection." # self.conn.putrequest("GET","/moodconductor/index.html", skip_host=True) self.conn.putrequest("GET","/moodconductor/test", skip_host=True) self.conn.putheader("Host", "www.isophonics.net") self.conn.endheaders() res = self.conn.getresponse() print res.read() if res.status == 200 : self.indicators["conn"].on() else : print "Server response:", res.status, res.reason self.indicators["conn"].off() if not hasattr(self,"noretry") and raw_input("Go offline? ") in ['y',''] : return False else : print "Failed. retrying." self.noretry = None self.connect(retry = retry-1) except : print "Exception while testing connection." self.indicators["conn"].off() # comment out in offline mode if not hasattr(self,"noretry") and raw_input("Go offline? ") in ['y',''] : return False else : self.noretry = None self.connect(retry = retry-1) self.configure_server() print "OK. Starting update thread..." self.start_update_thread() return True def reconnect(self): '''Called when c is pressed.''' self.init_reconnect = False self.stop_update_thread() time.sleep(1) try : self.conn.close() except : self.indicators["conn"].off() try : self.conn = ht.HTTPConnection(IP,timeout=HTTP_TIMEOUT) self.conn.connect() self.indicators["conn"].on() print "Reconnected." except : self.indicators["conn"].off() print "Error while reconnecting." self.start_update_thread() def quit(self): print "Quitting.." self.indicators["conn"].off() self.stop_update_thread() self.conn.close() pg.quit() sys.exit() def main(): v = VisualClient() v.run() if __name__ == '__main__': pass main()