Changeset - b6f8f1b08959
[Not reviewed]
default
0 3 0
drewp@bigasterisk.com - 20 months ago 2023-05-23 19:34:04
drewp@bigasterisk.com
finally: a fader controls an effect strength, which controls an effect, which emits deviceattrs
3 files changed with 135 insertions and 52 deletions:
0 comments (0 inline, 0 general)
light9/effect/effecteval.py
Show inline comments
 
import logging
 
import math
 
import random
 
from colorsys import hsv_to_rgb
 
from dataclasses import dataclass
 
from typing import Dict, Tuple
 
from typing import Dict, Optional, Tuple
 
from light9.typedgraph import typedValue
 

	
 
from noise import pnoise1
 
from PIL import Image
 
from rdfdb.syncedgraph.syncedgraph import SyncedGraph
 
from rdflib import Literal, Namespace
 
from rdflib import RDF, Literal, Namespace, URIRef
 
from webcolors import hex_to_rgb, rgb_to_hex
 

	
 
from light9.effect.scale import scale
 
from light9.effect.settings import BareEffectSettings, DeviceSettings, EffectSettings
 
from light9.effect.simple_outputs import SimpleOutputs
 
from light9.namespaces import DEV, L9
 
from light9.newtypes import (DeviceAttr, DeviceUri, EffectAttr, EffectClass, VTUnion)
 
from light9.newtypes import (DeviceAttr, DeviceUri, EffectAttr, EffectClass, EffectUri, VTUnion)
 

	
 
SKY = Namespace('http://light9.bigasterisk.com/theater/skyline/device/')
 

	
 
random.seed(0)
 

	
 
log = logging.getLogger('effecteval')
 
log.info("reload effecteval")
 

	
 

	
 
def literalColor(rnorm, gnorm, bnorm):
 
    return Literal(rgb_to_hex((
 
        int(rnorm * 255),  #
 
@@ -62,28 +63,72 @@ def clamp(lo, hi, x):
 

	
 
def clamp255(x):
 
    return min(255, max(0, x))
 

	
 

	
 
def _8bit(f):
 
    if not isinstance(f, (int, float)):
 
        raise TypeError(repr(f))
 
    return clamp255(int(f * 255))
 

	
 

	
 
@dataclass
 
class EffectEval2:
 
    graph: SyncedGraph
 
    uri: EffectUri
 

	
 
    effectFunction: Optional[URIRef]=None
 

	
 
    def __post_init__(self):
 
        self.graph.addHandler(self._compile)
 
        self.effectFunction = L9['todo']
 

	
 
    def _compile(self):
 
        if not self.graph.contains((self.uri, RDF.type, L9['Effect'])):
 
            raise ValueError(f'{self.uri} not an :Effect')
 

	
 
        self.function = effect_scale   
 
        devs = []
 
        for s in self.graph.objects(self.uri, L9['setting']):
 
            d = typedValue(DeviceUri, self.graph, s, L9['device'])     
 
            da = typedValue(DeviceAttr, self.graph, s, L9['deviceAttr'])     
 
            v = typedValue(VTUnion, self.graph, s, L9['value'])
 
            devs.append((d, da, v))
 
        self.devs = DeviceSettings(self.graph, devs)
 

	
 
    def compute(self, inputs:EffectSettings) -> DeviceSettings:
 

	
 
        s=0
 
        for e,ea,v in inputs.asList():
 
            if not isinstance(v, float):
 
                raise TypeError
 
            if ea==L9['strength']:
 
                s = v
 

	
 
        print('scaled to', effect_scale(s, self.devs))
 
        return effect_scale(s,self.devs )
 

	
 
        return self.function(inputs)
 

	
 
def effect_scale(strength: float, devs: DeviceSettings) -> DeviceSettings:
 
    out = []
 
    for d,da,v in devs.asList():
 
        out.append((d, da, scale(v, strength)))
 
    return DeviceSettings(devs.graph, out)
 

	
 
@dataclass
 
class EffectEval:
 
    """
 
    runs one effect's code to turn effect attr settings into output
 
    device settings. No state; suitable for reload().
 
    device settings. No effect state; suitable for reload().
 
    """
 
    graph: SyncedGraph
 
    effect: EffectClass
 
    simpleOutputs: SimpleOutputs
 

	
 
    def outputFromEffect(self, effectSettings: BareEffectSettings, songTime: float, noteTime: float) -> Tuple[DeviceSettings, Dict]:
 
        """
 
        From effect attr settings, like strength=0.75, to output device
 
        settings like light1/bright=0.72;light2/bright=0.78. This runs
 
        the effect code.
 
        """
 
        # todo: what does the next comment line mean?
light9/effect/sequencer/eval_faders.py
Show inline comments
 
import asyncio
 
from dataclasses import dataclass
 
import logging
 
import time
 
from typing import Callable, Coroutine, List, cast
 
from typing import Callable, Coroutine, List, Optional, cast
 
from light9.collector.collector import uriTail
 
from light9.typedgraph import typedValue
 

	
 
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.settings import DeviceSettings, EffectSettings
 
from light9.effect.simple_outputs import SimpleOutputs
 
from light9.metrics import metrics
 
from light9.namespaces import L9, RDF
 
from light9.newtypes import EffectAttr, NoteUri, UnixTime
 
from light9.newtypes import EffectAttr, EffectUri, NoteUri, UnixTime
 
from rdflib.term import Node
 

	
 
log = logging.getLogger('seq.fader')
 

	
 
@dataclass
 
class Fader:
 
    graph: SyncedGraph
 
    uri: URIRef
 
    effect: EffectUri
 
    setEffectAttr: EffectAttr
 

	
 
    value: Optional[float]=None # mutable
 

	
 
    def __post_init__(self):
 
        self.ee = effecteval.EffectEval2(self.graph, self.effect)
 

	
 
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.faders: List[Fader] = []
 

	
 
        self.simpleOutputs = SimpleOutputs(self.graph)
 
        # self.simpleOutputs = SimpleOutputs(self.graph)
 
        log.info('fader adds handler')
 
        self.graph.addHandler(self.compileGraph)
 
        self.graph.addHandler(self._compile)
 
        self.lastLoopSucceeded = False
 

	
 
        # self.codeWatcher = CodeWatcher(onChange=self.onCodeChange)
 
        # have caller do this
 
        #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.graph.addHandler(self._compile)
 
        #self.updateLoop()
 

	
 
    @metrics('compile_graph_fader').time()
 
    def compileGraph(self) -> None:
 
    def _compile(self) -> None:
 
        """rebuild our data from the graph"""
 
        self.notes = []
 
        self.faders = []
 
        for fader in self.graph.subjects(RDF.type, L9['Fader']):
 
            def compileFader() -> Note:
 
                return self.compileFader(cast(URIRef, fader))
 
            effect = typedValue(EffectUri, self.graph, fader, L9['effect'])
 
            setting = typedValue(Node, self.graph, fader, L9['setting'])
 
            setAttr = typedValue(EffectAttr,  self.graph, setting, L9['effectAttr'])
 
            self.faders.append(Fader(self.graph, cast(URIRef, fader), effect, setAttr))           
 

	
 
            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, cast(NoteUri, fader),
 
                    timed=False)
 
        # this could go in a second, smaller addHandler call to avoid rebuilding Fader objs constantly
 
        for f in self.faders:
 
            f.value = typedValue(float, self.graph, f.uri, L9['value'])
 

	
 
    def computeOutput(self) -> DeviceSettings:
 
        notesSettings = []
 
        notesSettings:List[DeviceSettings] = []
 
        now = UnixTime(time.time())
 
        for note in self.notes:
 
            effectSettings, report = note.outputCurrent()
 
        for f in self.faders:
 
            if f.value is None:
 
                raise TypeError('f.value should be set by now')
 
            effectSettings = EffectSettings(self.graph, [(f.effect, f.setEffectAttr, f.value)])
 

	
 
            if effectSettings.s[EffectAttr(L9['strength'])]==0:
 
                continue
 
            print(f'{effectSettings=}')
 
            notesSettings.append(f.ee.compute(effectSettings))
 

	
 
            ee = effecteval.EffectEval(self.graph, note.effectClass, self.simpleOutputs)
 
            deviceSettings, report = ee.outputFromEffect(
 
                effectSettings,
 
                songTime=now, # probably wrong
 
                noteTime=now, # wrong
 
                )
 
            if deviceSettings:
 
                notesSettings.append(deviceSettings)
 
            # ee = effecteval.EffectEval(self.graph, f.effectClass, self.simpleOutputs)
 
            # deviceSettings, report = ee.outputFromEffect(
 
            #     effectSettings,
 
            #     songTime=now, # probably wrong
 
            #     noteTime=now, # wrong
 
            #     )
 
            # log.info(f'  𝅘𝅥𝅮  {uriTail(f.uri)}: {effectSettings=} -> {deviceSettings=}')
 
            # if deviceSettings:
 
            #     notesSettings.append(deviceSettings)
 
        return DeviceSettings.merge(self.graph, notesSettings)
 

	
 

	
 
    # @metrics('update_call_fader').time()
 
    # async def update(self):
 
    #     log.info(f'update {len(self.notes)=}')
 
    #     devSettings = self.computeOutput()
 
    #     with metrics('update_s3_send_fader').time():  # our measurement
 
    #         sendSecs = await self.sendToCollector(devSettings)
show/dance2023/theaterLightConfig.n3
Show inline comments
 
@prefix : <http://light9.bigasterisk.com/> .
 
@prefix dev: <http://light9.bigasterisk.com/theater/vet/device/> .
 
@prefix dmxA: <http://light9.bigasterisk.com/output/dmxA/> .
 
@prefix effect: <http://light9.bigasterisk.com/effect/> .
 
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
 
@prefix show: <http://light9.bigasterisk.com/show/dance2023/> .
 
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
 

	
 
:EffectFunction rdfs:comment """
 
Linked to a code function. That func's Inputs are 
 
1) magic stuff like time, 
 
2) the effectAttrs listed under (?effect :input ?)
 

	
 
Outputs are always a DeviceSettings list which can affect arbitrary devices.
 
""" .
 
show:fadePage1f0 rdfs:comment ":strength connects the fader to sub0" .
 

	
 
show:fadePage1 a :FadePage; rdfs:label "live controls"; :fader show:fadePage1f0, show:fadePage1f1 .
 
show:fadePage1f0 a :Fader; :column "1"; :effectAttr :strength; :effectClass effect:effect0; :value 0.306 .
 
show:fadePage1f1 a :Fader; :column "2"; :effectAttr :strength; :effectClass effect:effect2; :value 0.58 .
 
show:fadePage1f0 
 
  a :Fader; 
 
  :column "1";  
 
  :effect effect:sub0; 
 
  :setting [ :effectAttr :strength ] . # fader value is applied to this attr.
 

	
 

	
 
effect:effect0 
 
effect:sub0 
 
  a :Effect; 
 
  :setting effect:effect0_set1 .
 
effect:effect0_set1 :device dev:plain1; :deviceAttr :brightness; :scaledValue 0.5 .
 
  :effectFunction effect:scale; 
 
  :input [ :effectAttr :strength ]; # also put the time here if the func needs it.
 
  :setting 
 
     [ :device dev:plain1; :deviceAttr :brightness; :value 0.5 ],
 
     [ :device dev:par2;   :deviceAttr :color;      :value "#ff8000" ] .
 

	
 
effect:effect2 a :Effect; 
 
  rdfs:label "effect2"; 
 
  :publishAttr :strength; 
 
  :setting effect:effect2_set0, effect:effect2_set1, effect:effect2_set2, effect:effect2_set3 .
 
effect:effect2_set0 :device dev:par6; :deviceAttr :color; :scaledValue 0.251 .
 
effect:effect2_set1 :device dev:par2; :deviceAttr :color; :scaledValue 0.714 .
 
effect:effect2_set2 :device dev:strip1; :deviceAttr :color; :scaledValue 0.651 .
 
effect:effect2_set3 :device dev:strip2; :deviceAttr :color; :scaledValue 0.22 .
 
effect:scale
 
  a :EffectFunction;
 
  rdfs:label "a submaster- a few devices at specified colors";
 
  :input 
 
    [ :effectAttr :strength; :value 0 ], # overridden by fader
 
    [ :effectAttr :output; :value2 [ # something to say 'this attr value must resemble the following graph'
 
        :device     :valueRequired;  # ...and it's repeatable, unlike :strength
 
        :deviceAttr :valueRequired;
 
        :value      :valueRequired ] ] .
 
  
 

	
 
# show:fadePage1f1 a :Fader; :column "2"; :effectAttr :strength; :effect effect:effect2 .
 
# effect:effect2 a :Effect; 
 
#   :publishAttr :strength; 
 
#   :setting effect:effect2_set0, effect:effect2_set1, effect:effect2_set2, effect:effect2_set3 .
 
# effect:effect2_set0 :device dev:par6; :deviceAttr :color; :scaledValue 0.251 .
 
# effect:effect2_set1 :device dev:par2; :deviceAttr :color; :scaledValue 0.714 .
 
# effect:effect2_set2 :device dev:strip1; :deviceAttr :color; :scaledValue 0.651 .
 
# effect:effect2_set3 :device dev:strip2; :deviceAttr :color; :scaledValue 0.22 .
 

	
 

	
 
dev:strip1  a :Bar612601d;        :dmxUniverse dmxA:; :dmxBase 175 .
 
dev:strip2  a :ChauvetColorStrip; :dmxUniverse dmxA:; :dmxBase 12 .
 
dev:strip3  a :Bar612601d;        :dmxUniverse dmxA:; :dmxBase 178 .
 

	
 
dev:par90   a :LedPar90;          :dmxUniverse dmxA:; :dmxBase 16 .
 

	
 
dev:par1    a :LedPar54;          :dmxUniverse dmxA:; :dmxBase 84 .
 
dev:par2    a :LedPar54;          :dmxUniverse dmxA:; :dmxBase 1 .
 
dev:par3    a :LedPar54;          :dmxUniverse dmxA:; :dmxBase 188 .
 
dev:par4    a :LedPar54;          :dmxUniverse dmxA:; :dmxBase 105 .
0 comments (0 inline, 0 general)