source: trunk/TouchOSC/RemixNet.py @ 11

Revision 11, 20.5 KB checked in by st8, 13 months ago (diff)

Added prelim TouchOSC script

Line 
1"""
2# Copyright (C) 2007 Nathan Ramella (nar@remix.net)
3#
4# This library is free software; you can redistribute it and/or
5# modify it under the terms of the GNU Lesser General Public
6# License as published by the Free Software Foundation; either
7# version 2.1 of the License, or (at your option) any later version.
8#
9# This library is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12# Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public
15# License along with this library; if not, write to the Free Software
16# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17#
18# For questions regarding this module contact
19# Nathan Ramella <nar@remix.net> or visit http://www.liveapi.org
20
21RemixNet Module
22
23This module contains four classes that have been assembled to facilitate
24remote control of Ableton Live. It's been an interesting experience learning
25Python and has given me a lot of time to think about music and networking
26protocols. I used OSC as it's somewhat of an accepted protocol and at least
27more flexible than MIDI. It's not the quickest protocol in terms of
28pure ops, but it gets the job done.
29
30For most uses all you'll need to do is create an OSCServer object, it
31in turn creates an OSCClient and registers a couple default callbacks
32for you to test with. Both OSCClient and OSCServer create their own UDP
33sockets this is settable on initialization and during runtime if you wish
34to change them.
35
36Any input or feedback on this code will always be appreciated and I look
37forward to seeing what will come next.
38
39-Nathan Ramella (nar@remix.net)
40
41-Updated 29/04/09 by ST8 (st8@q3f.org)
42    Works on Mac OSX with Live7/8
43   
44    The socket module is missing on osx and including it from the default python install doesnt work.
45    Turns out its the os module that causes all the problems, removing dependance on this module and
46    packaging the script with a modified version of the socket module allows it to run on osx.
47   
48"""
49import sys
50import Live
51
52# Import correct paths for os / version
53version = Live.Application.get_application().get_major_version()
54if sys.platform == "win32":
55    import socket   
56
57else:
58    if version > 7:
59        try:
60            file = open("/usr/lib/python2.5/string.pyc")
61        except IOError:
62            sys.path.append("/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5")
63            import socket_live8 as socket
64        else:
65            sys.path.append("/usr/lib/python2.5")
66            import socket 
67
68import OSC 
69
70def get_ip():
71    return socket.gethostbyname(socket.gethostname())
72     
73class OSCClient:
74    """
75    This is a helperclass for the OSCServer that will setup
76    a simple method for sending OSC messages
77    """
78   
79    def __init__(self, udpClient=None, address=None, msg=None):
80        """
81        Initializes a RemixNet.OSCClient object. You can pass
82        in a default address or default msg here. This is useful
83        for making 'beacon' clients that you can attach as
84        listeners on Live object attributes.
85        """
86   
87        if address is not None:
88            self.address = address
89           
90        if msg:
91            self.msg = msg
92        else:
93            self.msg = None
94           
95        if udpClient is not None:
96            self.udpClient = udpClient
97           
98    def setUDPClient(self, udpClient):
99        """
100        If we create our OSCClient object without defining a udpClient
101        we can set one after the fact here. If you don't and you try to
102        send, you'll raise an exception.
103        """
104       
105        if udpClient:
106            self.udpClient = udpClient
107       
108    def send(self, address=None, msg=None):
109       
110        """
111        Given an OSC address and OSC msg payload we construct our
112        OSC packet and send it to its destination. You can pass in lists
113        or tuples in msg and we will iterate over them and append each
114        to the end of a single OSC packet.
115       
116        This can be useful for transparently dealing with methods that
117        yield a variety of values in a list/tuple without the necessity of
118        combing through it yourself.
119        """
120       
121        if self.udpClient is None:
122            # SHOULD RAISE EXCEPTION
123            return
124       
125       
126        # If neither address or msg, we have nothing to do.
127       
128        if not address and not self.address:
129            # SHOULD RAISE EXCEPTION
130            return
131       
132        # I feel a little weird doing this, but I want to keep
133        # the 'default' self.msg that the object was initialized
134        # with, without playing Towers of Hanoi with another variable.
135       
136        if self.msg and self.msg is not None:
137            msg = self.msg
138       
139        # I don't like doing it here any more than I did up there.
140           
141        if not address:
142            if not self.address:
143                # SHOULD RAISE EXCEPTION
144                return
145            address = self.address
146
147        oscMessage = OSC.OSCMessage()
148        oscMessage.setAddress(address)
149
150        # We need to check for msgs that are actually
151        # instance methods here and do something with
152        # them...
153        # if type(msg) == instance method:
154        # blahblah
155       
156        # By default OSC.py doesn't look like it'll process tuples
157        # and pack them. So, we help it along by breaking them up
158        # and appending each entity.
159       
160        if type(msg) in (str,int,float):
161           oscMessage.append(msg)
162        elif type(msg) in (list,tuple):
163             for m in msg:
164                if type(m) not in (str,int,float):
165                    # SHOULD RAISE EXCEPTION
166                    return
167                oscMessage.append(m)     
168        elif msg == None:
169                self.udpClient.send(oscMessage.getBinary())
170                return
171        else:
172            # SHOULD RAISE EXCEPTION
173            # Likely, method or instancemethod object. We should
174            # actually execute the code here and send the result,
175            # but for now we'll just return.
176            return
177        # Done processing, send it off to its destination           
178        self.udpClient.send(oscMessage.getBinary())
179       
180class OSCServer:
181       
182    def __init__(self, dst=None, dstPort=None, src=None, srcPort=None, ):
183        """
184        This is the main class we the use as a nexus point in this module.
185       
186        - dst: destination/target host for OSC responses. If None will default to local network broadcast only.
187        - src: Which local interface / ip to bind to, if unset defaults to all
188        - srcPort: Source port to bind the server to for incoming OSC queries. Defaults to 9000
189        - dstPort: Destination port for OSC responses sent by callbacks. Defaults to 9001       
190       
191        By default we define and set callbacks for two utility functions that may
192        be useful in testing.
193       
194        /remix/echo -> OSCServer.callbackEcho() - For responding to /remix/echo queries.
195        /remix/time -> OSCServer.callbackTime() - Returns time.time() (time in float seconds)
196       
197        I chose OSC to deliver messages out of necessity, my opinion of OSC at this
198        point is that its addressing system is heavyweight although the idea is
199        a reasonable one. But taking into consideration the ratio of address:data
200        it becomes somewhat unreasonable unless you take the route of making unreadable
201        addresses. As an example I offer the following address,
202
203        /ableton/track/1/volume/set float(0.98)
204
205        To set a single 4 byte float value, we need to use a 27 byte string to get
206        it routed to the correct area, and even then we need to make an O(N) comparison
207        on the address since we don't have the luxury of a switch statement in Python.
208       
209        If you're trying to interact with devices in near-realtime the number of ops
210        wasted on just getting things to the right place can take the wind out of
211        your sails.
212       
213        But basically for this project to be accepted or useful to anyone it was
214        important to me that we provide a method of accessing that other tools
215        could use without having to introduce a new linewire protocol.
216       
217        It should be noted that performance even with the added ops isn't that bad.
218        On my dualcore system I was able to process about 1380 OSC callbacks per
219        second. Or, ~86 callbacks per 60ms tick.
220        """
221
222        self.udpServer = UDPServer(src, srcPort)
223        self.udpClient = UDPClient(dst, dstPort)
224        self.udpClient.open()
225       
226        self.oscClient = OSCClient(self.udpClient,None, None)
227       
228        # Create our callback manager and register some utility
229        # callbacks to show how its done.
230       
231        self.callbackManager = OSC.CallbackManager()
232        self.callbackManager.add(self.callbackEcho, '/remix/echo')
233        self.callbackManager.add(self.callbackEcho, '/remix/time')
234        self.udpServer.setCallbackManager(self.callbackManager)
235        self.udpServer.bind()
236 
237    #Should this method go here?
238    #def attachToCurrentSongTime(self):
239 
240    def callbackEcho(self, msg=None):
241        """
242        When re recieve a '/remix/echo' OSC query from another host
243        we respond in kind by passing back the message they sent to us.
244        Useful for verifying functionality.
245        """
246       
247        self.oscClient.send('/remix/echo', msg[2])
248       
249    def callbackTime(self, msg=None):
250        """
251        When we recieve a '/remix/time' OSC query from another host
252        we respond with the current value of time.time()
253       
254        This callback can be useful for testing timing/queue processing
255        between hosts
256        """
257
258        self.oscClient.send('/remix/time', time.time())
259       
260    def sendOSC(self, address=None, msg=None):
261        """
262        A convienence function so we don't have to dig into the objects
263        every time we want to send an OSC packet.
264        """
265       
266        if address and msg:
267            self.oscClient.send(address, msg)
268   
269    def sendUDP(self, data):
270        """
271        A convienence function so we don't have to dig into the objects
272        every time we want to send raw UDP.
273        """
274       
275        if data:
276            self.udpClient.send(data)
277           
278    def getCallbacks(self):
279        """
280        If you'd like to see what callbacks you have registered, this function
281        will pass you back the dict from the OSC.Manager object.
282        """
283       
284        return dict(self.callbackManager.callbacks)
285       
286    def addCallback(self, method=None, address=None):
287        """
288        This method will allow you to externally add callbacks into the
289        UDPServer. As a rule of thumb we'd like to keep everything seperate
290        for ease of maintenance.
291       
292        You call this method with the arguments:
293       
294        - method: The method object you want to register as a callback for an OSC address.
295        - address: The OSC address to bind to. (Example: /remix/mynewcallback/)
296       
297        If either of these values isn't set, nothing will get registered.
298        """
299       
300        if method and address:
301            self.callbackManager.add(method, address)
302        else:
303            # SHOULD RAISE EXCEPTION?
304            return
305
306    def processIncomingUDP(self):
307        """
308        This is the juice of our tool. While UDP is billed as an unreliable
309        protocol, as it turns out it's not that bad. In fact, it can be pretty
310        good.
311       
312        There are several limitations to the Ableton Live Python environment.
313       
314        * The Ableton Live Python environment is minimal. The included module
315          list is very short. For instance, we don't have 'select()'.
316         
317        * The Ableton Live Python version is a bit older than what most Python
318          programmers are used to. Its version string says 2.2.1, and the Python
319          webpage shows that the offical 2.2.3 came out May 30, 2003. So we've
320          got 4 years between us and it. Fortunately since I didn't know any Python
321          when I got started on this project the version differences didn't bother
322          me. But I know the lack of modern features has been a pain for a few
323          of our developers.
324         
325        * The Ableton Live Python environment, although it includes the thread
326          module, doesn't function how you'd expect it to. The threads appear to
327          be on a 100ms timer that cannot be altered consistently through Python.
328         
329          I did find an interesting behavior in that when you modify the
330          sys.setcheckinterval value to very large numbers for about 1-5/100ths of
331          a second thread focus goes away entirely and if your running thread is
332          a 'while 1:' loop with no sleep, it gets 4-5 iterations in before
333          the thread management stuff kicks in and puts you down back to 100ms
334          loop.
335         
336          As a goof I tried making a thread that was a 'while 1:' loop with a
337          sys.setcheckinterval(50000) inside it -- first iteration it triggered
338          the behavior, then it stopped.
339         
340          It should also be noted that you can make a blocking TCP socket using
341          the threads interface. But your refresh is going to be about 40ms slower
342          than using a non-blocking UDP socket reader. But hey, you're the boss!
343         
344          So far the best performance for processing incoming packets can be found
345          by attaching a method as a listener to the Song.current_song_time
346          attribute. This attribute updates every 60ms on the dot allowing for
347          16 passes on the incoming UDP traffic every second.
348         
349          My machine is pretty beefy but I was able to sustain an average of
350          over 1300 /remix/echo callback hits a second and only lost .006%
351          of my UDP traffic over 10 million packets on a machine running Live.
352         
353          One final note -- I make no promises as to the latency of triggers recieved.
354          I haven't tested that at all yet. Since the window is 60ms, don't get
355          your hopes up about MIDI over OSC.
356        """
357       
358        self.udpServer.processIncomingUDP()
359       
360    def bind(self):
361        """Bind to the socket and prepare for incoming connections."""
362        self.udpServer.bind()
363       
364    def shutdown(self):
365        """If we get shutdown by our parent, close the socket we had open"""
366        self.udpClient.close()
367        self.udpServer.close()
368         
369class UDPClient:
370    """
371    This is a fairly brain-dead UDPClient implementation that is
372    used by the OSCClient to send packets out. You shouldn't need
373    this unless you want to get tricky or make a linewire protocol.
374    """ 
375             
376    def __init__(self, dst=None, dstPort=None):
377        """
378        When the OSCClient instantiates its UDPClient it passes along:
379        - dst: The destination host. If none only send to localhost.
380        - dstPort: The destination port. If none, 9001 by default.
381        """
382               
383        if dst: 
384            self.dst = dst
385        else:
386            # If you'd like to try broadcast,
387            # set this to <broadcast>
388            # I've been unable to get it to work.
389            self.dst = 'localhost' 
390                                   
391        if dstPort:               
392            self.dstPort = dstPort
393        else: 
394            self.dstPort = 9001 
395                       
396    def setDstPort(self, dstPort=None):
397        """
398        If the port gets reset midstream, close down our UDPSock
399        and reopen to be sure. A little redundant.
400        """
401
402        # Manually set the port before init
403        if not dstPort:
404            return   
405           
406        self.DstPort = DstPort
407       
408        if self.UDPSock:
409            self.UDPSock.close()
410            self.open()
411           
412     
413         
414    def setDst(self, dst=None):
415        """
416        If the dst gets reset midstream, we close down our UDPSock
417        and reopen. A little redundant.
418        """
419       
420        if not dst:
421            return 
422       
423        self.dst = dst     
424       
425        if self.UDPSock:
426            self.UDPSock.close()
427            self.open()
428           
429     
430               
431    def open(self):
432        """
433        Open our UDPSock for listening, sets self.UDPSock
434        """
435       
436        if not self.dst:
437            return
438        if not self.dstPort:
439            return
440       
441        # Open up our socket, we're ready for business!
442       
443        self.addr = (self.dst,self.dstPort) 
444        self.UDPSock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) 
445       
446        #Broadcast doesn't work for answering callbacks for some reason.
447        #But, I'll leave this here if you'd like to try.
448        #if self.dst == '<broadcast>':
449        #    self.UDPSock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
450           
451       
452    def send(self, data):
453        """
454        If we have data to send, send it, otherwise return.
455        """
456        # Only send if we have data.
457        if not data == '':
458            self.UDPSock.sendto(data,self.addr)
459            data = ''
460           
461    def close(self):
462        """
463        Close our UDPSock
464        """
465        # Closing time!
466        self.UDPSock.close()
467       
468class UDPServer:
469
470    """
471    RemixNet.UDPServer
472       
473    This class is a barebones UDP server setup with the ability to
474    assign callbacks for incoming data. In the design as is, we use
475    an OSC.CallbackManager when we recieve any data.
476     
477    This class is designed to be used by RemixNet.OSCServer, as it
478    will do all the setup for you and register a few default OSCManager
479    callbacks.
480    """
481       
482    def __init__(self, src, srcPort):
483        """
484        Sets up the UDPServer component of this package. By default
485        we listen to all interfaces on port 9000 for incoming requests
486        with a 4096 byte buffer.
487       
488        You can modify these settings by using the methods setport() and setHost()
489        """
490       
491        if srcPort:
492            self.srcPort = srcPort
493        else:
494            self.srcPort = 9000 
495       
496        if src:
497            self.src = src
498        else:
499            self.src = ''
500       
501        self.buf = 4096
502
503    def processIncomingUDP(self):
504        """
505        Attempt to process incoming packets in the network buffer. If none are
506        available it will return. If there is data, and a callback manager has been
507        defined we'll send the data to the callback manager.
508       
509        You can specify a callback manager using the UDPServer.setCallbackManager()
510        function and passing it a populated OSC.Manager object.
511        """
512       
513        try:
514            # You'd think this while 1 loop would get stuck and block the
515            # program. But. As it turns out. It doesn't.
516           
517            while 1:
518                self.data,self.addr = self.UDPSock.recvfrom(self.buf)
519                if not self.data:
520                # No data buffered this round!
521                    return
522                else:
523                    if self.data != '\n':
524                        # Oh snap, we have data!
525                        # If you want to write your own special handlers for dealing
526                        # with incoming data, this is the place. self.data contains
527                        # the raw data sent to our UDP socket.
528               
529                        if self.callbackManager:
530                            self.callbackManager.handle(self.data)
531                       
532        except Exception, e:
533            pass
534
535    def setCallbackManager(self, callbackManager):
536        """
537        You can specify a callbackManager here as derived from OSC.py.
538        We use this function in OSCServer to register the default /remix/
539        namespace addresses as utility callbacks.
540        """
541       
542        self.callbackManager = callbackManager
543
544    def bind(self):
545        """
546        After initializing you must UDPServer.listen() to bind to the socket
547        and accept whatever packets are in the buffer. Since we're binding a
548        non-blocking socket, your program (and Ableton Live) will still be
549        able to run.
550        """
551       
552        self.addr = (self.src,self.srcPort)
553        self.UDPSock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
554        self.UDPSock.bind(self.addr)
555        self.UDPSock.setblocking(0)
556
557    def close(self):
558        """
559        Close our UDPSock
560        """
561        # Closing time!
562        self.UDPSock.close()
Note: See TracBrowser for help on using the repository browser.