view flax/curvecalc @ 15:c76b62eccdec

put **kw back in, they now go to the scale and can override the defaults. put **kw back in, they now go to the scale and can override the defaults. for our purposes, we probably want to edit the defaults so we don't have them in every call.
author dmcc
date Sun, 07 Jul 2002 06:18:40 +0000
parents 45b12307c695
children 07bac5061d69
line wrap: on
line source

#!/usr/bin/python

"""
todo: curveview should preserve more objects, for speed maybe

"""
from __future__ import division
import xmlrpclib,time,socket,sys,textwrap,math,glob
from bisect import bisect_left,bisect,bisect_right
import Tkinter as tk
from dispatch import dispatcher
from twisted.internet import reactor,tksupport
from twisted.web.xmlrpc import Proxy

sys.path.append("../light8")
import dmxclient
import Submaster
from TLUtility import make_attributes_from_args

from zoomcontrol import Zoomcontrol

class Curve:
    """curve does not know its name. see Curveset"""
    points = None # x-sorted list of (x,y)
    def __init__(self):
        self.points = []

    def load(self,filename):
        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):
        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

    __call__=eval

class Curveview(tk.Canvas):
    def __init__(self,master,curve,**kw):
        self.curve=curve
        tk.Canvas.__init__(self,master,width=10,height=10,
                           relief='sunken',bd=1,
                           closeenough=5,**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)
    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',)))
    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]
        
        self.delete('curve')
        linepts=[]
        for p in visible_points:
            linepts.extend(self.screen_from_world(p))
        if not linepts:
            return
        line = self.create_line(*linepts,**{'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.newpoint)

        self.dots = {} # idx : canvas rectangle

        if len(visible_points)<50: 
            for i,p in enumerate(visible_points):
                rad=3
                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'))
                self.tag_bind(dot,"<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 newpoint(self,ev):
        cp = self.curve.points
        
        p = self.world_from_screen(ev.x,ev.y)
        i = bisect(cp,(p[0],None))

        self.unselect()
        cp.insert(i,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.rfind('-')+1:]
            c=Curve()
            c.load(filename)
            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()

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)
        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).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://spot: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):
        self.curveset = curveset
        self.lasteval = None
        self.expr=""
    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
        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="no errors")
        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 scaled(self,t):
        return self.sub * 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)
        tk.Label(self,text="sub %r" % self.subterm.sub.name).pack(side='left')
        sev=Subexprview(self,self.subterm.subexpr)
        sev.pack(side='left',fill='both',exp=1)

class Output:
    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)
        dispatcher.send("output levels",val=out.get_levels())
        dmxclient.outputlevels(out.get_dmx_list(),twisted=1)

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()]),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):
    f = file(filename,'w')
    for st in subterms:
        f.write("%s %s\n" % (st.sub.name,st.subexpr.expr))
    f.close()

def save(song,subterms,curveset):
    savesubterms("subterms/"+song,subterms)
    curveset.save(basename="curves/"+song)

#######################################################################
root=tk.Tk()
root.wm_geometry("790x930")
#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)

song = "16mix.wav"

curveset.load(basename="curves/"+song)

subterms = []
for line in file("subterms/"+song):
    subname,expr = line.strip().split(" ",1)

    term = Subterm()

    sexpr = Subexpr(curveset)
    sexpr.expr = expr
    
    term.sub = Submaster.Submaster(subname)
    term.subexpr = sexpr
    subterms.append(term)
    
    stv=Subtermview(root,term)
    stv.pack(side='top',fill='x')

out = Output(subterms)

#save(song,subterms,curveset)

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()

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()