#!/usr/bin/python """ now launches like this: % bin/curvecalc http://light9.bigasterisk.com/show/dance2007/song1 todo: curveview should preserve more objects, for speed maybe """ from __future__ import division import time,textwrap,math,random,os,optparse import Tix as tk try: from dispatch import dispatcher except ImportError: import louie as dispatcher from twisted.internet import reactor,tksupport import jsonlib, restkit import twisted from twisted.web.client import Agent from twisted.internet.protocol import Protocol from twisted.internet.defer import Deferred from zope.interface import implements from twisted.internet.defer import succeed from twisted.web.iweb import IBodyProducer from rdflib import Literal, URIRef, RDF, RDFS from rdflib.Graph import Graph import rdflib import logging log = logging.getLogger() import run_local from light9 import Submaster, dmxclient, networking, showconfig, prof, Patch from light9.TLUtility import make_attributes_from_args from light9.zoomcontrol import Zoomcontrol from light9.curve import Curveset, Curvesetview from light9.wavelength import wavelength from light9.uihelpers import toplevelat from light9.namespaces import L9 import light9.Effects class GatherJson(Protocol): """calls back the 'finished' deferred with the parsed json data we received""" def __init__(self, finished): self.finished = finished self.buf = "" def dataReceived(self, bytes): self.buf += bytes def connectionLost(self, reason): self.finished.callback(jsonlib.read(self.buf, use_float=True)) class StringProducer(object): # http://twistedmatrix.com/documents/current/web/howto/client.html implements(IBodyProducer) def __init__(self, body): self.body = body self.length = len(body) def startProducing(self, consumer): consumer.write(self.body) return succeed(None) def pauseProducing(self): pass def stopProducing(self): pass class Music: def __init__(self): self.recenttime=0 self.player = Agent(reactor) dispatcher.connect(self.seekplay_or_pause,"music seek") def current_time(self): """return deferred which gets called with the current time""" d = self.player.request("GET", networking.musicUrl() + "time") d.addCallback(self._timeReturned) return d def _timeReturned(self, response): done = Deferred() done.addCallback(self._bodyReceived) response.deliverBody(GatherJson(done)) return done def _bodyReceived(self, data): dispatcher.send("input time",val=data['t']) return data['t'] # pass along to the real receiver def seekplay_or_pause(self,t): d = self.player.request("POST", networking.musicUrl() + "seekPlayOrPause", bodyProducer=StringProducer(jsonlib.write({"t" : t}))) class Expr(object): """singleton, provides functions for use in subterm expressions, e.g. chases""" def __init__(self): self.effectGlobals = light9.Effects.configExprGlobals() def exprGlobals(self, startDict, t): """globals dict for use by expressions""" glo = startDict.copy() # add in functions from Effects glo.update(self.effectGlobals) glo['nsin'] = lambda x: (math.sin(x * (2 * math.pi)) + 1) / 2 glo['ncos'] = lambda x: (math.cos(x * (2 * math.pi)) + 1) / 2 glo['within'] = lambda a, b: a < t < b glo['bef'] = lambda x: t < x def smoove(x): return -2 * (x ** 3) + 3 * (x ** 2) glo['smoove'] = smoove def aft(t, x, smooth=0): left = x - smooth / 2 right = x + smooth / 2 if left < t < right: return smoove((t - left) / (right - left)) return t > x glo['aft'] = lambda x, smooth=0: aft(t, x, smooth) def chan(name): return Submaster.Submaster( leveldict={Patch.get_dmx_channel(name) : 1.0}, temporary=True) glo['chan'] = chan def smooth_random(speed=1): """1 = new stuff each second, <1 is slower, fade-ier""" x = (t * speed) % len(self._smooth_random_items) x1 = int(x) x2 = (int(x) + 1) % len(self._smooth_random_items) y1 = self._smooth_random_items[x1] y2 = self._smooth_random_items[x2] return y1 + (y2 - y1) * ((x - x1)) def notch_random(speed=1): """1 = new stuff each second, <1 is slower, notch-ier""" x = (t * speed) % len(self._smooth_random_items) x1 = int(x) y1 = self._smooth_random_items[x1] return y1 glo['noise'] = smooth_random glo['notch'] = notch_random return glo exprglo = Expr() class Subexpr: curveset = None def __init__(self,curveset,expr=""): self.curveset = curveset self.lasteval = None self.expr=expr self._smooth_random_items = [random.random() for x in range(100)] def eval(self,t): if self.expr=="": dispatcher.send("expr_error",sender=self,exc="no expr, using 0") return 0 glo = self.curveset.globalsdict() glo['t'] = t glo = exprglo.exprGlobals(glo, t) try: self.lasteval = eval(self.expr,glo) except Exception,e: dispatcher.send("expr_error",sender=self,exc=e) else: dispatcher.send("expr_error",sender=self,exc="ok") return self.lasteval def expr(): doc = "python expression for level as a function of t, using curves" def fget(self): return self._expr def fset(self, value): self._expr = value dispatcher("expr_changed",sender=self) return locals() expr = property(**expr()) class Subexprview(tk.Frame): def __init__(self,master,se,**kw): self.subexpr=se tk.Frame.__init__(self,master,**kw) self.evar = tk.StringVar() e = self.ent = tk.Entry(self,textvariable=self.evar) e.pack(side='left',fill='x',exp=1) self.expr_changed() self.evar.trace_variable('w',self.evar_changed) dispatcher.connect(self.expr_changed,"expr_changed", sender=self.subexpr) self.error = tk.Label(self) self.error.pack(side='left') dispatcher.connect(lambda exc: self.error.config(text=str(exc)), "expr_error",sender=self.subexpr,weak=0) def expr_changed(self): if self.subexpr.expr!=self.evar.get(): self.evar.set(self.subexpr.expr) def evar_changed(self,*args): self.subexpr.expr = self.evar.get() class Subterm: """one Submaster and its Subexpr""" def __init__(self, submaster, subexpr): make_attributes_from_args('submaster', 'subexpr') def scaled(self, t): subexpr_eval = self.subexpr.eval(t) # we prevent any exceptions from escaping, since they cause us to # stop sending levels try: if isinstance(subexpr_eval, Submaster.Submaster): # if the expression returns a submaster, just return it return subexpr_eval else: # otherwise, return our submaster multiplied by the value # returned return self.submaster * subexpr_eval except Exception, e: dispatcher.send("expr_error", sender=self.subexpr, exc=str(e)) return Submaster.Submaster('Error: %s' % str(e), temporary=True) def __repr__(self): return "" % (self.submaster, self.subexpr) class Subtermview(tk.Frame): def __init__(self, master, graph, st, **kw): self.subterm = st tk.Frame.__init__(self,master,bd=1,relief='raised',**kw) l = tk.Label(self, text="sub %s" % self.subterm.submaster.name) l.pack(side='left') sev=Subexprview(self,self.subterm.subexpr) sev.pack(side='left',fill='both',exp=1) class Output: lastsendtime=0 lastsendlevs=None def __init__(self, subterms, music): make_attributes_from_args('subterms','music') self.recent_t=[] self.later = None self.update() def update(self): d = self.music.current_time() d.addCallback(self.update2) d.addErrback(self.updateerr) def updateerr(self,e): print e.getTraceback() dispatcher.send("update status",val=e.getErrorMessage()) if self.later and not self.later.cancelled and not self.later.called: self.later.cancel() self.later = reactor.callLater(1,self.update) def update2(self,t): # spot alsa soundcard offset is always 0, we get times about a # second ahead of what's really getting played #t = t - .7 dispatcher.send("update status", val="ok: receiving time from music player") if self.later and not self.later.cancelled and not self.later.called: self.later.cancel() self.later = reactor.callLater(.02, self.update) self.recent_t = self.recent_t[-50:]+[t] period = (self.recent_t[-1] - self.recent_t[0]) / len(self.recent_t) dispatcher.send("update period", val=period) self.send_dmx(t) def send_dmx(self,t): dispatcher.send("curves to sliders", t=t) scaledsubs=[] for st in self.subterms: scl = st.scaled(t) scaledsubs.append(scl) out = Submaster.sub_maxes(*scaledsubs) levs = out.get_levels() now=time.time() if now-self.lastsendtime>5 or levs!=self.lastsendlevs: dispatcher.send("output levels",val=levs) dmxclient.outputlevels(out.get_dmx_list(), twisted=1,clientid='curvecalc') self.lastsendtime = now self.lastsendlevs = levs def makeStatusLines(master): """various labels that listen for dispatcher signals""" for signame,textfilter in [ ('input time',lambda t: "%.2fs"%t), ('output levels', lambda levels: textwrap.fill("; ".join(["%s:%.2f"%(n,v) for n,v in levels.items()[:5] if v>0]),70)), ('update period',lambda t: "%.1fms"%(t*1000)), ('update status',lambda t: str(t)), ]: l = tk.Label(master, anchor='w', justify='left', text='%s:' % signame) l.pack(side='top',fill='x') dispatcher.connect(lambda val,l=l,sn=signame,tf=textfilter: l.config(text=sn+": "+tf(val)), signame, weak=False) def add_one_subterm(graph, subUri, curveset, subterms, master, expr=None): subname = graph.label(subUri) print "%s's label is %s" % (subUri, subname) if not subname: # fake sub, like for a chase st = graph.subjects(L9['sub'], subUri).next() subname = graph.label(st) print "using parent subterm's name instead. parent %r, name %r" % (st, subname) assert subname, "%s has no name" % subUri if expr is None: expr = '%s(t)' % subname term = Subterm(Submaster.Submaster(graph=graph, name=subname, sub=subUri), Subexpr(curveset,expr)) subterms.append(term) stv=Subtermview(master, graph, term) stv.pack(side='top',fill='x') return term def makeSubtermCommandRow(master, curveset, subterms, root, ssv, graph): """ the row that starts with 'reload subs' button """ f=tk.Frame(master,relief='raised',bd=1) newname = tk.StringVar() def add_cmd(evt): uri = L9['sub/%s' % newname.get()] graph.add((uri, RDF.type, L9.Subterm)) graph.add((uri, RDFS.label, Literal(newname.get()))) add_one_subterm(graph, uri, curveset, subterms, ssv, None) if evt.state & 4: # control key modifier curveset.new_curve(newname.get()) newname.set('') def reload_subs(): dispatcher.send('reload all subs') tk.Button(f, text="reload subs (C-r)", command=reload_subs).pack(side='left') tk.Label(f, text="new subterm named (C-Enter for curve too, C-n for focus):").pack(side='left') entry = tk.Entry(f, textvariable=newname) entry.pack(side='left', fill='x', exp=1) entry.bind("", add_cmd) def focus_entry(): entry.focus() dispatcher.connect(focus_entry, "focus new subterm", weak=False) return f def savesubterms(filename,subterms): raise NotImplementedError s="" for st in subterms: s=s+"%s %s\n" % (st.submaster.name, st.subexpr.expr) file(filename,'w').write(s) def createSubtermGraph(song, subterms): """rdf graph describing the subterms, readable by add_subterms_for_song""" graph = Graph() for subterm in subterms: assert subterm.submaster.name, "submaster has no name" uri = URIRef(song + "/subterm/" + subterm.submaster.name) graph.add((song, L9['subterm'], uri)) graph.add((uri, RDF.type, L9['Subterm'])) graph.add((uri, RDFS.label, Literal(subterm.submaster.name))) graph.add((uri, L9['sub'], L9['sub/%s' % subterm.submaster.name])) graph.add((uri, L9['expression'], Literal(subterm.subexpr.expr))) return graph def add_subterms_for_song(graph, song, curveset, subterms, master): for st in graph.objects(song, L9['subterm']): log.info("song %s has subterm %s", song, st) try: add_one_subterm(graph, graph.value(st, L9['sub']), curveset, subterms, master, graph.value(st, L9['expression'])) except rdflib.exceptions.UniquenessError: print "working around curvecalc save corruption" # curvecalc put all the expressions on one subterm, which is wrong for expr in graph.objects(st, L9['expression']): add_one_subterm(graph, graph.value(st, L9['sub']), curveset, subterms, master, expr) def graphPathForSubterms(song): return showconfig.subtermsForSong(showconfig.songFilenameFromURI(song)) + ".n3" def read_all_subs(graph): """read all sub files into this graph so when add_one_subterm tries to add, the sub will be available""" subsDir = showconfig.subsDir() for filename in os.listdir(subsDir): graph.parse(os.path.join(subsDir, filename), format="n3") def makeGraph(): graphOrig = showconfig.getGraph() graph = Graph() # a copy, since we're going to add subs into it for s in graphOrig: graph.add(s) read_all_subs(graph) return graph def savekey(song, subterms, curveset): print "saving", song g = createSubtermGraph(song, subterms) g.serialize(graphPathForSubterms(song), format="nt") curveset.save(basename=os.path.join(showconfig.curvesDir(), showconfig.songFilenameFromURI(song))) print "saved" def setupKeyBindings(root, song, subterms, curveset): root.bind("", lambda *args: savekey(song, subterms, curveset)) root.bind("", lambda evt: dispatcher.send('reload all subs')) root.bind("", lambda evt: dispatcher.send('focus new subterm')) root.bind("", lambda evt: dispatcher.send('focus new curve')) root.bind("",lambda ev: reactor.stop) root.bind("",lambda ev: reactor.stop) root.protocol('WM_DELETE_WINDOW', reactor.stop) def createHelpLines(root): for helpline in ["Bindings: C-s save subterms; Esc see current time; S-Esc see curtime to end; C-Esc show all; Mousewheel zoom; C-p play/pause music at mouse", "Curve point bindings: B1 drag point; C-B1 curve add point; S-B1 sketch points; Del selected points; 1..5 add point at time; B1 drag select points", "Available in functions: nsin/ncos period=amp=1; within(a,b) bef(x) aft(x) compare to time; smoove(x) cubic smoothstep; chan(name); curvename(t) eval curve"]: line = tk.Label(root, text=helpline, font="Helvetica -12 italic", anchor='w') line.pack(side='top',fill='x') def currentlyPlayingSong(): """ask the music player what song it's on""" t = jsonlib.read(restkit.Resource(networking.musicUrl()).get("time").body) if t['song'] is None: raise ValueError("music player is not playing any song") return URIRef(t['song']) def main(): parser = optparse.OptionParser() parser.set_usage("%prog [opts] [songURI]") parser.add_option("--sliders", action='store_true', help='use hardware sliders') parser.add_option("--skip-music", action='store_true', help="ignore music and smooth_music curve files") opts, args = parser.parse_args() logging.basicConfig(format="%(asctime)s %(levelname)-5s %(name)s %(filename)s:%(lineno)d: %(message)s") log.setLevel(logging.INFO) try: song = URIRef(args[0]) except IndexError: song = currentlyPlayingSong() log.debug("music") music=Music() graph = makeGraph() curveset = Curveset(sliders=opts.sliders) subterms = [] graph.parse(graphPathForSubterms(song), format='n3') log.debug("output") out = Output(subterms, music) musicfilename = showconfig.songOnDisk(song) maxtime = wavelength(musicfilename) dispatcher.connect(lambda: maxtime, "get max time", weak=False) root=tk.Tk() root.tk_setPalette("gray50") toplevelat("curvecalc",root) root.tk_focusFollowsMouse() root.title("Was Curvemaster 3000MX. Now Curvemaster 30002MX! - %s" % graph.label(song)) if 'fixed top rows': zc = Zoomcontrol(root) zc.pack(side='top', fill='x') if 'panes': panes = tk.PanedWindow(root, height=1) panes.add('curvesetView') panes.add('subterms') panes.pack(side='top', fill='both', expand=True) curvesetView = Curvesetview(panes.subwidget('curvesetView'), curveset, height=600) curvesetView.pack(fill='both', expand=True) subtermArea = tk.Frame(panes.subwidget('subterms'), height=100) subtermArea.pack(fill='both', expand=True) subtermScroll = tk.ScrolledWindow(subtermArea) subtermScroll.pack(fill='both') if 'fixed bottom rows': makeSubtermCommandRow(root, curveset, subterms, root, subtermArea, graph).pack(side='top', fill='x') makeStatusLines(root) helpBox = tk.Frame(root) createHelpLines(helpBox) helpBox.pack(side='top', fill='x') add_subterms_for_song(graph, song, curveset, subterms, subtermScroll.subwidget('window')) setupKeyBindings(root, song, subterms, curveset) # curvesetview must already exist, since this makes 'add_curve' # signals for all the initial curves curveset.load(basename=os.path.join(showconfig.curvesDir(), showconfig.songFilenameFromURI(song)), skipMusic=opts.skip_music) dispatcher.send("max time",maxtime=maxtime) dispatcher.send("show all") # this is scheduled after some tk shuffling, to try to minimize # the number of times we redraw the curve at startup. If tk is # very slow, it's ok. You'll just get some wasted redraws. reactor.callLater(.1, curvesetView.goLive) tksupport.install(root, ms=10) log.debug("run") prof.run(reactor.run, profile=False) main()