Mercurial > code > home > repos > light9
diff bin/curvecalc @ 210:f41004d5a507
factored out some networking, new show/ layout, curvecalc works
author | drewp@bigasterisk.com |
---|---|
date | Sun, 10 Apr 2005 20:54:14 +0000 |
parents | flax/curvecalc@3905d3c92aaa |
children | 9b360ee8636e |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/curvecalc Sun Apr 10 20:54:14 2005 +0000 @@ -0,0 +1,601 @@ +#!/usr/bin/python + +""" +todo: curveview should preserve more objects, for speed maybe + +""" +from __future__ import division +import xmlrpclib,time,socket,sys,textwrap,math,glob,random,os +from bisect import bisect_left,bisect,bisect_right +import Tkinter as tk +from dispatch import dispatcher +from twisted.internet import reactor,tksupport +import twisted +from twisted.web.xmlrpc import Proxy + +import run_local +from light9 import Submaster, dmxclient +from light9.TLUtility import make_attributes_from_args +from light9.zoomcontrol import Zoomcontrol + +sys.path.append("../../semprini") +from lengther import wavelength # for measuring duration of .wav + +class Curve: + """curve does not know its name. see Curveset""" + points = None # x-sorted list of (x,y) + def __init__(self): + self.points = [(0,0),(10,0)] + + def load(self,filename): + self.points[:]=[] + for line in file(filename): + self.points.append(tuple([float(a) for a in line.split()])) + self.points.sort() + dispatcher.send("points changed",sender=self) + + def save(self,filename): + if filename.endswith('-music') or filename.endswith('_music'): + print "not saving music track" + return + f = file(filename,'w') + for p in self.points: + f.write("%s %s\n" % p) + f.close() + + def eval(self,t): + i = bisect_left(self.points,(t,None))-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 insert_pt(self,new_pt): + i = bisect(self.points,(new_pt[0],None)) + self.points.insert(i,new_pt) + __call__=eval + +class Curveview(tk.Canvas): + def __init__(self,master,curve,**kw): + self.curve=curve + self._time = 0 + tk.Canvas.__init__(self,master,width=10,height=10, + relief='sunken',bd=1, + closeenough=5,takefocus=1, **kw) + self.selected_points=[] # idx of points being dragged + self.update() + # self.bind("<Enter>",self.focus) + dispatcher.connect(self.input_time,"input time") + dispatcher.connect(self.update,"zoom changed") + dispatcher.connect(self.update,"points changed",sender=self.curve) + self.bind("<Configure>",self.update) + for x in range(1, 6): + def add_kb_marker_point(evt, x=x): + print "add_kb_marker_point", evt + self.add_point((self.current_time(), (x - 1) / 4.0)) + + self.bind("<Key-%s>" % x, add_kb_marker_point) + + + for butnum,factor in (5, 1.5),(4, 1/1.5): + self.bind("<ButtonPress-%s>"%butnum, + lambda ev,factor=factor: + dispatcher.send("zoom about mouse", + t=self.world_from_screen(ev.x,0)[0], + factor=factor)) + self.bind("<Key-Escape>",lambda ev: + dispatcher.send("see time", + t=self.current_time())) + def current_time(self): + return self._time + + def screen_from_world(self,p): + start,end = self.zoom + ht = self.winfo_height() + return (p[0]-start)/(end-start)*self.winfo_width(), (ht-5)-p[1]*(ht-10) + def world_from_screen(self,x,y): + start,end = self.zoom + ht = self.winfo_height() + return x/self.winfo_width()*(end-start)+start, ((ht-5)-y)/(ht-10) + + def input_time(self,val): + t=val + pts = self.screen_from_world((val,0))+self.screen_from_world((val,1)) + self.delete('timecursor') + self.create_line(*pts,**dict(width=2,fill='red',tags=('timecursor',))) + self._time = t + def update(self,*args): + + self.zoom = dispatcher.send("zoom area")[0][1] + cp = self.curve.points + + visible_x = (self.world_from_screen(0,0)[0], + self.world_from_screen(self.winfo_width(),0)[0]) + + visleftidx = max(0,bisect_left(cp,(visible_x[0],None))-1) + visrightidx = min(len(cp)-1,bisect_left(cp,(visible_x[1],None))+1) + + visible_points = cp[visleftidx:visrightidx+1] + visible_idxs = range(visleftidx,visrightidx+1) + + self.delete('curve') + + self._draw_markers(visible_x) + + self._draw_line(visible_idxs,visible_points) + + self.dots = {} # idx : canvas rectangle + + if len(visible_points)<50: + self._draw_handle_points(visible_idxs,visible_points) + + def _draw_markers(self,visible_x): + mark = self._draw_one_marker + + mark(0,"0") + t1,t2=visible_x + if t2-t1<30: + for t in range(int(t1),int(t2)+1): + mark(t,str(t)) + mark(-4,"-4") + + endtimes = dispatcher.send("get max time") + if endtimes: + endtime = endtimes[0][1] + mark(endtime,"end %.1f"%endtime) + mark(endtime+10,"post %.1f"%(endtime+10)) + + def _draw_one_marker(self,t,label): + x = self.screen_from_world((t,0))[0] + self.create_line(x,self.winfo_height(),x,self.winfo_height()-20, + tags=('curve',)) + self.create_text(x,self.winfo_height()-20,text=label,anchor='s', + tags=('curve',)) + + + def _draw_line(self,visible_idxs,visible_points): + linepts=[] + step=1 + linewidth=2 + if len(visible_points)>800: + step = int(len(visible_points)/800) + linewidth=1 + for p in visible_points[::step]: + linepts.extend(self.screen_from_world(p)) + if len(linepts)<4: + return + line = self.create_line(*linepts,**dict(width=linewidth,tags='curve')) + + # canvas doesnt have keyboard focus, so i can't easily change the + # cursor when ctrl is pressed + # def curs(ev): + # print ev.state + # self.bind("<KeyPress>",curs) + # self.bind("<KeyRelease-Control_L>",lambda ev: curs(0)) + self.tag_bind(line,"<Control-ButtonPress-1>",self.newpointatmouse) + + + def _draw_handle_points(self,visible_idxs,visible_points): + for i,p in zip(visible_idxs,visible_points): + rad=3 + worldp = p + p = self.screen_from_world(p) + dot = self.create_rectangle(p[0]-rad,p[1]-rad,p[0]+rad,p[1]+rad, + outline='black',fill='blue', + tags=('curve','point', 'handle%d' % i)) + if worldp[1] == 0: + rad += 3 + dot2 = self.create_oval(p[0]-rad,p[1]-rad, + p[0]+rad,p[1]+rad, + outline='darkgreen', + tags=('curve','point', 'handle%d' % i)) + self.tag_bind('handle%d' % i,"<ButtonPress-1>", + lambda ev,i=i: self.dotpress(ev,i)) + self.bind("<Motion>", + lambda ev,i=i: self.dotmotion(ev,i)) + self.bind("<ButtonRelease-1>", + lambda ev,i=i: self.dotrelease(ev,i)) + self.dots[i]=dot + + self.highlight_selected_dots() + + + def newpointatmouse(self, ev): + p = self.world_from_screen(ev.x,ev.y) + x, y = p + y = max(0, y) + y = min(1, y) + p = x, y + self.add_point(p) + + def add_point(self, p): + self.unselect() + self.curve.insert_pt(p) + self.update() + + def highlight_selected_dots(self): + for i,d in self.dots.items(): + if i in self.selected_points: + self.itemconfigure(d,fill='red') + else: + self.itemconfigure(d,fill='blue') + + def dotpress(self,ev,dotidx): + self.selected_points=[dotidx] + self.highlight_selected_dots() + + def dotmotion(self,ev,dotidx): + cp = self.curve.points + moved=0 + for idx in self.selected_points: + x,y = self.world_from_screen(ev.x,ev.y) + y = max(0,min(1,y)) + if idx>0 and x<=cp[idx-1][0]: + continue + if idx<len(cp)-1 and x>=cp[idx+1][0]: + continue + moved=1 + cp[idx] = (x,y) + if moved: + self.update() + def unselect(self): + self.selected_points=[] + self.highlight_selected_dots() + + def dotrelease(self,ev,dotidx): + self.unselect() + +class Curveset: + curves = None # curvename : curve + def __init__(self): + self.curves = {} + def load(self,basename): + """find all files that look like basename-curvename and add + curves with their contents""" + for filename in glob.glob("%s-*"%basename): + curvename = filename[filename.find('-')+1:] + c=Curve() + c.load(filename) + curvename = curvename.replace('-','_') + self.add_curve(curvename,c) + def save(self,basename): + """writes a file for each curve with a name + like basename-curvename""" + for name,cur in self.curves.items(): + cur.save("%s-%s" % (basename,name)) + def add_curve(self,name,curve): + self.curves[name] = curve + dispatcher.send("add_curve",sender=self,name=name) + def globalsdict(self): + return self.curves.copy() + def new_curve(self,name): + if name=="": + print "no name given" + return + while name in self.curves: + name=name+"-1" + + self.add_curve(name,Curve()) + + +class Curvesetview(tk.Frame): + curves = None # curvename : Curveview + def __init__(self,master,curveset,**kw): + self.curves = {} + self.curveset = curveset + tk.Frame.__init__(self,master,**kw) + + f = tk.Frame(self,relief='raised',bd=1) + f.pack(side='top',fill='x') + tk.Button(f,text="new curve named:", + command=lambda: self.curveset.new_curve(self.newcurvename.get())).pack(side='left') + self.newcurvename = tk.StringVar() + tk.Entry(f,textvariable=self.newcurvename).pack(side='left', + fill='x',exp=1) + + + dispatcher.connect(self.add_curve,"add_curve",sender=self.curveset) + def add_curve(self,name): + f = tk.Frame(self,relief='raised',bd=1) + f.pack(side='top',fill='both',exp=1) + tk.Label(f,text="curve %r"%name,width=15).pack(side='left') + cv = Curveview(f,self.curveset.curves[name]) + cv.pack(side='right',fill='both',exp=1) + self.curves[name] = cv + +class Music: + def __init__(self): + self.player=None # xmlrpc Proxy to player + self.recenttime=0 + + def current_time(self): + """return deferred which gets called with the current time""" + if self.player is None: + self.player = Proxy("http://miles:8040") +# d = self.player.callRemote("songlength") +# d.addCallback(lambda l: dispatcher.send("max time",maxtime=l)) +# d = self.player.callRemote("songname") +# d.addCallback(lambda n: dispatcher.send("songname",name=n)) + d = self.player.callRemote('gettime') + def sendtime(t): + dispatcher.send("input time",val=t) + return t # pass along to the real receiver + def error(e): + pass#self.player=None + d.addCallback(sendtime) + return d + +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['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 + glo['aft'] = lambda x: x < t + glo['smoove'] = lambda x: -2 * (x ** 3) + 3 * (x ** 2) + + 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 + + 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(master,textvariable=self.evar) + e.pack(side='left',fill='both',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(master) + 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): + return self.submaster * self.subexpr.eval(t) + +class Subtermview(tk.Frame): + def __init__(self,master,st,**kw): + self.subterm = st + tk.Frame.__init__(self,master,bd=1,relief='raised',**kw) + l = tk.Label(self,text="sub %r" % 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): + make_attributes_from_args('subterms') + def send_dmx(self,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 create_status_lines(master): + 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)), + ]: + l = tk.Label(master,anchor='w',justify='left') + l.pack(side='top',fill='x') + dispatcher.connect(lambda val,l=l,sn=signame,tf=textfilter: + l.config(text=sn+": "+tf(val)), + signame,weak=0) + +def savesubterms(filename,subterms): + s="" + for st in subterms: + s=s+"%s %s\n" % (st.submaster.name,st.subexpr.expr) + + file(filename,'w').write(s) + +class SubtermSetView(tk.Frame): + def __init__(self, master, *args, **kw): + tk.Frame.__init__(self, master, *args, **kw) + self.cur_row = 0 + self.cur_col = 0 + self.ncols = 2 + def add_subtermview(self, stv): + stv.grid(row=self.cur_row, column=self.cur_col, sticky='news') + self.columnconfigure(self.cur_col, weight=1) + + self.cur_col += 1 + self.cur_col %= self.ncols + if self.cur_col == 0: + self.cur_row += 1 + +def add_one_subterm(subname, curveset, subterms, root, ssv, expr=''): + term = Subterm(Submaster.Submaster(subname), Subexpr(curveset,expr)) + subterms.append(term) + + stv=Subtermview(ssv,term) + # stv.pack(side='top',fill='x') + + ssv.add_subtermview(stv) + + return term + +def subterm_adder(master, curveset, subterms, root, ssv): + f=tk.Frame(master,relief='raised',bd=1) + newname = tk.StringVar() + + def add_cmd(): + add_one_subterm(newname.get(), curveset, subterms, root, ssv, '') + + tk.Button(f,text="new subterm named:", command=add_cmd).pack(side='left') + tk.Entry(f,textvariable=newname).pack(side='left',fill='x',exp=1) + return f + +####################################################################### +root=tk.Tk() +root.tk_setPalette("gray50") +root.wm_geometry("1120x850") +root.tk_focusFollowsMouse() + +music=Music() + +zc = Zoomcontrol(root) +zc.pack(side='top',fill='x') + +curveset = Curveset() +csv = Curvesetview(root,curveset) +csv.pack(side='top',fill='both',exp=1) + +ssv = SubtermSetView(root) +ssv.pack(side='top', fill='x') + +song = sys.argv[1] +root.title("Curemaster 2000MX - %s" % song) + +musicfilename = os.path.join(os.getenv("LIGHT9_SHOW"),'music', + "%s.wav" % song) +maxtime = wavelength(musicfilename) +dispatcher.send("max time",maxtime=maxtime) +dispatcher.connect(lambda: maxtime, "get max time",weak=0) +curveset.load(basename=os.path.join(os.getenv("LIGHT9_SHOW"),"curves",song)) + +subterms = [] +subterm_adder(root, curveset, subterms, root, ssv).pack(side='top',fill='x') +for line in file(os.path.join(os.getenv("LIGHT9_SHOW"), + "subterms", + song)): + subname,expr = line.strip().split(" ",1) + + term = add_one_subterm(subname, curveset, subterms, root, ssv, expr) + + # stv=Subtermview(root,term) + # stv.pack(side='top',fill='x') + +out = Output(subterms) + +def savekey(*args): + print "saving",song + savesubterms(os.path.join(os.getenv("LIGHT9_SHOW"),"subterms",song), + subterms) + curveset.save(basename="curves/"+song) + print "saved" + + +root.bind("<Control-Key-s>",savekey) + +create_status_lines(root) + +recent_t=[] +later = None +def update(): + global later + d = music.current_time() + d.addCallback(update2) + d.addErrback(updateerr) +def updateerr(e): + global later + print "err",e + if later and not later.cancelled and not later.called: later.cancel() + later = reactor.callLater(1,update) +def update2(t): + global recent_t,later + + if later and not later.cancelled and not later.called: later.cancel() + later = reactor.callLater(.01,update) + + recent_t = recent_t[-50:]+[t] + period = (recent_t[-1]-recent_t[0])/len(recent_t) + dispatcher.send("update period",val=period) + out.send_dmx(t) +update() + +#def logprint(msg): +# print "log",msg +#twisted.python.log.addObserver(logprint) + +root.bind("<Control-Key-q>",lambda ev: reactor.stop) +root.bind("<Destroy>",lambda ev: reactor.stop) +root.protocol('WM_DELETE_WINDOW', reactor.stop) +tksupport.install(root,ms=10) +if 0: + sys.path.append("/home/drewp/projects/editor/pour") + from utils import runstats + runstats("reactor.run()") +else: + reactor.run()