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()