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
 
@@ -4,25 +4,25 @@ copies from effectloop.py, which this sh
 

	
 
import asyncio
 
from louie import dispatcher,All
 
from rdflib import URIRef
 
from twisted.internet import reactor
 
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
 
from light9.newtypes import DeviceUri, DeviceAttr, NoteUri, Curve, Song
 
from rdfdb.syncedgraph.syncedgraph import SyncedGraph
 
from light9.metrics import metrics
 

	
 
import imp
 

	
 
@@ -33,44 +33,49 @@ class StateUpdate(All):
 
    pass
 

	
 
def pyType(n):
 
    ret = n.toPython()
 
    if isinstance(ret, Decimal):
 
        return float(ret)
 
    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:
 
            return []
 
        for point in [row[1] for row in po if row[0] == L9['point']]:
 
            po2 = dict(self.graph.predicate_objects(point))
 
            points.append(
 
                (originTime + float(po2[L9['time']]), float(po2[L9['value']])))
 
        return points
 
@@ -86,50 +91,59 @@ class Note(object):
 
        if self.points[i][0] > t:
 
            return self.points[i][1]
 
        if i >= len(self.points) - 1:
 
            return self.points[i][1]
 

	
 
        p1, p2 = self.points[i], self.points[i + 1]
 
        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):
 

	
 
    def __init__(self, onChange):
 
        self.onChange = onChange
 

	
 
        self.notifier = INotify()
 
        self.notifier.startReading()
 
        self.notifier.watch(FilePath(effecteval.__file__.replace('.pyc',
 
@@ -255,12 +269,76 @@ class Sequencer(object):
 
                noteReports.append(report)
 
                settings.append(s)
 
            devSettings = DeviceSettings.fromList(self.graph, settings)
 
        dispatcher.send(StateUpdate, update={'songNotes': noteReports})
 

	
 
        with metrics('update_s3_send').time():  # our measurement
 
            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
 
from starlette.applications import Starlette
 
from starlette.routing import Route
 
from starlette.types import Receive, Scope, Send
 
from starlette_exporter import PrometheusMiddleware, handle_metrics
 

	
 

	
 
@@ -47,24 +47,25 @@ async def send_page_updates(request):
 

	
 
def main():
 
    session = 'effectSequencer'
 
    graph = SyncedGraph(networking.rdfdb.url, "effectSequencer")
 
    logging.getLogger('autodepgraphapi').setLevel(logging.INFO)
 
    logging.getLogger('syncedgraph').setLevel(logging.INFO)
 
    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),
 
        ],
 
    )
 

	
 
    app.add_middleware(PrometheusMiddleware)
 
    app.add_route("/metrics", handle_metrics)
 

	
 
    return app
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");
 

	
 
@customElement("light9-fade-ui")
 
export class Light9FadeUi extends LitElement {
 
  static styles = [
 
    css`
 
      :host {
 
        display: block;
 
@@ -29,24 +29,25 @@ export class Light9FadeUi extends LitEle
 
      ${this.faders.map((fd) => html` <light9-fader .uri=${fd}></light9-fader> `)}
 
    `;
 
  }
 

	
 
  graph!: SyncedGraph;
 

	
 
  @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"),
 
        g.Uri(":show/dance2019/fadePage1f5"),
 
        g.Uri(":show/dance2019/fadePage1f6"),
 
        g.Uri(":show/dance2019/fadePage1f7"),
 
      ];
 
    });
 
  }
 
@@ -90,37 +91,60 @@ export class Light9Fader extends LitElem
 
  @property() column!: string;
 
  @property() effect: NamedNode | null = null;
 
  @property() effectAttr: NamedNode | null = null;
 

	
 
  @property() value: number = 0.111;
 

	
 
  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
 
@@ -120,18 +120,19 @@ export class AutoDependencies {
 
        //log('match', child.label, match)
 
        //child.innerHandlers = [] # let all children get called again
 
        this._rerunHandler(child, patch);
 
        rerunInners(child);
 
      }
 
    };
 
    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
 
@@ -335,25 +335,26 @@ export class SyncedGraph {
 

	
 
      if (rests.length !== 1) {
 
        throw new Error(`list node ${current} has ${rests.length} rdf:rest edges`);
 
      }
 
      current = rests[0].object;
 
    }
 

	
 
    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
 
    // always return 'name0'.
 
    if (base.id) {
 
      base = base.id;
 
    }
 
    const results = [];
 

	
light9/web/patch.ts
Show inline comments
 
@@ -15,25 +15,25 @@ interface SyncgraphPatchMessage {
 
}
 

	
 
export function patchUpdate(p1: Patch, p2: Patch): void {
 
  // this is approx, since it doesnt handle matching existing quads.
 
  p1.adds = p1.adds.concat(p2.adds);
 
  p1.dels = p1.dels.concat(p2.dels);
 
}
 

	
 
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) => {
 
      if (quad) {
 
        return patch.adds.push(quad);
 
      } else {
 
        return cb();
 
      }
 
    });
 
@@ -41,71 +41,70 @@ export function parseJsonPatch(input: Sy
 
  const parseDels = (cb: () => any) => {
 
    const parser = new Parser();
 
    return parser.parse(input.patch.deletes, (error: any, quad: any, prefixes: any) => {
 
      if (quad) {
 
        return patch.dels.push(quad);
 
      } else {
 
        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) {
 
      out.patch.deletes = result;
 
      return cb();
 
    });
 
  };
 

	
 
  const writeAdds = function (cb: () => any) {
 
    const writer = new Writer({ format: "N-Quads" });
 
    writer.addQuads(jsPatch.adds);
 
    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]) {
 
      for (let q of Array.from(qq)) {
 
        patch._allPredsCache.add(q.predicate.value);
 
      }
 
    }
 
  }
 

	
 
  for (let p of Array.from(preds)) {
 
    if (patch._allPredsCache.has(p.value)) {
 
      return true;
 
    }
 
  }
 
  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);
 
      }
 
    }
 
  }
 

	
 
  return patch._allSubjsCache;
 
}
0 comments (0 inline, 0 general)