changeset 2343:af38643cffd4

jam in fader page control, fix _lastSet feedback
author drewp@bigasterisk.com
date Sat, 03 Jun 2023 13:10:24 -0700
parents 81d5b6d97ed3
children 83135b0c8bba
files light9/midifade/midifade.py
diffstat 1 files changed, 95 insertions(+), 39 deletions(-) [+]
line wrap: on
line diff
--- a/light9/midifade/midifade.py	Sat Jun 03 12:09:09 2023 -0700
+++ b/light9/midifade/midifade.py	Sat Jun 03 13:10:24 2023 -0700
@@ -5,12 +5,13 @@
 import asyncio
 import logging
 import traceback
-from typing import Dict, List
+from typing import Dict, List, cast
+from light9.effect.edit import clamp
 
 import mido
 from rdfdb.syncedgraph.syncedgraph import SyncedGraph
-from rdflib import Literal, URIRef
-
+from rdflib import RDF, ConjunctiveGraph, Literal, URIRef
+from rdfdb.syncedgraph.readonly_graph import ReadOnlyConjunctiveGraph
 from light9 import networking
 from light9.namespaces import L9
 from light9.newtypes import decimalLiteral
@@ -20,10 +21,70 @@
 mido.set_backend('alsa_midi.mido_backend')
 MAX_SEND_RATE = 20
 
-_lastSet = {}  #Fader:value
+_lastSet = {}  #midictlchannel:value7bit
+
+currentFaders = {}  # midi control channel num : FaderUri
+ctx = URIRef(showUri() + '/fade')
+
+def compileCurrents(graph):
+    currentFaders.clear()
+    try:
+        new = getChansToFaders(graph)
+    except ValueError:
+        return  # e.g. empty-graph startup
+    currentFaders.update(new)
+
+
+def getGraphMappingNode(g: ReadOnlyConjunctiveGraph|SyncedGraph) -> URIRef:
+    mapping = g.value(L9['midiControl'], L9['map'])
+    if mapping is None:
+        raise ValueError('no :midiControl :map ?mapping')
+    midiDev = g.value(mapping, L9['midiDev'])
+    ourDev = 'bcf2000'
+    if midiDev != Literal(ourDev):
+        raise NotImplementedError(f'need {mapping} to have :midiDev {ourDev!r}')
+    return mapping
 
 
-def setFader(graph: SyncedGraph, ctx, fader: URIRef, strength: float):
+def getCurMappedPage(g: SyncedGraph):
+    mapping = getGraphMappingNode(g)
+    return g.value(mapping, L9['outputs'])
+
+def setCurMappedPage(g: SyncedGraph, mapping: URIRef, newPage:URIRef):
+    g.patchObject(ctx, mapping, L9.outputs, newPage)
+
+def getChansToFaders(g: SyncedGraph) -> Dict[int, URIRef]:
+    fadePage = getCurMappedPage(g)
+    ret = []
+    for f in g.objects(fadePage, L9.fader):
+        columnLit = cast(Literal, g.value(f, L9['column']))
+        col = int(columnLit.toPython())
+        ret.append((col, f))
+
+    ret.sort()
+    ctl_channels = list(range(81, 88 + 1))
+    out = {}
+    for chan, (col, f) in zip(ctl_channels, ret):
+        out[chan] = f
+    return out
+
+
+def changePage(g: SyncedGraph, dp: int):
+    """dp==-1, make the previous page active, etc. Writes to graph"""
+
+    with g.currentState() as current:
+        allPages = sorted(current.subjects(RDF.type, L9.FadePage), key=lambda fp: str(fp))
+        mapping = getGraphMappingNode(current)
+        curPage = current.value(mapping, L9.outputs)
+    if curPage is None:
+        curPage = allPages[0]
+    idx = allPages.index(curPage)
+    newIdx = clamp(idx + dp, 0, len(allPages)-1)
+    print('change from ', idx, newIdx)
+    newPage =allPages[newIdx]
+    setCurMappedPage(g, mapping, newPage)
+
+def writeHwValueToGraph(graph: SyncedGraph, ctx, fader: URIRef, strength: float):
     log.info(f'setFader(fader={fader}, strength={strength:.03f}')
     valueLit = decimalLiteral(round(strength, 3))
     with graph.currentState() as g:
@@ -31,13 +92,18 @@
     if fadeSet is None:
         raise ValueError(f'fader {fader} has no :setting')
     graph.patchObject(ctx, fadeSet, L9['value'], valueLit)
-    _lastSet[fader] = strength
 
 
 def onMessage(graph: SyncedGraph, ctx: URIRef, m: Dict):
     if m['type'] == 'active_sensing':
         return
     if m['type'] == 'control_change':
+        if m['dev'] == 'bcf2000' and m['control'] == 91:
+            changePage(graph, -1)
+            return
+        if m['dev'] == 'bcf2000' and m['control'] == 92:
+            changePage(graph, 1)
+            return
 
         try:
             fader = {
@@ -46,22 +112,14 @@
                     45: L9['show/dance2023/fadePage1f0'],
                     46: L9['show/dance2023/fadePage1f0'],
                 },
-                'bcf2000': {
-                    81: L9['show/dance2023/fader0'],
-                    82: L9['show/dance2023/fader1'],
-                    83: L9['show/dance2023/fader2'],
-                    84: L9['show/dance2023/fader3'],
-                    85: L9['show/dance2023/fader4'],
-                    86: L9['show/dance2023/fader5'],
-                    87: L9['show/dance2023/fader6'],
-                    88: L9['show/dance2023/fader7'],
-                }
+                'bcf2000': currentFaders,
             }[m['dev']][m['control']]
         except KeyError:
             log.info(f'unknown control {m}')
             return
         try:
-            setFader(graph, ctx, fader, m['value'] / 127)
+            writeHwValueToGraph(graph, ctx, fader, m['value'] / 127)
+            _lastSet[m['control']] = m['value']
         except ValueError as e:
             log.warning(f'{e!r} - ignoring')
     else:
@@ -91,29 +149,22 @@
 
     def _update(self):
         g = self.graph
-        mapping = g.value(L9['midiControl'], L9['map'])
-        if mapping is None:
-            raise ValueError('no :midiControl :map ?mapping')
-        midiDev = g.value(mapping, L9['midiDev'])
-        ourDev = 'bcf2000'
-        if midiDev != Literal(ourDev):
-            raise NotImplementedError(f'need {mapping} to have :midiDev {ourDev!r}')
-        fadePage = g.value(mapping, L9['outputs'])
-        nupdated=0
-        faders = list(g.objects(fadePage, L9.fader))
-        for f in faders:
-            column = int(g.value(f, L9.column).toPython())
+        nupdated = 0
+        m = getChansToFaders(g)
+        for midi_ctl_addr, f in m.items():
             fset = g.value(f, L9.setting)
             # could split this to a separate handler per fader
             value = g.value(fset, L9.value).toPython()
-            hwcurrent = int(self.getCurrentValue(f) * 127)
+            hwcurrent = self.getCurrentValue(midi_ctl_addr) 
             hwgoal = int(value * 127)
-            if abs(hwcurrent - hwgoal) > 5:
-                midi_ctl_addr = column + 80
+            print(f'{f} {hwcurrent=} {hwgoal=}')
+            if abs(hwcurrent - hwgoal) > 2:
                 self.sendToBcf(midi_ctl_addr, hwgoal)
-                nupdated+=1
-        log.info(f'updated {nupdated} of {len(faders)} connected faders')
+                nupdated += 1
+        log.info(f'wrote to {nupdated} of {len(m)} mapped faders')
+
     def sendToBcf(self, control, value):
+        _lastSet[control] = value
         msg = mido.Message('control_change', control=control, value=value)
         self.bcf_out.send(msg)
 
@@ -147,7 +198,7 @@
 
     asyncio.create_task(reader())
     openPorts = []
-    for inputName in mido.get_input_names():
+    for inputName in mido.get_input_names():  # type: ignore
         if inputName.startswith('Keystation'):
             dev = "keystation"
         elif inputName.startswith('BCF2000'):
@@ -157,13 +208,18 @@
         else:
             continue
         log.info(f'listening on input {inputName} {dev=}')
-        openPorts.append(mido.open_input(  #
+        openPorts.append(mido.open_input(  # type: ignore
             inputName,  #
             callback=lambda message, dev=dev: onMessageMidoThread(dev, message)))
 
-    bcf_out = mido.open_output('BCF2000:BCF2000 MIDI 1 28:0')
-    wb = WriteBackFaders(graph, bcf_out, getCurrentValue=lambda f: _lastSet.get(f, 0))
-    graph.addHandler(wb.update)
+    graph.addHandler(lambda: compileCurrents(graph))
+
+    for outputName in mido.get_output_names():  # type: ignore
+        if outputName.startswith('BCF2000'):
+            bcf_out = mido.open_output(outputName)  # type: ignore
+            wb = WriteBackFaders(graph, bcf_out, getCurrentValue=lambda f: _lastSet.get(f, 0))
+            graph.addHandler(wb.update)
+            break
 
     while True:
         await asyncio.sleep(1)