Changeset - 04612ba3fe45
[Not reviewed]
default
0 6 2
drewp@bigasterisk.com - 20 months ago 2023-05-20 03:55:28
drewp@bigasterisk.com
refactor
8 files changed with 153 insertions and 128 deletions:
0 comments (0 inline, 0 general)
light9/effect/sequencer/__init__.py
Show inline comments
 
from .note import Note
light9/effect/sequencer/eval_faders.py
Show inline comments
 

	
 
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
 
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
 
    
 
    The current faders become Notes in here, for more code reuse.
 
    """
 
    def __init__(self,
light9/effect/sequencer/note.py
Show inline comments
 
new file 100644
 
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
light9/effect/sequencer/sequencer.py
Show inline comments
 
'''
 
copies from effectloop.py, which this should replace
 
'''
 

	
 
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')
 

	
 

	
 
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):
 

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

	
 
@@ -255,13 +148,13 @@ class Sequencer(object):
 
                                'song': str(song),
 
                                't': musicState['t']
 
                            })
 

	
 
        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:
 
                    s, report = note.outputSettings(musicState['t'])
 
                except Exception:
 
                    traceback.print_exc()
light9/effect/sequencer/service.py
Show inline comments
 
@@ -4,26 +4,26 @@ plays back effect notes from the timelin
 

	
 
import asyncio
 
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
 
from light9.effect.sequencer.sequencer import 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
 

	
 

	
 
async def changes():
 
    state = {}
 
    q = asyncio.Queue()
 

	
light9/effect/sequencer/service_test.py
Show inline comments
 
new file 100644
 

	
 
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
light9/newtypes.py
Show inline comments
 
@@ -42,7 +42,8 @@ def typedValue(objType: Type[_ObjType], 
 
    """graph.value(subj, pred) with a given return type. 
 
    If objType is not an rdflib.Node, we toPython() the value."""
 
    obj = graph.value(subj, pred)
 
    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
light9/rdfdb/service_test.py
Show inline comments
 
@@ -3,9 +3,9 @@ from light9.run_local import log
 

	
 

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