Changeset - 0761cdd5bff0
[Not reviewed]
default
0 5 0
drewp@bigasterisk.com - 8 months ago 2024-05-31 18:52:47
drewp@bigasterisk.com
fix delay on setting midi, so it doesn't grab faders when you're sliding them
5 files changed with 79 insertions and 40 deletions:
0 comments (0 inline, 0 general)
src/light9/midifade/eventqueue.py
Show inline comments
 
import asyncio
 
import logging
 
import time
 
import traceback
 
from dataclasses import dataclass
 

	
 
@@ -20,8 +21,9 @@ class Event:
 
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 @@ class EventQueue:
 
            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,
src/light9/midifade/mididevs.py
Show inline comments
 
@@ -8,12 +8,13 @@ from rdfdb.syncedgraph.syncedgraph impor
 

	
 
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 @@ def listenToMidiInputs(q: EventQueue):
 
            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
src/light9/midifade/midifade.py
Show inline comments
 
@@ -82,13 +82,14 @@ async def main():
 
    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()
src/light9/midifade/pages.py
Show inline comments
 
@@ -21,7 +21,11 @@ class Pages:
 
        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']))
src/light9/midifade/writeback.py
Show inline comments
 
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
0 comments (0 inline, 0 general)