diff --git a/light9/effect/sequencer/sequencer.py b/light9/effect/sequencer/sequencer.py --- a/light9/effect/sequencer/sequencer.py +++ b/light9/effect/sequencer/sequencer.py @@ -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,15 +54,19 @@ class Note(object): 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 @@ 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) diff --git a/light9/effect/sequencer/service.py b/light9/effect/sequencer/service.py --- a/light9/effect/sequencer/service.py +++ b/light9/effect/sequencer/service.py @@ -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 @@ 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, diff --git a/light9/fade/web/Light9FadeUi.ts b/light9/fade/web/Light9FadeUi.ts --- a/light9/fade/web/Light9FadeUi.ts +++ b/light9/fade/web/Light9FadeUi.ts @@ -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 { 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() { + + 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); diff --git a/light9/web/AutoDependencies.ts b/light9/web/AutoDependencies.ts --- a/light9/web/AutoDependencies.ts +++ b/light9/web/AutoDependencies.ts @@ -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); diff --git a/light9/web/SyncedGraph.ts b/light9/web/SyncedGraph.ts --- a/light9/web/SyncedGraph.ts +++ b/light9/web/SyncedGraph.ts @@ -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; } diff --git a/light9/web/patch.ts b/light9/web/patch.ts --- a/light9/web/patch.ts +++ b/light9/web/patch.ts @@ -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 { // 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]) {