diff --git a/bin/effectsequencer b/bin/effectsequencer new file mode 100644 --- /dev/null +++ b/bin/effectsequencer @@ -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() diff --git a/light9/effect/__init__.py b/light9/effect/__init__.py new file mode 100644 diff --git a/light9/effect/sequencer.py b/light9/effect/sequencer.py new file mode 100644 --- /dev/null +++ b/light9/effect/sequencer.py @@ -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) diff --git a/light9/networking.py b/light9/networking.py --- a/light9/networking.py +++ b/light9/networking.py @@ -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']) diff --git a/light9/vidref/musictime.py b/light9/vidref/musictime.py --- a/light9/vidref/musictime.py +++ b/light9/vidref/musictime.py @@ -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): """