Mercurial > hg > mood-conductor
changeset 30:4a7fde8ff0fd
added MIDI client made during the Barbican hack day
author | gyorgyf |
---|---|
date | Mon, 29 Apr 2013 15:35:28 +0100 |
parents | f5ca9dbabe83 |
children | 1233c13c17d9 |
files | visualclient/midiclient.py visualclient/visclient.py |
diffstat | 2 files changed, 692 insertions(+), 1 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/visualclient/midiclient.py Mon Apr 29 15:35:28 2013 +0100 @@ -0,0 +1,690 @@ +#!/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 * +import httplib as ht +from pygame import midi + +import gradients +from gradients import genericFxyGradient + +from threading import Thread +from random import random +import numpy as np + +import colorsys as cs + +NOTE_OFF_CH0 = [[[0xb0,0x7b,0],0]] + +# 1 HONKY TONK 0x1 / 0x4 (5) +# 2 SITAR 0x9 / 0x0 (1) +# 3 Melancholia +# 4 Rock GITAR 0x3 / 0x2C (45) + +IMAP = {1 : (0x1,0x4), 2: (0x9,0x0), 3:(0x8,0xb), 4:(0x3,0x2c) } + + +# from pytagcloud import create_tag_image, make_tags +# from pytagcloud.lang.counter import get_tag_counts + +# YOUR_TEXT = "A tag cloud is a visual representation for text data, typically\ +# used to depict keyword metadata on websites, or to visualize free form text." +# +# tags = make_tags(get_tag_counts(YOUR_TEXT), maxsize=120) +# +# create_tag_image(tags, 'cloud_large.png', size=(900, 600), fontname='Lobster') + +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) + + 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): + # print x,y + self.x = x + self.y = y + self.quadrant = self.get_quadrant_number(x,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 get_quadrant_number(self,x,y): + if x > 0.5 and y > 0.5 : + return 1 + if x > 0.5 and y < 0.5 : + return 2 + if x < 0.5 and y < 0.5 : + return 3 + if x < 0.5 and y > 0.5 : + return 4 + + + 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 + + pg.midi.init() + + print pg.midi.get_device_info(4) + self.midi_out = pg.midi.Output(4,0) + self.midi_out.set_instrument(6) + + # self.midi_out.note_on(23,128,1) + # self.midi_out.write([[[0xc0,0,0],20000],[[0x90,60,100],20500]]) + + self.active_quadrant = 1 + self.prev_quadrant = 1 + + 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.running = True + self.thread = Thread(target = self.update_thread) + self.thread.daemon = True + self.thread.start() + + 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''' + while self.running : + 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] + self.compute_quadrant_weighting() + self.self_change_instrument() + return True + + def compute_quadrant_weighting(self): + quadrant_dict = {1:[],2:[],3:[],4:[]} + # sort blobs into quadrants + for blob in self.blobs : + quadrant_dict[blob.quadrant].append(blob) + # get weight for each + quadrant_weights = [] + for q,blob_list in quadrant_dict.iteritems() : + quadrant_weights.append(sum(map(lambda x: x.alpha * x.size,blob_list))) + self.active_quadrant = np.argmax(quadrant_weights) + 1 + print self.active_quadrant + return self.active_quadrant + + def self_change_instrument(self): + if self.active_quadrant != self.prev_quadrant : + self.prev_quadrant = self.active_quadrant + args = IMAP[self.active_quadrant] + self.send_midi_patch_change_GR20(*args) + print args + + 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) + # self.send_midi() + elif count > self.blobs[self.blobs.index(new)].count: + self.blobs[self.blobs.index(new)].increment(count) + pass + + def send_midi(self): + # self.midi_out.write([[[0xc0,0,0],20000],[[0x90,60,100],20500]]) + self.midi_out.write([[[0x90,60,100],0],[[0x90,60,100],500]]) + + def send_midi_patch_change_GR20(self,bank,instrument): + '''PIANO = BANK 1, Patch 5.. bank starts from 1, patch starts from 0 so I have to substract one...''' + self.midi_out.write([[[0xB0,0x0,bank],0],[[0xC0,instrument],100]]) + # Control change (B) followed by patch change (C): + # midi_out.write([[[0xB0,0x0,0x9],0],[[0xC0,0x0],100]]) + # midi_out.write([[[0xB0,0x0,int(bank)],0],[[0xC0,int(instrument)-1],100]]) + # 1 HONKY TONK 0x1 / 0x4 (5) + # 2 SITAR 0x9 / 0x0 (1) + # 3 Melancholia + # 4 Rock GITAR 0x3 / 0x2C (45) + + + 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.midi_out.write(NOTE_OFF_CH0) + 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 : + 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() + 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: + 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 : + self.conn = ht.HTTPConnection(IP,timeout=HTTP_TIMEOUT) + # self.start_update_thread() + self.indicators["conn"].on() + except : + self.indicators["conn"].off() + self.connect(retry = retry-1) + + try: + self.conn.putrequest("GET","/moodconductor/index.html", skip_host=True) + self.conn.putheader("Host", "www.isophonics.net") + self.conn.endheaders() + res = self.conn.getresponse() + 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 : + 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() + self.start_update_thread() + return True + + + + def reconnect(self): + '''Called when c is pressed.''' + self.init_reconnect = False + # self.indicators["conn"].off().draw() + # self.screen.blit(self.bg, (0, 0)) + # pg.display.flip() + + 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() +
--- a/visualclient/visclient.py Thu Apr 18 16:51:36 2013 +0100 +++ b/visualclient/visclient.py Mon Apr 29 15:35:28 2013 +0100 @@ -54,7 +54,7 @@ BLOBSIZE = 25 G=110 FADE = 15 -DIST = 0.1 # blob equivalence tolerance +DIST = 0.15 # blob equivalence tolerance FRAMERATE = 60 # Connection: @@ -233,6 +233,7 @@ self.running = False self.fullscreen = False self.init_reconnect = False + pass