#!/usr/bin/python # # Python bindings for Ingen # Copyright (C) 2005 Leonard Ritter # Copyright (C) 2006 Dave Robillard # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import os,sys,thread,time from OSC import OSCMessage, decodeOSC from twisted.internet.protocol import DatagramProtocol from twisted.internet import reactor INGEN_CALL_TIMEOUT = 5 INGEN_CALL_POLLTIME = 0.1 class TreeElement: def __init__(self,environment,parent,name): self.environment = environment self.parent = parent self.name = name self.metadata = {} def __del__(self): print "'%s': gone" % self.name def removeChild(self, child): pass def remove(self): self.parent.removeChild(self.name) self.parent = None del self def getParent(self): return self.parent def getName(self): return self.name def getPath(self): if self.parent: return self.parent.getPath() + "/" + self.name else: return self.name def getDepth(self): if self.parent: return self.parent.getDepth() + 1 else: return 0 def setMetaData(self,key,value): if (not self.metadata.has_key(value)) or (self.metadata[key] != value): print "||| '%s': '%s' = '%s'" % (self.getPath(), key, value) self.metadata[key] = value class Port(TreeElement): def __init__(self,environment,parent,name): TreeElement.__init__(self,environment,parent,name) self.porttype = "" self.direction = "" self.hint = "" self.defaultvalue = 0.0 self.minvalue = 0.0 self.maxvalue = 0.0 self.value = 0.0 self.connections = {} print "*** '%s': new port" % self.getPath() def remove(self): for connection in self.getConnections(): connection.remove() TreeElement.remove(self) def getConnections(self): return self.connections def addConnection(self,target,connection): self.connections[target] = connection def removeConnection(self,target): del self.connections[target] def setPortType(self,porttype): if self.porttype != porttype: print "*** '%s': changing porttype from '%s' to '%s'" % (self.getPath(), self.porttype, porttype) self.porttype = porttype def setDirection(self,direction): if self.direction != direction: print "*** '%s': changing direction from '%s' to '%s'" % (self.getPath(), self.direction, direction) self.direction = direction def setHint(self,hint): if self.hint != hint: print "*** '%s': changing hint from '%s' to '%s'" % (self.getPath(), self.hint, hint) self.hint = hint def setDefaultValue(self,defaultvalue): if self.defaultvalue != defaultvalue: print "*** '%s': changing defaultvalue from '%.3f' to '%.3f'" % (self.getPath(), self.defaultvalue, defaultvalue) self.defaultvalue = defaultvalue def setMinValue(self,minvalue): if self.minvalue != minvalue: print "*** '%s': changing minvalue from '%.3f' to '%.3f'" % (self.getPath(), self.minvalue, minvalue) self.minvalue = minvalue def setMaxValue(self,maxvalue): if self.maxvalue != maxvalue: print "*** '%s': changing maxvalue from '%.3f' to '%.3f'" % (self.getPath(), self.maxvalue, maxvalue) self.maxvalue = maxvalue def setValue(self,value): if self.value != value: print "*** '%s': changing value from '%.3f' to '%.3f'" % (self.getPath(), self.value, value) self.value = value class Node(TreeElement): def __init__(self,environment,parent,name): TreeElement.__init__(self,environment,parent,name) self.ports = {} self.polyphonic = 0 self.plugintype = "" self.libname = "" self.pluginlabel = "" print "+++ '%s': new node" % self.getPath() def remove(self): for port in self.getPorts(): port.remove() TreeElement.remove(self) def removeChild(self, child): del self.ports[child] def getPorts(self): return self.ports.values() def setPluginLabel(self,pluginlabel): if pluginlabel != self.pluginlabel: print "+++ '%s': changing pluginlabel from '%s' to '%s'" % (self.getPath(), self.pluginlabel, pluginlabel) self.pluginlabel = pluginlabel def setLibName(self,libname): if libname != self.libname: print "+++ '%s': changing libname from '%s' to '%s'" % (self.getPath(), self.libname, libname) self.libname = libname def setPluginType(self,plugintype): if plugintype != self.plugintype: print "+++ '%s': changing plugintype from '%s' to '%s'" % (self.getPath(), self.plugintype, plugintype) self.plugintype = plugintype def setPolyphonic(self,polyphonic): if polyphonic != self.polyphonic: print "+++ '%s': changing polyphony from %i to %i" % (self.getPath(), self.polyphonic, polyphonic) self.polyphonic = polyphonic def hasPort(self,name): return self.ports.has_key(name) def getPort(self,name,mustexist=False): if not self.hasPort(name): if mustexist: return None self.ports[name] = self.environment.getPortClass()(self.environment,self,name) return self.ports[name] class Patch(Node): def __init__(self,environment,parent,name): Node.__init__(self,environment,parent,name) self.nodes = {} self.patches = {} self.poly = 0 self.enabled = False print "### '%s': new patch" % self.getPath() def remove(self): for patch in self.getPatches(): patch.remove() for node in self.getNodes(): node.remove() Node.remove(self) def removeChild(self, child): if self.hasNode(child): del self.nodes[child] elif self.hasPatch(child): del self.patches[child] else: Node.removeChild(self,child) def getPatches(self): return self.patches.values() def getNodes(self): return self.nodes.values() def getEnabled(self): return self.enabled def setEnabled(self,enabled): if enabled != self.enabled: print "### '%s': changing enabled from %s to %s" % (self.getPath(), str(self.enabled), str(enabled)) enabled = self.enabled def getPoly(self): return self.poly def setPoly(self,poly): if poly != self.poly: print "### '%s': changing polyphony from %i to %i" % (self.getPath(), self.poly, poly) self.poly = poly def hasNode(self,name): return self.nodes.has_key(name) def getNode(self,name,mustexist=False): if not self.hasNode(name): if mustexist: return None self.nodes[name] = self.environment.getNodeClass()(self.environment,self,name) return self.nodes[name] def hasPatch(self,name): return self.patches.has_key(name) def getPatch(self,name,mustexist=False): if not self.hasPatch(name): if mustexist: return None self.patches[name] = self.environment.getPatchClass()(self.environment,self,name) return self.patches[name] class Connection: def __init__(self,environment,srcport,dstport): self.environment = environment self.srcport = srcport self.dstport = dstport self.srcport.addConnection(self.dstport,self) self.dstport.addConnection(self.srcport,self) print ">>> '%s'->'%s': new connection" % (self.srcport.getPath(),self.dstport.getPath()) def __del__(self): print "connection gone" def remove(self): self.srcport.removeConnection(self.dstport) self.dstport.removeConnection(self.srcport) del self def getSrcPort(self): return self.srcport def getDstPort(self): return self.dstport def getPortPair(self): return self.srcport, self.dstport class Environment: def __init__(self): self.omPatch = self.getPatchClass()(self,None,"") self.enabled = False self.connections = {} def getConnectionClass(self): return Connection def getPatchClass(self): return Patch def getNodeClass(self): return Node def getPortClass(self): return Port def getConnection(self,srcportpath,dstportpath,mustexist=False): srcport = self.getPort(srcportpath,True) if not srcport: return None dstport = self.getPort(dstportpath,True) if not dstport: return None if not self.connections.has_key((srcport,dstport)): if mustexist: return None self.connections[(srcport,dstport)] = self.getConnectionClass()(self,srcport,dstport) return self.connections[(srcport,dstport)] def getConnections(self): return self.connections.values() def getPatch(self,path,mustexist=False): elements = path.split("/") currentPatch = None for element in elements: if element == "": currentPatch = self.omPatch else: currentPatch = currentPatch.getPatch(element,mustexist) if not currentPatch: break return currentPatch def getNode(self,path,mustexist=False): elements = path.split("/") basepath = "/".join(elements[:-1]) nodename = elements[-1] patch = self.getPatch(basepath,True) if patch: return patch.getNode(nodename,mustexist) return None def getPort(self,path,mustexist=False): elements = path.split("/") basepath = "/".join(elements[:-1]) portname = elements[-1] node = self.getNode(basepath,True) if node: return node.getPort(portname,mustexist) patch = self.getPatch(basepath,True) if patch: return patch.getPort(portname,mustexist) return None def getObject(self,path): patch = self.getPatch(path,True) if patch: return patch node = self.getNode(path,True) if node: return node return self.getPort(path,True) def printPatch(self,patch=None): if not patch: patch = self.omPatch print patch.getDepth()*' ' + "### " + patch.getPath() for node in patch.getNodes(): print node.getDepth()*' ' + "+++ " + node.getPath() for port in node.getPorts(): print port.getDepth()*' ' + "*** " + port.getPath() for port in patch.getPorts(): print port.getDepth()*' ' + "*** " + port.getPath() for subpatch in patch.getPatches(): self.printPatch(subpatch) def printConnections(self): for connection in self.getConnections(): print ">>> %s -> %s" % (connection.getSrcPort().getPath(), connection.getDstPort().getPath()) #~ /ingen/engine_enabled - Notification engine's DSP has been enabled. def __ingen__engine_enabled(self): self.enabled = True #~ /ingen/engine_disabled - Notification engine's DSP has been disabled. def __ingen__engine_disabled(self): self.enabled = False #~ /ingen/new_node - Notification of a new node's creation. #~ * path (string) - Path of the new node #~ * polyphonic (integer-boolean) - Node is polyphonic (1 for yes, 0 for no) #~ * type (string) - Type of plugin (LADSPA, DSSI, Internal, Patch) #~ * lib-name (string) - Name of library if a plugin (ie cmt.so) #~ * plug-label (string) - Label of plugin in library (ie adsr_env) #~ * New nodes are sent as a blob. The first message in the blob will be this one (/ingen/new_node), followed by a series of /om/new_port commands, followed by /om/new_node_end. def __ingen__new_node(self,path,polyphonic,plugintype,libname,pluginlabel): node = self.getNode(path) node.setPolyphonic(polyphonic) node.setPluginType(plugintype) node.setLibName(libname) node.setPluginLabel(pluginlabel) def __ingen__new_node_end(self): pass #~ /ingen/node_removal - Notification of a node's destruction. #~ * path (string) - Path of node (which no longer exists) def __ingen__node_removal(self,path): node = self.getNode(path) node.remove() #~ /ingen/new_port - Notification of a node's destruction. #~ * path (string) - Path of new port #~ * type (string) - Type of port (CONTROL or AUDIO) #~ * direction (string) - Direction of data flow (INPUT or OUTPUT) #~ * hint (string) - Hint (INTEGER, LOGARITHMIC, TOGGLE, or NONE) #~ * default-value (float) - Default (initial) value #~ * min-value (float) - Suggested minimum value #~ * min-value (float) - Suggested maximum value #~ * Note that in the event of loading a patch, this message could be followed immediately by a control change, meaning the default-value is not actually the current value of the port (ahem, Lachlan). #~ * The minimum and maximum values are suggestions only, they are not enforced in any way, and going outside them is perfectly fine. Also note that the port ranges in ingenuity are not these ones! Those ranges are set as metadata. def __ingen__new_port(self,path,porttype,direction,hint,defaultvalue,minvalue,maxvalue): port = self.getPort(path) port.setPortType(porttype) port.setDirection(direction) port.setHint(hint) port.setDefaultValue(defaultvalue) port.setMinValue(minvalue) port.setMaxValue(maxvalue) #~ /ingen/port_removal - Notification of a port's destruction. #~ * path (string) - Path of port (which no longer exists) def __ingen__port_removal(self,path): port = self.getPort(path) port.remove() #~ /ingen/patch_destruction - Notification of a patch's destruction. #~ * path (string) - Path of patch (which no longer exists) def __ingen__patch_destruction(self,path): patch = self.getPatch(path) patch.remove() #~ /ingen/patch_enabled - Notification a patch's DSP processing has been enabled. #~ * path (string) - Path of enabled patch def __ingen__patch_enabled(self,path): patch = self.getPatch(path) patch.setEnabled(True) #~ /ingen/patch_disabled - Notification a patch's DSP processing has been disabled. #~ * path (string) - Path of disabled patch def __ingen__patch_disabled(self,path): patch = self.getPatch(path) patch.setEnabled(False) #~ /ingen/new_connection - Notification a new connection has been made. #~ * src-path (string) - Path of the source port #~ * dst-path (string) - Path of the destination port def __ingen__new_connection(self,srcpath,dstpath): self.getConnection(srcpath,dstpath) #~ /ingen/disconnection - Notification a connection has been unmade. #~ * src-path (string) - Path of the source port #~ * dst-path (string) - Path of the destination port def __ingen__disconnection(self,srcpath,dstpath): connection = self.getConnection(srcpath,dstpath) portpair = connection.getPortPair() connection.remove() del self.connections[portpair] #~ /ingen/metadata/update - Notification of a piece of metadata. #~ * path (string) - Path of the object associated with metadata (can be a node, patch, or port) #~ * key (string) #~ * value (string) def __ingen__metadata__update(self,path,key,value): object = self.getObject(path) object.setMetaData(key,value) #~ /ingen/control_change - Notification the value of a port has changed #~ * path (string) - Path of port #~ * value (float) - New value of port #~ * This will only send updates for values set by clients of course - not values changing because of connections to other ports! def __ingen__control_change(self,path,value): port = self.getPort(path) port.setValue(value) #~ /ingen/new_patch - Notification of a new patch #~ * path (string) - Path of new patch #~ * poly (int) - Polyphony of new patch (not a boolean like new_node) def __ingen__new_patch(self,path,poly): patch = self.getPatch(path) patch.setPoly(poly) class SocketError: pass class Call: pass class ClientProxy: def __init__(self, om, name, is_async = False): self.name = name self.om = om self.is_async = is_async def __call__(self, *args): if (self.is_async): self.om.sendMsg(self.name, *args) return True result = self.om.sendMsgBlocking(self.name, *args) if not result: return None if result[0] == "/ingen/response/ok": return True print "ERROR: %s" % result[1][1] return False def __getattr__(self, name): if (name[:2] == "__") and (name[-2:] == "__"): # special function raise AttributeError, name if name in self.__dict__: raise AttributeError, name if name == 'async': return ClientProxy(self.om, self.name, True) else: return ClientProxy(self.om, self.name + '/' + name) class Client(DatagramProtocol, ClientProxy): def __init__(self): ClientProxy.__init__(self, self, "/ingen") def startProtocol(self): self.transport.connect("127.0.0.1", 16180) host = self.transport.getHost() self.host = host.host self.port = host.port self.environment = None self.handlers = {} self.calls = {} #print "opened port at %s" % (str((self.host,self.port))) self.nextPacketNumber = 1 def setEnvironment(self, environment): self.handlers = {} self.environment = environment for name in dir(self.environment): element = getattr(self.environment,name) if (type(element).__name__ == 'instancemethod') and (element.__name__[:6] == "__ingen__"): handlername = element.__name__.replace("__","/") print "registering handler for '%s'" % handlername self.handlers[handlername] = element def getEnvironment(self): return self.environment def connectionRefused(self): print "Noone listening, aborting." os._exit(-1) def messageReceived(self, (msg, args)): if msg == "/ingen/error": print "ERROR: %r" % args return if msg == "/ingen/response/ok": omcall = self.calls[args[0]] omcall.result = (msg,args) omcall.done = True return if msg == "/ingen/response/error": omcall = self.calls[args[0]] omcall.result = (msg,args) omcall.done = True return if msg == "#bundle": for arg in args: self.messageReceived(arg) return if self.handlers.has_key(msg): try: self.handlers[msg](*args) except: a,b,c = sys.exc_info() sys.excepthook(a,b,c) print "with '%s'" % repr((msg,args)) return print "no handler for '%s'" % repr((msg,args)) def datagramReceived(self, data, (host, port)): self.messageReceived(decodeOSC(data)) def getPacketNumber(self): packetNumber = self.nextPacketNumber self.nextPacketNumber = (self.nextPacketNumber + 1) return packetNumber def sendMsg(self,msg,*args): packetNumber = self.getPacketNumber() print "Sending %r (#%i)..." % (msg,packetNumber) omcall = Call() omcall.result = None omcall.done = False self.calls[packetNumber] = omcall message = OSCMessage() message.setAddress(msg) message.append(packetNumber) for arg in args: message.append(arg) self.transport.write(message.getBinary()) time.sleep(0.01) return True def sendMsgBlocking(self,msg,*args): packetNumber = self.getPacketNumber() print "Sending %r (#%i)..." % (msg,packetNumber) omcall = Call() omcall.result = None omcall.done = False self.calls[packetNumber] = omcall message = OSCMessage() message.setAddress(msg) message.append(packetNumber) for arg in args: message.append(arg) self.transport.write(message.getBinary()) now = time.time() while not omcall.done: time.sleep(INGEN_CALL_POLLTIME) distance = time.time() - now if distance > INGEN_CALL_TIMEOUT: print "timeout" break del self.calls[packetNumber] return omcall.result def getAddressAsString(self): return "osc.udp://%s:%d" % (self.host, self.port) def startClient(func): om = Client() reactor.listenUDP(0, om) thread.start_new_thread(func,(om,)) reactor.run()