diff --git a/light9/effect/sequencer/__init__.py b/light9/effect/sequencer/__init__.py --- a/light9/effect/sequencer/__init__.py +++ b/light9/effect/sequencer/__init__.py @@ -0,0 +1,1 @@ +from .note import Note diff --git a/light9/effect/sequencer/eval_faders.py b/light9/effect/sequencer/eval_faders.py --- a/light9/effect/sequencer/eval_faders.py +++ b/light9/effect/sequencer/eval_faders.py @@ -3,12 +3,12 @@ import asyncio import logging import time from typing import Callable, Coroutine, List, cast -from light9.effect.sequencer.sequencer import Note from rdfdb.syncedgraph.syncedgraph import SyncedGraph from rdflib import URIRef from light9.effect import effecteval +from light9.effect.sequencer import Note from light9.effect.settings import DeviceSettings from light9.effect.simple_outputs import SimpleOutputs from light9.metrics import metrics @@ -16,6 +16,7 @@ from light9.namespaces import L9, RDF from light9.newtypes import NoteUri log = logging.getLogger('sequencer') + class FaderEval: """peer to Sequencer, but this one takes the current :Fader settings -> sendToCollector diff --git a/light9/effect/sequencer/note.py b/light9/effect/sequencer/note.py new file mode 100644 --- /dev/null +++ b/light9/effect/sequencer/note.py @@ -0,0 +1,116 @@ +import bisect +import logging +import time +from decimal import Decimal +from typing import Any, Dict, List, Optional, Tuple, Union, cast + +from rdfdb.syncedgraph.syncedgraph import SyncedGraph +from rdflib import Literal, URIRef + +from light9.namespaces import L9 +from light9.newtypes import Curve, DeviceAttr, DeviceUri, NoteUri, typedValue + +log = logging.getLogger('sequencer') + + +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, 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 = cast(URIRef, settingValues[L9['effectAttr']]) + self.baseEffectSettings[ea] = pyType(settingValues[L9['value']]) + + if timed: + + def floatVal(s, p): + return typedValue(float, g, s, p) + + 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]]: + 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)) + t = cast(Literal, po2[L9['time']]).toPython() + if not isinstance(t, float): + raise TypeError + + v = cast(Literal, po2[L9['value']]).toPython() + if not isinstance(v, float): + raise TypeError + points.append((originTime + t, v)) + return points + + def activeAt(self, t: float) -> bool: + return self.points[0][0] <= t <= self.points[-1][0] + + def evalCurve(self, t: float) -> float: + i = bisect.bisect_left(self.points, (t, None)) - 1 + + if i == -1: + return self.points[0][1] + 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, 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[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[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 - startTime) + report['devicesAffected'] = len(out.devices()) + return out, report 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 @@ -3,28 +3,27 @@ copies from effectloop.py, which this sh ''' import asyncio -from louie import dispatcher,All +import imp +import logging +import time +import traceback +from typing import Callable, Coroutine, Dict, List, cast + +from louie import All, dispatcher +from rdfdb.syncedgraph.syncedgraph import SyncedGraph 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, Optional, Tuple, cast, Union from light9.ascoltami.musictime_client import MusicTime from light9.effect import effecteval +from light9.effect.sequencer import Note from light9.effect.settings import DeviceSettings from light9.effect.simple_outputs import SimpleOutputs +from light9.metrics import metrics 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 +from light9.newtypes import NoteUri, Song log = logging.getLogger('sequencer') @@ -32,112 +31,6 @@ log = logging.getLogger('sequencer') 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, 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(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 - - def activeAt(self, t: float) -> bool: - return self.points[0][0] <= t <= self.points[-1][0] - - def evalCurve(self, t: float) -> float: - i = bisect.bisect_left(self.points, (t, None)) - 1 - - if i == -1: - return self.points[0][1] - 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, 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[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[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 - startTime) - report['devicesAffected'] = len(out.devices()) - return out, report - class CodeWatcher(object): @@ -258,7 +151,7 @@ class Sequencer(object): with metrics('update_s1_eval').time(): settings = [] - songNotes = sorted(self.notes.get(song, []), key=lambda n: n.uri) + songNotes = sorted(cast(List[Note], self.notes.get(song, [])), key=lambda n: n.uri) noteReports = [] for note in songNotes: try: 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 @@ -7,6 +7,13 @@ import json import logging import time +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_exporter import PrometheusMiddleware, handle_metrics + from light9 import networking from light9.collector.collector_client_asyncio import sendToCollector from light9.effect.sequencer.eval_faders import FaderEval @@ -14,13 +21,6 @@ from light9.effect.sequencer.sequencer i 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 async def changes(): diff --git a/light9/effect/sequencer/service_test.py b/light9/effect/sequencer/service_test.py new file mode 100644 --- /dev/null +++ b/light9/effect/sequencer/service_test.py @@ -0,0 +1,13 @@ + +import asyncio +from light9.run_local import log + + +def test_import(): + + async def go(): + # this sets up some watcher tasks + from light9.effect.sequencer.service import app + print(app) + + asyncio.run(go(), debug=True) \ No newline at end of file diff --git a/light9/newtypes.py b/light9/newtypes.py --- a/light9/newtypes.py +++ b/light9/newtypes.py @@ -45,4 +45,5 @@ def typedValue(objType: Type[_ObjType], if obj is None: raise ValueError() conv = obj if _isSubclass2(objType, Node) else obj.toPython() + # may need to turn Decimal to float here return cast(objType, conv) \ No newline at end of file diff --git a/light9/rdfdb/service_test.py b/light9/rdfdb/service_test.py --- a/light9/rdfdb/service_test.py +++ b/light9/rdfdb/service_test.py @@ -6,6 +6,6 @@ def test_import(): async def go(): # this sets up some watcher tasks - from service import app + from light9.rdfdb.service import app asyncio.run(go(), debug=True)