Changeset - dc474426845d
[Not reviewed]
default
0 3 0
Drew Perttula - 11 years ago 2014-06-03 07:11:12
drewp@bigasterisk.com
effecteval better logging. everyone put their curve files in the right place.
Ignore-this: 7cb0ea890a0602783eb581459f28b72d
3 files changed with 32 insertions and 18 deletions:
0 comments (0 inline, 0 general)
bin/effecteval
Show inline comments
 
@@ -12,68 +12,70 @@ from light9 import networking, showconfi
 
from light9.rdfdb.syncedgraph import SyncedGraph
 
from light9.namespaces import L9, RDF, RDFS
 
from light9.rdfdb.patch import Patch
 
from light9.effecteval.effect import EffectNode
 
from light9.greplin_cyclone import StatsForCyclone
 
from greplin import scales
 

	
 
from lib.cycloneerr import PrettyErrorHandler
 

	
 
class EffectEdit(PrettyErrorHandler, cyclone.web.RequestHandler):
 
    def get(self):
 
        self.write(open("light9/effecteval/effect.html").read())
 
    def delete(self):
 
        graph = self.settings.graph
 
        uri = URIRef(self.get_argument('uri'))
 
        with graph.currentState(tripleFilter=(None, L9['effect'], uri)) as g:
 
            song = ctx = list(g.subjects(L9['effect'], uri))[0]
 
        self.settings.graph.patch(Patch(delQuads=[
 
            (song, L9['effect'], uri, ctx),
 
            ]))
 
        
 
class SongEffects(PrettyErrorHandler, cyclone.web.RequestHandler):
 
    def post(self):
 
        song = URIRef(self.get_argument('uri'))
 
        drop = URIRef(self.get_argument('drop'))
 
        dropped = URIRef(self.get_argument('drop'))
 
        ctx = song
 
        graph = self.settings.graph
 
        effect = graph.sequentialUri(song + "/effect/e-")
 
        curve = graph.sequentialUri(song + "/curve/c-")
 
        effect = graph.sequentialUri(song + "/effect-")
 
        curve = graph.sequentialUri(song + "/curve-")
 

	
 
        with graph.currentState(
 
                tripleFilter=(drop, RDFS.label, None)) as g:
 
            dropSubLabel = g.label(drop)
 
                tripleFilter=(dropped, RDFS.label, None)) as g:
 
            droppedSubLabel = g.label(dropped)
 
        
 
        graph.patch(Patch(addQuads=[
 
            (song, L9['curve'], curve, ctx),
 
            (song, L9['effect'], effect, ctx),
 
            (effect, RDF.type, L9['Effect'], ctx),
 
            (effect, L9['code'],
 
             Literal('out = sub(%s, intensity=%s)' % (drop.n3(), curve.n3())),
 
             Literal('out = sub(%s, intensity=%s)' % (dropped.n3(), curve.n3())),
 
             ctx),
 
            (curve, RDF.type, L9['Curve'], ctx),
 
            (curve, RDFS.label, Literal('sub %s' % dropSubLabel), ctx),
 
            (curve, L9['points'], Literal('0 0'), ctx),
 
            (curve, RDFS.label, Literal('sub %s' % droppedSubLabel), ctx),
 
            ]))
 
        graph.patch(Patch(addQuads=[
 
            (curve, L9['points'], Literal('0 0'), Curve(uri=curve).curvePointsContext()),
 
            ]))
 
        
 
class SongEffectsUpdates(cyclone.websocket.WebSocketHandler):
 
    def connectionMade(self, *args, **kwargs):
 
        self.graph = self.settings.graph
 
        self.graph.addHandler(self.updateClient)
 
        
 
    def updateClient(self):
 
        # todo: abort if client is gone
 
        playlist = self.graph.value(showconfig.showUri(), L9['playList'])
 
        songs = list(self.graph.items(playlist))
 
        out = []
 
        for s in songs:
 
            out.append({'uri': s, 'label': self.graph.label(s)})
 
            out[-1]['effects'] = [{'uri': uri} for uri in sorted(self.graph.objects(s, L9['effect']))]
 
        self.sendMessage({'songs': out})
 
        
 
        
 
class EffectUpdates(cyclone.websocket.WebSocketHandler):
 
    """
 
    stays alive for the life of the effect page
 
    """
 
    def connectionMade(self, *args, **kwargs):
 
        log.info("websocket opened")
 
@@ -106,48 +108,50 @@ class EffectEval(PrettyErrorHandler, cyc
 
        songTime = json.loads(response.body)['t']
 

	
 
        node = EffectNode(self.settings.graph, uri)
 
        outSub = node.eval(songTime)
 
        self.write(json.dumps(outSub.get_dmx_list()))
 

	
 

	
 
# Completely not sure where the effect background loop should
 
# go. Another process could own it, and get this request repeatedly:
 
class SongEffectsEval(PrettyErrorHandler, cyclone.web.RequestHandler):
 
    def get(self):
 
        song = URIRef(self.get_argument('song'))
 
        effects = effectsForSong(self.settings.graph, song)
 
        raise NotImplementedError
 
        self.write(maxDict(effectDmxDict(e) for e in effects))
 
        # return dmx dict for all effects in the song, already combined
 

	
 
class EffectLoop(object):
 
    """maintains a collection of the current EffectNodes, gets time from
 
    music player, sends dmx"""
 
    def __init__(self, graph, stats):
 
        self.graph, self.stats = graph, stats
 
        self.currentSong = None
 
        self.currentEffects = []
 
        self.lastLogTime = 0
 
        self.lastLogMsg = ""
 
        self.graph.addHandler(self.setEffects)
 
        self.period = 1 / 30
 
        self.coastSecs = .3 # main reason to keep this low is to notice play/pause
 

	
 
        self.songTimeFromRequest = 0
 
        self.requestTime = 0 # unix sec for when we fetched songTime
 
        reactor.callLater(self.period, self.sendLevels)
 

	
 
    def setEffects(self):
 
        self.currentEffects = []
 
        if self.currentSong is None:
 
            return
 
        
 
        for effectUri in self.graph.objects(self.currentSong, L9['effect']):
 
            self.currentEffects.append(EffectNode(self.graph, effectUri))
 
        
 
    @inlineCallbacks
 
    def getSongTime(self):
 
        now = time.time()
 
        if now - self.requestTime < self.coastSecs:
 
            estimated = self.songTimeFromRequest
 
            if self.currentSong is not None and self.currentPlaying:
 
                estimated += now - self.requestTime
 
            returnValue((estimated, self.currentSong))
 
@@ -160,65 +164,79 @@ class EffectLoop(object):
 
            returnValue(
 
                (response['t'], (response['song'] and URIRef(response['song']))))
 
            
 
    @inlineCallbacks
 
    def sendLevels(self):
 
        t1 = time.time()
 
        try:
 
            with self.stats.sendLevels.time():
 
                with self.stats.getMusic.time():
 
                    songTime, song = yield self.getSongTime()
 

	
 
                if song != self.currentSong:
 
                    self.currentSong = song
 
                    # this may be piling on the handlers
 
                    self.graph.addHandler(self.setEffects)
 

	
 
                if song is None:
 
                    return
 

	
 
                outSubs = []
 
                for e in self.currentEffects:
 
                    outSubs.append(e.eval(songTime))
 
                out = Submaster.sub_maxes(*outSubs)
 

	
 
                self.logLevels(t1, out)
 
                dmx = out.get_dmx_list()
 

	
 
                if log.isEnabledFor(logging.DEBUG):
 
                    log.debug("send dmx: %r", out.get_levels())
 

	
 
                with self.stats.writeDmx.time():
 
                    yield dmxclient.outputlevels(dmx, twisted=True)
 

	
 
                elapsed = time.time() - t1
 
                dt = max(0, self.period - elapsed)
 
        except Exception:
 
            self.stats.errors += 1
 
            traceback.print_exc()
 
            dt = 1
 

	
 
        reactor.callLater(dt, self.sendLevels)
 
    
 
        
 
    def logLevels(self, now, out):
 
        # this would look nice on the top of the effecteval web pages too
 
        if log.isEnabledFor(logging.DEBUG):
 
            log.debug(self.logMessage(out))
 
        else:
 
            if now > self.lastLogTime + 5:
 
                msg = self.logMessage(out)
 
                if msg != self.lastLogMsg:
 
                    log.info(msg)
 
                    self.lastLogMsg = msg
 
                self.lastLogTime = now
 
                
 
    def logMessage(self, out):
 
        return ("send dmx: {%s}" %
 
                ", ".join("%r: %.3g" % (str(k), v)
 
                          for k,v in out.get_levels().items()))
 
        
 
class App(object):
 
    def __init__(self, show):
 
        self.show = show
 
        self.graph = SyncedGraph("effectEval")
 
        self.graph.initiallySynced.addCallback(self.launch)
 

	
 
        self.stats = scales.collection('/',
 
                                       scales.PmfStat('sendLevels'),
 
                                       scales.PmfStat('getMusic'),
 
                                       scales.PmfStat('writeDmx'),
 
                                       scales.IntStat('errors'),
 
                                       )
 

	
 
    def launch(self, *args):
 
        self.loop = EffectLoop(self.graph, self.stats)
 
        
 
        SFH = cyclone.web.StaticFileHandler
 
        self.cycloneApp = cyclone.web.Application(handlers=[
 
            (r'/()', SFH,
 
             {'path': 'light9/effecteval', 'default_filename': 'index.html'}),
 
            (r'/effect', EffectEdit),
 
            (r'/(websocket\.js)', SFH, {'path': 'light9/rdfdb/web/'}),
 
            (r'/effect\.js', StaticCoffee, {'src': 'light9/effecteval/effect.coffee'}),
 
            (r'/index\.js', StaticCoffee, {'src': 'light9/effecteval/index.coffee'}),
light9/curvecalc/curve.py
Show inline comments
 
@@ -310,49 +310,49 @@ class Curveset(object):
 
                num = None
 
        else:
 
            num = None
 
            
 
        dispatcher.send("add_curve", slider=num, knobEnabled=num is not None,
 
                        sender=self, name=name)
 

	
 
    def globalsdict(self):
 
        return self.curves.copy()
 
    
 
    def get_time_range(self):
 
        return 0, dispatcher.send("get max time")[0][1]
 

	
 
    def new_curve(self, name, renameIfExisting=True):
 
        if isinstance(name, Literal):
 
            name = str(name)
 
        if name=="":
 
            print "no name given"
 
            return
 
        if not renameIfExisting and name in self.curves:
 
            return
 
        while name in self.curves:
 
           name=name+"-1"
 

	
 
        uri = self.graph.sequentialUri(self.currentSong + '/curve/c-')
 
        uri = self.graph.sequentialUri(self.currentSong + '/curve-')
 
        c = Curve(uri)
 
        s, e = self.get_time_range()
 
        c.points.extend([(s, 0), (e, 0)])
 
        ctx = self.currentSong
 
        self.graph.patch(Patch(addQuads=[
 
            (self.currentSong, L9['curve'], uri, ctx),
 
            (uri, RDF.type, L9['Curve'], ctx),
 
            (uri, RDFS.label, Literal(name), ctx),
 
            ]))
 
        print "pts to", c.curvePointsContext()
 
        self.graph.patch(Patch(addQuads=[
 
            (uri, L9['points'], Literal(c.points_as_string()),
 
             c.curvePointsContext()),
 
            ]))
 

	
 
    def hw_slider_in(self, num, value):
 
        try:
 
            curve = self.curves[self.sliderCurve[num]]
 
        except KeyError:
 
            return
 

	
 
        now = time.time()
 
        if now < self.sliderIgnoreInputUntil.get(num):
 
            return
light9/effecteval/effect.py
Show inline comments
 
@@ -14,40 +14,36 @@ def uriFromCode(s):
 
    if (s[0], s[-1]) == ('<', '>'):
 
        return URIRef(s[1:-1])
 
    raise NotImplementedError
 

	
 
class EffectNode(object):
 
    def __init__(self, graph, uri):
 
        self.graph, self.uri = graph, uri
 
        # this is not expiring at the right time, when an effect goes away
 
        self.graph.addHandler(self.prepare)
 

	
 
    def prepare(self):
 
        self.code = self.graph.value(self.uri, L9['code'])
 
        if self.code is None:
 
            raise ValueError("effect %s has no code" % self.uri)
 
        m = re.match(r'^out = sub\((.*?), intensity=(.*?)\)', self.code)
 
        if not m:
 
            raise NotImplementedError
 
        subUri = uriFromCode(m.group(1))
 
        subs = Submaster.get_global_submasters(self.graph)
 
        self.sub = subs.get_sub_by_uri(subUri)
 
        
 
        intensityCurve = uriFromCode(m.group(2))
 
        self.curve = Curve(uri=intensityCurve)
 

	
 
        # read from disk ok? how do we know to reread? start with
 
        # mtime. the mtime check could be done occasionally so on
 
        # average we read at most one curve's mtime per effectLoop.       
 

	
 
        pts = self.graph.value(intensityCurve, L9['points'])
 
        if pts is None:
 
            log.info("curve %r has no points" % intensityCurve)
 
        else:
 
            self.curve.set_from_string(pts)
 

	
 
        
 
    def eval(self, songTime):
 
        # consider http://waxeye.org/ for a parser that can be used in py and js
 
        level = self.curve.eval(songTime)
 
        scaledSubs = self.sub * level
 
        return scaledSubs
0 comments (0 inline, 0 general)