tim@3: ''' 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@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@8: my_port = 12344 # to receive OSC messages tim@4: joe = ('localhost', "THIS_MUST_BE_SET") tim@4: ableton = ('localhost', "THIS_MUST_BE_SET") 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@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@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@4: last_update_times = {} 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@3: message = OSCMesssage(address) tim@3: message.extend(data) tim@3: client.sendto(message, joe) tim@4: print_d('==OSC Output to Joe %s:==\n %s' % (joe, data)) 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@4: pitch, # pitch to turn off tim@4: 0, # 0 to turn note off tim@4: 127, # doesn't matter for note-off (but never send 0) tim@4: channel, tim@4: ]) tim@3: tim@3: 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@3: pitch, velocity, channel, cc1, cc2 = grid_map(data) 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@4: pitch, tim@4: 1, # 1 to turn note on tim@4: velocity, tim@4: channel tim@4: ]) tim@3: 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@3: cc2 = round(interpolate(x, 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@4: global joe, ableton, server tim@4: if joe[1] == "THIS_MUST_BE_SET": tim@4: joe_port = input("Enter port number on %s for Joe's synth software: " % joe[0]) tim@4: joe = (joe[0], joe_port) tim@4: tim@4: if ableton[1] == "THIS_MUST_BE_SET": tim@4: ableton_port = input("Enter port number on %s for Ableton: " % ableton[0]) tim@4: ableton = (ableton[0], ableton_port) tim@4: tim@4: server = ThreadingOSCServer(('localhost', my_port)) tim@4: # Register OSC callbacks: tim@4: server.addMsgHandler('/person', person_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@4: if __name__=='__main__': tim@4: start() tim@4: while True: tim@4: try: tim@4: print(repr(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`.')