changeset 2108:e92db17f3e7e

effectSequencer can now also process some note-like values coming from the fade/ ui
author drewp@bigasterisk.com
date Wed, 01 Jun 2022 17:02:46 -0700
parents 8bb2f526d457
children 00dc570c3ba1
files light9/effect/sequencer/sequencer.py light9/effect/sequencer/service.py light9/fade/web/Light9FadeUi.ts light9/web/AutoDependencies.ts light9/web/SyncedGraph.ts light9/web/patch.ts
diffstat 6 files changed, 131 insertions(+), 27 deletions(-) [+]
line wrap: on
line diff
--- a/light9/effect/sequencer/sequencer.py	Wed Jun 01 17:00:29 2022 -0700
+++ b/light9/effect/sequencer/sequencer.py	Wed Jun 01 17:02:46 2022 -0700
@@ -13,7 +13,7 @@
 import logging, bisect, time
 import traceback
 from decimal import Decimal
-from typing import Any, Callable, Coroutine, Dict, List, Tuple, cast, Union
+from typing import Any, Callable, Coroutine, Dict, List, Optional, Tuple, cast, Union
 
 from light9.ascoltami.musictime_client import MusicTime
 from light9.effect import effecteval
@@ -42,9 +42,10 @@
 class Note(object):
 
     def __init__(self, graph: SyncedGraph, uri: NoteUri, effectevalModule,
-                 simpleOutputs):
+                 simpleOutputs, timed=True):
         g = self.graph = graph
         self.uri = uri
+        self.timed= timed
         self.effectEval = effectevalModule.EffectEval(
             graph, g.value(uri, L9['effectClass']), simpleOutputs)
         self.baseEffectSettings: Dict[URIRef, Any] = {}  # {effectAttr: value}
@@ -53,15 +54,19 @@
             ea = settingValues[L9['effectAttr']]
             self.baseEffectSettings[ea] = pyType(settingValues[L9['value']])
 
-        def floatVal(s, p):
-            return float(g.value(s, p).toPython())
+
+        if timed:
+            def floatVal(s, p):
+                return float(g.value(s, p).toPython())
 
-        originTime = floatVal(uri, L9['originTime'])
-        self.points: List[Tuple[float, float]] = []
-        for curve in g.objects(uri, L9['curve']):
-            self.points.extend(
-                self.getCurvePoints(curve, L9['strength'], originTime))
-        self.points.sort()
+            originTime = floatVal(uri, L9['originTime'])
+            self.points: List[Tuple[float, float]] = []
+            for curve in g.objects(uri, L9['curve']):
+                self.points.extend(
+                    self.getCurvePoints(cast(Curve, curve), L9['strength'], originTime))
+            self.points.sort()
+        else:
+            self.points = []
 
     def getCurvePoints(self, curve: Curve, attr,
                        originTime: float) -> List[Tuple[float, float]]:
@@ -95,17 +100,25 @@
 
     def outputSettings(
             self,
-            t: float) -> Tuple[List[Tuple[DeviceUri, DeviceAttr, float]], Dict]:
+            t: float, strength: Optional[float] = None
+            ) -> Tuple[List[Tuple[DeviceUri, DeviceAttr, float]], Dict]:
         """
         list of (device, attr, value), and a report for web
         """
+        if t is None:
+            if self.timed:
+                raise TypeError()
+            t = time.time() # so live effects will move
         report = {
             'note': str(self.uri),
             'effectClass': self.effectEval.effect,
         }
+
+        strengthAttr=cast(DeviceAttr, L9['strength'])
+
         effectSettings: Dict[DeviceAttr, Union[float, str]] = dict(
             (DeviceAttr(da), v) for da, v in self.baseEffectSettings.items())
-        effectSettings[cast(DeviceAttr, L9['strength'])] = self.evalCurve(t)
+        effectSettings[strengthAttr] = self.evalCurve(t) if strength is None else strength
 
         def prettyFormat(x: Union[float, str]):
             if isinstance(x, float):
@@ -115,12 +128,13 @@
         report['effectSettings'] = dict(
             (str(k), prettyFormat(v))
             for k, v in sorted(effectSettings.items()))
-        report['nonZero'] = cast(float, effectSettings[cast(DeviceAttr, L9['strength'])]) > 0
+        report['nonZero'] = cast(float, effectSettings[strengthAttr]) > 0
+        startTime = self.points[0][0] if self.timed else 0
         out, evalReport = self.effectEval.outputFromEffect(
             list(effectSettings.items()),
             songTime=t,
             # note: not using origin here since it's going away
-            noteTime=t - self.points[0][0])
+            noteTime=t - startTime)
         report['devicesAffected'] = len(out.devices())
         return out, report
 
@@ -264,3 +278,67 @@
         # (sometimes it's None, not sure why, and neither is mypy)
         #if isinstance(sendSecs, float):
         #    metrics('update_s3_send_client').observe(sendSecs)
+
+class FaderEval:
+    """peer to Sequencer, but this one takes the current :Fader settings -> sendToCollector
+    
+    The current faders become Notes in here, for more code reuse.
+    """
+    def __init__(self,
+                 graph: SyncedGraph,
+                 sendToCollector: Callable[[DeviceSettings], Coroutine[None ,None,None]],
+                 ):
+        self.graph = graph
+        self.sendToCollector = sendToCollector
+
+        # Notes without times- always on
+        self.notes: List[Note] = []
+
+        self.simpleOutputs = SimpleOutputs(self.graph)
+        self.graph.addHandler(self.compileGraph)
+        self.lastLoopSucceeded = False
+
+        # self.codeWatcher = CodeWatcher(onChange=self.onCodeChange)
+        log.info('startupdating task')
+        asyncio.create_task(self.startUpdating())
+
+    async def startUpdating(self):
+        await self.graph.addAsyncHandler(self.update)
+        log.info('startupdating task done')
+
+    def onCodeChange(self):
+        log.debug('seq.onCodeChange')
+        self.graph.addHandler(self.compileGraph)
+        #self.updateLoop()
+
+    @metrics('compile_graph_fader').time()
+    def compileGraph(self) -> None:
+        """rebuild our data from the graph"""
+        self.notes = []
+        for fader in self.graph.subjects(RDF.type, L9['Fader']):          
+            def compileFader() -> Note:
+                return self.compileFader(cast(URIRef, fader))
+
+            self.notes.append(compileFader())
+        if self.notes:
+            asyncio.create_task(self.startUpdating())
+
+
+    @metrics('compile_fader').time()
+    def compileFader(self, fader: URIRef) -> Note:
+        return Note(self.graph, NoteUri(cast(NoteUri, fader)), effecteval,
+                self.simpleOutputs, timed=False)
+    
+    @metrics('update_call_fader').time()
+    async def update(self):
+        settings = []
+        for note in self.notes:
+            effectValue = self.graph.value(note.uri, L9['value'])
+            if effectValue is None:
+                log.info(f'skip note {note}, no :value')
+                continue
+            s, report = note.outputSettings(t=time.time(), strength=float(effectValue))
+            settings.append(s)
+        devSettings = DeviceSettings.fromList(self.graph, settings)
+        with metrics('update_s3_send_fader').time():  # our measurement
+            sendSecs = await self.sendToCollector(devSettings)
--- a/light9/effect/sequencer/service.py	Wed Jun 01 17:00:29 2022 -0700
+++ b/light9/effect/sequencer/service.py	Wed Jun 01 17:02:46 2022 -0700
@@ -1,5 +1,5 @@
 """
-plays back effect notes from the timeline
+plays back effect notes from the timeline (and an untimed note from the faders)
 """
 
 import asyncio
@@ -9,7 +9,7 @@
 
 from light9 import networking
 from light9.collector.collector_client_asyncio import sendToCollector
-from light9.effect.sequencer.sequencer import StateUpdate, Sequencer
+from light9.effect.sequencer.sequencer import FaderEval, Sequencer, StateUpdate
 from light9.effect.settings import DeviceSettings
 from light9.metrics import metrics
 from light9.run_local import log
@@ -56,6 +56,7 @@
         await sendToCollector('effectSequencer', session, settings)
 
     seq = Sequencer(graph, send)
+    faders = FaderEval(graph, send)  # bin/fade's untimed notes
 
     app = Starlette(
         debug=True,
--- a/light9/fade/web/Light9FadeUi.ts	Wed Jun 01 17:00:29 2022 -0700
+++ b/light9/fade/web/Light9FadeUi.ts	Wed Jun 01 17:02:46 2022 -0700
@@ -1,3 +1,4 @@
+import { fastSlider, fastSliderLabel, provideFASTDesignSystem } from "@microsoft/fast-components";
 import debug from "debug";
 import { css, html, LitElement } from "lit";
 import { customElement, property } from "lit/decorators.js";
@@ -5,7 +6,6 @@
 import { getTopGraph } from "../../web/RdfdbSyncedGraph";
 import { SyncedGraph } from "../../web/SyncedGraph";
 export { EditChoice } from "../../web/EditChoice";
-import { provideFASTDesignSystem, fastSlider, fastSliderLabel } from "@microsoft/fast-components";
 
 provideFASTDesignSystem().register(fastSlider(), fastSliderLabel());
 
@@ -38,6 +38,7 @@
     super();
     getTopGraph().then((g) => {
       this.graph = g;
+      // todo: start with a page, then find the faders on that page
       this.faders = [
         g.Uri(":show/dance2019/fadePage1f0"),
         g.Uri(":show/dance2019/fadePage1f1"),
@@ -99,26 +100,49 @@
       this.graph = g;
       this.graph.runHandler(this.configure.bind(this), `config ${this.uri.value}`);
       this.graph.runHandler(this.valueSync.bind(this), `valueSync ${this.uri.value}`);
-
     });
   }
+
   configure() {
     //   console.time(`fader configure ${this.uri.value}`)
     const U = this.graph.U();
+    if (!this.graph.contains(this.uri, U("rdf:type"), U(":Fader"))) {
+      // not loaded yet
+      //   console.timeEnd(`fader configure ${this.uri.value}`)
+
+      return;
+    }
     this.column = this.graph.stringValue(this.uri, U(":column"));
     this.effect = this.graph.uriValue(this.uri, U(":effectClass"));
     this.effectAttr = this.graph.uriValue(this.uri, U(":effectAttr"));
     // console.timeEnd(`fader configure ${this.uri.value}`)
   }
-valueSync() {
+
+  valueSync() {
     // console.time(`valueSync ${this.uri.value}`)
+    const U = this.graph.U();
+    if (!this.graph.contains(this.uri, U("rdf:type"), U(":Fader"))) {
+      // not loaded yet
+      // console.timeEnd(`valueSync ${this.uri.value}`)
+      return;
+    }
+
     this.value = this.graph.floatValue(this.uri, this.graph.Uri(":value"));
-}
     // console.timeEnd(`valueSync ${this.uri.value}`)
+  }
+
   onSliderInput(ev: CustomEvent) {
-    this.value = (ev.target as any).valueAsNumber;
+    const prev = this.value;
+    const v: number = (ev.target as any).valueAsNumber;
+    this.value = parseFloat(v.toPrecision(3)); // rewrite pls
+    if (this.value == prev) {
+      return;
+    }
+    log(`new value ${this.value}`);
+
     this.graph.patchObject(this.uri, this.graph.Uri(":value"), this.graph.LiteralRoundedFloat(this.value), this.ctx);
   }
+
   onEffectChange(ev: CustomEvent) {
     const { newValue } = ev.detail;
     this.graph.patchObject(this.uri, this.graph.Uri(":effectClass"), newValue, this.ctx);
--- a/light9/web/AutoDependencies.ts	Wed Jun 01 17:00:29 2022 -0700
+++ b/light9/web/AutoDependencies.ts	Wed Jun 01 17:02:46 2022 -0700
@@ -129,6 +129,7 @@
   askedFor(s: Quad_Subject | null, p: Quad_Predicate | null, o: Quad_Object | null, g: Quad_Graph | null) {
     // SyncedGraph is telling us someone did a query that depended on
     // quads in the given pattern.
+    // console.log(`  asked for s/${s?.id} p/${p?.id} o/${o?.id}`)
     const current = this.handlerStack[this.handlerStack.length - 1];
     if (current != null && current !== this.handlers) {
       current.patterns.push({ subject: s, predicate: p, object: o, graph: g } as QuadPattern);
--- a/light9/web/SyncedGraph.ts	Wed Jun 01 17:00:29 2022 -0700
+++ b/light9/web/SyncedGraph.ts	Wed Jun 01 17:02:46 2022 -0700
@@ -344,7 +344,8 @@
 
   contains(s: any, p: any, o: any): boolean {
     this._autoDeps.askedFor(s, p, o, null);
-    log("contains calling getQuads when graph has ", this.graph.size);
+    // Sure this is a nice warning to remind me to rewrite, but the graph.size call itself was taking 80% of the time in here
+    // log("contains calling getQuads when graph has ", this.graph.size);
     return this.graph.getQuads(s, p, o, null).length > 0;
   }
 
--- a/light9/web/patch.ts	Wed Jun 01 17:00:29 2022 -0700
+++ b/light9/web/patch.ts	Wed Jun 01 17:02:46 2022 -0700
@@ -24,7 +24,7 @@
   return "-" + patch.dels.length + " +" + patch.adds.length;
 }
 
-export function parseJsonPatch(input: SyncgraphPatchMessage, cb: (p: Patch) => void) {
+export function parseJsonPatch(input: SyncgraphPatchMessage, cb: (p: Patch) => void): void {
   // note response cb doesn't have an error arg.
   const patch: Patch = { dels: [], adds: [] };
 
@@ -50,10 +50,10 @@
   };
 
   // todo: is it faster to run them in series? might be
-  return async.parallel([parseAdds, parseDels], (err: any) => cb(patch));
+  async.parallel([parseAdds, parseDels], (err: any) => cb(patch));
 }
 
-export function toJsonPatch(jsPatch: Patch, cb: { (json: any): any; (arg0: any): any }) {
+export function toJsonPatch(jsPatch: Patch, cb: (jsonString: string) => void): void {
   const out: SyncgraphPatchMessage = { patch: { adds: "", deletes: "" } };
 
   const writeDels = function (cb: () => any) {
@@ -74,7 +74,7 @@
     });
   };
 
-  return async.parallel([writeDels, writeAdds], (err: any) => cb(JSON.stringify(out)));
+  async.parallel([writeDels, writeAdds], (err: any) => cb(JSON.stringify(out)));
 }
 
 export function patchContainsPreds(patch: Patch, preds: NamedNode[]): boolean {
@@ -97,7 +97,6 @@
 
 export function allPatchSubjs(patch: Patch): Set<string> {
   // returns subjs as Set of strings
-  const out = new Set();
   if (patch._allSubjsCache === undefined) {
     patch._allSubjsCache = new Set();
     for (let qq of [patch.adds, patch.dels]) {