view tim_grid_mapper/grid_mapper.py @ 50:f4c6999ecfe9 tip

added the files on my computer that aren't aiff s> these shoudl be everything for the big bang fair 2011 - heresy, and tim's file's also here
author Andrew N Robertson <andrew.robertson@eecs.qmul.ac.uk>
date Sat, 08 Oct 2011 22:12:49 +0100
parents 5b6f63b6f76d
children
line wrap: on
line source
#! /usr/bin/python
'''
grid_mapper.py - maintained by Tim.

This module implements a mapping from person positions (id,x,y,z) to generate 
pitch, velocities and channels for Joe's synthesiser and controller data for
Ableton.

'''

from OSC import ThreadingOSCServer, OSCClient, OSCMessage, OSCClientError
from threading import Thread
from time import sleep
import pdb

#### PUBLIC OPTIONS ####
num_channels = 3 # number of instruments (and max number of people who can 
                 # make noise).

#### OSC OPTIONS - THESE NEED TO BE SET MANUALLY ####
my_port = 12343 # to receive OSC messages from kinect

debugging = True # display incoming packets

# Both joe and ableton ip addresses are now read from the file ip_addresses.txt
# which is written to by the set_ip_addresses() function.

#joe     = ('localhost', 12346)#changed by andrew - python sending to joe 
#ableton = ('localhost', 12345)#changed by andrew - max receiving from Joe
ip_address_file = 'ip_addresses.txt'
ableton = None ### THESE WILL BE SET AUTOMATICALLY BELOW
joe = None     ### DO NOT CHANGE THEM HERE

### Constants for grid mapping:
# The range of values that the input coordinates and output values may take:
# (ranges are inclusive)
MIN = {
	'x'		: 0.,
	'y'		: 0.,
	'z'		: 0.,
	'pitch'	: 0,
	'cc1'   : 0,
	'cc2'   : 0,
	'velocity' : 0,
}
MAX = {
	'x'     : 1.,
	'y'     : 1.,
	'z'     : 1.,
	'pitch' : 15,
	'cc1'   : 127,
	'cc2'   : 127,
	'velocity' : 127,
}



#### PRIVATE VARIABLES ####

# mapping from channel to the currently playing note (pitch values) or None
# initialize each channel to None:
currently_playing = [None] * num_channels

# mapping from personId to time of last update
# ((not yet implemented))
#last_update_times = {}

# mapping from (channel, CC_number) to last CC value sent:
last_value_sent = {
	(1, 1) : 0,
	(1, 2) : 0,
	(2, 1) : 0,
	(2, 2) : 0,
	(3, 1) : 0,
	(3, 2) : 0,
}
	

# OSC OBJECTS
server = None # Initialised when start() is run
client = OSCClient()





### MAPPING

def send_to_joe(data, address='/test'):
	'''Sends `data` to Joe directly as an OSC message.
	'''
	message = OSCMessage(address)
	message.extend(data)
	client.sendto(message, joe)
	print_d('\n==OSC Output "%s" to Joe %s:==\n    %s' % (address, joe, data))


def send_to_ableton(data, address='/cc'):
	'''Sends `data` to Ableton (via Max) as an OSC message.
	'''
	#pdb.set_trace()
	message = OSCMessage(address)
	message.extend(data)
	client.sendto(message, ableton)
#	print('\n==OSC Output "%s" to Ableton %s:==\n    %s' % (address, ableton, data))
	


def flush(channel):
	'''Sends note off messages for whatever note is currently playing on 
	`channel`.
	'''
	pitch = currently_playing[channel]
	if pitch:
		#print_d('Sending note-off for note %i on channel %i.' % (pitch, channel))
		send_to_joe([
			'Turn off note %i on channel %i' % (pitch, channel), 
			    # first string is ignored
			int(pitch),       # pitch to turn off
			0,                # 0 to turn note off
			127,              # doesn't matter for note-off (but never send 0)
			int(channel),
		])
	currently_playing[channel] = None


def stopperson_handler(address, tags, data, clinet_address):
	''' Handles OSC input matching 'stopperson'
	
	Called when a person leaves the screen.
	'''
	flush(data[0])

def person_handler(address, tags, data, client_address):
	''' Handles OSC input matching the 'person' tag.
	
	`data` should be in form [person_id, x, y, z]
	'''
	if debugging:
		print('Received packet:')
		print(str(data))
	pitch, velocity, channel, cc1, cc2 = grid_map(*data)
	channel = channel + 1 
	cc1, cc2 = int(round(cc1)), int(round(cc2))
	if cc1 != last_value_sent[(channel, 1)]:
		send_to_ableton([channel, 1, cc1], '/cc')
		last_value_sent[(channel, 1)] = cc1
	if cc2 != last_value_sent[(channel, 2)]:
		send_to_ableton([channel, 2, cc2], '/cc')
		last_value_sent[(channel, 2)] = cc2		
	
	
	## Format data for Joe - done using Specification.txt on 2011-02-15
	
	# constrain and round off pitch and velocity
	pitch = max(min(round(pitch), MAX['pitch']), MIN['pitch'])
	velocity = max(min(round(velocity), MAX['velocity']), MIN['velocity'])
	
	if velocity and pitch == currently_playing[channel]:
		return # keep playing current note
	
	# otherwise turn note off:
	flush(channel)
	
	if velocity: # if there is a new note to play
		send_to_joe([
			'Turn on note %i on channel %i' % (pitch, channel),
				# first value is string which is ignored
			int(pitch),    
			1,               # 1 to turn note on
			int(velocity), 
			int(channel)
		])
		currently_playing[channel] = pitch





def grid_map(person_id, x, y, z):
	'''This function maps from a person's location to MIDI data
	returning a tuple (pitch, velocity, channel, cc1, cc2).
	
	The current mapping creates higher pitch values as the person moves
	closer to the Kinect (i.e. z decreases). x and y values are mapped to cc1 
	and cc2 (to be sent straight to Ableton and determined by a particular
	synth)
	
	NB. channel == person_id and velocity==0 when note is off.
	Midi-Velocity is currently unimplemented but will use Person-velocity data
	when that becomes available.
	This function does not guarantee that the output will be in range if the 
	input goes out of range.
	'''
	pitch = round(interpolate(z, 
	                          MIN['z'],     MAX['z'],
	                          MIN['pitch'], MAX['pitch'])
	        )
	cc1 = round(interpolate(x, 
	                          MIN['x'],     MAX['x'],
	                          MIN['cc1'], MAX['cc1'])
	        )
	cc2 = round(interpolate(y, 
	                          MIN['y'],     MAX['y'],
	                          MIN['cc2'], MAX['cc2'])
	        )
	velocity = 127
	return (pitch, velocity, person_id, cc1, cc2)
	



def interpolate(x, a, b, A, B):
	''' Interpolates x from the range [a, b] to the range [A, B].
	
	
	Interpolation is linear. From [a,b] to [A,B] this would be:
	(B-A)*(x-a)/(b-a) + A
	'''
	return (B-A)*(x-a)/(b-a) + A


def print_d(string):
	'''Function to print out debug method. Disable if processing power is in
	demand.
	'''
	print(string)






#### CONTROL

def start():
	'''Set up OSC servers and start the program running.
	'''
	global server
	set_ip_addresses()
	server = ThreadingOSCServer(('localhost', my_port))
	# Register OSC callbacks:
	server.addMsgHandler('/person', person_handler)
	server.addMsgHandler('/stopperson', stopperson_handler)
	t = Thread(target=server.serve_forever)
	t.start()
	if server.running and t.is_alive():
		print('OSC Server running on port %i.' % my_port)
		print("Use 'stop()' to close it.")
	else:
		print('Error: Either server thread died or server is not reported as running.')


def stop():
	'''Close the OSC server.
	'''
	if server:
		server.close()
		sleep(0.3)
		if not server.running:
			print("\n\nSuccessfully closed the OSC server. Ignore a 'Bad file descriptor' Exception - there's nothing I can do about that.")
			print("Type 'quit()' to exit.")
		else:
			print('Error: server has been told to close but is still running.')


def get_ip_addresses_from_file(filename=ip_address_file):
	'''Load up ip addresses from a file in the format saved by the 
	set_ip_addresses function.
	'''
	f = open(filename, 'r')
	lines = f.readlines()
	global ableton, joe
	ableton = eval(lines[1].split('#')[0].strip()) # set ableton
	joe = eval(lines[2].split('#')[0].strip()) # set joe
	if not ableton:
		print('Failed to set IP for Ableton from file.')
		
	if not joe:
		print('Failed to set IP for Joe from file.')
	f.close()
	


def set_ip_addresses(save_file=ip_address_file):
	'''Manually set IP addresses based on input from user. IPs are then saved to
	a file.
	'''
	global joe, ableton
	if not joe: joe = ('localhost', 12346)
	if not ableton: ableton = ('localhost', 12345)
	# set joe
	try:
		new_joe = eval(raw_input("Enter IP address and port for Joe in form: "+str(joe)+' or leave blank to keep that address.\n'))
	except:
		new_joe = None
	if new_joe: joe = new_joe
	print("Address for Joe: "+str(joe)+"\n")
	# set ableton
	try:
		new_ableton = eval(raw_input('Enter IP address and port for Ableton in form: '+str(ableton)+' or leave blank to keep that address.\n'))
	except:
		new_ableton = None
	if new_ableton: ableton = new_ableton
	print('Address for Ableton: '+str(ableton)+'\n')
	# save to file
	if new_ableton or new_joe:
		print('Writing new addresses to file %s.' % save_file)
		try:
			f = open(save_file, 'w')
			f.write(
'''# This is a cache. These addresses will be overridden by Python if you set something different. Do not leave comments here they will be overridden
%s # set Ableton IP and port
%s     # set Joe's processing IP and port
''' % (str(ableton),str(joe))
			)
			f.flush()
			f.close()
		except IOError:
			print('SAVE FAILED.')
			




get_ip_addresses_from_file(ip_address_file)


if __name__=='__main__':
	start()
	while True:
		try:
			print(eval(input()))
		except Exception as e:
			print('Caught %s:' % repr(e))
			exception = e
			trace = traceback.format_exc()
			print('Exception saved as `exception`. Stack trace saved as `trace`.')