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
 
@@ -10,13 +10,13 @@ from twisted.internet import defer
 
from twisted.internet.defer import Deferred, inlineCallbacks
 
from twisted.internet.inotify import INotify
 
from twisted.python.filepath import FilePath
 
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
 
from light9.effect.settings import DeviceSettings
 
from light9.effect.simple_outputs import SimpleOutputs
 
from light9.namespaces import L9, RDF
 
@@ -39,32 +39,37 @@ def pyType(n):
 
    return ret
 

	
 

	
 
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}
 
        for s in g.objects(uri, L9['setting']):
 
            settingValues = dict(g.predicate_objects(s))
 
            ea = settingValues[L9['effectAttr']]
 
            self.baseEffectSettings[ea] = pyType(settingValues[L9['value']])
 

	
 

	
 
        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.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]]:
 
        points = []
 
        po = list(self.graph.predicate_objects(curve))
 
        if dict(po).get(L9['attr'], None) != attr:
 
@@ -92,38 +97,47 @@ class Note(object):
 
        frac = (t - p1[0]) / (p2[0] - p1[0])
 
        y = p1[1] + (p2[1] - p1[1]) * frac
 
        return y
 

	
 
    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):
 
                return round(x, 4)
 
            return x
 

	
 
        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
 

	
 

	
 
class CodeWatcher(object):
 

	
 
@@ -261,6 +275,70 @@ class Sequencer(object):
 
            sendSecs = await self.sendToCollector(devSettings)
 

	
 
        # sendToCollector's own measurement.
 
        # (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
 
import json
 
import logging
 
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
 
from louie import dispatcher
 
from rdfdb.syncedgraph.syncedgraph import SyncedGraph
 
from sse_starlette.sse import EventSourceResponse
 
@@ -53,12 +53,13 @@ def main():
 
    logging.getLogger('sse_starlette.sse').setLevel(logging.INFO)
 

	
 
    async def send(settings: DeviceSettings):
 
        await sendToCollector('effectSequencer', session, settings)
 

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

	
 
    app = Starlette(
 
        debug=True,
 
        routes=[
 
            Route('/updates', endpoint=send_page_updates),
 
        ],
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";
 
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());
 

	
 
debug.enable("*");
 
const log = debug("fade");
 

	
 
@@ -35,12 +35,13 @@ export class Light9FadeUi extends LitEle
 
  @property() faders: NamedNode[] = [];
 

	
 
  constructor() {
 
    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"),
 
        g.Uri(":show/dance2019/fadePage1f2"),
 
        g.Uri(":show/dance2019/fadePage1f3"),
 
        g.Uri(":show/dance2019/fadePage1f4"),
 
@@ -96,31 +97,54 @@ export class Light9Fader extends LitElem
 
  constructor() {
 
    super();
 
    getTopGraph().then((g) => {
 
      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
 
@@ -126,12 +126,13 @@ export class AutoDependencies {
 
    rerunInners(this.handlers);
 
  }
 

	
 
  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
 
@@ -341,13 +341,14 @@ export class SyncedGraph {
 

	
 
    return out;
 
  }
 

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

	
 
  nextNumberedResources(base: { id: any }, howMany: number) {
 
    // base is NamedNode or string
 
    // Note this is unsafe before we're synced with the graph. It'll
light9/web/patch.ts
Show inline comments
 
@@ -21,13 +21,13 @@ export function patchUpdate(p1: Patch, p
 
}
 

	
 
export function patchSizeSummary(patch: 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: [] };
 

	
 
  const parseAdds = (cb: () => any) => {
 
    const parser = new Parser();
 
    return parser.parse(input.patch.adds, (error: any, quad: Quad, prefixes: any) => {
 
@@ -47,16 +47,16 @@ export function parseJsonPatch(input: Sy
 
        return cb();
 
      }
 
    });
 
  };
 

	
 
  // 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) {
 
    const writer = new Writer({ format: "N-Quads" });
 
    writer.addQuads(jsPatch.dels);
 
    return writer.end(function (err: any, result: string) {
 
@@ -71,13 +71,13 @@ export function toJsonPatch(jsPatch: Pat
 
    return writer.end(function (err: any, result: string) {
 
      out.patch.adds = result;
 
      return cb();
 
    });
 
  };
 

	
 
  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 {
 
  if (patch._allPredsCache === undefined) {
 
    patch._allPredsCache = new Set();
 
    for (let qq of [patch.adds, patch.dels]) {
 
@@ -94,13 +94,12 @@ export function patchContainsPreds(patch
 
  }
 
  return false;
 
}
 

	
 
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]) {
 
      for (let q of Array.from(qq)) {
 
        patch._allSubjsCache.add(q.subject.value);
 
      }
0 comments (0 inline, 0 general)