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