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`.')
|