annotate tim_grid_mapper/grid_mapper.py @ 8:6df5e29a65f9

Updated OSC spec for communication between Ableton and Joe's synth.
author Tim MB <tim.murraybrowne@eecs.qmul.ac.uk>
date Thu, 17 Feb 2011 13:57:51 +0000
parents 5f9ad838d417
children d68f98883e63
rev   line source
tim@3 1 '''
tim@3 2 grid_mapper.py - maintained by Tim.
tim@3 3
tim@3 4 This module implements a mapping from person positions (id,x,y,z) to generate
tim@3 5 pitch, velocities and channels for Joe's synthesiser and controller data for
tim@3 6 Ableton.
tim@3 7
tim@3 8 '''
tim@3 9
tim@3 10 from OSC import ThreadingOSCServer, OSCClient, OSCMessage, OSCClientError
tim@4 11 from threading import Thread
tim@4 12 from time import sleep
tim@3 13
tim@3 14 #### PUBLIC OPTIONS ####
tim@4 15 num_channels = 3 # number of instruments (and max number of people who can
tim@4 16 # make noise).
tim@3 17
tim@3 18 #### OSC OPTIONS - THESE NEED TO BE SET MANUALLY ####
tim@8 19 my_port = 12344 # to receive OSC messages
tim@4 20 joe = ('localhost', "THIS_MUST_BE_SET")
tim@4 21 ableton = ('localhost', "THIS_MUST_BE_SET")
tim@3 22
tim@3 23 ### Constants for grid mapping:
tim@3 24 # The range of values that the input coordinates and output values may take:
tim@3 25 # (ranges are inclusive)
tim@3 26 MIN = {
tim@3 27 'x' : 0.,
tim@3 28 'y' : 0.,
tim@3 29 'z' : 0.,
tim@3 30 'pitch' : 0,
tim@3 31 'cc1' : 0,
tim@3 32 'cc2' : 0,
tim@3 33 }
tim@3 34 MAX = {
tim@3 35 'x' : 1.,
tim@3 36 'y' : 1.,
tim@3 37 'z' : 1.,
tim@3 38 'pitch' : 15,
tim@3 39 'cc1' : 127,
tim@3 40 'cc2' : 127,
tim@3 41 }
tim@3 42
tim@3 43
tim@3 44
tim@3 45 #### PRIVATE VARIABLES ####
tim@4 46
tim@4 47 # mapping from channel to the currently playing note (pitch values) or None
tim@4 48 # initialize each channel to None:
tim@4 49 currently_playing = [None] * num_channels
tim@4 50
tim@4 51 # mapping from personId to time of last update
tim@4 52 last_update_times = {}
tim@3 53
tim@3 54 # OSC OBJECTS
tim@4 55 server = None # Initialised when start() is run
tim@3 56 client = OSCClient()
tim@3 57
tim@3 58
tim@4 59
tim@4 60
tim@4 61
tim@4 62 ### MAPPING
tim@4 63
tim@3 64 def send_to_joe(data, address='/test'):
tim@3 65 '''Sends `data` to Joe directly as an OSC message.
tim@3 66 '''
tim@3 67 message = OSCMesssage(address)
tim@3 68 message.extend(data)
tim@3 69 client.sendto(message, joe)
tim@4 70 print_d('==OSC Output to Joe %s:==\n %s' % (joe, data))
tim@3 71
tim@4 72 def flush(channel):
tim@4 73 '''Sends note off messages for whatever note is currently playing on
tim@4 74 `channel`.
tim@4 75 '''
tim@4 76 pitch = currently_playing[channel]
tim@4 77 if pitch:
tim@4 78 #print_d('Sending note-off for note %i on channel %i.' % (pitch, channel))
tim@4 79 send_to_joe([
tim@4 80 'Turn off note %i on channel %i' % (pitch, channel),
tim@4 81 # first string is ignored
tim@4 82 pitch, # pitch to turn off
tim@4 83 0, # 0 to turn note off
tim@4 84 127, # doesn't matter for note-off (but never send 0)
tim@4 85 channel,
tim@4 86 ])
tim@3 87
tim@3 88
tim@3 89 def person_handler(address, tags, data, client_address):
tim@3 90 ''' Handles OSC input matching the 'person' tag.
tim@3 91
tim@4 92 `data` should be in form [person_id, x, y, z]
tim@3 93 '''
tim@3 94 pitch, velocity, channel, cc1, cc2 = grid_map(data)
tim@3 95
tim@3 96 ## Format data for Joe - done using Specification.txt on 2011-02-15
tim@3 97
tim@3 98 # constrain and round off pitch and velocity
tim@3 99 pitch = max(min(round(pitch), MAX['pitch']), MIN['pitch'])
tim@4 100 velocity = max(min(round(velocity), MAX['velocity']), MIN['velocity'])
tim@3 101
tim@4 102 if velocity and pitch == currently_playing[channel]:
tim@4 103 return # keep playing current note
tim@3 104
tim@4 105 # otherwise turn note off:
tim@4 106 flush(channel)
tim@4 107
tim@4 108 if velocity: # if there is a new note to play
tim@4 109 send_to_joe([
tim@4 110 'Turn on note %i on channel %i' % (pitch, channel),
tim@4 111 # first value is string which is ignored
tim@4 112 pitch,
tim@4 113 1, # 1 to turn note on
tim@4 114 velocity,
tim@4 115 channel
tim@4 116 ])
tim@3 117
tim@3 118
tim@3 119
tim@3 120
tim@3 121 def grid_map(person_id, x, y, z):
tim@3 122 '''This function maps from a person's location to MIDI data
tim@3 123 returning a tuple (pitch, velocity, channel, cc1, cc2).
tim@3 124
tim@3 125 The current mapping creates higher pitch values as the person moves
tim@3 126 closer to the Kinect (i.e. z decreases). x and y values are mapped to cc1
tim@3 127 and cc2 (to be sent straight to Ableton and determined by a particular
tim@3 128 synth)
tim@3 129
tim@3 130 NB. channel == person_id and velocity==0 when note is off.
tim@3 131 Midi-Velocity is currently unimplemented but will use Person-velocity data
tim@3 132 when that becomes available.
tim@3 133 This function does not guarantee that the output will be in range if the
tim@3 134 input goes out of range.
tim@3 135 '''
tim@3 136 pitch = round(interpolate(z,
tim@3 137 MIN['z'], MAX['z'],
tim@3 138 MIN['pitch'], MAX['pitch'])
tim@3 139 )
tim@3 140 cc1 = round(interpolate(x,
tim@3 141 MIN['x'], MAX['x'],
tim@3 142 MIN['cc1'], MAX['cc1'])
tim@3 143 )
tim@3 144 cc2 = round(interpolate(x,
tim@3 145 MIN['y'], MAX['y'],
tim@3 146 MIN['cc2'], MAX['cc2'])
tim@3 147 )
tim@3 148 velocity = 127
tim@3 149 return (pitch, velocity, person_id, cc1, cc2)
tim@3 150
tim@3 151
tim@3 152
tim@3 153
tim@3 154 def interpolate(x, a, b, A, B):
tim@3 155 ''' Interpolates x from the range [a, b] to the range [A, B].
tim@3 156
tim@3 157
tim@3 158 Interpolation is linear. From [a,b] to [A,B] this would be:
tim@3 159 (B-A)*(x-a)/(b-a) + A
tim@3 160 '''
tim@3 161 return (B-A)*(x-a)/(b-a) + A
tim@4 162
tim@4 163
tim@4 164 def print_d(string):
tim@4 165 '''Function to print out debug method. Disable if processing power is in
tim@4 166 demand.
tim@4 167 '''
tim@4 168 print(string)
tim@4 169
tim@4 170
tim@4 171
tim@4 172
tim@4 173
tim@4 174
tim@4 175 #### CONTROL
tim@4 176
tim@4 177 def start():
tim@4 178 '''Set up OSC servers and start the program running.
tim@4 179 '''
tim@4 180 global joe, ableton, server
tim@4 181 if joe[1] == "THIS_MUST_BE_SET":
tim@4 182 joe_port = input("Enter port number on %s for Joe's synth software: " % joe[0])
tim@4 183 joe = (joe[0], joe_port)
tim@4 184
tim@4 185 if ableton[1] == "THIS_MUST_BE_SET":
tim@4 186 ableton_port = input("Enter port number on %s for Ableton: " % ableton[0])
tim@4 187 ableton = (ableton[0], ableton_port)
tim@4 188
tim@4 189 server = ThreadingOSCServer(('localhost', my_port))
tim@4 190 # Register OSC callbacks:
tim@4 191 server.addMsgHandler('/person', person_handler)
tim@4 192 t = Thread(target=server.serve_forever)
tim@4 193 t.start()
tim@4 194 if server.running and t.is_alive():
tim@4 195 print('OSC Server running on port %i.' % my_port)
tim@4 196 print("Use 'stop()' to close it.")
tim@4 197 else:
tim@4 198 print('Error: Either server thread died or server is not reported as running.')
tim@4 199
tim@4 200
tim@4 201 def stop():
tim@4 202 '''Close the OSC server.
tim@4 203 '''
tim@4 204 if server:
tim@4 205 server.close()
tim@4 206 sleep(0.3)
tim@4 207 if not server.running:
tim@4 208 print("\n\nSuccessfully closed the OSC server. Ignore a 'Bad file descriptor' Exception - there's nothing I can do about that.")
tim@4 209 print("Type 'quit()' to exit.")
tim@4 210 else:
tim@4 211 print('Error: server has been told to close but is still running.')
tim@4 212
tim@4 213 if __name__=='__main__':
tim@4 214 start()
tim@4 215 while True:
tim@4 216 try:
tim@4 217 print(repr(input()))
tim@4 218 except Exception as e:
tim@4 219 print('Caught %s:' % repr(e))
tim@4 220 exception = e
tim@4 221 trace = traceback.format_exc()
tim@4 222 print('Exception saved as `exception`. Stack trace saved as `trace`.')