andrew@50: #! /usr/bin/python andrew@50: ''' andrew@50: grid_mapper.py - maintained by Tim. andrew@50: andrew@50: This module implements a mapping from person positions (id,x,y,z) to generate andrew@50: pitch, velocities and channels for Joe's synthesiser and controller data for andrew@50: Ableton. andrew@50: andrew@50: ''' andrew@50: andrew@50: from OSC import ThreadingOSCServer, OSCClient, OSCMessage, OSCClientError andrew@50: from threading import Thread andrew@50: from time import sleep andrew@50: import pdb andrew@50: andrew@50: #### PUBLIC OPTIONS #### andrew@50: num_channels = 3 # number of instruments (and max number of people who can andrew@50: # make noise). andrew@50: andrew@50: #### OSC OPTIONS - THESE NEED TO BE SET MANUALLY #### andrew@50: my_port = 12344 # to receive OSC messages andrew@50: joe = ('localhost', 12345) andrew@50: ableton = ('localhost', 12346) andrew@50: andrew@50: ### Constants for grid mapping: andrew@50: # The range of values that the input coordinates and output values may take: andrew@50: # (ranges are inclusive) andrew@50: MIN = { andrew@50: 'x' : 0., andrew@50: 'y' : 0., andrew@50: 'z' : 0., andrew@50: 'pitch' : 0, andrew@50: 'cc1' : 0, andrew@50: 'cc2' : 0, andrew@50: 'velocity' : 0, andrew@50: } andrew@50: MAX = { andrew@50: 'x' : 1., andrew@50: 'y' : 1., andrew@50: 'z' : 1., andrew@50: 'pitch' : 15, andrew@50: 'cc1' : 127, andrew@50: 'cc2' : 127, andrew@50: 'velocity' : 127, andrew@50: } andrew@50: andrew@50: andrew@50: andrew@50: #### PRIVATE VARIABLES #### andrew@50: andrew@50: # mapping from channel to the currently playing note (pitch values) or None andrew@50: # initialize each channel to None: andrew@50: currently_playing = [None] * num_channels andrew@50: andrew@50: # mapping from personId to time of last update andrew@50: # ((not yet implemented)) andrew@50: #last_update_times = {} andrew@50: andrew@50: # mapping from (channel, CC_number) to last CC value sent: andrew@50: last_value_sent = { andrew@50: (1, 1) : 0, andrew@50: (1, 2) : 0, andrew@50: (2, 1) : 0, andrew@50: (2, 2) : 0, andrew@50: (3, 1) : 0, andrew@50: (3, 2) : 0, andrew@50: } andrew@50: andrew@50: andrew@50: # OSC OBJECTS andrew@50: server = None # Initialised when start() is run andrew@50: client = OSCClient() andrew@50: andrew@50: andrew@50: andrew@50: andrew@50: andrew@50: ### MAPPING andrew@50: andrew@50: def send_to_joe(data, address='/test'): andrew@50: '''Sends `data` to Joe directly as an OSC message. andrew@50: ''' andrew@50: message = OSCMessage(address) andrew@50: message.extend(data) andrew@50: client.sendto(message, joe) andrew@50: print_d('\n==OSC Output "%s" to Joe %s:==\n %s' % (address, joe, data)) andrew@50: andrew@50: andrew@50: def send_to_ableton(data, address='/cc'): andrew@50: '''Sends `data` to Ableton (via Max) as an OSC message. andrew@50: ''' andrew@50: #pdb.set_trace() andrew@50: message = OSCMessage(address) andrew@50: message.extend(data) andrew@50: client.sendto(message, ableton) andrew@50: print('\n==OSC Output "%s" to Ableton %s:==\n %s' % (address, ableton, data)) andrew@50: andrew@50: andrew@50: andrew@50: def flush(channel): andrew@50: '''Sends note off messages for whatever note is currently playing on andrew@50: `channel`. andrew@50: ''' andrew@50: pitch = currently_playing[channel] andrew@50: if pitch: andrew@50: #print_d('Sending note-off for note %i on channel %i.' % (pitch, channel)) andrew@50: send_to_joe([ andrew@50: 'Turn off note %i on channel %i' % (pitch, channel), andrew@50: # first string is ignored andrew@50: int(pitch), # pitch to turn off andrew@50: 0, # 0 to turn note off andrew@50: 127, # doesn't matter for note-off (but never send 0) andrew@50: int(channel), andrew@50: ]) andrew@50: currently_playing[channel] = None andrew@50: andrew@50: andrew@50: def person_handler(address, tags, data, client_address): andrew@50: ''' Handles OSC input matching the 'person' tag. andrew@50: andrew@50: `data` should be in form [person_id, x, y, z] andrew@50: ''' andrew@50: pitch, velocity, channel, cc1, cc2 = grid_map(*data) andrew@50: andrew@50: cc1, cc2 = int(round(cc1)), int(round(cc2)) andrew@50: if cc1 != last_value_sent[(channel, 1)]: andrew@50: send_to_ableton([channel, 1, cc1], '/cc') andrew@50: last_value_sent[(channel, 1)] = cc1 andrew@50: if cc2 != last_value_sent[(channel, 2)]: andrew@50: send_to_ableton([channel, 2, cc2], '/cc') andrew@50: last_value_sent[(channel, 2)] = cc2 andrew@50: andrew@50: andrew@50: ## Format data for Joe - done using Specification.txt on 2011-02-15 andrew@50: andrew@50: # constrain and round off pitch and velocity andrew@50: pitch = max(min(round(pitch), MAX['pitch']), MIN['pitch']) andrew@50: velocity = max(min(round(velocity), MAX['velocity']), MIN['velocity']) andrew@50: andrew@50: if velocity and pitch == currently_playing[channel]: andrew@50: return # keep playing current note andrew@50: andrew@50: # otherwise turn note off: andrew@50: flush(channel) andrew@50: andrew@50: if velocity: # if there is a new note to play andrew@50: send_to_joe([ andrew@50: 'Turn on note %i on channel %i' % (pitch, channel), andrew@50: # first value is string which is ignored andrew@50: int(pitch), andrew@50: 1, # 1 to turn note on andrew@50: int(velocity), andrew@50: int(channel) andrew@50: ]) andrew@50: currently_playing[channel] = pitch andrew@50: andrew@50: andrew@50: andrew@50: andrew@50: andrew@50: def grid_map(person_id, x, y, z): andrew@50: '''This function maps from a person's location to MIDI data andrew@50: returning a tuple (pitch, velocity, channel, cc1, cc2). andrew@50: andrew@50: The current mapping creates higher pitch values as the person moves andrew@50: closer to the Kinect (i.e. z decreases). x and y values are mapped to cc1 andrew@50: and cc2 (to be sent straight to Ableton and determined by a particular andrew@50: synth) andrew@50: andrew@50: NB. channel == person_id and velocity==0 when note is off. andrew@50: Midi-Velocity is currently unimplemented but will use Person-velocity data andrew@50: when that becomes available. andrew@50: This function does not guarantee that the output will be in range if the andrew@50: input goes out of range. andrew@50: ''' andrew@50: pitch = round(interpolate(z, andrew@50: MIN['z'], MAX['z'], andrew@50: MIN['pitch'], MAX['pitch']) andrew@50: ) andrew@50: cc1 = round(interpolate(x, andrew@50: MIN['x'], MAX['x'], andrew@50: MIN['cc1'], MAX['cc1']) andrew@50: ) andrew@50: cc2 = round(interpolate(y, andrew@50: MIN['y'], MAX['y'], andrew@50: MIN['cc2'], MAX['cc2']) andrew@50: ) andrew@50: velocity = 127 andrew@50: return (pitch, velocity, person_id, cc1, cc2) andrew@50: andrew@50: andrew@50: andrew@50: andrew@50: def interpolate(x, a, b, A, B): andrew@50: ''' Interpolates x from the range [a, b] to the range [A, B]. andrew@50: andrew@50: andrew@50: Interpolation is linear. From [a,b] to [A,B] this would be: andrew@50: (B-A)*(x-a)/(b-a) + A andrew@50: ''' andrew@50: return (B-A)*(x-a)/(b-a) + A andrew@50: andrew@50: andrew@50: def print_d(string): andrew@50: '''Function to print out debug method. Disable if processing power is in andrew@50: demand. andrew@50: ''' andrew@50: print(string) andrew@50: andrew@50: andrew@50: andrew@50: andrew@50: andrew@50: andrew@50: #### CONTROL andrew@50: andrew@50: def start(): andrew@50: '''Set up OSC servers and start the program running. andrew@50: ''' andrew@50: global joe, ableton, server andrew@50: if joe[1] == "THIS_MUST_BE_SET": andrew@50: joe_port = input("Enter port number on %s for Joe's synth software: " % joe[0]) andrew@50: joe = (joe[0], joe_port) andrew@50: andrew@50: if ableton[1] == "THIS_MUST_BE_SET": andrew@50: ableton_port = input("Enter port number on %s for Ableton: " % ableton[0]) andrew@50: ableton = (ableton[0], ableton_port) andrew@50: andrew@50: server = ThreadingOSCServer(('localhost', my_port)) andrew@50: # Register OSC callbacks: andrew@50: server.addMsgHandler('/person', person_handler) andrew@50: t = Thread(target=server.serve_forever) andrew@50: t.start() andrew@50: if server.running and t.is_alive(): andrew@50: print('OSC Server running on port %i.' % my_port) andrew@50: print("Use 'stop()' to close it.") andrew@50: else: andrew@50: print('Error: Either server thread died or server is not reported as running.') andrew@50: andrew@50: andrew@50: def stop(): andrew@50: '''Close the OSC server. andrew@50: ''' andrew@50: if server: andrew@50: server.close() andrew@50: sleep(0.3) andrew@50: if not server.running: andrew@50: print("\n\nSuccessfully closed the OSC server. Ignore a 'Bad file descriptor' Exception - there's nothing I can do about that.") andrew@50: print("Type 'quit()' to exit.") andrew@50: else: andrew@50: print('Error: server has been told to close but is still running.') andrew@50: andrew@50: if __name__=='__main__': andrew@50: start() andrew@50: while True: andrew@50: try: andrew@50: print(repr(input())) andrew@50: except Exception as e: andrew@50: print('Caught %s:' % repr(e)) andrew@50: exception = e andrew@50: trace = traceback.format_exc() andrew@50: print('Exception saved as `exception`. Stack trace saved as `trace`.')