# HG changeset patch # User drewp@bigasterisk.com # Date 2024-05-31 18:52:47 # Node ID 0761cdd5bff04d0dce314d4a3fcbf9d6bece00dd # Parent d1f86109e3cc1f219b054a7cf57f1f509ff6e478 fix delay on setting midi, so it doesn't grab faders when you're sliding them diff --git a/src/light9/midifade/eventqueue.py b/src/light9/midifade/eventqueue.py --- a/src/light9/midifade/eventqueue.py +++ b/src/light9/midifade/eventqueue.py @@ -1,5 +1,6 @@ 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, diff --git a/src/light9/midifade/mididevs.py b/src/light9/midifade/mididevs.py --- a/src/light9/midifade/mididevs.py +++ b/src/light9/midifade/mididevs.py @@ -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 diff --git a/src/light9/midifade/midifade.py b/src/light9/midifade/midifade.py --- a/src/light9/midifade/midifade.py +++ b/src/light9/midifade/midifade.py @@ -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() diff --git a/src/light9/midifade/pages.py b/src/light9/midifade/pages.py --- a/src/light9/midifade/pages.py +++ b/src/light9/midifade/pages.py @@ -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'])) diff --git a/src/light9/midifade/writeback.py b/src/light9/midifade/writeback.py --- a/src/light9/midifade/writeback.py +++ b/src/light9/midifade/writeback.py @@ -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