Changeset - ba6fd5eaa0cf
[Not reviewed]
default
0 2 3
Drew Perttula - 9 years ago 2016-06-07 10:52:31
drewp@bigasterisk.com
start effectSequencer
Ignore-this: 6f8d5ba38410d26b0bf1660818c5d447
5 files changed with 186 insertions and 2 deletions:
0 comments (0 inline, 0 general)
bin/effectsequencer
Show inline comments
 
new file 100644
 
#!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()
light9/effect/__init__.py
Show inline comments
 
new file 100644
light9/effect/sequencer.py
Show inline comments
 
new file 100644
 
'''
 
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)
light9/networking.py
Show inline comments
 
@@ -41,6 +41,7 @@ dmxServerZmq = ServiceAddress(L9['dmxSer
 
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'])
light9/vidref/musictime.py
Show inline comments
 
@@ -11,7 +11,7 @@ class MusicTime(object):
 
    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 @@ class MusicTime(object):
 
        # 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):
 
        """
0 comments (0 inline, 0 general)