Audio/AudioControl: Lots of fixes
[audiocontrol.git] / Joyboard.py
index ef06b87..ab0621e 100644 (file)
@@ -33,7 +33,7 @@ class JSEvent:
     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)
 
@@ -106,16 +106,86 @@ class Source(Events.EventSource):
             return [Events.ControlEvent(self.context(), 'p%d' % number, value)]
         return []
 
+# New Controller state machine:
+#
+# First, we work in two steps: The controller gets an internal
+# controller value which is an integer between 0 and 100. All
+# operations change this value which is then converted to the output
+# value.
+#
+# The controller position is managed as a signed integer between -2
+# and 2.
+#
+# The current state is managed in a state variable. There are the
+# following states:
+#
+#   INIT    In this state, the controller is waiting for the initial
+#           parameter value.
+#
+#   RUNNING The value is currently being changed. The speed and
+#           direction are given by the controller position. If the
+#           current controller position changes to 0 or to the
+#           opposite sign, the state is switched to WAIT.
+#
+#   WAIT    The state machine waits for the controller to enter the
+#           center position. Only when the controller has been in the
+#           center position for some time, it will enter the IDLE
+#           state. It may however go to RUNNING, if the controller
+#           position is again changed to have the same sign t had
+#           before entering WAIT. On entering this state, the
+#           controller position is checked. If the position is 0 now
+#           or if it is changed to become 0, a timeout is started. If
+#           the controller is not changed within this timeout, we
+#           enter IDLE. Any change of the controlle value terminates
+#           thiw timeout.
+#
+#   SWAIT   Wait at a stop. The controller will stay in this state
+#           until the controller position is changed to 0 or the opposite
+#           sign. It then enters the WAIT state.
+#
+#   IDLE    In this state, nothing happens, however, if the controller
+#           position changes from 0, the state machine enters the
+#           running state.
+#
+# Events are:
+#
+#   ev_controllerChanged  The controller position changes
+#
+#   ev_timeout            The timeout handle got called
+#
+# The stop positions are handled by converting them to appropriate
+# integer values. When such a value is reached or crossed, the value
+# is set to the stop value and the state machine enters the SWAIT
+# state.
+#
+# When converting the integer value to the return float value, we
+# first check, wether the integer value is one of the stop values. In
+# this case, the associated float value is returned otherweise ahe
+# integer is scaled to the correct float range.
 
 class Controller(Views.View):
 
+    ########################################
+    # external API
+
     def __init__(self, context, name, x, y, dx, dy, keylist, dispatcher,
                  controller, source, low, high):
         Views.View.__init__(self, context, name, x, y, dx, dy)
+
+        # External value interface
         self._valueEvent = None
         self._setCommand = None
         self._valueCommand = None
+
+        # Parameter information
         self._parameter = None
+        self._min = None
+        self._max = None
+        self._stops = None
+        self._rawStops = None
+        self._steps = 40
+
+        # Controller keymap stuff
         self._keylist = keylist
         self._keymap = Bindings.KeyMap()
         if source:
@@ -125,19 +195,14 @@ class Controller(Views.View):
         self._keylist.prepend(self._keymap)
         if source:
             source.registerController(controller, low, high)
+
+        # Working variables
         self._dispatcher = dispatcher
-        self._min = None
-        self._max = None
-        self._stops = None
         self._value = None
+        self._rawValue = None
         self._controlValue = None
-
-    def updateView(self, bindings):
-        pass
-
-    def init(self):
-        Views.View.init(self)
-        self._redraw()
+        self._controlSign = None
+        self._state = None
 
     def assign(self, parameter,  setCommand, valueCommand, valueEvent, min, max, stops=[]):
         self._keylist.removeMap(self._keymap)
@@ -150,79 +215,202 @@ class Controller(Views.View):
         self._min = min
         self._max = max
         self._stops = stops
+        self._rawStops = [ self._rawFromCooked(x) for x in self._stops ]
         if self._valueEvent:
             self._keymap.add( Bindings.Binding( self._valueEvent, '',
                                                 Actions.Command('updateValue',
                                                                 self._updateValue)) )
         self._keylist.prepend(self._keymap)
         self._value = None
-        if self._valueEvent:
-            self._valueCommand()
-        else:
-            self._value = self._valueCommand()
+        self._rawValue = 0
+        self._controlValue = None
+        self._controlSign = None
+        self._state = self.S_INIT
+        self._redraw()
+        self._getValue()
+
+    def stepValue(self, direction):
+        Logger.log('ctl','direction = %d' % direction)
+        self._rawValue += max(-1,min(1,direction))
+        if self._rawValue > self._steps:
+            self._rawValue = self._steps
+        elif self._rawValue < 0:
+            self._rawValue = 0
+
+        Logger.log('ctl','rawValue = %d' % self._rawValue)
+        self._setValue(self._cookedFromRaw(self._rawValue))
+
+        return self._rawValue in self._rawStops
+
+    ########################################
+    # Base class interface implementation
+
+    def updateView(self, bindings):
+        pass
+
+    def init(self):
+        Views.View.init(self)
         self._redraw()
 
+    ########################################
+    # External event callbacks
+
     def _updateValue(self, binding):
+        # Called to return the value after executing the valueCommand
+        # (when valueCommand initiates an async value get like via
+        # osc)
         event = self._dispatcher.currentEvent()
-        self._value = event.value
+        self.__updateValue(event.value)
+        self._updateWindow()
+
+    def __updateValue(self, value):
+        self._value = value
+        if self._state == self.S_INIT:
+            self._rawValue = self._rawFromCooked(self._value)
+            self._setState(self.S_SWAIT)
         self._redrawValue()
+        self._redrawState()
+        self._updateWindow()
 
     def _controllerChanged(self, binding):
+        # Called, whenever the controller position changes
         event = self._dispatcher.currentEvent()
-        Logger.log('ctl',"value = %d" % event.value)
-        self._controlValue = event.value
-        if self._controlValue >= 999 or self._controlValue <= 1:
-            self._dispatcher.setIdleCallback(self._changeValue,75)
-        elif self._controlValue >= 700 or self._controlValue <= 200:
-            self._dispatcher.setIdleCallback(self._changeValue,200)
+        if event.value >= 999:
+            newControlValue = 2
+        elif event.value >= 700:
+            newControlValue = 1
+        elif event.value >= 200:
+            newControlValue = 0
+        elif event.value >= 1:
+            newControlValue = -1
         else:
-            self._dispatcher.unsetIdleCallback()
-        self._redrawController()
+            newControlValue = -2
+        Logger.log('ctl',"controlValue = %d" % newControlValue)
+        if self._controlValue is None or self._controlValue != newControlValue:
+            self._controlValue = newControlValue
+            self._ev_controllerChanged()
 
-    def _changeValue(self):
-        if self._value is None: return
-        if self._controlValue > 500:
-            self.stepValue(+1)
-        else:
-            self.stepValue(-1)
+    ########################################
+    # Internal API
 
-    def stepValue(self, direction):
-        if direction > 0:
-            newValue = self._value + (self._max - self._min) / (3000/50)
-            crossed = [ x for x in self._stops if x > self._value*1.0001 and x <= newValue ]
-        elif direction < 0:
-            newValue = self._value - (self._max - self._min) / (3000/50)
-            crossed = [ x for x in self._stops if x < self._value/1.0001 and x >= newValue ]
-        if newValue >= self._max:
-            crossed = [ self._max ]
-        elif newValue <= self._min:
-            crossed = [ self._min ]
-        if crossed:
-            newValue = crossed[0]
-            self._dispatcher.unsetIdleCallback()
-            self._setCommand(newValue)
-            # Hmm ... why does value_command not work sometimes ??
-            self._value = newValue
-            self._redrawValue()
+    def _setValue(self, value):
+        self._setCommand(value)
+        self._getValue()
+
+    def _getValue(self):
+        if self._valueEvent:
+            self._valueCommand()
         else:
-            self._setCommand(newValue)
-            if self._valueEvent:
-                self._valueCommand()
+            self.__updateValue(self._valueCommand())
+
+    def _rawFromCooked(self, value):
+        return min(self._steps,max(0,int(float(self._steps)*(value-self._min)/(self._max-self._min)+.5)))
+
+    def _cookedFromRaw(self, value):
+        try:
+            return self._stops[ self._rawStops.index(value) ]
+        except ValueError:
+            return (self._min*(self._steps-value) + self._max*value)/self._steps
+
+    ########################################
+    # State machine
+
+    S_INIT = 0
+    S_RUNNING = 1
+    S_WAIT = 2
+    S_SWAIT = 3
+    S_IDLE = 4
+
+    def _setState(self, state):
+        self._state = state
+        self._redrawState()
+
+    def _ev_controllerChanged(self):
+        self._redrawController()
+
+        if self._state in (self.S_IDLE, self.S_RUNNING):
+            if abs(self._controlValue)==1:
+                self._dispatcher.setIdleCallback(self._ev_timeout,200)
+                self._setState(self.S_RUNNING)
+            elif self._controlValue != 0:
+                self._dispatcher.setIdleCallback(self._ev_timeout,75)
+                self._setState(self.S_RUNNING)
+            else:
+                self._dispatcher.unsetIdleCallback()
+                if self._state == self.S_RUNNING:
+                    self._setState(self.S_WAIT)
+                    self._ev_controllerChanged()
+                else:
+                    self._setState(self.S_IDLE)
+
+        elif self._state == self.S_SWAIT:
+            if self._controlValue == 0 or self._controlSign and self._controlSign*self._controlValue < 0:
+                self._setState(self.S_WAIT)
+                self._ev_controllerChanged()
+
+        elif self._state == self.S_WAIT:
+            if self._controlValue == 0:
+                self._dispatcher.setIdleCallback(self._ev_timeout,300)
+            else:
+                self._dispatcher.unsetIdleCallback()
+                if self._controlSign and self._controlSign*self._controlValue > 0:
+                    self._setState(self.S_RUNNING)
+                    self._ev_controllerChanged()
+
+        self._updateWindow()
+
+    def _ev_timeout(self):
+        if self._state == self.S_RUNNING:
+            self._controlSign = max(-1, min(1, self._controlValue))
+            if self._controlSign == 0:
+                # Cannot happen but ...
+                self._setState(self.S_IDLE)
             else:
-                self._value = self._valueCommand()
-                self._redrawValue()
+                if self.stepValue(self._controlSign):
+                    self._setState(self.S_SWAIT)
+
+        elif self._state == self.S_WAIT:
+            self._setState(self.S_IDLE)
+            self._dispatcher.unsetIdleCallback()
+
+        self._updateWindow()
+
+    ########################################
+    # Drawing
+
+    # +----------+
+    # |   Name   |
+    # |  -       |
+    # |  |       |
+    # |  |-1.234 |
+    # |-S|       |
+    # |  |       |
+    # |  |       |
+    # |  -       |
+    # +----------+
+
 
     def _redraw(self):
         height, width = self.win().getmaxyx()
         if self._parameter is not None:
             self.win().addstr(1,2,self._parameter[:width-4].ljust(width-4), curses.A_BOLD)
-        self._redrawValue(False)
+        self.win().addch(3,3,curses.ACS_TTEE)
+        self.win().addch(height-3,3,curses.ACS_BTEE)
+        self.win().addch(height/2,1,'-')
+        self._redrawValue()
+        self._redrawController()
+        self._updateWindow()
+
+    def _redrawState(self):
         self._redrawController()
 
+    def _updateWindow(self):
+        self.win().refresh()
+
     def _flt(self, value, width):
         return ('%.3f' % value)[:width].ljust(width)
 
-    def _redrawValue(self, refresh=True):
+    def _redrawValue(self):
         height, width = self.win().getmaxyx()
         pos = None
         if self._value is not None:
@@ -247,25 +435,17 @@ class Controller(Views.View):
             self.win().addstr(height-3, 5, "".ljust(width-7))
         if self._min is not None:
             self.win().addstr(height-2,2,self._flt(self._min,width-7))
-        
-        if refresh:
-            self.win().refresh()
 
     def _redrawController(self, refresh=True):
+        if self._controlValue is None: return
         height, width = self.win().getmaxyx()
-        if self._controlValue is not None and self._controlValue >= 700:
-            self.win().addch(3,3,curses.ACS_UARROW)
-        else:
-            self.win().addch(3,3,curses.ACS_TTEE)
-        if self._controlValue is not None and self._controlValue <= 200:
-            self.win().addch(height-3,3,curses.ACS_DARROW)
-        else:
-            self.win().addch(height-3,3,curses.ACS_BTEE)
-            
-        if refresh:
-            self.win().refresh()
-    
-
+        pos = height - 3 - int( (self._controlValue + 2) * (height-6) / 4 + .5 )
+        for row in range(3,height-2):
+            if row == pos:
+                self.win().addch(row,2,('N','R','W','S','I')[self._state])
+            else:
+                self.win().addch(row,2,' ')
+        
 class StepController(Actions.Action):
 
     def __init__(self, name, controller, direction):
@@ -275,7 +455,7 @@ class StepController(Actions.Action):
 
     def __call__(self, binding):
         self._controller.stepValue(self._direction)
-        
+
 
 def register( viewmanager,
               dispatcher,