tim@30: #! /usr/bin/python tim@30: ''' tim@3: grid_mapper.py - maintained by Tim. tim@3: tim@3: This module implements a mapping from person positions (id,x,y,z) to generate tim@3: pitch, velocities and channels for Joe's synthesiser and controller data for tim@3: Ableton. tim@3: tim@3: ''' tim@3: tim@3: from OSC import ThreadingOSCServer, OSCClient, OSCMessage, OSCClientError tim@4: from threading import Thread tim@4: from time import sleep tim@30: import pdb tim@3: tim@3: #### PUBLIC OPTIONS #### tim@4: num_channels = 3 # number of instruments (and max number of people who can tim@4: # make noise). tim@3: tim@3: #### OSC OPTIONS - THESE NEED TO BE SET MANUALLY #### tim@41: my_port = 12343 # to receive OSC messages from kinect tim@41: tim@41: debugging = True # display incoming packets tim@40: tim@40: # Both joe and ableton ip addresses are now read from the file ip_addresses.txt tim@40: # which is written to by the set_ip_addresses() function. tim@40: andrew@38: #joe = ('localhost', 12346)#changed by andrew - python sending to joe tim@40: #ableton = ('localhost', 12345)#changed by andrew - max receiving from Joe tim@40: ip_address_file = 'ip_addresses.txt' tim@40: ableton = None ### THESE WILL BE SET AUTOMATICALLY BELOW tim@40: joe = None ### DO NOT CHANGE THEM HERE tim@3: tim@3: ### Constants for grid mapping: tim@3: # The range of values that the input coordinates and output values may take: tim@3: # (ranges are inclusive) tim@3: MIN = { tim@3: 'x' : 0., tim@3: 'y' : 0., tim@3: 'z' : 0., tim@3: 'pitch' : 0, tim@3: 'cc1' : 0, tim@3: 'cc2' : 0, tim@22: 'velocity' : 0, tim@3: } tim@3: MAX = { tim@3: 'x' : 1., tim@3: 'y' : 1., tim@3: 'z' : 1., tim@3: 'pitch' : 15, tim@3: 'cc1' : 127, tim@3: 'cc2' : 127, tim@22: 'velocity' : 127, tim@3: } tim@3: tim@3: tim@3: tim@3: #### PRIVATE VARIABLES #### tim@4: tim@4: # mapping from channel to the currently playing note (pitch values) or None tim@4: # initialize each channel to None: tim@4: currently_playing = [None] * num_channels tim@4: tim@4: # mapping from personId to time of last update tim@30: # ((not yet implemented)) tim@30: #last_update_times = {} tim@30: tim@30: # mapping from (channel, CC_number) to last CC value sent: tim@30: last_value_sent = { tim@30: (1, 1) : 0, tim@30: (1, 2) : 0, tim@30: (2, 1) : 0, tim@30: (2, 2) : 0, tim@30: (3, 1) : 0, tim@30: (3, 2) : 0, tim@30: } tim@30: tim@3: tim@3: # OSC OBJECTS tim@4: server = None # Initialised when start() is run tim@3: client = OSCClient() tim@3: tim@3: tim@4: tim@4: tim@4: tim@4: ### MAPPING tim@4: tim@3: def send_to_joe(data, address='/test'): tim@3: '''Sends `data` to Joe directly as an OSC message. tim@3: ''' tim@22: message = OSCMessage(address) tim@3: message.extend(data) tim@3: client.sendto(message, joe) tim@30: print_d('\n==OSC Output "%s" to Joe %s:==\n %s' % (address, joe, data)) tim@30: tim@30: tim@30: def send_to_ableton(data, address='/cc'): tim@30: '''Sends `data` to Ableton (via Max) as an OSC message. tim@30: ''' tim@30: #pdb.set_trace() tim@30: message = OSCMessage(address) tim@30: message.extend(data) tim@30: client.sendto(message, ableton) andrew@38: # print('\n==OSC Output "%s" to Ableton %s:==\n %s' % (address, ableton, data)) tim@30: tim@30: tim@3: tim@4: def flush(channel): tim@4: '''Sends note off messages for whatever note is currently playing on tim@4: `channel`. tim@4: ''' tim@4: pitch = currently_playing[channel] tim@4: if pitch: tim@4: #print_d('Sending note-off for note %i on channel %i.' % (pitch, channel)) tim@4: send_to_joe([ tim@4: 'Turn off note %i on channel %i' % (pitch, channel), tim@4: # first string is ignored tim@22: int(pitch), # pitch to turn off tim@30: 0, # 0 to turn note off tim@30: 127, # doesn't matter for note-off (but never send 0) tim@22: int(channel), tim@4: ]) tim@30: currently_playing[channel] = None tim@3: tim@3: tim@41: def stopperson_handler(address, tags, data, clinet_address): tim@41: ''' Handles OSC input matching 'stopperson' tim@41: tim@41: Called when a person leaves the screen. tim@41: ''' tim@41: flush(data[0]) tim@41: tim@3: def person_handler(address, tags, data, client_address): tim@3: ''' Handles OSC input matching the 'person' tag. tim@3: tim@4: `data` should be in form [person_id, x, y, z] tim@3: ''' tim@41: if debugging: tim@41: print('Received packet:') tim@41: print(str(data)) tim@22: pitch, velocity, channel, cc1, cc2 = grid_map(*data) andrew@38: channel = channel + 1 tim@30: cc1, cc2 = int(round(cc1)), int(round(cc2)) tim@30: if cc1 != last_value_sent[(channel, 1)]: tim@30: send_to_ableton([channel, 1, cc1], '/cc') tim@30: last_value_sent[(channel, 1)] = cc1 tim@30: if cc2 != last_value_sent[(channel, 2)]: tim@30: send_to_ableton([channel, 2, cc2], '/cc') tim@30: last_value_sent[(channel, 2)] = cc2 tim@41: tim@3: tim@3: ## Format data for Joe - done using Specification.txt on 2011-02-15 tim@3: tim@3: # constrain and round off pitch and velocity tim@3: pitch = max(min(round(pitch), MAX['pitch']), MIN['pitch']) tim@4: velocity = max(min(round(velocity), MAX['velocity']), MIN['velocity']) tim@3: tim@4: if velocity and pitch == currently_playing[channel]: tim@4: return # keep playing current note tim@3: tim@4: # otherwise turn note off: tim@4: flush(channel) tim@4: tim@4: if velocity: # if there is a new note to play tim@4: send_to_joe([ tim@4: 'Turn on note %i on channel %i' % (pitch, channel), tim@4: # first value is string which is ignored tim@22: int(pitch), tim@30: 1, # 1 to turn note on tim@22: int(velocity), tim@22: int(channel) tim@4: ]) tim@30: currently_playing[channel] = pitch tim@30: tim@30: tim@3: tim@3: tim@3: tim@3: def grid_map(person_id, x, y, z): tim@3: '''This function maps from a person's location to MIDI data tim@3: returning a tuple (pitch, velocity, channel, cc1, cc2). tim@3: tim@3: The current mapping creates higher pitch values as the person moves tim@3: closer to the Kinect (i.e. z decreases). x and y values are mapped to cc1 tim@3: and cc2 (to be sent straight to Ableton and determined by a particular tim@3: synth) tim@3: tim@3: NB. channel == person_id and velocity==0 when note is off. tim@3: Midi-Velocity is currently unimplemented but will use Person-velocity data tim@3: when that becomes available. tim@3: This function does not guarantee that the output will be in range if the tim@3: input goes out of range. tim@3: ''' tim@3: pitch = round(interpolate(z, tim@3: MIN['z'], MAX['z'], tim@3: MIN['pitch'], MAX['pitch']) tim@3: ) tim@3: cc1 = round(interpolate(x, tim@3: MIN['x'], MAX['x'], tim@3: MIN['cc1'], MAX['cc1']) tim@3: ) tim@30: cc2 = round(interpolate(y, tim@3: MIN['y'], MAX['y'], tim@3: MIN['cc2'], MAX['cc2']) tim@3: ) tim@3: velocity = 127 tim@3: return (pitch, velocity, person_id, cc1, cc2) tim@3: tim@3: tim@3: tim@3: tim@3: def interpolate(x, a, b, A, B): tim@3: ''' Interpolates x from the range [a, b] to the range [A, B]. tim@3: tim@3: tim@3: Interpolation is linear. From [a,b] to [A,B] this would be: tim@3: (B-A)*(x-a)/(b-a) + A tim@3: ''' tim@3: return (B-A)*(x-a)/(b-a) + A tim@4: tim@4: tim@4: def print_d(string): tim@4: '''Function to print out debug method. Disable if processing power is in tim@4: demand. tim@4: ''' tim@4: print(string) tim@4: tim@4: tim@4: tim@4: tim@4: tim@4: tim@4: #### CONTROL tim@4: tim@4: def start(): tim@4: '''Set up OSC servers and start the program running. tim@4: ''' tim@40: global server tim@40: set_ip_addresses() tim@4: server = ThreadingOSCServer(('localhost', my_port)) tim@4: # Register OSC callbacks: tim@4: server.addMsgHandler('/person', person_handler) tim@41: server.addMsgHandler('/stopperson', stopperson_handler) tim@4: t = Thread(target=server.serve_forever) tim@4: t.start() tim@4: if server.running and t.is_alive(): tim@4: print('OSC Server running on port %i.' % my_port) tim@4: print("Use 'stop()' to close it.") tim@4: else: tim@4: print('Error: Either server thread died or server is not reported as running.') tim@4: tim@4: tim@4: def stop(): tim@4: '''Close the OSC server. tim@4: ''' tim@4: if server: tim@4: server.close() tim@4: sleep(0.3) tim@4: if not server.running: tim@4: print("\n\nSuccessfully closed the OSC server. Ignore a 'Bad file descriptor' Exception - there's nothing I can do about that.") tim@4: print("Type 'quit()' to exit.") tim@4: else: tim@4: print('Error: server has been told to close but is still running.') tim@4: tim@40: tim@40: def get_ip_addresses_from_file(filename=ip_address_file): tim@40: '''Load up ip addresses from a file in the format saved by the tim@40: set_ip_addresses function. tim@40: ''' tim@40: f = open(filename, 'r') tim@40: lines = f.readlines() tim@40: global ableton, joe tim@40: ableton = eval(lines[1].split('#')[0].strip()) # set ableton tim@40: joe = eval(lines[2].split('#')[0].strip()) # set joe tim@40: if not ableton: tim@40: print('Failed to set IP for Ableton from file.') tim@40: tim@40: if not joe: tim@40: print('Failed to set IP for Joe from file.') tim@40: f.close() tim@40: tim@40: tim@40: tim@40: def set_ip_addresses(save_file=ip_address_file): tim@40: '''Manually set IP addresses based on input from user. IPs are then saved to tim@40: a file. tim@40: ''' tim@40: global joe, ableton tim@40: if not joe: joe = ('localhost', 12346) tim@40: if not ableton: ableton = ('localhost', 12345) tim@40: # set joe tim@40: try: tim@40: new_joe = eval(raw_input("Enter IP address and port for Joe in form: "+str(joe)+' or leave blank to keep that address.\n')) tim@40: except: tim@40: new_joe = None tim@40: if new_joe: joe = new_joe tim@40: print("Address for Joe: "+str(joe)+"\n") tim@40: # set ableton tim@40: try: tim@40: new_ableton = eval(raw_input('Enter IP address and port for Ableton in form: '+str(ableton)+' or leave blank to keep that address.\n')) tim@40: except: tim@40: new_ableton = None tim@40: if new_ableton: ableton = new_ableton tim@40: print('Address for Ableton: '+str(ableton)+'\n') tim@40: # save to file tim@40: if new_ableton or new_joe: tim@40: print('Writing new addresses to file %s.' % save_file) tim@40: try: tim@40: f = open(save_file, 'w') tim@40: f.write( tim@40: '''# 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 tim@41: %s # set Ableton IP and port tim@41: %s # set Joe's processing IP and port tim@40: ''' % (str(ableton),str(joe)) tim@40: ) tim@40: f.flush() tim@40: f.close() tim@40: except IOError: tim@40: print('SAVE FAILED.') tim@40: tim@40: tim@40: tim@40: tim@40: tim@40: get_ip_addresses_from_file(ip_address_file) tim@40: tim@40: tim@4: if __name__=='__main__': tim@4: start() tim@4: while True: tim@4: try: tim@41: print(eval(input())) tim@4: except Exception as e: tim@4: print('Caught %s:' % repr(e)) tim@4: exception = e tim@4: trace = traceback.format_exc() tim@4: print('Exception saved as `exception`. Stack trace saved as `trace`.') tim@40: