Changeset - e92db17f3e7e
[Not reviewed]
default
0 6 0
drewp@bigasterisk.com - 3 years ago 2022-06-02 00:02:46
drewp@bigasterisk.com
effectSequencer can now also process some note-like values coming from the fade/ ui
6 files changed with 123 insertions and 19 deletions:
0 comments (0 inline, 0 general)
light9/effect/sequencer/sequencer.py
Show inline comments
 
@@ -13,7 +13,7 @@ from twisted.python.filepath import File
 
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 @@ def pyType(n):
 
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,6 +54,8 @@ class Note(object):
 
            ea = settingValues[L9['effectAttr']]
 
            self.baseEffectSettings[ea] = pyType(settingValues[L9['value']])
 

	
 

	
 
        if timed:
 
        def floatVal(s, p):
 
            return float(g.value(s, p).toPython())
 

	
 
@@ -60,8 +63,10 @@ class Note(object):
 
        self.points: List[Tuple[float, float]] = []
 
        for curve in g.objects(uri, L9['curve']):
 
            self.points.extend(
 
                self.getCurvePoints(curve, L9['strength'], originTime))
 
                    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 @@ class Note(object):
 

	
 
    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 @@ class Note(object):
 
        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 @@ class Sequencer(object):
 
        # (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)
light9/effect/sequencer/service.py
Show inline comments
 
"""
 
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 @@ import time
 

	
 
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 @@ def main():
 
        await sendToCollector('effectSequencer', session, settings)
 

	
 
    seq = Sequencer(graph, send)
 
    faders = FaderEval(graph, send)  # bin/fade's untimed notes
 

	
 
    app = Starlette(
 
        debug=True,
light9/fade/web/Light9FadeUi.ts
Show inline comments
 
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 { NamedNode } from "n3";
 
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 @@ export class Light9FadeUi extends LitEle
 
    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 @@ export class Light9Fader extends LitElem
 
      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() {
 
    // 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);
light9/web/AutoDependencies.ts
Show inline comments
 
@@ -129,6 +129,7 @@ export class AutoDependencies {
 
  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);
light9/web/SyncedGraph.ts
Show inline comments
 
@@ -344,7 +344,8 @@ export class SyncedGraph {
 

	
 
  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;
 
  }
 

	
light9/web/patch.ts
Show inline comments
 
@@ -24,7 +24,7 @@ export function patchSizeSummary(patch: 
 
  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 @@ export function parseJsonPatch(input: Sy
 
  };
 

	
 
  // 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 @@ export function toJsonPatch(jsPatch: Pat
 
    });
 
  };
 

	
 
  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 patchContainsPreds(patch
 

	
 
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]) {
0 comments (0 inline, 0 general)