tim@30
|
1 #! /usr/bin/python
|
tim@30
|
2 '''
|
tim@3
|
3 grid_mapper.py - maintained by Tim.
|
tim@3
|
4
|
tim@3
|
5 This module implements a mapping from person positions (id,x,y,z) to generate
|
tim@3
|
6 pitch, velocities and channels for Joe's synthesiser and controller data for
|
tim@3
|
7 Ableton.
|
tim@3
|
8
|
tim@3
|
9 '''
|
tim@3
|
10
|
tim@3
|
11 from OSC import ThreadingOSCServer, OSCClient, OSCMessage, OSCClientError
|
tim@4
|
12 from threading import Thread
|
tim@4
|
13 from time import sleep
|
tim@30
|
14 import pdb
|
tim@3
|
15
|
tim@3
|
16 #### PUBLIC OPTIONS ####
|
tim@4
|
17 num_channels = 3 # number of instruments (and max number of people who can
|
tim@4
|
18 # make noise).
|
tim@3
|
19
|
tim@3
|
20 #### OSC OPTIONS - THESE NEED TO BE SET MANUALLY ####
|
tim@41
|
21 my_port = 12343 # to receive OSC messages from kinect
|
tim@41
|
22
|
tim@41
|
23 debugging = True # display incoming packets
|
tim@40
|
24
|
tim@40
|
25 # Both joe and ableton ip addresses are now read from the file ip_addresses.txt
|
tim@40
|
26 # which is written to by the set_ip_addresses() function.
|
tim@40
|
27
|
andrew@38
|
28 #joe = ('localhost', 12346)#changed by andrew - python sending to joe
|
tim@40
|
29 #ableton = ('localhost', 12345)#changed by andrew - max receiving from Joe
|
tim@40
|
30 ip_address_file = 'ip_addresses.txt'
|
tim@40
|
31 ableton = None ### THESE WILL BE SET AUTOMATICALLY BELOW
|
tim@40
|
32 joe = None ### DO NOT CHANGE THEM HERE
|
tim@3
|
33
|
tim@3
|
34 ### Constants for grid mapping:
|
tim@3
|
35 # The range of values that the input coordinates and output values may take:
|
tim@3
|
36 # (ranges are inclusive)
|
tim@3
|
37 MIN = {
|
tim@3
|
38 'x' : 0.,
|
tim@3
|
39 'y' : 0.,
|
tim@3
|
40 'z' : 0.,
|
tim@3
|
41 'pitch' : 0,
|
tim@3
|
42 'cc1' : 0,
|
tim@3
|
43 'cc2' : 0,
|
tim@22
|
44 'velocity' : 0,
|
tim@3
|
45 }
|
tim@3
|
46 MAX = {
|
tim@3
|
47 'x' : 1.,
|
tim@3
|
48 'y' : 1.,
|
tim@3
|
49 'z' : 1.,
|
tim@3
|
50 'pitch' : 15,
|
tim@3
|
51 'cc1' : 127,
|
tim@3
|
52 'cc2' : 127,
|
tim@22
|
53 'velocity' : 127,
|
tim@3
|
54 }
|
tim@3
|
55
|
tim@3
|
56
|
tim@3
|
57
|
tim@3
|
58 #### PRIVATE VARIABLES ####
|
tim@4
|
59
|
tim@4
|
60 # mapping from channel to the currently playing note (pitch values) or None
|
tim@4
|
61 # initialize each channel to None:
|
tim@4
|
62 currently_playing = [None] * num_channels
|
tim@4
|
63
|
tim@4
|
64 # mapping from personId to time of last update
|
tim@30
|
65 # ((not yet implemented))
|
tim@30
|
66 #last_update_times = {}
|
tim@30
|
67
|
tim@30
|
68 # mapping from (channel, CC_number) to last CC value sent:
|
tim@30
|
69 last_value_sent = {
|
tim@30
|
70 (1, 1) : 0,
|
tim@30
|
71 (1, 2) : 0,
|
tim@30
|
72 (2, 1) : 0,
|
tim@30
|
73 (2, 2) : 0,
|
tim@30
|
74 (3, 1) : 0,
|
tim@30
|
75 (3, 2) : 0,
|
tim@30
|
76 }
|
tim@30
|
77
|
tim@3
|
78
|
tim@3
|
79 # OSC OBJECTS
|
tim@4
|
80 server = None # Initialised when start() is run
|
tim@3
|
81 client = OSCClient()
|
tim@3
|
82
|
tim@3
|
83
|
tim@4
|
84
|
tim@4
|
85
|
tim@4
|
86
|
tim@4
|
87 ### MAPPING
|
tim@4
|
88
|
tim@3
|
89 def send_to_joe(data, address='/test'):
|
tim@3
|
90 '''Sends `data` to Joe directly as an OSC message.
|
tim@3
|
91 '''
|
tim@22
|
92 message = OSCMessage(address)
|
tim@3
|
93 message.extend(data)
|
tim@3
|
94 client.sendto(message, joe)
|
tim@30
|
95 print_d('\n==OSC Output "%s" to Joe %s:==\n %s' % (address, joe, data))
|
tim@30
|
96
|
tim@30
|
97
|
tim@30
|
98 def send_to_ableton(data, address='/cc'):
|
tim@30
|
99 '''Sends `data` to Ableton (via Max) as an OSC message.
|
tim@30
|
100 '''
|
tim@30
|
101 #pdb.set_trace()
|
tim@30
|
102 message = OSCMessage(address)
|
tim@30
|
103 message.extend(data)
|
tim@30
|
104 client.sendto(message, ableton)
|
andrew@38
|
105 # print('\n==OSC Output "%s" to Ableton %s:==\n %s' % (address, ableton, data))
|
tim@30
|
106
|
tim@30
|
107
|
tim@3
|
108
|
tim@4
|
109 def flush(channel):
|
tim@4
|
110 '''Sends note off messages for whatever note is currently playing on
|
tim@4
|
111 `channel`.
|
tim@4
|
112 '''
|
tim@4
|
113 pitch = currently_playing[channel]
|
tim@4
|
114 if pitch:
|
tim@4
|
115 #print_d('Sending note-off for note %i on channel %i.' % (pitch, channel))
|
tim@4
|
116 send_to_joe([
|
tim@4
|
117 'Turn off note %i on channel %i' % (pitch, channel),
|
tim@4
|
118 # first string is ignored
|
tim@22
|
119 int(pitch), # pitch to turn off
|
tim@30
|
120 0, # 0 to turn note off
|
tim@30
|
121 127, # doesn't matter for note-off (but never send 0)
|
tim@22
|
122 int(channel),
|
tim@4
|
123 ])
|
tim@30
|
124 currently_playing[channel] = None
|
tim@3
|
125
|
tim@3
|
126
|
tim@41
|
127 def stopperson_handler(address, tags, data, clinet_address):
|
tim@41
|
128 ''' Handles OSC input matching 'stopperson'
|
tim@41
|
129
|
tim@41
|
130 Called when a person leaves the screen.
|
tim@41
|
131 '''
|
tim@41
|
132 flush(data[0])
|
tim@41
|
133
|
tim@3
|
134 def person_handler(address, tags, data, client_address):
|
tim@3
|
135 ''' Handles OSC input matching the 'person' tag.
|
tim@3
|
136
|
tim@4
|
137 `data` should be in form [person_id, x, y, z]
|
tim@3
|
138 '''
|
tim@41
|
139 if debugging:
|
tim@41
|
140 print('Received packet:')
|
tim@41
|
141 print(str(data))
|
tim@22
|
142 pitch, velocity, channel, cc1, cc2 = grid_map(*data)
|
andrew@38
|
143 channel = channel + 1
|
tim@30
|
144 cc1, cc2 = int(round(cc1)), int(round(cc2))
|
tim@30
|
145 if cc1 != last_value_sent[(channel, 1)]:
|
tim@30
|
146 send_to_ableton([channel, 1, cc1], '/cc')
|
tim@30
|
147 last_value_sent[(channel, 1)] = cc1
|
tim@30
|
148 if cc2 != last_value_sent[(channel, 2)]:
|
tim@30
|
149 send_to_ableton([channel, 2, cc2], '/cc')
|
tim@30
|
150 last_value_sent[(channel, 2)] = cc2
|
tim@41
|
151
|
tim@3
|
152
|
tim@3
|
153 ## Format data for Joe - done using Specification.txt on 2011-02-15
|
tim@3
|
154
|
tim@3
|
155 # constrain and round off pitch and velocity
|
tim@3
|
156 pitch = max(min(round(pitch), MAX['pitch']), MIN['pitch'])
|
tim@4
|
157 velocity = max(min(round(velocity), MAX['velocity']), MIN['velocity'])
|
tim@3
|
158
|
tim@4
|
159 if velocity and pitch == currently_playing[channel]:
|
tim@4
|
160 return # keep playing current note
|
tim@3
|
161
|
tim@4
|
162 # otherwise turn note off:
|
tim@4
|
163 flush(channel)
|
tim@4
|
164
|
tim@4
|
165 if velocity: # if there is a new note to play
|
tim@4
|
166 send_to_joe([
|
tim@4
|
167 'Turn on note %i on channel %i' % (pitch, channel),
|
tim@4
|
168 # first value is string which is ignored
|
tim@22
|
169 int(pitch),
|
tim@30
|
170 1, # 1 to turn note on
|
tim@22
|
171 int(velocity),
|
tim@22
|
172 int(channel)
|
tim@4
|
173 ])
|
tim@30
|
174 currently_playing[channel] = pitch
|
tim@30
|
175
|
tim@30
|
176
|
tim@3
|
177
|
tim@3
|
178
|
tim@3
|
179
|
tim@3
|
180 def grid_map(person_id, x, y, z):
|
tim@3
|
181 '''This function maps from a person's location to MIDI data
|
tim@3
|
182 returning a tuple (pitch, velocity, channel, cc1, cc2).
|
tim@3
|
183
|
tim@3
|
184 The current mapping creates higher pitch values as the person moves
|
tim@3
|
185 closer to the Kinect (i.e. z decreases). x and y values are mapped to cc1
|
tim@3
|
186 and cc2 (to be sent straight to Ableton and determined by a particular
|
tim@3
|
187 synth)
|
tim@3
|
188
|
tim@3
|
189 NB. channel == person_id and velocity==0 when note is off.
|
tim@3
|
190 Midi-Velocity is currently unimplemented but will use Person-velocity data
|
tim@3
|
191 when that becomes available.
|
tim@3
|
192 This function does not guarantee that the output will be in range if the
|
tim@3
|
193 input goes out of range.
|
tim@3
|
194 '''
|
tim@3
|
195 pitch = round(interpolate(z,
|
tim@3
|
196 MIN['z'], MAX['z'],
|
tim@3
|
197 MIN['pitch'], MAX['pitch'])
|
tim@3
|
198 )
|
tim@3
|
199 cc1 = round(interpolate(x,
|
tim@3
|
200 MIN['x'], MAX['x'],
|
tim@3
|
201 MIN['cc1'], MAX['cc1'])
|
tim@3
|
202 )
|
tim@30
|
203 cc2 = round(interpolate(y,
|
tim@3
|
204 MIN['y'], MAX['y'],
|
tim@3
|
205 MIN['cc2'], MAX['cc2'])
|
tim@3
|
206 )
|
tim@3
|
207 velocity = 127
|
tim@3
|
208 return (pitch, velocity, person_id, cc1, cc2)
|
tim@3
|
209
|
tim@3
|
210
|
tim@3
|
211
|
tim@3
|
212
|
tim@3
|
213 def interpolate(x, a, b, A, B):
|
tim@3
|
214 ''' Interpolates x from the range [a, b] to the range [A, B].
|
tim@3
|
215
|
tim@3
|
216
|
tim@3
|
217 Interpolation is linear. From [a,b] to [A,B] this would be:
|
tim@3
|
218 (B-A)*(x-a)/(b-a) + A
|
tim@3
|
219 '''
|
tim@3
|
220 return (B-A)*(x-a)/(b-a) + A
|
tim@4
|
221
|
tim@4
|
222
|
tim@4
|
223 def print_d(string):
|
tim@4
|
224 '''Function to print out debug method. Disable if processing power is in
|
tim@4
|
225 demand.
|
tim@4
|
226 '''
|
tim@4
|
227 print(string)
|
tim@4
|
228
|
tim@4
|
229
|
tim@4
|
230
|
tim@4
|
231
|
tim@4
|
232
|
tim@4
|
233
|
tim@4
|
234 #### CONTROL
|
tim@4
|
235
|
tim@4
|
236 def start():
|
tim@4
|
237 '''Set up OSC servers and start the program running.
|
tim@4
|
238 '''
|
tim@40
|
239 global server
|
tim@40
|
240 set_ip_addresses()
|
tim@4
|
241 server = ThreadingOSCServer(('localhost', my_port))
|
tim@4
|
242 # Register OSC callbacks:
|
tim@4
|
243 server.addMsgHandler('/person', person_handler)
|
tim@41
|
244 server.addMsgHandler('/stopperson', stopperson_handler)
|
tim@4
|
245 t = Thread(target=server.serve_forever)
|
tim@4
|
246 t.start()
|
tim@4
|
247 if server.running and t.is_alive():
|
tim@4
|
248 print('OSC Server running on port %i.' % my_port)
|
tim@4
|
249 print("Use 'stop()' to close it.")
|
tim@4
|
250 else:
|
tim@4
|
251 print('Error: Either server thread died or server is not reported as running.')
|
tim@4
|
252
|
tim@4
|
253
|
tim@4
|
254 def stop():
|
tim@4
|
255 '''Close the OSC server.
|
tim@4
|
256 '''
|
tim@4
|
257 if server:
|
tim@4
|
258 server.close()
|
tim@4
|
259 sleep(0.3)
|
tim@4
|
260 if not server.running:
|
tim@4
|
261 print("\n\nSuccessfully closed the OSC server. Ignore a 'Bad file descriptor' Exception - there's nothing I can do about that.")
|
tim@4
|
262 print("Type 'quit()' to exit.")
|
tim@4
|
263 else:
|
tim@4
|
264 print('Error: server has been told to close but is still running.')
|
tim@4
|
265
|
tim@40
|
266
|
tim@40
|
267 def get_ip_addresses_from_file(filename=ip_address_file):
|
tim@40
|
268 '''Load up ip addresses from a file in the format saved by the
|
tim@40
|
269 set_ip_addresses function.
|
tim@40
|
270 '''
|
tim@40
|
271 f = open(filename, 'r')
|
tim@40
|
272 lines = f.readlines()
|
tim@40
|
273 global ableton, joe
|
tim@40
|
274 ableton = eval(lines[1].split('#')[0].strip()) # set ableton
|
tim@40
|
275 joe = eval(lines[2].split('#')[0].strip()) # set joe
|
tim@40
|
276 if not ableton:
|
tim@40
|
277 print('Failed to set IP for Ableton from file.')
|
tim@40
|
278
|
tim@40
|
279 if not joe:
|
tim@40
|
280 print('Failed to set IP for Joe from file.')
|
tim@40
|
281 f.close()
|
tim@40
|
282
|
tim@40
|
283
|
tim@40
|
284
|
tim@40
|
285 def set_ip_addresses(save_file=ip_address_file):
|
tim@40
|
286 '''Manually set IP addresses based on input from user. IPs are then saved to
|
tim@40
|
287 a file.
|
tim@40
|
288 '''
|
tim@40
|
289 global joe, ableton
|
tim@40
|
290 if not joe: joe = ('localhost', 12346)
|
tim@40
|
291 if not ableton: ableton = ('localhost', 12345)
|
tim@40
|
292 # set joe
|
tim@40
|
293 try:
|
tim@40
|
294 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
|
295 except:
|
tim@40
|
296 new_joe = None
|
tim@40
|
297 if new_joe: joe = new_joe
|
tim@40
|
298 print("Address for Joe: "+str(joe)+"\n")
|
tim@40
|
299 # set ableton
|
tim@40
|
300 try:
|
tim@40
|
301 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
|
302 except:
|
tim@40
|
303 new_ableton = None
|
tim@40
|
304 if new_ableton: ableton = new_ableton
|
tim@40
|
305 print('Address for Ableton: '+str(ableton)+'\n')
|
tim@40
|
306 # save to file
|
tim@40
|
307 if new_ableton or new_joe:
|
tim@40
|
308 print('Writing new addresses to file %s.' % save_file)
|
tim@40
|
309 try:
|
tim@40
|
310 f = open(save_file, 'w')
|
tim@40
|
311 f.write(
|
tim@40
|
312 '''# 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
|
313 %s # set Ableton IP and port
|
tim@41
|
314 %s # set Joe's processing IP and port
|
tim@40
|
315 ''' % (str(ableton),str(joe))
|
tim@40
|
316 )
|
tim@40
|
317 f.flush()
|
tim@40
|
318 f.close()
|
tim@40
|
319 except IOError:
|
tim@40
|
320 print('SAVE FAILED.')
|
tim@40
|
321
|
tim@40
|
322
|
tim@40
|
323
|
tim@40
|
324
|
tim@40
|
325
|
tim@40
|
326 get_ip_addresses_from_file(ip_address_file)
|
tim@40
|
327
|
tim@40
|
328
|
tim@4
|
329 if __name__=='__main__':
|
tim@4
|
330 start()
|
tim@4
|
331 while True:
|
tim@4
|
332 try:
|
tim@41
|
333 print(eval(input()))
|
tim@4
|
334 except Exception as e:
|
tim@4
|
335 print('Caught %s:' % repr(e))
|
tim@4
|
336 exception = e
|
tim@4
|
337 trace = traceback.format_exc()
|
tim@4
|
338 print('Exception saved as `exception`. Stack trace saved as `trace`.')
|
tim@40
|
339
|