Add new project 'AudioControl'
g0dil [Mon, 18 Jun 2007 10:23:27 +0000 (10:23 +0000)]
14 files changed:
Actions.py [new file with mode: 0644]
Bindings.py [new file with mode: 0644]
Events.py [new file with mode: 0644]
Joyboard.py [new file with mode: 0644]
Keyboard.py [new file with mode: 0644]
Logger.py [new file with mode: 0644]
Looper.py [new file with mode: 0644]
Node.py [new file with mode: 0644]
Node.test.py [new file with mode: 0644]
Process.py [new file with mode: 0644]
Views.py [new file with mode: 0644]
audiocontroller [new file with mode: 0755]
config.py [new file with mode: 0644]
main.py [new file with mode: 0644]

diff --git a/Actions.py b/Actions.py
new file mode 100644 (file)
index 0000000..3b29f0a
--- /dev/null
@@ -0,0 +1,91 @@
+import Bindings, Node
+
+class Action(Node.Node):
+
+    def __init__(self, name):
+        Node.Node.__init__(self)
+        self.name = name
+
+    def __call__(self, binding):
+        raise NotImplementedError()
+
+    def bindings(self):
+        return [ binding
+                 for binding in self.owners
+                 if isinstance(binding, Bindings.Binding) ]
+
+
+class _ActionRegistry(object):
+
+    def __init__(self):
+        self._registry = {}
+
+    def register(self, action):
+        self._registry[action.name] = action
+
+    def __getitem__(self, key):
+        return self._registry[key]
+
+Actions = _ActionRegistry()
+
+
+class Nop(Action):
+
+    def __init__(self, name='nop'):
+        Action.__init__(self, name)
+
+    def __call__(self, binding):
+        pass
+    
+
+class Macro(Action):
+
+    _actions = Node.NodeList()
+
+    def __init__(self, name, actions):
+        Action.__init__(self, name)
+        for i in range(len(actions)):
+            if type(actions[i]) is str:
+                actions[i] = Actions[actions[i]]
+        self._actions = actions
+
+    def __call__(self, binding):
+        for action in self._actions:
+            action(binding)
+            
+class Command(Action):
+
+    def __init__(self, name, callable):
+        Action.__init__(self, name)
+        self._callable = callable
+
+    def __call__(self, binding):
+        self._callable(binding)
+        
+
+class ChangeBindingsAbsolute(Action):
+
+    def __init__(self, name, mapIndex, keymaps):
+        Action.__init__(self, name)
+        self._mapIndex = mapIndex
+        self._keymaps = keymaps
+
+    def __call__(self, binding):
+        binding.keymap().keylist().replaceFromIndex(self._mapIndex, self._keymaps)
+
+
+class ChangeBindingsRelative(Action):
+
+    def __init__(self, name, mapIndex, keymaps):
+        Action.__init__(self, name)
+        self._mapIndex = mapIndex
+        self._keymaps = keymaps
+
+    def __call__(self, binding):
+        keymap = binding.keymap()
+        keymap.keylist().replaceFromIndex(keymap.index()+self._mapIndex, self._keymaps)
+
+def action(func):
+    action = Command(func.__name__, func)
+    Actions.register(action)
+    return func
diff --git a/Bindings.py b/Bindings.py
new file mode 100644 (file)
index 0000000..aaac851
--- /dev/null
@@ -0,0 +1,118 @@
+"""The Bindings module manages an infrastructure for key bindings."""
+
+# Event = (context, sequence ...) wobei alle elsts strings sind
+
+# gebuynbden wird ein Action object. Jede Action hat einen namen unter
+# der sie registriert wird.  die funktionen zum definieren eines
+# keymnap und zum binden von keys nehmen diesen namen als argument =>
+# alle Aktionen m"ussen vorher registriert werden
+
+# KeyStack => KeyMap => Binding => Action
+
+# Das Binding Objekt enthaelt das Event unter welchem die Action
+# gebunden ist. Jedes Binding aht auch einen label zum anzeigen des
+# bindings
+
+import Node, Actions, Events, Logger
+
+class Binding(Node.Node):
+
+    action = Node.NodeReference()
+
+    def __init__(self, event, label, action):
+        Node.Node.__init__(self)
+        self.event = event
+        self.label = label
+        self.action = action
+
+    def keymap(self):
+        return self.owner
+
+    def execute(self):
+        Logger.log('binding', 'execute: %s' % self.action.name)
+        self.action(self)
+
+
+class KeyMap(Node.Node):
+
+    _map = Node.NodeMap()
+
+    def __init__(self, name=None):
+        Node.Node.__init__(self)
+        self._name = name
+
+    def add(self, binding):
+        if self.owner : raise RuntimeError, 'changing an active KeyMap not supported'
+        self._map[binding.event] = binding
+
+    def unbind(self, event):
+        if self.owner : raise RuntimeError, 'changing an active KeyMap not supported'
+        del self._map[event]
+
+    def name(self):
+        return self._name
+
+    def index(self):
+        if self.owner : return self.owner.indexOf(self)
+        else          : return None
+
+    def keylist(self):
+        return self.owner
+
+    def lookup(self, event):
+        return self._map.get(event,None)
+
+    def bindings(self):
+        return self._map
+
+
+class KeyList(Node.Node):
+
+    _list = Node.NodeList()
+    
+    def __init__(self):
+        Node.Node.__init__(self)
+        self._callbacks = []
+
+    def indexOf(self, map):
+        return self._list.index(map)
+
+    def append(self, map):
+        self._list.append(map)
+        self._callCallbacks()
+
+    def replaceFromIndex(self, index, maps):
+        self._list[index:] = maps
+        self._callCallbacks()
+
+    def lookup(self, event):
+        for i in range(len(self._list)-1,-1,-1):
+            binding = self._list[i].lookup(event)
+            if binding is not None : return binding
+        return None
+
+    def bindings(self):
+        bindings = {}
+        for map in self._list:
+            bindings.update(map.bindings())
+        return bindings
+
+    def bindingsByContext(self):
+        bindings = {}
+        for map in self._list:
+            for binding in map.bindings().itervalues():
+                bindings.setdefault(binding.event.context,{})
+                bindings[binding.event.context][binding.event] = binding
+        return bindings
+
+    def registerCallback(self, cb):
+        self._callbacks.append(cb)
+
+    def unregisterCallback(self, cb):
+        self._callbacks.remove(cb)
+
+    def _callCallbacks(self):
+        for cb in self._callbacks : cb(self)
+
+    def path(self):
+        return ' / '.join([ map.name() for map in self._list if map.name() ])
diff --git a/Events.py b/Events.py
new file mode 100644 (file)
index 0000000..4a51f79
--- /dev/null
+++ b/Events.py
@@ -0,0 +1,83 @@
+"""Event handling and generation.
+
+The Event module manages event generation. Events are created by
+EventSource instances. Each event source is associated with a file
+handle. This handle will be poll()'ed and the eventsource will be
+called, as sonn as input is available."""
+
+import select, Logger, Node
+
+class _Event(tuple):
+
+    def __init__(self, elts):
+        tuple.__init__(self, elts)
+
+    context = property(lambda self: self[0], None, None)
+    code = property(lambda self: self[1], None, None)
+
+
+def Event(context, code):
+    return _Event((context,code))
+
+
+class EventSource(Node.Node):
+
+    def __init__(self, fd, context):
+       Node.Node.__init__(self)
+        self._fd = fd
+        self._context = context
+
+    def fd(self):
+        return self._fd
+
+    def context(self):
+        return self._context
+
+    def fileno(self):
+        return self._fd.fileno()
+
+    def readEvents(self):
+        raise NotImplementedError
+
+
+class Dispatcher(Node.Node):
+
+    _sources = Node.NodeMap()
+
+    def __init__(self, keylist):
+       Node.Node.__init__(self)
+       self._sources = {}
+        self._callbacks = []
+        self._keylist = keylist
+        self._poller = select.poll()
+
+    def registerSource(self, eventSource):
+        self._sources[eventSource.fileno()] = eventSource
+       self._poller.register(eventSource.fileno(),select.POLLIN)
+
+    def unregisterSource(self, eventSource):
+       del self._sources[eventSource.fileno()]
+       self._poller.unregister(eventSource.fileno())
+
+    def registerCallback(self,cb):
+        self._callbacks.append(cb)
+
+    def unregisterCallback(self,cb):
+        self._callbacks.remove(cb)
+
+    def run(self):
+        while 1:
+            for cb in self._callbacks : cb()
+            try:
+                pollEvents = self._poller.poll()
+            except select.error, v:
+                if v[0]==4 : continue
+                else       : raise
+            if not pollEvents : return
+            for fd, pollEvent in pollEvents:
+                if pollEvent != select.POLLIN : return
+                for event in  self._sources[fd].readEvents():
+                    Logger.log('dispatcher', 'event: ' + str(event))
+                    binding = self._keylist.lookup(event)
+                    if binding is not None:
+                        binding.execute()
diff --git a/Joyboard.py b/Joyboard.py
new file mode 100644 (file)
index 0000000..2629257
--- /dev/null
@@ -0,0 +1,100 @@
+import Views, Events, Logger
+from Views import EventWidget
+import time, os, struct
+
+class View(Views.WidgetView):
+
+    def __init__(self, context, label, numeric_switches, alpha_switches, x, y, size=11):
+        size += 1
+        Views.WidgetView.__init__(self, context, label, x, y,size*numeric_switches+3,7)
+
+        split = numeric_switches // 2
+        for i in range(split):
+            self.add( EventWidget(i,str(i),size*i+1,4,size) )
+        for i in range(split,numeric_switches):
+            self.add( EventWidget(i,str(i),size*i+3,4,size) )
+        split = max(0,alpha_switches-(numeric_switches-split))
+        offset = size//2+(numeric_switches-alpha_switches-1)*size
+        for i in range(split):
+            self.add( EventWidget(i+numeric_switches, chr(ord('A')+i), size*i+1+offset,1,size) )
+        for i in range(split, alpha_switches):
+            self.add( EventWidget(i+numeric_switches, chr(ord('A')+i), size*i+3+offset,1,size) )
+
+
+class JSEvent:
+
+    STRUCT = "IhBB"
+    STRUCT_len = struct.calcsize(STRUCT)
+
+    TYPE_BUTTON = 0x01
+    TYPE_AXIS = 0x02
+    TYPE_INIT = 0x80
+    
+    def __init__(self, data):
+        self.time, self.value, self.type, self.number = struct.unpack(self.STRUCT,data)
+
+    def __str__(self):
+        return "%s(time=%d, value=%d,type=%d (%s)), number=%d)" % (
+            str(self.__class__),self.time, self.value, self.type, self.decode_type(), self.number)
+
+    def decode_type(self):
+        return ('','INIT|')[(self.type & self.TYPE_INIT) and 1 or 0] \
+               + ('','BUTTON','AXIS')[self.type & ~self.TYPE_INIT]
+
+    def readFrom(klass,f):
+        return klass(os.read(f.fileno(),klass.STRUCT_len))
+    readFrom=classmethod(readFrom)
+
+    def readMultipleFrom(klass,f,maxevents=256):
+        data = os.read(f.fileno(),maxevents*klass.STRUCT_len)
+        rv = []
+        while data:
+            rv.append(klass(data[:klass.STRUCT_len]))
+            data = data[klass.STRUCT_len:]
+        return rv
+    readMultipleFrom=classmethod(readMultipleFrom)
+
+class Source(Events.EventSource):
+
+    def __init__(self, joydev, context, bits=None, mindelay=100):
+        Events.EventSource.__init__(self, file(joydev), context)
+        self._bits = bits
+        self._lastevent = 0
+        self._mindelay = mindelay
+
+    def readEvents(self):
+        n = 0
+        lev = self._lastevent
+        if self._bits:
+            time.sleep(0.0005)
+            jsevents = JSEvent.readMultipleFrom(self.fd())
+            for event in jsevents:
+                if event.type == JSEvent.TYPE_BUTTON and event.value == 1:
+                    self._lastevent = event.time
+                    if event.time - lev < self._mindelay : return []
+                    n = n | self._bits[event.number]
+            if n == 0 : return []
+            n -= 1
+        else:
+            event = JSEvent.readFrom(self.fd())
+            if event.type == JSEvent.TYPE_BUTTON and event.value == 1:
+                self._lastevent = event.time
+                if event.time - lev < self._mindelay : return []
+                n = event.number
+            else:
+                return []
+        return [Events.Event(self.context(), n)]
+    
+def register( viewmanager,
+              dispatcher,
+              context,
+              label,
+              numeric_switches,
+              alpha_switches,
+              x,
+              y,
+              size,
+              device,
+              bits = None ):
+    viewmanager.registerView( View(context, label, numeric_switches, alpha_switches, x, y, size) )
+    dispatcher.registerSource( Source(device, context, bits) )
diff --git a/Keyboard.py b/Keyboard.py
new file mode 100644 (file)
index 0000000..4c2a03b
--- /dev/null
@@ -0,0 +1,54 @@
+import Views, Events
+import sys, curses, curses.ascii
+
+class Source(Events.EventSource):
+
+    def __init__(self, context, win):
+        Events.EventSource.__init__(self, sys.stdin, context)
+        self._win = win
+
+    def readEvents(self):
+        return [Events.Event(self.context(),  self._win.getch())]
+
+
+class View(Views.View):
+
+    def __init__(self, context, label, x, y, dx, dy, size=9):
+        Views.View.__init__(self, context, label, x, y, dx, dy)
+        self._size = size
+
+    def updateView(self, bindings):
+        self.win().clear()
+        self.drawFrame()
+        keys = bindings.keys()
+        keys.sort()
+        column = 2
+        row = 2
+        for key in keys:
+            keyname = curses.keyname(key.code)
+            if keyname.startswith('KEY_'):
+                keyname = '%s' % keyname[4:10].lower()
+            if keyname[-2:-1] == '^':
+                keyname = '%sC-%s' % (keyname[:-2],keyname[-1:].lower())
+            if curses.ascii.isupper(ord(keyname[-1:])):
+                keyname = '%sS-%s' % (keyname[:-1],keyname[-1:].lower())
+            self.win().addstr(row, column, '%-6s %s'
+                              % (keyname, bindings[key].label[:self._size]))
+            row += 1
+            if row >= self.dy-2:
+                row = 2
+                column += self._size+10
+                if column+self._size+7 >= self.dx : break
+                self.win().vline(1,column-2,curses.ACS_VLINE,self.dy-2)
+        self.win().nooutrefresh()
+
+def register( viewmanager,
+              dispatcher,
+              context,
+              label,
+              x,
+              y,
+              dx,
+              dy ):
+    viewmanager.registerView( View(context, label, x,y,dx,dy) )
+    dispatcher.registerSource( Source(context, viewmanager.win()) )
diff --git a/Logger.py b/Logger.py
new file mode 100644 (file)
index 0000000..ab98615
--- /dev/null
+++ b/Logger.py
@@ -0,0 +1,32 @@
+import textwrap, time
+
+logger = None
+
+def init(viewmanager, x, y, dx, dy):
+    global logger
+    logger = Logger(viewmanager.win(), x, y, dx, dy)
+
+class Logger(object):
+
+    def __init__(self, win, x, y, dx, dy):
+        self._win = win.derwin(dy,dx,y,x)
+        self._win.border()
+        self._win.addstr(0,2,' Log ')
+        self._win.nooutrefresh()
+        self._textwin = self._win.derwin(dy-2,dx-3,1,2)
+        self._textwin.scrollok(1)
+        self._wrapper = textwrap.TextWrapper(width = dx-4,
+                                             subsequent_indent = ' '*4)
+        
+    def log(self, src, msg):
+        lines = self._wrapper.wrap(
+            '[%s] (%s) %s' %  (time.strftime("%H:%M:%S",time.localtime()),src, msg))
+        self._textwin.scroll(len(lines))
+        for i in range(len(lines)):
+            self._textwin.addstr(self._textwin.getmaxyx()[0]-len(lines)+i,0,
+                                 lines[i])
+        self._textwin.nooutrefresh()
+
+def log(src,msg):
+    if logger:
+        logger.log(src,msg)
diff --git a/Looper.py b/Looper.py
new file mode 100644 (file)
index 0000000..075e98b
--- /dev/null
+++ b/Looper.py
@@ -0,0 +1,54 @@
+import socket, struct
+import Actions, Logger
+
+def osc_string(s) :
+    n = (len(s)+1)%4
+    if n : return s+'\0'*(5-n)
+    else : return s+'\0'
+
+def osc_command(url,*args):
+    tag=","
+    argstring=""
+    for arg in args :
+        if type(arg) is int:
+            tag += 'i'
+            argstring += struct.pack(">I",arg)
+        elif type(arg) is str:
+            tag += 's'
+            argstring += osc_string(arg)
+        else:
+            raise RunstimeError, "Invalid OSC argument"
+    return osc_string(url) + osc_string(tag) + argstring
+
+class OSCSender(object):
+
+    def __init__(self, host, port):
+        self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        self._target = (host,port)
+
+    def command(self, path, *args):
+        if type(args[0]) in (tuple, list) : args = args[0]
+        Logger.log('osc','osc://%s:%s%s %s'
+                   % (self._target[0], self._target[1], path,
+                      ' '.join(map(repr,args))))
+        self._sock.sendto(osc_command(path, *args),self._target)
+        
+
+class Controller(object) :
+
+    def __init__(self, host, port):
+        self._sender = OSCSender(host,port)
+
+    def hit(self, command, index=0):
+        self._sender.command("/sl/%d/hit" % index, str(command))
+
+
+class Command(Actions.Action):
+
+    def __init__(self, name, controller, command):
+        Actions.Action.__init__(self, name)
+        self._controller = controller
+        self._command = command
+
+    def __call__(self, binding):
+        self._controller.hit(self._command)
diff --git a/Node.py b/Node.py
new file mode 100644 (file)
index 0000000..f62907e
--- /dev/null
+++ b/Node.py
@@ -0,0 +1,180 @@
+"""A Node is an object which autmatically manages it's owner(s).
+
+A Node should alweys be stored in attribute members declared as
+NodeReference or as member of a NodeMap or NodeList."""
+
+
+import weakref
+
+class Node(object):
+
+    def __init__(self):
+        self._owners = []
+
+    def _addOwner(self,owner):
+        self._owners.append(weakref.ref(owner,self._delRef))
+
+    def _delOwner(self,owner):
+        for i in range(len(self._owners)):
+            if self._owners[i]() is owner:
+                del self._owners[i]
+                return
+
+    def _delRef(self,ref):
+        for i in range(len(self._owners)):
+            if self._owners[i] is ref:
+                del self._owners[i]
+                return
+
+    def _owners(self):
+        return ( x() for x in self._owners )
+
+    def _owner(self):
+        if self._owners and self._owners[0]() is not None:
+            return self._owners[0]()
+        return None
+
+    owners = property(_owners, None, None, None)
+    owner = property(_owner, None, None, None)
+
+id_index = 0
+def _make_id():
+    global id_index
+    id_index += 1
+    return '__node_elt_%d' % id_index
+
+def NodeReference(id=None):
+    if id is None : id = _make_id()
+    return property(lambda self, id=id: getattr(self,id,None),
+                    lambda self, node, id=id: _NodeReference_set(self,node,id),
+                    None,
+                    None)
+
+def _NodeReference_set(self, node, id):
+    old = getattr(self,id,None)
+    if old is not None : old._delOwner(self)
+    setattr(self,id,node)
+    if node is not None : node._addOwner(self)
+
+
+class _NodeMap(dict):
+
+    def __init__(self, owner):
+        dict.__init__(self)
+        self._owner = weakref.ref(owner)
+
+    def __setitem__(self, key, node):
+        try: v = self[key]
+        except KeyError: pass
+        else: v._delOwner(self._owner())
+        dict.__setitem__(self, key, node)
+        node._addOwner(self._owner())
+
+    def __delitem__(self, key):
+        try: v = self[key]
+        except KeyError: pass
+        else: v._delOwner(self._owner())
+        dict.__delitem__(self,key)
+
+    def clear(self):
+        owner = self._owner()
+        for v in self.itervalues() : v._delOwner(owner)
+        dict.clear(self)
+
+    def setdefault(self, key, node):
+        if not self.has_key(key) : self[key] = node
+
+    def pop(self, key):
+        v = self[key]
+        del self[key]
+        return v
+
+    def popitem(self):
+        try: k = self.iterkeys().next()
+        except StopIteration: raise KeyError, 'popitem(): dictionary is empty'
+        return (k, self.pop(k))
+
+    def update(self, *args, **kws):
+        for k,v in dict(*args,**kws).iteritems() : self[k] = v
+
+def NodeMap(id=None):
+    if id is None : id = _make_id()
+    return property(lambda self, id=id : _NodeMap_get(self, id),
+                    lambda self, elts, id=id : _NodeMap_set(self, elts, id),
+                    None)
+
+def _NodeMap_get(self, id):
+    if not hasattr(self, id) : setattr(self, id, _NodeMap(self))
+    return getattr(self, id)
+
+def _NodeMap_set(self, elts, id):
+    if hasattr(self, id) : getattr(self, id).clear()
+    else                 : setattr(self, id, _NodeMap(self))
+    getattr(self, id).update(elts)
+    
+
+class _NodeList(list):
+
+    def __init__(self, owner):
+        list.__init__(self)
+        self._owner = weakref.ref(owner)
+
+    def __setitem__(self, index, node):
+        if type(index) is not slice:
+            self[index] # throw IndexError if index invalid
+            index = slice(index,index+1,None)
+            node = [ node ]
+        owner = self._owner()
+        for i in range(*index.indices(len(self))) : self[i]._delOwner(owner)
+        try:
+            list.__setitem__(self, index, node)
+        except ValueError:
+            for i in range(*index.indices(len(self))) : self[i]._addOwner(owner)
+            raise
+        else:
+            for n in node : n._addOwner(owner)
+
+    def __delitem__(self, index):
+        if type(index) is not slice:
+            self[index] # throw IndexError if index invalid
+            index = slice(index,index+1,None)
+        self.__setitem__(index,[])
+
+    def __setslice__(self, i, j, seq):
+        self[max(0, i):max(0, j):] = seq
+
+    def __delslice__(self, i, j):
+        del self[max(0, i):max(0, j):]
+
+    def append(self, node):
+        self[len(self):len(self)] = [node]
+
+    def extend(self, nodes):
+        self[len(self):len(self)] = nodes
+
+    def insert(self, index, node):
+        self[index:index] = [node]
+
+    def pop(self, index=None):
+        if index is None : index = len(self)-1
+        x = self[index]
+        del self[index]
+        return x
+
+    def remove(self, node):
+        del self[self.index(node)]
+
+def NodeList(id=None):
+    if id is None : id = _make_id()
+    return property(lambda self, id=id : _NodeList_get(self, id),
+                    lambda self, elts, id=id : _NodeList_set(self, elts, id),
+                    None)
+
+def _NodeList_get(self, id):
+    if not hasattr(self, id) : setattr(self, id, _NodeList(self))
+    return getattr(self, id)
+
+def _NodeList_set(self, elts, id):
+    if hasattr(self, id) : del getattr(self, id)[:]
+    else                 : setattr(self, id, _NodeList(self))
+    getattr(self, id).extend(elts)
diff --git a/Node.test.py b/Node.test.py
new file mode 100644 (file)
index 0000000..b34674d
--- /dev/null
@@ -0,0 +1,155 @@
+import Node
+import unittest
+
+class Owner(object):
+    child = Node.NodeReference()
+
+class OwnerMap(object):
+    children = Node.NodeMap()
+
+class OwnerList(object):
+    children = Node.NodeList()
+
+class Child(Node.Node):
+    pass
+
+class UnitTest(unittest.TestCase):
+
+    def testNode(self):
+        owner = Owner()
+        child1 = Child()
+        child2 = Child()
+        self.assertEqual(list(child1.owners),[])
+        self.assertEqual(list(child2.owners),[])
+        self.assertEqual(owner.child,None)
+
+        owner.child = child1
+        self.assertEqual(list(child1.owners), [owner])
+        self.assertEqual(list(child2.owners), [])
+        self.assertEqual(owner.child, child1)
+
+        owner.child = child2
+        self.assertEqual(list(child1.owners), [])
+        self.assertEqual(list(child2.owners), [owner])
+        self.assertEqual(owner.child, child2)
+
+        owner.child = None
+        self.assertEqual(list(child1.owners),[])
+        self.assertEqual(list(child2.owners),[])
+        self.assertEqual(owner.child,None)
+        
+        owner.child = child1
+        self.assertEqual(list(child1.owners), [owner])
+        owner = None
+        self.assertEqual(list(child1.owners),[])
+        self.assertEqual(owner, None)
+
+
+    def testNodeMap(self):
+        owner = OwnerMap()
+        child = Child()
+        owner.children['a'] = child
+        self.assertEqual(list(child.owners),[owner])
+        del owner.children['a']
+        self.assertEqual(list(child.owners),[])
+
+        child2 = Child()
+        owner.children.setdefault('a',child)
+        owner.children['b'] = child2
+        self.assertEqual(list(child.owners),[owner])
+        self.assertEqual(list(child2.owners),[owner])
+        owner.children['a'] = owner.children['b']
+        self.assertEqual(list(child.owners),[])
+        self.assertEqual(list(child2.owners),[owner,owner])
+        del owner.children['a']
+        self.assertEqual(list(child2.owners),[owner])
+        owner.children.clear()
+        self.assertEqual(list(child2.owners),[])
+
+        owner.children = dict(a=child, b=child2)
+        self.assertEqual(list(owner.children.pop('a').owners), [])
+        self.assertEqual(list(child2.owners), [owner])
+        self.assertEqual(owner.children.popitem(),('b',child2))
+        self.assertEqual(list(child2.owners), [])
+
+        owner.children['a'] = child
+        owner.children.update(b=child2)
+        self.assertEqual(list(child2.owners), [owner])
+        self.assertEqual(list(child.owners), [owner])
+        owner.children.update(a=child2)
+        self.assertEqual(list(child.owners), [])
+        self.assertEqual(list(child2.owners),[owner,owner])
+
+        del owner
+        self.assertEqual(list(child2.owners), [])
+
+
+    def testNodeList(self):
+        owner = OwnerList()
+        child1 = Child()
+        child2 = Child()
+
+        owner.children.append(child1)
+        owner.children.append(child2)
+        self.assertEqual(owner.children, [child1,child2])
+        self.assertEqual(list(child1.owners),[owner])
+        self.assertEqual(list(child2.owners),[owner])
+
+        owner.children.extend([child1,child2])
+        self.assertEqual(owner.children, [child1,child2,child1,child2])
+        self.assertEqual(list(child1.owners),[owner,owner])
+        self.assertEqual(list(child2.owners),[owner,owner])
+
+        self.assertRaises(ValueError, self._testNodeList_1, owner)
+        self.assertEqual(owner.children, [child1,child2,child1,child2])
+        self.assertEqual(list(child1.owners),[owner,owner])
+        self.assertEqual(list(child2.owners),[owner,owner])
+
+        owner.children[1:2] = []
+        self.assertEqual(list(child1.owners), [owner,owner])
+        self.assertEqual(list(child2.owners), [owner])
+        
+        owner.children[:] = []
+        self.assertEqual(list(child1.owners), [])
+        self.assertEqual(list(child2.owners), [])
+
+        owner.children[:] = [child1,child2]
+        self.assertEqual(list(child1.owners),[owner])
+        self.assertEqual(list(child2.owners),[owner])
+
+        del owner.children[0]
+        self.assertEqual(list(child1.owners),[])
+        self.assertEqual(list(child2.owners),[owner])
+
+        del owner.children[:]
+        self.assertEqual(list(child2.owners),[])
+
+        owner.children = [child1,child1]
+        owner.children.insert(1,child2)
+        self.assertEqual(owner.children,[child1,child2,child1])
+        self.assertEqual(list(child1.owners), [owner,owner])
+        self.assertEqual(list(child2.owners), [owner])
+
+        self.assertEqual(list(owner.children.pop().owners), [owner])
+        self.assertEqual(owner.children.pop(), child2)
+        self.assertEqual(owner.children, [child1])
+
+        owner.children.append(child2)
+        self.assertEqual(list(owner.children.pop(0).owners), [])
+        self.assertEqual(owner.children, [child2])
+        owner.children.append(child1)
+        owner.children.remove(child2)
+        self.assertEqual(list(child1.owners),[owner])
+        self.assertEqual(list(child2.owners),[])
+        self.assertEqual(owner.children, [child1])
+
+        del owner
+        self.assertEqual(list(child1.owners), [])
+        
+
+    def _testNodeList_1(self, owner):
+        owner.children[::2] = []
+        
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Process.py b/Process.py
new file mode 100644 (file)
index 0000000..379a704
--- /dev/null
@@ -0,0 +1,30 @@
+import Events, Logger
+import popen2, fcntl, os
+
+class Process(Events.EventSource):
+
+    def __init__(self, command, context):
+       self._proc = popen2.Popen4(command)
+       fcntl.fcntl(self._proc.fromchild, fcntl.F_SETFL, os.O_NDELAY)
+       Events.EventSource.__init__(self, self._proc.fromchild, context)
+       self._buffer = ""
+       Logger.log(self.context(),"launched %s" % command)
+
+    def readEvents(self):
+       data = (self._buffer + self._proc.fromchild.read()).split('\n')
+       self._buffer = data.pop(-1)
+       for line in data:
+           Logger.log(self.context(), line)
+       rv = self.poll()
+       if rv != -1 : self._term(rv)
+       return []
+
+    def terminate(self):
+        os.kill(self._proc.pid,signal.SIGTERM)
+       self._term(self._proc.wait())
+
+    def _term(self, rv):
+       if self._buffer : Logger.log(self.context(),self._buffer)
+       self._buffer = ""
+       Logger.log(self.context(), 'Terminated with exit code %s' % rv)
+       self.owner().unregisterSource(self)
diff --git a/Views.py b/Views.py
new file mode 100644 (file)
index 0000000..7f653ca
--- /dev/null
+++ b/Views.py
@@ -0,0 +1,109 @@
+"""Manage binding views.
+
+The Views module manages the on-screen keymap representations. A View
+is registered for a specific context. The view will be called with a
+list of all active bindings whenever the bindings change."""
+
+import Events, Node, curses
+
+class View(Node.Node):
+    def __init__(self, context, label, x, y, dx, dy):
+        Node.Node.__init__(self)
+        self._context = context
+        self._label = label
+        self._win = None
+        self.x, self.y, self.dx, self.dy = x, y, dx, dy
+    
+    def init(self):
+        self._win = self.owner.win().derwin(self.dy, self.dx, self.y, self.x)
+        self.drawFrame()
+
+    def drawFrame(self, subtitle=None):
+        label = ' %s ' % self._label
+        if subtitle:
+            label = '%s- %s ' % (label, subtitle)
+        self._win.border()
+        self._win.addstr(0,2,label)
+        self._win.nooutrefresh()
+        
+    def win(self):
+        return self._win
+
+    def updateView(self, bindings):
+        raise NotImplementedError
+        
+
+class WidgetView(View):
+
+    _widgets = Node.NodeMap()
+
+    def __init__(self, context, label, x, y, dx, dy):
+        View.__init__(self, context, label, x, y, dx, dy)
+
+    def init(self):
+        View.init(self)
+        for widget in self._widgets.itervalues():
+            widget.init()
+        
+    def add(self,widget):
+        if self.win() : raise RuntimeError, 'Cannot change active view'
+        self._widgets[widget.code] = widget
+
+    def updateView(self, bindings):
+        if bindings:
+            self.drawFrame(bindings.itervalues().next().keymap().keylist().path())
+        for code, widget in self._widgets.iteritems():
+            event = Events.Event(self._context,code)
+            if bindings.has_key(event):
+                widget.updateView(bindings[event])
+            else:
+                widget.updateView(None)
+
+
+class EventWidget(Node.Node):
+
+    def __init__(self, code, label, x, y, size=9):
+        Node.Node.__init__(self)
+        self.x, self.y, self.size = x, y, size
+        self._win = None
+        self.code = code
+        self.label = label
+
+    def init(self):
+        self._win = self.owner.win().derwin(2, self.size, self.y, self.x)
+        self._win.hline(0,0,curses.ACS_HLINE,self.size-1)
+        self._win.addstr(0,(self.size-1-min(self.size-3,len(self.label)))/2, self.label[:self.size-3])
+        self._win.nooutrefresh()
+
+    def updateView(self, binding):
+        if binding:
+            self._win.addstr(1,0, binding.label[:self.size-1].center(self.size-1), curses.A_BOLD)
+        else:
+            self._win.addstr(1, 0, ' '*(self.size-1))
+        self._win.nooutrefresh()
+
+
+class ViewManager(object):
+
+    _views = Node.NodeMap()
+
+    def __init__(self, keylist, dispatcher, win):
+        self._win = win
+        keylist.registerCallback(self.updateViews)
+        dispatcher.registerCallback(self.beforeIdle)
+
+    def win(self):
+        return self._win
+
+    def registerView(self, view):
+        self._views[view._context] = view
+        view.init()
+
+    def updateViews(self, keylist):
+        bindings = keylist.bindingsByContext()
+        for context, keys in bindings.iteritems():
+            if self._views.has_key(context):
+                self._views[context].updateView(keys)
+
+    def beforeIdle(self):
+        self._win.refresh()
diff --git a/audiocontroller b/audiocontroller
new file mode 100755 (executable)
index 0000000..70e03ff
--- /dev/null
@@ -0,0 +1,5 @@
+#!/usr/bin/python2.4
+
+import main
+
+main.main()
diff --git a/config.py b/config.py
new file mode 100644 (file)
index 0000000..d4b5abf
--- /dev/null
+++ b/config.py
@@ -0,0 +1,132 @@
+import Bindings, Actions, Views, Events, Logger
+from Bindings import Binding
+from Actions import Actions as Action, action
+from Views import EventWidget
+from Events import Event
+import main
+import Joyboard, Keyboard, Process
+import Looper
+import sys, curses
+from curses.ascii import alt, ctrl
+
+def shift(letter) : return ord(chr(letter).upper())
+def key(letter) : return ord(letter.lower())
+
+###########################################################################
+# Setup views and controllers
+#
+# Display size: 77x17
+
+Logger.init(main.viewmanager, 38, 0, 39, 10)
+
+Joyboard.register(
+    viewmanager = main.viewmanager,
+    dispatcher = main.dispatcher,
+
+    context          = 'jb0',
+    label            = 'Foot Switch',
+    numeric_switches =  10,
+    alpha_switches   =   5,
+    x                =   2,
+    y                =  10,
+    size             =   6,
+
+    device           = '/dev/input/js0',
+    bits             = { 1:1, 3:2, 2:4, 0:8 }
+)
+
+Keyboard.register(
+    viewmanager = main.viewmanager,
+    dispatcher = main.dispatcher,
+
+    context          = 'kbd',
+    label            = 'Key Bindings',
+    x                =  0,
+    y                =  0,
+    dx               = 38,
+    dy               = 10
+)
+
+jackd = Process.Process("jackd -d alsa -S -r 44100 -p 256",'jack')
+
+###########################################################################
+# Global keymap and auxilary actions
+
+global_map = Bindings.KeyMap()
+
+@action
+def quit(binding):
+    sys.exit(0)
+
+global_map.add( Binding( Event('jb0',0), '()', Actions.Nop() ) )
+global_map.add( Binding( Event('jb0',1), '()', Actions.Nop() ) )
+global_map.add( Binding( Event('jb0',10), '()', Actions.Nop() ) )
+
+global_map.add( Binding( Event('kbd',key('q')), 'Quit', Action['quit'] ) )
+
+Action.register( Actions.ChangeBindingsRelative( 'unset_this_map', 0, [] ) )
+
+###########################################################################
+# Looper mode
+
+looper_main_map = Bindings.KeyMap( 'Looper' )
+looper_aux_map = Bindings.KeyMap( 'Aux' )
+
+Action.register( Actions.ChangeBindingsAbsolute('set_mode_looper', 1, [looper_main_map]) )
+Action.register( Actions.ChangeBindingsRelative('looper_set_aux_map', 1, [looper_aux_map]) )
+
+looper = Looper.Controller('localhost',9951)
+
+Action.register( Looper.Command('looper_record', looper, 'record') )
+Action.register( Looper.Command('looper_overdub', looper, 'overdub') )
+Action.register( Looper.Command('looper_multiply', looper, 'multiply') )
+Action.register( Looper.Command('looper_mute', looper, 'mute') )
+Action.register( Looper.Command('looper_undo', looper, 'undo') )
+Action.register( Looper.Command('looper_redo', looper, 'redo') )
+Action.register( Looper.Command('looper_trigger', looper, 'trigger') )
+Action.register( Looper.Command('looper_insert', looper, 'insert') )
+Action.register( Looper.Command('looper_replace', looper, 'replace') )
+Action.register( Looper.Command('looper_substitute', looper, 'substitute') )
+Action.register( Looper.Command('looper_reverse', looper, 'reverse') )
+Action.register( Looper.Command('looper_oneshot', looper, 'oneshot') )
+
+looper_main_map.add ( Binding( Event('jb0',2),  'Rec',     Action['looper_record'] ) )
+looper_main_map.add ( Binding( Event('jb0',3),  'Over',    Action['looper_overdub'] ) )
+looper_main_map.add ( Binding( Event('jb0',4),  'Mult',   Action['looper_multiply'] ) )
+looper_main_map.add ( Binding( Event('jb0',5),  'Mute',       Action['looper_mute'] ) )
+looper_main_map.add ( Binding( Event('jb0',6),  'Undo',       Action['looper_undo'] ) )
+looper_main_map.add ( Binding( Event('jb0',7),  'Redo',       Action['looper_redo'] ) )
+looper_main_map.add ( Binding( Event('jb0',8),  'Trig',    Action['looper_trigger'] ) )
+looper_main_map.add ( Binding( Event('jb0',9),  'Aux',        Action['looper_set_aux_map'] ) )
+
+looper_aux_map.add  ( Binding( Event('jb0',2),  'Ins',     Action['looper_insert'] ) )
+looper_aux_map.add  ( Binding( Event('jb0',3),  'Repl',    Action['looper_replace'] ) )
+looper_aux_map.add  ( Binding( Event('jb0',4),  'Subst', Action['looper_substitute'] ) )
+looper_aux_map.add  ( Binding( Event('jb0',6),  'Rev',    Action['looper_reverse'] ) )
+looper_aux_map.add  ( Binding( Event('jb0',7),  'Once',    Action['looper_oneshot'] ) )
+looper_aux_map.add  ( Binding( Event('jb0',9),  'Main',       Action['unset_this_map'] ) )
+
+###########################################################################
+# MegaPedal mode
+
+megapedal_main_map = Bindings.KeyMap( 'MegaPedal' )
+
+Action.register( Actions.ChangeBindingsAbsolute('set_mode_megapedal', 1, [megapedal_main_map]) )
+
+megapedal_main_map.add ( Binding( Event('jb0',11), 'Looper', Action['set_mode_looper'] ) )
+global_map.add ( Binding( Event('jb0',11), 'MegaPedal', Action['set_mode_megapedal'] ) )
+
+###########################################################################
+# RythmBox mode
+
+rythmbox_main_map = Bindings.KeyMap( 'RythmBox' )
+
+Action.register( Actions.ChangeBindingsAbsolute('set_mode_rythmbox', 1, [rythmbox_main_map]) )
+
+rythmbox_main_map.add ( Binding( Event('jb0',11), 'MegaPedal', Action['set_mode_megapedal'] ) )
+global_map.add ( Binding( Event('jb0',11), 'RythmBox', Action['set_mode_rythmbox'] ) )
+
+###########################################################################
+
+main.keylist.append(global_map)
+main.keylist.append(looper_main_map)
diff --git a/main.py b/main.py
new file mode 100644 (file)
index 0000000..caa6418
--- /dev/null
+++ b/main.py
@@ -0,0 +1,31 @@
+import Bindings, Events, Views, curses.wrapper
+
+keylist = None
+dispatcher = None
+viewmanager = None
+stdscr = None
+
+
+def run(scr):
+
+    global keylist
+    global dispatcher
+    global viewmanager
+    global stdscr
+    
+    keylist = Bindings.KeyList()
+    dispatcher = Events.Dispatcher(keylist)
+    viewmanager = Views.ViewManager(keylist, dispatcher, scr)
+    stdscr = scr
+
+    import config
+
+    curses.raw()
+    stdscr.keypad(1)
+    try: curses.curs_set(0)
+    except: pass
+    dispatcher.run()
+
+
+def main():
+    curses.wrapper(run)