changeset 1373:ba6fd5eaa0cf

start effectSequencer Ignore-this: 6f8d5ba38410d26b0bf1660818c5d447
author Drew Perttula <drewp@bigasterisk.com>
date Tue, 07 Jun 2016 10:52:31 +0000
parents f427801da9f6
children 640c9e4de909
files bin/effectsequencer light9/effect/__init__.py light9/effect/sequencer.py light9/networking.py light9/vidref/musictime.py
diffstat 4 files changed, 186 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/effectsequencer	Tue Jun 07 10:52:31 2016 +0000
@@ -0,0 +1,78 @@
+#!bin/python
+"""
+plays back effect notes from the timeline
+"""
+from __future__ import division
+from run_local import log
+from twisted.internet import reactor
+from light9.greplin_cyclone import StatsForCyclone
+from light9.rdfdb.syncedgraph import SyncedGraph
+from light9 import networking, showconfig
+from greplin import scales
+import optparse, sys, logging
+import cyclone.web
+from rdflib import URIRef
+from light9.effect.sequencer import Sequencer
+import treq
+import json
+from light9.rdfdb import clientsession
+
+class App(object):
+    def __init__(self, show, session):
+        self.show = show
+        self.session = session
+
+        self.graph = SyncedGraph(networking.rdfdb.url, "effectSequencer")
+        self.graph.initiallySynced.addCallback(self.launch)
+
+
+        self.stats = scales.collection('/',
+                                       scales.PmfStat('sendLevels'),
+                                       scales.PmfStat('getMusic'),
+                                       scales.PmfStat('evals'),
+                                       scales.PmfStat('sendOutput'),
+                                       scales.IntStat('errors'),
+                                       )
+    def launch(self, *args):
+        print 'launch'
+        def sendToCollector(settings):
+            return treq.put(networking.collector.path('attrs'),
+                            data=json.dumps({'settings': settings,
+                                             'client': 'effectSequencer',
+                                             'clientSession': self.session}))
+
+        seq = Sequencer(self.graph, sendToCollector)
+
+        self.cycloneApp = cyclone.web.Application(handlers=[
+            (r'/stats', StatsForCyclone),
+        ],
+                                                  debug=True,
+                                                  graph=self.graph,
+                                                  stats=self.stats)
+        reactor.listenTCP(networking.effectSequencer.port, self.cycloneApp)
+        log.info("listening on %s" % networking.effectSequencer.port)
+
+
+if __name__ == "__main__":
+    parser = optparse.OptionParser()
+    parser.add_option('--show',
+        help='show URI, like http://light9.bigasterisk.com/show/dance2008',
+                      default=showconfig.showUri())
+    parser.add_option("-v", "--verbose", action="store_true",
+                      help="logging.DEBUG")
+    parser.add_option("--twistedlog", action="store_true",
+                      help="twisted logging")
+    clientsession.add_option(parser)
+    (options, args) = parser.parse_args()
+    log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
+
+    if not options.show:
+        raise ValueError("missing --show http://...")
+        
+    session = clientsession.getUri('effectSequencer', options)
+
+    app = App(URIRef(options.show), session)
+    if options.twistedlog:
+        from twisted.python import log as twlog
+        twlog.startLogging(sys.stderr)
+    reactor.run()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/effect/sequencer.py	Tue Jun 07 10:52:31 2016 +0000
@@ -0,0 +1,104 @@
+'''
+copies from effectloop.py, which this should replace
+'''
+
+from __future__ import division
+from rdflib import URIRef, Literal
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from webcolors import hex_to_rgb, rgb_to_hex
+import time, json, logging, traceback, bisect
+
+from light9.namespaces import L9, RDF, RDFS
+from light9.vidref.musictime import MusicTime
+
+log = logging.getLogger('sequencer')
+
+class Note(object):
+    def __init__(self, graph, uri):
+        g = self.graph = graph
+        self.uri = uri
+        floatVal = lambda s, p: float(g.value(s, p).toPython())
+        originTime = floatVal(uri, L9['originTime'])
+        self.points = []
+        for curve in g.objects(uri, L9['curve']):
+            if g.value(curve, L9['attr']) != L9['strength']:
+                continue
+            for point in g.objects(curve, L9['point']):
+                self.points.append((
+                    originTime + floatVal(point, L9['time']),
+                    floatVal(point, L9['value'])))
+            self.points.sort()
+
+        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 activeAt(self, t):
+        return self.points[0][0] <= t <= self.points[-1][0]
+
+    def evalCurve(self, t):
+        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):
+
+        c = int(255 * self.evalCurve(t))
+        color = [0, 0, 0]
+        if self.setting[1] == L9['red']: # throwaway
+            color[0] = c
+        elif self.setting[1] == L9['blue']:
+            color[2] = c
+        
+        return [
+            # device, attr, lev
+            (self.setting[0],
+             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.notes = {} # song: [notes]
+        self.graph.addHandler(self.compileGraph)
+        self.update()
+
+    def compileGraph(self):
+        """rebuild our data from the graph"""
+        g = self.graph
+        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))
+        
+    def update(self):
+        reactor.callLater(1/30, self.update)
+
+        musicState = self.music.getLatest()
+        song = URIRef(musicState['song']) if 'song' in musicState else None
+        if 't' not in musicState:
+            return
+        t = musicState['t']
+
+        settings = []
+        
+        for note in self.notes.get(song, []):
+            # we have to send zeros to make past settings go
+            # away. might be better for collector not to merge our
+            # past requests, and then we can omit zeroed notes?
+            settings.extend(note.outputSettings(t))
+        self.sendToCollector(settings)
--- a/light9/networking.py	Tue Jun 07 10:51:23 2016 +0000
+++ b/light9/networking.py	Tue Jun 07 10:52:31 2016 +0000
@@ -41,6 +41,7 @@
 collector = ServiceAddress(L9['collector'])
 collectorZmq = ServiceAddress(L9['collectorZmq'])
 effectEval = ServiceAddress(L9['effectEval'])
+effectSequencer = ServiceAddress(L9['effectSequencer'])
 keyboardComposer = ServiceAddress(L9['keyboardComposer'])
 musicPlayer = ServiceAddress(L9['musicPlayer'])
 oscDmxServer = ServiceAddress(L9['oscDmxServer'])
--- a/light9/vidref/musictime.py	Tue Jun 07 10:51:23 2016 +0000
+++ b/light9/vidref/musictime.py	Tue Jun 07 10:52:31 2016 +0000
@@ -11,7 +11,7 @@
     fetch times from ascoltami in a background thread; return times
     upon request, adjusted to be more precise with the system clock
     """
-    def __init__(self, period=.2, onChange=lambda position: None):
+    def __init__(self, period=.2, onChange=lambda position: None, pollCurvecalc=True):
         """period is the seconds between http time requests.
 
         We call onChange with the time in seconds and the total time
@@ -30,7 +30,8 @@
         # driven by our pollCurvecalcTime and also by Gui.incomingTime
         self.lastHoverTime = None # None means "no recent value"
         self.pollMusicTime()
-        self.pollCurvecalcTime()
+        if pollCurvecalc:
+            self.pollCurvecalcTime()
 
     def getLatest(self, frameTime=None):
         """