0
|
1 #!/usr/bin/python
|
|
2
|
|
3 """
|
|
4 todo: curveview should preserve more objects, for speed maybe
|
|
5
|
|
6 """
|
|
7 from __future__ import division
|
|
8 import xmlrpclib,time,socket,sys,textwrap,math,glob
|
|
9 from bisect import bisect_left,bisect,bisect_right
|
|
10 import Tkinter as tk
|
|
11 from dispatch import dispatcher
|
|
12 from twisted.internet import reactor,tksupport
|
|
13 from twisted.web.xmlrpc import Proxy
|
|
14
|
|
15 sys.path.append("../light8")
|
|
16 import dmxclient
|
|
17 import Submaster
|
|
18 from TLUtility import make_attributes_from_args
|
|
19
|
|
20 from zoomcontrol import Zoomcontrol
|
|
21
|
|
22 class Curve:
|
|
23 """curve does not know its name. see Curveset"""
|
|
24 points = None # x-sorted list of (x,y)
|
|
25 def __init__(self):
|
|
26 self.points = []
|
|
27
|
|
28 def load(self,filename):
|
|
29 for line in file(filename):
|
|
30 self.points.append(tuple([float(a) for a in line.split()]))
|
|
31 self.points.sort()
|
|
32 dispatcher.send("points changed",sender=self)
|
|
33
|
|
34 def save(self,filename):
|
|
35 f = file(filename,'w')
|
|
36 for p in self.points:
|
|
37 f.write("%s %s\n" % p)
|
|
38 f.close()
|
|
39
|
|
40 def eval(self,t):
|
|
41 i = bisect_left(self.points,(t,None))-1
|
|
42
|
|
43 if self.points[i][0]>t:
|
|
44 return self.points[i][1]
|
|
45 if i>=len(self.points)-1:
|
|
46 return self.points[i][1]
|
|
47
|
|
48 p1,p2 = self.points[i],self.points[i+1]
|
|
49 frac = (t-p1[0])/(p2[0]-p1[0])
|
|
50 y = p1[1]+(p2[1]-p1[1])*frac
|
|
51 return y
|
|
52
|
|
53 __call__=eval
|
|
54
|
|
55 class Curveview(tk.Canvas):
|
|
56 def __init__(self,master,curve,**kw):
|
|
57 self.curve=curve
|
|
58 tk.Canvas.__init__(self,master,width=10,height=10,
|
|
59 relief='sunken',bd=1,
|
|
60 closeenough=5,**kw)
|
|
61 self.selected_points=[] # idx of points being dragged
|
|
62 self.update()
|
|
63 self.bind("<Enter>",self.focus)
|
|
64 dispatcher.connect(self.input_time,"input time")
|
|
65 dispatcher.connect(self.update,"zoom changed")
|
|
66 dispatcher.connect(self.update,"points changed",sender=self.curve)
|
|
67 self.bind("<Configure>",self.update)
|
|
68 def screen_from_world(self,p):
|
|
69 start,end = self.zoom
|
|
70 ht = self.winfo_height()
|
|
71 return (p[0]-start)/(end-start)*self.winfo_width(), (ht-5)-p[1]*(ht-10)
|
|
72 def world_from_screen(self,x,y):
|
|
73 start,end = self.zoom
|
|
74 ht = self.winfo_height()
|
|
75 return x/self.winfo_width()*(end-start)+start, ((ht-5)-y)/(ht-10)
|
|
76
|
|
77 def input_time(self,val):
|
|
78 t=val
|
|
79 pts = self.screen_from_world((val,0))+self.screen_from_world((val,1))
|
|
80 self.delete('timecursor')
|
|
81 self.create_line(*pts,**dict(width=2,fill='red',tags=('timecursor',)))
|
|
82 def update(self,*args):
|
|
83
|
|
84 self.zoom = dispatcher.send("zoom area")[0][1]
|
|
85 cp = self.curve.points
|
|
86
|
|
87 visible_x = (self.world_from_screen(0,0)[0],
|
|
88 self.world_from_screen(self.winfo_width(),0)[0])
|
|
89
|
|
90 visleftidx = max(0,bisect_left(cp,(visible_x[0],None))-1)
|
|
91 visrightidx = min(len(cp)-1,bisect_left(cp,(visible_x[1],None))+1)
|
|
92
|
|
93 visible_points = cp[visleftidx:visrightidx+1]
|
|
94
|
|
95 self.delete('curve')
|
|
96 linepts=[]
|
|
97 for p in visible_points:
|
|
98 linepts.extend(self.screen_from_world(p))
|
|
99 if not linepts:
|
|
100 return
|
|
101 line = self.create_line(*linepts,**{'tags':'curve'})
|
|
102
|
|
103 # canvas doesnt have keyboard focus, so i can't easily change the
|
|
104 # cursor when ctrl is pressed
|
|
105 # def curs(ev):
|
|
106 # print ev.state
|
|
107 # self.bind("<KeyPress>",curs)
|
|
108 # self.bind("<KeyRelease-Control_L>",lambda ev: curs(0))
|
|
109 self.tag_bind(line,"<Control-ButtonPress-1>",self.newpoint)
|
|
110
|
|
111 self.dots = {} # idx : canvas rectangle
|
|
112
|
|
113 if len(visible_points)<50:
|
|
114 for i,p in enumerate(visible_points):
|
|
115 rad=3
|
|
116 p = self.screen_from_world(p)
|
|
117 dot = self.create_rectangle(p[0]-rad,p[1]-rad,p[0]+rad,p[1]+rad,
|
|
118 outline='black',fill='blue',
|
|
119 tags=('curve','point'))
|
|
120 self.tag_bind(dot,"<ButtonPress-1>",
|
|
121 lambda ev,i=i: self.dotpress(ev,i))
|
|
122 self.bind("<Motion>",
|
|
123 lambda ev,i=i: self.dotmotion(ev,i))
|
|
124 self.bind("<ButtonRelease-1>",
|
|
125 lambda ev,i=i: self.dotrelease(ev,i))
|
|
126 self.dots[i]=dot
|
|
127
|
|
128 self.highlight_selected_dots()
|
|
129
|
|
130 def newpoint(self,ev):
|
|
131 cp = self.curve.points
|
|
132
|
|
133 p = self.world_from_screen(ev.x,ev.y)
|
|
134 i = bisect(cp,(p[0],None))
|
|
135
|
|
136 self.unselect()
|
|
137 cp.insert(i,p)
|
|
138 self.update()
|
|
139
|
|
140 def highlight_selected_dots(self):
|
|
141 for i,d in self.dots.items():
|
|
142 if i in self.selected_points:
|
|
143 self.itemconfigure(d,fill='red')
|
|
144 else:
|
|
145 self.itemconfigure(d,fill='blue')
|
|
146
|
|
147 def dotpress(self,ev,dotidx):
|
|
148 self.selected_points=[dotidx]
|
|
149 self.highlight_selected_dots()
|
|
150
|
|
151 def dotmotion(self,ev,dotidx):
|
|
152 cp = self.curve.points
|
|
153
|
|
154 moved=0
|
|
155 for idx in self.selected_points:
|
|
156 x,y = self.world_from_screen(ev.x,ev.y)
|
|
157 y = max(0,min(1,y))
|
|
158 if idx>0 and x<=cp[idx-1][0]:
|
|
159 continue
|
|
160 if idx<len(cp)-1 and x>=cp[idx+1][0]:
|
|
161 continue
|
|
162 moved=1
|
|
163 cp[idx] = (x,y)
|
|
164 if moved:
|
|
165 self.update()
|
|
166 def unselect(self):
|
|
167 self.selected_points=[]
|
|
168 self.highlight_selected_dots()
|
|
169
|
|
170 def dotrelease(self,ev,dotidx):
|
|
171 self.unselect()
|
|
172
|
|
173 class Curveset:
|
|
174 curves = None # curvename : curve
|
|
175 def __init__(self):
|
|
176 self.curves = {}
|
|
177 def load(self,basename):
|
|
178 """find all files that look like basename-curvename and add
|
|
179 curves with their contents"""
|
|
180 for filename in glob.glob("%s-*"%basename):
|
|
181 curvename = filename[filename.rfind('-')+1:]
|
|
182 c=Curve()
|
|
183 c.load(filename)
|
|
184 self.add_curve(curvename,c)
|
|
185 def save(self,basename):
|
|
186 """writes a file for each curve with a name
|
|
187 like basename-curvename"""
|
|
188 for name,cur in self.curves.items():
|
|
189 cur.save("%s-%s" % (basename,name))
|
|
190 def add_curve(self,name,curve):
|
|
191 self.curves[name] = curve
|
|
192 dispatcher.send("add_curve",sender=self,name=name)
|
|
193 def globalsdict(self):
|
|
194 return self.curves.copy()
|
|
195
|
|
196 class Curvesetview(tk.Frame):
|
|
197 curves = None # curvename : Curveview
|
|
198 def __init__(self,master,curveset,**kw):
|
|
199 self.curves = {}
|
|
200 self.curveset = curveset
|
|
201 tk.Frame.__init__(self,master,**kw)
|
|
202 dispatcher.connect(self.add_curve,"add_curve",sender=self.curveset)
|
|
203 def add_curve(self,name):
|
|
204 f = tk.Frame(self,relief='raised',bd=1)
|
|
205 f.pack(side='top',fill='both',exp=1)
|
|
206 tk.Label(f,text="curve %r"%name).pack(side='left')
|
|
207 cv = Curveview(f,self.curveset.curves[name])
|
|
208 cv.pack(side='right',fill='both',exp=1)
|
|
209 self.curves[name] = cv
|
|
210
|
|
211 class Music:
|
|
212 def __init__(self):
|
|
213 self.player=None # xmlrpc Proxy to player
|
|
214 self.recenttime=0
|
|
215
|
|
216 def current_time(self):
|
|
217 """return deferred which gets called with the current time"""
|
|
218 if self.player is None:
|
|
219 self.player = Proxy("http://spot:8040")
|
|
220 d = self.player.callRemote("songlength")
|
|
221 d.addCallback(lambda l: dispatcher.send("max time",maxtime=l))
|
|
222 d = self.player.callRemote("songname")
|
|
223 d.addCallback(lambda n: dispatcher.send("songname",name=n))
|
|
224 d = self.player.callRemote('gettime')
|
|
225 def sendtime(t):
|
|
226 dispatcher.send("input time",val=t)
|
|
227 return t # pass along to the real receiver
|
|
228 def error(e):
|
|
229 pass#self.player=None
|
|
230 d.addCallback(sendtime)
|
|
231 return d
|
|
232
|
|
233 class Subexpr:
|
|
234 curveset = None
|
|
235 def __init__(self,curveset):
|
|
236 self.curveset = curveset
|
|
237 self.lasteval = None
|
|
238 self.expr=""
|
|
239 def eval(self,t):
|
|
240 if self.expr=="":
|
|
241 dispatcher.send("expr_error",sender=self,exc="no expr, using 0")
|
|
242 return 0
|
|
243 glo = self.curveset.globalsdict()
|
|
244 glo['t'] = t
|
|
245 try:
|
|
246 self.lasteval = eval(self.expr,glo)
|
|
247 except Exception,e:
|
|
248 dispatcher.send("expr_error",sender=self,exc=e)
|
|
249 else:
|
|
250 dispatcher.send("expr_error",sender=self,exc="no errors")
|
|
251 return self.lasteval
|
|
252
|
|
253 def expr():
|
|
254 doc = "python expression for level as a function of t, using curves"
|
|
255 def fget(self):
|
|
256 return self._expr
|
|
257 def fset(self, value):
|
|
258 self._expr = value
|
|
259 dispatcher("expr_changed",sender=self)
|
|
260 return locals()
|
|
261 expr = property(**expr())
|
|
262
|
|
263 class Subexprview(tk.Frame):
|
|
264 def __init__(self,master,se,**kw):
|
|
265 self.subexpr=se
|
|
266 tk.Frame.__init__(self,master,**kw)
|
|
267 self.evar = tk.StringVar()
|
|
268 e = self.ent = tk.Entry(master,textvariable=self.evar)
|
|
269 e.pack(side='left',fill='both',exp=1)
|
|
270 self.expr_changed()
|
|
271 self.evar.trace_variable('w',self.evar_changed)
|
|
272 dispatcher.connect(self.expr_changed,"expr_changed",
|
|
273 sender=self.subexpr)
|
|
274 self.error = tk.Label(master)
|
|
275 self.error.pack(side='left')
|
|
276 dispatcher.connect(lambda exc: self.error.config(text=str(exc)),
|
|
277 "expr_error",sender=self.subexpr,weak=0)
|
|
278 def expr_changed(self):
|
|
279 if self.subexpr.expr!=self.evar.get():
|
|
280 self.evar.set(self.subexpr.expr)
|
|
281 def evar_changed(self,*args):
|
|
282 self.subexpr.expr = self.evar.get()
|
|
283
|
|
284 class Subterm:
|
|
285 """one Submaster and its Subexpr"""
|
|
286 def scaled(self,t):
|
|
287 return self.sub * self.subexpr.eval(t)
|
|
288
|
|
289 class Subtermview(tk.Frame):
|
|
290 def __init__(self,master,st,**kw):
|
|
291 self.subterm = st
|
|
292 tk.Frame.__init__(self,master,bd=1,relief='raised',**kw)
|
|
293 tk.Label(self,text="sub %r" % self.subterm.sub.name).pack(side='left')
|
|
294 sev=Subexprview(self,self.subterm.subexpr)
|
|
295 sev.pack(side='left',fill='both',exp=1)
|
|
296
|
|
297 class Output:
|
|
298 def __init__(self,subterms):
|
|
299 make_attributes_from_args('subterms')
|
|
300 def send_dmx(self,t):
|
|
301
|
|
302 scaledsubs=[]
|
|
303 for st in self.subterms:
|
|
304 scl = st.scaled(t)
|
|
305 scaledsubs.append(scl)
|
|
306 out = Submaster.sub_maxes(*scaledsubs)
|
|
307 dispatcher.send("output levels",val=out.get_levels())
|
|
308 dmxclient.outputlevels(out.get_dmx_list(),twisted=1)
|
|
309
|
|
310 def create_status_lines(master):
|
|
311 for signame,textfilter in [
|
|
312 ('input time',lambda t: "%.2fs"%t),
|
|
313 ('output levels',
|
|
314 lambda levels: textwrap.fill("; ".join(["%s:%.2f"%(n,v)
|
|
315 for n,v in
|
|
316 levels.items()]),70)),
|
|
317 ('update period',lambda t: "%.1fms"%(t*1000)),
|
|
318 ]:
|
|
319 l = tk.Label(master,anchor='w',justify='left')
|
|
320 l.pack(side='top',fill='x')
|
|
321 dispatcher.connect(lambda val,l=l,sn=signame,tf=textfilter:
|
|
322 l.config(text=sn+": "+tf(val)),
|
|
323 signame,weak=0)
|
|
324
|
|
325 def savesubterms(filename,subterms):
|
|
326 f = file(filename,'w')
|
|
327 for st in subterms:
|
|
328 f.write("%s %s\n" % (st.sub.name,st.subexpr.expr))
|
|
329 f.close()
|
|
330
|
|
331 def save(song,subterms,curveset):
|
|
332 savesubterms("subterms/"+song,subterms)
|
|
333 curveset.save(basename="curves/"+song)
|
|
334
|
|
335 #######################################################################
|
|
336 root=tk.Tk()
|
|
337 root.wm_geometry("790x930")
|
|
338 #root.tk_focusFollowsMouse()
|
|
339
|
|
340 music=Music()
|
|
341
|
|
342 zc = Zoomcontrol(root)
|
|
343 zc.pack(side='top',fill='x')
|
|
344
|
|
345 curveset = Curveset()
|
|
346 csv = Curvesetview(root,curveset)
|
|
347 csv.pack(side='top',fill='both',exp=1)
|
|
348
|
|
349 song = "16mix.wav"
|
|
350
|
|
351 curveset.load(basename="curves/"+song)
|
|
352
|
|
353 subterms = []
|
|
354 for line in file("subterms/"+song):
|
|
355 subname,expr = line.strip().split(" ",1)
|
|
356
|
|
357 term = Subterm()
|
|
358
|
|
359 sexpr = Subexpr(curveset)
|
|
360 sexpr.expr = expr
|
|
361
|
|
362 term.sub = Submaster.Submaster(subname)
|
|
363 term.subexpr = sexpr
|
|
364 subterms.append(term)
|
|
365
|
|
366 stv=Subtermview(root,term)
|
|
367 stv.pack(side='top',fill='x')
|
|
368
|
|
369 out = Output(subterms)
|
|
370
|
|
371 #save(song,subterms,curveset)
|
|
372
|
|
373 create_status_lines(root)
|
|
374
|
|
375 recent_t=[]
|
|
376 later = None
|
|
377 def update():
|
|
378 global later
|
|
379 d = music.current_time()
|
|
380 d.addCallback(update2)
|
|
381 d.addErrback(updateerr)
|
|
382 def updateerr(e):
|
|
383 global later
|
|
384 print "err",e
|
|
385 if later and not later.cancelled and not later.called: later.cancel()
|
|
386 later = reactor.callLater(1,update)
|
|
387 def update2(t):
|
|
388 global recent_t,later
|
|
389
|
|
390 if later and not later.cancelled and not later.called: later.cancel()
|
|
391 later = reactor.callLater(.01,update)
|
|
392
|
|
393 recent_t = recent_t[-50:]+[t]
|
|
394 period = (recent_t[-1]-recent_t[0])/len(recent_t)
|
|
395 dispatcher.send("update period",val=period)
|
|
396 out.send_dmx(t)
|
|
397 update()
|
|
398
|
|
399 tksupport.install(root,ms=10)
|
|
400 if 0:
|
|
401 sys.path.append("/home/drewp/projects/editor/pour")
|
|
402 from utils import runstats
|
|
403 runstats("reactor.run()")
|
|
404 else:
|
|
405 reactor.run()
|