# HG changeset patch # User Drew Perttula # Date 2007-06-15 06:04:55 # Node ID fc87327e29c4a1740d16a0f3c4ef22c53da98dae # Parent ff914126f3eac05ad92cbc5773cd13dc2fad02c7 CC now attaches to hardware sliders and knobs. tres cool. KC gets a --sliders option to enable the sliders diff --git a/bcf2000.py b/bcf2000.py --- a/bcf2000.py +++ b/bcf2000.py @@ -99,6 +99,7 @@ class BCF2000(object): assert len(which) == 1, "unknown control name %r" % name if isinstance(value, bool): value = value * 127 + #print "bcf: write %s %s" % (name, value) self.dev.write(chr(0xb0) + chr(which[0]) + chr(int(value))) diff --git a/bin/curvecalc b/bin/curvecalc --- a/bin/curvecalc +++ b/bin/curvecalc @@ -228,6 +228,7 @@ class Output: 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) @@ -355,7 +356,11 @@ toplevelat("curvecalc",root) root.tk_focusFollowsMouse() parser = optparse.OptionParser() -options,args = parser.parse_args() +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() try: song = URIRef(args[0]) @@ -368,8 +373,8 @@ music=Music() zc = Zoomcontrol(root) zc.pack(side='top',fill='x') -curveset = Curveset() -csv = Curvesetview(root,curveset) +curveset = Curveset(sliders=opts.sliders) +csv = Curvesetview(root, curveset) csv.pack(side='top',fill='both',exp=1) ssv = SubtermSetView(root) @@ -387,7 +392,8 @@ maxtime = wavelength(musicfilename) dispatcher.send("max time",maxtime=maxtime) dispatcher.connect(lambda: maxtime, "get max time",weak=0) curveset.load(basename=os.path.join(showconfig.curvesDir(), - showconfig.songFilenameFromURI(song))) + showconfig.songFilenameFromURI(song)), + skipMusic=opts.skip_music) subterms = [] sub_commands_tk(root, curveset, subterms, root, ssv, graph).pack(side='top',fill='x') diff --git a/bin/keyboardcomposer b/bin/keyboardcomposer --- a/bin/keyboardcomposer +++ b/bin/keyboardcomposer @@ -62,7 +62,8 @@ class SubmasterTk(Frame): subprocess.Popen(["bin/subcomposer", self.name]) class KeyboardComposer(Frame, SubClient): - def __init__(self, root, submasters, current_sub_levels=None): + def __init__(self, root, submasters, current_sub_levels=None, + hw_sliders=False): Frame.__init__(self, root, bg='black') SubClient.__init__(self) self.submasters = submasters @@ -77,8 +78,10 @@ class KeyboardComposer(Frame, SubClient) except IOError: pass + self.connect_to_hw(hw_sliders) self.draw_ui() self.send_levels_loop() + def draw_ui(self): self.rows = [] # this holds Tk Frames for each row self.slider_vars = {} # this holds subname:sub Tk vars @@ -86,7 +89,6 @@ class KeyboardComposer(Frame, SubClient) self.name_to_subtk.clear() # subname : SubmasterTk instance self.current_row = 0 - self.connect_to_hw() self.make_key_hints() self.draw_sliders() self.highlight_row(self.current_row) @@ -108,15 +110,14 @@ class KeyboardComposer(Frame, SubClient) self.sub_name.pack(side=LEFT) self.stop_frequent_update_time = 0 - def connect_to_hw(self): - try: + def connect_to_hw(self, hw_sliders): + if hw_sliders: self.sliders = Sliders(self.hw_slider_moved) - except IOError: + else: class dummy: def valueOut(self, name, value): pass self.sliders = dummy() - print "no hw sliders found" def make_key_hints(self): keyhintrow = Frame(self) @@ -325,6 +326,8 @@ if __name__ == "__main__": parser = OptionParser() parser.add_option('--nonpersistent', action="store_true", help="don't load or save levels") + parser.add_option('--sliders', action='store_true', + help="attach to hardware sliders") opts, args = parser.parse_args() s = Submasters() @@ -335,7 +338,7 @@ if __name__ == "__main__": startLevels = None if opts.nonpersistent: startLevels = {} - kc = KeyboardComposer(tl, s, startLevels) + kc = KeyboardComposer(tl, s, startLevels, hw_sliders=opts.sliders) kc.pack(fill=BOTH, expand=1) for helpline in ["Bindings: B3 mute; C-l edit levels in subcomposer"]: diff --git a/light9/curve.py b/light9/curve.py --- a/light9/curve.py +++ b/light9/curve.py @@ -1,5 +1,5 @@ from __future__ import division -import sys,math,glob,random,os +import sys,math,glob,random,os, time from bisect import bisect_left,bisect,bisect_right import Tkinter as tk try: @@ -12,6 +12,7 @@ from light9 import Submaster, dmxclient, from light9.TLUtility import make_attributes_from_args from light9.dmxchanedit import gradient from light9.zoomcontrol import RegionZoom +from bcf2000 import BCF2000 class Curve: """curve does not know its name. see Curveset""" @@ -62,6 +63,20 @@ class Curve: def points_between(self, x1, x2): return [self.points[i] for i in self.indices_between(x1,x2)] + def point_before(self, x): + """(x,y) of the point left of x, or None""" + leftidx = self.index_before(x) + if leftidx is None: + return None + return self.points[leftidx] + + def index_before(self, x): + leftidx = bisect(self.points, (x,None)) - 1 + if leftidx < 0: + return None + return leftidx + + def vlen(v): return math.sqrt(v[0]*v[0] + v[1]*v[1]) @@ -127,8 +142,11 @@ class Sketch: class Curveview(tk.Canvas): - def __init__(self,master,curve,**kw): + def __init__(self, master, curve, knobEnabled=False, **kw): + """knobEnabled=True highlights the previous key and ties it to a + hardware knob""" self.curve=curve + self.knobEnabled = knobEnabled self._time = 0 self.last_mouse_world = None tk.Canvas.__init__(self,master,width=10,height=10, @@ -139,8 +157,12 @@ class Curveview(tk.Canvas): # self.bind("",self.focus) dispatcher.connect(self.input_time,"input time") dispatcher.connect(self.update_curve,"zoom changed") - dispatcher.connect(self.update_curve,"points changed",sender=self.curve) + dispatcher.connect(self.update_curve,"points changed", + sender=self.curve) dispatcher.connect(self.select_between,"select between") + if self.knobEnabled: + dispatcher.connect(self.knob_in, "knob in") + dispatcher.connect(self.slider_in, "slider in") self.bind("",self.update_curve) for x in range(1, 6): def add_kb_marker_point(evt, x=x): @@ -191,6 +213,25 @@ class Curveview(tk.Canvas): self.bind("", self.check_deselect, add=True) + def knob_in(self, curve, value): + """user turned a hardware knob, which edits the point to the + left of the current time""" + if curve != self.curve: + return + idx = self.curve.index_before(self.current_time()) + if idx is not None: + pos = self.curve.points[idx] + self.curve.points[idx] = (pos[0], value) + self.update_curve() + + def slider_in(self, curve, value): + """user pushed on a slider. make a new key""" + if curve != self.curve: + return + + self.curve.insert_pt((self.current_time(), value)) + self.update_curve() + def print_state(self, msg=""): if 0: print "%s: dragging_dots=%s selecting=%s" % ( @@ -257,12 +298,29 @@ class Curveview(tk.Canvas): ht = self.winfo_height() return x/self.winfo_width()*(end-start)+start, ((ht-5)-y)/(ht-10) - def input_time(self,val): + def input_time(self, val, forceUpdate=False): + # i tried various things to make this not update like crazy, + # but the timeline was always missing at startup, so i got + # scared that things were getting built in a funny order. + #if self._time == val: + # return + 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.have_time_line = True self._time = t + if self.knobEnabled: + self.delete('knob') + prevKey = self.curve.point_before(t) + if prevKey is not None: + pos = self.screen_from_world(prevKey) + self.create_oval(pos[0] - 8, pos[1] - 8, + pos[0] + 8, pos[1] + 8, + outline='#800000', + tags=('knob',)) + dispatcher.send("knob out", value=prevKey[1], curve=self.curve) def update_curve(self,*args): @@ -485,16 +543,45 @@ class Curveview(tk.Canvas): if not self.dragging_dots: return self.last_mouse_world = None + self.dragging_dots = False + +class Sliders(BCF2000): + def __init__(self, cb, knobCallback): + BCF2000.__init__(self) + self.cb = cb + self.knobCallback = knobCallback + def valueIn(self, name, value): + if name.startswith("slider"): + self.cb(int(name[6:]), value / 127) + if name.startswith("knob"): + self.knobCallback(int(name[4:]), value / 127) + class Curveset: curves = None # curvename : curve - def __init__(self): - self.curves = {} - def load(self,basename): + def __init__(self, sliders=False): + """sliders=True means support the hardware sliders""" + self.curves = {} # name : Curve + self.curveName = {} # reverse + self.sliderCurve = {} # slider number (1 based) : curve name + self.sliderNum = {} # reverse + if sliders: + self.sliders = Sliders(self.hw_slider_in, self.hw_knob_in) + dispatcher.connect(self.curvesToSliders, "curves to sliders") + dispatcher.connect(self.knobOut, "knob out") + self.lastSliderTime = {} # num : time + self.sliderSuppressOutputUntil = {} # num : time + self.sliderIgnoreInputUntil = {} + else: + self.sliders = None + + def load(self,basename, skipMusic=False): """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:] + if skipMusic and curvename in ['music', 'smooth_music']: + continue c=Curve() c.load(filename) curvename = curvename.replace('-','_') @@ -504,9 +591,20 @@ class Curveset: 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) + self.curveName[curve] = name + + if self.sliders and name not in ['smooth_music', 'music']: + num = len(self.sliderCurve) + 1 + self.sliderCurve[num] = name + self.sliderNum[name] = num + else: + num = None + + dispatcher.send("add_curve", slider=num, knobEnabled=num is not None, + sender=self,name=name) def globalsdict(self): return self.curves.copy() @@ -526,10 +624,53 @@ class Curveset: c.points.extend([(s,0), (e,0)]) self.add_curve(name,c) + def hw_slider_in(self, num, value): + try: + curve = self.curves[self.sliderCurve[num]] + except KeyError: + return + + now = time.time() + if now < self.sliderIgnoreInputUntil.get(num): + return + # don't make points too fast. This is the minimum spacing + # between slider-generated points. + self.sliderIgnoreInputUntil[num] = now + .1 + + # don't push back on the slider for a little while, since the + # user might be trying to slowly move it. This should be + # bigger than the ignore time above. + self.sliderSuppressOutputUntil[num] = now + .2 + + dispatcher.send("slider in", curve=curve, value=value) + + def hw_knob_in(self, num, value): + try: + curve = self.curves[self.sliderCurve[num]] + except KeyError: + return + dispatcher.send("knob in", curve=curve, value=value) + + def curvesToSliders(self, t): + now = time.time() + for num, name in self.sliderCurve.items(): + if now < self.sliderSuppressOutputUntil.get(num): + continue +# self.lastSliderTime[num] = now + + value = self.curves[name].eval(t) + self.sliders.valueOut("slider%s" % num, value * 127) + + def knobOut(self, curve, value): + try: + num = self.sliderNum[self.curveName[curve]] + except KeyError: + return + self.sliders.valueOut("knob%s" % num, value * 127) class Curvesetview(tk.Frame): curves = None # curvename : Curveview - def __init__(self,master,curveset,**kw): + def __init__(self, master, curveset, **kw): self.curves = {} self.curveset = curveset tk.Frame.__init__(self,master,**kw) @@ -550,7 +691,7 @@ class Curvesetview(tk.Frame): dispatcher.connect(self.add_curve,"add_curve",sender=self.curveset) - def add_curve(self,name): + def add_curve(self,name, slider=None, knobEnabled=False): f = tk.Frame(self,relief='raised',bd=1) f.pack(side='top',fill='both',exp=1) @@ -559,20 +700,33 @@ class Curvesetview(tk.Frame): leftside.pack(side='left') collapsed = tk.IntVar() - txt = "curve %r" % name + txt = "curve '%s'" % name if len(name) > 7: - txt = repr(name) + txt = name tk.Label(leftside,text=txt,font="6x10", width=15).pack(side='top') - + + sliderLabel = None def cmd(): if collapsed.get(): + if sliderLabel: + sliderLabel.pack_forget() f.pack(exp=0) else: + if sliderLabel: + sliderLabel.pack(side='top') f.pack(exp=1) tk.Checkbutton(leftside, text="collapsed", font="6x10", variable=collapsed, command=cmd).pack(side='top') - cv = Curveview(f,self.curveset.curves[name]) + if slider is not None: + # slider should have a checkbutton, defaults to off for + # music tracks + sliderLabel = tk.Label(leftside, text="Slider %s" % slider, + fg='#800000', font='arial 12 bold') + sliderLabel.pack(side='top') + + cv = Curveview(f, self.curveset.curves[name], + knobEnabled=knobEnabled) cv.pack(side='left',fill='both',exp=1) self.curves[name] = cv