changeset 1414:c35ec37c3c6e

sequencer reloads effecteval on the fly. plus some /stats support. Ignore-this: 964f4c9007de6532457e0a507d2106f1
author Drew Perttula <drewp@bigasterisk.com>
date Fri, 10 Jun 2016 06:56:34 +0000
parents cb1379b3555b
children edc17fdcaaf1
files bin/effectsequencer light9/effect/effecteval.py light9/effect/sequencer.py light9/subclient.py light9/web/index.html light9/web/timeline/timeline.coffee show/dance2016/effect.n3 show/dance2016/song1.n3
diffstat 8 files changed, 169 insertions(+), 68 deletions(-) [+]
line wrap: on
line diff
--- a/bin/effectsequencer	Fri Jun 10 03:27:22 2016 +0000
+++ b/bin/effectsequencer	Fri Jun 10 06:56:34 2016 +0000
@@ -32,7 +32,6 @@
                                        scales.IntStat('errors'),
                                        )
     def launch(self, *args):
-        print 'launch'
         self.seq = Sequencer(
             self.graph,
             lambda settings: sendToCollector('effectSequencer', self.session,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/effect/effecteval.py	Fri Jun 10 06:56:34 2016 +0000
@@ -0,0 +1,77 @@
+from __future__ import division
+from rdflib import URIRef, Literal
+from light9.namespaces import L9, RDF
+from webcolors import rgb_to_hex
+import math
+
+
+class EffectEval(object):
+    """
+    runs one effect's code to turn effect attr settings into output
+    device settings. No state; suitable for reload().
+    """
+    def __init__(self, graph, effect):
+        self.graph = graph
+        self.effect = effect
+        
+        #for ds in g.objects(g.value(uri, L9['effectClass']), L9['deviceSetting']):
+        #    self.setting = (g.value(ds, L9['device']), g.value(ds, L9['attr']))
+
+    def outputFromEffect(self, effectSettings, songTime):
+        """
+        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.
+        """
+        attr, value = effectSettings[0]
+        value = float(value)
+        assert attr == L9['strength']
+        c = int(255 * value)
+        color = [0, 0, 0]
+        if self.effect == L9['effect/RedStrip']: # throwaway
+
+            mov = URIRef('http://light9.bigasterisk.com/device/moving1')
+            col = [
+                    (songTime + .0) % 1.0,
+                    (songTime + .4) % 1.0,
+                    (songTime + .8) % 1.0,
+                ]
+            return [
+                # device, attr, lev
+                
+                (mov, L9['color'], Literal(rgb_to_hex([value*x*255 for x in col]))),
+                (mov, L9['rx'], Literal(100 + 70 * math.sin(songTime*2))),
+            ] * (value>0)
+
+        elif self.effect == L9['effect/BlueStrip']:
+            color[2] = c
+        elif self.effect == L9['effect/WorkLight']:
+            color[1] = c
+        elif self.effect == L9['effect/Curtain']:
+            color[0] = color[2] = 70/255 * c
+        elif self.effect == L9['effect/Strobe']:
+            attr, value = effectSettings[0]
+            assert attr == L9['strength']
+            strength = float(value)
+            rate = 2
+            duty = .3
+            offset = 0
+            f = (((songTime + offset) * rate) % 1.0)
+            c = (f < duty) * strength
+            col = rgb_to_hex([c * 255, c * 255, c * 255])
+            return [
+                (L9['device/colorStrip'], L9['color'], Literal(col)),
+            ]
+        else:
+            color[0] = color[1] = color[2] = c
+
+        return [
+            # device, attr, lev
+            (URIRef('http://light9.bigasterisk.com/device/moving1'),
+             URIRef("http://light9.bigasterisk.com/color"),
+             Literal(rgb_to_hex(color)))
+            ]
+        
+
+
+    
--- a/light9/effect/sequencer.py	Fri Jun 10 03:27:22 2016 +0000
+++ b/light9/effect/sequencer.py	Fri Jun 10 06:56:34 2016 +0000
@@ -9,13 +9,22 @@
 import json, logging, bisect
 import treq
 import math
+import time
+from twisted.internet.inotify import INotify
+from twisted.python.filepath import FilePath
 
 from light9 import networking
 from light9.namespaces import L9, RDF
 from light9.vidref.musictime import MusicTime
+from light9.effect import effecteval
+from greplin import scales
 
 log = logging.getLogger('sequencer')
+stats = scales.collection('/sequencer/',
+                                       scales.PmfStat('update'),
+                          scales.DoubleStat('recentFps'),
 
+                                       )
 def sendToCollector(client, session, settings):
     return treq.put(networking.collector.path('attrs'),
                     data=json.dumps({'settings': settings,
@@ -24,10 +33,11 @@
 
 
 class Note(object):
-    def __init__(self, graph, uri):
+    def __init__(self, graph, uri, effectevalModule):
         g = self.graph = graph
         self.uri = uri
-        self.effectEval = EffectEval(graph, g.value(uri, L9['effectClass']))
+        self.effectEval = effectevalModule.EffectEval(
+            graph, g.value(uri, L9['effectClass']))
         floatVal = lambda s, p: float(g.value(s, p).toPython())
         originTime = floatVal(uri, L9['originTime'])
         self.points = []
@@ -66,81 +76,56 @@
         return self.effectEval.outputFromEffect(effectSettings, t)
                 
 
-class EffectEval(object):
-    """
-    runs one effect's code to turn effect attr settings into output
-    device settings
-    """
-    def __init__(self, graph, effect):
-        self.graph = graph
-        self.effect = effect
-        
-        #for ds in g.objects(g.value(uri, L9['effectClass']), L9['deviceSetting']):
-        #    self.setting = (g.value(ds, L9['device']), g.value(ds, L9['attr']))
-
-    def outputFromEffect(self, effectSettings, songTime):
-        """
-        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.
-        """
-        attr, value = effectSettings[0]
-        value = float(value)
-        assert attr == L9['strength']
-        c = int(255 * value)
-        color = [0, 0, 0]
-        if self.effect == L9['effect/RedStrip']: # throwaway
-
-            mov = URIRef('http://light9.bigasterisk.com/device/moving1')
-            col = [
-                    (songTime + .1) % 1.0,
-                    (songTime + .4) % 1.0,
-                    (songTime + .8) % 1.0,
-                ]
-            print 'col', col
-            return [
-                # device, attr, lev
-                
-                (mov, L9['color'], Literal(rgb_to_hex([value*x*255 for x in col]))),
-                (mov, L9['rx'], Literal(100 + 70 * math.sin(songTime*2))),
-            ]
-
-        elif self.effect == L9['effect/BlueStrip']:
-            color[2] = c
-        elif self.effect == L9['effect/WorkLight']:
-            color[1] = c
-        elif self.effect == L9['effect/Curtain']:
-            color[0] = color[2] = 70/255 * c
-        else:
-            color[0] = color[1] = color[2] = c
-
-        return [
-            # device, attr, lev
-            (URIRef('http://light9.bigasterisk.com/device/moving1'),
-             URIRef("http://light9.bigasterisk.com/color"),
-             Literal(rgb_to_hex(color)))
-            ]
-        
 class Sequencer(object):
     def __init__(self, graph, sendToCollector):
         self.graph = graph
         self.sendToCollector = sendToCollector
         self.music = MusicTime(period=.2, pollCurvecalc=False)
 
+        self.recentUpdateTimes = []
+        self.lastStatLog = 0
         self.notes = {} # song: [notes]
         self.graph.addHandler(self.compileGraph)
         self.update()
 
+        self.watchCode()
+
+    def watchCode(self):
+        self.notifier = INotify()
+        self.notifier.startReading()
+        self.notifier.watch(
+            FilePath(effecteval.__file__.replace('.pyc', '.py')),
+            callbacks=[self.codeChange])
+
+    def codeChange(self, watch, path, mask):
+        def go():
+            reload(effecteval)
+            self.graph.addHandler(self.compileGraph)
+        # in case we got an event at the start of the write
+        reactor.callLater(.1, go) 
+
     def compileGraph(self):
         """rebuild our data from the graph"""
         g = self.graph
+
+        log.info("compileGraph")
+        reload(effecteval)
+        
         for song in g.subjects(RDF.type, L9['Song']):
             self.notes[song] = []
             for note in g.objects(song, L9['note']):
-                self.notes[song].append(Note(g, note))
+                self.notes[song].append(Note(g, note, effecteval))
+
+    @stats.update.time()
+    def update(self):
+        now = time.time()
+        self.recentUpdateTimes = self.recentUpdateTimes[-20:] + [now]
+        stats.recentFps = len(self.recentUpdateTimes) / (self.recentUpdateTimes[-1] - self.recentUpdateTimes[0] + .0001)
+        if now > self.lastStatLog + 10:
+            log.info("%.2f fps", stats.recentFps)
+            self.lastStatLog = now
         
-    def update(self):
-        reactor.callLater(1/30, self.update)
+        reactor.callLater(1/50, self.update)
 
         musicState = self.music.getLatest()
         song = URIRef(musicState['song']) if musicState.get('song') else None
@@ -155,6 +140,6 @@
             # away. might be better for collector not to merge our
             # past requests, and then we can omit zeroed notes?
             outs = note.outputSettings(t)
-            print 'out', outs
+            #print 'out', outs
             settings.extend(outs)
         self.sendToCollector(settings)
--- a/light9/subclient.py	Fri Jun 10 03:27:22 2016 +0000
+++ b/light9/subclient.py	Fri Jun 10 06:56:34 2016 +0000
@@ -20,5 +20,4 @@
 
     def _send_sub(self):
         outputSettings = self.get_output_settings()
-        print outputSettings
         sendToCollector('subclient', self.session, outputSettings)
--- a/light9/web/index.html	Fri Jun 10 03:27:22 2016 +0000
+++ b/light9/web/index.html	Fri Jun 10 06:56:34 2016 +0000
@@ -28,6 +28,7 @@
         <div>
           <span class="left"><a class="big" href="{{name}}/">{{name}}</a></span>
           <span><button on-click="click">window</button></span>
+          <span><a href="{{name}}/stats">stats</a></span>
         </div>
       </template>
       <script>
@@ -49,16 +50,17 @@
     <h1>light9 home page</h1>
 
     <div style="float: left">
+      <service-button-row name="."></service-button-row>
       <service-button-row name="rdfdb"></service-button-row>
-      <service-button-row name="effectEval"></service-button-row>
+      <service-button-row name="collector"></service-button-row>
+      <service-button-row name="effectSequencer"></service-button-row>
       <service-button-row name="ascoltami"></service-button-row>
-      <service-button-row name="subComposer"></service-button-row>
       <service-button-row name="subServer"></service-button-row>
       <service-button-row name="picamserve"></service-button-row>
       <service-button-row name="vidref"></service-button-row>
-      <service-button-row name="."></service-button-row>
       <service-button-row name="live"></service-button-row>
       <service-button-row name="timeline"></service-button-row>
+      <service-button-row name="subComposer"></service-button-row>
     </div>
     
   </body>
--- a/light9/web/timeline/timeline.coffee	Fri Jun 10 03:27:22 2016 +0000
+++ b/light9/web/timeline/timeline.coffee	Fri Jun 10 06:56:34 2016 +0000
@@ -469,9 +469,22 @@
     if elem
       elem.remove()
       delete @elemById[uri]
+
+  anyPointsInView: (pts) ->
+    for pt in pts
+      if pt.e(1) > -100 && pt.e(1) < 2500
+        return true
+    return false
     
   setNote: (uri, curvePts, effectLabel) ->
-    elem = @getOrCreateElem(uri, 'notes', 'path', {style:"fill:#53774b; stroke:#000000; stroke-width:1.5;"})
+    areaId = uri + '/area'
+    labelId = uri + '/label'
+    if not @anyPointsInView(curvePts)
+      @clearElem(areaId)
+      @clearElem(labelId)
+      return
+    elem = @getOrCreateElem(areaId, 'notes', 'path',
+      {style:"fill:#53774b; stroke:#000000; stroke-width:1.5;"})
     elem.setAttribute('d', svgPathFromPoints(curvePts))
 
     elem = @getOrCreateElem(uri+'/label', 'noteLabels', 'text', {style: "font-size:13px;line-height:125%;font-family:'Verana Sans';text-align:start;text-anchor:start;fill:#000000;"})
@@ -504,5 +517,9 @@
     ])
 
   setAdjusterConnector: (uri, center, target) ->
+    id = uri + '/adj'
+    if not @anyPointsInView([center, target])
+      @clearElem(uri)
+      return
     elem = @getOrCreateElem(uri, 'connectors', 'path', {style: "fill:none;stroke:#d4d4d4;stroke-width:0.9282527;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:2.78475821, 2.78475821;stroke-dashoffset:0;"})
     elem.setAttribute('d', svgPathFromPoints([center, target]))
--- a/show/dance2016/effect.n3	Fri Jun 10 03:27:22 2016 +0000
+++ b/show/dance2016/effect.n3	Fri Jun 10 06:56:34 2016 +0000
@@ -38,6 +38,10 @@
 :fc1p3 :time 0.15; :value 0 .
 
 
+effect:Strobe a :Effect;  rdfs:label "strobe";
+  :publishAttr :strength, :rate, :offset, :duty .
+
+
 effect:WorkLight a :Effect;
   rdfs:label "work light";
   :publishAttr :strength;
--- a/show/dance2016/song1.n3	Fri Jun 10 03:27:22 2016 +0000
+++ b/show/dance2016/song1.n3	Fri Jun 10 06:56:34 2016 +0000
@@ -7,9 +7,18 @@
 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
 
 :a01 :effectAttr :chaseOffset; :value 0.12 .
-<http://light9.bigasterisk.com/show/dance2016/song1> :note song:n1, song:n25, song:n26, song:n27, song:n28 .
+<http://light9.bigasterisk.com/show/dance2016/song1> :note song:n0, song:n1, song:n2, song:n25, song:n26, song:n27, song:n28 .
 :ao0 :effectAttr :chaseTempo; :value 100 .
 
+song:n0 a :Note; :curve song:n0c0; :effectClass effect:WorkLight;
+     :originTime 161.162 .
+
+song:n0c0 a :Curve; :attr :strength; :point song:n0c0p0, song:n0c0p1, song:n0c0p2, song:n0c0p3 .
+song:n0c0p0 :time 0.000; :value 0.000 .
+song:n0c0p1 :time 1.000; :value 1.000 .
+song:n0c0p2 :time 2.000; :value 1.000 .
+song:n0c0p3 :time 3.000; :value 0.000 .
+
 song:n1 a :Note; :attrOverride :ao0, :ao1; :curve song:n1c1;
      :effectClass effect:RedStrip; :originTime 41.011 .
 
@@ -19,6 +28,9 @@
 song:n1c1p2 :time 2; :value 1 .
 song:n1c1p3 :time 3; :value 0 .
 
+song:n2 a :Note; :curve song:n2c0; :effectClass effect:Strobe;
+     :originTime 47.087 .
+
 song:n25 a :Note; :curve song:n25c0; :effectClass effect:BlueStrip;
      :originTime 71.234 .
 
@@ -54,3 +66,9 @@
 song:n28c0p1 :time 1.000; :value 1.000 .
 song:n28c0p2 :time 2.000; :value 1.000 .
 song:n28c0p3 :time 3.000; :value 0.000 .
+
+song:n2c0 a :Curve; :attr :strength; :point song:n2c0p0, song:n2c0p1, song:n2c0p2, song:n2c0p3 .
+song:n2c0p0 :time 0.000; :value 0.000 .
+song:n2c0p1 :time 1.000; :value 1.000 .
+song:n2c0p2 :time 2.000; :value 1.000 .
+song:n2c0p3 :time 3.000; :value 0.000 .