# HG changeset patch # User drewp@bigasterisk.com # Date 2023-06-03 20:10:24 # Node ID af38643cffd44258ce0f12c058279abedccc8644 # Parent 81d5b6d97ed3f1af03dd005d3dfb95ac9d6ffbd2 jam in fader page control, fix _lastSet feedback diff --git a/light9/midifade/midifade.py b/light9/midifade/midifade.py --- a/light9/midifade/midifade.py +++ b/light9/midifade/midifade.py @@ -5,12 +5,13 @@ Read midi events, write fade levels to g 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 @@ from light9.showconfig import showUri 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 @@ def setFader(graph: SyncedGraph, ctx, fa 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 @@ def onMessage(graph: SyncedGraph, ctx: U 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 @@ class WriteBackFaders: 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 @@ async def main(): 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 @@ async def main(): 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)