changeset 2441:0761cdd5bff0

fix delay on setting midi, so it doesn't grab faders when you're sliding them
author drewp@bigasterisk.com
date Fri, 31 May 2024 11:52:47 -0700
parents d1f86109e3cc
children a8281a147cb6
files src/light9/midifade/eventqueue.py src/light9/midifade/mididevs.py src/light9/midifade/midifade.py src/light9/midifade/pages.py src/light9/midifade/writeback.py
diffstat 5 files changed, 79 insertions(+), 40 deletions(-) [+]
line wrap: on
line diff
--- a/src/light9/midifade/eventqueue.py	Thu May 30 01:08:45 2024 -0700
+++ b/src/light9/midifade/eventqueue.py	Fri May 31 11:52:47 2024 -0700
@@ -1,5 +1,6 @@
 import asyncio
 import logging
+import time
 import traceback
 from dataclasses import dataclass
 
@@ -20,8 +21,9 @@
 class EventQueue:
     """midi events come in fast; graph consumes them slower"""
 
-    def __init__(self, MAX_SEND_RATE: float, onMessage) -> None:
+    def __init__(self, MAX_SEND_RATE: float, _lastMidiInTime, onMessage) -> None:
         self.MAX_SEND_RATE = MAX_SEND_RATE
+        self._lastMidiInTime = _lastMidiInTime
         self.onMessage = onMessage
         self.newEvents: asyncio.Queue[Event] = asyncio.Queue()
 
@@ -33,6 +35,7 @@
             d = message.dict()
             ev = Event(dev, d['type'], d['control'], d['value'])
             log.info(f'enqueue {ev} {"*" * ev.value}')
+            self._lastMidiInTime[ev.control] = time.time()
             mainThreadLoop.call_soon_threadsafe(
                 self.newEvents.put_nowait,
                 ev,
--- a/src/light9/midifade/mididevs.py	Thu May 30 01:08:45 2024 -0700
+++ b/src/light9/midifade/mididevs.py	Fri May 31 11:52:47 2024 -0700
@@ -8,12 +8,13 @@
 
 log = logging.getLogger()
 
-_csb=[]
-openPorts = []
+_openPorts = []
+
+
 def listenToMidiInputs(q: EventQueue):
     """put midi events on EventQueue (presumably mido does this in a bg
     thread)"""
-    
+
     for inputName in mido.get_input_names():  # type: ignore
         if inputName.startswith('Keystation'):
             dev = "keystation"
@@ -25,16 +26,15 @@
             continue
         log.info(f'listening on input {inputName} {dev=}')
         cb = q.callbackFor(dev)
-        _csb.append(cb)
-        openPorts.append(mido.open_input(  # type: ignore
+        _openPorts.append(mido.open_input(  # type: ignore
             inputName,  #
             callback=cb))
 
 
-def connectToMidiOutput(graph: SyncedGraph, pages: Pages, _lastSet: dict[int, int]):
+def connectToMidiOutput(graph: SyncedGraph, pages: Pages, _lastSet: dict[int, int], _lastMidiInTime: dict[int, float]):
     for outputName in mido.get_output_names():  # type: ignore
         if outputName.startswith('BCF2000'):
             bcf_out = mido.open_output(outputName)  # type: ignore
-            wb = WriteBackFaders(graph, pages, bcf_out, _lastSet)
-            graph.addHandler(wb.update)
+            wb = WriteBackFaders(graph, pages, bcf_out, _lastSet, _lastMidiInTime, {})
+            graph.addHandler(wb.onGraphPatch)
             break
--- a/src/light9/midifade/midifade.py	Thu May 30 01:08:45 2024 -0700
+++ b/src/light9/midifade/midifade.py	Fri May 31 11:52:47 2024 -0700
@@ -82,13 +82,14 @@
     MAX_SEND_RATE = 50
 
     _lastSet = {}  #midictlchannel:value7bit
+    _lastMidiInTime = {} # midictlchannel:time
 
     ctx = URIRef(showUri() + '/fade')
     graph = SyncedGraph(networking.rdfdb.url, "midifade")
     pages = Pages(graph, ctx)
-    queue = EventQueue(MAX_SEND_RATE, partial(onMessage, graph, pages, ctx, _lastSet))
+    queue = EventQueue(MAX_SEND_RATE, _lastMidiInTime, partial(onMessage, graph, pages, ctx, _lastSet))
     listenToMidiInputs(queue)
-    connectToMidiOutput(graph, pages, _lastSet)
+    connectToMidiOutput(graph, pages, _lastSet, _lastMidiInTime)
     graph.addHandler(pages.compileCurrents)
     # todo: serve fps metrics, at least
     await queue.run()
--- a/src/light9/midifade/pages.py	Thu May 30 01:08:45 2024 -0700
+++ b/src/light9/midifade/pages.py	Fri May 31 11:52:47 2024 -0700
@@ -21,7 +21,11 @@
         self.currentFaders = {}  # midi control channel num : FaderUri
 
     def getChansToFaders(self) -> dict[int, URIRef]:
-        fadePage = self.getCurMappedPage()
+        try:
+            fadePage = self.getCurMappedPage()
+        except ValueError:
+            log.warning("skip", exc_info=True)
+            return {}
         ret = []
         for f in self.graph.objects(fadePage, L9.fader):
             columnLit = cast(Literal, self.graph.value(f, L9['column']))
--- a/src/light9/midifade/writeback.py	Thu May 30 01:08:45 2024 -0700
+++ b/src/light9/midifade/writeback.py	Fri May 31 11:52:47 2024 -0700
@@ -1,57 +1,88 @@
 import asyncio
+from functools import partial
 import logging
 from dataclasses import dataclass
+import time
 from typing import cast
 
+from light9.typedgraph import typedValue
 import mido
 from debouncer import DebounceOptions, debounce
 from light9.midifade.pages import Pages
 from light9.namespaces import L9
 from rdfdb.syncedgraph.syncedgraph import SyncedGraph
-from rdflib import Literal
+from rdflib import Literal, URIRef
 
 log = logging.getLogger()
 
 
+class FaderSyncer:
+    """get this midi output set to the right value
+    
+    todo: note that these could be active at the moment we switch fader pages,
+    which might result in the wrong output
+    """
+
+    def __init__(self, fader: URIRef, getHwValue, getGraphValue, setHwValue, getLastMidiInTime):
+        self.fader = fader
+        self.getHwValue = getHwValue
+        self.getGraphValue = getGraphValue
+        self.setHwValue = setHwValue
+        self.getLastMidiInTime = getLastMidiInTime
+        self.done = False
+        asyncio.create_task(self.task())
+
+    async def task(self):
+        try:
+            await self._waitForPauseInInput()
+            
+            goal = self.getGraphValue()
+            hw = self.getHwValue()
+            if abs(goal - hw) > 2:
+                log.info(f'hw at {hw} {goal=} -> send to hw')
+                self.setHwValue(goal)
+        finally:
+            self.done = True
+
+    async def _waitForPauseInInput(self):
+        while True:
+            if time.time() > self.getLastMidiInTime() + 1:
+                return
+            await asyncio.sleep(.05)
+
+
+
 @dataclass
 class WriteBackFaders:
     graph: SyncedGraph
     pages: Pages
     bcf_out: mido.ports.BaseOutput
     _lastSet: dict[int, int]
+    _lastMidiInTime: dict[int, float]
+    _syncer: dict[int, FaderSyncer]
 
     def getCurrentValue(self, f):
         return self._lastSet.get(f, 0)
 
-    def update(self):
-        try:
-            asyncio.create_task(self._update())
-        except ValueError as e:
-            log.warning(repr(e))
+    def onGraphPatch(self):
+        for midi_ctl_addr, f in self.pages.getChansToFaders().items():
+            fset = typedValue(URIRef, self.graph, f, L9.setting)
+            self.graph.value(fset, L9.value)  # dependency for handler
 
-    async def _update(self):
-        # to make this work again:
-        #  - track the goal of all sliders
-        #  - in a debounced handler, sendToBcf
-        return
-        g = self.graph
-        nupdated = 0
-        m = self.pages.getChansToFaders()
-        for midi_ctl_addr, f in m.items():
-            fset = g.value(f, L9.setting)
-            # could split this to a separate handler per fader
+            existingSyncer = self._syncer.get(midi_ctl_addr, None)
+            if not existingSyncer or existingSyncer.done:
+                self._syncer[midi_ctl_addr] = FaderSyncer(
+                    f,
+                    partial(self.getCurrentValue, midi_ctl_addr),
+                    partial(self.getMidiValue, fset),
+                    partial(self.sendToBcf, midi_ctl_addr),
+                    partial(self._lastMidiInTime.get, midi_ctl_addr, 0),
+                )
+
+    def getMidiValue(self, fset: URIRef) -> int:
+        with self.graph.currentState() as g:
             value = cast(Literal, g.value(fset, L9.value)).toPython()
-            hwcurrent = self.getCurrentValue(midi_ctl_addr)
-            hwgoal = int(value * 127)
-            maxValueDiff = 3
-            # todo: 3 is no good; we should correct after a short time of no
-            # movement
-            if abs(hwcurrent - hwgoal) > maxValueDiff:
-                log.info(f'writing back to {f} {hwcurrent=} {hwgoal=}')
-                self.sendToBcf(midi_ctl_addr, hwgoal)
-                nupdated += 1
-        if nupdated > 0:
-            log.info(f'wrote to {nupdated} of {len(m)} mapped faders')
+            return int(value * 127)
 
     def sendToBcf(self, control: int, value: int):
         self._lastSet[control] = value